From c75dc497d2e48ea86e2330fe43c1a2b8f8032511 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 02:44:25 +0000 Subject: [PATCH 01/12] Add PLAN.md for the neutral EDA UI redesign Contract for the unattended implementation run: decisions, interfaces, task breakdown with tiers, verification commands and follow-ups. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Np3Vgtux8ZgiKcc75wYrZE --- PLAN.md | 447 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..988b19f8 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,447 @@ +# PLAN — OpenPCB UI refactor to the neutral EDA design (Claude Design handoff) + +status: ready +owner: orchestrator session +branch: ui/neutral-eda-redesign +design bundle: /home/claude/repo/project (read-only source of truth) +design specs (extracted, exact px/hex): + - /tmp/claude-0/-home-claude-repo/caff7693-acd5-53e8-ac09-a6bfa2ebf24d/scratchpad/design-D2-pcb-schematic.md + - /tmp/claude-0/-home-claude-repo/caff7693-acd5-53e8-ac09-a6bfa2ebf24d/scratchpad/design-D3-lib-bom-home.md +current-state recon (file:line, must-preserve lists): + - .../scratchpad/recon-A1-shell.md, recon-A2-pcb.md, recon-A3-schematic.md, recon-A4-lib-bom-home.md +token source: /home/claude/repo/project/openpcb-theme.css + +--- + +## 0. Goal and non-goals + +Goal: re-skin and restructure the OpenPCB desktop frontend so the six screens in +`Compare Screens.dc.html` (2a PCB editor, 3a Schematic editor, 3b Library, 3c BOM, +3d Home, 2b Token sheet) match the handoff designs, **without changing behaviour**. +Every callback, store, hotkey, persisted key, DnD MIME contract, cross-probe path and +e2e accessible name listed in the recon files keeps working. + +Non-goals (explicitly out of scope, recorded as follow-ups in §9): +- Canvas palette (schematic wire colours, PCB layer colours, selection colour inside + WebGL canvases). Owned by external package `@openpcb/r3f-eda-canvas` + (`OpenPCB-app/shared`); `EdaCanvas` wraps its own `CanvasThemeProvider(mode)`. +- Website (4a) — different repo. +- New product features the designs sketch but the backend does not support + (multi-sheet "Sheets", design tags, recent-activity feed, ERC dock, DRC + waive/severity, BOM "Group by", file "Open…" dialog). Rule in §2.D6. +- 3D view, Settings, Assistant space, Knowledge/Docs, Tasks, import wizard, dialogs: + **token re-skin only** (they pick up new fonts/colours/radii via §3 T1), no + structural changes. + +## 1. Context (from Phase 1 recon) + +- Stack: React 19, Vite 7, Tailwind v4 (CSS-only config in + `src/core/frontend/src/index.css`: `@theme`, `@theme inline` semantic layer, + `:root` light + `html.dark` dark, class-based dark via + `@custom-variant dark (&:where(.dark, .dark *))`), lucide-react, Radix + (tabs/tooltip/dialog/dropdown/context-menu/scroll-area), zustand, clsx + + tailwind-merge via `cn()` in `src/core/frontend/src/lib/utils.ts`. +- Styling reality: 4,032 raw `slate-*` and 699 `violet-*` class usages across ~150 + TSX files; only ~80 uses of semantic tokens. Radii tokens today: card 14px, + control 8px. Fonts: "Inter" declared but never loaded (system fallback). +- Shared primitives: `src/shared/frontend/ui/*` (button, card, chip, icon-button, + pill, tabs, tooltip, textarea, stacked-card, dropdown-menu, context-menu, + relevance-bar) — hand-rolled variant maps + `cn()`, no cva. +- Shell: `AppShell.tsx` = TitleBar (36px, Electron only) + `grid-cols-[80px_1fr]` + (LeftSidebar 80px rail | main). Modules registered in rail: Designer, Library, + Docs (id `knowledge`), Assistant; Home is a fixed first item; footer = bug link + + Settings. +- Designer `Space.tsx` (1460 lines): DesignerHeader (44px, 3-col grid: DesignTabs | + view Tabs schem/pcb/3d/bom/drc | trailing cloud+chat) → error strip → main flex + row: [left sidebar (300 default, 240–520, not persisted) | resizer | canvas + wrapper | resizer + Selection Inspector dock (schem, 260–440, persisted) | + resizer + DRC dock (pcb, 280–560, persisted) | resizer + Chat dock (any, 320–560, + persisted)]. PCB/DRC views append `DesignerStatusBar` (24px). +- PCB chrome is mostly rendered by `PcbCanvas.tsx` (~6000 lines): floating + `PcbTopToolbar` (top-centre), floating `RouteHud`/`TuneHud`/`BundleHud` + (bottom-centre), floating `PcbSelectionInspector` (top-right, free + holes/pads/text only), and `PcbBoardPanel`/`PcbLayersPanel` **portalled** into + `CollapsibleSection`s in `DesignerSidebar` via `pcbSlotRef`/`pcbLayersSlotRef` + (sections stay mounted while collapsed; ids `pcb.sidebar.board`, + `pcb.sidebar.layers` are localStorage keys). PCB selection lives inside + PcbCanvas (`PcbSelection` sets); only `onSelectionCountChange` and + `onViewportChange(zoom,x,y)` reach Space. `cursorMm` state exists inside + PcbCanvas (line ~696) but is not surfaced. +- Schematic chrome: floating `DesignerFloatingToolbar` mounted by Space; docked + `OutlinePanel` (Parts/Nets/Labels tabs, search, sortable columns) in the left + sidebar; docked `SelectionInspector` (Part/Multi/Label/Wire panels) on the + right; no status bar; no ERC UI (backend only). +- Library: header + `FacetSidebar` (w-60, checkbox facets Source/Family/Mount/ + Package/Other) + card grid (`LibraryCard` h-56); detail = full page swap to + `ComponentDetailPage` (edit/clone/STEP upload/fullscreen previews live there). + `LibraryCard` sets drag MIME `application/x-openpcb-library-component`. +- BOM: `DesignerBomView.tsx` two-pane `grid-cols-[minmax(0,1fr)_360px]`; CSS-grid + table (checkbox | severity pill | Ref | Qty | Value | MPN·Source | Cost); + severity stripe + tint per row; footer stats; `BomInspector` rail with 650ms + debounced autosave; export menu. +- Home: `HomeScreen.tsx` centred column; chips All/Recent/Starred/Archived; search + ⌘K; sort; grid/list toggle; `DesignCard` with `SchematicThumbnail`; `CloudSyncPill` + ("Sign in to sync") next to "New design". DTO: id, name, revision, createdAt, + updatedAt, schematicPreview, drcStatus — **no** board size / layer count / nets. +- Verification baseline (green at start): `cd src/core/frontend && npx tsc --noEmit + --pretty false -p tsconfig.json` exit 0; `npm run test:react` 47 files / 326 tests. + E2E (Playwright) exist but need a backend + browser; not run in this session — + selectors preserved by rule (§2.D9). + +## 2. Decisions (with rationale) + +D1. **Token foundation = `openpcb-theme.css` dropped into `index.css`, plus a +compatibility layer.** Replace the `@theme` / `@theme inline` / `:root` / +`html.dark` blocks with the design's tokens (both themes). Keep the existing +semantic names alive as aliases so unmigrated files keep compiling and look +coherent: `--color-surface-card → surface-panel`, `--color-surface-card-hover → +surface-hover`, `--color-text-primary → text-strong`, `--color-accent → +selection`, `--color-accent-soft → selection-soft`, `--color-accent-text → text-strong`, +`--color-status-*-soft` (new soft values), `--radius-card → 2px`, +`--radius-control → 2px`, `--radius-float → 3px`, `--radius-pill → 999px`. +Additionally override Tailwind's own scales inside `@theme` so the ~150 untouched +files immediately lose the blue cast, violet accent and pill geometry: + - `--color-slate-50…950` → neutral ramp (50 #f7f7f8, 100 #ececee, 200 #dcdce0, + 300 #c4c4c9, 400 #a8a8ad, 500 #7f7f84, 600 #55555a, 700 #2c2c31, 800 #1c1c1f, + 900 #111113, 950 #0c0c0d). + - `--color-violet-50…950` → neutral "active" ramp (50 #f0f0f2, 100 #e4e4e7, + 200 #d0d0d5, 300 #a8a8ad, 400 #8a8a90, 500 #55555a, 600 #3a3a40, 700 #2c2c31, + 800 #1c1c1f, 900 #1c1c1f, 950 #151517). + - `--radius-sm: 2px; --radius-md: 2px; --radius-lg: 2px; --radius-xl: 2px; + --radius-2xl: 3px; --radius-3xl: 3px` (so `rounded-md/lg/xl/2xl` flatten; + `rounded-full` stays round for dots/spinners). + Rationale: one CSS file re-skins the whole app on day one; the five target + screens are then migrated to semantic utilities properly; the remap is the + documented stopgap for the rest (follow-up §9). + Added semantic tokens the designs need (both themes): `surface-hover` + (#1c1c1f / #e6e6e9), `surface-selected` (#26262b / #dcdce0), `surface-section` + (= panel-head), `surface-canvas-well` (#08090a / #08090a), `text-caps` (#6a6a70 / + #6f6f76), `border-control` (#2a2a2e / #cfcfd4), `primary` (#e8e8e8 / #111114), + `primary-foreground` (#111114 / #f5f5f5), `status-*-soft` at 12% alpha. + +D2. **Fonts bundled, not fetched.** Add `@fontsource/ibm-plex-sans` (400/500/600) +and `@fontsource/ibm-plex-mono` (400/500) to `src/core/frontend/package.json`; import +the weight CSS files in `main.tsx`. Electron runs offline; no Google Fonts. + +D3. **Light theme stays.** `ThemeToggle`/`applyThemeClass` untouched; light values +come from `openpcb-theme.css`. Designs were only drawn dark; light is the token +sheet's light column. + +D4. **Shared primitives are rewritten first** (T2) and new shared building blocks +added so screens share one vocabulary: `PanelSectionHeader`, `PropertyGrid`/ +`PropertyRow`, `DataTable` helpers (`TableHeaderRow`, `TableRow`), `SegmentedControl`, +`SearchField`, `Checkbox`, `StatusDot`, `SeverityDiamond`, `DockTabs`, `StatusBar`/ +`StatusSegment`, `ToolbarButton`/`ToolbarSeparator`. All in `src/shared/frontend/ui/`. +Exact px/hex per design-D2 §7 and design-D3 §5. + +D5. **Docked chrome via portal slots, not prop-lifting.** `PcbCanvas` owns the +state the toolbar/HUDs/inspector need. Space.tsx renders empty slot `
`s +(toolbar row 30px, parameter row, layer tab strip 22px, right-dock Properties +body) and passes refs down exactly like the existing `pcbSlotRef`/`pcbLayersSlotRef` +pattern; PcbCanvas portals `PcbTopToolbar`, the HUDs, `PcbLayerTabStrip` and +`PcbPropertiesPanel` into them. Zero behavioural wiring changes; every prop stays. + +D6. **Design elements without backing data are omitted, not faked.** No disabled +placeholders, no static demo rows. Concretely omitted: schematic "Sheets" section, +Home "Tags" group and "Recent activity", Home board-size/layers/nets columns, +Home "Open…" button, BOM "Group by" and "Unplaced" filter, DRC "Waive / Set +severity / Exclude type / Show waived" footer, schematic ERC dock tab and ERC +status segment, "Docs"-style items that don't exist. Where the *data* exists but +the *control* is new UI over existing actions (layer tab strip → `onSetActiveLayer`; +Home "Import KiCad…" → existing `KicadProjectImportWizard`), it is built. +(Confirm with user — Q1 in §8.) + +D7. **One right dock per editor view, tabbed.** Replace the three sequential +right docks with a single `DesignerRightDock` (default 300px, resizable 260–560, +width persisted under new key `openpcb:designer:dock-width`; open state under +`openpcb:designer:dock-open`; active tab under `openpcb:designer:dock-tab`). +Tabs: PCB → Properties | DRC | Assistant; Schematic → Properties | Assistant; +3D → Assistant only; BOM/DRC full views → dock hidden (BOM has its own rail). +Migration: on first load read legacy keys (`chat-open`, `chat-width`, +`inspector-open`, `inspector-width`, `drc-width`) to seed the new ones. Hotkeys: +Cmd/Ctrl+I → open dock on Assistant; Cmd/Ctrl+. → toggle dock; DRC toolbar +button / status-bar DRC counter / `useDrcStore` open → dock on DRC tab. The +full-screen `drc` view tab is kept (design header has a DRC view tab). + +D8. **PCB Properties tab content** (rendered by PcbCanvas into the dock slot): +- nothing selected → "Board" state: existing `PcbBoardPanel` content re-laid as + property grid (Outline: shape/width/height + existing edit/draw/DXF/fit actions; + Design rules row opens existing `PcbDesignRulesDialog`; Summary: parts count, + nets/unrouted from workspace if available). +- one placement selected → Reference, Value, Footprint, Layer (side), X, Y, + Rotation (read from projection; editable only where a dispatch command already + exists: rotate/flip via existing actions), Pads table (# | net | size). +- free hole/pad/text selected → existing `PcbSelectionInspector` panels, restyled. +- multi → count + kinds. +The floating `PcbSelectionInspector` container goes away; its panel bodies move +into the dock. + +D9. **E2E accessible names are frozen.** Buttons keep names: "Route (R)", +"Tune (U)…", "Bundle", "Board (O)…", "Flip part", "Undo", "Redo", "Fit schematic", +"Import outline", "Import DXF…", "Draw custom shape…", "Redraw shape…", +"Reset to rectangle", "New Design"/"New design", "Designer", "Library", "Edit", +"Preview…", "Link to Cloud…", "Open from Cloud…", "Import to local & open"; +tabs "Schem"/"PCB"; headings "Designs", "Settings", "No design open"; label +"Settings"; texts "Route — click a pad to start", "Bundle — click 2+ pads to +collect…", "100% routed", "Untitled Design", "Custom shape", "Alternatives", +"Recommended", "Keep current placement", "Auto Layout applied"; testids +`pcb-route-board-button`, `pcb-autolayout-button`, `pcb-autoplace-button`, +`component-footprint-variants`, `component-mount-type`, `component-pad-count`, +`footprint-preview-canvas`. Icon-only buttons keep `aria-label`/`title` equal to +the old visible label. + +D10. **Layout constants.** Rail 80px (unchanged). TitleBar 36px (unchanged). +Designer header 44 → 34px. Docked toolbar 30px. Parameter row 28px (only while +Route/Tune/Bundle active). Status bar 24 → 22px, shown for PCB, DRC **and** +schematic. Left panel default 260 (bounds 240–520 unchanged). Section headers +24px, list rows 22px, property rows 22px, BOM rows 24px, Home list rows 64px. + +D11. **Library gets a table view with a sticky preview pane; the detail page stays.** +Table is the default; the existing card grid remains behind the Table/Grid toggle +(persist choice in localStorage `openpcb.library.view`). Selecting a row loads the +existing detail payload (`GET …/components/{id}/detail`) into a 380px preview pane +(symbol + footprint previews via existing `SymbolPreviewCanvas`/ +`FootprintPreviewCanvas`, Part fields, Footprints with default badge, Pins, +Specs). "Open" / double-click → `ComponentDetailPage` unchanged (editing, clone, +STEP upload, fullscreen). Rows carry the same drag MIME/payload as `LibraryCard`. +Bulk selection/delete keeps working (checkbox column appears in selection mode). + +D12. **BOM columns** → [checkbox 24px] [tier dot 28px] Designators | Value | +Footprint | Description | Qty | MPN | Unit | Ext. Severity map: `sourced` → Exact +(#6fbf7a), `suggested` → Suggested (#d9a441), `critical`/`review` → Missing +(#e0705f), `dnp` → neutral dot + strikethrough designators. Missing MPN renders +italic "Add part number" in danger colour. In-table totals row + 22px page footer. +Sourcing rail = property grid (Line / Sourcing / Alternates-if-data / Match tier +legend). All autosave/export/cross-probe logic untouched. + +D13. **Home** → header 34px (title, count, search, List/Grid toggle, "Import +KiCad…", "New design" primary) | left sidebar 200px (All/Recent/Starred/Archived +rows with counts; footer "Local only — not signed in" + "Sign in to sync" = +relocated `CloudSyncPill` logic) | list table (Preview 220×52 thumbnail | Name + +path-less second line (created date) | Rev | DRC pill | Modified | ★) with 64px +rows | right detail panel 300px (large thumbnail, Design: Revision/Created/ +Modified, Status: DRC, Open button, ActionsMenu) | footer bar 22px ("designs N", +"Local", app version). Grid view keeps restyled `DesignCard`. Sort dropdown kept. + +D14. **Schematic** → docked 30px toolbar (existing tools, Fit keeps name "Fit +schematic"); left panel: Outline header (24px, count) + filter + segmented +Parts/Nets/Labels + column header + 22px rows; right dock Properties = existing +inspector panels restyled as property grids, idle state = "Sheet" summary +(symbols, nets, labels counts from projection; PCB sync row if +`pcbStale`/changes info exists); status bar (grid 2.54 mm, zoom, hint, selection). + +D15. **Docs updated.** `docs/design/design-tokens.md` rewritten to the new system +(short, points at `index.css`), `docs/design/ui-backlog.md` gets a header note +that mockups predate the neutral redesign. + +## 3. Task breakdown + +Tiers: standard (Sonnet) · careful (Opus) · critical (Opus, xhigh review). +Dependency order: T1 ∥ T2 → wave {T3, T4a→T4b (one agent chain), T6, T7} → T5 → T8. +Execution rules (Phase 3 decision): NO worktrees. All agents edit the main checkout +on branch `ui/neutral-eda-redesign`, touch only their listed files, never run +`git add`/`git commit`; the orchestrator commits after review. Parallel agents may +see transient `tsc` errors from another agent's in-flight files — act only on errors +in your own files and say so in the report. + +### T1 — Token foundation + fonts + docs [careful] +Files: `src/core/frontend/src/index.css`, `src/core/frontend/src/main.tsx`, +`src/core/frontend/package.json`, `package-lock.json`, `docs/design/design-tokens.md`, +`docs/design/ui-backlog.md`. +- Implement D1 (all tokens, aliases, slate/violet/radius remaps), D2, D15. +- Keep `.tiptap-is-empty` rule and `@source`/`@plugin`/`@custom-variant` lines. +- body: `font-family: var(--font-sans); font-size: 12px; font-variant-numeric: + tabular-nums` (12px base, not 11 — app is denser than the 1440×900 mock and + existing text-xs classes map to 11px via the scale). +VERIFY: `npm ci` ok; tsc clean; `npm run test:react` green; `npm run build:frontend` +succeeds; grep confirms no `#7c3aed`/`Inter` left in index.css. + +### T2 — Shared UI primitives [careful] +Files: `src/shared/frontend/ui/*` (rewrite existing to token sheet; add +`panel-section-header.tsx`, `property-grid.tsx`, `data-table.tsx`, +`segmented-control.tsx`, `search-field.tsx`, `checkbox.tsx`, `status-dot.tsx`, +`severity-diamond.tsx`, `dock-tabs.tsx`, `status-bar.tsx`, `toolbar.tsx`), update +`index.ts` barrel (also export Tabs/ContextMenu). +- Existing exported prop APIs stay backward compatible (variants/tones may map to + new looks; no removed props). +- Specs: design-D2 §3/§7/§9, design-D3 §5. Heights 22px, radius via tokens only, + no raw palette classes in these files. +VERIFY: tsc; test:react; `grep -c "slate-\|violet-" src/shared/frontend/ui/*.tsx` = 0. + +### T3 — App shell: rail, title bar, theme toggle, dialogs, scroll-area [standard] +Files: `AppShell.tsx`, `components/LeftSidebar.tsx`, `components/TitleBar.tsx`, +`components/ThemeToggle.tsx`, `components/AppContextMenu.tsx`, `components/ui/*`, +`screens/ModuleScreen.tsx`, `screens/SettingsScreen.tsx`, `settings/SettingsSidebar.tsx` +(settings: token classes only, no layout change). +Rail per design-D2 §2 (items 72px, active #1c1c1f r2 64px, icon 20 stroke 1.5, +label 10px). Keep aria-labels "Home", "Settings", module labels. +VERIFY: tsc; test:react; no `slate-|violet-` in touched files. + +### T4 — Designer shell + PCB editor [critical] — run as T4a then T4b in one agent +T4a = `Space.tsx` + header/tabs/sidebar/status bar/empty state + new `DesignerRightDock` ++ portal slot divs + dock prefs migration. T4b = everything under `pcb/` portalled +into the slots. `ToolbarButton` must emit the exact former `title`/`aria-label` +strings (e.g. "Route (R)", "Board (O)", "Flip part", "Fit") — e2e locators depend on them. +Files: `Space.tsx`, `components/DesignerHeader.tsx`, `DesignTabs.tsx`, +`DesignerSidebar.tsx`, `CollapsibleSection.tsx`, `DesignerStatusBar.tsx`, +`DesignerEmptyState.tsx`, `DesignerPlaceholderView.tsx`, `DesignerDrcView.tsx`, +`CloudSyncBadge.tsx`, `CloudPresenceIndicator.tsx`; new `components/DesignerRightDock.tsx`; +`pcb/PcbCanvas.tsx` (mount points only), `pcb/PcbTopToolbar.tsx`, `pcb/RouteHud.tsx`, +`pcb/TuneHud.tsx`, `pcb/BundleHud.tsx`, `pcb/PcbLayersPanel.tsx`, `pcb/PcbBoardPanel.tsx`, +`pcb/PcbSelectionInspector.tsx` → `pcb/PcbPropertiesPanel.tsx` (new), new +`pcb/PcbLayerTabStrip.tsx`, `pcb/PcbSelectionFilter.tsx`, `pcb/PcbPlacePreviewBar.tsx`, +`pcb/PcbSideModeButton.tsx`. +Implement D5, D7, D8, D10 for PCB; header per design-D2 §3 (34px; view tabs with +2px underline; trailing Local/Cloud + dock toggle); toolbar docked per §4 (keep +existing tool set and order semantics; Route/Board/Add/DRC/View; Measure/Tune/ +Bundle stay hotkey-only); parameter row per §5 hosts RouteHud/TuneHud/BundleHud +content (secondary rows for proposals/gate warnings render as an additional +28px row); Layers panel per §6 (rows 22px, swatch, friendly + KiCad hint, eye +toggle, Solo/Alt-click, Normal/Dim/Hide segmented, presets); layer tab strip +per §8 driven by active layer + `onSetActiveLayer`; status bar per §9 (surface +`cursorMm` via new optional `onCursorChange` prop; grid; zoom %; active layer; +hint; DRC counter; selection; view side; mm). +VERIFY: tsc; test:react; manual checklist in §6; localStorage migration unit test +for dock keys (add to `Space` neighbour test file if one exists, else +`stores/designer-dock-prefs.test.ts`). + +### T5 — Schematic editor [careful] (worktree; depends on T4's dock + slots — run +after T4 merges, or in the same agent chain as T4) +Files: `components/DesignerFloatingToolbar.tsx` (→ docked row), `OutlinePanel/*`, +`SelectionInspector/*`, `LabelPicker.tsx`, `ComponentCommandPalette.tsx`, +`ComponentClassIcon.tsx`, schematic branches in `Space.tsx`. +Implement D14. Keep every hotkey, selection nonce sync, DnD MIME, context menus. +VERIFY: tsc; test:react; §6 checklist. + +### T6 — Library [careful] (worktree, parallel with T4) +Files: `src/modules/library/frontend/Space.tsx`, `LibraryCard.tsx`, new +`components/LibraryTable.tsx`, new `components/LibraryPreviewPane.tsx`, +`components/FacetSidebar.tsx`, `ActiveFilterChips.tsx`, `TagChip.tsx`, +`TagFilterChips.tsx`, `TagTokenInput.tsx`, `DetailsCard.tsx`, `FootprintOptionsList.tsx`, +`PinsTable.tsx`, `PreviewModal.tsx`, `ComponentDetailPage.tsx` (token re-skin + +2px radii; layout unchanged), `CloudLibrarySyncButton.tsx`. +Implement D11; specs design-D3 §2. +VERIFY: tsc; test:react (incl. `ComponentDetailPage.test.ts`); §6 checklist. + +### T7 — BOM + Home [careful] (worktree, parallel with T4) +Files: `components/DesignerBomView.tsx`; `screens/HomeScreen.tsx`, +`screens/home/DesignCard.tsx`, `screens/home/SchematicThumbnail.tsx`, new +`screens/home/HomeSidebar.tsx`, `screens/home/DesignListRow.tsx`, +`screens/home/DesignDetailPanel.tsx`. +Implement D12, D13; specs design-D3 §3–4. +VERIFY: tsc; test:react (`schematic-preview.test.ts`); §6 checklist. + +### T8 — Integration, sweep, verification [orchestrator + reviewer agents] +Merge worktrees, resolve conflicts, run full VERIFY, grep sweep for leftover +`violet-`/`rounded-xl`/`rounded-2xl`/`rounded-full` on non-circular elements in +touched files, run `npm run build:frontend`, write Run log. + +## 4. Interfaces + +```ts +// src/modules/designer/frontend/components/DesignerRightDock.tsx +export type DockTab = "properties" | "drc" | "assistant"; +export interface DesignerRightDockProps { + tabs: ReadonlyArray<{ id: DockTab; label: string; badge?: number | string }>; + activeTab: DockTab; + onTabChange: (t: DockTab) => void; + width: number; // clamped 260–560 by owner + onResizeStart: (e: React.PointerEvent) => void; + onClose: () => void; + children: React.ReactNode; // body for activeTab +} + +// Space.tsx → PcbCanvas (new optional props; portal slots) +pcbToolbarSlotRef?: React.RefObject; +pcbParamRowSlotRef?: React.RefObject; +pcbLayerStripSlotRef?: React.RefObject; +pcbPropertiesSlotRef?: React.RefObject; +onCursorChange?: (pt: { xMm: number; yMm: number } | null) => void; +onActiveLayerChange?: (layer: PcbLayerId) => void; // for status bar chip + +// shared/ui additions (all accept className, forward refs where sensible) +PanelSectionHeader({ title, count?, trailing?, collapsed?, onToggle? }) +PropertyGrid({ children }) PropertyRow({ label, mono?, hint?, children }) +TableHeaderRow({ cols: string /* grid-template-columns */, children }) +TableRow({ cols, selected?, onClick?, ... }) +SegmentedControl({ options: {id:T; label; icon?}[], value, onChange, size? }) +SearchField({ value, onChange, placeholder, shortcutHint?, ... }) +Checkbox({ checked, onChange, label?, indeterminate? }) +StatusDot({ tone }) SeverityDiamond({ severity: "error"|"warning"|"info" }) +DockTabs({ tabs, active, onChange }) +StatusBar({ children }) StatusSegment({ children, flex?, mono? }) +ToolbarButton({ icon, label, hotkey?, active?, disabled?, onClick, ... }) ToolbarSeparator() +``` + +## 5. Edge cases and failure modes + +- Light theme: every new token has a light value; run the app with `.dark` removed + once per screen (manual). +- Electron title bar: `TitleBar` only renders under Electron; rail must not assume it. +- Persisted keys: `pcb.sidebar.board`, `pcb.sidebar.layers`, `openpcb.designer.tabs.v1`, + `openpcb:designer:recents`, `openpcb.home.starred/archived` untouched; dock keys + migrated (D7). +- Portal slots: slot `
`s must exist before PcbCanvas mounts its portals; guard + with `slotRef.current && createPortal(...)` and re-render on ref availability + (existing pattern in PcbCanvas for sidebar slots — copy it). +- The parameter row must not steal keyboard focus from the canvas; inline editors + keep their Enter/Escape/blur contract (recon-A2 §10). +- Library table with 60+ rows: no virtualization today; keep rows cheap (no + per-row preview canvases — glyph only). +- BOM `checkedIds` bulk DNP requires the checkbox column to remain. +- `LibraryCard` drag payload duplicated into table rows — share one helper. +- Reduced-motion / `backdrop-blur` removed with floating chrome (docked panels + are opaque). + +## 6. Manual verification checklist (run phase, per screen, both themes) + +PCB: open design → PCB tab; toolbar docked, Route toggles parameter row, Esc +exits; select footprint → Properties fills; click empty → Board state; DRC button +→ dock DRC tab, row click centres canvas; layer strip click → active layer +changes in Layers panel + status bar; Cmd/Ctrl+I → Assistant tab; Cmd/Ctrl+. → +dock toggles; reload → dock width/tab restored; Layers eye toggles work; Alt+click +solo works; status bar shows cursor X/Y while moving. +Schematic: toolbar docked, ⌘K palette, G/P/H place, outline row click selects on +canvas, F2 rename, inspector edits value, "View on PCB" cross-probes, status bar +zoom updates. +Library: table default, facets filter rows, row click fills preview, double-click +opens detail page, Edit/Save/Clone still work, drag row to schematic places part, +Grid toggle shows cards, selection-mode bulk delete. +BOM: rows/tiers render, row click fills rail, edit MPN autosaves ("Saved"), +DNP checkbox, Show in schematic/PCB, Export CSV, totals correct. +Home: list default with thumbnails, ★ toggles persist, filters/counts, search +⌘K, N creates design, delete flow, "Sign in to sync" in sidebar footer opens +Settings → Account, grid toggle. + +## 7. Verification commands + +``` +cd /home/claude/openpcb +npx tsc --noEmit --pretty false -p src/core/frontend/tsconfig.json # exit 0 +npm run test:react # all green +npm run build:frontend # succeeds +grep -rnE "(bg|text|border|ring)-(violet|purple|indigo)-" # 0 hits +``` + +## 8. User decisions (answered 2026-09-05, before run) + +Q1. Unsupported design elements → **omit** (D6 stands). +Q2. Library → **table + preview pane, keep detail page** (D11 stands). +Q3. Delivery → **push branch and open a PR** at the end. +Q4. Execution → **Agent waves** (≤4 concurrent implementers), no Workflow. + +## 9. Follow-ups (not in this run) + +- Canvas palette in `OpenPCB-app/shared` (`canvasTheme.ts` SCHEMATIC_DARK, PREVIEW_DARK, + PCB_CANVAS_TOKENS, PCB_LAYER_COLORS/PCB_TRACE_COLORS) → design values in + design-D2 §10 and `openpcb-theme.css` layer palette; then bump the dep. +- Migrate remaining ~100 files from remapped `slate-*`/`violet-*` to semantic tokens + (Assistant, Knowledge, Settings panels, import wizard, autolayout dialogs, 3D). +- ERC frontend (dock tab + status segment) once an ERC API exists. +- Design tags, activity feed, board metadata in `DesignerDesignSummary` for Home. +- Website (4a) in `OpenPCB-app/web`. +- Dead code noticed: `OutlineGroup.tsx` unused, `SortableTh` in BOM unused, + `gridVisible` prop on the schematic toolbar unused, Comment "(C)" hotkey not wired. + +## Run log +(empty — filled by Phase 4) From ab0c0f42e4de4a20e0bb9290c75727fac46e4088 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 02:51:12 +0000 Subject: [PATCH 02/12] T1: neutral EDA design tokens, bundled IBM Plex, docs Replace the slate/violet theme in index.css with the handoff token sheet (light + dark), keep compatibility aliases for pre-redesign semantic names, and remap Tailwind's slate/violet scales and radius scale so unmigrated files render neutral and flat. Bundle IBM Plex Sans/Mono via @fontsource (Electron runs offline). Rewrite docs/design/design-tokens.md for the new system. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Np3Vgtux8ZgiKcc75wYrZE --- docs/design/design-tokens.md | 429 +++++++------------------------- docs/design/ui-backlog.md | 4 + package-lock.json | 20 ++ src/core/frontend/package.json | 2 + src/core/frontend/src/index.css | 354 ++++++++++++++++++-------- src/core/frontend/src/main.tsx | 9 + 6 files changed, 369 insertions(+), 449 deletions(-) diff --git a/docs/design/design-tokens.md b/docs/design/design-tokens.md index 056778f4..479a8ac4 100644 --- a/docs/design/design-tokens.md +++ b/docs/design/design-tokens.md @@ -1,362 +1,105 @@ # Design tokens -> Consolidated 2026-08-02 from the per-screen token fragments scattered through a UI review -> transcript (home, schematic, PCB, 3D, BOM, settings, stacked cards, chat, diagrams, docked -> panel) plus the visual design system recorded in the website notes. -> -> The fragments were written independently, one per screen, and disagreed in several places. -> **Every disagreement is resolved to a single value below, and each resolution says what was -> picked and why.** Nothing is carried on both sides. +> Rewritten 2026-09-05 for the neutral EDA redesign. This document describes the system; +> the **values live in one place**: `src/core/frontend/src/index.css`. If a value here and +> a value there disagree, `index.css` wins — fix this file. -**Ownership:** tokens live in `core/` and are exposed to renderers and UI components through -`shared/`, per the one-way import rule. No module defines its own copy of a colour. +The UI is chrome for an EDA tool: dense, neutral, low-chroma, so that saturated canvas +artwork (copper layers, net colours, DRC markers) is the only thing that draws the eye. -**Rendered reference:** the HTML mockups under [`mockups/`](mockups/) are these token values on a -screen — open one in a browser to see what a value looks like in context before changing it. +**Where tokens live.** `index.css` has four blocks: ---- - -## 1. Structure - -The system has two surface families. They are deliberately distinct, not an unresolved conflict. - -| Family | Where it applies | Theming | -|---|---|---| -| **App chrome** | Shell, panels, modals, settings, dashboards, tables, forms | Light and dark | -| **Canvas** | Schematic, PCB and 3D viewports, and the chat surfaces that sit against them | Dark-biased; see §7 | - -App chrome uses the Tailwind slate and violet scales. The canvas family uses a slightly cooler, -darker near-black. They are close in value and different on purpose: canvas backgrounds sit -behind saturated artwork and need more separation from it than panel chrome does. - ---- - -## 2. Colour — accent - -One accent ramp, used for selection, primary actions, AI-proposal framing and highlights. - -| Token | Value | Use | -|---|---|---| -| `--accent-600` | `#7C3AED` | Primary buttons, primary strokes, the brand accent | -| `--accent-500` | `#8B5CF6` | Base for translucent fills — used only through the alpha tokens below | -| `--accent-400` | `#A78BFA` | Borders on active cards, hover strokes, selection halos | -| `--accent-300` | `#C4B5FD` | Accent text on dark surfaces, pill labels | -| `--accent-fill-subtle` | `rgba(139,92,246,0.06)` | Selection fill on canvas, expanded-card background | -| `--accent-fill` | `rgba(139,92,246,0.10)` | Selected list row, user message bubble, cloud banner | -| `--accent-fill-strong` | `rgba(139,92,246,0.18)` | Type pills, emphasis chips | -| `--accent-border` | `rgba(139,92,246,0.25)` | Expanded-card and banner borders | -| `--accent-border-strong` | `rgba(139,92,246,0.40)` | Resize-handle hover, drag indicators | -| `--accent-halo` | `rgba(167,139,250,0.35)` | Canvas selection halo | - -The website notes name the primary accent violet-600 `#7c3aed`; the transcript fragments used -the same hex under a different name. **No conflict — one value.** The 500/400/300 steps are the -same Tailwind violet family and are kept as a ramp rather than flattened, because dark surfaces -need a lighter step for text and borders than for fills. - ---- - -## 3. Colour — status - -**Resolution:** the transcript used the 400-level of each Tailwind status family (tuned for dark -backgrounds); the website notes used the 500/600-level (tuned for light). Both are correct for -their theme, so each status is one semantic token with two theme values rather than two -competing tokens. - -| Token | Dark theme | Light theme | Meaning | -|---|---|---|---| -| `--status-success` | `#34D399` (emerald-400) | `#10B981` (emerald-500) | Passed, connected, sourced, clean | -| `--status-warning` | `#FBBF24` (amber-400) | `#F59E0B` (amber-500) | Needs attention, unsigned, extended part | -| `--status-danger` | `#F87171` (red-400) | `#DC2626` (red-600) | Error, destructive action, blocking violation | -| `--status-neutral` | `#6B7280` (gray-500) | `#6B7280` | Not run, unknown, disabled | - -**Second resolution:** the transcript contained two greens — `#34D399` and `#5DCAA5` — used -interchangeably for success and for "medium relevance". `#34D399` is the single success token. -`#5DCAA5` survives **only** as the ground-net colour in §6, where it is a domain colour, not a -status. - -Relevance tiers, used by search-result and component cards, are expressed in status terms rather -than as a fourth palette: - -| Tier | Token | -|---|---| -| High (≥ 90%) | `--status-success` | -| Medium (60–90%) | `--status-success` at 70% opacity | -| Low (30–60%) | `--status-warning` | -| Poor (< 30%) | `--status-neutral` | - ---- +1. `@theme` — theme-invariant primitives: fonts, type scale, radii, rhythm, canvas and + layer palette, plus the Tailwind scale remaps (see §6). +2. `@theme inline` — semantic `--color-*` names mapped to `var(--…)`, so utilities such as + `bg-surface-panel` resolve per theme. +3. `:root` — light values for every raw variable. +4. `html.dark` — dark values. Dark mode is class-based (`@custom-variant dark`). -## 4. Colour — surfaces - -### 4.1 App chrome - -| Token | Dark | Light | -|---|---|---| -| `--surface-app` | `#020617` (slate-950) | `#F8FAFC` (slate-50) | -| `--surface-panel` | `#0F172A` (slate-900) | `#FFFFFF` | -| `--border-default` | `#1E293B` (slate-800) | `#E2E8F0` (slate-200) | -| `--border-subtle` | `rgba(255,255,255,0.06)` | `rgba(0,0,0,0.06)` | - -### 4.2 Canvas family - -| Token | Value | Use | -|---|---|---| -| `--surface-canvas` | `#0A0E14` | Schematic, PCB and 3D-dark viewport background; also the recessed body of a diagram card | -| `--surface-rail` | `#070A0F` | Side rails, docked-panel header strips | -| `--surface-card` | `#13191F` | Cards, list rows, tool-call blocks, diagram cards | -| `--surface-card-hover` | `#171E26` | Card hover | -| `--surface-input` | `#10141B` | Inputs, recessed wells, collapsed stacked-card headers | - -**Resolution:** three fragments assigned a card background — `#13191F` from the home set, -`#10141B` from the stacked-card and diagram-card sets. `#13191F` is the single card surface. -`#10141B` is retained only as the *recessed* surface (inputs, collapsed headers), which is the -role it was actually playing in those fragments. A card and an input should not share a value. - -### 4.3 Text - -| Token | Value | -|---|---| -| `--text-primary` | `#F3F4F6` | -| `--text-secondary` | `#9CA3AF` | -| `--text-tertiary` | `#6B7280` | -| `--text-disabled` | `#4B5563` | - -Light-theme text inverts against the slate scale; the dark values above are authoritative for -every canvas surface regardless of app theme. - ---- - -## 5. Shape, spacing and motion - -| Token | Value | Use | -|---|---|---| -| `--radius-card` | `10px` | Dashboard cards, chat cards, modals | -| `--radius-control` | `8px` | Buttons, inputs, stacked cards | -| `--radius-pill` | `999px` | Status pills, tags, model pills | -| `--card-header-padding` | `11px 14px` | Stacked-card collapsed header | -| `--card-body-padding` | `14px` | Stacked-card expanded body | -| `--card-gap` | `6px` | Vertical gap in a stacked-card list | - -Shadows are minimal: a subtle elevation for toolbars, a stronger one for modals. Backdrop blur is -used on floating toolbars and overlays. Scrollbars are hidden on tab strips and thin on panels. +Consume tokens through Tailwind utilities (`bg-surface-panel`, `text-text-secondary`, +`border-border-subtle`, `rounded-control`) — not raw hex, and not `slate-*`/`violet-*`. --- -## 6. Domain colours - -These encode meaning from the electronics domain, not visual hierarchy. They are not -interchangeable with the status palette. - -### 6.1 Net classes — schematic - -Three net classes, three colours. The reasoning: a single-colour schematic (the KiCad -convention) is slow to parse, and colour-coding by net class is the single change that most -improves scan speed on a dense sheet. - -| Token | Value | Applies to | -|---|---|---| -| `--net-power` | `#E0573A` | VCC, +5V, +3V3, and other supply nets | -| `--net-ground` | `#5DCAA5` | GND, AGND, and other return nets | -| `--net-signal` | `#94A3B8` | Everything else — the default | -| `--net-bus` | `#FBBF24` | Multi-bit buses | -| `--net-hover` | `#A78BFA` | The net under the cursor or selection | - -Hover behaviour that these tokens exist to support: hovering a wire fades every other net to -about 30% opacity and raises the hovered net to `--net-hover`. This is net tracing, and it is -the reason the hover colour is a distinct token rather than a generic selection colour. - -### 6.2 Schematic canvas +## 1. Semantic tokens -| Token | Value | +| Group | Tokens | |---|---| -| `--schem-bg` | `--surface-canvas` | -| `--grid-major` | `rgba(255,255,255,0.04)` — 100 mil rectangular grid | -| `--grid-minor-dot` | `rgba(255,255,255,0.06)` — 20 mil dot grid | -| `--sel-halo` | `--accent-halo` | -| `--sel-fill` | `--accent-fill-subtle` | +| Surfaces | `surface-app`, `surface-rail`, `surface-panel`, `surface-panel-head`, `surface-section`, `surface-raised`, `surface-control`, `surface-input`, `surface-hover`, `surface-selected`, `surface-canvas-well` | +| Lines | `border`, `border-subtle`, `border-control`, `divider` | +| Text | `text-strong`, `text`, `text-secondary`, `text-tertiary`, `text-disabled`, `text-caps` (uppercase micro-labels) | +| Action | `primary`, `primary-foreground` — neutral, not chromatic | +| Selection | `selection`, `selection-soft` | +| Status | `status-danger`, `status-warning`, `status-success`, `status-info`, `status-neutral`, each with a `-soft` 12–14% fill | +| Net classes | `net-power`, `net-ground`, `net-signal`, `net-bus` | +| Canvas (invariant) | `canvas`, `canvas-board`, `canvas-grid`, `canvas-grid-major`, `canvas-axis`, `canvas-ratsnest`, `canvas-refdes`, `canvas-pad-number` | +| Layers (invariant) | `layer-f-cu`, `layer-in1-cu`, `layer-in2-cu`, `layer-b-cu`, `layer-f-silks`, `layer-b-silks`, `layer-f-mask`, `layer-b-mask`, `layer-f-paste`, `layer-b-paste`, `layer-f-crtyd`, `layer-b-crtyd`, `layer-edge-cuts`, `layer-drill`, `layer-metadata`, plus `--opacity-copper` | -### 6.3 PCB — realistic preview materials +Status, layer and net-class colours are three **non-overlapping** families: a colour never +means "error" in one place and "bottom copper" in another. -**These are the materials of the realistic/soldermask preview render, not the per-layer artwork -palette.** The authoritative per-layer colour palette for the 2D editor — top copper, mid -layers, bottom copper, overlays, mask, paste, board outline, drill, metadata — lives in -`docs/designer/pcb-layer-rendering.md` §6 and is not duplicated here. Where the two disagree (for -example, the board outline is green in the layer palette and yellow in the preview materials), -they are describing different render modes and both are correct within their own mode. +## 2. The accent rule -| Token | Value | -|---|---| -| `--pcb-bg` | `--surface-canvas` | -| `--board-mask` | `#0D4D2C` — default green soldermask | -| `--edge-cuts` | `#D4A017` — board outline in preview mode | -| `--pad-copper` | `#D97757` — exposed pad | -| `--trace-copper` | `#D97757` — may darken slightly relative to pads | -| `--silkscreen` | `#FFFFFF` | -| `--silkscreen-faded` | `rgba(255,255,255,0.4)` — courtyard outlines | -| `--ratsnest` | `#94A3B8` | -| `--drc-warn` | `--status-warning` | -| `--drc-error` | `--status-danger` | -| `--sel-courtyard` | `--accent-400` | - -Default soldermask is green because it is the most common manufactured output. A per-design -colour picker is a backlog item, not a token change. - -### 6.4 3D viewport +**Violet is retired.** Chrome is neutral greys. The single chromatic accent is the +selection colour — `#33d1ff` cyan in dark, `#0891b2` in light — and it is used **only** for +selection, focus rings and net highlight. Primary buttons are neutral (`primary` / +`primary-foreground`), not coloured. -| Token | Value | -|---|---| -| `--3d-bg-dark` | `--surface-canvas` | -| `--3d-bg-light` | `#F5F5F4` | -| `--3d-floor` | `rgba(31,41,55,0.4)` | -| `--heatmap-cold` | `--status-success` — shortest components | -| `--heatmap-mid` | `--status-warning` | -| `--heatmap-hot` | `--status-danger` — tallest components | -| `--enclosure-margin-default` | `1.0` mm | -| `--enclosure-airgap-default` | `1.0` mm | - -The height heatmap reuses the status ramp deliberately: red reads as "tall enough to be a -problem", which is the question the heatmap answers. It is not colourblind-safe; an alternative -perceptual ramp is a backlog item. +## 3. Light and dark -### 6.5 BOM row states +Every semantic token has both values. Light is not an afterthought: the designs were drawn +dark, but `:root` carries the full light column and each screen must be checked with the +`.dark` class removed. Only the canvas and layer palettes are theme-invariant — the PCB +canvas is always dark, because the layer palette is chosen against black. -Row tints are near-transparent so the table still reads as a table. The accent is the left rule -and the status text. +## 4. Type scale -| State | Row tint | Accent | +| Token | Size / line-height | Typical use | |---|---|---| -| Sourced | `rgba(52,211,153,0.10)` | `--status-success` | -| Suggested | `rgba(139,92,246,0.07)` | `--accent-400` | -| Extended part | `rgba(251,191,36,0.04)` | `--status-warning` | -| Critical / unsourced | `rgba(248,113,113,0.04)` | `--status-danger` | -| Do not populate | `rgba(255,255,255,0.02)` | `--status-neutral` | - ---- - -## 7. The always-dark canvas rule - -**The PCB canvas is always dark, regardless of the app theme.** - -This is a product rule, not a styling preference. PCB artwork is a set of saturated, -high-contrast layer colours designed to be distinguishable from one another; those colours are -chosen against a dark background and lose their separation against a light one. Inverting the -canvas with the app theme would mean maintaining a second layer palette that is worse. - -Consequences: - -| Surface | Theme behaviour | -|---|---| -| PCB canvas | Always dark | -| 3D viewport | Has its own token set with an explicit light preset (`--3d-bg-light`), selected by scene, not by app theme | -| Schematic canvas | Follows the app theme; the values in §6.2 are its dark-theme values | -| Library | Follows the app theme | -| App chrome | Follows the app theme | - ---- - -## 8. Typography - -| Property | Value | -|---|---| -| Font stack | `Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif` | -| Monospace | Used for identifiers, part numbers, net names, coordinates and latency values | -| Scale | Small sizes dominant. The UI is dense by design; `text-xs` and `text-sm` carry most content | - -Monospace is a semantic choice, not decoration: it marks a string as a machine identifier the -user may need to copy or compare character by character. - ---- - -## 9. Icons and primitives - -| Concern | Choice | -|---|---| -| Icon set | Lucide | -| Interactive primitives | Radix — dialog, context menu, tabs, scroll area — wrapped in local UI components | - -Provider and vendor marks are deliberately **not** used as icons; a Lucide icon that suggests the -provider type is used instead, to avoid trademark obligations on third-party logos. - ---- - -## 10. Component-specific tokens - -### 10.1 Chat - -| Token | Value | -|---|---| -| `--bubble-user-bg` | `--accent-fill` | -| `--bubble-user-bd` | `rgba(139,92,246,0.20)` | -| `--bubble-user-radius` | `10px 10px 2px 10px` — tail at bottom right | -| `--card-pending-bd` | `rgba(139,92,246,0.20)` | -| `--card-ready-bd` | `rgba(52,211,153,0.25)` | -| `--card-applied-bd` | `rgba(139,92,246,0.10)` — faded | -| `--badge-best-match-bg` | `rgba(52,211,153,0.12)` | -| `--badge-best-match-text` | `--status-success` | - -Proposal card borders encode state: pending is accent, ready is success, applied fades. The card -does not change fill, only its rule — an applied proposal stays readable as history. - -### 10.2 Diagram cards - -| Token | Value | -|---|---| -| `--diagram-card-bg` | `--surface-card` | -| `--diagram-card-bd` | `--border-subtle` | -| `--diagram-body-bg` | `--surface-canvas` | -| `--diagram-type-pill-bg` | `--accent-fill-strong` | -| `--diagram-type-pill-text` | `--accent-300` | -| `--diagram-node-bg` | `--surface-card` | -| `--diagram-node-bd` | `--accent-400` | -| `--diagram-arrow` | `--text-secondary` | -| `--diagram-arrow-yes` | `--status-success` | -| `--diagram-arrow-no` | `--status-warning` | -| `--diagram-arrow-err` | `--status-danger` | - -Categorical series colour, for pie and multi-series diagrams, in order: -`--accent-600`, `--status-success`, `--status-warning`, `--status-danger`, `#94A3B8`. - -### 10.3 Stacked card - -| Token | Value | -|---|---| -| `--card-collapsed-bg` | `--surface-input` | -| `--card-collapsed-bd` | `--border-subtle` | -| `--card-expanded-bg` | `--accent-fill-subtle` | -| `--card-expanded-bd` | `--accent-border` | -| `--card-warning-bg` | `rgba(251,191,36,0.04)` | -| `--card-warning-bd` | `rgba(251,191,36,0.20)` | - -### 10.4 Docked panel - -| Token | Value | -|---|---| -| `--docked-chat-width-default` | `380px` | -| `--docked-chat-width-min` | `280px` | -| `--docked-chat-width-max` | `600px` | -| `--resize-handle-width` | `4px` | -| `--resize-handle-bg` | `--surface-rail` | -| `--resize-handle-bg-hover` | `--accent-border-strong` | -| `--panel-header-bg` | `--surface-rail` | -| `--panel-subheader-bg` | `rgba(0,0,0,0.10)` | -| `--panel-header-pad` | `7px 10px` | -| `--panel-subheader-pad` | `6px 10px` | -| `--card-narrow-symbol-size` | `30px × 24px` | -| `--card-narrow-padding` | `7px 9px` | -| `--card-narrow-gap` | `5px` | - -The behavioural contract these serve — bounds, double-click snap, per-design persistence, and -the 480 px reflow breakpoint — is in `docs/assistant/chat-ui-spec.md` §12. - ---- - -## 11. Summary of resolutions - -| Disagreement | Resolution | -|---|---| -| Two dark backgrounds: `#020617` (slate-950) and `#0A0E14` | Both kept, with distinct scopes: slate-950 for app chrome, `#0A0E14` for canvas surfaces. This is a structural split, not a conflict. | -| Two greens for success: `#34D399` and `#5DCAA5` | `#34D399` is the single success token. `#5DCAA5` survives only as the ground-net domain colour. | -| Status colours at 400-level vs 500/600-level | One semantic token per status, with a dark value and a light value. | -| Two card backgrounds: `#13191F` and `#10141B` | `#13191F` is the card surface. `#10141B` is the recessed/input surface. | -| Board outline green (layer palette) vs yellow (preview materials) | Different render modes; both correct. The layer palette is authoritative for the 2D editor and lives in `docs/designer/pcb-layer-rendering.md`. | -| Purple named variously accent-purple, violet-600, `#8B5CF6`, `#A78BFA` | One accent ramp, 600/500/400/300, plus named alpha fills. | +| `text-2xs` | 10 / 14 | uppercase micro-labels, status bar | +| `text-xs` | 11 / 15 | table rows, property values, most chrome | +| `text-sm` | 12 / 16 | body default | +| `text-base` | 13 / 18 | panel titles | +| `text-lg` | 15 / 20 | screen headings | +| `text-xl` | 20 / 26 | empty states | + +Fonts are **IBM Plex Sans** and **IBM Plex Mono**, bundled via `@fontsource` and imported in +`main.tsx` — Electron runs offline, so nothing is fetched from Google Fonts. Mono is +semantic: it marks a machine identifier (refdes, MPN, net name, coordinate). `body` sets +12px and `font-variant-numeric: tabular-nums` globally so columns of numbers align. + +## 5. Radii and rhythm + +Radii: `radius-none` 0 (docked panels, rows, tabs), `radius-control` 2px (buttons, inputs, +chips), `radius-float` 3px (menus, tooltips, HUDs), `radius-pill` 999px (status pills only). +`radius-card` is kept at 2px as a compatibility alias. + +Rhythm (`--spacing-*`, usable as `h-row`, `h-toolbar`, …): `row` 22px, `row-lg` 26px, +`panel-head` 24px, `toolbar` 30px, `tabbar` 34px, `statusbar` 22px, `rail` 80px. + +## 6. Compatibility layer (stopgap — remove it) + +The redesign lands on a codebase with ~4,000 `slate-*` and ~700 `violet-*` class usages. +Two shims keep those files coherent until they are migrated: + +- **Aliases** in `@theme inline`: `surface-card` → `surface-panel`, `surface-card-hover` → + `surface-hover`, `text-primary` → `text-strong`, `accent` → `selection`, `accent-soft` → + `selection-soft`, `accent-text` → `text-strong`, plus the `status-*-soft` and `net-*` + names that predate this system. +- **Tailwind scale remaps** in `@theme`: `--color-slate-50…950` becomes a neutral grey ramp, + `--color-violet-50…950` becomes a neutral "active" ramp, and `--radius-sm/md/lg/xl` are + flattened to 2px with `2xl/3xl` at 3px (`rounded-full` still rounds, for dots and + spinners). + +Both are **documented stopgaps**, not API. New code uses the semantic names. The remaining +migration (Assistant, Knowledge, Settings, import wizard, 3D) is tracked as a follow-up; when +it lands, delete the remaps. + +## 7. Canvas palette + +The `canvas-*` and `layer-*` tokens above are the design's values, but the 2D/3D canvases do +**not** read them yet: the renderer palette is owned by the external package +`@openpcb/r3f-eda-canvas` (`canvasTheme.ts` — `SCHEMATIC_DARK`, `PREVIEW_DARK`, +`PCB_CANVAS_TOKENS`, `PCB_LAYER_COLORS`, `PCB_TRACE_COLORS`), and `EdaCanvas` wraps its own +`CanvasThemeProvider(mode)`. Aligning that package with these values, then bumping the +dependency, is a follow-up. diff --git a/docs/design/ui-backlog.md b/docs/design/ui-backlog.md index d073b20d..1bf4a236 100644 --- a/docs/design/ui-backlog.md +++ b/docs/design/ui-backlog.md @@ -1,5 +1,9 @@ # UI backlog +> **Predates the neutral EDA redesign (2026-09).** The mockups and token values +> referenced below describe the old violet-on-slate look, not the shipped chrome. +> Where this file and [`design-tokens.md`](design-tokens.md) disagree, design-tokens.md wins. + > Source note: this backlog was distilled from a UI/UX review that took place as a chat > transcript against screen mockups. **The mockups survive.** They are preserved under > [`mockups/`](mockups/): 13 standalone HTML mockups, plus the 17 PNG captures the review was diff --git a/package-lock.json b/package-lock.json index 4060e7fe..ff0ab503 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1965,6 +1965,24 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz", + "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/ibm-plex-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz", + "integrity": "sha512-CbE4CbbEEZJX860XyUiRpsksXIQR8Rp2XDva2VO53NJox9tVNtusrysd2x5YkUEY3ErQ66W1IiiQL8/wihhw5w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -17637,6 +17655,8 @@ "name": "openpcb-core-frontend", "version": "0.1.0-beta.5", "dependencies": { + "@fontsource/ibm-plex-mono": "^5.3.0", + "@fontsource/ibm-plex-sans": "^5.3.0", "@radix-ui/react-context-menu": "^2.2.4", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", diff --git a/src/core/frontend/package.json b/src/core/frontend/package.json index 483cf172..a0efc710 100644 --- a/src/core/frontend/package.json +++ b/src/core/frontend/package.json @@ -12,6 +12,8 @@ "test:watch": "vitest --config vitest.config.ts" }, "dependencies": { + "@fontsource/ibm-plex-mono": "^5.3.0", + "@fontsource/ibm-plex-sans": "^5.3.0", "@radix-ui/react-context-menu": "^2.2.4", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", diff --git a/src/core/frontend/src/index.css b/src/core/frontend/src/index.css index e52798a4..0323d034 100644 --- a/src/core/frontend/src/index.css +++ b/src/core/frontend/src/index.css @@ -4,141 +4,279 @@ @source "../../../shared"; @custom-variant dark (&:where(.dark, .dark *)); +/* + * OpenPCB — neutral EDA design tokens (direction 1c, merged 2a). + * Source of truth for the whole app chrome. Violet is retired from chrome; the + * only chromatic accent is the selection cyan, used for selection, focus rings + * and net highlight. Status, layer and net-class colours are three + * non-overlapping families. See docs/design/design-tokens.md. + */ @theme { - /* Blue-grey slate overrides — light mode tint */ - --color-slate-50: #f0f4fb; - --color-slate-100: #e4eaf4; - --color-slate-200: #d2dded; - - /* Radii (theme-invariant) */ - --radius-card: 14px; - --radius-control: 8px; - --radius-pill: 999px; - - /* Mono font (was missing) */ - --font-mono: - ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", - monospace; + --font-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + + /* Type scale — small sizes dominant, tabular numerals everywhere */ + --text-2xs: 10px; --text-2xs--line-height: 14px; + --text-xs: 11px; --text-xs--line-height: 15px; + --text-sm: 12px; --text-sm--line-height: 16px; + --text-base: 13px; --text-base--line-height: 18px; + --text-lg: 15px; --text-lg--line-height: 20px; + --text-xl: 20px; --text-xl--line-height: 26px; + + /* Shape */ + --radius-none: 0px; /* docked panels, table rows, tabs */ + --radius-control: 2px; /* buttons, inputs, chips */ + --radius-float: 3px; /* menus, tooltips, HUDs */ + --radius-pill: 999px; /* status pills only */ + --radius-card: 2px; /* compat alias — flattened with the rest */ + + /* Tailwind radius scale flattened so unmigrated files lose the pill + geometry. `rounded-full` stays round for dots and spinners. */ + --radius-sm: 2px; + --radius-md: 2px; + --radius-lg: 2px; + --radius-xl: 2px; + --radius-2xl: 3px; + --radius-3xl: 3px; + + /* Rhythm (4 px base) */ + --spacing-row: 22px; /* list / tree / table row */ + --spacing-row-lg: 26px; /* property-grid row with input */ + --spacing-panel-head: 24px; /* panel section header */ + --spacing-toolbar: 30px; + --spacing-tabbar: 34px; + --spacing-statusbar: 22px; + --spacing-rail: 80px; + + /* Tailwind palette remap — documented stopgap so the ~150 not-yet-migrated + files render neutral instead of blue/violet. Migrate call sites to the + semantic tokens above; see docs/design/design-tokens.md. */ + --color-slate-50: #f7f7f8; + --color-slate-100: #ececee; + --color-slate-200: #dcdce0; + --color-slate-300: #c4c4c9; + --color-slate-400: #a8a8ad; + --color-slate-500: #7f7f84; + --color-slate-600: #55555a; + --color-slate-700: #2c2c31; + --color-slate-800: #1c1c1f; + --color-slate-900: #111113; + --color-slate-950: #0c0c0d; + + --color-violet-50: #f0f0f2; + --color-violet-100: #e4e4e7; + --color-violet-200: #d0d0d5; + --color-violet-300: #a8a8ad; + --color-violet-400: #8a8a90; + --color-violet-500: #55555a; + --color-violet-600: #3a3a40; + --color-violet-700: #2c2c31; + --color-violet-800: #1c1c1f; + --color-violet-900: #1c1c1f; + --color-violet-950: #151517; + + /* Canvas — theme-invariant (PCB canvas is always dark) */ + --color-canvas: #08090a; + --color-canvas-board: #121315; + --color-canvas-grid: rgba(255, 255, 255, 0.22); + --color-canvas-grid-major: rgba(255, 255, 255, 0.35); + --color-canvas-axis: #3a3a3a; + --color-canvas-ratsnest: #8a8a8a; + --color-canvas-refdes: #e6e6e6; + --color-canvas-pad-number: #ffffff; + + /* Layer palette (2D editor) */ + --color-layer-f-cu: #d43a3a; + --color-layer-in1-cu: #d9a441; + --color-layer-in2-cu: #3fb8c9; + --color-layer-b-cu: #3a63c4; + --color-layer-f-silks: #f1f1f1; + --color-layer-b-silks: #7dd3fc; + --color-layer-f-mask: #5b3a7a; + --color-layer-b-mask: #3a5a7a; + --color-layer-f-paste: #b8b8c8; + --color-layer-b-paste: #8a8a9a; + --color-layer-f-crtyd: #a78050; + --color-layer-b-crtyd: #604836; + --color-layer-edge-cuts: #b59a4a; + --color-layer-drill: #88e090; + --color-layer-metadata: #a3a3a3; + --opacity-copper: 0.92; } /* - * Semantic design tokens. `inline` makes utilities (e.g. bg-surface-card, + * Semantic colour layer. `inline` makes utilities (e.g. bg-surface-panel, * text-status-warning) compile to var(--) so they resolve per theme - * from the :root / html.dark definitions below — incremental on top of the - * existing slate theme, no full re-skin. + * from the :root / html.dark blocks below. */ @theme inline { + /* Surfaces */ --color-surface-app: var(--surface-app); --color-surface-rail: var(--surface-rail); - --color-surface-card: var(--surface-card); - --color-surface-card-hover: var(--surface-card-hover); + --color-surface-panel: var(--surface-panel); + --color-surface-panel-head: var(--surface-panel-head); + --color-surface-section: var(--surface-section); + --color-surface-raised: var(--surface-raised); + --color-surface-control: var(--surface-control); --color-surface-input: var(--surface-input); + --color-surface-hover: var(--surface-hover); + --color-surface-selected: var(--surface-selected); + --color-surface-canvas-well: var(--surface-canvas-well); - --color-accent: var(--accent-purple); - --color-accent-soft: var(--accent-purple-soft); - --color-accent-text: var(--accent-purple-text); + /* Lines */ + --color-border: var(--border); + --color-border-subtle: var(--border-subtle); + --color-border-control: var(--border-control); + --color-divider: var(--divider); - --color-status-success: var(--status-success); - --color-status-success-soft: var(--status-success-soft); - --color-status-warning: var(--status-warning); - --color-status-warning-soft: var(--status-warning-soft); + /* Text */ + --color-text-strong: var(--text-strong); + --color-text: var(--text); + --color-text-secondary: var(--text-secondary); + --color-text-tertiary: var(--text-tertiary); + --color-text-disabled: var(--text-disabled); + --color-text-caps: var(--text-caps); + + /* Primary action (neutral, not chromatic) */ + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + /* Selection — the only chromatic accent */ + --color-selection: var(--selection); + --color-selection-soft: var(--selection-soft); + + /* Status */ --color-status-danger: var(--status-danger); --color-status-danger-soft: var(--status-danger-soft); + --color-status-warning: var(--status-warning); + --color-status-warning-soft: var(--status-warning-soft); + --color-status-success: var(--status-success); + --color-status-success-soft: var(--status-success-soft); + --color-status-info: var(--status-info); + --color-status-info-soft: var(--status-info-soft); --color-status-neutral: var(--status-neutral); --color-status-neutral-soft: var(--status-neutral-soft); - --color-text-primary: var(--text-primary); - --color-text-secondary: var(--text-secondary); - --color-text-tertiary: var(--text-tertiary); - --color-text-disabled: var(--text-disabled); - + /* Net classes (domain colours) */ --color-net-power: var(--net-power); --color-net-ground: var(--net-ground); --color-net-signal: var(--net-signal); + --color-net-bus: var(--net-bus); + + /* Compatibility aliases — keep pre-redesign call sites compiling and + coherent until they are migrated to the names above. */ + --color-surface-card: var(--surface-panel); + --color-surface-card-hover: var(--surface-hover); + --color-text-primary: var(--text-strong); + --color-accent: var(--selection); + --color-accent-soft: var(--selection-soft); + --color-accent-text: var(--text-strong); } :root { - font-family: - Inter, - system-ui, - -apple-system, - Segoe UI, - Roboto, - sans-serif; - color: #111827; - background-color: #f0f4fb; - --color-slate-50: #f0f4fb; - --color-slate-100: #e4eaf4; - --color-slate-200: #d2dded; - - /* Semantic tokens — light mode (mapped to current light surfaces) */ - --surface-app: #f0f4fb; - --surface-rail: #ffffff; - --surface-card: #ffffff; - --surface-card-hover: #f8fafc; - --surface-input: #f8fafc; - - --accent-purple: #7c3aed; - --accent-purple-soft: rgba(124, 58, 237, 0.1); - --accent-purple-text: #6d28d9; - - /* Darker status hues so contrast holds on white */ - --status-success: #059669; - --status-success-soft: rgba(5, 150, 105, 0.1); - --status-warning: #b45309; - --status-warning-soft: rgba(180, 83, 9, 0.1); - --status-danger: #dc2626; - --status-danger-soft: rgba(220, 38, 38, 0.08); - --status-neutral: #64748b; - --status-neutral-soft: rgba(100, 116, 139, 0.1); - - --text-primary: #111827; - --text-secondary: #475569; - --text-tertiary: #64748b; - --text-disabled: #94a3b8; + color-scheme: light; + color: var(--text); + background-color: var(--surface-app); + font-family: var(--font-sans); - --net-power: #e0573a; - --net-ground: #5dcaa5; - --net-signal: #94a3b8; + --surface-app: #f2f2f3; + --surface-rail: #ececee; + --surface-panel: #f7f7f8; + --surface-panel-head: #ebebed; + --surface-section: #ebebed; + --surface-raised: #e2e2e5; + --surface-control: #d6d6da; + --surface-input: #ffffff; + --surface-hover: #e6e6e9; + --surface-selected: #dcdce0; + --surface-canvas-well: #08090a; + + --border: #d4d4d8; + --border-subtle: #e6e6e9; + --border-control: #cfcfd4; + --divider: #dcdce0; + + --text-strong: #111114; + --text: #26262b; + --text-secondary: #55555c; + --text-tertiary: #7f7f86; + --text-disabled: #a8a8ae; + --text-caps: #6f6f76; + + --primary: #111114; + --primary-foreground: #f5f5f5; + + --selection: #0891b2; + --selection-soft: rgba(8, 145, 178, 0.1); + + --status-danger: #c2402f; + --status-danger-soft: rgba(194, 64, 47, 0.12); + --status-warning: #9a6b12; + --status-warning-soft: rgba(154, 107, 18, 0.12); + --status-success: #2f7a3d; + --status-success-soft: rgba(47, 122, 61, 0.12); + --status-info: #2f5f9a; + --status-info-soft: rgba(47, 95, 154, 0.12); + --status-neutral: #7f7f86; + --status-neutral-soft: rgba(127, 127, 134, 0.14); + + --net-power: #c94a2f; + --net-ground: #2f8a6a; + --net-signal: #55606e; + --net-bus: #b07d12; } html.dark { - color: #e2e8f0; - background-color: #0f1520; - --color-slate-950: #0f1520; - --color-slate-900: #151e30; - --color-slate-800: #1a2438; - --color-slate-700: #2c3a55; - --color-slate-600: #3d5070; - - /* Semantic tokens — dark mode (proposal palette, mapped to slate theme) */ - --surface-app: #0f1520; - --surface-rail: #0b1018; - --surface-card: #151e30; - --surface-card-hover: #1a2438; - --surface-input: #10141b; - - --accent-purple: #7c3aed; - --accent-purple-soft: rgba(139, 92, 246, 0.15); - --accent-purple-text: #c4b5fd; - - --status-success: #34d399; - --status-success-soft: rgba(52, 211, 153, 0.1); - --status-warning: #fbbf24; - --status-warning-soft: rgba(251, 191, 36, 0.1); - --status-danger: #f87171; - --status-danger-soft: rgba(248, 113, 113, 0.08); - --status-neutral: #6b7280; - --status-neutral-soft: rgba(107, 114, 128, 0.12); - - --text-primary: #f3f4f6; - --text-secondary: #9ca3af; - --text-tertiary: #6b7280; - --text-disabled: #4b5563; + color-scheme: dark; + color: var(--text); + background-color: var(--surface-app); + font-family: var(--font-sans); + + --surface-app: #0c0c0d; + --surface-rail: #0f0f10; + --surface-panel: #111113; + --surface-panel-head: #151517; + --surface-section: #151517; + --surface-raised: #1c1c1f; + --surface-control: #2c2c31; + --surface-input: #0c0c0d; + --surface-hover: #1c1c1f; + --surface-selected: #26262b; + --surface-canvas-well: #08090a; + + --border: #1e1e21; + --border-subtle: #16161a; + --border-control: #2a2a2e; + --divider: #232326; + + --text-strong: #f5f5f5; + --text: #d9d9d9; + --text-secondary: #a8a8ad; + --text-tertiary: #7f7f84; + --text-disabled: #55555a; + --text-caps: #6a6a70; + + --primary: #e8e8e8; + --primary-foreground: #111114; + + --selection: #33d1ff; + --selection-soft: rgba(51, 209, 255, 0.08); + + --status-danger: #e0705f; + --status-danger-soft: rgba(224, 112, 95, 0.12); + --status-warning: #d9a441; + --status-warning-soft: rgba(217, 164, 65, 0.12); + --status-success: #6fbf7a; + --status-success-soft: rgba(111, 191, 122, 0.12); + --status-info: #7aa7d9; + --status-info-soft: rgba(122, 167, 217, 0.12); + --status-neutral: #7f7f84; + --status-neutral-soft: rgba(127, 127, 132, 0.14); --net-power: #e0573a; --net-ground: #5dcaa5; --net-signal: #94a3b8; + --net-bus: #fbbf24; } html, @@ -150,8 +288,12 @@ body, } body { - background: inherit; - color: inherit; + font-family: var(--font-sans); + font-size: 12px; + font-variant-numeric: tabular-nums; + background: var(--surface-app); + color: var(--text); + -webkit-font-smoothing: antialiased; } /* TipTap placeholder — Placeholder extension tags the empty first node with diff --git a/src/core/frontend/src/main.tsx b/src/core/frontend/src/main.tsx index 53e70c0b..07ae7cb5 100644 --- a/src/core/frontend/src/main.tsx +++ b/src/core/frontend/src/main.tsx @@ -2,6 +2,15 @@ import { initRendererSentry } from "./sentry"; import { createRoot } from "react-dom/client"; import { App } from "./App"; + +// Fonts are bundled, not fetched — Electron runs offline. +// Imported before index.css so the @font-face rules land before the +// token layer that references them. +import "@fontsource/ibm-plex-sans/400.css"; +import "@fontsource/ibm-plex-sans/500.css"; +import "@fontsource/ibm-plex-sans/600.css"; +import "@fontsource/ibm-plex-mono/400.css"; +import "@fontsource/ibm-plex-mono/500.css"; import "./index.css"; // Sentry is opt-in. Read the preference before init; renderer events route via From ea10c37041e2a1864c56c08cc8faede72c593c3d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 02:54:11 +0000 Subject: [PATCH 03/12] T2: shared UI primitives on the neutral token sheet Restyle button/card/chip/pill/icon-button/tabs/tooltip/menus/textarea to the design tokens (22px controls, 2px radii, no violet) with additive-only API changes, and add the shared building blocks the redesigned screens use: PanelSectionHeader, PropertyGrid/PropertyRow, TableHeaderRow/TableRow, SegmentedControl, SearchField, Checkbox, StatusDot, SeverityDiamond, DockTabs, StatusBar/StatusSegment, Toolbar/ToolbarButton. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Np3Vgtux8ZgiKcc75wYrZE --- src/shared/frontend/ui/button.tsx | 39 +++++-- src/shared/frontend/ui/card.tsx | 6 +- src/shared/frontend/ui/checkbox.tsx | 96 ++++++++++++++++ src/shared/frontend/ui/chip.tsx | 13 +-- src/shared/frontend/ui/context-menu.tsx | 25 +++-- src/shared/frontend/ui/data-table.tsx | 56 ++++++++++ src/shared/frontend/ui/dock-tabs.tsx | 81 ++++++++++++++ src/shared/frontend/ui/dropdown-menu.tsx | 29 ++++- src/shared/frontend/ui/icon-button.tsx | 32 +++++- src/shared/frontend/ui/index.ts | 60 +++++++++- .../frontend/ui/panel-section-header.tsx | 103 ++++++++++++++++++ src/shared/frontend/ui/pill.tsx | 36 ++++-- src/shared/frontend/ui/property-grid.tsx | 69 ++++++++++++ src/shared/frontend/ui/relevance-bar.tsx | 8 +- src/shared/frontend/ui/search-field.tsx | 46 ++++++++ src/shared/frontend/ui/segmented-control.tsx | 74 +++++++++++++ src/shared/frontend/ui/severity-diamond.tsx | 38 +++++++ src/shared/frontend/ui/stacked-card.tsx | 15 +-- src/shared/frontend/ui/status-bar.tsx | 69 ++++++++++++ src/shared/frontend/ui/status-dot.tsx | 38 +++++++ src/shared/frontend/ui/tabs.tsx | 10 +- src/shared/frontend/ui/textarea.tsx | 3 +- src/shared/frontend/ui/toolbar.tsx | 89 +++++++++++++++ src/shared/frontend/ui/tooltip.tsx | 2 +- 24 files changed, 971 insertions(+), 66 deletions(-) create mode 100644 src/shared/frontend/ui/checkbox.tsx create mode 100644 src/shared/frontend/ui/data-table.tsx create mode 100644 src/shared/frontend/ui/dock-tabs.tsx create mode 100644 src/shared/frontend/ui/panel-section-header.tsx create mode 100644 src/shared/frontend/ui/property-grid.tsx create mode 100644 src/shared/frontend/ui/search-field.tsx create mode 100644 src/shared/frontend/ui/segmented-control.tsx create mode 100644 src/shared/frontend/ui/severity-diamond.tsx create mode 100644 src/shared/frontend/ui/status-bar.tsx create mode 100644 src/shared/frontend/ui/status-dot.tsx create mode 100644 src/shared/frontend/ui/toolbar.tsx diff --git a/src/shared/frontend/ui/button.tsx b/src/shared/frontend/ui/button.tsx index a0d4897c..b742045c 100644 --- a/src/shared/frontend/ui/button.tsx +++ b/src/shared/frontend/ui/button.tsx @@ -1,23 +1,40 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -type ButtonVariant = "primary" | "secondary" | "ghost" | "danger"; -type ButtonSize = "sm" | "md"; +/** + * `outline` is an alias of `secondary` and `destructive` an alias of `danger` + * (kept so both naming conventions in the codebase compile). + */ +export type ButtonVariant = + | "primary" + | "secondary" + | "outline" + | "ghost" + | "danger" + | "destructive"; + +/** `md` is the historical name of the 22px default size. */ +export type ButtonSize = "sm" | "md" | "default" | "lg"; const VARIANTS: Record = { - primary: - "bg-violet-600 text-white hover:bg-violet-700 dark:bg-violet-700 dark:hover:bg-violet-600", + primary: "bg-primary text-primary-foreground font-medium hover:opacity-90", secondary: - "border border-slate-300 bg-transparent text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800", + "border border-border-control bg-transparent text-text hover:bg-surface-hover hover:text-text-strong", + outline: + "border border-border-control bg-transparent text-text hover:bg-surface-hover hover:text-text-strong", ghost: - "bg-transparent text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800", + "bg-transparent text-text-secondary hover:bg-surface-hover hover:text-text-strong", danger: - "bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600", + "bg-status-danger text-primary-foreground font-medium hover:opacity-90", + destructive: + "bg-status-danger text-primary-foreground font-medium hover:opacity-90", }; const SIZES: Record = { - sm: "h-8 gap-1.5 px-3 text-xs", - md: "h-9 gap-2 px-4 text-sm", + sm: "h-5 gap-1 px-2 text-xs", + md: "h-[22px] gap-1.5 px-[10px] text-xs", + default: "h-[22px] gap-1.5 px-[10px] text-xs", + lg: "h-7 gap-2 px-3 text-sm", }; export interface ButtonProps extends React.ButtonHTMLAttributes { @@ -34,7 +51,9 @@ export const Button = React.forwardRef( + ); + })} + {trailing ? ( +
{trailing}
+ ) : null} +
+ ); +} diff --git a/src/shared/frontend/ui/dropdown-menu.tsx b/src/shared/frontend/ui/dropdown-menu.tsx index 275d654e..d0f917a8 100644 --- a/src/shared/frontend/ui/dropdown-menu.tsx +++ b/src/shared/frontend/ui/dropdown-menu.tsx @@ -4,18 +4,35 @@ import { cn } from "@/lib/utils"; export const DropdownMenu = DropdownMenuPrimitive.Root; export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; +export const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + export const DropdownMenuSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); DropdownMenuSeparator.displayName = "DropdownMenuSeparator"; +export const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = "DropdownMenuLabel"; + export const DropdownMenuContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -26,8 +43,7 @@ export const DropdownMenuContent = React.forwardRef< sideOffset={sideOffset} align={align} className={cn( - "z-50 min-w-[11rem] overflow-hidden rounded-control border border-slate-200 bg-white p-1 text-sm text-slate-700 shadow-lg", - "dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200", + "z-50 min-w-[11rem] overflow-hidden rounded-float border border-border bg-surface-raised p-1 text-xs text-text shadow-lg", className, )} {...props} @@ -49,11 +65,12 @@ export const DropdownMenuItem = React.forwardRef< ( - ({ label, size = "md", className, children, ...props }, ref) => { + ( + { + label, + size = "md", + variant = "outline", + active, + tooltip = true, + className, + children, + ...props + }, + ref, + ) => { const button = ( ); + if (!tooltip) return button; return {button}; }, ); diff --git a/src/shared/frontend/ui/index.ts b/src/shared/frontend/ui/index.ts index 4138693b..ae7ce661 100644 --- a/src/shared/frontend/ui/index.ts +++ b/src/shared/frontend/ui/index.ts @@ -1,4 +1,4 @@ -export { Button, type ButtonProps } from "./button"; +export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from "./button"; export { Card, type CardProps } from "./card"; export { Pill, StatusPill, type PillProps, type PillTone } from "./pill"; export { Chip, type ChipProps } from "./chip"; @@ -10,14 +10,26 @@ export { TooltipTrigger, TooltipContent, } from "./tooltip"; +export { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs"; export { DropdownMenu, DropdownMenuTrigger, + DropdownMenuPortal, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, DropdownMenuSeparator, type DropdownMenuItemProps, } from "./dropdown-menu"; +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuPortal, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + type ContextMenuItemProps, +} from "./context-menu"; export { RelevanceBar, relevanceTier } from "./relevance-bar"; export { StackedCard, @@ -25,3 +37,49 @@ export { type StackedCardTone, } from "./stacked-card"; export { Textarea, type TextareaProps } from "./textarea"; + +/* Neutral-EDA building blocks (design D2 §3/§6/§7/§9, design D3 §5). */ +export { + PanelSectionHeader, + type PanelSectionHeaderProps, +} from "./panel-section-header"; +export { + PropertyGrid, + PropertyRow, + type PropertyGridProps, + type PropertyRowProps, +} from "./property-grid"; +export { + TableHeaderRow, + TableRow, + type TableHeaderRowProps, + type TableRowProps, +} from "./data-table"; +export { + SegmentedControl, + type SegmentedControlProps, + type SegmentedOption, +} from "./segmented-control"; +export { SearchField, type SearchFieldProps } from "./search-field"; +export { Checkbox, type CheckboxProps } from "./checkbox"; +export { StatusDot, type StatusDotProps, type StatusTone } from "./status-dot"; +export { + SeverityDiamond, + type SeverityDiamondProps, + type SeverityLevel, +} from "./severity-diamond"; +export { DockTabs, type DockTabsProps, type DockTabItem } from "./dock-tabs"; +export { + StatusBar, + StatusSegment, + type StatusBarProps, + type StatusSegmentProps, +} from "./status-bar"; +export { + Toolbar, + ToolbarButton, + ToolbarSeparator, + ToolbarSpacer, + type ToolbarProps, + type ToolbarButtonProps, +} from "./toolbar"; diff --git a/src/shared/frontend/ui/panel-section-header.tsx b/src/shared/frontend/ui/panel-section-header.tsx new file mode 100644 index 00000000..7bad7a6b --- /dev/null +++ b/src/shared/frontend/ui/panel-section-header.tsx @@ -0,0 +1,103 @@ +import * as React from "react"; +import { ChevronDown } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export interface PanelSectionHeaderProps + extends Omit, "title"> { + title: React.ReactNode; + /** Right-aligned count badge (mono). */ + count?: number | string; + /** Extra controls rendered after the count. */ + trailing?: React.ReactNode; + /** When defined together with `onToggle`, renders a chevron affordance. */ + collapsed?: boolean; + onToggle?: () => void; + /** + * `default` = 24px panel section header. + * `uppercase` = 22px 10px-uppercase property-section header. + */ + variant?: "default" | "uppercase"; +} + +/** + * Docked panel section header (design D2 §6) and, via `variant="uppercase"`, + * the property-grid section header (design D2 §7). + */ +export const PanelSectionHeader = React.forwardRef< + HTMLDivElement, + PanelSectionHeaderProps +>( + ( + { + title, + count, + trailing, + collapsed, + onToggle, + variant = "default", + className, + children, + ...props + }, + ref, + ) => { + const uppercase = variant === "uppercase"; + const titleNode = ( + + {title} + + ); + const chevron = onToggle ? ( +