handon-qnative.md
Hand-on to qnative: the QGUIkit cleanup (c140 D14)
2026-11-12. For: the qnative cycle that carries c140's code rulings, opened after the merge thread is done. Before starting, check that qnative main has the c3, c6 and c7 branches merged (git branch -a, git log). Reads first: c140's review-1.md § Ruled, findings.md, and the evidence on Two Surfaces, Scored. Read at: qnative e609f42. Re-read any line number below at the merged head.
What it is. The design rulings of 2026-11-12, in Mark's order (D14). There are five steps, with a review stop after step 2 and at the end. Design is the driver; the rulings are not reopened here.
1 · The crate takes its name (D1, D2)
crates/layer→crates/qguikit. Update the workspaceCargo.tomland everyuse layer::inapp,storiesanddevmode.- The crate's doc comment (
lib.rs) says QGUIkit is our component library: GPUI Kit draws the shell, GPUI is the framework, and Vue is the other surface (designbrand/glossary.md, Surface). - CLAUDE.md, the README and
/gpui-story's paths follow. - The app's name stays provisional (c0). QGUIkit is the library's name, not the app's.
2 · The four small items (D13)
- The specs. Copy
../design/cycles/c140.surface-evaluation/rescued-specs/spec-*.md(ten files, about 400 KB) intocycles/c2.expressive-range/specs/and commit them. Point c2's CYCLE.md "A spec first" lines at them. Leave design's copy until c140 closes. - The bridge fixture. Copy qnative c4's
it_contact(crates/fixtures/bridge/pilot.json,crates/types/src/bridge.rs:62) into design'sapp/data/worlds/bridge/pilot.jsonand itstypes.ts, sosync-bridge.shcannot delete it. Then decide which repo is the bridge's source and say it in both READMEs. - Six flaws, one commit:
app/src/pages/harness.rs:280: the Skills table's.qp("mcp.tool").sticky_top(…)is a copy from the table above; keep one.qp("harness.skill").layer/src/table.rs:734: the empty box'srgb(0xE8E8E8)→ a role, so it themes in dark mode.app/src/pages/account.rs:119:GRAPE_500on hover → the role, as dark mode wants GRAPE_400.app/src/pages/inbox.rs:53–55:unwrap_or_default()on the store reads draws an SQLite error as "Nothing yet". Draw the failed state instead.app/src/pages/sensor.rs(around 160–170): cycle ids ("design c132 D10", "c131 D17") in UI copy. Say what the person needs; keep the ids in comments.layer/src/scorecard.rs:82, 84: the two stale rows (the sticky header and the expand row's keyboard, both built by D19). Add a row for DataHistoryGraph (c5).
- The token drift check. Add
--checkto../design/scripts/tokens-to-kit.mjs: build both outputs in memory and fail on a diff, against the layer at the tag stamped intokens.rs. Add a CI step in.github/workflows/build.ymlthat checks outq-nuxt-layerat that tag. The read-only comparison c140's token reader wrote is a starting point. It was in a session scratchpad, so write it again if it's gone.
Review stop 1 after step 2: the commits, CI on three systems, and anything step 1 broke.
3 · What leaves the crate, what comes in (D9)
- Out:
- The tour engine (
tour.rs: the registry, the overlay, the glide) moves to its own crate or anappmodule..qp_id()stays a thin trait the components call. - The 17-entry harness registry and its alias resolver (
ux.rs:235–269) andworkspace_mark's git-remote parser (data/surface.rs:81) move into the app's data.
- The tour engine (
- The doc pages:
pub mod demos(2,364 lines) moves to a docs crate, which devmode and stories depend on. The component crate ships no docs (designcomponent_philosophy.md§ Documentation architecture). - In, from the pages:
section()andfailed()(pages/mod.rs),absence_box,severity_pill(inbox),outcome_tag(tools), the lede (copied into seven detail pages), the primary text link (three copies). Each gets a doc page and a scorecard row. - Helpers:
pct,short,plural,short_dateand skill.rs'sbytesmove intoqguikit::data::format. There should be one of each.
4 · The web's names (D8)
- Every QGUIkit component takes its Vue name, with the
DataandUxprefixes. For example:RankedBars→DataRankedBarsSegmentBar→DataSegmentBarDonutChart→DataDonutChartSurfacePanel→DataSurfacePanelMetricValue→DataMetricValuestroke_button()→UxButtonwith its stroke varianttab_group()→UxTabGroup
The full list comes from the scorecard'swebcolumn. - Keep a
#[deprecated]alias per old name for one release. - The scorecard gets a
rustcolumn so the map is a lookup.
5 · Tokens everywhere, motion added, a lint (D5)
- Type:
- 433
text_size(px(literal))and 156line_height(px(literal))become thetext::T*pairs, as devmode already does. - The off-scale values (px 10, 12.8; line heights 22.75, 24.375, 47.5) are decided one by one: fold each to a step, or ask design for one.
- 433
- Radius: 52
rounded(px(literal))becomeradius::R*. - Colour leftovers:
- the 14 hex literals (six of them off the palette: give the tour's
0x0099FFa role); - the 14 raw palette constants that do not theme;
- the 51
.opacity()calls →qp_alphawhere it is meant.
- the 14 hex literals (six of them off the palette: give the tour's
- Motion (all three repos):
- Add tokens to the layer's
tokens.json: fast, base, slow and the one easing. - The bridge emits them to
tokens.rs. - Replace the table's
BORN_FADEandGROW_MSand the duplicateease_in_out_quint. - Design rewrites
component_philosophy.md's 200 / 500 ms rule to what the web does (it uses 300 ms most).
- Add tokens to the layer's
- The lint (a script in CI) refuses:
- hex outside
tokens.rs; text_size(px(<literal>));rounded(px(<literal>));- cycle ids in UI strings;
unwrap_or_default()on store reads.
- hex outside
Review stop 2 at the end: the budget with strobe, against c2's readings (the rename and moves should cost nothing); captures per world compared with the atlas; the calls.
Not in this cycle
- The slot equivalents (D6): named in the docs now; the APIs change when a component is next touched.
- Keyboard and focus (D7): after V1 ships.
- The parity register and the Surfaces panels (D3, D10): design c141.
/native-portandspec-measure(D11): design's skill and script. If they land first, use them here.