From 3401cc0d80c4a1dbbad244872040937bc8ddd8f8 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:04:00 +0100
Subject: [PATCH 01/35] Document design system audit approach
---
.../2026-08-28-design-system-audit-design.md | 101 ++++++++++++++++++
1 file changed, 101 insertions(+)
create mode 100644 docs/superpowers/specs/2026-08-28-design-system-audit-design.md
diff --git a/docs/superpowers/specs/2026-08-28-design-system-audit-design.md b/docs/superpowers/specs/2026-08-28-design-system-audit-design.md
new file mode 100644
index 00000000..c8ad339a
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-28-design-system-audit-design.md
@@ -0,0 +1,101 @@
+# Design system audit
+
+## Aim
+
+Make Chopin's visual system easier to inspect and harder to drift. The first pass should fix clear,
+repeatable inconsistencies while leaving taste-heavy choices visible for a human review.
+
+The audit covers `apps/web` and the user-facing parts of `packages/editor`. Existing product identity,
+behaviour, and terminology stay intact.
+
+## Current state
+
+The foundations are stronger than the individual screens suggest. `apps/web/src/theme.css` already
+owns the colour, type, spacing, radius, focus, shadow, button, field, and choice-control scales. The
+main problem is inconsistent use across roughly 3,400 lines of app and editor CSS.
+
+Icons currently mix local SVG files with Phosphor React components and explicit sizes from 14px to
+24px. At least one pair is byte-identical: `assets/figma/navigation/collapse.svg` and
+`assets/icons/conversation-close.svg`.
+
+Many editor widgets depend on Lexical, Yjs, live stores, or application state. Reimplementing their
+markup in a showcase would hide the drift we want to find.
+
+## Architecture
+
+Add a development-only `/design-audit` route to `apps/web`. It renders before session loading, so the
+catalogue can be reviewed without GitHub authentication. Production builds must not expose or bundle
+the route.
+
+The page has four layers:
+
+1. **Foundations** – colour, typography, spacing, radii, shadows, icon sizes, and focus treatment.
+2. **Controls** – buttons, icon buttons, links, fields, selections, tabs, menus, dropdowns, and their
+ resting, hover, active, focus, disabled, busy, error, and selected states.
+3. **Surfaces** – dialogs, popovers, cards, lists, navigation rows, chat messages, composer elements,
+ decisions, resolved comments, and loading/empty/error states.
+4. **Authored content** – a real read-only editor fixture containing headings, lists, links, image,
+ formula, Mermaid diagram, code, diff, callout, table, tabs, decision, comment-related prose, and
+ research states. Where a widget cannot be mounted through the document, use its exported component
+ with a small typed fixture adapter.
+
+Catalogue scaffolding belongs in a small `apps/web/src/design-audit/` module. It may compose real
+components and fixtures but must not become a second component library.
+
+## State and interaction
+
+Sections render all stable visual states side by side. Pointer-only states get a labelled forced-state
+sample so screenshots remain deterministic; real controls remain interactive for keyboard and focus
+review. Dialogs and menus have both inline specimen and interactive examples.
+
+Fixtures are local and deterministic. They do not call HTTP, WebSocket, GitHub, or Planner APIs. A
+compact viewport control lets the same page expose desktop and narrow arrangements without copying
+the app shell.
+
+## Audit method
+
+Create a machine-readable inventory alongside the page for:
+
+- component and state coverage;
+- icon source, semantic name, geometry, and duplicate-content hash;
+- button and icon-button class combinations;
+- shadow, radius, spacing, and colour-token use; and
+- items deliberately left for human judgement.
+
+Inspect the catalogue and authenticated app at desktop and mobile widths. Use one broad screenshot
+pass, fix the resulting clear issues in a batch, then use one confirmation pass. This is a bounded
+review, not an endless polish loop.
+
+Clear first-pass fixes include duplicate icon assets, mismatched icon geometry or colour, missing
+focus and disabled treatments, undersized targets, accidental one-off spacing, incorrect shadow
+levels, and hierarchy that contradicts the shared button variants. Ambiguous brand or composition
+changes go into the human-review list instead.
+
+## Change boundaries
+
+Normalisation should deepen the existing system:
+
+- add semantic utilities or small shared components only when at least two real consumers need them;
+- migrate consumers in small vertical slices;
+- preserve accessible names, keyboard behaviour, responsive behaviour, and editor invariants; and
+- avoid compatibility aliases unless persisted data or an external consumer requires one.
+
+The catalogue stays in the repository as a regression and review surface after the audit.
+
+## Verification
+
+The catalogue gets focused render or source-contract tests for route gating and required specimens.
+Existing component tests remain authoritative for behaviour. Browser checks cover layout, focus,
+menus, dialogs, target sizes, overflow, and screenshots at desktop and mobile widths.
+
+After edits, run `bun run fix`, focused tests, `bun run types`, `bun run ci`, and the narrowest relevant
+Playwright coverage. Run the design-quality detector once over changed UI files after visual work is
+finished.
+
+## Delivery slices
+
+1. Add the dev-only catalogue shell, inventory, and foundation/control specimens.
+2. Add real app surfaces, chat, dialogs, decisions, comments, and loading states.
+3. Add the authored-content fixture and research states.
+4. Capture the first screenshots and normalise clear token, icon, control, spacing, and surface drift.
+5. Confirm desktop/mobile results and publish the remaining human-review list.
From 3a2ad0b34d74dfbeeb1d51b4cac4c8d3772570b2 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:05:52 +0100
Subject: [PATCH 02/35] Plan design audit catalogue implementation
---
.../2026-08-28-design-audit-catalogue.md | 272 ++++++++++++++++++
1 file changed, 272 insertions(+)
create mode 100644 docs/superpowers/plans/2026-08-28-design-audit-catalogue.md
diff --git a/docs/superpowers/plans/2026-08-28-design-audit-catalogue.md b/docs/superpowers/plans/2026-08-28-design-audit-catalogue.md
new file mode 100644
index 00000000..8bae3669
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-28-design-audit-catalogue.md
@@ -0,0 +1,272 @@
+# Design Audit Catalogue Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build a development-only page that exposes Chopin's foundations, controls, application surfaces, and authored-content components in deterministic visual states.
+
+**Architecture:** `apps/web` owns a lazily loaded `/design-audit` route. Small catalogue modules compose real web and editor components; pure fixture data describes coverage. A read-only static editor entry point renders authored MDX through the production dialect and widget renderers without opening a WebSocket.
+
+**Tech Stack:** React 19, TypeScript, Vite, Tailwind CSS v4, MDXEditor/Lexical, Bun tests.
+
+---
+
+This plan builds the inspection surface. Screenshot findings will produce a separate, evidence-based
+normalisation plan; naming exact remediation files before seeing the catalogue would be guesswork.
+
+### Task 1: Development-only route
+
+**Files:**
+- Create: `apps/web/src/design-audit/route.ts`
+- Create: `apps/web/src/design-audit/route.test.ts`
+- Create: `apps/web/src/design-audit/page.tsx`
+- Modify: `apps/web/src/main.tsx`
+
+- [ ] **Step 1: Write the route test**
+
+```ts
+import { describe, expect, it } from "bun:test";
+import { isDesignAuditRoute } from "./route";
+
+describe("design audit route", () => {
+ it("exists only in development at its exact path", () => {
+ expect(isDesignAuditRoute("/design-audit", true)).toBe(true);
+ expect(isDesignAuditRoute("/design-audit", false)).toBe(false);
+ expect(isDesignAuditRoute("/design-audit/extra", true)).toBe(false);
+ });
+});
+```
+
+- [ ] **Step 2: Verify the test fails**
+
+Run: `bun test apps/web/src/design-audit/route.test.ts`
+Expected: FAIL because `route.ts` does not exist.
+
+- [ ] **Step 3: Add the pure route predicate**
+
+```ts
+export function isDesignAuditRoute(pathname: string, development: boolean): boolean {
+ return development && pathname === "/design-audit";
+}
+```
+
+- [ ] **Step 4: Add lazy route selection to `main.tsx`**
+
+Keep the current application root unchanged for ordinary paths. On the audit path, dynamically import
+`./design-audit/page` and render its `DesignAuditPage`. The import must be inside the
+`import.meta.env.DEV`-guarded branch so a normal production build removes the route chunk.
+
+- [ ] **Step 5: Add a minimal labelled page and verify**
+
+```tsx
+export function DesignAuditPage() {
+ return Chopin design audit ;
+}
+```
+
+Run: `bun test apps/web/src/design-audit/route.test.ts && bun --cwd apps/web run types`
+Expected: PASS.
+
+- [ ] **Step 6: Verify production exclusion and commit**
+
+Run: `bun --cwd apps/web run build && ! rg -l "Chopin design audit" apps/web/dist`
+Expected: build passes and `rg` finds no catalogue string.
+
+Commit: `git commit -am "Add development design audit route"`
+
+### Task 2: Coverage inventory and catalogue frame
+
+**Files:**
+- Create: `apps/web/src/design-audit/inventory.ts`
+- Create: `apps/web/src/design-audit/inventory.test.ts`
+- Create: `apps/web/src/design-audit/frame.tsx`
+- Create: `apps/web/src/design-audit/styles.css`
+- Modify: `apps/web/src/design-audit/page.tsx`
+
+- [ ] **Step 1: Write inventory coverage tests**
+
+Define `AuditGroup`, `AuditItem`, and exported `AUDIT_INVENTORY`. Assert that the inventory contains the
+required groups `foundations`, `controls`, `surfaces`, `authored-content`, and the named specimens from
+the approved spec. Assert every item has a unique `id`, a source path, and at least one state.
+
+- [ ] **Step 2: Verify the inventory test fails**
+
+Run: `bun test apps/web/src/design-audit/inventory.test.ts`
+Expected: FAIL because `inventory.ts` does not exist.
+
+- [ ] **Step 3: Implement the typed inventory**
+
+```ts
+export type AuditItem = {
+ id: string;
+ label: string;
+ source: string;
+ states: readonly string[];
+ judgement?: string;
+};
+
+export type AuditGroup = {
+ id: "foundations" | "controls" | "surfaces" | "authored-content";
+ label: string;
+ items: readonly AuditItem[];
+};
+```
+
+Populate it with buttons, icon buttons, links, fields, selection, tabs, menus, dropdowns, dialogs,
+lists, navigation rows, chat, decisions, resolved comments, loading, empty, error, callout, research,
+code, diff, diagram, formula, image, and table.
+
+- [ ] **Step 4: Build the frame**
+
+`frame.tsx` owns `AuditSection`, `AuditPlate`, `StateLabel`, the sticky table of contents, and the
+desktop/narrow preview-width control. `styles.css` may style only catalogue scaffolding; specimens use
+the real application classes.
+
+- [ ] **Step 5: Run tests and commit**
+
+Run: `bun test apps/web/src/design-audit/inventory.test.ts && bun --cwd apps/web run types`
+Expected: PASS.
+
+Commit: `git add apps/web/src/design-audit && git commit -m "Map design audit coverage"`
+
+### Task 3: Foundations and controls
+
+**Files:**
+- Create: `apps/web/src/design-audit/foundations.tsx`
+- Create: `apps/web/src/design-audit/controls.tsx`
+- Modify: `apps/web/src/design-audit/page.tsx`
+
+- [ ] **Step 1: Add source-contract assertions to the inventory test**
+
+Assert each foundation and control inventory ID appears in its specimen module. This is intentionally
+a source contract because Bun has no DOM and the repository forbids synthetic layout DOMs.
+
+- [ ] **Step 2: Render foundations**
+
+Show the emitted colour tokens, type scale, 4px spacing scale, radii, and the three semantic shadows.
+Render all local SVG and Phosphor icons in one 16px default frame, with separate 14px compact and 20px
+emphasis examples. Label the byte-identical collapse/conversation-close pair.
+
+- [ ] **Step 3: Render every control state**
+
+Use the real `btn`, `btn-md`, `btn-sm`, `btn-icon`, `btn-primary`, `btn-secondary`, `btn-ghost`,
+`btn-destructive`, `field`, `field-ghost`, and `choice-control` utilities. Include resting, labelled
+forced-hover, labelled forced-active, keyboard-focusable, disabled, busy, selected, invalid, and
+read-only examples. Include text links, tablist/tab states, list selection, dropdown, and menu rows.
+
+- [ ] **Step 4: Verify and commit**
+
+Run: `bun test apps/web/src/design-audit/inventory.test.ts && bun --cwd apps/web run types`
+Expected: PASS.
+
+Commit: `git add apps/web/src/design-audit && git commit -m "Catalogue foundations and controls"`
+
+### Task 4: Application surfaces
+
+**Files:**
+- Create: `apps/web/src/design-audit/surfaces.tsx`
+- Create: `apps/web/src/design-audit/fixtures.ts`
+- Modify: `apps/web/src/design-audit/page.tsx`
+
+- [ ] **Step 1: Add surface fixtures**
+
+Create deterministic fixture records for a user, chat messages, repository/document rows, research
+requests in queued/running/failed/cancelled/ready states, resolved decisions, resolved comments, and
+loading/empty/error content. Use valid protocol shapes and fixed ULIDs/UUIDs.
+
+- [ ] **Step 2: Compose real surfaces**
+
+Render actual exported or app-owned components where their API is small: `NavigationDialog`,
+`DocumentActionsMenu`, `ConversationToggle`, `TerminalAlert`, `Face`, `SendAction`, `ResearchCard`,
+`ResearchComposer`, `QuestionnaireCard`, and chat transcript elements. For tightly coupled surfaces,
+extract a reusable presentational component from the current owner rather than copying its markup.
+
+- [ ] **Step 3: Show layout variants**
+
+Place dialogs, menus, popovers, cards, navigation/list rows, conversation messages, composer controls,
+decision cards, resolved-comment cards, and loading states in labelled plates. Interactive dialog/menu
+launchers must restore focus and close with Escape; inline specimens remain open for screenshots.
+
+- [ ] **Step 4: Verify and commit**
+
+Run: `bun test apps/web/src/design-audit/inventory.test.ts packages/editor/src/card.test.ts && bun --cwd apps/web run types`
+Expected: PASS.
+
+Commit: `git add apps/web/src/design-audit apps/web/src packages/editor/src && git commit -m "Catalogue application surfaces"`
+
+### Task 5: Authored-content fixture
+
+**Files:**
+- Create: `packages/editor/src/static-plan-editor.tsx`
+- Create: `packages/editor/src/static-plan-editor.test.ts`
+- Modify: `packages/editor/package.json`
+- Create: `apps/web/src/design-audit/authored-content.tsx`
+- Modify: `apps/web/src/design-audit/page.tsx`
+
+- [ ] **Step 1: Write the static-editor source contract**
+
+Assert the fixture entry point registers the production widget renderers, uses `dialectPlugins`, uses
+the production `widgetsPlugin`, sets `readOnly`, and does not import collaboration or `PlanProvider`.
+
+- [ ] **Step 2: Implement `StaticPlanEditor`**
+
+Mount `MDXEditor` with supplied canonical `source`, the same lexical theme and content class as
+`PlanEditor`, `dialectPlugins({ core: false })`, `markdownShortcutPlugin()`, and `widgetsPlugin`.
+Accept optional `research` and `questions` fixture stores. Export it only from an explicit
+`@chopin/editor/static` package subpath.
+
+- [ ] **Step 3: Create the canonical document fixture**
+
+Include ordinary prose and links, headings, ordered/unordered/task lists, table, note/tip/warning/danger
+callouts, tabs, inline/block maths, plain and TypeScript code, valid and invalid diffs, Mermaid diagram,
+image success/failure specimens, a research node, and decision-related prose. Use the real dialect
+syntax and fixed valid IDs.
+
+- [ ] **Step 4: Show sidecar states beside the document**
+
+Render `ResearchCard` for each lifecycle state, a populated `ResearchComposer`, resolved decision and
+comment cards, plus their empty/loading/error variants. This keeps record-owned state authoritative
+instead of forging decision records inside MDX.
+
+- [ ] **Step 5: Verify and commit**
+
+Run: `bun test packages/editor/src/static-plan-editor.test.ts apps/web/src/design-audit/inventory.test.ts && bun --cwd packages/editor run types && bun --cwd apps/web run types`
+Expected: PASS.
+
+Commit: `git add packages/editor apps/web/src/design-audit && git commit -m "Catalogue authored document components"`
+
+### Task 6: Browser audit handoff
+
+**Files:**
+- Create: `docs/design-system-audit.md`
+
+- [ ] **Step 1: Start the real development server**
+
+Run: `bun run dev`
+Expected: the supervisor reports the local application URL. Open `/design-audit` without signing in.
+
+- [ ] **Step 2: Inspect desktop and mobile in one broad pass**
+
+Capture full-page and section screenshots at 1440px and 390px. Check overflow, target size, focus,
+disabled state, hierarchy, icon geometry/colour, spacing rhythm, surface elevation, modal/menu behaviour,
+tabs, authored blocks, and loading states.
+
+- [ ] **Step 3: Inspect the authenticated application**
+
+Open the ordinary application root. If it shows GitHub sign-in, stop and notify the user so they can
+complete authentication. Once signed in, compare the repository picker, navigation, document,
+conversation, decisions, comments, research, dialogs, menus, and loading transitions against the
+catalogue at desktop and mobile widths.
+
+- [ ] **Step 4: Record evidence**
+
+Create `docs/design-system-audit.md` with a health score, P0–P3 findings with file/line evidence,
+positive findings, exact duplicate icons, and a human-judgement parking lot. Each finding must explain
+impact and name the token/component boundary that should own the fix.
+
+- [ ] **Step 5: Write the evidence-based normalisation plan**
+
+Create a second small-slice plan from the verified P1/P2 findings. Do not include speculative P3 work.
+
+- [ ] **Step 6: Commit the audit**
+
+Commit: `git add docs/design-system-audit.md docs/superpowers/plans && git commit -m "Document design system audit findings"`
From c039aae31d3b4db03187bbaab2dad0026bba8fe1 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:08:23 +0100
Subject: [PATCH 03/35] Add development design audit route
---
apps/web/src/design-audit/page.test.tsx | 14 +++++++++
apps/web/src/design-audit/page.tsx | 9 ++++++
apps/web/src/design-audit/route.test.ts | 11 +++++++
apps/web/src/design-audit/route.ts | 3 ++
apps/web/src/main.tsx | 13 ++++----
.../2026-08-28-design-audit-catalogue.md | 30 +++++++++++--------
6 files changed, 62 insertions(+), 18 deletions(-)
create mode 100644 apps/web/src/design-audit/page.test.tsx
create mode 100644 apps/web/src/design-audit/page.tsx
create mode 100644 apps/web/src/design-audit/route.test.ts
create mode 100644 apps/web/src/design-audit/route.ts
diff --git a/apps/web/src/design-audit/page.test.tsx b/apps/web/src/design-audit/page.test.tsx
new file mode 100644
index 00000000..e7520904
--- /dev/null
+++ b/apps/web/src/design-audit/page.test.tsx
@@ -0,0 +1,14 @@
+import { describe, expect, it } from "bun:test";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { DesignAuditPage } from "./page";
+
+describe("design audit page", () => {
+ it("identifies itself as a development inspection surface", () => {
+ let markup = renderToStaticMarkup(createElement(DesignAuditPage));
+
+ expect(markup).toContain('data-design-audit=""');
+ expect(markup).toContain("Chopin design audit");
+ });
+});
diff --git a/apps/web/src/design-audit/page.tsx b/apps/web/src/design-audit/page.tsx
new file mode 100644
index 00000000..15b3c78b
--- /dev/null
+++ b/apps/web/src/design-audit/page.tsx
@@ -0,0 +1,9 @@
+export function DesignAuditPage() {
+ return (
+
+
+
Chopin design audit
+
+
+ );
+}
diff --git a/apps/web/src/design-audit/route.test.ts b/apps/web/src/design-audit/route.test.ts
new file mode 100644
index 00000000..f7fd5bf8
--- /dev/null
+++ b/apps/web/src/design-audit/route.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, it } from "bun:test";
+
+import { isDesignAuditRoute } from "./route";
+
+describe("design audit route", () => {
+ it("exists only in development at its exact path", () => {
+ expect(isDesignAuditRoute("/design-audit", true)).toBe(true);
+ expect(isDesignAuditRoute("/design-audit", false)).toBe(false);
+ expect(isDesignAuditRoute("/design-audit/extra", true)).toBe(false);
+ });
+});
diff --git a/apps/web/src/design-audit/route.ts b/apps/web/src/design-audit/route.ts
new file mode 100644
index 00000000..e02d8cff
--- /dev/null
+++ b/apps/web/src/design-audit/route.ts
@@ -0,0 +1,3 @@
+export function isDesignAuditRoute(pathname: string, development: boolean): boolean {
+ return development && pathname === "/design-audit";
+}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index d128d3aa..810d0b5c 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import { usePointerCapabilities } from "@chopin/editor/pointer";
import { App } from "./app";
+import { isDesignAuditRoute } from "./design-audit/route";
import { useMotionInput } from "./motion-input";
import { useVisualViewport } from "./viewport";
@@ -22,8 +23,10 @@ function Root() {
return ;
}
-createRoot(root).render(
-
-
- ,
-);
+let content = isDesignAuditRoute(location.pathname, import.meta.env.DEV)
+ ? import("./design-audit/page").then(({ DesignAuditPage }) => )
+ : Promise.resolve( );
+
+void content.then(value => {
+ createRoot(root).render({value} );
+});
diff --git a/docs/superpowers/plans/2026-08-28-design-audit-catalogue.md b/docs/superpowers/plans/2026-08-28-design-audit-catalogue.md
index 8bae3669..9bf7810a 100644
--- a/docs/superpowers/plans/2026-08-28-design-audit-catalogue.md
+++ b/docs/superpowers/plans/2026-08-28-design-audit-catalogue.md
@@ -63,12 +63,12 @@ export function DesignAuditPage() {
}
```
-Run: `bun test apps/web/src/design-audit/route.test.ts && bun --cwd apps/web run types`
+Run: `bun test apps/web/src/design-audit/route.test.ts && bun run --cwd apps/web types`
Expected: PASS.
- [ ] **Step 6: Verify production exclusion and commit**
-Run: `bun --cwd apps/web run build && ! rg -l "Chopin design audit" apps/web/dist`
+Run: `bun run --cwd apps/web build && ! rg -l "Chopin design audit" apps/web/dist`
Expected: build passes and `rg` finds no catalogue string.
Commit: `git commit -am "Add development design audit route"`
@@ -123,7 +123,7 @@ the real application classes.
- [ ] **Step 5: Run tests and commit**
-Run: `bun test apps/web/src/design-audit/inventory.test.ts && bun --cwd apps/web run types`
+Run: `bun test apps/web/src/design-audit/inventory.test.ts && bun run --cwd apps/web types`
Expected: PASS.
Commit: `git add apps/web/src/design-audit && git commit -m "Map design audit coverage"`
@@ -133,12 +133,15 @@ Commit: `git add apps/web/src/design-audit && git commit -m "Map design audit co
**Files:**
- Create: `apps/web/src/design-audit/foundations.tsx`
- Create: `apps/web/src/design-audit/controls.tsx`
+- Create: `apps/web/src/design-audit/specimens.test.tsx`
- Modify: `apps/web/src/design-audit/page.tsx`
-- [ ] **Step 1: Add source-contract assertions to the inventory test**
+- [ ] **Step 1: Write rendered specimen tests**
-Assert each foundation and control inventory ID appears in its specimen module. This is intentionally
-a source contract because Bun has no DOM and the repository forbids synthetic layout DOMs.
+Render the foundation and control sections with `renderToStaticMarkup`. Assert the resulting landmarks,
+headings, control roles, accessible names, native disabled attributes, selected state, and invalid state.
+The break caught is a catalogue specimen disappearing or losing the same semantics as its real control;
+CSS geometry remains a browser test.
- [ ] **Step 2: Render foundations**
@@ -155,7 +158,7 @@ read-only examples. Include text links, tablist/tab states, list selection, drop
- [ ] **Step 4: Verify and commit**
-Run: `bun test apps/web/src/design-audit/inventory.test.ts && bun --cwd apps/web run types`
+Run: `bun test apps/web/src/design-audit/inventory.test.ts && bun run --cwd apps/web types`
Expected: PASS.
Commit: `git add apps/web/src/design-audit && git commit -m "Catalogue foundations and controls"`
@@ -188,7 +191,7 @@ launchers must restore focus and close with Escape; inline specimens remain open
- [ ] **Step 4: Verify and commit**
-Run: `bun test apps/web/src/design-audit/inventory.test.ts packages/editor/src/card.test.ts && bun --cwd apps/web run types`
+Run: `bun test apps/web/src/design-audit/inventory.test.ts packages/editor/src/card.test.ts && bun run --cwd apps/web types`
Expected: PASS.
Commit: `git add apps/web/src/design-audit apps/web/src packages/editor/src && git commit -m "Catalogue application surfaces"`
@@ -197,15 +200,16 @@ Commit: `git add apps/web/src/design-audit apps/web/src packages/editor/src && g
**Files:**
- Create: `packages/editor/src/static-plan-editor.tsx`
-- Create: `packages/editor/src/static-plan-editor.test.ts`
+- Create: `packages/editor/src/static-plan-editor.test.tsx`
- Modify: `packages/editor/package.json`
- Create: `apps/web/src/design-audit/authored-content.tsx`
- Modify: `apps/web/src/design-audit/page.tsx`
-- [ ] **Step 1: Write the static-editor source contract**
+- [ ] **Step 1: Write the static-editor render contract**
-Assert the fixture entry point registers the production widget renderers, uses `dialectPlugins`, uses
-the production `widgetsPlugin`, sets `readOnly`, and does not import collaboration or `PlanProvider`.
+Render `StaticPlanEditor` with a heading, callout, and code fixture. Assert its labelled read-only
+document landmark and source identity render without collaboration status or connection fallback.
+The browser pass later asserts the hydrated editor's actual authored blocks.
- [ ] **Step 2: Implement `StaticPlanEditor`**
@@ -229,7 +233,7 @@ instead of forging decision records inside MDX.
- [ ] **Step 5: Verify and commit**
-Run: `bun test packages/editor/src/static-plan-editor.test.ts apps/web/src/design-audit/inventory.test.ts && bun --cwd packages/editor run types && bun --cwd apps/web run types`
+Run: `bun test packages/editor/src/static-plan-editor.test.tsx apps/web/src/design-audit/inventory.test.ts && bun run --cwd packages/editor types && bun run --cwd apps/web types`
Expected: PASS.
Commit: `git add packages/editor apps/web/src/design-audit && git commit -m "Catalogue authored document components"`
From 1632f51122fc4d79b2fda8810465b6196bd54b44 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:11:15 +0100
Subject: [PATCH 04/35] Map design audit coverage
---
apps/web/src/design-audit/frame.test.tsx | 21 +++
apps/web/src/design-audit/frame.tsx | 78 ++++++++
apps/web/src/design-audit/inventory.test.ts | 63 +++++++
apps/web/src/design-audit/inventory.ts | 186 ++++++++++++++++++++
apps/web/src/design-audit/page.test.tsx | 7 +
apps/web/src/design-audit/page.tsx | 19 +-
apps/web/src/design-audit/styles.css | 185 +++++++++++++++++++
7 files changed, 554 insertions(+), 5 deletions(-)
create mode 100644 apps/web/src/design-audit/frame.test.tsx
create mode 100644 apps/web/src/design-audit/frame.tsx
create mode 100644 apps/web/src/design-audit/inventory.test.ts
create mode 100644 apps/web/src/design-audit/inventory.ts
create mode 100644 apps/web/src/design-audit/styles.css
diff --git a/apps/web/src/design-audit/frame.test.tsx b/apps/web/src/design-audit/frame.test.tsx
new file mode 100644
index 00000000..973802f0
--- /dev/null
+++ b/apps/web/src/design-audit/frame.test.tsx
@@ -0,0 +1,21 @@
+import { describe, expect, it } from "bun:test";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { AuditPlate, StateLabel } from "./frame";
+
+describe("design audit specimen framing", () => {
+ it("labels a specimen and its represented state", () => {
+ let markup = renderToStaticMarkup(
+ createElement(
+ AuditPlate,
+ { title: "Primary button", description: "Shared action hierarchy" },
+ createElement(StateLabel, null, "Focus"),
+ ),
+ );
+
+ expect(markup).toContain("Primary button");
+ expect(markup).toContain("Shared action hierarchy");
+ expect(markup).toContain("Focus");
+ });
+});
diff --git a/apps/web/src/design-audit/frame.tsx b/apps/web/src/design-audit/frame.tsx
new file mode 100644
index 00000000..2808b31e
--- /dev/null
+++ b/apps/web/src/design-audit/frame.tsx
@@ -0,0 +1,78 @@
+import { useState } from "react";
+
+import type { ReactNode } from "react";
+import type { AuditGroup } from "./inventory";
+
+export function AuditFrame(
+ { children, groups }: { children: ReactNode; groups: readonly AuditGroup[] },
+) {
+ let [width, setWidth] = useState<"wide" | "narrow">("wide");
+ return (
+
+
+
+
Chopin design audit
+
+ A live map of the foundations, controls, surfaces, and authored content in the
+ application.
+
+
+
+ setWidth("wide")}
+ type="button"
+ >
+ Wide
+
+ setWidth("narrow")}
+ type="button"
+ >
+ Narrow
+
+
+
+
+
+ );
+}
+
+export function AuditSection(
+ { children, id, title }: { children: ReactNode; id: string; title: string },
+) {
+ return (
+
+ );
+}
+
+export function AuditPlate(
+ { children, description, title }: { children?: ReactNode; description?: string; title: string },
+) {
+ return (
+
+
+ {title}
+ {description ? {description}
: null}
+
+ {children}
+
+ );
+}
+
+export function StateLabel({ children }: { children: ReactNode }) {
+ return {children} ;
+}
diff --git a/apps/web/src/design-audit/inventory.test.ts b/apps/web/src/design-audit/inventory.test.ts
new file mode 100644
index 00000000..710fa2d6
--- /dev/null
+++ b/apps/web/src/design-audit/inventory.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, it } from "bun:test";
+
+import { AUDIT_INVENTORY } from "./inventory";
+
+const REQUIRED = [
+ "colours",
+ "typography",
+ "spacing",
+ "radii",
+ "shadows",
+ "icons",
+ "buttons",
+ "icon-buttons",
+ "links",
+ "fields",
+ "selections",
+ "tabs",
+ "menus",
+ "dropdowns",
+ "dialogs",
+ "lists",
+ "navigation",
+ "chat",
+ "decisions",
+ "resolved-comments",
+ "loading",
+ "empty",
+ "errors",
+ "callouts",
+ "research",
+ "code",
+ "diff",
+ "diagram",
+ "formula",
+ "image",
+ "table",
+] as const;
+
+describe("design audit inventory", () => {
+ it("covers every required system area exactly once", () => {
+ expect(AUDIT_INVENTORY.map(group => group.id)).toEqual([
+ "foundations",
+ "controls",
+ "surfaces",
+ "authored-content",
+ ]);
+
+ let items = AUDIT_INVENTORY.flatMap(group => group.items);
+ let ids = items.map(item => item.id);
+ expect(new Set(ids).size).toBe(ids.length);
+ expect(REQUIRED.every(id => ids.includes(id))).toBe(true);
+ });
+
+ it("records an inspectable source and state for every specimen", () => {
+ for (let group of AUDIT_INVENTORY) {
+ for (let item of group.items) {
+ expect(item.label.trim().length).toBeGreaterThan(0);
+ expect(item.source).toMatch(/^(?:apps|packages)\//);
+ expect(item.states.length).toBeGreaterThan(0);
+ }
+ }
+ });
+});
diff --git a/apps/web/src/design-audit/inventory.ts b/apps/web/src/design-audit/inventory.ts
new file mode 100644
index 00000000..8a3f84d8
--- /dev/null
+++ b/apps/web/src/design-audit/inventory.ts
@@ -0,0 +1,186 @@
+export type AuditItem = {
+ id: string;
+ label: string;
+ source: string;
+ states: readonly string[];
+ judgement?: string;
+};
+
+export type AuditGroup = {
+ id: "foundations" | "controls" | "surfaces" | "authored-content";
+ label: string;
+ items: readonly AuditItem[];
+};
+
+const STATIC = ["default"] as const;
+const INTERACTIVE = ["default", "hover", "active", "focus", "disabled"] as const;
+const ASYNC = ["loading", "empty", "error", "ready"] as const;
+
+export const AUDIT_INVENTORY: readonly AuditGroup[] = [
+ {
+ id: "foundations",
+ label: "Foundations",
+ items: [
+ { id: "colours", label: "Colour roles", source: "apps/web/src/theme.css", states: STATIC },
+ { id: "typography", label: "Typography", source: "apps/web/src/theme.css", states: STATIC },
+ { id: "spacing", label: "Spacing", source: "apps/web/src/theme.css", states: STATIC },
+ { id: "radii", label: "Radii", source: "apps/web/src/theme.css", states: STATIC },
+ { id: "shadows", label: "Elevation", source: "apps/web/src/theme.css", states: STATIC },
+ {
+ id: "icons",
+ label: "Icons",
+ source: "apps/web/src/assets/icons",
+ states: ["default", "active", "disabled"],
+ judgement: "Confirm the compact, default, and emphasis size roles.",
+ },
+ ],
+ },
+ {
+ id: "controls",
+ label: "Controls",
+ items: [
+ { id: "buttons", label: "Buttons", source: "apps/web/src/theme.css", states: INTERACTIVE },
+ {
+ id: "icon-buttons",
+ label: "Icon buttons",
+ source: "apps/web/src/theme.css",
+ states: INTERACTIVE,
+ },
+ { id: "links", label: "Links", source: "apps/web/src/theme.css", states: INTERACTIVE },
+ { id: "fields", label: "Fields", source: "apps/web/src/theme.css", states: INTERACTIVE },
+ {
+ id: "selections",
+ label: "Choices and selections",
+ source: "apps/web/src/theme.css",
+ states: ["default", "selected", "focus", "disabled"],
+ },
+ {
+ id: "tabs",
+ label: "Tabs",
+ source: "packages/editor/src/widgets/tabs.tsx",
+ states: ["default", "selected", "focus", "overflow"],
+ },
+ {
+ id: "menus",
+ label: "Menus",
+ source: "apps/web/src/document-actions-menu.tsx",
+ states: ["closed", "open", "focus", "disabled"],
+ },
+ {
+ id: "dropdowns",
+ label: "Dropdowns",
+ source: "apps/web/src/document-picker.tsx",
+ states: ["closed", "open", "selected", "loading", "error"],
+ },
+ ],
+ },
+ {
+ id: "surfaces",
+ label: "Application surfaces",
+ items: [
+ {
+ id: "dialogs",
+ label: "Dialogs",
+ source: "apps/web/src/navigation-dialog.tsx",
+ states: ["open", "busy", "destructive", "error"],
+ },
+ {
+ id: "lists",
+ label: "Lists",
+ source: "apps/web/src/document-picker.tsx",
+ states: ["default", "hover", "selected", "empty"],
+ },
+ {
+ id: "navigation",
+ label: "Navigation",
+ source: "apps/web/src/project-sidebar.tsx",
+ states: ["default", "current", "ancestor", "archived", "loading"],
+ },
+ {
+ id: "chat",
+ label: "Conversation",
+ source: "apps/web/src/chat/chat.tsx",
+ states: ["member", "planner", "tool", "busy", "error"],
+ },
+ {
+ id: "decisions",
+ label: "Decisions",
+ source: "packages/editor/src/decisions.tsx",
+ states: ["unanswered", "answered", "resolved", "orphaned", "empty"],
+ },
+ {
+ id: "resolved-comments",
+ label: "Resolved comments",
+ source: "packages/editor/src/comments.tsx",
+ states: ["resolved", "deliberately-empty", "orphaned"],
+ },
+ { id: "loading", label: "Loading", source: "apps/web/src/hosted.tsx", states: ASYNC },
+ {
+ id: "empty",
+ label: "Empty states",
+ source: "apps/web/src/repository-picker.tsx",
+ states: ["first-use", "no-results", "unavailable"],
+ },
+ {
+ id: "errors",
+ label: "Errors",
+ source: "apps/web/src/terminal-alert.tsx",
+ states: ["inline", "terminal", "recoverable"],
+ },
+ ],
+ },
+ {
+ id: "authored-content",
+ label: "Authored content",
+ items: [
+ {
+ id: "callouts",
+ label: "Callouts",
+ source: "packages/editor/src/widgets/callout.tsx",
+ states: ["note", "tip", "warning", "danger", "focus"],
+ },
+ {
+ id: "research",
+ label: "Research",
+ source: "packages/editor/src/widgets/research.tsx",
+ states: ["question", "queued", "running", "failed", "cancelled", "ready"],
+ },
+ {
+ id: "code",
+ label: "Code blocks",
+ source: "packages/editor/src/widgets/code-view.tsx",
+ states: ["plain", "named", "collapsed", "editing"],
+ },
+ {
+ id: "diff",
+ label: "Diff blocks",
+ source: "packages/editor/src/widgets/code-view.tsx",
+ states: ["valid", "invalid", "collapsed"],
+ },
+ {
+ id: "diagram",
+ label: "Diagrams",
+ source: "packages/editor/src/widgets/code-view.tsx",
+ states: ["rendered", "source", "invalid"],
+ },
+ {
+ id: "formula",
+ label: "Formulae",
+ source: "packages/editor/src/widgets/render-blocks.tsx",
+ states: ["inline", "block", "source"],
+ },
+ {
+ id: "image",
+ label: "Images",
+ source: "packages/editor/src/widgets/image.tsx",
+ states: ["loaded", "unavailable", "missing-alt"],
+ },
+ {
+ id: "table",
+ label: "Tables",
+ source: "packages/editor/src/table/chrome.tsx",
+ states: ["default", "selected-cell", "toolbar", "overflow"],
+ },
+ ],
+ },
+];
diff --git a/apps/web/src/design-audit/page.test.tsx b/apps/web/src/design-audit/page.test.tsx
index e7520904..986e4283 100644
--- a/apps/web/src/design-audit/page.test.tsx
+++ b/apps/web/src/design-audit/page.test.tsx
@@ -10,5 +10,12 @@ describe("design audit page", () => {
expect(markup).toContain('data-design-audit=""');
expect(markup).toContain("Chopin design audit");
+ expect(markup).toContain('aria-label="Design audit sections"');
+ expect(markup).toContain('aria-label="Preview wide layout"');
+ expect(markup).toContain('aria-label="Preview narrow layout"');
+ expect(markup).toContain('data-preview-width="wide"');
+ for (let id of ["foundations", "controls", "surfaces", "authored-content"]) {
+ expect(markup).toContain(`id="${id}"`);
+ }
});
});
diff --git a/apps/web/src/design-audit/page.tsx b/apps/web/src/design-audit/page.tsx
index 15b3c78b..80918ee1 100644
--- a/apps/web/src/design-audit/page.tsx
+++ b/apps/web/src/design-audit/page.tsx
@@ -1,9 +1,18 @@
+import { AuditFrame, AuditSection } from "./frame";
+import { AUDIT_INVENTORY } from "./inventory";
+
+import "./styles.css";
+
export function DesignAuditPage() {
return (
-
-
-
Chopin design audit
-
-
+
+ {AUDIT_INVENTORY.map(group => (
+
+
+ {group.items.length} component {group.items.length === 1 ? "family" : "families"}
+
+
+ ))}
+
);
}
diff --git a/apps/web/src/design-audit/styles.css b/apps/web/src/design-audit/styles.css
new file mode 100644
index 00000000..7550c40f
--- /dev/null
+++ b/apps/web/src/design-audit/styles.css
@@ -0,0 +1,185 @@
+.design-audit {
+ min-height: 100dvh;
+ padding: max(1.5rem, env(safe-area-inset-top)) max(1.5rem, env(safe-area-inset-right))
+ max(2.5rem, env(safe-area-inset-bottom)) max(1.5rem, env(safe-area-inset-left));
+ background: var(--color-ground);
+ color: var(--color-text-primary);
+}
+
+.design-audit-header,
+.design-audit-layout {
+ width: min(100%, 90rem);
+ margin-inline: auto;
+}
+
+.design-audit-header {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 2rem;
+ padding-block-end: 2rem;
+}
+
+.design-audit-header h1,
+.design-audit-section h2,
+.design-audit-plate h3,
+.design-audit-header p,
+.design-audit-plate p {
+ margin: 0;
+}
+
+.design-audit-header h1 {
+ font-size: var(--text-2xl);
+ font-weight: 600;
+ line-height: var(--text-2xl--line-height);
+ text-wrap: balance;
+}
+
+.design-audit-header p {
+ max-width: 65ch;
+ margin-block-start: 0.5rem;
+ font-size: var(--text-sm);
+ line-height: var(--text-sm--line-height);
+ color: var(--color-text-tertiary);
+ text-wrap: pretty;
+}
+
+.design-audit-width-controls {
+ display: inline-flex;
+ flex: 0 0 auto;
+ gap: 0.125rem;
+ padding: 0.125rem;
+ border-radius: var(--radius-lg);
+ background: var(--color-control);
+}
+
+.design-audit-width-controls [aria-pressed="true"] {
+ background: var(--color-page);
+ box-shadow: var(--shadow-resting);
+ color: var(--color-text-primary);
+}
+
+.design-audit-layout {
+ display: grid;
+ grid-template-columns: 10rem minmax(0, 1fr);
+ align-items: start;
+ gap: 2rem;
+}
+
+.design-audit-nav {
+ position: sticky;
+ top: 1.5rem;
+ display: grid;
+ gap: 0.25rem;
+}
+
+.design-audit-nav a {
+ border-radius: var(--radius-md);
+ padding: 0.375rem 0.5rem;
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-text-tertiary);
+ text-decoration: none;
+}
+
+.design-audit-nav a:hover {
+ background: var(--color-hover);
+ color: var(--color-text-primary);
+}
+
+.design-audit-preview {
+ width: 100%;
+ min-width: 0;
+ margin-inline: auto;
+}
+
+.design-audit-preview[data-preview-width="narrow"] {
+ max-width: 24.375rem;
+}
+
+.design-audit-section {
+ padding-block: 2.5rem;
+ border-block-start: var(--edge-width) solid var(--color-edge);
+}
+
+.design-audit-section:first-child {
+ padding-block-start: 0;
+ border-block-start: 0;
+}
+
+.design-audit-section h2 {
+ font-size: var(--text-xl);
+ font-weight: 600;
+ line-height: var(--text-xl--line-height);
+ text-wrap: balance;
+}
+
+.design-audit-section-summary {
+ margin: 0.375rem 0 0;
+ font-size: var(--text-sm);
+ color: var(--color-text-quaternary);
+}
+
+.design-audit-plate {
+ padding-block: 1.5rem;
+}
+
+.design-audit-plate > header {
+ display: grid;
+ gap: 0.25rem;
+ margin-block-end: 1rem;
+}
+
+.design-audit-plate h3 {
+ font-size: var(--text-lg);
+ font-weight: 600;
+ line-height: var(--text-lg--line-height);
+ text-wrap: balance;
+}
+
+.design-audit-plate p {
+ max-width: 70ch;
+ font-size: var(--text-sm);
+ color: var(--color-text-tertiary);
+ text-wrap: pretty;
+}
+
+.design-audit-specimen {
+ min-width: 0;
+}
+
+.design-audit-state {
+ display: inline-flex;
+ align-items: center;
+ min-height: 1.5rem;
+ padding-inline: 0.5rem;
+ border-radius: 9999px;
+ background: var(--color-gray-200);
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-text-tertiary);
+}
+
+@media (max-width: 639px) {
+ .design-audit {
+ padding-right: max(1rem, env(safe-area-inset-right));
+ padding-left: max(1rem, env(safe-area-inset-left));
+ }
+
+ .design-audit-header {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .design-audit-layout {
+ display: block;
+ }
+
+ .design-audit-nav {
+ position: static;
+ display: flex;
+ overflow-x: auto;
+ margin-block-end: 2rem;
+ }
+}
From 0ea1b2855179420ccbd782646b52e47055b80ccb Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:16:49 +0100
Subject: [PATCH 05/35] Catalogue design foundations and controls
---
apps/web/src/design-audit/controls.css | 287 +++++++++++++++++++
apps/web/src/design-audit/controls.tsx | 277 ++++++++++++++++++
apps/web/src/design-audit/foundations.css | 185 ++++++++++++
apps/web/src/design-audit/foundations.tsx | 133 +++++++++
apps/web/src/design-audit/frame.tsx | 9 +-
apps/web/src/design-audit/icons.tsx | 133 +++++++++
apps/web/src/design-audit/page.tsx | 16 +-
apps/web/src/design-audit/specimens.test.tsx | 40 +++
apps/web/src/design-audit/styles.css | 3 +
9 files changed, 1077 insertions(+), 6 deletions(-)
create mode 100644 apps/web/src/design-audit/controls.css
create mode 100644 apps/web/src/design-audit/controls.tsx
create mode 100644 apps/web/src/design-audit/foundations.css
create mode 100644 apps/web/src/design-audit/foundations.tsx
create mode 100644 apps/web/src/design-audit/icons.tsx
create mode 100644 apps/web/src/design-audit/specimens.test.tsx
diff --git a/apps/web/src/design-audit/controls.css b/apps/web/src/design-audit/controls.css
new file mode 100644
index 00000000..3e4024dc
--- /dev/null
+++ b/apps/web/src/design-audit/controls.css
@@ -0,0 +1,287 @@
+.design-audit-button-families,
+.design-audit-button-families > div,
+.design-audit-control-state {
+ display: grid;
+}
+
+.design-audit-button-families {
+ gap: 1.5rem;
+}
+
+.design-audit-button-families > div {
+ gap: 0.75rem;
+}
+
+.design-audit-button-families h4 {
+ margin: 0;
+ font-size: var(--text-sm);
+ font-weight: 600;
+ color: var(--color-text-secondary);
+}
+
+.design-audit-control-grid,
+.design-audit-button-sizes {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
+ gap: 0.75rem;
+}
+
+.design-audit-control-state {
+ align-content: start;
+ justify-items: start;
+ gap: 0.5rem;
+}
+
+.design-audit-control-state > div {
+ display: flex;
+ min-height: 2.5rem;
+ align-items: center;
+}
+
+.design-audit-busy-mark {
+ width: 0.5rem;
+ height: 0.5rem;
+ margin-inline-end: 0.375rem;
+ border-radius: 9999px;
+ background: currentColor;
+}
+
+.design-audit [data-audit-state="hover"].btn-primary {
+ background: var(--color-brand-hover);
+}
+
+.design-audit [data-audit-state="active"].btn-primary {
+ background: var(--color-brand-active);
+}
+
+.design-audit [data-audit-state="hover"].btn-secondary {
+ background: var(--color-gray-300);
+}
+
+.design-audit [data-audit-state="active"].btn-secondary {
+ background: var(--color-gray-400);
+}
+
+.design-audit [data-audit-state="hover"].btn-ghost {
+ background: var(--color-gray-200);
+}
+
+.design-audit [data-audit-state="active"].btn-ghost {
+ background: var(--color-gray-300);
+}
+
+.design-audit [data-audit-state="hover"].btn-destructive {
+ background: var(--color-destructive-hover);
+}
+
+.design-audit [data-audit-state="active"].btn-destructive {
+ background: var(--color-destructive-active);
+}
+
+.design-audit [data-audit-state="focus"] {
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
+ outline-offset: var(--focus-ring-offset);
+}
+
+.design-audit-link {
+ border-radius: var(--radius-sm);
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-brand-ink);
+ text-decoration: underline;
+ text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
+ text-decoration-thickness: 1px;
+ text-underline-offset: 0.18em;
+}
+
+.design-audit-link[data-audit-state="hover"] {
+ color: var(--color-brand-hover);
+ text-decoration-color: currentColor;
+}
+
+.design-audit-link[data-audit-state="active"] {
+ color: var(--color-brand-active);
+}
+
+.design-audit-link[aria-disabled="true"] {
+ color: var(--color-text-quaternary);
+ text-decoration: none;
+}
+
+.design-audit-field-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.design-audit-field-grid label {
+ display: grid;
+ gap: 0.375rem;
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-text-secondary);
+}
+
+.design-audit-field-grid :is(input, select, textarea) {
+ width: 100%;
+ min-width: 0;
+ padding: 0.375rem 0.5rem;
+ font-size: var(--text-sm);
+ line-height: var(--text-sm--line-height);
+}
+
+.design-audit-field-wide {
+ grid-column: 1 / -1;
+}
+
+.design-audit-choice-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem 1.5rem;
+ margin-block-end: 1rem;
+}
+
+.design-audit-choice-row label {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: var(--text-sm);
+ color: var(--color-text-secondary);
+}
+
+.design-audit-choice-row input {
+ width: 1rem;
+ height: 1rem;
+}
+
+.design-audit-selection-list,
+.design-audit-menu,
+.design-audit-dropdown-list {
+ width: min(100%, 18rem);
+ padding: 0.25rem;
+ border-radius: var(--radius-lg);
+ background: var(--color-page);
+ box-shadow: var(--shadow-overlay);
+}
+
+.design-audit-selection-list [role="option"],
+.design-audit-dropdown-list [role="option"] {
+ display: flex;
+ min-height: 2rem;
+ align-items: center;
+ gap: 0.5rem;
+ border-radius: var(--radius-md);
+ padding-inline: 0.5rem;
+ font-size: var(--text-sm);
+ color: var(--color-text-secondary);
+}
+
+.design-audit-selection-list [aria-selected="true"],
+.design-audit-dropdown-list [aria-selected="true"] {
+ background: var(--color-selected);
+ color: var(--color-text-primary);
+}
+
+.design-audit-selection-list [aria-disabled="true"] {
+ color: var(--color-text-quaternary);
+}
+
+.design-audit-tabs {
+ display: flex;
+ width: fit-content;
+ max-width: 100%;
+ gap: 0.25rem;
+ overflow-x: auto;
+ border-block-end: var(--edge-width) solid var(--color-edge);
+}
+
+.design-audit-tabs button {
+ position: relative;
+ min-height: 2rem;
+ padding-inline: 0.5rem;
+ font-size: var(--text-sm);
+ font-weight: 500;
+ white-space: nowrap;
+ color: var(--color-text-tertiary);
+}
+
+.design-audit-tabs button[aria-selected="true"] {
+ color: var(--color-text-primary);
+}
+
+.design-audit-tabs button[aria-selected="true"]::after {
+ position: absolute;
+ right: 0.5rem;
+ bottom: -1px;
+ left: 0.5rem;
+ height: 2px;
+ border-radius: 9999px;
+ background: var(--color-brand);
+ content: "";
+}
+
+.design-audit-tabs button:disabled {
+ color: var(--color-text-quaternary);
+}
+
+.design-audit-tabs button span {
+ margin-inline-start: 0.25rem;
+ font-variant-numeric: tabular-nums;
+}
+
+.design-audit-menu {
+ display: grid;
+}
+
+.design-audit-menu button {
+ min-height: 2rem;
+ border-radius: var(--radius-md);
+ padding-inline: 0.5rem;
+ font-size: var(--text-sm);
+ text-align: left;
+ color: var(--color-text-secondary);
+}
+
+.design-audit-menu button:hover {
+ background: var(--color-hover);
+ color: var(--color-text-primary);
+}
+
+.design-audit-menu .design-audit-menu-destructive {
+ margin-block-start: 0.25rem;
+ border-block-start: var(--edge-width) solid var(--color-edge);
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
+ color: var(--color-destructive-ink);
+}
+
+.design-audit-dropdown-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ gap: 2rem;
+}
+
+.design-audit-dropdown-trigger {
+ display: inline-flex;
+ min-height: 2rem;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding-inline: 0.5rem;
+ font-size: var(--text-sm);
+}
+
+.design-audit-dropdown-open {
+ display: grid;
+ gap: 0.25rem;
+}
+
+@media (max-width: 639px) {
+ .design-audit-field-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .design-audit-field-wide {
+ grid-column: auto;
+ }
+}
diff --git a/apps/web/src/design-audit/controls.tsx b/apps/web/src/design-audit/controls.tsx
new file mode 100644
index 00000000..74667aed
--- /dev/null
+++ b/apps/web/src/design-audit/controls.tsx
@@ -0,0 +1,277 @@
+import { CaretDownIcon, CheckIcon, PlusIcon } from "@phosphor-icons/react";
+
+import { AuditPlate, StateLabel } from "./frame";
+
+import type { ReactNode } from "react";
+
+const STATES = ["Default", "Hover", "Active", "Focus", "Disabled"] as const;
+
+function StateSample({ children, state }: { children: ReactNode; state: string }) {
+ return (
+
+ );
+}
+
+function ButtonRow(
+ { className, label }: { className: string; label: string },
+) {
+ return (
+
+ {STATES.map(state => (
+
+
+ {label}
+
+
+ ))}
+
+ );
+}
+
+export function Controls() {
+ return (
+ <>
+
+
+
+
Primary
+
+
+
+
Secondary
+
+
+
+
Ghost
+
+
+
+
Destructive
+
+
+
+
+ Medium
+
+
+ Small
+
+
+
+ Saving
+
+
+
+
+
+
+
+
+ {STATES.map(state => (
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ Default
+
+
+ Focus
+
+
+ Disabled
+
+
+ Error
+
+
+ Read only
+
+
+ Select
+ Active documents
+ Archived documents
+
+
+
+ Textarea
+
+
+
+
+
+
+
+ Unchecked
+
+
+ Checked
+
+
+ Disabled
+
+
+ Selected
+
+
+ Unselected
+
+
+
+
Architecture notes
+
+ Design system audit
+
+
Archived proposal
+
+
+
+
+
+ Document
+
+ Decisions 3
+
+ Unavailable
+
+
+
+
+
+ Rename document
+ Archive document
+
+ Delete document
+
+
+
+
+
+
+
+ Active documents
+
+
+
+ Active documents
+
+
+
+ Active documents
+
+
Archived documents
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/design-audit/foundations.css b/apps/web/src/design-audit/foundations.css
new file mode 100644
index 00000000..dedc15f1
--- /dev/null
+++ b/apps/web/src/design-audit/foundations.css
@@ -0,0 +1,185 @@
+.design-audit-swatch-grid,
+.design-audit-icon-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
+ gap: 0.75rem;
+}
+
+.design-audit-swatch-grid figure,
+.design-audit-icon-grid figure,
+.design-audit-token-row figure {
+ min-width: 0;
+ margin: 0;
+}
+
+.design-audit-swatch-grid figure > span {
+ display: block;
+ height: 4rem;
+ border: var(--edge-width) solid var(--color-edge);
+ border-radius: var(--radius-md);
+}
+
+.design-audit-swatch-grid figcaption,
+.design-audit-icon-grid figcaption {
+ display: grid;
+ gap: 0.125rem;
+ margin-block-start: 0.5rem;
+ font-size: var(--text-sm);
+ line-height: var(--text-sm--line-height);
+}
+
+.design-audit-swatch-grid code,
+.design-audit-icon-grid figcaption span {
+ overflow: hidden;
+ font-size: var(--text-sm);
+ color: var(--color-text-quaternary);
+ text-overflow: ellipsis;
+}
+
+.design-audit-type-stack {
+ display: grid;
+ gap: 1.25rem;
+}
+
+.design-audit-type-stack > div {
+ display: grid;
+ grid-template-columns: 9rem minmax(0, 1fr);
+ align-items: baseline;
+ gap: 1.5rem;
+}
+
+.design-audit-type-stack span {
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-text-quaternary);
+}
+
+.design-audit-type-stack strong {
+ font-weight: 500;
+ text-wrap: pretty;
+}
+
+.design-audit-spacing-scale {
+ display: grid;
+ gap: 0.625rem;
+}
+
+.design-audit-spacing-scale > div {
+ display: grid;
+ grid-template-columns: 3rem minmax(0, 1fr);
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.design-audit-spacing-scale code {
+ font-size: var(--text-sm);
+ color: var(--color-text-tertiary);
+}
+
+.design-audit-spacing-scale span {
+ display: block;
+ height: 0.5rem;
+ border-radius: var(--radius-sm);
+ background: var(--color-brand);
+}
+
+.design-audit-token-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
+ gap: 1rem;
+}
+
+.design-audit-token-row figure > span {
+ display: block;
+ height: 5rem;
+ border: var(--edge-width) solid var(--color-edge);
+ background: var(--color-page);
+}
+
+.design-audit-token-row figcaption {
+ margin-block-start: 0.5rem;
+ font-size: var(--text-sm);
+ font-weight: 500;
+ color: var(--color-text-tertiary);
+}
+
+.design-audit-icon-catalogue,
+.design-audit-icon-catalogue h4 {
+ margin: 0;
+}
+
+.design-audit-icon-catalogue {
+ display: grid;
+ gap: 1.5rem;
+}
+
+.design-audit-icon-catalogue h4 {
+ font-size: var(--text-sm);
+ font-weight: 600;
+ color: var(--color-text-secondary);
+}
+
+.design-audit-icon-grid figure {
+ display: grid;
+ grid-template-columns: 2rem minmax(0, 1fr);
+ align-items: center;
+ gap: 0.625rem;
+ padding: 0.5rem;
+ border-radius: var(--radius-md);
+ background: var(--color-page);
+}
+
+.design-audit-icon-grid figcaption {
+ margin: 0;
+}
+
+.design-audit-icon-frame {
+ display: grid;
+ width: 2rem;
+ height: 2rem;
+ place-items: center;
+ border-radius: var(--radius-sm);
+ background: var(--color-gray-100);
+ color: var(--color-text-secondary);
+}
+
+.design-audit-icon-frame img {
+ width: 1rem;
+ height: 1rem;
+ object-fit: contain;
+}
+
+.design-audit-icon-states,
+.design-audit-icon-sizes {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.design-audit-icon-states > div,
+.design-audit-icon-sizes > div {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: var(--text-sm);
+ font-weight: 500;
+}
+
+.design-audit-icon-states [data-icon-state="default"] {
+ color: var(--color-text-secondary);
+}
+
+.design-audit-icon-states [data-icon-state="active"] {
+ color: var(--color-brand);
+}
+
+.design-audit-icon-states [data-icon-state="disabled"] {
+ color: var(--color-gray-500);
+}
+
+@media (max-width: 639px) {
+ .design-audit-type-stack > div {
+ grid-template-columns: 1fr;
+ gap: 0.25rem;
+ }
+}
diff --git a/apps/web/src/design-audit/foundations.tsx b/apps/web/src/design-audit/foundations.tsx
new file mode 100644
index 00000000..23004214
--- /dev/null
+++ b/apps/web/src/design-audit/foundations.tsx
@@ -0,0 +1,133 @@
+import { AuditPlate } from "./frame";
+import { IconCatalogue } from "./icons";
+
+const COLOURS = [
+ ["Page", "--color-page"],
+ ["Ground", "--color-ground"],
+ ["Inset", "--color-inset"],
+ ["Selected", "--color-selected"],
+ ["Control", "--color-control"],
+ ["Primary text", "--color-text-primary"],
+ ["Secondary text", "--color-text-secondary"],
+ ["Tertiary text", "--color-text-tertiary"],
+ ["Quaternary text", "--color-text-quaternary"],
+ ["Brand", "--color-brand"],
+ ["Brand hover", "--color-brand-hover"],
+ ["Brand wash", "--color-brand-wash"],
+ ["Success", "--color-success"],
+ ["Success wash", "--color-success-wash"],
+ ["Warning", "--color-warning"],
+ ["Warning wash", "--color-warning-wash"],
+ ["Destructive", "--color-destructive"],
+ ["Destructive wash", "--color-destructive-wash"],
+] as const;
+
+const TYPE = [
+ ["Chrome and labels", "--text-sm", "--text-sm--line-height"],
+ ["Document prose", "--text-base", "--text-base--line-height"],
+ ["Subheading", "--text-lg", "--text-lg--line-height"],
+ ["Section heading", "--text-xl", "--text-xl--line-height"],
+ ["Document title", "--text-2xl", "--text-2xl--line-height"],
+] as const;
+
+const SPACING = [2, 4, 6, 8, 12, 16, 24, 32] as const;
+
+const RADII = [
+ ["Small", "--radius-sm"],
+ ["Medium", "--radius-md"],
+ ["Large", "--radius-lg"],
+ ["Extra large", "--radius-xl"],
+] as const;
+
+const SHADOWS = [
+ ["Resting", "--shadow-resting"],
+ ["Raised", "--shadow-raised"],
+ ["Overlay", "--shadow-overlay"],
+] as const;
+
+export function Foundations() {
+ return (
+ <>
+
+
+ {COLOURS.map(([label, token]) => (
+
+
+
+ {label}
+ {token}
+
+
+ ))}
+
+
+
+
+ {TYPE.map(([label, size, lineHeight]) => (
+
+ {label}
+ Several people and a Planner share one document.
+
+ ))}
+
+
+
+
+ {SPACING.map(value => (
+
+ {value}px
+
+
+ ))}
+
+
+
+
+ {RADII.map(([label, token]) => (
+
+
+ {label}
+
+ ))}
+
+
+
+
+ {SHADOWS.map(([label, token]) => (
+
+
+ {label}
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/design-audit/frame.tsx b/apps/web/src/design-audit/frame.tsx
index 2808b31e..5ba50004 100644
--- a/apps/web/src/design-audit/frame.tsx
+++ b/apps/web/src/design-audit/frame.tsx
@@ -60,10 +60,15 @@ export function AuditSection(
}
export function AuditPlate(
- { children, description, title }: { children?: ReactNode; description?: string; title: string },
+ { children, description, item, title }: {
+ children?: ReactNode;
+ description?: string;
+ item?: string;
+ title: string;
+ },
) {
return (
-
+
{title}
{description ? {description}
: null}
diff --git a/apps/web/src/design-audit/icons.tsx b/apps/web/src/design-audit/icons.tsx
new file mode 100644
index 00000000..52bb466f
--- /dev/null
+++ b/apps/web/src/design-audit/icons.tsx
@@ -0,0 +1,133 @@
+import {
+ ArrowUpIcon,
+ CaretDownIcon,
+ CaretRightIcon,
+ ChatCircleIcon,
+ CheckIcon,
+ InfoIcon,
+ LightbulbIcon,
+ PlusIcon,
+ SignInIcon,
+ SirenIcon,
+ StarFourIcon,
+ WarningIcon,
+ XIcon,
+} from "@phosphor-icons/react";
+
+import addProject from "../assets/figma/navigation/add-project.svg";
+import archive from "../assets/figma/navigation/box-archive.svg";
+import book from "../assets/figma/navigation/book-bookmark.svg";
+import chopin from "../assets/figma/navigation/chopin.svg";
+import collapse from "../assets/figma/navigation/collapse.svg";
+import documentActions from "../assets/figma/navigation/document-actions.svg";
+import newDocument from "../assets/figma/navigation/new-document.svg";
+import search from "../assets/figma/navigation/search.svg";
+import hideSidebar from "../assets/figma/navigation/sidebar-right-3-hide.svg";
+import conversationClose from "../assets/icons/conversation-close.svg";
+import conversation from "../assets/icons/conversation.svg";
+import navigationChevron from "../assets/icons/navigation-chevron-right.svg";
+import navigationXmark from "../assets/icons/navigation-xmark.svg";
+import plannerStop from "../assets/icons/planner-stop.svg";
+import sendArrow from "../assets/icons/send-arrow-up.svg";
+import toolChevronDown from "../assets/icons/tool-chevron-down.svg";
+import toolChevronRight from "../assets/icons/tool-chevron-right.svg";
+import toolLoader from "../assets/icons/tool-loader.svg";
+import linkPlus from "../../../../packages/editor/src/assets/icons/link-plus.svg";
+import messagePlus from "../../../../packages/editor/src/assets/icons/message-plus.svg";
+
+import type { Icon } from "@phosphor-icons/react";
+
+type LocalIcon = {
+ duplicate?: string;
+ name: string;
+ source: string;
+};
+
+const LOCAL_ICONS: readonly LocalIcon[] = [
+ { name: "Add project", source: addProject },
+ { name: "Book bookmark", source: book },
+ { name: "Archive", source: archive },
+ { name: "Chopin", source: chopin },
+ { duplicate: "conversation-close.svg", name: "Collapse", source: collapse },
+ { name: "Document actions", source: documentActions },
+ { name: "New document", source: newDocument },
+ { name: "Search", source: search },
+ { name: "Hide sidebar", source: hideSidebar },
+ { duplicate: "collapse.svg", name: "Conversation close", source: conversationClose },
+ { name: "Conversation", source: conversation },
+ { name: "Navigation chevron", source: navigationChevron },
+ { name: "Navigation close", source: navigationXmark },
+ { name: "Planner stop", source: plannerStop },
+ { name: "Send", source: sendArrow },
+ { name: "Tool chevron down", source: toolChevronDown },
+ { name: "Tool chevron right", source: toolChevronRight },
+ { name: "Tool loader", source: toolLoader },
+ { name: "Link plus", source: linkPlus },
+ { name: "Message plus", source: messagePlus },
+];
+
+const PHOSPHOR_ICONS: readonly { icon: Icon; name: string }[] = [
+ { icon: ArrowUpIcon, name: "Arrow up" },
+ { icon: CaretDownIcon, name: "Caret down" },
+ { icon: CaretRightIcon, name: "Caret right" },
+ { icon: ChatCircleIcon, name: "Chat circle" },
+ { icon: CheckIcon, name: "Check" },
+ { icon: InfoIcon, name: "Info" },
+ { icon: LightbulbIcon, name: "Lightbulb" },
+ { icon: PlusIcon, name: "Plus" },
+ { icon: SignInIcon, name: "Sign in" },
+ { icon: SirenIcon, name: "Siren" },
+ { icon: StarFourIcon, name: "Important" },
+ { icon: WarningIcon, name: "Warning" },
+ { icon: XIcon, name: "Close" },
+];
+
+export function IconCatalogue() {
+ return (
+
+
+ {(["Default", "Active", "Disabled"] as const).map(state => (
+
+
+ {state}
+
+ ))}
+
+
Local SVG assets
+
+ {LOCAL_ICONS.map(icon => (
+
+
+
+
+
+ {icon.name}
+ {icon.duplicate ? Exact duplicate: {icon.duplicate} : null}
+
+
+ ))}
+
+
Phosphor components
+
+ {PHOSPHOR_ICONS.map(({ icon: Glyph, name }) => (
+
+
+
+
+
+ {name}
+
+
+ ))}
+
+
+ {([14, 16, 20] as const).map(size => (
+
+
+ {size}px
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/design-audit/page.tsx b/apps/web/src/design-audit/page.tsx
index 80918ee1..e434f65a 100644
--- a/apps/web/src/design-audit/page.tsx
+++ b/apps/web/src/design-audit/page.tsx
@@ -1,16 +1,24 @@
import { AuditFrame, AuditSection } from "./frame";
+import { Controls } from "./controls";
+import { Foundations } from "./foundations";
import { AUDIT_INVENTORY } from "./inventory";
+import "./controls.css";
+import "./foundations.css";
import "./styles.css";
export function DesignAuditPage() {
return (
- {AUDIT_INVENTORY.map(group => (
+
+
+
+
+
+
+ {AUDIT_INVENTORY.slice(2).map(group => (
-
- {group.items.length} component {group.items.length === 1 ? "family" : "families"}
-
+ {group.items.length} component families
))}
diff --git a/apps/web/src/design-audit/specimens.test.tsx b/apps/web/src/design-audit/specimens.test.tsx
new file mode 100644
index 00000000..9542d24b
--- /dev/null
+++ b/apps/web/src/design-audit/specimens.test.tsx
@@ -0,0 +1,40 @@
+import { describe, expect, it } from "bun:test";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { Controls } from "./controls";
+import { Foundations } from "./foundations";
+
+describe("design audit specimens", () => {
+ it("renders every foundation family with a visible label", () => {
+ let markup = renderToStaticMarkup(createElement(Foundations));
+
+ for (let id of ["colours", "typography", "spacing", "radii", "shadows", "icons"]) {
+ expect(markup).toContain(`data-audit-item="${id}"`);
+ }
+ expect(markup).toContain("Exact duplicate");
+ });
+
+ it("renders controls with their native accessibility states", () => {
+ let markup = renderToStaticMarkup(createElement(Controls));
+
+ for (
+ let id of [
+ "buttons",
+ "icon-buttons",
+ "links",
+ "fields",
+ "selections",
+ "tabs",
+ "menus",
+ "dropdowns",
+ ]
+ ) {
+ expect(markup).toContain(`data-audit-item="${id}"`);
+ }
+ expect(markup).toContain('aria-label="Add document"');
+ expect(markup).toContain("disabled");
+ expect(markup).toContain('aria-selected="true"');
+ expect(markup).toContain('role="menu"');
+ });
+});
diff --git a/apps/web/src/design-audit/styles.css b/apps/web/src/design-audit/styles.css
index 7550c40f..77150c76 100644
--- a/apps/web/src/design-audit/styles.css
+++ b/apps/web/src/design-audit/styles.css
@@ -1,5 +1,8 @@
.design-audit {
+ height: 100%;
min-height: 100dvh;
+ overflow: auto;
+ scroll-padding-top: 1.5rem;
padding: max(1.5rem, env(safe-area-inset-top)) max(1.5rem, env(safe-area-inset-right))
max(2.5rem, env(safe-area-inset-bottom)) max(1.5rem, env(safe-area-inset-left));
background: var(--color-ground);
From b80fec6a4338b0420d82e9e9e066545dfd2cc2e1 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:19:58 +0100
Subject: [PATCH 06/35] Catalogue application surfaces
---
apps/web/src/design-audit/page.tsx | 7 +-
apps/web/src/design-audit/specimens.test.tsx | 14 +
apps/web/src/design-audit/surfaces.css | 275 +++++++++++++++
apps/web/src/design-audit/surfaces.tsx | 335 +++++++++++++++++++
4 files changed, 630 insertions(+), 1 deletion(-)
create mode 100644 apps/web/src/design-audit/surfaces.css
create mode 100644 apps/web/src/design-audit/surfaces.tsx
diff --git a/apps/web/src/design-audit/page.tsx b/apps/web/src/design-audit/page.tsx
index e434f65a..68a99573 100644
--- a/apps/web/src/design-audit/page.tsx
+++ b/apps/web/src/design-audit/page.tsx
@@ -2,9 +2,11 @@ import { AuditFrame, AuditSection } from "./frame";
import { Controls } from "./controls";
import { Foundations } from "./foundations";
import { AUDIT_INVENTORY } from "./inventory";
+import { Surfaces } from "./surfaces";
import "./controls.css";
import "./foundations.css";
+import "./surfaces.css";
import "./styles.css";
export function DesignAuditPage() {
@@ -16,7 +18,10 @@ export function DesignAuditPage() {
- {AUDIT_INVENTORY.slice(2).map(group => (
+
+
+
+ {AUDIT_INVENTORY.slice(3).map(group => (
{group.items.length} component families
diff --git a/apps/web/src/design-audit/specimens.test.tsx b/apps/web/src/design-audit/specimens.test.tsx
index 9542d24b..6807e3d9 100644
--- a/apps/web/src/design-audit/specimens.test.tsx
+++ b/apps/web/src/design-audit/specimens.test.tsx
@@ -4,6 +4,8 @@ import { renderToStaticMarkup } from "react-dom/server";
import { Controls } from "./controls";
import { Foundations } from "./foundations";
+import { AUDIT_INVENTORY } from "./inventory";
+import { Surfaces } from "./surfaces";
describe("design audit specimens", () => {
it("renders every foundation family with a visible label", () => {
@@ -37,4 +39,16 @@ describe("design audit specimens", () => {
expect(markup).toContain('aria-selected="true"');
expect(markup).toContain('role="menu"');
});
+
+ it("renders every application surface and its meaningful states", () => {
+ let markup = renderToStaticMarkup(createElement(Surfaces));
+
+ for (let item of AUDIT_INVENTORY.find(group => group.id === "surfaces")!.items) {
+ expect(markup).toContain(`data-audit-item="${item.id}"`);
+ }
+ expect(markup).toContain('role="dialog"');
+ expect(markup).toContain('aria-current="page"');
+ expect(markup).toContain('data-chat-entry="true"');
+ expect(markup).toContain('role="alert"');
+ });
});
diff --git a/apps/web/src/design-audit/surfaces.css b/apps/web/src/design-audit/surfaces.css
new file mode 100644
index 00000000..0d2a5826
--- /dev/null
+++ b/apps/web/src/design-audit/surfaces.css
@@ -0,0 +1,275 @@
+.design-audit-dialog-grid,
+.design-audit-card-grid,
+.design-audit-empty-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
+ gap: 1rem;
+}
+
+.design-audit-dialog-grid > div {
+ display: grid;
+ align-content: start;
+ gap: 0.5rem;
+}
+
+.design-audit-dialog {
+ padding: 1rem;
+ border-radius: var(--radius-xl);
+ background: var(--color-page);
+ box-shadow: var(--shadow-overlay);
+}
+
+.design-audit-dialog h4,
+.design-audit-dialog p {
+ margin: 0;
+}
+
+.design-audit-dialog h4 {
+ font-size: var(--text-lg);
+ font-weight: 600;
+}
+
+.design-audit-dialog p,
+.design-audit-dialog label,
+.design-audit-dialog [role="alert"] {
+ font-size: var(--text-sm);
+ line-height: var(--text-sm--line-height);
+}
+
+.design-audit-dialog label {
+ display: grid;
+ gap: 0.375rem;
+ margin-block-start: 0.75rem;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+}
+
+.design-audit-dialog [role="alert"] {
+ margin-block-start: 0.75rem;
+ color: var(--color-destructive-ink);
+}
+
+.design-audit-dialog-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin-block-start: 1.25rem;
+}
+
+.design-audit-surface-list,
+.design-audit-sidebar {
+ width: min(100%, 21rem);
+ padding: 0.375rem;
+ border-radius: var(--radius-lg);
+ background: var(--color-page);
+ box-shadow: var(--shadow-resting);
+}
+
+.design-audit-surface-list [role="option"],
+.design-audit-sidebar a,
+.design-audit-navigation-loading {
+ display: flex;
+ min-height: 2rem;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 0.5rem;
+ padding-inline: 0.625rem;
+ border-radius: var(--radius-md);
+ font-size: var(--text-sm);
+ color: var(--color-text-secondary);
+ text-align: left;
+}
+
+.design-audit-surface-list [role="option"] svg:last-child:not(:first-child) {
+ margin-inline-start: auto;
+}
+
+.design-audit-surface-list [aria-selected="true"],
+.design-audit-sidebar a[aria-current="page"] {
+ background: var(--color-selected);
+ color: var(--color-text-primary);
+}
+
+.design-audit-surface-list [data-audit-state="hover"] {
+ background: var(--color-hover);
+}
+
+.design-audit-empty-row {
+ display: flex;
+ min-height: 5rem;
+ width: min(100%, 21rem);
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ margin-block-start: 0.75rem;
+ border: var(--edge-width) dashed var(--color-edge-strong);
+ border-radius: var(--radius-lg);
+ font-size: var(--text-sm);
+ color: var(--color-text-tertiary);
+}
+
+.design-audit-sidebar-heading {
+ display: flex;
+ min-height: 2.25rem;
+ align-items: center;
+ justify-content: space-between;
+ padding-inline-start: 0.625rem;
+ font-size: var(--text-sm);
+ font-weight: 600;
+}
+
+.design-audit-sidebar a[data-navigation-state="ancestor"] {
+ font-weight: 500;
+}
+
+.design-audit-sidebar a[data-navigation-state="archived"] {
+ color: var(--color-text-quaternary);
+}
+
+.design-audit-navigation-loading svg {
+ animation: design-audit-spin 900ms linear infinite;
+}
+
+@keyframes design-audit-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.design-audit-conversation {
+ display: flex;
+ height: 33rem;
+ max-height: 70dvh;
+ flex-direction: column;
+ overflow: hidden;
+ border: var(--edge-width) solid var(--color-edge-strong);
+ border-radius: var(--radius-xl);
+ background: var(--color-conversation);
+}
+
+.design-audit-conversation .conversation-composer textarea {
+ width: 100%;
+ min-height: 4.5rem;
+ resize: none;
+ border: 0;
+ background: transparent;
+}
+
+.design-audit-composer-actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 0 0.5rem 0.5rem 0.75rem;
+}
+
+.design-audit-composer-actions span {
+ font-size: var(--text-sm);
+ color: var(--color-text-quaternary);
+}
+
+.design-audit-chat-error {
+ margin-block-start: 0.5rem;
+ font-size: var(--text-sm);
+ color: var(--color-destructive-ink);
+}
+
+.design-audit-card-grid > article {
+ height: fit-content;
+}
+
+.design-audit-choice-stack {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.design-audit-resolved-toggle {
+ margin-block-start: 0.75rem;
+}
+
+.design-audit-muted,
+.design-audit-card-grid blockquote,
+.design-audit-card-grid p {
+ margin: 0;
+ font-size: var(--text-sm);
+ color: var(--color-text-secondary);
+}
+
+.design-audit-card-grid article > div:first-child span {
+ font-size: var(--text-sm);
+ color: var(--color-text-tertiary);
+}
+
+.design-audit-status-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
+ gap: 0.75rem;
+}
+
+.design-audit-status-grid > div {
+ display: grid;
+ min-width: 0;
+ align-content: start;
+ gap: 0.5rem;
+ min-height: 4rem;
+ padding: 0.75rem;
+ border-radius: var(--radius-lg);
+ background: var(--color-page);
+}
+
+.design-audit-status-grid .plan-status {
+ position: static;
+ inset: auto;
+ max-width: 100%;
+}
+
+.design-audit-empty-grid > div {
+ display: flex;
+ min-height: 13rem;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 1.25rem;
+ border: var(--edge-width) solid var(--color-edge);
+ border-radius: var(--radius-lg);
+ text-align: center;
+}
+
+.design-audit-empty-grid strong {
+ font-size: var(--text-md);
+}
+
+.design-audit-empty-grid span {
+ max-width: 18rem;
+ font-size: var(--text-sm);
+ color: var(--color-text-secondary);
+}
+
+.design-audit-error-stack {
+ display: grid;
+ gap: 0.75rem;
+ font-size: var(--text-sm);
+}
+
+.design-audit-error-stack [role="alert"] {
+ color: var(--color-destructive-ink);
+}
+
+.design-audit-recoverable-error {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.75rem;
+ border-radius: var(--radius-lg);
+ background: var(--color-destructive-soft);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .design-audit-navigation-loading svg {
+ animation: none;
+ }
+}
diff --git a/apps/web/src/design-audit/surfaces.tsx b/apps/web/src/design-audit/surfaces.tsx
new file mode 100644
index 00000000..e1edc3b7
--- /dev/null
+++ b/apps/web/src/design-audit/surfaces.tsx
@@ -0,0 +1,335 @@
+import {
+ ArchiveIcon,
+ CaretDownIcon,
+ CaretRightIcon,
+ ChatCircleIcon,
+ CheckCircleIcon,
+ CircleNotchIcon,
+ FileTextIcon,
+ MagnifyingGlassIcon,
+ PlusIcon,
+} from "@phosphor-icons/react";
+
+import { PlanStatus, SendAction } from "@chopin/editor";
+import { SidecarCard } from "../../../../packages/editor/src/card";
+import { DecisionCard } from "../../../../packages/editor/src/widgets/decision";
+import { Transcript } from "../chat/transcript";
+import { TerminalAlert } from "../terminal-alert";
+import { AuditPlate, StateLabel } from "./frame";
+
+import type { Chat } from "@chopin/protocol";
+
+let CHAT_ENTRIES: Chat.Entry[] = [
+ {
+ author: { kind: "member", handle: "maggieappleton" },
+ id: "audit-member",
+ text: "Can you make the empty state clearer?",
+ ts: 1_787_913_600,
+ },
+ {
+ author: { kind: "agent" },
+ id: "audit-planner",
+ text: "I’ll compare the existing states and propose a smaller, consistent pattern.",
+ tools: [
+ { id: "audit-tool-1", name: "read_plan", status: "done", took: 420 },
+ { id: "audit-tool-2", name: "update_plan", status: "done", took: 180 },
+ ],
+ ts: 1_787_913_660,
+ },
+];
+
+function DialogSpecimens() {
+ return (
+
+
+
+
Open
+
+
Rename document
+
+ Document name
+
+
+ Cancel
+ Save
+
+
+
+
+
Destructive + error
+
+
Delete document permanently?
+
+ Design system audit {" "}
+ will be permanently deleted. This cannot be undone.
+
+
Could not delete the document.
+
+ Cancel
+
+ Delete permanently
+
+
+
+
+
+
+ );
+}
+
+function Lists() {
+ return (
+
+
+
+ Design notes
+
+
+ Design system audit
+
+
+ Editor architecture
+
+
+
+
+ No matching documents
+
+
+ );
+}
+
+function Navigation() {
+ return (
+
+
+
+
+ Product direction
+
+
+ Design system audit
+
+
+ Research notes
+
+
+ Archived draft
+
+
+ Loading more…
+
+
+
+ );
+}
+
+function Conversation() {
+ return (
+
+
+
{}}
+ queued={[{
+ id: "audit-queued",
+ handle: "maggieappleton",
+ text: "Also check the compact layout.",
+ }]}
+ />
+
+
+
+
+ Use # to reference a document
+ {}} />
+
+
+
+ Message could not be sent. Try again.
+
+
+
+
+ );
+}
+
+function Decisions() {
+ return (
+ <>
+
+
+
+ Which density should the navigation use?
+
+ Compact
+ Comfortable
+
+
+
+
+
+
+ 3
+ resolved
+
+
+
+
+
Accepted by @maggieappleton}>
+ Give controls a little more room.
+ Raised the shared horizontal inset.
+
+
Resolved}>
+ No prose was linked intentionally.
+
+
Orphaned}>
+
+ The original passage can no longer be located safely.
+
+
+
+
+ >
+ );
+}
+
+function Feedback() {
+ return (
+ <>
+
+
+
+
+
+
+
+ No documents yet
+ Create the first shared document for this project.
+ Create document
+
+
+
+ No results
+ Try a shorter search or clear the filters.
+ Clear search
+
+
+
+ Conversation unavailable
+ Reconnect to keep collaborating.
+ Try again
+
+
+
+
+
+
Enter a document name.
+
Could not open this document. Reloading may help.
+
+ Research stopped before it finished.
+ Retry
+
+
+
+ >
+ );
+}
+
+export function Surfaces() {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
From 11f26ce4f0b8b3f07b9a9fcf6a9acc4974d3ee96 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:24:54 +0100
Subject: [PATCH 07/35] Catalogue authored document components
---
.../web/src/design-audit/authored-content.css | 56 +++++
.../web/src/design-audit/authored-content.tsx | 238 ++++++++++++++++++
apps/web/src/design-audit/page.tsx | 10 +-
apps/web/src/design-audit/specimens.test.tsx | 12 +
packages/editor/package.json | 4 +
packages/editor/src/plan-editor.tsx | 17 +-
packages/editor/src/plan-theme.ts | 13 +
.../editor/src/static-plan-editor.test.tsx | 19 ++
packages/editor/src/static-plan-editor.tsx | 83 ++++++
9 files changed, 433 insertions(+), 19 deletions(-)
create mode 100644 apps/web/src/design-audit/authored-content.css
create mode 100644 apps/web/src/design-audit/authored-content.tsx
create mode 100644 packages/editor/src/plan-theme.ts
create mode 100644 packages/editor/src/static-plan-editor.test.tsx
create mode 100644 packages/editor/src/static-plan-editor.tsx
diff --git a/apps/web/src/design-audit/authored-content.css b/apps/web/src/design-audit/authored-content.css
new file mode 100644
index 00000000..954091a1
--- /dev/null
+++ b/apps/web/src/design-audit/authored-content.css
@@ -0,0 +1,56 @@
+.design-audit-static-plan {
+ width: 100%;
+ min-width: 0;
+ border: var(--edge-width) solid var(--color-edge);
+ border-radius: var(--radius-lg);
+ background: var(--color-page);
+}
+
+.design-audit-static-plan .plan-workspace,
+.design-audit-static-plan .plan-document {
+ height: auto;
+ min-height: 0;
+}
+
+.design-audit-static-plan .plan-content {
+ min-height: 0;
+ padding: 1rem;
+}
+
+.design-audit-static-plan .mdxeditor {
+ min-height: 0;
+}
+
+.design-audit-research-composer {
+ display: grid;
+ gap: 0.5rem;
+ margin-block-end: 1rem;
+}
+
+.design-audit-research-composer .plan-research-composer {
+ width: min(100%, 32rem);
+}
+
+.design-audit-research-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
+ gap: 1rem;
+}
+
+.design-audit-research-grid > div {
+ display: grid;
+ align-content: start;
+ gap: 0.5rem;
+}
+
+.design-audit-research-grid article {
+ height: 100%;
+}
+
+.design-audit-static-plan .plan-image {
+ max-height: 10rem;
+}
+
+.design-audit-static-plan .plan-diagram {
+ max-height: 18rem;
+}
diff --git a/apps/web/src/design-audit/authored-content.tsx b/apps/web/src/design-audit/authored-content.tsx
new file mode 100644
index 00000000..463acd7d
--- /dev/null
+++ b/apps/web/src/design-audit/authored-content.tsx
@@ -0,0 +1,238 @@
+import { ResearchCard, ResearchComposer } from "@chopin/editor";
+import { StaticPlanEditor } from "@chopin/editor/static";
+
+import { AuditPlate, StateLabel } from "./frame";
+
+import type { Research } from "@chopin/protocol";
+
+let callouts = [
+ '',
+ "",
+ "A neutral detail that supports the surrounding prose.",
+ "",
+ " ",
+ "",
+ '',
+ "",
+ "A useful shortcut or recommended next step.",
+ "",
+ " ",
+ "",
+ '',
+ "",
+ "A condition that needs attention before continuing.",
+ "",
+ " ",
+ "",
+ '',
+ "",
+ "An action with serious or irreversible consequences.",
+ "",
+ " ",
+].join("\n") + "\n";
+
+let code = [
+ "```",
+ "Plain text keeps its authored appearance.",
+ "```",
+ "",
+ '```typescript title="tokens.ts"',
+ "export const spacing = { compact: 8, comfortable: 12 };",
+ "```",
+ "",
+ "```css collapsed",
+ ".button { padding-inline: var(--space-3); }",
+ "```",
+].join("\n") + "\n";
+
+let diff = [
+ "```diff",
+ "--- a/button.css",
+ "+++ b/button.css",
+ "@@ -1 +1 @@",
+ "-padding-inline: 8px;",
+ "+padding-inline: 12px;",
+ "```",
+ "",
+ "```diff",
+ "This is temporarily invalid patch content.",
+ "```",
+].join("\n") + "\n";
+
+let diagram = [
+ "```mermaid",
+ "flowchart LR",
+ " Tokens --> Controls",
+ " Controls --> Surfaces",
+ " Surfaces --> Documents",
+ "```",
+ "",
+ "```mermaid",
+ "this is not a valid diagram",
+ "```",
+].join("\n") + "\n";
+
+let formula = [
+ "Inline spacing can be described as $s_n = 4n$ pixels.",
+ "",
+ "$$",
+ "c = \\sqrt{a^2 + b^2}",
+ "$$",
+].join("\n") + "\n";
+
+let images = [
+ "",
+ "",
+ "",
+ "",
+ "",
+].join("\n") + "\n";
+
+let table = [
+ "| Component | Default | Active |",
+ "| :-- | :-- | :-- |",
+ "| Button | Secondary | Brand |",
+ "| Icon | 16px tertiary | 16px brand |",
+ "| Row | Page | Selected wash |",
+].join("\n") + "\n";
+
+let timestamp = "2026-08-28T10:00:00.000Z";
+
+function request(
+ stage: Research.RequestStage,
+): Research.RequestView {
+ let base: Research.RequestViewBase = {
+ channelId: "audit-channel",
+ createdAt: timestamp,
+ id: `audit-research-${stage}`,
+ question: "Which interaction patterns should the design system standardize?",
+ sources: [],
+ updatedAt: timestamp,
+ };
+ if (stage === "failed") {
+ return { ...base, error: "The source search timed out.", stage, state: "failed" };
+ }
+ if (stage === "cancelled") return { ...base, stage, state: "cancelled" };
+ if (stage === "ready") {
+ return {
+ ...base,
+ child: {
+ id: "audit-research-child",
+ slug: "design-system-patterns",
+ sourceCount: 8,
+ summary: "A comparison of durable interaction patterns.",
+ title: "Design system interaction patterns",
+ },
+ stage,
+ state: "completed",
+ };
+ }
+ return { ...base, stage, state: stage === "queued" ? "pending" : "running" };
+}
+
+function EditorPlate(
+ { description, item, source, title }: {
+ description: string;
+ item: string;
+ source: string;
+ title: string;
+ },
+) {
+ return (
+
+
+
+ );
+}
+
+function ResearchSpecimens() {
+ let stages: Research.RequestStage[] = [
+ "queued",
+ "searching",
+ "writing",
+ "failed",
+ "cancelled",
+ "ready",
+ ];
+ return (
+
+
+ Question
+ {}}
+ onChange={() => {}}
+ onSubmit={() => {}}
+ question="Where does the current interface drift from its shared tokens?"
+ />
+
+
+ {stages.map(stage => (
+
+ {stage}
+ {}}
+ onOpen={() => {}}
+ onRemove={() => {}}
+ onRetry={() => {}}
+ request={request(stage)}
+ />
+
+ ))}
+
+
+ );
+}
+
+export function AuthoredContent() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/design-audit/page.tsx b/apps/web/src/design-audit/page.tsx
index 68a99573..cf0fc70e 100644
--- a/apps/web/src/design-audit/page.tsx
+++ b/apps/web/src/design-audit/page.tsx
@@ -1,10 +1,12 @@
import { AuditFrame, AuditSection } from "./frame";
+import { AuthoredContent } from "./authored-content";
import { Controls } from "./controls";
import { Foundations } from "./foundations";
import { AUDIT_INVENTORY } from "./inventory";
import { Surfaces } from "./surfaces";
import "./controls.css";
+import "./authored-content.css";
import "./foundations.css";
import "./surfaces.css";
import "./styles.css";
@@ -21,11 +23,9 @@ export function DesignAuditPage() {
- {AUDIT_INVENTORY.slice(3).map(group => (
-
- {group.items.length} component families
-
- ))}
+
+
+
);
}
diff --git a/apps/web/src/design-audit/specimens.test.tsx b/apps/web/src/design-audit/specimens.test.tsx
index 6807e3d9..803b71d6 100644
--- a/apps/web/src/design-audit/specimens.test.tsx
+++ b/apps/web/src/design-audit/specimens.test.tsx
@@ -3,6 +3,7 @@ import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { Controls } from "./controls";
+import { AuthoredContent } from "./authored-content";
import { Foundations } from "./foundations";
import { AUDIT_INVENTORY } from "./inventory";
import { Surfaces } from "./surfaces";
@@ -51,4 +52,15 @@ describe("design audit specimens", () => {
expect(markup).toContain('data-chat-entry="true"');
expect(markup).toContain('role="alert"');
});
+
+ it("renders every authored-content family through the static editor or record card", () => {
+ let markup = renderToStaticMarkup(createElement(AuthoredContent));
+
+ for (let item of AUDIT_INVENTORY.find(group => group.id === "authored-content")!.items) {
+ expect(markup).toContain(`data-audit-item="${item.id}"`);
+ }
+ expect(markup).toContain('role="document"');
+ expect(markup).toContain("Research question");
+ expect(markup).toContain("Research ready");
+ });
});
diff --git a/packages/editor/package.json b/packages/editor/package.json
index cca8eece..88f97a8b 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -19,6 +19,10 @@
"types": "./src/pointer.ts",
"default": "./src/pointer.ts"
},
+ "./static": {
+ "types": "./src/static-plan-editor.tsx",
+ "default": "./src/static-plan-editor.tsx"
+ },
"./transition-presence": {
"types": "./src/transition-presence.ts",
"default": "./src/transition-presence.ts"
diff --git a/packages/editor/src/plan-editor.tsx b/packages/editor/src/plan-editor.tsx
index 3f5e0d07..6e3b6b10 100644
--- a/packages/editor/src/plan-editor.tsx
+++ b/packages/editor/src/plan-editor.tsx
@@ -7,7 +7,7 @@
*/
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { lexicalTheme, markdownShortcutPlugin, MDXEditor } from "@mdxeditor/editor";
+import { markdownShortcutPlugin, MDXEditor } from "@mdxeditor/editor";
// Structural editor CSS, then our retheme over the top.
import "@mdxeditor/editor/style.css";
@@ -19,6 +19,7 @@ import { ChangeStore } from "./changes";
import { PlanChanges } from "./changes-chip";
import { collaborationPlugin } from "./collaboration";
import { PlanStatus } from "./status";
+import { PLAN_LEXICAL_THEME } from "./plan-theme";
import { ResearchDraftStore } from "./research-draft";
import { register } from "./widgets";
import { widgetsPlugin } from "./widgets-plugin";
@@ -43,18 +44,6 @@ import type { CommentPresentation, QuestionStepMotion, ResearchStore } from "./w
* at all — draws a cell selection that is completely invisible. Dragging across
* cells then appears to do nothing while a `TableSelection` is very much live.
*/
-const THEME = {
- ...lexicalTheme,
- collaboration: {
- cursor: "plan-cursor",
- cursorName: "plan-cursor-name",
- selection: "plan-cursor-selection",
- selectionBg: "plan-cursor-selection-bg",
- },
- tableCellSelected: "plan-cell-selected",
- tableSelection: "plan-table-selecting",
-};
-
// Decorator nodes render through whatever the UI registered, so this has to
// happen before an editor mounts.
register();
@@ -338,7 +327,7 @@ export function PlanEditor(
suppressSharedHistory
readOnly={locked}
plugins={plugins}
- lexicalTheme={THEME}
+ lexicalTheme={PLAN_LEXICAL_THEME}
contentEditableClassName="plan-content focus-caret"
placeholder="Start writing, or ask Chopin to plan"
spellCheck
diff --git a/packages/editor/src/plan-theme.ts b/packages/editor/src/plan-theme.ts
new file mode 100644
index 00000000..25eb0844
--- /dev/null
+++ b/packages/editor/src/plan-theme.ts
@@ -0,0 +1,13 @@
+import { lexicalTheme } from "@mdxeditor/editor";
+
+export const PLAN_LEXICAL_THEME = {
+ ...lexicalTheme,
+ collaboration: {
+ cursor: "plan-cursor",
+ cursorName: "plan-cursor-name",
+ selection: "plan-cursor-selection",
+ selectionBg: "plan-cursor-selection-bg",
+ },
+ tableCellSelected: "plan-cell-selected",
+ tableSelection: "plan-table-selecting",
+};
diff --git a/packages/editor/src/static-plan-editor.test.tsx b/packages/editor/src/static-plan-editor.test.tsx
new file mode 100644
index 00000000..d6783e35
--- /dev/null
+++ b/packages/editor/src/static-plan-editor.test.tsx
@@ -0,0 +1,19 @@
+import { describe, expect, it } from "bun:test";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { StaticPlanEditor } from "./static-plan-editor";
+
+describe("StaticPlanEditor", () => {
+ it("provides a labelled read-only document without collaboration chrome", () => {
+ let source =
+ '# Audit fixture\n\n\n\nInspect this.\n\n \n\n```ts\nlet checked = true;\n```\n';
+ let markup = renderToStaticMarkup(createElement(StaticPlanEditor, { source }));
+
+ expect(markup).toContain('aria-label="Authored content specimen"');
+ expect(markup).toContain('data-source-length="');
+ expect(markup).toContain('data-read-only="true"');
+ expect(markup).not.toContain("Not connected");
+ expect(markup).not.toContain("plan-status");
+ });
+});
diff --git a/packages/editor/src/static-plan-editor.tsx b/packages/editor/src/static-plan-editor.tsx
new file mode 100644
index 00000000..35819056
--- /dev/null
+++ b/packages/editor/src/static-plan-editor.tsx
@@ -0,0 +1,83 @@
+import { useLayoutEffect, useMemo, useState } from "react";
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import {
+ addComposerChild$,
+ markdownShortcutPlugin,
+ MDXEditor,
+ realmPlugin,
+} from "@mdxeditor/editor";
+
+import { importPlan, plugins as dialectPlugins } from "@chopin/dialect";
+
+import { PLAN_LEXICAL_THEME } from "./plan-theme";
+import { register } from "./widgets";
+import { widgetsPlugin } from "./widgets-plugin";
+
+import type { QuestionnaireStore } from "./questionnaires";
+import type { ResearchStore } from "./widget-options";
+
+register();
+
+function StaticSource({ onError, source }: { onError: (error: Error) => void; source: string }) {
+ let [editor] = useLexicalComposerContext();
+ useLayoutEffect(() => {
+ try {
+ importPlan(editor, source);
+ } catch (problem) {
+ onError(problem instanceof Error ? problem : new Error("Could not import the document."));
+ }
+ }, [editor, onError, source]);
+ return null;
+}
+
+let staticSourcePlugin = realmPlugin<{ onError: (error: Error) => void; source: string }>({
+ init(realm, params) {
+ if (!params) return;
+ realm.pub(addComposerChild$, () => );
+ },
+});
+
+export function StaticPlanEditor(
+ { questions, research, source }: {
+ questions?: QuestionnaireStore;
+ research?: ResearchStore;
+ source: string;
+ },
+) {
+ let [error, setError] = useState();
+ let plugins = useMemo(
+ () => [
+ ...dialectPlugins({ core: false }),
+ markdownShortcutPlugin(),
+ widgetsPlugin({ canEdit: false, questions, research }),
+ staticSourcePlugin({ onError: problem => setError(problem.message), source }),
+ ],
+ [questions, research, source],
+ );
+
+ return (
+
+ {error && {error}
}
+
+
+ setError(problem.error)}
+ plugins={plugins}
+ readOnly
+ spellCheck
+ suppressHtmlProcessing
+ />
+
+
+
+ );
+}
From 3bcb3530447c279b62a1cf9d7bfba6e34e2751ca Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:32:48 +0100
Subject: [PATCH 08/35] Normalize design system controls
---
apps/web/src/assets/icons/chat-close.svg | 53 -------------------
.../collapse.svg => icons/panel-close.svg} | 0
apps/web/src/chat/chat.tsx | 2 +-
apps/web/src/chat/transcript.test.tsx | 15 ++++++
apps/web/src/chat/transcript.tsx | 4 +-
apps/web/src/design-audit/icons.tsx | 12 +++--
apps/web/src/design-audit/specimens.test.tsx | 3 +-
apps/web/src/design-audit/surfaces.css | 11 ++++
apps/web/src/design-audit/surfaces.tsx | 11 ++++
apps/web/src/hosted.test.ts | 2 +-
apps/web/src/icon-assets.test.ts | 15 ++++++
apps/web/src/navigation-chrome.test.ts | 10 ++++
apps/web/src/project-sidebar.tsx | 2 +-
apps/web/src/theme.css | 7 +++
apps/web/src/tokens.test.ts | 10 ++++
apps/web/src/workspace-feedback.test.ts | 2 +
apps/web/src/workspace.tsx | 6 +--
packages/editor/src/send-action.tsx | 2 +-
packages/editor/src/styles.css | 10 ----
.../src/widgets/research-composer.test.tsx | 1 +
20 files changed, 100 insertions(+), 78 deletions(-)
delete mode 100644 apps/web/src/assets/icons/chat-close.svg
rename apps/web/src/assets/{figma/navigation/collapse.svg => icons/panel-close.svg} (100%)
create mode 100644 apps/web/src/icon-assets.test.ts
diff --git a/apps/web/src/assets/icons/chat-close.svg b/apps/web/src/assets/icons/chat-close.svg
deleted file mode 100644
index 998d1511..00000000
--- a/apps/web/src/assets/icons/chat-close.svg
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/apps/web/src/assets/figma/navigation/collapse.svg b/apps/web/src/assets/icons/panel-close.svg
similarity index 100%
rename from apps/web/src/assets/figma/navigation/collapse.svg
rename to apps/web/src/assets/icons/panel-close.svg
diff --git a/apps/web/src/chat/chat.tsx b/apps/web/src/chat/chat.tsx
index 1005e696..1be3b47e 100644
--- a/apps/web/src/chat/chat.tsx
+++ b/apps/web/src/chat/chat.tsx
@@ -420,7 +420,7 @@ export function Chat(
title="Stop Planner"
type="button"
>
-
+
)}
{
expect(markup).toContain("Sam joined");
expect(markup).not.toContain("data-motion-feedback");
});
+
+test("queued messages use the standard icon-button glyph", () => {
+ let markup = renderToStaticMarkup(
+ createElement(Transcript, {
+ active: true,
+ entries: [],
+ handle: "ana",
+ onWithdraw: () => {},
+ queued: [{ handle: "ana", id: "queued", text: "One more thought" }],
+ }),
+ );
+
+ expect(markup).toMatch(/aria-label="Withdraw queued message"[^>]*>.*?width="16".*?height="16"/s);
+ expect(markup).not.toContain(">×");
+});
diff --git a/apps/web/src/chat/transcript.tsx b/apps/web/src/chat/transcript.tsx
index d065c5a8..26331fe5 100644
--- a/apps/web/src/chat/transcript.tsx
+++ b/apps/web/src/chat/transcript.tsx
@@ -1,7 +1,7 @@
/** The shared chat, grouped for reading rather than event delivery. */
import { useEffect, useId, useRef, useState } from "react";
-import { SignInIcon } from "@phosphor-icons/react";
+import { SignInIcon, XIcon } from "@phosphor-icons/react";
import { AgentFace, Face, MotionDisclosure, MotionDisclosureIcon } from "@chopin/editor";
@@ -140,7 +140,7 @@ function MessageBody(
title="Withdraw"
type="button"
>
- ×
+
)}
diff --git a/apps/web/src/design-audit/icons.tsx b/apps/web/src/design-audit/icons.tsx
index 52bb466f..92fbb79f 100644
--- a/apps/web/src/design-audit/icons.tsx
+++ b/apps/web/src/design-audit/icons.tsx
@@ -18,12 +18,11 @@ import addProject from "../assets/figma/navigation/add-project.svg";
import archive from "../assets/figma/navigation/box-archive.svg";
import book from "../assets/figma/navigation/book-bookmark.svg";
import chopin from "../assets/figma/navigation/chopin.svg";
-import collapse from "../assets/figma/navigation/collapse.svg";
import documentActions from "../assets/figma/navigation/document-actions.svg";
import newDocument from "../assets/figma/navigation/new-document.svg";
import search from "../assets/figma/navigation/search.svg";
import hideSidebar from "../assets/figma/navigation/sidebar-right-3-hide.svg";
-import conversationClose from "../assets/icons/conversation-close.svg";
+import panelClose from "../assets/icons/panel-close.svg";
import conversation from "../assets/icons/conversation.svg";
import navigationChevron from "../assets/icons/navigation-chevron-right.svg";
import navigationXmark from "../assets/icons/navigation-xmark.svg";
@@ -48,12 +47,15 @@ const LOCAL_ICONS: readonly LocalIcon[] = [
{ name: "Book bookmark", source: book },
{ name: "Archive", source: archive },
{ name: "Chopin", source: chopin },
- { duplicate: "conversation-close.svg", name: "Collapse", source: collapse },
+ {
+ duplicate: "collapse.svg and conversation-close.svg consolidated",
+ name: "Panel close",
+ source: panelClose,
+ },
{ name: "Document actions", source: documentActions },
{ name: "New document", source: newDocument },
{ name: "Search", source: search },
{ name: "Hide sidebar", source: hideSidebar },
- { duplicate: "collapse.svg", name: "Conversation close", source: conversationClose },
{ name: "Conversation", source: conversation },
{ name: "Navigation chevron", source: navigationChevron },
{ name: "Navigation close", source: navigationXmark },
@@ -102,7 +104,7 @@ export function IconCatalogue() {
{icon.name}
- {icon.duplicate ? Exact duplicate: {icon.duplicate} : null}
+ {icon.duplicate ? Consolidated exact duplicate: {icon.duplicate} : null}
))}
diff --git a/apps/web/src/design-audit/specimens.test.tsx b/apps/web/src/design-audit/specimens.test.tsx
index 803b71d6..e7b0c612 100644
--- a/apps/web/src/design-audit/specimens.test.tsx
+++ b/apps/web/src/design-audit/specimens.test.tsx
@@ -15,7 +15,7 @@ describe("design audit specimens", () => {
for (let id of ["colours", "typography", "spacing", "radii", "shadows", "icons"]) {
expect(markup).toContain(`data-audit-item="${id}"`);
}
- expect(markup).toContain("Exact duplicate");
+ expect(markup).toContain("Consolidated exact duplicate");
});
it("renders controls with their native accessibility states", () => {
@@ -49,6 +49,7 @@ describe("design audit specimens", () => {
}
expect(markup).toContain('role="dialog"');
expect(markup).toContain('aria-current="page"');
+ expect(markup).toContain('aria-label="Compact workspace view"');
expect(markup).toContain('data-chat-entry="true"');
expect(markup).toContain('role="alert"');
});
diff --git a/apps/web/src/design-audit/surfaces.css b/apps/web/src/design-audit/surfaces.css
index 0d2a5826..5c34f8a8 100644
--- a/apps/web/src/design-audit/surfaces.css
+++ b/apps/web/src/design-audit/surfaces.css
@@ -119,6 +119,17 @@
font-weight: 600;
}
+.design-audit-compact-navigation {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ width: min(100%, 32rem);
+ margin-block-start: 1rem;
+}
+
+.design-audit-compact-navigation button {
+ min-width: 0;
+}
+
.design-audit-sidebar a[data-navigation-state="ancestor"] {
font-weight: 500;
}
diff --git a/apps/web/src/design-audit/surfaces.tsx b/apps/web/src/design-audit/surfaces.tsx
index e1edc3b7..28499899 100644
--- a/apps/web/src/design-audit/surfaces.tsx
+++ b/apps/web/src/design-audit/surfaces.tsx
@@ -147,6 +147,17 @@ function Navigation() {
Loading more…
+
+ Conversation
+ Document
+
+ Decisions
+
+ Background
+
);
}
diff --git a/apps/web/src/hosted.test.ts b/apps/web/src/hosted.test.ts
index f6943451..c0a9aed1 100644
--- a/apps/web/src/hosted.test.ts
+++ b/apps/web/src/hosted.test.ts
@@ -404,7 +404,7 @@ describe("anchored child lifecycle", () => {
expect(child).toContain('aria-label="Close Source review"');
expect(child).toContain('data-child-document-close="true"');
expect(child).toContain("navigation-xmark.svg");
- expect(child).toContain('class="size-[18px]"');
+ expect(child).toContain('class="size-[16px]"');
expect(parent).not.toContain("data-child-document-close");
});
diff --git a/apps/web/src/icon-assets.test.ts b/apps/web/src/icon-assets.test.ts
new file mode 100644
index 00000000..88c1765d
--- /dev/null
+++ b/apps/web/src/icon-assets.test.ts
@@ -0,0 +1,15 @@
+import { expect, test } from "bun:test";
+import { existsSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+
+let root = import.meta.dir;
+
+test("the sidebar and conversation share one panel-close asset", () => {
+ let sidebar = readFileSync(join(root, "project-sidebar.tsx"), "utf8");
+ let workspace = readFileSync(join(root, "workspace.tsx"), "utf8");
+
+ expect(sidebar).toContain("assets/icons/panel-close.svg");
+ expect(workspace).toContain("assets/icons/panel-close.svg");
+ expect(existsSync(join(root, "assets/figma/navigation/collapse.svg"))).toBe(false);
+ expect(existsSync(join(root, "assets/icons/conversation-close.svg"))).toBe(false);
+});
diff --git a/apps/web/src/navigation-chrome.test.ts b/apps/web/src/navigation-chrome.test.ts
index 8cd4e564..0d55d5f1 100644
--- a/apps/web/src/navigation-chrome.test.ts
+++ b/apps/web/src/navigation-chrome.test.ts
@@ -3,11 +3,21 @@ import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { NavigationIcon, ProjectSidebar, toggleCollapsedProjectIds } from "./project-sidebar";
+import { ProjectSidebarExpandButton } from "./project-sidebar-chrome";
import { Header } from "./room-workspace";
import type { ComponentProps } from "react";
describe("the Figma navigation chrome", () => {
+ test("uses the standard sixteen-pixel glyph in an icon button", () => {
+ let markup = renderToStaticMarkup(createElement(ProjectSidebarExpandButton, {
+ onExpand: () => {},
+ }));
+
+ expect(markup).toContain('height="16"');
+ expect(markup).toContain('width="16"');
+ });
+
test("renders sidebar control glyphs at fourteen pixels", () => {
let markup = renderToStaticMarkup(createElement(NavigationIcon, { src: "/control.svg" }));
diff --git a/apps/web/src/project-sidebar.tsx b/apps/web/src/project-sidebar.tsx
index e0e0f149..daed4b1b 100644
--- a/apps/web/src/project-sidebar.tsx
+++ b/apps/web/src/project-sidebar.tsx
@@ -2,7 +2,7 @@ import addProjectIcon from "./assets/figma/navigation/add-project.svg";
import bookBookmarkIcon from "./assets/figma/navigation/book-bookmark.svg";
import boxArchiveIcon from "./assets/figma/navigation/box-archive.svg";
import chopinIcon from "./assets/figma/navigation/chopin.svg";
-import collapseIcon from "./assets/figma/navigation/collapse.svg";
+import collapseIcon from "./assets/icons/panel-close.svg";
import documentActionsIcon from "./assets/figma/navigation/document-actions.svg";
import newDocumentIcon from "./assets/figma/navigation/new-document.svg";
import searchIcon from "./assets/figma/navigation/search.svg";
diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css
index 108bcfc2..f9cd4091 100644
--- a/apps/web/src/theme.css
+++ b/apps/web/src/theme.css
@@ -212,6 +212,7 @@
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 500;
+ white-space: nowrap;
&:disabled {
background-color: var(--color-gray-200);
@@ -602,6 +603,12 @@ body {
padding-left: calc(0.25rem + env(safe-area-inset-left));
}
+.workspace-navigation [aria-current="page"] {
+ background-color: var(--color-page);
+ color: var(--color-text-primary);
+ box-shadow: var(--shadow-resting);
+}
+
/* The split workspace is one elevated surface; its panes only own the divider. */
.workspace-frame .plan-document {
box-shadow: none;
diff --git a/apps/web/src/tokens.test.ts b/apps/web/src/tokens.test.ts
index 95e53693..de984aa3 100644
--- a/apps/web/src/tokens.test.ts
+++ b/apps/web/src/tokens.test.ts
@@ -237,6 +237,16 @@ describe("controls", () => {
expect(rule).toMatch(/&:disabled\s*\{[\s\S]*color:\s*var\(--color-gray-600\)/);
});
+ it("keeps button labels on one line", () => {
+ expect(utility("btn")).toMatch(/white-space:\s*nowrap/);
+ });
+
+ it("distinguishes the active compact workspace destination", () => {
+ expect(THEME).toMatch(
+ /\.workspace-navigation \[aria-current="page"\]\s*\{[\s\S]*background-color:\s*var\(--color-page\);[\s\S]*color:\s*var\(--color-text-primary\)/,
+ );
+ });
+
it("keeps focus and invalid outlines visible above their surface", () => {
expect(declared("--focus-ring-color")).toBe("var(--color-brand)");
expect(declared("--focus-ring-width")).toBe("2px");
diff --git a/apps/web/src/workspace-feedback.test.ts b/apps/web/src/workspace-feedback.test.ts
index 74f50afb..6690b356 100644
--- a/apps/web/src/workspace-feedback.test.ts
+++ b/apps/web/src/workspace-feedback.test.ts
@@ -16,6 +16,7 @@ test("chat state swaps use purposeful icon feedback", () => {
expect(markup).toContain('data-motion-feedback="icon"');
expect(markup).toContain("motion-feedback");
+ expect(markup).toContain("size-[14px]");
});
test("the open chat hover swap crossfades without display changes", () => {
@@ -30,6 +31,7 @@ test("the open chat hover swap crossfades without display changes", () => {
);
expect(markup.match(/class="chat-toggle-icon/g)).toHaveLength(2);
+ expect(markup.match(/size-\[14px\]/g)).toHaveLength(3);
expect(markup).not.toContain("group-hover:hidden");
expect(markup).not.toContain("group-hover:block");
});
diff --git a/apps/web/src/workspace.tsx b/apps/web/src/workspace.tsx
index 27637294..09f045b6 100644
--- a/apps/web/src/workspace.tsx
+++ b/apps/web/src/workspace.tsx
@@ -14,7 +14,7 @@ import {
workspaceMode,
workspaceProfile,
} from "./workspace-model";
-import chatCloseIcon from "./assets/icons/chat-close.svg";
+import chatCloseIcon from "./assets/icons/panel-close.svg";
import chatIcon from "./assets/icons/chat.svg";
import navigationXmark from "./assets/icons/navigation-xmark.svg";
import { ResizeHandle, usePaneWidth } from "./resizable-pane";
@@ -175,7 +175,7 @@ export function ChatToggle(
: (
diff --git a/packages/editor/src/send-action.tsx b/packages/editor/src/send-action.tsx
index d4bb7050..b12e30fc 100644
--- a/packages/editor/src/send-action.tsx
+++ b/packages/editor/src/send-action.tsx
@@ -18,7 +18,7 @@ export function SendAction(
title={label}
type="button"
>
-
+
);
}
diff --git a/packages/editor/src/styles.css b/packages/editor/src/styles.css
index 057eb599..eca71683 100644
--- a/packages/editor/src/styles.css
+++ b/packages/editor/src/styles.css
@@ -128,16 +128,6 @@
gap: 0.5rem;
}
-.send-action {
- width: 1.5rem;
- height: 1.5rem;
-}
-
-.send-action-icon {
- width: 1.125rem;
- height: 1.125rem;
-}
-
.plan,
.plan-decisions {
--plan-gutter: 4rem;
diff --git a/packages/editor/src/widgets/research-composer.test.tsx b/packages/editor/src/widgets/research-composer.test.tsx
index 95923bcf..fd006c45 100644
--- a/packages/editor/src/widgets/research-composer.test.tsx
+++ b/packages/editor/src/widgets/research-composer.test.tsx
@@ -160,6 +160,7 @@ describe("research composer", () => {
expect(markup).toContain('title="Start research"');
expect(markup).toContain("send-action btn btn-icon btn-primary rounded-full");
expect(markup).toContain('aria-hidden="true"');
+ expect(markup).toContain('width="16" height="16"');
});
it("keeps one exact brief actionable after a failed create", () => {
From a32bbea2325b73a2622d9341f93797ba6bf1e2a7 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:36:55 +0100
Subject: [PATCH 09/35] Document design system audit findings
---
apps/web/src/design-audit/surfaces.css | 10 +--
apps/web/src/tokens.test.ts | 5 ++
docs/design-system-audit.md | 63 +++++++++++++++++++
.../2026-08-28-design-system-normalization.md | 31 +++++++++
packages/editor/src/styles.css | 5 --
5 files changed, 104 insertions(+), 10 deletions(-)
create mode 100644 docs/design-system-audit.md
create mode 100644 docs/superpowers/plans/2026-08-28-design-system-normalization.md
diff --git a/apps/web/src/design-audit/surfaces.css b/apps/web/src/design-audit/surfaces.css
index 5c34f8a8..23bc9ffb 100644
--- a/apps/web/src/design-audit/surfaces.css
+++ b/apps/web/src/design-audit/surfaces.css
@@ -103,7 +103,7 @@
justify-content: center;
gap: 0.5rem;
margin-block-start: 0.75rem;
- border: var(--edge-width) dashed var(--color-edge-strong);
+ border: var(--edge-width) dashed var(--color-control-edge);
border-radius: var(--radius-lg);
font-size: var(--text-sm);
color: var(--color-text-tertiary);
@@ -154,9 +154,9 @@
max-height: 70dvh;
flex-direction: column;
overflow: hidden;
- border: var(--edge-width) solid var(--color-edge-strong);
+ border: var(--edge-width) solid var(--color-control-edge);
border-radius: var(--radius-xl);
- background: var(--color-conversation);
+ background: var(--color-conversation-pane);
}
.design-audit-conversation .conversation-composer textarea {
@@ -250,7 +250,7 @@
}
.design-audit-empty-grid strong {
- font-size: var(--text-md);
+ font-size: var(--text-lg);
}
.design-audit-empty-grid span {
@@ -276,7 +276,7 @@
gap: 1rem;
padding: 0.75rem;
border-radius: var(--radius-lg);
- background: var(--color-destructive-soft);
+ background: var(--color-destructive-wash);
}
@media (prefers-reduced-motion: reduce) {
diff --git a/apps/web/src/tokens.test.ts b/apps/web/src/tokens.test.ts
index de984aa3..1f216e5a 100644
--- a/apps/web/src/tokens.test.ts
+++ b/apps/web/src/tokens.test.ts
@@ -14,6 +14,7 @@ type Oklch = { l: number; c: number; h: number };
const ROOT = join(import.meta.dir, "../../..");
const THEME = readFileSync(join(import.meta.dir, "theme.css"), "utf8");
+const EDITOR_STYLES = readFileSync(join(ROOT, "packages/editor/src/styles.css"), "utf8");
function declared(name: string): string {
let found = new RegExp(`\\n\\s*${name}:\\s*([^;]+);`).exec(THEME);
@@ -241,6 +242,10 @@ describe("controls", () => {
expect(utility("btn")).toMatch(/white-space:\s*nowrap/);
});
+ it("keeps consumer classes from resizing standard icon buttons", () => {
+ expect(EDITOR_STYLES).not.toMatch(/\.plan-research-dismiss\s*\{[^}]*(?:width|height):/s);
+ });
+
it("distinguishes the active compact workspace destination", () => {
expect(THEME).toMatch(
/\.workspace-navigation \[aria-current="page"\]\s*\{[\s\S]*background-color:\s*var\(--color-page\);[\s\S]*color:\s*var\(--color-text-primary\)/,
diff --git a/docs/design-system-audit.md b/docs/design-system-audit.md
new file mode 100644
index 00000000..aca4bc88
--- /dev/null
+++ b/docs/design-system-audit.md
@@ -0,0 +1,63 @@
+# Design system audit
+
+First pass completed 28 August 2026. **Health: 8/10.** The system is healthy, with a compact token
+vocabulary and shared control primitives. The audit found a small amount of consumer drift rather
+than a second, competing design system. There are no open P0, P1, or objective P2 findings after the
+first correction pass.
+
+## Catalogue
+
+Run `bun run dev` and open `/design-audit`. The route is development-only and is excluded from the
+production entry point.
+
+The catalogue contains 31 review areas in four sections:
+
+- foundations: semantic colours, typography, spacing, radii, elevation, and icons;
+- controls: buttons, icon buttons, links, fields, choices, tabs, menus, and dropdowns;
+- application surfaces: dialogs, lists, navigation, Conversation, Decisions, resolved comments,
+ loading, empty, and error states;
+- authored content: callouts, research, code, diffs, diagrams, formulae, images, and tables.
+
+Authored examples use the canonical dialect importer and static Lexical renderer. Application
+examples reuse production primitives where they can be rendered safely without a live room.
+
+## System contract
+
+- Type has five rungs and semantic ink roles with AA contrast on the page surface.
+- Controls use three sizes: 28px icon and small controls, 32px medium controls, and 44px coarse
+ pointer targets.
+- Standard icon-button glyphs are 16px. Fourteen-pixel glyphs belong to compact navigation and
+ disclosure roles; larger glyphs are status or content, not standard controls.
+- Buttons have primary, secondary, ghost, and destructive tiers with shared hover, active, focus,
+ and disabled states. Labels stay on one line.
+- Depth has three roles: resting, raised, and overlay. Borders use passive and control edges.
+- Dialogs use `NavigationDialog`; document cards and decision records retain their domain-specific
+ structures rather than imitating dialogs.
+
+## Findings and corrections
+
+| Priority | Finding and impact | Correction and evidence |
+| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| P1, resolved | Compact navigation exposed `aria-current` without a visible selected state, so the mobile destination depended on accessibility metadata alone. | Added a page surface, primary ink, and resting elevation in `apps/web/src/theme.css:606`. |
+| P2, resolved | Send actions used a 24px target and 18px glyph beside 28px icon buttons. | Moved the shared action onto the 28px/16px contract in `packages/editor/src/send-action.tsx:12`. |
+| P2, resolved | Pane, close, stop, sidebar, queued-message, and research-dismiss icon buttons mixed 14px and 18px artwork or 28px and 32px targets. | Normalized them to 28px targets with 16px glyphs; examples are `apps/web/src/workspace.tsx:150`, `apps/web/src/chat/chat.tsx:416`, and `apps/web/src/chat/transcript.tsx:136`. |
+| P2, resolved | Shared button labels could wrap under pressure and produce inconsistent control heights. | Added `white-space: nowrap` to the base utility at `apps/web/src/theme.css:207`. |
+| P2, resolved | Sidebar collapse and Conversation close shipped identical SVGs under different names. | Consolidated both onto `panel-close.svg`; `apps/web/src/icon-assets.test.ts:7` guards the boundary. |
+| P3, open | Eighteen local SVGs contain fixed `#212121` artwork, so image-loaded interactive icons cannot inherit semantic active or disabled colours. | `apps/web/src/assets/icons/conversation.svg:15` is representative. Choose Phosphor or a colour-aware wrapper before migration. |
+
+## Human review queue
+
+These are coherent today but need product taste rather than mechanical normalization:
+
+1. Desktop navigation deliberately uses a denser 14px icon role, while general controls use 16px.
+ Confirm that distinction should remain.
+2. The empty Decisions view is intentionally quiet: explanatory copy without illustration or CTA.
+ Decide whether it needs stronger guidance.
+3. A short Conversation anchors to the bottom and leaves open space above. Confirm that the
+ messaging convention is preferable to top anchoring.
+4. The project sidebar animates its width at `apps/web/src/navigation.css:198` so document space
+ yields to it. A transform would be cheaper but would overlay rather than reflow the workspace;
+ profile before changing the interaction.
+
+The authored blockquote's three-pixel logical border is a conventional quotation affordance, not a
+card accent, so it remains unchanged.
diff --git a/docs/superpowers/plans/2026-08-28-design-system-normalization.md b/docs/superpowers/plans/2026-08-28-design-system-normalization.md
new file mode 100644
index 00000000..1736b13e
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-28-design-system-normalization.md
@@ -0,0 +1,31 @@
+# Design system normalization plan
+
+**Status:** Complete
+
+This small pass addresses only the verified P1/P2 findings from the browser audit. The P3 icon
+colour migration and product-taste questions remain outside this plan.
+
+## 1. Make the current compact destination visible
+
+- Add a shared `aria-current="page"` treatment to `.workspace-navigation`.
+- Guard it in `apps/web/src/tokens.test.ts`.
+- Show the compact navigation state in the surface catalogue.
+
+## 2. Normalize standard icon buttons
+
+- Keep `.btn-icon` at 28px with a 16px glyph.
+- Move send, Conversation, close, stop, sidebar, and queued-message actions onto that contract.
+- Preserve 14px compact-navigation and disclosure icons because they are a separate role.
+- Add focused render contracts for the affected controls.
+
+## 3. Remove mechanical drift
+
+- Keep base button labels on one line.
+- Replace the two byte-identical close icons with `panel-close.svg`.
+- Add an asset regression test so the duplicate names do not return.
+
+## 4. Verify
+
+- Run the focused design-system and component tests.
+- Run workspace TypeScript checks, validation, unit tests, and production build.
+- Recheck the catalogue at desktop and compact widths.
diff --git a/packages/editor/src/styles.css b/packages/editor/src/styles.css
index eca71683..6d3e6cb1 100644
--- a/packages/editor/src/styles.css
+++ b/packages/editor/src/styles.css
@@ -60,11 +60,6 @@
justify-content: space-between;
}
-.plan-research-dismiss {
- width: 2rem;
- height: 2rem;
-}
-
.plan-research-composer textarea {
width: 100%;
min-height: 5rem;
From 3b3965fd07e087204092af136774e1e0148d899e Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:49:49 +0100
Subject: [PATCH 10/35] Fix design audit navigation overlay
---
docs/design-system-audit.md | 17 +++++++++--------
packages/editor/src/card.tsx | 4 ++--
packages/editor/src/styles.css | 1 -
packages/editor/src/widgets/research.test.tsx | 3 +++
packages/editor/src/widgets/research.tsx | 6 +++++-
5 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/docs/design-system-audit.md b/docs/design-system-audit.md
index aca4bc88..84a32644 100644
--- a/docs/design-system-audit.md
+++ b/docs/design-system-audit.md
@@ -36,14 +36,15 @@ examples reuse production primitives where they can be rendered safely without a
## Findings and corrections
-| Priority | Finding and impact | Correction and evidence |
-| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| P1, resolved | Compact navigation exposed `aria-current` without a visible selected state, so the mobile destination depended on accessibility metadata alone. | Added a page surface, primary ink, and resting elevation in `apps/web/src/theme.css:606`. |
-| P2, resolved | Send actions used a 24px target and 18px glyph beside 28px icon buttons. | Moved the shared action onto the 28px/16px contract in `packages/editor/src/send-action.tsx:12`. |
-| P2, resolved | Pane, close, stop, sidebar, queued-message, and research-dismiss icon buttons mixed 14px and 18px artwork or 28px and 32px targets. | Normalized them to 28px targets with 16px glyphs; examples are `apps/web/src/workspace.tsx:150`, `apps/web/src/chat/chat.tsx:416`, and `apps/web/src/chat/transcript.tsx:136`. |
-| P2, resolved | Shared button labels could wrap under pressure and produce inconsistent control heights. | Added `white-space: nowrap` to the base utility at `apps/web/src/theme.css:207`. |
-| P2, resolved | Sidebar collapse and Conversation close shipped identical SVGs under different names. | Consolidated both onto `panel-close.svg`; `apps/web/src/icon-assets.test.ts:7` guards the boundary. |
-| P3, open | Eighteen local SVGs contain fixed `#212121` artwork, so image-loaded interactive icons cannot inherit semantic active or disabled colours. | `apps/web/src/assets/icons/conversation.svg:15` is representative. Choose Phosphor or a colour-aware wrapper before migration. |
+| Priority | Finding and impact | Correction and evidence |
+| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| P1, resolved | A ready Research card rendered outside the editor lacked a positioning anchor, so its absolute open button covered the catalogue viewport and blocked navigation. | Made the card own its structural anchor through composable `SidecarCard` classes in `packages/editor/src/card.tsx:45` and `packages/editor/src/widgets/research.tsx:236`. |
+| P1, resolved | Compact navigation exposed `aria-current` without a visible selected state, so the mobile destination depended on accessibility metadata alone. | Added a page surface, primary ink, and resting elevation in `apps/web/src/theme.css:606`. |
+| P2, resolved | Send actions used a 24px target and 18px glyph beside 28px icon buttons. | Moved the shared action onto the 28px/16px contract in `packages/editor/src/send-action.tsx:12`. |
+| P2, resolved | Pane, close, stop, sidebar, queued-message, and research-dismiss icon buttons mixed 14px and 18px artwork or 28px and 32px targets. | Normalized them to 28px targets with 16px glyphs; examples are `apps/web/src/workspace.tsx:150`, `apps/web/src/chat/chat.tsx:416`, and `apps/web/src/chat/transcript.tsx:136`. |
+| P2, resolved | Shared button labels could wrap under pressure and produce inconsistent control heights. | Added `white-space: nowrap` to the base utility at `apps/web/src/theme.css:207`. |
+| P2, resolved | Sidebar collapse and Conversation close shipped identical SVGs under different names. | Consolidated both onto `panel-close.svg`; `apps/web/src/icon-assets.test.ts:7` guards the boundary. |
+| P3, open | Eighteen local SVGs contain fixed `#212121` artwork, so image-loaded interactive icons cannot inherit semantic active or disabled colours. | `apps/web/src/assets/icons/conversation.svg:15` is representative. Choose Phosphor or a colour-aware wrapper before migration. |
## Human review queue
diff --git a/packages/editor/src/card.tsx b/packages/editor/src/card.tsx
index e1746400..2ce1d1b0 100644
--- a/packages/editor/src/card.tsx
+++ b/packages/editor/src/card.tsx
@@ -43,7 +43,7 @@ export type SidecarCardProps = {
};
export function SidecarCard(
- { children, focused, footer, label, padded = true, settled, status, ...rest }:
+ { children, className, focused, footer, label, padded = true, settled, status, ...rest }:
& SidecarCardProps
& Omit, "children">,
) {
@@ -54,7 +54,7 @@ export function SidecarCard(
aria-label={label}
className={`flex flex-col overflow-hidden rounded-lg ring-hairline ${surface} ${
focused ? "bg-selected" : ""
- }`}
+ } ${className ?? ""}`}
data-focus-boundary=""
{...rest}
>
diff --git a/packages/editor/src/styles.css b/packages/editor/src/styles.css
index 6d3e6cb1..f513cb03 100644
--- a/packages/editor/src/styles.css
+++ b/packages/editor/src/styles.css
@@ -22,7 +22,6 @@
}
.plan-content [data-research-ready] {
- position: relative;
box-shadow: var(--shadow-raised);
transition: box-shadow var(--duration-fast) var(--ease-out);
}
diff --git a/packages/editor/src/widgets/research.test.tsx b/packages/editor/src/widgets/research.test.tsx
index 18bc7b3d..5576c769 100644
--- a/packages/editor/src/widgets/research.test.tsx
+++ b/packages/editor/src/widgets/research.test.tsx
@@ -257,6 +257,9 @@ describe("research card", () => {
expect(markup).toContain('aria-label="Open Rollout evidence"');
expect(markup).toContain('class="plan-research-open"');
expect(markup).toContain('data-research-ready=""');
+ expect(markup).toMatch(
+ /]*class="[^"]*\brelative\b[^"]*"[^>]*data-research-ready=""/,
+ );
expect(markup).not.toContain("https://example.com/releases");
expect(markup).not.toContain("Remove research reference");
expect(markup).not.toContain("plan-research-actions");
diff --git a/packages/editor/src/widgets/research.tsx b/packages/editor/src/widgets/research.tsx
index c5a850b6..ec93cd7c 100644
--- a/packages/editor/src/widgets/research.tsx
+++ b/packages/editor/src/widgets/research.tsx
@@ -233,7 +233,11 @@ export function ResearchCard(
: undefined;
let actions = researchActions(request, canEdit);
return (
-
+
{ready ? ready.title : STAGES[request.stage]}
{ready ? STAGES[request.stage] : request.question}
From 051fe97e1993af5a5f63d21ffe8170fc96529b6e Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:51:16 +0100
Subject: [PATCH 11/35] Document brand wash alpha decision
---
.../2026-08-28-brand-wash-alpha-design.md | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
create mode 100644 docs/superpowers/specs/2026-08-28-brand-wash-alpha-design.md
diff --git a/docs/superpowers/specs/2026-08-28-brand-wash-alpha-design.md b/docs/superpowers/specs/2026-08-28-brand-wash-alpha-design.md
new file mode 100644
index 00000000..6081068e
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-28-brand-wash-alpha-design.md
@@ -0,0 +1,23 @@
+# Brand wash alpha design
+
+## Decision
+
+Define `--color-brand-wash` as the brand colour at true 30% opacity:
+
+```css
+color-mix(in srgb, var(--color-brand) 30%, transparent)
+```
+
+This preserves the brand hue while allowing the surface beneath it to contribute to the result.
+
+## Scope
+
+- Change only `--color-brand-wash`.
+- Keep all existing consumers and the design-audit swatch on the shared token.
+- Do not change the brand, hover, active, or ink colours.
+
+## Verification
+
+- Add a token contract that distinguishes true alpha from an opaque page tint.
+- Run the token tests and validation.
+- Inspect the swatch and representative brand-wash consumers in the live catalogue.
From 5c8240ed12f9d7ac179bd2c03348e5f8c94569a4 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 10:55:56 +0100
Subject: [PATCH 12/35] Refine brand wash and type specimens
---
apps/web/src/design-audit/foundations.css | 24 +++++++++++++++++---
apps/web/src/design-audit/foundations.tsx | 17 ++++++++------
apps/web/src/design-audit/specimens.test.tsx | 9 ++++++++
apps/web/src/theme.css | 2 +-
apps/web/src/tokens.test.ts | 9 ++++++--
5 files changed, 48 insertions(+), 13 deletions(-)
diff --git a/apps/web/src/design-audit/foundations.css b/apps/web/src/design-audit/foundations.css
index dedc15f1..85ef76dc 100644
--- a/apps/web/src/design-audit/foundations.css
+++ b/apps/web/src/design-audit/foundations.css
@@ -43,17 +43,30 @@
.design-audit-type-stack > div {
display: grid;
- grid-template-columns: 9rem minmax(0, 1fr);
+ grid-template-columns: 12rem minmax(0, 1fr);
align-items: baseline;
gap: 1.5rem;
}
-.design-audit-type-stack span {
+.design-audit-type-label {
+ display: grid;
+ gap: 0.125rem;
+}
+
+.design-audit-type-label span,
+.design-audit-type-label code {
font-size: var(--text-sm);
- font-weight: 500;
color: var(--color-text-quaternary);
}
+.design-audit-type-label code {
+ white-space: nowrap;
+}
+
+.design-audit-type-label span {
+ font-weight: 500;
+}
+
.design-audit-type-stack strong {
font-weight: 500;
text-wrap: pretty;
@@ -183,3 +196,8 @@
gap: 0.25rem;
}
}
+
+.design-audit-preview[data-preview-width="narrow"] .design-audit-type-stack > div {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 0.25rem;
+}
diff --git a/apps/web/src/design-audit/foundations.tsx b/apps/web/src/design-audit/foundations.tsx
index 23004214..32469eb0 100644
--- a/apps/web/src/design-audit/foundations.tsx
+++ b/apps/web/src/design-audit/foundations.tsx
@@ -23,11 +23,11 @@ const COLOURS = [
] as const;
const TYPE = [
- ["Chrome and labels", "--text-sm", "--text-sm--line-height"],
- ["Document prose", "--text-base", "--text-base--line-height"],
- ["Subheading", "--text-lg", "--text-lg--line-height"],
- ["Section heading", "--text-xl", "--text-xl--line-height"],
- ["Document title", "--text-2xl", "--text-2xl--line-height"],
+ ["Chrome and labels", "13px / 20px line-height", "--text-sm", "--text-sm--line-height"],
+ ["Document prose", "15px / 22px line-height", "--text-base", "--text-base--line-height"],
+ ["Subheading", "17px / 27px line-height", "--text-lg", "--text-lg--line-height"],
+ ["Section heading", "24px / 30px line-height", "--text-xl", "--text-xl--line-height"],
+ ["Document title", "32px / 38px line-height", "--text-2xl", "--text-2xl--line-height"],
] as const;
const SPACING = [2, 4, 6, 8, 12, 16, 24, 32] as const;
@@ -71,9 +71,12 @@ export function Foundations() {
description="The complete five-rung type scale."
>
- {TYPE.map(([label, size, lineHeight]) => (
+ {TYPE.map(([label, measurement, size, lineHeight]) => (
-
{label}
+
+ {label}
+ {measurement}
+
Several people and a Planner share one document.
))}
diff --git a/apps/web/src/design-audit/specimens.test.tsx b/apps/web/src/design-audit/specimens.test.tsx
index e7b0c612..3d48a34a 100644
--- a/apps/web/src/design-audit/specimens.test.tsx
+++ b/apps/web/src/design-audit/specimens.test.tsx
@@ -16,6 +16,15 @@ describe("design audit specimens", () => {
expect(markup).toContain(`data-audit-item="${id}"`);
}
expect(markup).toContain("Consolidated exact duplicate");
+ for (
+ let measurement of [
+ "13px / 20px line-height",
+ "15px / 22px line-height",
+ "17px / 27px line-height",
+ "24px / 30px line-height",
+ "32px / 38px line-height",
+ ]
+ ) expect(markup).toContain(measurement);
});
it("renders controls with their native accessibility states", () => {
diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css
index f9cd4091..f4e71d0a 100644
--- a/apps/web/src/theme.css
+++ b/apps/web/src/theme.css
@@ -54,7 +54,7 @@
--color-brand: oklch(0.50006 0.08514 210.06); /* #06707e */
--color-brand-hover: oklch(0.42914 0.07315 210.634); /* #045a66 */
--color-brand-active: oklch(0.37546 0.06382 210.435); /* #034a54 */
- --color-brand-wash: oklch(0.95547 0.0277 207.843); /* #dcf6fa */
+ --color-brand-wash: color-mix(in srgb, var(--color-brand) 30%, transparent);
--color-brand-ink: oklch(0.43882 0.07476 210.193); /* #045d69 */
--focus-ring-color: var(--color-brand);
--focus-ring-width: 2px;
diff --git a/apps/web/src/tokens.test.ts b/apps/web/src/tokens.test.ts
index 1f216e5a..f7506f37 100644
--- a/apps/web/src/tokens.test.ts
+++ b/apps/web/src/tokens.test.ts
@@ -134,7 +134,13 @@ describe("palette", () => {
);
});
- it("keeps the Chat pane edge subtly stronger than the frame hairline", () => {
+it("makes the brand wash the brand colour at thirty percent opacity", () => {
+ expect(declared("--color-brand-wash")).toBe(
+ "color-mix(in srgb, var(--color-brand) 30%, transparent)",
+ );
+});
+
+it("keeps the Chat pane edge subtly stronger than the frame hairline", () => {
expect(THEME).toMatch(
/\.workspace-frame \.workspace-chat-panel\s*\{\s*border-color:\s*rgb\(0 0 0 \/ 9%\);/,
);
@@ -162,7 +168,6 @@ describe("palette", () => {
"--color-brand",
"--color-brand-hover",
"--color-brand-active",
- "--color-brand-wash",
"--color-brand-ink",
]);
});
From 11e24cf6b0d95ce128d16f0ed286629de7429f58 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 11:05:13 +0100
Subject: [PATCH 13/35] Consolidate passive control surfaces
---
apps/web/src/design-audit/foundations.tsx | 1 -
apps/web/src/design-audit/inventory.ts | 6 ++
apps/web/src/design-audit/specimens.test.tsx | 2 +
apps/web/src/design-audit/styles.css | 2 +-
apps/web/src/design-audit/surfaces.css | 88 +++++++++++++++++++
apps/web/src/design-audit/surfaces.tsx | 41 +++++++++
apps/web/src/theme.css | 2 -
apps/web/src/tokens.test.ts | 14 ++-
packages/editor/src/face.tsx | 2 +-
packages/editor/src/styles.css | 6 +-
.../src/widgets/research-composer.test.tsx | 1 +
packages/editor/src/widgets/research.tsx | 1 +
packages/question/src/react/question-view.tsx | 2 +-
13 files changed, 155 insertions(+), 13 deletions(-)
diff --git a/apps/web/src/design-audit/foundations.tsx b/apps/web/src/design-audit/foundations.tsx
index 32469eb0..82cf0d29 100644
--- a/apps/web/src/design-audit/foundations.tsx
+++ b/apps/web/src/design-audit/foundations.tsx
@@ -6,7 +6,6 @@ const COLOURS = [
["Ground", "--color-ground"],
["Inset", "--color-inset"],
["Selected", "--color-selected"],
- ["Control", "--color-control"],
["Primary text", "--color-text-primary"],
["Secondary text", "--color-text-secondary"],
["Tertiary text", "--color-text-tertiary"],
diff --git a/apps/web/src/design-audit/inventory.ts b/apps/web/src/design-audit/inventory.ts
index 8a3f84d8..6c44935e 100644
--- a/apps/web/src/design-audit/inventory.ts
+++ b/apps/web/src/design-audit/inventory.ts
@@ -102,6 +102,12 @@ export const AUDIT_INVENTORY: readonly AuditGroup[] = [
source: "apps/web/src/chat/chat.tsx",
states: ["member", "planner", "tool", "busy", "error"],
},
+ {
+ id: "identity",
+ label: "Identity and collaboration",
+ source: "packages/editor/src/face.tsx",
+ states: ["avatar-loading", "editing-question"],
+ },
{
id: "decisions",
label: "Decisions",
diff --git a/apps/web/src/design-audit/specimens.test.tsx b/apps/web/src/design-audit/specimens.test.tsx
index 3d48a34a..3205355f 100644
--- a/apps/web/src/design-audit/specimens.test.tsx
+++ b/apps/web/src/design-audit/specimens.test.tsx
@@ -60,6 +60,8 @@ describe("design audit specimens", () => {
expect(markup).toContain('aria-current="page"');
expect(markup).toContain('aria-label="Compact workspace view"');
expect(markup).toContain('data-chat-entry="true"');
+ expect(markup).toContain("Avatar image loading");
+ expect(markup).toContain("Editing this question");
expect(markup).toContain('role="alert"');
});
diff --git a/apps/web/src/design-audit/styles.css b/apps/web/src/design-audit/styles.css
index 77150c76..609531a3 100644
--- a/apps/web/src/design-audit/styles.css
+++ b/apps/web/src/design-audit/styles.css
@@ -53,7 +53,7 @@
gap: 0.125rem;
padding: 0.125rem;
border-radius: var(--radius-lg);
- background: var(--color-control);
+ background: var(--color-selected);
}
.design-audit-width-controls [aria-pressed="true"] {
diff --git a/apps/web/src/design-audit/surfaces.css b/apps/web/src/design-audit/surfaces.css
index 23bc9ffb..57d5cd89 100644
--- a/apps/web/src/design-audit/surfaces.css
+++ b/apps/web/src/design-audit/surfaces.css
@@ -186,6 +186,94 @@
color: var(--color-destructive-ink);
}
+.design-audit-identity-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
+ gap: 1rem;
+}
+
+.design-audit-identity-grid > div {
+ display: grid;
+ align-content: start;
+ gap: 0.5rem;
+}
+
+.design-audit-identity-row,
+.design-audit-question-context {
+ border-radius: var(--radius-lg);
+ background: var(--color-page);
+ box-shadow: var(--shadow-resting);
+}
+
+.design-audit-identity-row {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ padding: 0.75rem;
+}
+
+.design-audit-avatar-loading {
+ display: block;
+ width: 1.5rem;
+ height: 1.5rem;
+ flex: none;
+ border-radius: var(--radius-md);
+ background: var(--color-selected);
+}
+
+.design-audit-identity-row > div {
+ display: grid;
+ min-width: 0;
+}
+
+.design-audit-identity-row strong,
+.design-audit-question-context strong {
+ font-size: var(--text-sm);
+ font-weight: 600;
+}
+
+.design-audit-identity-row span,
+.design-audit-question-context p {
+ margin: 0;
+ font-size: var(--text-sm);
+ color: var(--color-text-secondary);
+}
+
+.design-audit-question-context {
+ display: grid;
+ gap: 0.5rem;
+ padding: 0.75rem;
+}
+
+.design-audit-question-context header {
+ display: flex;
+ min-width: 0;
+ flex-wrap: wrap;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.design-audit-collaborator-badges {
+ display: flex;
+ min-width: 0;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+}
+
+.design-audit-collaborator-badges > span {
+ max-width: 7rem;
+ overflow: hidden;
+ border-radius: 9999px;
+ background: var(--color-selected);
+ padding: 0.125rem 0.375rem;
+ color: var(--color-text-tertiary);
+ font-size: var(--text-sm);
+ font-weight: 500;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.design-audit-card-grid > article {
height: fit-content;
}
diff --git a/apps/web/src/design-audit/surfaces.tsx b/apps/web/src/design-audit/surfaces.tsx
index 28499899..13613bba 100644
--- a/apps/web/src/design-audit/surfaces.tsx
+++ b/apps/web/src/design-audit/surfaces.tsx
@@ -261,6 +261,46 @@ function Decisions() {
);
}
+function IdentityAndCollaboration() {
+ return (
+
+
+
+
Avatar image loading
+
+
+
+ Maggie Appleton
+ Joining the document…
+
+
+
+
+
Editing this question
+
+
+ Rollout
+
+ @maggieappleton
+ @olivia
+
+
+
How should we introduce the new document workflow?
+
+
+
+
+ );
+}
+
function Feedback() {
return (
<>
@@ -339,6 +379,7 @@ export function Surfaces() {
+
>
diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css
index f4e71d0a..0450f5f2 100644
--- a/apps/web/src/theme.css
+++ b/apps/web/src/theme.css
@@ -40,8 +40,6 @@
--color-hover: var(--color-gray-100);
--color-inset: var(--color-gray-100);
--color-selected: var(--color-gray-200);
- --color-control: var(--color-gray-200);
- --color-control-hover: var(--color-gray-300);
--color-text-primary: var(--color-gray-900);
--color-text-secondary: var(--color-gray-700);
diff --git a/apps/web/src/tokens.test.ts b/apps/web/src/tokens.test.ts
index f7506f37..7c5a3d4e 100644
--- a/apps/web/src/tokens.test.ts
+++ b/apps/web/src/tokens.test.ts
@@ -154,12 +154,22 @@ it("keeps the Chat pane edge subtly stronger than the frame hairline", () => {
});
it("keeps text used on tinted surfaces at AA contrast", () => {
- for (let surface of ["ground", "hover", "selected", "control"]) {
+ for (let surface of ["ground", "hover", "selected"]) {
let ratio = contrast("--color-text-tertiary", `--color-${surface}`);
expect({ surface, passes: ratio >= 4.5 }).toEqual({ surface, passes: true });
}
});
+ it("uses selected as the single passive control fill", () => {
+ expect(THEME).not.toMatch(/\n\s*--color-control(?:-hover)?:/);
+ expect(EDITOR_STYLES).not.toContain("var(--color-control)");
+ for (
+ let source of sources(join(ROOT, "apps/web/src")).concat(sources(join(ROOT, "packages")))
+ ) {
+ expect(withoutComments(readFileSync(source, "utf8"))).not.toMatch(/\bbg-control\b/);
+ }
+ });
+
it("keeps petrol as the only blue family", () => {
let blue = [...THEME.matchAll(/\n\s*(--color-[\w-]+):\s*oklch\([\d.]+\s+[\d.]+\s+([\d.]+)/g)]
.filter(match => Number(match[2]) >= 180 && Number(match[2]) <= 270)
@@ -201,7 +211,7 @@ describe("edges and depth", () => {
});
it("keeps the control boundary visible on every surface", () => {
- for (let surface of ["page", "ground", "hover", "selected", "control"]) {
+ for (let surface of ["page", "ground", "hover", "selected"]) {
let ratio = contrast("--color-control-boundary", `--color-${surface}`);
expect({ surface, passes: ratio >= 3 }).toEqual({ surface, passes: true });
}
diff --git a/packages/editor/src/face.tsx b/packages/editor/src/face.tsx
index f065cbf4..ed9f890f 100644
--- a/packages/editor/src/face.tsx
+++ b/packages/editor/src/face.tsx
@@ -63,7 +63,7 @@ export function Face({ handle, ring, size = 20 }: FaceProps) {
return (
setFailed(true)}
referrerPolicy="no-referrer"
src={photograph(handle, size)}
diff --git a/packages/editor/src/styles.css b/packages/editor/src/styles.css
index f513cb03..ecd516e2 100644
--- a/packages/editor/src/styles.css
+++ b/packages/editor/src/styles.css
@@ -63,11 +63,7 @@
width: 100%;
min-height: 5rem;
resize: vertical;
- border: var(--edge-width) solid var(--color-control-edge);
- border-radius: var(--radius-md);
- background: var(--color-control);
padding: 0.625rem;
- color: var(--color-text-primary);
font: inherit;
}
@@ -944,7 +940,7 @@
.plan-grip {
border-radius: var(--radius-sm);
- background: var(--color-control);
+ background: var(--color-selected);
transition: background-color var(--duration-fast) var(--ease-out);
}
diff --git a/packages/editor/src/widgets/research-composer.test.tsx b/packages/editor/src/widgets/research-composer.test.tsx
index fd006c45..1f8af617 100644
--- a/packages/editor/src/widgets/research-composer.test.tsx
+++ b/packages/editor/src/widgets/research-composer.test.tsx
@@ -177,6 +177,7 @@ describe("research composer", () => {
expect(markup).toContain("Research could not be started.");
expect(markup).toContain("Start research");
expect(markup).toContain("Discard research question");
+ expect(markup).toMatch(/
);
@@ -168,7 +187,7 @@ function Conversation() {
item="chat"
title="Conversation"
>
-
+
-
+
diff --git a/apps/web/src/icon-assets.test.ts b/apps/web/src/icon-assets.test.ts
index 79903a1e..e012bc91 100644
--- a/apps/web/src/icon-assets.test.ts
+++ b/apps/web/src/icon-assets.test.ts
@@ -15,14 +15,14 @@ function sourceFiles(directory: string, found: string[] = []): string[] {
return found;
}
-test("the sidebar and conversation share one panel-close asset", () => {
+test("the sidebar and Chat share one panel-close asset", () => {
let sidebar = readFileSync(join(root, "project-sidebar.tsx"), "utf8");
let workspace = readFileSync(join(root, "workspace.tsx"), "utf8");
expect(sidebar).toContain("assets/icons/panel-close.svg");
expect(workspace).toContain("assets/icons/panel-close.svg");
expect(existsSync(join(root, "assets/figma/navigation/collapse.svg"))).toBe(false);
- expect(existsSync(join(root, "assets/icons/conversation-close.svg"))).toBe(false);
+ expect(existsSync(join(root, "assets/icons/chat-close.svg"))).toBe(false);
});
test("the interface uses only the shared Nucleo icon family", () => {
@@ -125,7 +125,8 @@ test("directional controls reuse one chevron and one panel icon", () => {
let room = readFileSync(join(root, "room-workspace.tsx"), "utf8");
let sidebarChrome = readFileSync(join(root, "project-sidebar-chrome.tsx"), "utf8");
- expect(room).toContain("navigation-chevron-right.svg");
+ expect(room).toContain("ChevronIcon");
+ expect(existsSync(join(root, "assets/icons/navigation-chevron-right.svg"))).toBe(false);
expect(room).not.toContain("tool-chevron-down.svg");
expect(sidebarChrome).toContain("assets/icons/panel-close.svg");
expect(existsSync(join(root, "assets/icons/tool-chevron-down.svg"))).toBe(false);
diff --git a/apps/web/src/navigation-chrome.test.ts b/apps/web/src/navigation-chrome.test.ts
index 4e098019..3bf3456b 100644
--- a/apps/web/src/navigation-chrome.test.ts
+++ b/apps/web/src/navigation-chrome.test.ts
@@ -78,18 +78,17 @@ describe("the Figma navigation chrome", () => {
expect(markup).toMatch(
/aria-label="New document in testing-sql-transcripts"[^>]*>.*?new-document\.svg/s,
);
- expect(markup).toContain("book-bookmark.svg");
- expect(markup).toMatch(/src="[^"]*book-bookmark\.svg"/);
+ expect(markup).toContain("book-bookmark");
expect(markup).not.toContain('src="/repository.png"');
expect(markup).toMatch(
/aria-label="Add Project"[^>]*>.*?class="size-3\.5"[^>]*add-project\.svg/s,
);
expect(markup).toMatch(
- /
]*class="project-sidebar-primary-action"[^>]*>.*?search\.svg.*?Search<\/span>/s,
+ /]*class="project-sidebar-primary-action"[^>]*>.*?search.*?Search<\/span>/s,
);
expect(markup).toContain('class="project-sidebar-projects gap-2"');
expect(markup).toMatch(
- /]*aria-expanded="true"[^>]*class="project-sidebar-project-disclosure[^>]*>.*?book-bookmark\.svg.*?testing-sql-transcripts<\/span><\/button>/s,
+ /]*aria-expanded="true"[^>]*class="project-sidebar-project-disclosure[^>]*>.*?book-bookmark.*?testing-sql-transcripts<\/span><\/button>/s,
);
expect(markup).toContain('data-feedback-icon="open"');
expect(markup).toContain('data-motion-feedback="icon"');
@@ -171,7 +170,7 @@ describe("the Figma navigation chrome", () => {
};
let markup = renderToStaticMarkup(createElement(Header, props));
- expect(markup).toMatch(/src="[^"]*book-bookmark\.svg"/);
+ expect(markup).toContain("book-bookmark");
expect(markup).not.toContain('src="/repository.png"');
expect(markup).toContain('aria-label="Document: Hushed mountain"');
expect(markup).toContain('aria-label="Actions for Hushed mountain"');
@@ -202,8 +201,8 @@ describe("the Figma navigation chrome", () => {
expect(markup).toContain('aria-label="Child document: Source review"');
expect(markup).toContain('aria-label="People here: MaggieAppleton"');
expect(markup).not.toContain('aria-label="Actions for Release plan"');
- expect(markup).toContain("navigation-chevron-right.svg");
- expect(markup).toContain('class="document-breadcrumb-separator size-[14px]');
+ expect(markup).toContain("chevron-right");
+ expect(markup).toContain('class="document-breadcrumb-separator shrink-0"');
expect(markup).not.toContain(">›");
});
diff --git a/apps/web/src/project-sidebar.tsx b/apps/web/src/project-sidebar.tsx
index 724cdaaa..ed994ef0 100644
--- a/apps/web/src/project-sidebar.tsx
+++ b/apps/web/src/project-sidebar.tsx
@@ -1,11 +1,8 @@
import addProjectIcon from "./assets/figma/navigation/add-project.svg";
-import bookBookmarkIcon from "./assets/figma/navigation/book-bookmark.svg";
-import boxArchiveIcon from "./assets/figma/navigation/box-archive.svg";
import chopinIcon from "./assets/figma/navigation/chopin.svg";
import collapseIcon from "./assets/icons/panel-close.svg";
import documentActionsIcon from "./assets/figma/navigation/document-actions.svg";
import newDocumentIcon from "./assets/figma/navigation/new-document.svg";
-import searchIcon from "./assets/figma/navigation/search.svg";
import { DocumentActionsMenu } from "./document-actions-menu";
import { motionContract } from "./motion-contract";
import { motionImmediately } from "./motion-input";
@@ -14,7 +11,7 @@ import { MotionDisclosure, MotionDisclosureIcon } from "@chopin/editor";
import { childDocumentPath, documentPath } from "@chopin/protocol/document-url";
import { useId, useRef, useState } from "react";
-import { ChevronIcon } from "@chopin/icons";
+import { ArchiveIcon, ChevronIcon, DocumentIcon, SearchIcon } from "@chopin/icons";
import type * as Api from "./api";
import type { DocumentAction } from "./document-actions-menu";
import type { ProjectDocuments } from "./document-actions";
@@ -227,7 +224,7 @@ function Project(
open={expanded}
opened={ }
/>
-
+
{label}
{!archiveMode && project.available && canManage && (
@@ -335,7 +332,7 @@ export function ProjectSidebar(
onClick={onSearch}
type="button"
>
-
+
Search
>
@@ -354,7 +351,7 @@ export function ProjectSidebar(
ref={archivedChats}
type="button"
>
-
+
Archived chats
diff --git a/apps/web/src/room-workspace.tsx b/apps/web/src/room-workspace.tsx
index d7e5f470..c3c8a246 100644
--- a/apps/web/src/room-workspace.tsx
+++ b/apps/web/src/room-workspace.tsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { documentPath } from "@chopin/protocol/document-url";
+import { ChevronIcon, DocumentIcon } from "@chopin/icons";
import {
advanceDecisionView,
countUnanswered,
@@ -15,8 +16,6 @@ import {
visibleDecisionView,
} from "@chopin/editor";
-import bookBookmarkIcon from "./assets/figma/navigation/book-bookmark.svg";
-import navigationChevronRight from "./assets/icons/navigation-chevron-right.svg";
import { Chat } from "./chat/chat";
import { rememberChannel } from "./channel-recovery";
import { decisionAttention, DecisionViewControl } from "./decision-view-control";
@@ -25,7 +24,6 @@ import { DocumentActionsMenu } from "./document-actions-menu";
import { motionContract } from "./motion-contract";
import { motionImmediately } from "./motion-input";
import { useNavigationDocument } from "./navigation-shell";
-import { NavigationIcon } from "./project-sidebar";
import { peopleHere } from "./presence";
import { ResearchRequestStore } from "./research-requests";
import { Wire } from "./wire";
@@ -78,7 +76,7 @@ export function Header(
aria-label={`Document: ${label}`}
className="flex min-w-0 flex-1 items-center gap-0.5"
>
-
+
{presentation.type === "parent-with-child"
? (
<>
@@ -90,11 +88,9 @@ export function Header(
>
{label}
-
{label}
-
+
>
}
/>
diff --git a/packages/editor/src/assets/icons/link-plus.svg b/packages/editor/src/assets/icons/link-plus.svg
deleted file mode 100644
index 02e1eac2..00000000
--- a/packages/editor/src/assets/icons/link-plus.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
- link-plus
-
-
-
-
-
-
-
diff --git a/packages/editor/src/assets/icons/message-plus.svg b/packages/editor/src/assets/icons/message-plus.svg
deleted file mode 100644
index e614dc65..00000000
--- a/packages/editor/src/assets/icons/message-plus.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
- msg-plus
-
-
-
-
-
-
diff --git a/packages/editor/src/comments.tsx b/packages/editor/src/comments.tsx
index 20c96d76..743d59e1 100644
--- a/packages/editor/src/comments.tsx
+++ b/packages/editor/src/comments.tsx
@@ -13,7 +13,7 @@
*/
import { useEffect, useLayoutEffect, useRef, useState } from "react";
-import { ArrowUpIcon, ChatCircleIcon, CheckIcon, XIcon } from "@phosphor-icons/react";
+import { ArrowUpIcon, CheckIcon, CloseIcon, MessageIcon } from "@chopin/icons";
import { limits } from "@chopin/dialect";
@@ -69,7 +69,7 @@ function CloseButton({ onClose }: { onClose: () => void }) {
title="Close comment"
type="button"
>
-
+
);
}
@@ -80,7 +80,7 @@ function DraftHeader({ onClose, showClose }: { onClose: () => void; showClose: b
className="flex min-h-7 items-center justify-between text-text-tertiary"
data-plan-comment-draft-header
>
-
+
{showClose && }
);
@@ -196,7 +196,7 @@ function Composer({
title={sendLabel ?? `Send ${label.toLowerCase()}`}
type="button"
>
-
+
)}
{onCancel && (
@@ -388,7 +388,7 @@ export function ThreadCard({
onClick={() => setConfirming("dismiss")}
type="button"
>
-
+
Dismiss
setConfirming("accept")}
type="button"
>
-
+
Apply feedback
diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts
index e1bcc6ce..1e2497de 100644
--- a/packages/editor/src/index.ts
+++ b/packages/editor/src/index.ts
@@ -1,3 +1,5 @@
+export { SidecarCard } from "./card";
+export type { SidecarCardProps } from "./card";
export { collaborationPlugin } from "./collaboration";
export type { CollaborationOptions } from "./collaboration";
export { ContentSwapLayer } from "./content-swap";
@@ -55,3 +57,4 @@ export {
ResearchComposer,
ResearchReference,
} from "./widgets";
+export { DecisionCard } from "./widgets/decision";
diff --git a/packages/editor/src/toolbar/bubble.tsx b/packages/editor/src/toolbar/bubble.tsx
index f30d62bd..bad4d844 100644
--- a/packages/editor/src/toolbar/bubble.tsx
+++ b/packages/editor/src/toolbar/bubble.tsx
@@ -10,9 +10,8 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { TOGGLE_LINK_COMMAND } from "@lexical/link";
import { LINK_PROTOCOLS } from "@chopin/dialect";
+import { LinkPlusIcon, MessagePlusIcon } from "@chopin/icons";
-import linkPlusIcon from "../assets/icons/link-plus.svg";
-import messagePlusIcon from "../assets/icons/message-plus.svg";
import { askForUrl } from "./url";
import { placeSurface } from "./placement";
import {
@@ -380,7 +379,7 @@ export function SelectionBubble(
}}
className={`${CELL} ${CELL_OFF}`}
>
-
+
{onComment && (
@@ -403,12 +402,7 @@ export function SelectionBubble(
}}
className={`${CELL} ${CELL_OFF}`}
>
-
+
>
)}
diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts
index de7dac7a..8e232f31 100644
--- a/packages/icons/src/index.ts
+++ b/packages/icons/src/index.ts
@@ -6,7 +6,9 @@ export {
CloseIcon,
InfoIcon,
LightbulbIcon,
+ LinkPlusIcon,
MessageIcon,
+ MessagePlusIcon,
PlusIcon,
SignInIcon,
SirenIcon,
diff --git a/packages/icons/src/line.tsx b/packages/icons/src/line.tsx
index 472f7b3b..8866ef4a 100644
--- a/packages/icons/src/line.tsx
+++ b/packages/icons/src/line.tsx
@@ -124,3 +124,24 @@ export function WarningIcon(props: IconProps) {
);
}
+
+export function LinkPlusIcon(props: IconProps) {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export function MessagePlusIcon(props: IconProps) {
+ return (
+
+
+
+
+
+ );
+}
From 16bb72cccd38accec38ba67038bc57959ad1d631 Mon Sep 17 00:00:00 2001
From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com>
Date: Fri, 28 Aug 2026 15:01:56 +0100
Subject: [PATCH 34/35] Fix design system CI regressions
---
Dockerfile | 1 +
apps/web/src/icon-assets.test.ts | 12 +++++++++++-
e2e/responsive-comments.e2e.ts | 2 +-
e2e/sidecar.e2e.ts | 4 ++--
packages/icons/src/icon.tsx | 10 +++++++++-
packages/icons/src/system.tsx | 2 ++
6 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 45a23426..5cfc4ac4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,6 +11,7 @@ COPY apps/server/package.json ./apps/server/package.json
COPY apps/web/package.json ./apps/web/package.json
COPY packages/dialect/package.json ./packages/dialect/package.json
COPY packages/editor/package.json ./packages/editor/package.json
+COPY packages/icons/package.json ./packages/icons/package.json
COPY packages/protocol/package.json ./packages/protocol/package.json
COPY packages/question/package.json ./packages/question/package.json
COPY packages/viewport/package.json ./packages/viewport/package.json
diff --git a/apps/web/src/icon-assets.test.ts b/apps/web/src/icon-assets.test.ts
index e012bc91..91c5c782 100644
--- a/apps/web/src/icon-assets.test.ts
+++ b/apps/web/src/icon-assets.test.ts
@@ -1,6 +1,9 @@
import { expect, test } from "bun:test";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { SearchIcon } from "@chopin/icons";
let root = import.meta.dir;
let repository = join(root, "../../..");
@@ -80,6 +83,14 @@ test("interface icons default to fourteen pixels", () => {
expect(offenders).toEqual([]);
});
+test("interface icons are decorative unless explicitly labelled", () => {
+ let decorative = renderToStaticMarkup(createElement(SearchIcon));
+ let labelled = renderToStaticMarkup(createElement(SearchIcon, { "aria-label": "Search" }));
+
+ expect(decorative).toContain('aria-hidden="true"');
+ expect(labelled).not.toContain('aria-hidden="true"');
+});
+
test("interface icons share one neutral default colour", () => {
let theme = readFileSync(join(root, "theme.css"), "utf8");
let icon = readFileSync(join(repository, "packages/icons/src/icon.tsx"), "utf8");
@@ -92,7 +103,6 @@ test("interface icons share one neutral default colour", () => {
let assetRoots = [
join(root, "assets/figma/navigation"),
join(root, "assets/icons"),
- join(repository, "packages/editor/src/assets/icons"),
];
for (let assetRoot of assetRoots) {
for (let entry of readdirSync(assetRoot)) {
diff --git a/e2e/responsive-comments.e2e.ts b/e2e/responsive-comments.e2e.ts
index b4254dc0..a5cb59d0 100644
--- a/e2e/responsive-comments.e2e.ts
+++ b/e2e/responsive-comments.e2e.ts
@@ -56,7 +56,7 @@ test("a representative compact viewport keeps a passage above the sheet and rest
exact: true,
});
let actionBox = await commentAction.boundingBox();
- let iconBox = await commentAction.locator("img").boundingBox();
+ let iconBox = await commentAction.locator("[data-nucleo-icon]").boundingBox();
expect(actionBox).not.toBeNull();
expect(iconBox).not.toBeNull();
expect(Math.abs(
diff --git a/e2e/sidecar.e2e.ts b/e2e/sidecar.e2e.ts
index b08bff25..c18243b4 100644
--- a/e2e/sidecar.e2e.ts
+++ b/e2e/sidecar.e2e.ts
@@ -1114,8 +1114,8 @@ test("the reply composer grows and keeps one inset send action", async ({ join,
})
));
expect(actionStyles[0].background).not.toBe(actionStyles[1].background);
- expect(actionStyles[0].paddingLeft).toBe("8px");
- expect(actionStyles[0].paddingRight).toBe("8px");
+ expect(actionStyles[0].paddingLeft).toBe("12px");
+ expect(actionStyles[0].paddingRight).toBe("12px");
});
test("a reply joins the thread without a duplicate reply count", async ({ join, seed }) => {
diff --git a/packages/icons/src/icon.tsx b/packages/icons/src/icon.tsx
index 37fe7dfb..10d2867c 100644
--- a/packages/icons/src/icon.tsx
+++ b/packages/icons/src/icon.tsx
@@ -11,8 +11,16 @@ export function LineIcon(
viewBox?: string;
},
) {
+ let labelled = props["aria-label"] !== undefined || props["aria-labelledby"] !== undefined;
return (
-
+
{title && {title} }
Date: Fri, 28 Aug 2026 15:15:15 +0100
Subject: [PATCH 35/35] Stabilize comment geometry recovery test
---
e2e/sidecar.e2e.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/e2e/sidecar.e2e.ts b/e2e/sidecar.e2e.ts
index c18243b4..78c712f2 100644
--- a/e2e/sidecar.e2e.ts
+++ b/e2e/sidecar.e2e.ts
@@ -853,7 +853,9 @@ test("an unavailable comment position keeps its compact sheet mounted until geom
expect(documentBox).not.toBeNull();
expect(markerBox!.y).toBeGreaterThanOrEqual(documentBox!.y + documentBox!.height);
- await page.setViewportSize({ width: 430, height: 844 });
+ // Recover with enough vertical room for the marker after the passage. Whether a
+ // 44px marker fits beside the wrapped text depends on platform font metrics.
+ await page.setViewportSize({ width: 430, height: 3_200 });
await expect(sheet).toBeVisible();
await expect(grabber).toBeFocused();
await expect(marker).toBeAttached();