From 5150d4d1662232803ee4da9c43c0851637e5e9d8 Mon Sep 17 00:00:00 2001 From: Grovy311 Date: Sat, 26 Sep 2026 10:12:03 +0200 Subject: [PATCH 01/56] Use icon buttons in the circuit list editor - New IconButton/IconButtonSeparator component based on lucide-react. - Toolbar: icon plus text, grouped into history, view and insert blocks. - Group headers: compact icons for add circuit, upstream fuse (VS), group RCD (FI), edit, move up/down, renumber and delete; the header no longer overflows the grid. - Rows: add device, delete circuit (trash) and delete device (ListX). - The previous German labels remain accessible names and tooltips; destructive actions highlight in the danger color. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 7 + src/app/globals.css | 65 ++- .../components/circuit-tree-editor.tsx | 448 ++++++++++-------- src/frontend/components/icon-button.tsx | 59 +++ 4 files changed, 374 insertions(+), 205 deletions(-) create mode 100644 src/frontend/components/icon-button.tsx diff --git a/AGENTS.md b/AGENTS.md index f1a509d..0ec3242 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -167,6 +167,13 @@ Keyboard behavior: Support Ctrl+Plus and Ctrl+Shift+Plus for insertion. +Editor actions use `IconButton` (`src/frontend/components/icon-button.tsx`, +`lucide-react` icons). The German action text is always the accessible name and +tooltip; the toolbar shows icon plus text, group headers and rows show icons +only, separated into logical blocks with `IconButtonSeparator`. Destructive +actions use `variant="danger"` (trash icon for circuits and groups, `ListX` for +removing a device row). + ## Bootstrap / Styling Rule Bootstrap may be used for the general application UI, such as navigation, page layout, buttons, forms, cards, alerts and modals. diff --git a/src/app/globals.css b/src/app/globals.css index 22da5df..ec92d91 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -487,6 +487,54 @@ a.kpi:hover { opacity: 0.45; } +/* Icon buttons (circuit list editor toolbar, group and row actions) */ +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + line-height: 1.2; + cursor: pointer; + transition: color 0.12s ease, border-color 0.12s ease, background-color 0.12s ease; +} + +.icon-button svg { + flex: none; +} + +.icon-button:disabled { + cursor: not-allowed; +} + +.icon-button:not(:disabled):hover { + border-color: var(--accent-blue); + color: var(--accent-blue); +} + +.icon-button-danger:not(:disabled):hover { + border-color: var(--color-danger); + background: var(--color-danger-bg); + color: var(--color-danger); +} + +.editor-toolbar .project-device-drawer-toggle:not(:disabled):hover { + color: #fff; + filter: brightness(1.08); +} + +.icon-button-text { + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.02em; +} + +.icon-button-separator { + align-self: stretch; + width: 1px; + margin: 0.1rem 0.15rem; + background: var(--panel-border-strong); +} + .editor-toolbar .project-device-drawer-toggle { border-color: var(--accent-blue); background: var(--accent-blue); @@ -1153,7 +1201,20 @@ a.kpi:hover { .tree-grid .section-actions { display: flex; - gap: 0.35rem; + align-items: center; + gap: 0.25rem; +} + +.tree-grid .section-actions .icon-button, +.tree-grid .action-cell .icon-button { + min-width: 1.75rem; + min-height: 1.6rem; + padding: 0.15rem 0.3rem; +} + +.tree-grid .section-actions .icon-button:disabled, +.tree-grid .action-cell .icon-button:disabled { + opacity: 0.35; } .tree-grid .section-actions button { @@ -1265,7 +1326,7 @@ a.kpi:hover { .tree-grid .action-cell { display: flex; - gap: 0.3rem; + gap: 0.25rem; } .tree-grid .action-cell button { diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index 03aa38d..cc3d522 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -1,6 +1,28 @@ "use client"; import { DragEvent, KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"; +import { + ArrowDown, + ArrowDown01, + ArrowUp, + Check, + Columns3, + FilterX, + FolderPlus, + ListOrdered, + ListPlus, + ListX, + PackagePlus, + PanelRightClose, + PanelRightOpen, + Pencil, + Plus, + Redo2, + ShieldPlus, + Trash2, + Undo2, +} from "lucide-react"; +import { IconButton, IconButtonSeparator } from "./icon-button"; import { inferProjectDeviceSectionKey, isProjectDevicePlacementValid, @@ -2995,32 +3017,37 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str return (
- - - - - {renderColumnSettingsMenu("col-empty")} -
+ icon={Redo2} + label="Wiederholen" + onClick={() => void handleRedo()} + showLabel + /> + + + + {renderColumnSettingsMenu("col-empty")} +
{renderActiveViewSummary()} {renderDistributionPowerSummary()} {error ? ( @@ -3140,82 +3167,90 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str /> ) : null}
- - + /> + {isSortedView ? ( - + /> ) : null} - - - - - + showLabel + /> + + setIsProjectDeviceDrawerOpen((current) => !current) + } + showLabel + /> {renderColumnSettingsMenu("col")}
{renderActiveViewSummary()} @@ -3717,105 +3752,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
- - - - - - - - - + tabIndex={-1} + title={ + hasActiveSortOrFilter + ? "Vor der Neunummerierung Sortierung und Filter zurücksetzen." + : "Stromkreise dieser Gruppe neu nummerieren" + } + /> + + section.category + ? void handleRenumberCircuitGroups( + section.category + ) + : undefined + } + tabIndex={-1} + title={ + hasActiveSortOrFilter + ? "Vor der Gruppenneunummerierung Sortierung und Filter zurücksetzen." + : "Gruppen-BMK: alle Gruppen dieser Kategorie gemäß ihrer Reihenfolge neu nummerieren" + } + /> + + + void handleDeleteCircuitGroup(section) + } + tabIndex={-1} + title={ + canDeleteCircuitGroup(section) + ? "Leere Stromkreisgruppe entfernen" + : "Befüllte Stromkreisgruppe entfernen (mit Warnung)" + } + variant="danger" + />
{dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? ( @@ -4467,28 +4505,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str > {row.circuit && row.rowType !== "deviceRow" ? ( <> - - + tabIndex={-1} + variant="danger" + /> ) : null} {row.device ? ( - + void handleDeleteDevice(row.device!.id)} + tabIndex={-1} + variant="danger" + /> ) : null} {dropIntent?.kind === "new-circuit" && row.rowType === "placeholder" && dropIntent.sectionId === row.sectionId ? ( diff --git a/src/frontend/components/icon-button.tsx b/src/frontend/components/icon-button.tsx new file mode 100644 index 0000000..605036d --- /dev/null +++ b/src/frontend/components/icon-button.tsx @@ -0,0 +1,59 @@ +"use client"; + +import type { LucideIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +/** + * Compact editor button with a lucide icon. `label` is always the accessible + * name; it is also the tooltip unless a more specific `title` is given. + * With `showLabel` the label (or `children`) is rendered next to the icon. + */ +export function IconButton({ + icon: Icon, + label, + title, + onClick, + disabled, + variant = "default", + showLabel = false, + children, + className, + tabIndex, + ariaExpanded, + ariaControls, +}: { + icon: LucideIcon; + label: string; + title?: string; + onClick: () => void; + disabled?: boolean; + variant?: "default" | "danger" | "primary"; + showLabel?: boolean; + children?: ReactNode; + className?: string; + tabIndex?: number; + ariaExpanded?: boolean; + ariaControls?: string; +}) { + return ( + + ); +} + +/** Thin vertical divider between groups of icon buttons. */ +export function IconButtonSeparator() { + return
- - - - - - - - - + tabIndex={-1} + title={ + hasActiveSortOrFilter + ? "Vor der Neunummerierung Sortierung und Filter zurücksetzen." + : "Stromkreise dieser Gruppe neu nummerieren" + } + /> + + section.category + ? void handleRenumberCircuitGroups( + section.category + ) + : undefined + } + tabIndex={-1} + title={ + hasActiveSortOrFilter + ? "Vor der Gruppenneunummerierung Sortierung und Filter zurücksetzen." + : "Gruppen-BMK: alle Gruppen dieser Kategorie gemäß ihrer Reihenfolge neu nummerieren" + } + /> + + + void handleDeleteCircuitGroup(section) + } + tabIndex={-1} + title={ + canDeleteCircuitGroup(section) + ? "Leere Stromkreisgruppe entfernen" + : "Befüllte Stromkreisgruppe entfernen (mit Warnung)" + } + variant="danger" + />
{dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? ( @@ -4467,28 +4505,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str > {row.circuit && row.rowType !== "deviceRow" ? ( <> - - + tabIndex={-1} + variant="danger" + /> ) : null} {row.device ? ( - + void handleDeleteDevice(row.device!.id)} + tabIndex={-1} + variant="danger" + /> ) : null} {dropIntent?.kind === "new-circuit" && row.rowType === "placeholder" && dropIntent.sectionId === row.sectionId ? ( diff --git a/src/frontend/components/icon-button.tsx b/src/frontend/components/icon-button.tsx new file mode 100644 index 0000000..605036d --- /dev/null +++ b/src/frontend/components/icon-button.tsx @@ -0,0 +1,59 @@ +"use client"; + +import type { LucideIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +/** + * Compact editor button with a lucide icon. `label` is always the accessible + * name; it is also the tooltip unless a more specific `title` is given. + * With `showLabel` the label (or `children`) is rendered next to the icon. + */ +export function IconButton({ + icon: Icon, + label, + title, + onClick, + disabled, + variant = "default", + showLabel = false, + children, + className, + tabIndex, + ariaExpanded, + ariaControls, +}: { + icon: LucideIcon; + label: string; + title?: string; + onClick: () => void; + disabled?: boolean; + variant?: "default" | "danger" | "primary"; + showLabel?: boolean; + children?: ReactNode; + className?: string; + tabIndex?: number; + ariaExpanded?: boolean; + ariaControls?: string; +}) { + return ( + + ); +} + +/** Thin vertical divider between groups of icon buttons. */ +export function IconButtonSeparator() { + return