Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/desktop/layout-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
}
},
"shell": {
"titlebarHeight": 48,
"titlebarHeight": 46,
"regions": {
"navigationRail": {
"defaultWidth": 288,
Expand Down Expand Up @@ -114,9 +114,9 @@
}
},
"verticalRhythm": {
"titlebarHeight": 48,
"normalControlHeight": 28,
"fieldControlHeight": 32,
"titlebarHeight": 46,
"normalControlHeight": 32,
"fieldControlHeight": 36,
"sectionGap": 16,
"pageSectionGap": 32
},
Expand Down
71 changes: 25 additions & 46 deletions apps/desktop/src/github/PullRequestsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "react";
import type { ReactNode } from "react";

import { LoadFeedback } from "@/components/business/load-feedback";
import { MasterDetailRow } from "@/components/business/master-detail-row";
import { StatusBadge } from "@/components/business/status-badge";
import type { StatusTone } from "@/components/business/status-badge";
Expand Down Expand Up @@ -461,28 +462,17 @@ export function PullRequestsPage({
<ScrollArea className="min-h-0 flex-1">
<div className="px-3 pb-4">
{loading && items.length === 0 ? (
<div
role="status"
className="text-body text-muted-foreground flex items-center justify-center gap-2 py-12"
>
<ActivityOrb state="searching" visualSize={14} />
{t("pullRequests.loading")}
</div>
<LoadFeedback
state="loading"
message={t("pullRequests.loading")}
/>
) : error != null && error !== "" ? (
<div
role="alert"
className="text-body text-muted-foreground mx-1 flex flex-col items-center gap-3 py-12 text-center"
>
<CircleAlert className="text-destructive size-4" />
<p className="max-w-72">{error}</p>
<Button
variant="secondary"
size="compact"
onClick={() => void reload()}
>
{t("pullRequests.retry")}
</Button>
</div>
<LoadFeedback
state="error"
message={error}
retryLabel={t("pullRequests.retry")}
onRetry={() => void reload()}
/>
) : groups.length === 0 ? (
<Empty className="py-section">
<EmptyHeader>
Expand Down Expand Up @@ -682,32 +672,21 @@ export function PullRequestsPage({
</header>
{selected ? (
detailState?.loading === true && !detail ? (
<div
role="status"
className="text-body text-muted-foreground flex min-h-0 flex-1 items-center justify-center gap-2"
>
<ActivityOrb state="searching" visualSize={14} />
{t("pullRequests.loadingDetail")}
</div>
<LoadFeedback
state="loading"
message={t("pullRequests.loadingDetail")}
/>
) : detailState?.error != null && detailState.error !== "" ? (
<div
role="alert"
className="text-body text-muted-foreground flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6 text-center"
>
<CircleAlert className="text-destructive size-4" />
<p>{detailState.error}</p>
<Button
variant="secondary"
size="compact"
onClick={() => {
const current = selected;
setSelectedId(null);
setTimeout(() => setSelectedId(current.id), 0);
}}
>
{t("pullRequests.retry")}
</Button>
</div>
<LoadFeedback
state="error"
message={detailState.error}
retryLabel={t("pullRequests.retry")}
onRetry={() => {
const current = selected;
setSelectedId(null);
setTimeout(() => setSelectedId(current.id), 0);
}}
/>
) : detail ? (
<div className="pull-request-detail-workspace min-h-0 flex-1">
<ScrollArea className="pull-request-primary min-h-0">
Expand Down
105 changes: 105 additions & 0 deletions apps/desktop/tests/designContract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";

const normalizeSource = (source: string) => source.replaceAll(/\r\n?/gu, "\n");
const readSource = (relativePath: string) =>
normalizeSource(
readFileSync(new URL(relativePath, import.meta.url), "utf-8")
);

const layoutSpec = JSON.parse(readSource("../layout-spec.json")) as {
spacing: Record<string, number>;
shell: {
titlebarHeight: number;
regions: Record<
string,
{ defaultWidth: number; minWidth?: number; maxWidth?: number }
>;
};
content: {
primaryColumn: { maxWidth: number };
settings: { maxWidth: number };
};
verticalRhythm: Record<string, number>;
};
const tokens = readSource("../src/design/tokens.css");
const styles = readSource("../src/styles.css");
const app = readSource("../src/App.tsx");
const pullRequests = readSource("../src/github/PullRequestsPage.tsx");

const declarations = new Map<string, string>();
for (const match of tokens.matchAll(/--([\w-]+):\s*([^;]+);/gu)) {
declarations.set(match[1], match[2].trim());
}

// Semantic roles alias foundations (`--ds-space-page: var(--ds-foundation-space-24)`), and the
// foundation names embed their size, so walk the chain and read the trailing number.
const resolvePx = (name: string): number | null => {
const value = declarations.get(name);
if (value == null) return null;
const px = /^(\d+)px$/u.exec(value);
if (px != null) return Number(px[1]);
const ref = /^var\(--([\w-]+)\)$/u.exec(value);
if (ref != null) return resolvePx(ref[1]);
const trailing = /-(\d+)$/u.exec(name);
return trailing == null ? null : Number(trailing[1]);
};

describe("design contract alignment", () => {
test("normalizes source contracts across platform line endings", () => {
expect(normalizeSource("first\r\nsecond\rthird")).toBe(
"first\nsecond\nthird"
);
});

test("keeps the layout spec on the token vertical rhythm", () => {
expect(resolvePx("ds-titlebar-height")).toBe(
layoutSpec.verticalRhythm.titlebarHeight
);
expect(resolvePx("ds-titlebar-height")).toBe(
layoutSpec.shell.titlebarHeight
);
expect(resolvePx("ds-control-normal")).toBe(
layoutSpec.verticalRhythm.normalControlHeight
);
expect(resolvePx("ds-control-field")).toBe(
layoutSpec.verticalRhythm.fieldControlHeight
);
expect(styles).toContain("height: var(--ds-titlebar-height);");
});

test("keeps the layout spec on the token spacing scale", () => {
for (const [name, value] of Object.entries(layoutSpec.spacing)) {
const camel = name.replace(/[A-Z]/gu, (c) => `-${c.toLowerCase()}`);
expect(resolvePx(`ds-space-${camel}`)).toBe(value);
}
});

test("keeps the shell defaults and clamps on the spec", () => {
const rail = layoutSpec.shell.regions.navigationRail;
const dock = layoutSpec.shell.regions.dock;
expect(app).toContain(`"codetwo.railWidth",\n ${rail.defaultWidth}`);
expect(app).toContain(
`Math.min(${rail.maxWidth}, Math.max(${rail.minWidth}, railWidth))`
);
expect(app).toContain(`"codetwo.dockWidth",\n ${dock.defaultWidth}`);
});

test("keeps the content measure on the spec", () => {
const column = layoutSpec.content.primaryColumn.maxWidth;
expect(column).toBe(768);
expect(layoutSpec.content.settings.maxWidth).toBe(column);
expect(column / 16).toBe(48);
});

test("routes the pull-request blocking states through LoadFeedback", () => {
expect(pullRequests).toContain(
'import { LoadFeedback } from "@/components/business/load-feedback";'
);
expect(pullRequests.match(/<LoadFeedback/gu)).toHaveLength(4);
expect(pullRequests).not.toContain('pullRequests.loadingDetail")}</div>');
expect(pullRequests).not.toContain(
'role="status"\n className="text-body text-muted-foreground flex items-center justify-center gap-2 py-12"'
);
});
});
11 changes: 10 additions & 1 deletion docs/design/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ The shared business set is:

| module | owns | current callers |
| --- | --- | --- |
| `PageHeader` | page heading hierarchy, description measure, responsive action placement | Automations, Plugin Hub, Scene Studio, Task Board |
| `PageHeader` | page heading hierarchy, description measure, responsive action placement | Automations, Plugin Hub, Scene Studio, Task Board, settings pages |
| `SearchField` | labelled search input, icon geometry, optional accessible clear action | Automations, Docker, Task Board, Plugin Manager, Memory, Trajectory |
| `Empty` primitive | empty-state hierarchy, media, description, and action composition | Automations, Pull Requests |
| `SelectableRow` | compact picker choice, visible selection mark, accessible selected/disabled state, description and metadata layout | Composer mode, memory, collaboration, worktree, provider, and model pickers; Scene picker; Checkout picker |
Expand All @@ -191,6 +191,15 @@ The shared business set is:
| `StatusIndicator` | semantic dot-and-label status with theme-managed tones | Docker, Device connections |
| `SettingToggle` | visible label and description association, immediate boolean control, disabled presentation, and row layout | Project actions, Memory, Sync, Project scheduling, Appshots, Pets |

The settings cohort composes those primitives once, in `src/settings/SettingsPrimitives.tsx`: `Page`
wraps `SettingsPanel`/`PageHeader`, `Row` and `ProjectRow` wrap `SettingRow` (the project variant
adding the shared trailing control lane), and `GroupHeading` owns the 14px/600 group label used by
the pages whose sections already own their spacing. Settings pages consume that module instead of
re-deriving the anatomy; the module is cohort-local rather than a business primitive, and it is
subject to the same lint restrictions as every other product file. Current callers: General,
Import, Keybindings, Project, Worktrees, Memory, Providers, Appshots, and the Appearance, Pets and
Sync pages that are wrapped through `SettingsPage`.

`SelectableRow` is deliberately limited to persistent selection inside compact pickers. Radio or
checkbox questions use `ChoiceRow`; navigation/current-page rows, disclosure rows, and master-detail
list rows keep their own interaction contracts. `StatusBadge` is limited to labelled pills; dot-and-label status
Expand Down
57 changes: 57 additions & 0 deletions docs/sdlc/changes/2026-09-16-align-design-contracts/intent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
id: 2026-09-16-align-design-contracts
schema: 5
stage: intent
status: accepted
owner: chenli
created: 2026-09-16
source: user
risk: medium
approved_by: chenli
approved_at: 2026-09-16
approval_source: "Direct request: 进行治理, after the audit list of governance findings in this session."
next_trigger: chenli reviews the verified work.
---

# Intent: Align the design contracts with the code

## Intent

The governance item of the agreed repair order: make the design system's machine-readable contracts
describe what the code actually does, register the shared module the doc omits, and give the loading
contract real callers.

Findings, re-verified against the live checkout:

1. `apps/desktop/layout-spec.json` disagrees with the token sheet on three vertical-rhythm numbers
(`titlebarHeight` 48 versus the 46px `--ds-titlebar-height`, `normalControlHeight` 28 versus the
32px `--ds-control-normal`, `fieldControlHeight` 32 versus the 36px `--ds-control-field`) and the
stale titlebar value also appears under `shell`. Nothing fails when the two drift apart: the file
is read only by tests.
2. `apps/desktop/src/settings/SettingsPrimitives.tsx` is the settings-scoped shared layer (Page, Row,
ProjectRow, GroupHeading) used by six settings pages, but `docs/design/system.md` never registers
it and the `PageHeader` caller list omits every settings page, so the doc's component map and the
code disagree.
3. `LoadFeedback` — the doc's owner for content-blocking loading and recoverable failures — has no
product caller; only the development preview renders it. `github/PullRequestsPage.tsx` hand-rolls
the identical shape four times (list and detail, loading and failure).
4. Correction to this session's earlier audit: `ControlChip` is **not** a dead contract — the
composer and `SceneChip` import it (aliased as `Chip`), with nine call sites. The earlier claim
came from grepping the literal `<ControlChip` tag; the record for that audit is superseded here.

Outcome: the layout spec and the token sheet agree and a test fails if they drift again; the design
doc registers the settings-scoped layer and names its real callers; the pull-request workspace uses
the shared `LoadFeedback` for all four of its blocking states.

Constraints: no visual redesign of the loading and failure states (the shared component's treatment
is the target), no change to layout-spec's already-correct numbers, and no move or rename of the
settings module in this change — only its registration.

Non-goals: the remaining governance items from the audit (status-language convergence, action
budget, motion and focus work, the `xs` variant, legacy aliases, off-scale icons) stay in their own
records.

## Non-goals

No new component, no behavior change beyond routing the four existing blocking states through the
shared component, and no rewrite of the design doc's enforcement section.
47 changes: 47 additions & 0 deletions docs/sdlc/changes/2026-09-16-align-design-contracts/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
id: 2026-09-16-align-design-contracts
schema: 5
stage: plan
status: accepted
owner: chenli
created: 2026-09-16
based_on: spec.md
scope: apps/desktop/layout-spec.json, apps/desktop/tests/designContract.test.ts, apps/desktop/src/github/PullRequestsPage.tsx, docs/design/system.md, docs/sdlc/changes/2026-09-16-align-design-contracts
---

# Plan: Align the design contracts with the code

## Plan

1. `apps/desktop/layout-spec.json` — `shell.titlebarHeight` and `verticalRhythm.titlebarHeight`
48 → 46, `verticalRhythm.normalControlHeight` 28 → 32, `fieldControlHeight` 32 → 36. (AC-1)
2. `apps/desktop/tests/designContract.test.ts` — new source contract that parses the spec, the token
sheet and the shell code and asserts agreement: titlebar/control heights, the rail's persisted
default and 220/420 clamps, the dock's 440 default, the spacing scale, and the 768 content
measure. (AC-2)
3. `docs/design/system.md` — register `src/settings/SettingsPrimitives.tsx` as the settings-scoped
composition with its six callers, and add the settings pages to the `PageHeader` caller row.
(AC-3)
4. `apps/desktop/src/github/PullRequestsPage.tsx` — route the list and detail loading/failure states
through `LoadFeedback` and delete the four hand-rolled blocks, keeping the page's messages and
retry handlers. (AC-4)
5. Correct the false `ControlChip` claim in this session's audit inside the Intent record; no code
change, because the composer and `SceneChip` already have nine call sites. (record)

Checks by risk and affected behavior:

- Desktop: `bun run lint`, `bunx tsc --noEmit`, `bun test`, `bun run build:renderer` from
`apps/desktop`.
- Rendered (AC-4): the UI Lab pull-request scenario renders the real workspace with fixtures from
this worktree's renderer (`bun run dev:renderer`, port 1420); the loading and failure states are
transient or Core-dependent, so they are covered by the source contract and recorded as residual
risk instead of a claimed capture.
- Repository: `bun script/verify/sdlc.ts --worktree` and `bun script/verify/docs.ts` before handoff.

Temporary resources: the task-owned renderer log at
`/var/folders/nl/47s4vtc92m74_j8pmm7d0chh0000gn/T/opencode/renderer-1420.log`, the Vite server on
port 1420, and the ignored `apps/desktop/dist/` build output. All are stopped or removed before
handoff; screenshots are retained as Verification evidence.

Rollback: `git revert` of the single commit; the spec and doc edits are inert, and the page change
restores four local blocks.
Loading
Loading