diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..0327761f --- /dev/null +++ b/PLAN.md @@ -0,0 +1,472 @@ +# PLAN — OpenPCB UI refactor to the neutral EDA design (Claude Design handoff) + +status: done +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) +- 2026-09-05 T1 tokens/fonts/docs — reviewed (index.css read in full, tsc 0, 326 tests, build ok) — committed ab0c0f4. +- 2026-09-05 T2 shared primitives — reviewed (toolbar/data-table/dock-tabs/status-bar/property-grid/button read; additive API only; tsc 0) — committed ea10c37. +- 2026-09-05 wave 2 launched: T3 (sonnet), T4 (opus, critical), T6 (opus), T7 (opus) in the main checkout, no worktrees. +- 2026-09-05 T3 app shell — reviewed (rail/title bar diffs read; tsc 0) — committed a2e5265. +- 2026-09-05 T7 BOM + Home — reviewed (HomeSidebar, layout, BomRow read; "New design" markup unchanged for e2e) — committed 186c592. Omitted for lack of data: BOM Description/Stock, Home Import KiCad/version/tags/activity. +- 2026-09-05 T6 Library — reviewed; fixed inline: e2e specs now open components via `library-component-row-*` + dblclick (table is default); card preview SVG pinned to theme=dark (wells are always dark) — committed fa3e098. +- 2026-09-05 FINDING: root `npm run typecheck` (tsc -b) fails on `master` too (36 pre-existing errors in assistant/**, library/backend/**, core/backend/tests — `AiProviderKind` lacks "openpcb-cloud"). Gate for this branch = no NEW errors: `tsc -p tsconfig.modules.json` filtered to designer/library/shared/core frontend must be empty, plus frontend tsc + vitest + build. +- 2026-09-05 T4 designer/PCB — critical review by reviewer agent + own read. Fixed before commit: cursor readout moved to `pcb-cursor-store` (Space re-rendered per pointer move); DRC full view height/scroll; selection opens a closed dock and never leaves Assistant; single trace/via selection state; "Fit board" name; ToolbarButton `title` override (Undo/Redo hotkey tooltips) — committed 0ef7b65. Deferred to T8a: dead PcbTopToolbar props, `boardPanelTarget`, raw palette classes in PcbCanvas overlays. Note: `pcb.sidebar.board` localStorage key is now unused (Board moved to the Properties dock). +- 2026-09-05 launched T5 (opus) + T8a PCB cleanup (sonnet). +- 2026-09-05 T8a PCB cleanup — committed 129138b. T5 schematic — reviewed (toolbar names, Space diff) — committed a593e12 together with a Library fix: Mount column removed (list DTO has no mount type; it rendered "—" on every row — found via screenshots). +- 2026-09-05 VISUAL VERIFICATION: backend (bun) + Vite booted in the container; Playwright captured Home, Library (+selected row), Schematic, PCB (+Route param row), BOM and DRC in dark and light. Structure matches the mocks; light theme has full parity; PCB canvas stays dark. Known in-container artefact: `troika-three-text` fetches Unicode font data from jsdelivr, which the sandbox proxy blocks → WebGL text-dependent previews show "Loading preview…" here (pre-existing, environmental). +- 2026-09-05 FINAL GATES: frontend tsc 0; `tsc -p tsconfig.modules.json` has no errors under core/designer/library/shared frontend (only the 36 pre-existing master errors); vitest 48 files / 341 tests; `npm run build:frontend` ok. Raw palette classes remaining: core 20 (non-screen), designer 179 (KicadProjectImportWizard, comments/*, CloudDesignBrowser), library 862 (all import-wizard), assistant 655, knowledge 59, tasks 12, shared 41 (markdown) — all in the documented remap-stopgap bucket. + +## Follow-ups discovered during the run (in addition to §9) +- Status-bar hint segment is empty on PCB; wire an `onHintChange` from PcbCanvas and retire the floating `RouteHintStrip`/`MeasureHintStrip`/`SketchHintStrip` overlays (currently duplicated with the parameter row). +- Schematic status bar lacks cursor X/Y; add `onCursorChange` to SchematicCanvas mirroring the PCB one (`pcb-cursor-store`). +- `lib/net-class.ts#netClassTextClass` still returns emerald/rose/slate classes → move to `text-net-*` tokens. +- `PcbDisambiguationPopup` / `SnapTargetIndicator` per-kind hit colours still include violet (canvas domain colours; revisit with the shared-repo canvas palette). +- `PcbPropertiesPanel` reads pads from `placement.footprint.preview.pads`; placements without a preview snapshot show an empty pads table. Footprint "Value" needs a projection field. +- `bundleRoutingEnabled` feature-flag hook in PcbCanvas is now unused. +- Library: `onPlace` handlers on cards/preview pane are unwired (no place action in the Library space); Pins column needs `padCount` on the list DTO; sortKey "recent" still a no-op. +- BOM: Description/Stock/Alternates need `BomLine` fields. Home: KiCad import entry point from core, app version constant, `DesignCard` list variant now dead. +- e2e `pcb-routing.spec.ts` expects "Tune (U)"/"Bundle" toolbar buttons that were already hidden before this work. +- `pcb.sidebar.board` localStorage key is no longer read (Board moved into the Properties dock). +- Settings panels keep raw amber/red/emerald banner colours (token re-skin only covered slate/violet). 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/AppShell.tsx b/src/core/frontend/src/AppShell.tsx index 1b35077d..c8c50e8d 100644 --- a/src/core/frontend/src/AppShell.tsx +++ b/src/core/frontend/src/AppShell.tsx @@ -10,7 +10,7 @@ import { openContextMenu } from "@shared/frontend/context-menu"; function LoadingScreen() { return ( -
+
Initializing OpenPCB...
); @@ -18,8 +18,8 @@ function LoadingScreen() { function ErrorScreen({ message }: { message: string }) { return ( -
-
+
+
{message}
@@ -56,7 +56,7 @@ export function AppShell() { return ( <> -
+
{title && ( -
+
{title}
)} @@ -174,7 +173,7 @@ export function AppContextMenu() {
); } @@ -192,16 +191,16 @@ export function AppContextMenu() { onClick={item.disabled ? undefined : () => handleItemClick(idx)} className={cn( "flex w-full items-center justify-between px-3 py-1.5 text-sm outline-none", - "text-slate-700 dark:text-slate-200", + "text-text", item.disabled && "cursor-not-allowed opacity-50", - !item.disabled && "hover:bg-slate-100 dark:hover:bg-slate-800", - isFocused && !item.disabled && "bg-slate-100 dark:bg-slate-800", - item.destructive && "text-red-600 dark:text-red-400", + !item.disabled && "hover:bg-surface-hover", + isFocused && !item.disabled && "bg-surface-hover", + item.destructive && "text-status-danger", )} > {item.label} {item.shortcut && ( - + {item.shortcut} )} @@ -212,7 +211,7 @@ export function AppContextMenu() { return (
{group.label && ( -
+
{group.label}
)} diff --git a/src/core/frontend/src/components/LeftSidebar.tsx b/src/core/frontend/src/components/LeftSidebar.tsx index 50ba6d1d..acc6706a 100644 --- a/src/core/frontend/src/components/LeftSidebar.tsx +++ b/src/core/frontend/src/components/LeftSidebar.tsx @@ -11,19 +11,11 @@ interface LeftSidebarProps { } function navButtonClass(active: boolean): string { - return `flex w-16 cursor-pointer flex-col items-center justify-center rounded-2xl border border-transparent py-2 transition-colors ${ - active - ? "border-violet-600 bg-violet-100 text-violet-600 dark:border-violet-400 dark:bg-violet-900/40 dark:text-violet-300" - : "text-slate-400 hover:bg-slate-100 hover:text-slate-600 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200" - }`; + return `flex ${active ? "w-16 bg-surface-hover text-text-strong" : "w-[72px] text-text-tertiary hover:bg-surface-hover/60 hover:text-text"} cursor-pointer flex-col items-center gap-1 rounded-control py-2 pb-1.5 transition-colors`; } function navLabelClass(active: boolean): string { - return `mt-1 text-xs leading-tight text-center ${ - active - ? "font-medium text-violet-600 dark:text-violet-300" - : "text-slate-500 dark:text-slate-400" - }`; + return `text-2xs leading-tight text-center ${active ? "font-medium" : ""}`; } export function LeftSidebar({ onSettingsClick }: LeftSidebarProps) { @@ -61,7 +53,7 @@ export function LeftSidebar({ onSettingsClick }: LeftSidebarProps) { return ( -