Skip to content

feat(ui): adopt macOS system tokens, shadcn primitives, and a Mail-style window shell - #26

Open
hsusul wants to merge 4 commits into
mainfrom
feat/ui-macos-shell
Open

hsusul wants to merge 4 commits into
mainfrom
feat/ui-macos-shell

Conversation

@hsusul

@hsusul hsusul commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Why

The owner's verdict on the PR 20 UI was that it "looks way too vibe coded", and the follow-up ruling set the direction: Lore should look like a first-party Mac app anchored on Apple Mail, built on shadcn/ui restyled to a spec, with no bottom status bar. This is PR 1 of that redesign (docs/design/lore-design-spec.md v2, local). It lays the foundation every later PR builds on: one token system, system fonts, restyled primitives, and the Mail window shell. Session rows, the reader, settings, the palette, and onboarding only pick up the new tokens here; later PRs redesign them.

What changed

Tokens and theme

  • src/tokens.css defines every spec Section 4 to 6 token for light, dark (system appearance and the data-theme override), and Increase Contrast. Measured macOS values come first; WKWebView then swaps in the -apple-system-* keywords under @supports. Includes the new --red-text and --orange-icon tokens.
  • Tailwind v3 (not v4: v4 needs Safari 16.4 and tauri.conf.json targets macOS 10.15). tailwind.config.ts uses presets: [] and a theme built only from the tokens, so bg-blue-500, p-7, or rounded-xl emit no CSS. Preflight stays on.
  • IBM Plex is removed (@fontsource/*). Sans and mono are the system stacks; nothing is bundled.
  • The pre-redesign component rules in src/styles.css are remapped onto the new tokens. Agent dot colors are gone (spec 17.1).

Primitives (src/components/ui/)

  • Dialog, DropdownMenu, ContextMenu, Select, Tooltip, Command (cmdk), Tabs (as the macOS segmented control), Popover, Toggle, ScrollArea, plus a Button. Each is a React 18 forwardRef wrapper over Radix, restyled to spec 9.3, 9.7, 9.10, 9.12. Each has a behavior test (open, keyboard, close, focus return; a ref-forwarding test for Dialog).
  • src/test-setup.ts shims ResizeObserver, pointer capture, and scrollIntoView for jsdom.

Window shell

  • Source-list sidebar (spec 9.1): All Sessions, Repositories, Folders, Agents, with counts. Selection is Mail's filled inset row, blue while the sidebar has focus and gray otherwise. Arrow keys move the selection. Sections collapse from a hover chevron. Missing repositories are dimmed with "Missing" on hover. Hidden by default below 1000px; ⌃⌘S or the toolbar button toggles it.
  • 52px toolbar split at the column edges (7.1, 18.1): the scope title and "N sessions" over the list, which becomes a scope menu with the same sources and counts when the sidebar is hidden; an empty slot for PR 3's reader actions; then Rescan, the palette button, and the fixed 260px capsule search field. Empty areas carry data-tauri-drag-region. No traffic lights are drawn.
  • Rescan (9.9, 18.8): icon button with tooltip, a spinner for the first 3 seconds, then a progress ring; a failure warning before it that opens a popover with the error and Try Again; the last scan time stored with setSetting("ui.lastScanAt", ...); the subtitle reads "Scanning…" during a scan; a polite live region announces each phase; ⌘R rescans.
  • The bottom status bar is gone. The total moves to the toolbar subtitle, per-agent counts to the sidebar and scope menu, scan state to the Rescan control.
  • Pane hairlines run up through the toolbar; user-select: none on chrome, text on the reader (18.2).
  • Shortcuts: ⌃⌘S sidebar, ⌘R rescan, ⌥⌘F search, ⌘/ filter reference.

Guards as tests

  • src/design-guards.test.ts fails on a hex color, or a px literal or arbitrary Tailwind value in a string, in any file under src/components or src/App.tsx. No file under src/components/ui needs an exception.
  • src/tokens.test.ts computes WCAG ratios for every text and background pair in light, dark, and Increase Contrast (4.5:1 text, 3:1 focus ring, segment ring, toolbar icons), and checks the data-theme="dark" block equals the system dark block.
  • src/theme.test.ts proves stock Tailwind classes compile to nothing and token classes compile to the tokens.
  • src/dead-classes.test.ts fails when a utility class in a component compiles to no CSS.

Scope

In: tokens, Tailwind theme, system type, shadcn primitives, the sidebar, the toolbar, Rescan and scan state, removal of the status bar, the guard tests. Out (later PRs): session row anatomy and search tokens (PR 2), the reader and its toolbar actions (PR 3), settings, palette, sheets, onboarding (PR 4). No changes to src/ipc.ts, src/main.tsx, src/mock/, crates/, or src-tauri/.

Tradeoffs

Deviations from the spec or the prototype, with reasons:

  1. Palette button in the toolbar (⌘ icon). Without a native menu bar the spec has no mouse path to the palette or Settings, and every capability must stay reachable by mouse. Settings is one click further, through the palette.
  2. Notices line under the toolbar. The status bar also carried notices ("Filed in …", "Copied as Markdown") and errors. Until PR 3 moves each confirmation onto its control (spec 9.12), they show in one dismissible line under the toolbar. Scan failures use the Rescan warning popover instead.
  3. Agents rows run agent: searches. The brief allowed an Agents section; its rows set the query to agent:<id> and show selected while that is the whole query.
  4. Filter-only guard removed here, ahead of PR 2. The Agents rows need filter-only queries, which the backend supports since feat(search): answer filter-only queries and match repo: by name #22, so the "Filters narrow a search" guard is gone and a filter-only query now searches (spec 8).
  5. Filter reference as a popover. The query line had to move into the 260px capsule, so its reference became a popover opened from the magnifier, ? in an empty field, or ⌘/, never on focus (17.13). PR 2 owns the token redesign.
  6. No sidebar collapse animation. Animating the grid column would be layout animation, which 17.19 forbids; the sidebar shows and hides instantly.
  7. New Folder footer and folder context menu. Rename and Delete moved from the list header to a context menu on folder rows, with matching palette commands for the selected folder. A "New Folder" button sits at the sidebar foot, as in Notes.
  8. Radix numeric offsets. Radix takes tooltip delay and side offsets as numbers, so three values live in src/components/ui/metrics.ts.
  9. clsx only. No class-variance-authority or tailwind-merge: the spike measured cn at 11 kB gzip, and the token vocabulary needs no conflict merging.
  10. Toolbar order is Rescan, then the palette button, then search. The scan warning appears before Rescan, so nothing after it moves (17.23).

Cost: JS grows from about 67 kB to 114 kB gzip, mostly Radix, in line with the spike's measurement.

Blast Radius

UI only. Every screen changes appearance because the fonts and colors change. Behavior changes: the scope chip and its popover are replaced by the sidebar and the scope menu; the status bar is removed; filter-only queries now search; the filter reference no longer opens on focus; ⌘R, ⌃⌘S, ⌥⌘F, and ⌘/ are new shortcuts. The only new persisted value is the ui.lastScanAt setting. The reader, list rows, settings, palette, and onboarding keep their behavior.

Verification

  • npm run lint: exit 0.
  • npm run typecheck: exit 0.
  • npm test: 12 files, 114 tests passed. Existing App tests were updated for the new shell and new ones cover the sidebar, the scope menu, the narrow-window default, folder rename and delete, New Folder, Settings by mouse, the notices line, Rescan progress, failure and Try Again, the last scan time, ⌘R, filter-only queries, and the filter reference.
  • npm run build: passed. CSS 39.45 kB (8.46 kB gzip), JS 355.17 kB (113.90 kB gzip).
  • Reviewer gate: one must-fix (the running Rescan spinner inherited the disabled color, about 1.6:1) and small items (--red-text and --orange-icon tokens with contrast checks, a fixed position for the scan warning, a rebase onto main). All are fixed in the last commit, with a test that the running spinner keeps the active color and that the toolbar order holds when the warning appears.
  • Screenshots in real WebKit (docs/design/tools/wkshot.swift) at 1280x800 and 900x700, light and dark, of main and this branch. They are gitignored, so they exist only locally:
    • /Volumes/990 Pro/Development/Lore/.claude/worktrees/agent-a465e531a504ecfbd/docs/design/shots/pr1/main/
    • /Volumes/990 Pro/Development/Lore/.claude/worktrees/agent-a465e531a504ecfbd/docs/design/shots/pr1/head/ (session open, sidebar collapsed at 1280, sidebar shown at 900, scope menu, filter reference, Rescan tooltip, scanning, folder context menu)
    • /Volumes/990 Pro/Development/Lore/.claude/worktrees/agent-a465e531a504ecfbd/docs/design/shots/pr1-review/ (the reviewer's shots)
  • Not run: the Tauri app (npm run tauri dev) and the Rust checks; no file under src-tauri/ or crates/ changed.

Add src/tokens.css with every spec v2 Section 4 to 6 token for light,
dark (system and data-theme), and Increase Contrast, with the WebKit
system color keywords declared after the measured fallbacks. Tailwind v3
runs with presets: [] and a theme built only from those tokens, so stock
classes such as bg-blue-500, p-7, and rounded-xl emit no CSS.

Drop IBM Plex for the system sans and mono stacks, and remap the existing
component styles onto the tokens. Vendor dialog, dropdown-menu,
context-menu, select, tooltip, command, tabs, popover, toggle, and
scroll-area as React 18 forwardRef wrappers over Radix and cmdk, restyled
to the spec, each with a behavior test.

Guard tests fail on a hex color, px literal, or arbitrary Tailwind value
in a component file, on any token pair under WCAG AA, and on stock
Tailwind classes producing CSS.
Replace the top bar, scope chip, and bottom status bar with the spec v2
window (Sections 7, 8, 9.1, 9.9):

- A source-list sidebar: All Sessions, Repositories, Folders, and
  Agents, with counts, Mail selection colors for focused and unfocused,
  arrow-key selection, collapsible sections, a folder context menu, and
  New Folder. Agent rows run the filter-only agent: search. Hidden below
  1000px; toggled with Control-Command-S or the toolbar button.
- A 52px toolbar split at the column edges: the scope title and session
  count over the list (a scope menu when the sidebar is hidden), an
  empty slot for reader actions, the palette button, Rescan, and the
  260px capsule search field. Empty areas are Tauri drag regions.
- Rescan shows a spinner, then a progress ring, a failure popover with
  Try Again, and the last scan time stored as ui.lastScanAt. Command-R
  rescans.
- The filter reference opens from the magnifier, ?, or Command-/,
  never on focus. Filter-only queries now search.
- Notices move from the status bar to a line under the toolbar.
With presets: [] a class outside the tokens compiles to nothing, so a
typo or a stock value fails silently. The new test compiles every
utility-looking class token in component strings and lists the ones
that emit no CSS. It would have caught left-1.5 in the menu indicator
before inset had a spacing scale.
The running Rescan button is aria-disabled, which grayed its spinner to
the disabled label color (about 1.6:1 on the toolbar). The spinner now
keeps the secondary label color. The scan warning moves before Rescan
so the controls after it never shift when it appears (spec 17.23).

Add the spec's --red-text and --orange-icon tokens for destructive
labels and the toolbar warning icon, use them for the existing red text,
and check both in the contrast test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant