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}
-
-
-
+
+
+ setCircuitGroupEditorIntent({ kind: "create" })}
+ showLabel
+ />
+
setComponentEditorIntent({
kind: "create",
role: "auxiliary",
})
}
- >
- Verteilergerät hinzufügen
-
-
-
+ 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" ? (
<>
-
-
+ void handleDeleteCircuit(row.circuit!.id)}
- >
- Stromkreis löschen
-
+ 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 ;
+}