Compact circuit list column headers with filter icons

- Replace the "Filtern" text buttons with a funnel icon next to the column
  label; an active filter is highlighted and shows its value count.
- Header labels no longer break inside words (e.g. "Bemerkung"); hyphens
  are non-breaking and a trailing unit moves to its own line, so all
  headers share one height.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-09-26 10:30:15 +02:00
parent e394ff93f4
commit d95f0bf5c0
3 changed files with 74 additions and 12 deletions

View file

@ -173,6 +173,10 @@ tooltip; the toolbar shows icon plus text, group headers and rows show icons
only, separated into logical blocks with `IconButtonSeparator`. Destructive only, separated into logical blocks with `IconButtonSeparator`. Destructive
actions use `variant="danger"` (trash icon for circuits and groups, `ListX` for actions use `variant="danger"` (trash icon for circuits and groups, `ListX` for
removing a device row). removing a device row).
Column headers show the label (click to sort) with a funnel icon for the
value filter; an active filter is highlighted and shows its value count.
Header labels break only between words and put a trailing unit such as
`[kW]` on a second line.
## Bootstrap / Styling Rule ## Bootstrap / Styling Rule

View file

@ -892,24 +892,37 @@ a.kpi:hover {
white-space: nowrap; white-space: nowrap;
} }
/* One header row: label (sort) on the left, filter icon on the right. */
.tree-grid .header-cell { .tree-grid .header-cell {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: stretch; align-items: flex-start;
justify-content: space-between;
gap: 0.2rem; gap: 0.2rem;
min-width: 0; min-width: 0;
} }
.tree-grid .header-filter-btn { .tree-grid .header-filter-btn {
display: block; display: inline-flex;
width: 100%; flex: none;
border: 1px solid var(--panel-border-strong); align-items: center;
background: var(--panel-bg); gap: 0.15rem;
color: var(--text-strong); border: 1px solid transparent;
background: transparent;
color: var(--text-faint);
border-radius: 3px; border-radius: 3px;
font-size: 0.75rem; font-size: 0.7rem;
padding: 0.15rem 0.35rem; font-weight: 700;
line-height: 1;
padding: 0.15rem 0.2rem;
white-space: nowrap; white-space: nowrap;
cursor: pointer;
}
.tree-grid .header-filter-btn:hover,
.tree-grid .header-filter-btn:focus-visible {
border-color: var(--panel-border-strong);
color: var(--text-strong);
} }
.tree-grid .header-sort-btn { .tree-grid .header-sort-btn {
@ -927,7 +940,10 @@ a.kpi:hover {
font-size: 0.78rem; font-size: 0.78rem;
font-weight: 600; font-weight: 600;
line-height: 1.15; line-height: 1.15;
overflow-wrap: anywhere; /* Break only between words; never inside a word such as "Bemerkung". */
overflow-wrap: normal;
word-break: normal;
hyphens: manual;
white-space: normal; white-space: normal;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
@ -967,6 +983,18 @@ a.kpi:hover {
font-size: 0.78rem; font-size: 0.78rem;
} }
.tree-grid .header-cell .header-sort-btn {
flex: 1 1 auto;
width: auto;
align-items: flex-start;
}
.tree-grid .header-unit {
display: block;
font-weight: 500;
color: var(--text-faint);
}
.tree-grid .header-sort-btn:hover span:first-child, .tree-grid .header-sort-btn:hover span:first-child,
.tree-grid .header-sort-btn:focus-visible span:first-child { .tree-grid .header-sort-btn:focus-visible span:first-child {
text-decoration: underline; text-decoration: underline;
@ -983,6 +1011,11 @@ a.kpi:hover {
background: var(--accent-blue-soft-bg); background: var(--accent-blue-soft-bg);
} }
.tree-grid .header-filter-count {
min-width: 0.7rem;
text-align: center;
}
.tree-grid .header-filter-menu { .tree-grid .header-filter-menu {
position: absolute; position: absolute;
z-index: 7; z-index: 7;

View file

@ -9,6 +9,7 @@ import {
Columns3, Columns3,
FilterX, FilterX,
FolderPlus, FolderPlus,
Funnel,
ListOrdered, ListOrdered,
ListPlus, ListPlus,
ListX, ListX,
@ -268,6 +269,24 @@ function createValuesFromEditPatch(
); );
} }
/**
* Header labels break only between words: hyphens become non-breaking and a
* trailing unit such as "[kW]" is always shown on its own second line, so
* header heights stay uniform.
*/
function renderColumnHeaderLabel(label: string) {
const match = /^(.*?)s*([[^]]+])$/.exec(label);
const name = (match ? match[1] : label).replace(/-/g, "‑");
return match ? (
<>
{name}
<span className="header-unit">{match[2]}</span>
</>
) : (
name
);
}
export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) { export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) {
/* /*
Circuit-tree editor invariant overview: Circuit-tree editor invariant overview:
@ -3440,7 +3459,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}); });
}} }}
> >
<span>{column.label}</span> <span className="header-label">
{renderColumnHeaderLabel(column.label)}
</span>
{sortState?.key === column.key ? ( {sortState?.key === column.key ? (
<span className="sort-indicator" aria-label={sortState.direction === "asc" ? "aufsteigend" : "absteigend"}> <span className="sort-indicator" aria-label={sortState.direction === "asc" ? "aufsteigend" : "absteigend"}>
{sortState.direction === "asc" ? "↑" : "↓"} {sortState.direction === "asc" ? "↑" : "↓"}
@ -3451,6 +3472,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
type="button" type="button"
className={`header-filter-btn ${(columnFilters[column.key]?.length ?? 0) > 0 ? "active" : ""}`} className={`header-filter-btn ${(columnFilters[column.key]?.length ?? 0) > 0 ? "active" : ""}`}
onClick={() => toggleColumnFilterMenu(column.key)} onClick={() => toggleColumnFilterMenu(column.key)}
aria-label={`${getFullColumnLabel(column)} filtern`}
aria-pressed={(columnFilters[column.key]?.length ?? 0) > 0} aria-pressed={(columnFilters[column.key]?.length ?? 0) > 0}
title={ title={
(columnFilters[column.key]?.length ?? 0) > 0 (columnFilters[column.key]?.length ?? 0) > 0
@ -3458,7 +3480,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
: `${getFullColumnLabel(column)} filtern` : `${getFullColumnLabel(column)} filtern`
} }
> >
Filtern{(columnFilters[column.key]?.length ?? 0) > 0 ? ` (${columnFilters[column.key]?.length})` : ""} <Funnel aria-hidden="true" size={13} strokeWidth={2.2} />
{(columnFilters[column.key]?.length ?? 0) > 0 ? (
<span className="header-filter-count">{columnFilters[column.key]?.length}</span>
) : null}
</button> </button>
</div> </div>
{openFilterColumn === column.key ? ( {openFilterColumn === column.key ? (