From d1615bcfeeea59bda6f01305e8d26b4a78645f7b Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 22 Jul 2026 22:57:25 +0200 Subject: [PATCH] Clarify active grid filters --- docs/spec/07-implementation-phases-todo.md | 2 + src/app/globals.css | 61 ++++++++- .../components/circuit-tree-editor.tsx | 122 ++++++++++++++---- 3 files changed, 161 insertions(+), 24 deletions(-) diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index b3d2c96..45700ad 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -287,6 +287,8 @@ Implemented layout foundation: - the toolbar remains reachable while the page scrolls - programmatic grid focus preserves the viewport during undo/redo reloads - wide tables no longer expand the surrounding browser layout +- active sorts and filters are summarized above the grid and can be removed individually or reset together +- column filter buttons show their selected-value count and filter menus explain their matching behavior Acceptance criteria: diff --git a/src/app/globals.css b/src/app/globals.css index 9038930..dae8ba5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -71,6 +71,44 @@ body { opacity: 0.45; } +.active-view-summary { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem; + padding: 0.4rem 0.5rem; + border: 1px solid #bfdbfe; + border-radius: 5px; + background: #eff6ff; + color: #1e3a5f; + font-size: 0.78rem; +} + +.active-view-label { + font-weight: 600; +} + +.active-view-chip, +.active-view-reset { + border: 1px solid #93b4df; + border-radius: 999px; + background: #fff; + color: #1e3a5f; + padding: 0.18rem 0.48rem; + font-size: 0.76rem; + cursor: pointer; +} + +.active-view-chip:hover, +.active-view-reset:hover { + border-color: #2563eb; +} + +.active-view-reset { + margin-left: auto; + font-weight: 600; +} + .column-settings-menu { position: absolute; z-index: 9; @@ -278,6 +316,7 @@ body { .tree-grid .header-filter-btn.active { border-color: #2563eb; color: #2563eb; + background: #eff6ff; } .tree-grid .header-filter-menu { @@ -293,13 +332,33 @@ body { padding: 0.35rem; } +.tree-grid .header-filter-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.4rem; + margin-bottom: 0.35rem; + font-size: 0.72rem; +} + +.tree-grid .header-filter-explanation { + margin-bottom: 0.4rem; + color: #4b5563; + font-size: 0.72rem; + line-height: 1.3; + white-space: normal; +} + .tree-grid .header-filter-clear { border: 1px solid #c4cddc; background: #fff; border-radius: 3px; font-size: 0.72rem; padding: 0.12rem 0.3rem; - margin-bottom: 0.25rem; +} + +.tree-grid .header-filter-clear:disabled { + opacity: 0.45; } .tree-grid .header-filter-values { diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index b5a5927..6fae96c 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -315,6 +315,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str [columnFilters] ); + const activeColumnFilters = useMemo( + () => + visibleColumns.flatMap((column) => { + const values = columnFilters[column.key] ?? []; + return values.length > 0 ? [{ column, values }] : []; + }), + [columnFilters, visibleColumns] + ); + const distinctValuesByColumn = useMemo(() => { // Filter options are generated only for visible columns to match current header UI. // Hidden columns still exist in row model, but are intentionally not filterable from header. @@ -333,6 +342,67 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str return buildVisibleGridRows(filteredSortedSections); }, [filteredSortedSections]); + function clearColumnFilter(key: CellKey) { + setColumnFilters((current) => { + const next = { ...current }; + delete next[key]; + return next; + }); + setOpenFilterColumn((current) => (current === key ? null : current)); + } + + function clearSortAndFilters() { + setSortState(null); + setColumnFilters({}); + setOpenFilterColumn(null); + } + + function renderActiveViewSummary() { + if (!sortState && activeColumnFilters.length === 0) { + return null; + } + + const sortedColumn = sortState + ? allColumns.find((column) => column.key === sortState.key) + : undefined; + + return ( +
+ Active view + {sortState ? ( + + ) : null} + {activeColumnFilters.map(({ column, values }) => { + const shownValues = values.slice(0, 2).join(", "); + const valueSummary = values.length > 2 ? `${shownValues} +${values.length - 2}` : shownValues; + return ( + + ); + })} + +
+ ); + } + const searchableProjectDevices = useMemo(() => { const term = projectDeviceSearch.trim().toLowerCase(); if (!term) { @@ -2247,11 +2317,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str {openFilterColumn === column.key ? (
- +
+ Select one or more values. A circuit is shown when its block contains any selected value. +
+
+ + {(columnFilters[column.key]?.length ?? 0) > 0 + ? `${columnFilters[column.key]?.length} selected` + : "Showing all"} + + +
{distinctValuesByColumn[column.key].map((value) => { const selected = columnFilters[column.key] ?? [];