plan.md
c132 plan: Native Design Prototyping (draft, before kick-off)
Gate: c130 Phase 1 (the primer) written. Runs beside c131 Phase 1–2 so the first page uses the process. Reader: Mark, then the designers who will mock native screens, then c131's executor.
Why
Design works in a loop: prototype on the design site, experiment in the lab, refine with Mark, push into the real app (the phase-4 lab → qdash port is the current form). The native app breaks the loop's tools: no DOM, no Vue, no Tailwind classes to transcribe, no capture.mts against a URL. This cycle finds the medium and rebuilds the loop so native screens can be mocked, reviewed and pushed with the same rhythm, by designers who may not write Rust.
What we have today
- Media: Vue mocks on the design site (
app/pages/pages/phase*/), Figma through the Figma MCP (/figma-to-vue,/figma-to-component), Illustrator from a page's DOM (/illustrator,scripts/illustrator-export.mjs), the baked lab worlds, the review walk and the cycle atlas (/capture,capture.mts). - Tokens:
brand/design_tokens.md, the layer's Tailwind tokens (--qp-*RGB triplets). - The kit's side: semantic theme tokens (
Theme,ThemeColor), many built-in themes, a component gallery, headless tests that render a view, a wasm build (nightly), design guides and coding guides,/skillsand/llms-full.txt. - Native Shell: a browser playground and gallery; TSX that targets GPUI among others.
Candidate media (the bake-off)
| # | Medium | For | Against |
|---|---|---|---|
| A | Vue mocks in a "native skin" on the design site: a theme that mirrors the kit's tokens and chrome | our fastest muscle; every existing tool works; reviewers need nothing | a second implementation that can drift from what the kit can draw; the transcription step remains |
| B | A stories crate in the c131 repo: one Rust story per page state, screenshotted by headless tests | the real thing; the stories double as tests (R6); no transcription | a Rust build loop; captures by test harness, not by URL; designers need the toolchain or a CI artifact |
| C | The kit's wasm build embedded in the lab: the real UI in a <canvas> on a design-site page | the real thing, reviewable in the browser; capture.mts and the review walk work unchanged | nightly toolchain; unknown fidelity (text input, tables, fonts) in wasm; build size |
| D | Figma, with a kit-shaped library | designers' native tool; the MCP already exists | no Figma library for GPUI Kit is known; Figma → Rust has no skill yet |
| E | Native Shell's playground / TSX targeting GPUI | a browser playground; TSX is near Vue for our designers | a second framework in the stack; its GPUI target's maturity is unknown |
Default to test first: C if it renders our screens, else B with A for early ideation only. D is evaluated as an input (a kit-shaped Figma library) rather than the medium. E is noted unless the primer finds it strong.
Deliverables
process.md— the loop for native screens: brief → mock (medium) → experiment (states and worlds) → refine (review stop, Illustrator if hand work is wanted) → push (the port into c131, with the record in the cycle). Who does which step and with what. Written 2026-11-11 (Phase 3).- The token bridge —
brand/design_tokens.md→ a kit theme file (Qpoint light and dark), generated by a script so the web and native tokens come from one source. Includes the type decision: Inter, bundled (c131 D14, ruled 2026-11-09). Exists:scripts/tokens-to-kit.mjs, built in qnative c2 (2026-11-10), documented inprocess.md(Phase 3). - Captures for native — a
nativemode forcapture.mts(wasm page, or the stories' screenshots collected into the same manifest), so the cycle atlas shows native screens beside web ones. Built 2026-11-11 (Phase 3):--native <program.json>, the stories per world. - Skills — the kit's skills installed into design's
.claude/skills(from the primer), plus ours: a/gpui-story(page state → story + test) and, if D holds, a/figma-to-gpui. Only what the worked example needed./gpui-storywritten 2026-11-11 (Phase 3); the kit's skills not installed yet; no/figma-to-gpui(Q2 open). - One worked example — the Inventory page (or Status) taken through the whole loop and into c131. Status, picked 2026-11-11: through the loop in A the same day; the push into c131 handed to qnative c2 (Phase 4).
- A design-site page — the process documented where the other processes live (
/methodor the components docs), with the bake-off record. Done 2026-11-11 (Phase 5): theCLAUDE.mdsection and/pages/native, with the bake-off page as the record.
Phases
0. Survey
Read the primer. List what each medium needs installed. Pick the page for the bake-off (Inventory: tables, lists, empty and overflow states, the hardest common case). Adopt the named synthetic worlds (ruled 2026-11-11, c131 D15): one composable, useNativeWorld(name), reads ../qnative/crates/fixtures/world/<name>.json by request key, so every native-skin mock draws the rows the app draws and invents none.
Done 2026-11-11. The page is Inventory (ruled): the landing, the Harnesses listing and the harness page, in three states by world. The worlds are copied into the lab (app/data/worlds/, scripts/sync-worlds.sh, the deploy builds here so the copies must live here) and read by app/composables/useNativeWorld.ts: one dynamic import per world (only the named one is bundled), the manifest, and get(route, params?) building the nozzle's key (route?sorted-query, period=all dropped, quote_plus encoding, from types::Request::key and the keys in the files); loaded on the client so the server renders the honest loading state and the payload carries no rows. What each medium needs installed, as found:
| Medium | A person who mocks needs | An agent that mocks needs | Captures |
|---|---|---|---|
| A Vue mocks in a native skin | a browser on :3000, or the static site | the design repo (npm install, Node 22), the layer (@qpoint-io/q-nuxt-layer or NUXT_LOCAL_LAYER), the worlds synced | capture.mts unchanged (Playwright's Chromium, in the repo); a shot program names the world per shot (scripts/shot-programs/c132-bake-off.json) |
B the stories crate in ../qnative | a built binary (a CI artifact or cargo run -p stories --show), or the PNGs | Rust 1.95 or later (Homebrew's 1.99.1; the kit calls std::hint::cold_path), the workspace (cargo build: about 3 min clean, 424 crates; 2–5 s incremental), gpui-kit pinned =0.9.1, Python 3 for ts-to-rust.py and cut-world.py (the latter needs the qdash binary from ../qcontrol for a recut) | cargo run -p stories writes target/stories/<story>-<mode>.png at 2× off-screen (Window::render_to_image, no Screen Recording); scripts/publish-stories.py copies chosen PNGs into public/cycles/qnative/ with surface: native |
| C the kit's wasm build in the lab | a browser | nightly Rust, the wasm32 target, the kit's showcase build | would be capture.mts unchanged |
| D Figma with a kit-shaped library | Figma | the Figma MCP (connected in this session); no kit-shaped library exists to install | get_screenshot |
| E Native Shell's playground / TSX | a browser | Node and the Native Shell repo at main (its desktop features are unreleased) | none into the atlas |
1. Bake-off
The same page in A, B and C (and D as an input if a library turns up), each with three states. Captures side by side on a lab page. Time and friction recorded per medium.
2. Mark picks the medium
Review stop. The pick and its reasons recorded here and in CYCLE.md.
Ruled 2026-11-11 (evening), from review.md. B, the stories crate in ../qnative, is the medium of record for a native screen: the real thing, the stories are the page's tests, the captures are solved. A, Vue mocks in a native skin on the design site, is the medium to sketch and to review: for a person without a toolchain and for an agent sketching before Rust. D6 holds: an A mock names its world and the story that follows is checked against it row for row. The review's amendment stands: A is not "ideation only" but the review medium for people. Two more rulings: the chrome (D7) and the mock's lifetime (D8), below.
3. Build the loop
The token bridge, the capture mode, the skills, process.md.
Done 2026-11-11 (evening), at its review stop (review-phase3.md). The native capture mode: scripts/capture.mts --native <program.json> runs the stories binary once per shot with QPOINT_WORLD set and merges the PNGs into the atlas with surface: native, subject: native/<story>, variant: world=<name> (D3); scripts/shot-programs/c132-native-worlds.json took the three Inventory views in all three worlds (13 captures, thread worlds), which closed B's side of the bake-off. The stories read QPOINT_WORLD through a one-line patch, applied in a scratch copy for the first captures and handed to qnative c2, which landed it the same evening (b4cbc23) together with the recut (meridian-day90 = nina-mbp, pilot = priya-x1); the lab re-synced and both media were recaptured across four worlds. capture.mts also gained scale (device scale factor) and clip (one element as the picture) for programs and single shots, so A is captured as the window at the app's 2× (the eleven bake-off captures retaken). The skill /gpui-story (.claude/skills/gpui-story/): a page state → a Story, its test, the per-world capture, and the path from a Vue mock to a story. process.md: the five steps, who does which with what, the two media exactly, the native capture mode, the token bridge documented (scripts/tokens-to-kit.mjs, built in qnative c2; not rebuilt). Not done: the kit's own skills are not installed (install from the repo at the pinned tag when a cycle needs them); no dark variant per world for A.
4. The worked example
Status or Inventory through the loop and pushed into c131 Phase 2.
Mark picked Status (2026-11-11, at the Phase 3 review stop). Status has no page in either medium yet, so it exercises the loop end to end: the brief (status-brief.md), the mock in A on the four worlds, the experiment, the review stop, the push into c131 as a story. The brief names the one fork the page has: the sensor's own shapes (state, resources, effective config, logs; c131 D17) are served by no route yet, so no world carries them and the mock cannot draw them without inventing (D6).
Built 2026-11-11, at its review stop (review-phase4.md): the mock on four worlds with a knob for the states no world carries and degraded derived from the coverage counters; eight captures; the calls S1–S6 taken by default; the push handed to qnative c2 as a story per state.
Ruled 2026-11-11 (evening, from review-phase3.md, before the Phase 4 note was read): Status is the page, through the whole loop: sketch in A on the four worlds, then the story and the page in B, captures per world into both atlases (D9), the page into c131. The six endpoint-only shapes get the bridge of D10 instead of the mock's "not served yet" (S3 overturned) and the state knob narrows to the states no world or bridge carries (S2 narrowed). B is built in this session once qnative c2 has closed (S6 overturned): pages/status.rs, the nozzle requests for the six shapes behind the bridge, the story; committing only its own paths there, small commits with a message to the other sessions on shared files, never a push.
Built the same evening, at its review stop: the bridge per world (app/data/worlds/bridge/), the mock reading it, the mirror (D9) retro-applied, and Status in B in qnative (pages/status.rs, the bridge in types and fixtures, the nozzle, the route, four stories, the composed asset source), captured per world into design's atlas (p4-status-b); the bake-off page's Phase 4 section wipes A over B for seven states.
S1–S6 ruled (c131 D21, the same evening): Status is the landing with the overview's micros below; a "This machine" group (Status, Sensor, Activity) before Inventory; a Sensor page for the five facts in full and the log; an Activity page for sessions over time; the knob kept; the push is qnative c5 on design's first cut. The lab followed: /pages/native/status reshaped, /pages/native/sensor and /pages/native/activity built on the four worlds and captured.
5. Document
The design-site page; CYCLES.md and c130's record updated.
Done 2026-11-11: the loop documented in CLAUDE.md (a section after Sketches and stories, where the other processes live) and on /pages/native (the design-site page), with process.md as the full text; CYCLES.md in line; c130's record left to its own session with a suggested line.
Decisions (defaults)
| # | Question | Default |
|---|---|---|
| D1 | Medium | Ruled 2026-11-11: B (the stories crate) of record; A (Vue mocks in a native skin) to sketch and to review, for a person without a toolchain and for an agent sketching before Rust. The default had been C if it works, else B, A for ideation only; the primer removed C and the bake-off promoted A to the person's review medium. |
| D2 | Token source | brand/design_tokens.md stays the source; the kit theme is generated, never hand-edited. |
| D3 | Where native captures live | the same manifest and atlas as web captures, with a surface: native field. Built (Phase 3): the native capture mode merges per-world story captures into public/cycles/<project>/manifest.json with surface: native, variant: world=<name>, world. |
| D4 | Review stop form | the lab's review walk for C; a captures page for B. |
| D5 | Illustrator | available from captures (raster) only; the DOM-based export doesn't apply to native. |
| D6 | Rows in mocks | Never invented (ruled 2026-11-11): the lab reads the same named worlds as the app through useNativeWorld(name); a mock names its world the way a story does. |
| D7 | The native skin's chrome | Ruled 2026-11-11: keep the imitation in app/components/native/ (the kit's shell drawn with our tokens), re-checked at each review stop by a wipe against a capture of the real shell; no Vue port of the kit's Shell, no compositing of mocks into shell PNGs. |
| D8 | A mock's lifetime | Ruled 2026-11-11: when a page exists as a story, its Vue mock stays as the person's medium, naming its world so the story stays checkable against it. |
| D9 | Native captures in design's atlas | Ruled 2026-11-11 (evening, from review-phase3.md): the native capture mode also copies each native capture into design's own atlas under the cycle's thread, the qnative atlas keeping the originals, so a design cycle's visual record shows B beside A without crossing projects. Retro-applied to the worlds thread. |
| D10 | The bridge for the six endpoint-only shapes | Ruled 2026-11-11 (evening): until c131 D17's routes exist (engineering took the ask the same day), the sensor's six shapes get a clearly marked bridge fixture per world beside the world copies (app/data/worlds/bridge/<world>.json, never inside the world files, which stay copies of qnative's), typed from synthetic-data.md's shapes; the footer and the record say it is a bridge; the same bridge feeds the story in qnative. |
Clarifying questions
Who mocks native screens?Answered 2026-11-09: both. Mark and agents will each mock various aspects at different times, and being able to mock is crucial to any plan. Consequence: the medium must serve a person without a Rust toolchain and an agent with one; C (the real UI in the browser) and B (stories as tests) are both pursued, and whichever wins, a captured, reviewable artifact per page state is non-negotiable.- Is a kit-shaped Figma library worth building if none exists, or is Figma out for native?
Brand type on native?Answered 2026-11-09: Inter, bundled (c131 D14); the bridge carries the type scale.
Patterns to consider
- Stories as both the design site and the tests (c131 R6): one artifact, two readers.
- The wasm build as the lab's embed: the native app's screens reviewed in the browser, captured with the tools we have.
- Tokens generated from one file into Tailwind and the kit theme: the seam between the two component systems (c130 Risks).
- The kit's
llms-full.txtas the executor's ground, installed once, versioned with the kit.