-
Notifications
You must be signed in to change notification settings - Fork 0
Rebuild UI in the bench-instrument design language #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
eb13be4
1dde4ad
dbb5bda
9973cd3
afc0872
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # design-sync notes for agent-render | ||
|
|
||
| ## Build mechanics (learned on first sync, Aug 2026) | ||
|
|
||
| - The converter needs `cfg.entry` = `.design-sync/ds-entry.ts` (authored re-export list). Without it, PKG_DIR resolves to the nonexistent `node_modules/agent-render` and the build crashes. The entry also exports `sampleEnvelopes` (repo sample payloads) for previews and for the design agent. | ||
| - `.design-sync/process-shim.ts` must stay the first import in ds-entry.ts: components read `process.env.NEXT_PUBLIC_*` at module init and `next/dynamic` expects `process`; without the shim every preview throws `ReferenceError: process is not defined` and window.AgentRender ends up non-component. | ||
| - `.design-sync/harvest-css.mjs` (wired into buildCmd) turns the hashed Next CSS output into the stable cssEntry at `.design-sync/.cache/ds-css/main.css`: rewrites `/_next/static/media/` font urls to repo-relative, lifts the next/font `--font-*` variable classes to `:root`. Re-syncs must run `npm run build` first or the harvest fails loudly. | ||
| - Props auto-extraction can't work here (no shipped .d.ts tree; props types are file-local), so `cfg.dtsPropsFor` carries hand-written bodies for all 13 components. If a component's props change in src, update the config body too; nothing will fail automatically. | ||
| - Preview pattern for `<details>` components (FragmentDetailsDisclosure): open it post-mount via a ref effect (`querySelector("details").setAttribute("open","")`), exactly like a user click. Hover/drag states stay unauthored. | ||
| - tokens/ is empty by design: all custom properties live in the compiled globals.css inside `_ds_bundle.css`. | ||
|
|
||
| ## Preview-authoring learnings (folded from the first-sync wave) | ||
|
|
||
| - App bug found while previewing (surface in any rebuild): DiffRenderer's `oldContent`/`newContent` fallback renders an empty diff body. `src/components/renderers/diff-renderer.tsx` (~line 501) builds `new DiffFile(..., [])` with an empty hunk list; `@git-diff-view/core` never diffs raw contents and `@git-diff-view/file` isn't installed. Previews only use the `patch` path. | ||
| - brotli-wasm cannot load under the static capture server (its `.wasm` fetch 404s), so arx-family encode/decode rejects with "Failed to fetch". Previews drive flows with the deflate codec / `#d...` sample hashes. Any preview using codec "auto" or an arx hash will hit this. | ||
| - Mermaid renders reliably headless, but font metrics clip the last glyph of labels: quote labels and append ` `. Keep `flowchart TD` to <= 5 ranks for the ~536px cell. | ||
| - Native `button.click()` drives React delegated handlers in previews (LinkCreator generates a real link); sequence dependent clicks with ~50ms setTimeout. | ||
| - ViewerShell decoded state: set `window.location.hash` to a deflate sample hash in an effect, then mount behind a ready flag. | ||
| - `lucide-react` bare imports compile in preview-rebuild. Kind-icon map: `src/components/artifact-kind-icons.ts`; getHeading/getSupportingLabel live in `artifact-stage.tsx`. | ||
| - ArtifactSelector's 5-tab strip is ~1490px intrinsic and clips at its own scroll edge (no escape); previews scroll to the active tab post-mount instead of a viewport override (an explicit viewport re-keys and clears grades). | ||
| - ViewerShell has `overrides.ViewerShell = {cardMode: single, primaryStory: EmptyHomepage, viewport: 1280x900}` for a fuller single-card capture. | ||
|
|
||
| ## Known render warns | ||
|
|
||
| - (none recorded yet; add any warn triaged as legitimate here so re-syncs don't chase it) | ||
|
|
||
| ## Re-sync risks (watch-list for the next run) | ||
|
|
||
| - The claude.ai/design agent (Aug 6 redesign run) applied lint fixes directly to the uploaded `_ds_bundle.css`: folded the next-font `__variable_*` classes into `:root`, added `@kind` annotations to unclassifiable tokens, inlined utility-scoped `--tw-*` state vars, and re-scoped the diff theme vars to plain `[data-theme=...]` scopes. A re-sync will overwrite these with the locally-built CSS and re-introduce the adherence flags; ideally teach harvest-css.mjs the same transforms (or accept the flags as known). | ||
|
|
||
| - `dtsPropsFor` bodies are hand-copied from file-local src types; a props change in src updates nothing automatically. When any component's props change, update the config body in the same PR. | ||
| - `.design-sync/ds-entry.ts` is the export list; adding or removing a component in src requires editing it (and `componentSrcMap`, `dtsPropsFor`). | ||
| - `harvest-css.mjs` assumes the Next build emits exactly one CSS file and `__variable_*` font classes; a Next major upgrade can change both. It throws loudly rather than shipping stale CSS, but the fix will be manual. | ||
| - Previews index into `sampleEnvelopes` by position (0, 2, 3, 4, 5); reordering `src/lib/payload/examples.ts` silently changes preview content without failing anything. | ||
| - The ViewerShell/LinkCreator/SampleLinks previews run real decode/generation flows against deflate sample hashes from `sample-link-data.ts`; edits there shift what the cards show. | ||
| - Fonts are fetched by next/font at `npm run build` time (network dependency of the buildCmd). | ||
| - Render check ran against the playwright/chromium-1228 cache matching the repo pin; a repo playwright bump needs a matching browser install. | ||
| - Verified: everything except hover/drag states and the DiffRenderer `oldContent`/`newContent` path (broken in the app, see above). Grades carry via the uploaded `_ds_sync.json`. | ||
|
|
||
|
|
||
|
|
||
| - Purpose of this sync (Aug 2026): the user is rebuilding agent-render's UI in claude.ai/design. Three prior Kimi K3 sessions failed at it. The decided design direction (from the cursor session, `~/Downloads/kimi-failed-cursor.md`) is "fused bench-instrument world": skeuomorphism + Japanese high density + airplane QRH card. Target project: agent-render-skeumorphism. | ||
| - This repo is an app, not a library: no dist entry, converter runs in synth-entry mode from `src/` with tsconfig `@/*` path aliases. | ||
| - Fonts come from `next/font/google` (Fraunces display, IBM Plex Sans body, IBM Plex Mono). There are no @font-face files in source; harvest compiled CSS + woff2 from `out/_next/static/` after `npm run build`. Font families are exposed as `--font-display`/`--font-sans`/`--font-mono` variables set by Next-generated classes on `<html>`, so the shipped styles need a hand-authored bridge that defines those variables at `:root`. | ||
| - `src/app/globals.css` starts with `@import "tailwindcss"` (Tailwind 4 via PostCSS); components mostly use bespoke classes defined in globals.css, but the compiled CSS from the Next build is the honest cssEntry source. | ||
| - Component set (13): ViewerShell, ThemeToggle, ArtifactSelector, ArtifactStage, FragmentDetailsDisclosure, MarkdownRenderer, CodeRenderer, DiffRenderer, CsvRenderer, JsonRenderer, MermaidBlock, LinkCreator, SampleLinks. `use-theme-controller` is a hook, `hash-preview.ts` a utility; neither is a component. | ||
| - Heavy renderers (CodeMirror, mermaid, @git-diff-view/react, papaparse) are dynamically imported in the app; previews must allow them time to mount (`onReady` callbacks exist on renderer props). | ||
| - Vision guideline docs are user-approved to ship in `guidelines/` (skeuomorphism + Japanese density + QRH direction, 16 UI criticisms as hard rules). | ||
| - Agent feedback the user endorsed (Aug 6, from "brodie" and "Orion"): do NOT average the three aesthetics; assign each one a job. QRH supplies procedural grammar (numbered steps, WARN/CAUTION/NOTE callouts, limits, revision/state markers). Japanese UI supplies density. Skeuomorphism supplies material (laminated cards, recessed fields, molded keys, stamped labels, dividers, hardware; never generic shadows). Kill the hero; the link generator is the first viewport; whole viewport a fixed instrument grid; sections become numbered procedure/status blocks; orange reserved for CAUTION/action states; samples become a compact indexed sidebar; CREATE A LINK becomes a five-step operations card with explicit READY/error states. "QRH first, Japanese density second, marketing page never." | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| { | ||
| "projectId": "cadc747e-85f1-42c6-8cc7-230f2bf35887", | ||
| "pkg": "agent-render", | ||
| "globalName": "AgentRender", | ||
| "shape": "package", | ||
| "buildCmd": "npm run build && node .design-sync/harvest-css.mjs", | ||
| "cssEntry": ".design-sync/.cache/ds-css/main.css", | ||
| "srcDir": "src", | ||
| "tsconfig": "tsconfig.json", | ||
| "entry": ".design-sync/ds-entry.ts", | ||
| "guidelinesGlob": ["docs/*.md"], | ||
| "readmeHeader": ".design-sync/conventions.md", | ||
| "overrides": { | ||
| "ViewerShell": { "cardMode": "single", "primaryStory": "EmptyHomepage", "viewport": "1280x900" }, | ||
| "LinkCreator": { "cardMode": "column" } | ||
| }, | ||
| "dtsPropsFor": { | ||
| "MarkdownRenderer": "artifact: { id: string; kind: \"markdown\"; content: string; title?: string; filename?: string }; onReady?: () => void;", | ||
| "CodeRenderer": "artifact: { id: string; kind: \"code\"; content: string; language?: string; title?: string; filename?: string }; compact?: boolean; onReady?: () => void;", | ||
| "DiffRenderer": "artifact: { id: string; kind: \"diff\"; patch?: string; oldContent?: string; newContent?: string; language?: string; view?: \"unified\" | \"split\"; title?: string; filename?: string }; onReady?: () => void;", | ||
| "CsvRenderer": "artifact: { id: string; kind: \"csv\"; content: string; title?: string; filename?: string }; onReady?: () => void;", | ||
| "JsonRenderer": "artifact: { id: string; kind: \"json\"; content: string; title?: string; filename?: string }; onReady?: () => void;", | ||
| "MermaidBlock": "code: string; onReady?: () => void;", | ||
| "ArtifactSelector": "artifacts: Array<{ id: string; kind: \"markdown\" | \"code\" | \"diff\" | \"csv\" | \"json\"; content?: string; patch?: string; oldContent?: string; newContent?: string; language?: string; view?: \"unified\" | \"split\"; title?: string; filename?: string }>; activeArtifactId: string; onSelect: (artifactId: string) => void; kindIcons: Record<\"markdown\" | \"code\" | \"diff\" | \"csv\" | \"json\", React.ComponentType<{ className?: string }>>; getHeading: (artifact: { id: string; kind: string; title?: string; filename?: string }) => string; getSupportingLabel: (artifact: { id: string; kind: string; title?: string; filename?: string }) => string;", | ||
| "ArtifactStage": "activeArtifact: { id: string; kind: \"markdown\" | \"code\" | \"diff\" | \"csv\" | \"json\"; content?: string; patch?: string; oldContent?: string; newContent?: string; language?: string; view?: \"unified\" | \"split\"; title?: string; filename?: string }; envelope: { v: 1; codec: \"plain\" | \"lz\" | \"deflate\" | \"arx\" | \"arx2\" | \"arx3\" | \"arx4\"; title?: string; activeArtifactId?: string; artifacts: Array<{ id: string; kind: string; content?: string; title?: string; filename?: string }> }; fragmentLength: number; hash: string; onArtifactSelect: (artifactId: string) => void; onRendererReady: (readyKey: string) => void; rendererReadyKey: string; statusTone: { color: string; label: string; message: string };", | ||
| "FragmentDetailsDisclosure": "statusLabel: string; fragmentLength: string; maxLength: string; codec: string; hashPreview: string;", | ||
| "LinkCreator": "onPreviewHash: (hash: string) => void;", | ||
| "SampleLinks": "activeHash: string; animationStyle: React.CSSProperties;", | ||
| "ThemeToggle": "className?: string;", | ||
| "ViewerShell": "" | ||
| }, | ||
| "componentSrcMap": { | ||
| "ViewerShell": "src/components/viewer-shell.tsx", | ||
| "ThemeToggle": "src/components/theme-toggle.tsx", | ||
| "ArtifactSelector": "src/components/viewer/artifact-selector.tsx", | ||
| "ArtifactStage": "src/components/viewer/artifact-stage.tsx", | ||
| "FragmentDetailsDisclosure": "src/components/viewer/fragment-details-disclosure.tsx", | ||
| "MarkdownRenderer": "src/components/renderers/markdown-renderer.tsx", | ||
| "CodeRenderer": "src/components/renderers/code-renderer.tsx", | ||
| "DiffRenderer": "src/components/renderers/diff-renderer.tsx", | ||
| "CsvRenderer": "src/components/renderers/csv-renderer.tsx", | ||
| "JsonRenderer": "src/components/renderers/json-renderer.tsx", | ||
| "MermaidBlock": "src/components/renderers/mermaid-block.tsx", | ||
| "LinkCreator": "src/components/home/link-creator.tsx", | ||
| "SampleLinks": "src/components/home/sample-links.tsx" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,50 @@ | ||||||
| # agent-render conventions | ||||||
|
|
||||||
| This library is the component set of a zero-retention artifact viewer app, not a generic UI kit. Read `guidelines/docs/design-vision.md` before designing: it is the owner's target direction (bench-instrument skeuomorphism, QRH procedural grammar, Japanese density) for a ground-up rebuild. The current components carry the old look; keep their behavior and data contracts, push the visuals toward the vision. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Correct the design-vision path.
Proposed fix-Read `guidelines/docs/design-vision.md` before designing
+Read `docs/design-vision.md` before designing📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| ## Setup | ||||||
|
|
||||||
| No provider is required. All styling comes from the stylesheet closure (`styles.css` imports `_ds_bundle.css` and `fonts/fonts.css`). Theme: light is default; dark mode is the `dark` class on the root element (`<html class="dark">`). Never use `prefers-color-scheme` alone; the app toggles the class. | ||||||
|
|
||||||
| ## Styling idiom: CSS custom properties | ||||||
|
|
||||||
| Style your own layout glue with the token variables, never hardcoded colors. The vocabulary (defined at `:root` and overridden in `.dark` inside `_ds_bundle.css`): | ||||||
|
|
||||||
| - Surfaces: `--page-bg`, `--surface`, `--surface-strong`, `--surface-muted`, `--surface-elevated` | ||||||
| - Borders: `--border`, `--border-strong` (hairline 1px borders are the separation grammar; avoid shadows) | ||||||
| - Text: `--text-primary`, `--text-muted`, `--text-soft` | ||||||
| - Accent and semantic: `--accent`, `--accent-strong`, `--accent-secondary`, `--success`, `--warning`, `--danger` | ||||||
| - Depth and shape: `--shadow-lg`, `--shadow-md`, `--radius-xl`, `--radius-lg` (radii are deliberately sharp, 2px) | ||||||
| - Fonts: `--font-display` (Fraunces), `--font-sans` (IBM Plex Sans), `--font-mono` (IBM Plex Mono). Mono is for data and labels, uppercase with letter-spacing for the label register. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [WARNING]: Stale font references that contradict this PR's own changes.
Suggested change
Reply with |
||||||
|
|
||||||
| Component chrome classes ship in `_ds_bundle.css` (`.artifact-action`, `.mono-pill`, `.creator-input`, `.sample-link`, `.viewer-frame`, `.artifact-disclosure`); prefer composing the exported components over reaching for their internal classes. | ||||||
|
|
||||||
| ## Data contracts | ||||||
|
|
||||||
| Renderers take an `artifact` object: `{ id, kind: "markdown"|"code"|"diff"|"csv"|"json", content, title?, filename?, language? }` (diff uses `patch` instead of `content`). Realistic ready-made payloads ship on `window.AgentRender.sampleEnvelopes` (6 envelopes; index 4 holds one artifact of every kind). Use them instead of inventing content. | ||||||
|
|
||||||
| ## Gotchas | ||||||
|
|
||||||
| - Heavy renderers (CodeMirror, mermaid, diff) mount through dynamic imports; they appear a beat after first paint. Each renderer accepts `onReady`. | ||||||
| - DiffRenderer needs a real git `patch` string. The `oldContent`/`newContent` fallback currently renders an empty body (known app gap). | ||||||
| - arx-family codecs need `brotli-wasm`, which may not load in sandboxed frames. For `ViewerShell`/`SampleLinks` hashes, use deflate-coded `#d...` samples. | ||||||
| - `ViewerShell` is the whole page and reads `window.location.hash`; render it alone, not inside a layout. | ||||||
|
|
||||||
| ## Idiomatic composition | ||||||
|
|
||||||
| ```jsx | ||||||
| const { ArtifactStage, sampleEnvelopes } = window.AgentRender; | ||||||
| const env = sampleEnvelopes[4]; | ||||||
| <div style={{ background: "var(--page-bg)", padding: "2rem" }}> | ||||||
| <ArtifactStage | ||||||
| activeArtifact={env.artifacts[0]} | ||||||
| envelope={{ ...env, codec: "arx4" }} | ||||||
| fragmentLength={1982} | ||||||
| hash="#e..." | ||||||
| onArtifactSelect={() => {}} | ||||||
| onRendererReady={() => {}} | ||||||
| rendererReadyKey={env.artifacts[0].id} | ||||||
| statusTone={{ label: "Decoded", color: "var(--success)", message: "Fragment decoded successfully." }} | ||||||
| /> | ||||||
| </div> | ||||||
| ``` | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // design-sync bundle entry: the library view of this app's components. | ||
| // The app has no dist; this is what window.AgentRender exposes. | ||
| import "./process-shim"; | ||
| export { ViewerShell } from "../src/components/viewer-shell"; | ||
| export { ThemeToggle } from "../src/components/theme-toggle"; | ||
| export { ArtifactSelector } from "../src/components/viewer/artifact-selector"; | ||
| export { ArtifactStage } from "../src/components/viewer/artifact-stage"; | ||
| export { FragmentDetailsDisclosure } from "../src/components/viewer/fragment-details-disclosure"; | ||
| export { MarkdownRenderer } from "../src/components/renderers/markdown-renderer"; | ||
| export { CodeRenderer } from "../src/components/renderers/code-renderer"; | ||
| export { DiffRenderer } from "../src/components/renderers/diff-renderer"; | ||
| export { CsvRenderer } from "../src/components/renderers/csv-renderer"; | ||
| export { JsonRenderer } from "../src/components/renderers/json-renderer"; | ||
| export { MermaidBlock } from "../src/components/renderers/mermaid-block"; | ||
| export { LinkCreator } from "../src/components/home/link-creator"; | ||
| export { SampleLinks } from "../src/components/home/sample-links"; | ||
| // Data, not components: the repo's canonical sample payloads, for previews and | ||
| // for building realistic screens with the renderers. | ||
| export { sampleEnvelopes } from "../src/lib/payload/examples"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Harvests the Next build's compiled CSS into a stable cssEntry for design-sync. | ||
| // Next emits hashed filenames and absolute /_next/ font URLs, and sets the | ||
| // --font-* variables via generated classes on <html> that previews never get; | ||
| // this rewrites all three into .design-sync/.cache/ds-css/main.css. | ||
| import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"; | ||
| import { join, dirname } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const repo = join(dirname(fileURLToPath(import.meta.url)), ".."); | ||
| const cssDir = join(repo, "out/_next/static/css"); | ||
| const files = readdirSync(cssDir).filter((f) => f.endsWith(".css")); | ||
| if (files.length !== 1) throw new Error(`expected 1 compiled css, found: ${files.join(", ") || "none"} — run npm run build first`); | ||
|
|
||
| let css = readFileSync(join(cssDir, files[0]), "utf8"); | ||
| css = css.replaceAll("url(/_next/static/media/", "url(../../../out/_next/static/media/"); | ||
|
|
||
| const rootVars = []; | ||
| for (const m of css.matchAll(/\.__variable_[a-f0-9]+\{([^}]+)\}/g)) rootVars.push(m[1]); | ||
| if (rootVars.length === 0) throw new Error("no __variable_ font classes found in compiled css"); | ||
|
|
||
| const outDir = join(repo, ".design-sync/.cache/ds-css"); | ||
| mkdirSync(outDir, { recursive: true }); | ||
| writeFileSync(join(outDir, "main.css"), `:root{${rootVars.join(";")}}\n` + css); | ||
| console.log(`harvested ${files[0]} -> .design-sync/.cache/ds-css/main.css (${rootVars.length} font vars lifted to :root)`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update stale design-sync visual guidance.
These files describe the pre-redesign font system and homepage. A re-sync can use obsolete visual requirements instead of the new bench-instrument interface.
.design-sync/NOTES.md#L44-L44: Replace the Fraunces and IBM Plex font description with the current Manrope, SUSE, and Spline Sans Mono mapping..design-sync/conventions.md#L18-L18: Replace the old font-family claims with the current font variables and families..design-sync/previews/ViewerShell.tsx#L7-L7: Describe the current five-step link creator, indexed samples sidebar, and chassis-style footer instead of the removed hero and bento grid.📍 Affects 3 files
.design-sync/NOTES.md#L44-L44(this comment).design-sync/conventions.md#L18-L18.design-sync/previews/ViewerShell.tsx#L7-L7🤖 Prompt for AI Agents