Two Surfaces, Scored
c1402026-11-12 · the QGUIkit surface against twenty principlesOur component system has two surfaces: Vue, through q-nuxt-layer, and QGUIkit, our components drawn in Rust on GPUI in the native app. This page scores the QGUIkit surface against the principles in our foundation docs: the eight Mark named, and the twelve the docs add. Each row has a score, its evidence, and the smallest change that would raise it by one.
Read at qnative e609f42, design 4453a8c085 and the layer at e7e1454, while another session merged branches in both repos. The rubric was written before the evidence came back (the plan). The Vue surface is the reference, not re-audited. The trend column compares the two only where the evidence did.
Mark walked the eleven calls the same day. The second surface is QGUIkit, and the crate takes the name. c135 cleans both surfaces. The token sweep comes with motion tokens and a lint. The components take the web's names, and the crate is restructured. All of that goes to one qnative cycle after the merge. The slot mapping is written down now, and the APIs change later. Keyboard access waits until V1 ships. The dual-surface docs get their own cycle, c141.
A fast, faithful port with honest data boundaries, held together by attention.
In three days, agents drew 30 of our components on GPUI, ported the Inventory section, Status, Activity, the inbox and developer mode, and kept the budget. Where a machine holds the work, it is sound: the crate graph keeps the layer away from every data source, the types are generated, the worlds pass a contract test, the stream is held by vectors run from qdash’s own code, and the footprint is measured at every phase.
Wherever a person was meant to remember, it is soft: type set by 433 literals instead of tokens, one slot kind of three, keyboard access in one component, half the names changed, a scorecard stale within a day, and public docs that do not know the second surface exists.
The correction is not a rewrite. It is to move the half held by eye into the half held by machines, and to start writing down where the surfaces differ on purpose.
None at 5: nothing on the QGUIkit surface is held by construction and checked by a machine, though the layer's data boundary comes closest. None at 1. Against the Vue surface: 2 better, 9 the same, 8 worse, and 1 with nothing on the Vue side to compare (the cost of two surfaces itself).
At a glance
Each row links to its evidence below.
The eight Mark named
| # | Principle | Score | Against Vue | In a line |
|---|---|---|---|---|
| 1 | Modularity | 3 | same | The units are real and the crate boundary is clean, but four of the biggest files are several components and an engine each. |
| 2 | Clean data boundaries | 4 | better | The strongest row. The layer cannot see a source by construction; the pages mostly go page → repository → nozzle, with four named leaks. |
| 3 | Composition over configuration | 3 | worse | DataMachine kept its six slots, so the card composes as on the web. Elsewhere the builder pattern pulled toward flags, typed children and pre-built cells. |
| 4 | View complexity contained | 3 | worse | The hard internals live inside the table: measuring, easing, focus. But a listing is not a listing until the page wires sort, filter, columns and the sticky band back in. |
| 5 | Low knowledge of context | 3 | same | No component knows the rail or the window. Several know the page: its spacing, its viewport and the theme global. |
| 6 | Slots | 3 | worse | GPUI has all three kinds of slot. We use one of them. |
| 7 | Brand tokens | 2 | worse | Colour travels well and matches today. Type, radius and spacing do not travel at all. Nothing notices when either drifts. |
| 8 | Two surfaces | 2 | — | Not drift hell yet: the surfaces are two days apart and the data contracts are held by machines. But everything a person sees is held by hand and by eye, and four drifts happened in the first three days. |
What our docs add
| # | Principle | Score | Against Vue | In a line |
|---|---|---|---|---|
| 9 | Functional, not decorative; colour is semantic | 3 | same | Variants, not colour arguments, as on the web. But the variant sets are too small, so pages paint their own. |
| 10 | Native to the medium | 3 | same | The layer reads as GPUI, with builders, RenderOnce and keyed state. In places it fights the framework instead of using it. |
| 11 | Progressive enhancement; base and presets | 3 | same | The bases are simple where the web’s are. The missing pieces are small primitives, not big presets. |
| 12 | Accessibility by default | 2 | worse | The table is accessible. Nothing else is. The web gets roles and tab order from the DOM for free; GPUI gives nothing for free, so every component must ask. |
| 13 | States are explicit | 2 | worse | Hover is drawn; pressed, focus and disabled are not. The honest data states have a shared shape, but each page draws them its own way and none is tested. |
| 14 | Replicate first, optimize later | 4 | same | Applied with real discipline: every page measured on the web before it was drawn, every oddity decided rather than copied. The record of it was nearly lost. |
| 15 | The demo is the test | 3 | same | Every page state is drawn, light and dark, and the newest pages are real tests. The Inventory section, the inbox and every error state are pictures only. |
| 16 | Constrained size and consistent motion | 3 | same | Size steps are named enums, as on the web. Motion has no tokens on either surface, and the written rule is out of date on both. |
| 17 | The unified pantry | 3 | same | The vocabulary is in the layer. Small pantry pieces still sit in the pages, and a few app-wired pieces sit in the layer. |
| 18 | Documentation architecture | 2 | worse | The native docs are good and in the wrong place: inside the library, and inside developer mode. The public docs do not know the QGUIkit surface exists. |
| 19 | Naming | 2 | worse | The crate promises the web’s names and keeps about half of them. |
| 20 | The budget | 4 | better | A principle the web never wrote down, and the QGUIkit surface keeps it best of all: measured per release, recorded, and drawn. |
The rubric
- 5Holds by construction: the shape of the code makes the wrong thing hard, and a machine checks it
- 4Holds in practice: every instance we read honours it, checked by a person’s eye
- 3Mostly holds, with named exceptions that have a known fix
- 2Holds only where someone was careful; the structure does not help
- 1Does not hold
Each divergence between the surfaces is one of three kinds:
- flavorthe medium does it differently on purpose: welcome it and write it down for the operator
- gapone surface cannot do it yet: write it down until it can
- flawa mistake on our side: fix it
The evidence
Paths are under ../qnative/crates/ unless named. Counts were taken at e609f42; four readers gathered them, and the sharper claims were checked by hand.
The eight Mark named
Modularity
3same as Vuecomponent_philosophy.md · Design Lineage; design_methodology.md · Decomposition and Modularity
The units are real and the crate boundary is clean, but four of the biggest files are several components and an engine each.
- The layer crate depends on gpui-kit alone (crates/layer/Cargo.toml); nothing in it can reach the app, the types, the nozzle or the fixtures.
- 18 components are plain structs that derive IntoElement and implement RenderOnce; 8 more are builders over a private RenderOnce element. No component in the core is an Entity view.
- Four ways to write a component: RenderOnce structs (18), builders over a private element (8), free functions returning AnyElement (about 20, which take cx as an argument), and low-level Element impls (4). A reader cannot tell from a name which one they are holding.
- TableEl::render is 387 lines (table.rs:443–830): column measurement, the new-row flash, the well easing, keyboard navigation, AccessKit roles, tour registration and scroll-into-view in one function.
- editor.rs holds a file tree, a frame, a tab switch, a four-language highlighter and a parser for skill files ($ARGUMENTS, shell spans). ux.rs holds nine pieces, a 17-entry harness registry and global image caches.
- tour.rs is an app-wide engine (a global registry, an overlay, a scroll glide that spawns tasks) living in the component crate.
Clean data boundaries
4better than Vueqnative CLAUDE.md R1–R2; component_philosophy.md · the pantry boundary test
The strongest row. The layer cannot see a source by construction; the pages mostly go page → repository → nozzle, with four named leaks.
- The Cargo graph is the check: layer has no dependency on types, nozzle, fixtures or app, so a component cannot import a served shape even by accident.
- Inputs are view data: SurfaceRow::new(key, label, value), RankRow::new(label, value, pct), Row::new(key, cells). Navigation is always a callback. The Vue DataMachine calls useRouter().push(props.to) itself; the Rust one takes on_click.
- Listings and details all read through load(cx, |repo, p| repo.harnesses(p), …). No page reads the filesystem, env vars or fixtures.
- The layer reads the wall clock: recency_dot (ux.rs:507) and format::ago (format.rs:131) call SystemTime::now. A story that freezes the world’s clock still gets “ago” from today.
- Domain logic in the layer: workspace_mark parses git remote URLs (surface.rs:81); kind_pill string-matches "builtin" and "skill".
- Status keeps its read model (SensorRead) in the page and compares Source::Plane (status.rs:132, 159); messages_settings.rs:67 branches on Source::Local.
- messaging.rs (667 lines) is the inbox repository, branches on the source in about seven places, reads QPOINT_INBOX, and its drill() takes message E2 straight from fixtures::messages::examples() (messaging.rs:654) instead of through the nozzle: the one break of R1’s “through the real pipes”.
Composition over configuration
3worse than Vuecomponent_philosophy.md · Principle 2
DataMachine kept its six slots, so the card composes as on the web. Elsewhere the builder pattern pulled toward flags, typed children and pre-built cells.
- DataMachine: 19 builders, 6 of which take elements. The Vue one has about 18 props and 6 named slots, so they are even.
- PageHeader takes elements in 4 of its 6 builders and ActorHeader in 5 of 6.
- Typed children only: DataMachineGroup::child(m: DataMachine) and DataIndexSection::micro(m: DataMachineMicro) accept their own card and nothing else.
- UxPill::new(text: SharedString) takes a string where Vue has a default slot. So the inbox draws its own severity_pill line for line (inbox.rs:64), and tools.rs has an outcome_tag: three pill styles.
- RankedBars has 8 builders, 4 enums and no element input. Looks are set by flags: DataTable::quiet(), PageHeader::pinned(bool), NavJumpPills::compact(bool), DataMachineMicro::card(bool).
- There are four callback conventions, and OnClick is defined twice (machine.rs:28, nav.rs:24).
View complexity contained
3worse than VueMark’s c140 prompt; component_philosophy.md · modularity (internals are free)
The hard internals live inside the table: measuring, easing, focus. But a listing is not a listing until the page wires sort, filter, columns and the sticky band back in.
- TableMemory (table.rs:305) keeps focus, the cursor row, measured widths, the born-row flash and the well heights inside the table, on use_keyed_state with request_animation_frame.
- SurfacePanel keeps its expanded and hovered state; FileTree its closed folders; DataIndexSection its width probe.
- The page holds sort, the quick filter, visible columns, the picker’s open state and the row order, and builds cells in visible-column order. pages/ledger.rs exists to wire that back (148 lines). The Vue DataTable owns its own sort.
- The sticky band is the page’s job: pages/detail.rs (193 lines) computes the scroll thresholds (scrolled > px(55.)), the scroll-spy, jump() and the switch to pinned and compact.
- The sticky table header only pins if the page registers its scroller with .qp_scroller(&handle) and passes .sticky_top(band.sticky_top()); keyboard scroll-into-view only works with .qp() set.
- Builder overwrite: harness.rs:280 calls .qp(…).sticky_top(…) twice in one chain on the Skills table. The first .qp("mcp.tool") is a copy from the table above, and the second call silently wins.
Low knowledge of context
3same as VueMark’s c140 prompt; component_philosophy.md · the flexibility rule
No component knows the rail or the window. Several know the page: its spacing, its viewport and the theme global.
- No hard-coded 212 px rail or 1120 px window in any component; 1120 appears only in a test (tour.rs:886).
- Every colour read goes through cx.qp(role), 509 times, so a component sits on any surface colour the theme gives it.
- Page spacing baked into components: PageHeader carries mt(19) and a h(40) spacer (nav.rs:145, 149); DataTable carries mb(16). A component placed anywhere but a page brings the page’s margins.
- SurfaceSection picks its columns from window.viewport_size() at 1024 and 1280 (surface.rs:445). The viewport includes the rail. DataIndexSection measures its container, so two sibling components answer “how wide am I” two ways.
- Every component assumes layer::install has set the kit’s Theme global (15 direct cx.theme() reads for the mono font and the dark check). That is acceptable as Tailwind’s config is acceptable, but it is not written down.
Slots
3worse than Vuecomponent_philosophy.md · Principle 2 (“slots for flexible content”)
GPUI has all three kinds of slot. We use one of them.
- Named slots: 24 Option<AnyElement> seats set by builders: title_el, toolbar, empty, footer, details. All six of DataMachine’s Vue slots have one.
- Default slot: GPUI’s ParentElement trait (.child(), .children()) is the equivalent, and the layer implements it zero times.
- Scoped slots: the Vue DataTable’s per-column slot (<slot :name="c.key" :row :value>) became pre-built cells, Row::new(key, cells: Vec<AnyElement>), zipped with the columns (table.rs:583). A count mismatch truncates without a word.
- #details="{ row, close }" lost its close handle; #footer="{ total, shown }" lost its scope; SurfacePanel’s scoped #row and #caption became fixed fields and a string.
Brand tokens
2worse than Vuedesign_tokens.md; c132 process.md · The token bridge; qnative CLAUDE.md · Brand
Colour travels well and matches today. Type, radius and spacing do not travel at all. Nothing notices when either drifts.
- Every value matched on 2026-11-12: 21 of 21 roles, 29 palette steps, 27 type steps and 14 radii, layer against tokens.rs and qpoint.json. Nobody has hand-edited the generated files (one commit, 12cc30a).
- Roles are dark-correct by construction: cx.qp(role) picks light or dark from the kit’s mode (theme.rs:105).
- devmode, the newest crate (c138), takes all 22 of its font sizes from text::T* and its radii from radius::R*. The pattern works when it is used.
- Type is set by literals, never by tokens: 433 text_size(px(…)) calls and 156 line_height(px(…)) in the components and pages; the only text::T* reads outside devmode are the doc pages. Off-scale values: px(10.) ×4, px(12.8), line heights of 22.75, 24.375, 47.5.
- Radius: 52 rounded(px(…)) literals and no radius::R* reads. Spacing: about 658 padding, margin and gap literals, and tokens.rs has no spacing tokens.
- The bridge is run by hand: no script in package.json, no CI step, no stamp check. The @8b15751 stamped in tokens.rs and qpoint.json is never verified. A colour change is four manual steps across three repos, plus the literal sites; on the web it is two.
- Colour bypasses: 14 hex literals in 7 files, six of them off the palette (UxPill’s warn 0xFB7185, the tour’s 0x0099FF, 0x64748B, 0xB45309); 14 raw palette constants that do not theme; 51 .opacity() calls where the helper qp_alpha is used 3 times. Two likely dark-mode bugs: the empty box’s 0xE8E8E8 (table.rs:734) and GRAPE_500 on hover in account.rs:119.
- Two modes are hard-coded: Role { light, dark } and qp()’s single is_dark() test. A third theme (high contrast, a customer brand) means a new column in semantic.mjs, a third theme from the bridge, Role chosen by name, and about 40 literal sites fixed.
Two surfaces
2not comparableMark’s c140 prompt; the Surface glossary entry
Not drift hell yet: the surfaces are two days apart and the data contracts are held by machines. But everything a person sees is held by hand and by eye, and four drifts happened in the first three days.
- Generated, not copied: the 2,953-line api.rs from qdash’s TypeScript, the worlds, the tokens and the theme, the stream vectors.
- scripts/live-vectors.sh runs qdash’s own TypeScript over a corpus and the Rust tests compare against its output: the strongest parity mechanism in the stack.
- The worlds pass a contract test in CI and are byte-identical in both repos today. The native skin’s mocks compose the real Vue layer, so they follow a layer change for free.
- Porting is fast. The six Inventory listings after the layer took 11 to 23 minutes each (agent wall-clock, 2026-11-10).
- One change lands in many places: a pill tone in 9–10 places across three repos plus recaptures; a new Tools column in 9–11 places plus a type regeneration and a world recut; a copy change in 2–4.
- The scorecard (36 rows) is the only record of component parity. It is edited by hand, nothing reads it, and it was stale within a day: it still says the sticky header is not pinned and the expand row has no keyboard, both built by D19.
- Drift in three days: the native rail “grew its own look” (c7; the rail now exists four times, the fix is on an unmerged branch); the bridge fixture gained it_contact in qnative but not in design, so running sync-bridge.sh now would delete it; the Tools column widths differ (web 260/90/160…, native 200/84/150…) with no record; the layer’s uncommitted JumpPills and PageTitle restyles will land with nothing to flag the Rust twins.
- Logic held three times by hand: fmtN, fmtC, ago and shortDate in qdash, in format.rs and in design’s useNativeMock.ts; the rollups ported from qdash to Rust, then from Rust back to TypeScript for the lab (native-inventory.ts); the request key in Rust, Python and TypeScript.
- Fixes travel one way. The native port fixed web bugs (rows closing on sort, the Models initial sort, the “all-time” subtitle) and none was filed back to the web.
What our docs add
Functional, not decorative; colour is semantic
3same as Vuecomponent_philosophy.md · Principle 1, Color is semantic
Variants, not colour arguments, as on the web. But the variant sets are too small, so pages paint their own.
- UxPill takes a Tone (Grape, Leaf, Grey, Warn) and a PillSize, never a colour, which mirrors the web exactly.
- Four tones are not enough for a message’s severity, so inbox.rs:64 draws severity_pill with its own colours, and tools.rs has outcome_tag.
- Off-palette colours with no role: the tour’s cyan 0x0099FF (tour/view.rs:35), the listing bar’s slate 0x64748B, the shell fence’s 0xB45309.
Native to the medium
3same as Vuecomponent_philosophy.md · Principle 3 (Tailwind-native); qnative R8; c131 D18
The layer reads as GPUI, with builders, RenderOnce and keyed state. In places it fights the framework instead of using it.
- D18 is honoured: the kit draws the shell (title bar, sidebar, scrolling, spinner, Lucide, the segmented tabs); our vocabulary is drawn because the kit has none of it.
- State lives where GPUI wants it: use_keyed_state for the table, request_animation_frame for the easing.
- Five private traits (Clickable, WhenOn, FirstRow, SwatchFill, NoteExt) reimplement GPUI’s .when(), which the crate never calls.
- ease_in_out_quint is defined twice (table.rs:119, tour.rs:809); kebab twice (machine.rs:310, app/pages/detail.rs:35).
- The pages use the hand-drawn tab_group (editor.rs:251); the kit’s TabBar, which the scorecard lists as UxTabGroup’s build, appears only in the doc pages.
Progressive enhancement; base and presets
3same as Vuecomponent_philosophy.md · Principle 4
The bases are simple where the web’s are. The missing pieces are small primitives, not big presets.
- Behaviour arrives through callbacks and builders the consumer adds, never through a dependency the layer forces (the layer’s one dependency is the kit).
- DataTable carries tour registration and AccessKit inside its base. That is right for accessibility, but it makes the base the heaviest component in the crate.
- The pages keep re-drawing primitives the layer lacks: a lede (copied into seven detail pages), a primary text link (three times), a not-found state (per detail page), a section heading.
- No curated presets on the QGUIkit side yet; the web has qmap’s and DataMachine’s.
Accessibility by default
2worse than Vuecomponent_philosophy.md · Principle 5
The table is accessible. Nothing else is. The web gets roles and tab order from the DOM for free; GPUI gives nothing for free, so every component must ask.
- DataTable: track_focus, ↑ ↓ Home End Enter Space Esc (table.rs:757), AccessKit roles (Table, Row, Cell, ColumnHeader, Button), aria_row_index, aria_selected, aria_expanded.
- No focus handle and no role on NavJumpPills, the crumbs, tab_group, FileTree (the scorecard admits it), DataListingBar, stroke_button or a linked DataMachine.
- The column picker’s popover has no Escape and no click-outside close.
- No focus ring style anywhere in the layer.
States are explicit
2worse than Vuecomponent_philosophy.md · Principle 6; qnative R5 (honest states)
Hover is drawn; pressed, focus and disabled are not. The honest data states have a shared shape, but each page draws them its own way and none is tested.
- A shared Load<T> (Loading, Ready, Failed) on every page; state_empty draws the skeleton, filter_no_matches the filtered-to-nothing state; the minimal and fresh worlds carry the thin cases.
- 17 .hover( styles in the core and no pressed style, no focus style and no tooltip. Disabled exists only on the kit’s Checkbox in the column picker. stroke_button has hover only, where UxButton has disabled, loading and an icon slot.
- No story or test draws any page’s Failed or Loading state, although nozzle::scenario::Condition (Slow, Unavailable, ShapeMismatch) exists and Live already uses it.
- The inbox reads its store with unwrap_or_default() (inbox.rs:53–55), so an SQLite error is drawn as “Nothing yet”: an error shown as empty.
- The not-found state and the failed() view are written per page.
Replicate first, optimize later
4same as Vuecomponent_philosophy.md · Principle 7; qnative CLAUDE.md (a port, not a redesign)
Applied with real discipline: every page measured on the web before it was drawn, every oddity decided rather than copied. The record of it was nearly lost.
- A spec per page, measured on qdash :3030 at 1120 × 720 before the port: ten files, about 400 KB, cited 30 times in code and cycle docs.
- The fleet → personal translation had six written rules (c131 phase2-plan.md), and each web oddity was “decided rather than copied”: five lists in c2’s CYCLE.md, and a module comment in 18 of 25 page files.
- 28 of 30 drawn components at fidelity 3 (indistinguishable from the web at a glance). Where the native page now differs on purpose (Activity’s lanes by harness), Mark ruled it.
- The ten specs were never committed. They lived only in a session scratchpad under /private/tmp, which macOS clears on reboot. c140 copied them into its folder on 2026-11-12 (rescued-specs/), uncommitted.
- Small undecided differences slipped through: the Tools column widths differ and nothing records why.
The demo is the test
3same as Vuecomponent_philosophy.md · Principle 8; qnative R6
Every page state is drawn, light and dark, and the newest pages are real tests. The Inventory section, the inbox and every error state are pictures only.
- About 105 stories and 28 layer doc pages, each light and dark; the native capture mode files them per world into the design atlas.
- 34 #[gpui::test] in the stories crate. The 16 written for c5’s pages assert the text each region reports (.qp_id().text()), the footer and the state: the demo as the test, literally.
- CI runs cargo test --workspace on three systems; the worlds pass a strict contract test against the types.
- The Inventory section’s 35 stories and the inbox’s 10 are captures with no assertion.
- gpui_kit::test, which R6 names, is used nowhere; the layer crate has 9 unit tests for 9,776 lines.
- DataHistoryGraph, added in c5, has no doc page and no scorecard row; the scorecard stopped at c2.
Constrained size and consistent motion
3same as Vuecomponent_philosophy.md · Size is constrained, Transitions are consistent
Size steps are named enums, as on the web. Motion has no tokens on either surface, and the written rule is out of date on both.
- PillSize Sm, Md and Lg (ux.rs:69–72), MarkerSize in the tour: named steps, never a free size argument.
- Type sizes are on the scale mostly by care: about half of the size and line-height pairs match Tailwind’s pairs, and some are off the scale (px 10, 12.8).
- Motion constants per component: the table’s BORN_FADE and GROW_MS of 300 ms, an 8 ms glide tick in the tour.
- The written rule is 200 ms for interaction and 500 ms for content. The web layer itself uses duration-300 13 times, duration-200 7 times and duration-500 6 times. The rule is out of date on both surfaces.
The unified pantry
3same as Vuecomponent_philosophy.md · Component scope
The vocabulary is in the layer. Small pantry pieces still sit in the pages, and a few app-wired pieces sit in the layer.
- Every component in the layer passes the boundary test: it renders without side effects beyond its own callbacks (the clock and the tour registry aside, row 2).
- Pantry pieces left in the pages: section() and failed() (pages/mod.rs), absence_box, severity_pill, outcome_tag, Sensor’s fact list and log frame, the inbox’s banner and empty state.
- Helpers copied by hand: pct ×4, short ×4, plural ×2, a bytes in skill.rs beside the layer’s own format::bytes; short_date lives in harnesses.rs and seven other pages import it from there.
- The other way round: the harness registry and the git-remote parser are domain data inside the layer.
Documentation architecture
2worse than Vuecomponent_philosophy.md · Documentation architecture
The native docs are good and in the wrong place: inside the library, and inside developer mode. The public docs do not know the QGUIkit surface exists.
- 27 doc pages in Dev › Components (8 of the kit, 16 of ours, the scorecard, 2 of the skin). Each shows its own scorecard row and calls itself “the counterpart of the design site’s /components/<name>” (demos/mod.rs:5).
- They are cut from the lean build that ships (check-lean.sh), so a customer pays nothing for them.
- pub mod demos (lib.rs:15) compiles 2,364 lines of doc pages into the component library itself, used only by devmode and stories. Our rule is that the library ships components and the docs live in a separate consumer.
- The design site’s registry (193 entries, component-registry.ts) has no surface field, and the registry and /components never mention GPUI or qnative.
- Nowhere can a reader see “this component exists on both surfaces, and differs here”. The only trace of the QGUIkit layer on the public site is the PNGs in qnative c2’s story.
- c135 (Component Cleanup) puts the native components out of scope (“new components for the native app (the kit’s)”). It was planned before D18 made the layer a drawn Rust twin, so 36 counterparts are outside every cleanup.
Naming
2worse than Vuecomponent_philosophy.md · Naming
The crate promises the web’s names and keeps about half of them.
- DataMachine, DataMachineGroup, DataMachineMicro, DataTable, DataListingBar, DataIndexSection and UxPill carry their web names.
- lib.rs:5 says the layer keeps “the same names (DataMachine, DataRankedBars, UxPill…)”, but DataRankedBars is RankedBars; likewise SegmentBar, DonutChart, Spark, HistoryGraph, MetricValue, Trend, SurfacePanel, SurfaceSection, EditorFrame, FileTree, CodeBlock.
- UxTabGroup became tab_group() and UxButton’s stroke variant became stroke_button(). DataPercentBar and UxShowMore are inlined in SurfacePanel and have no name at all.
- The scorecard maps web names to doc pages, but has no column for the Rust symbol.
The budget
4better than Vuenew for the QGUIkit surface: qnative R7, c131 D22, c138’s footprint page
A principle the web never wrote down, and the QGUIkit surface keeps it best of all: measured per release, recorded, and drawn.
- Every phase from the spike to c5 measured with strobe on one recipe and recorded with its recordings; the working targets ruled (c131 D22); the developer-mode gate measured in four states; every reading drawn at /cycles/design/c138/footprint.
- scripts/footprint/measure.sh automates the recipe and checks validity.
- Readings owed: c4 and c5 were read “on a Mac in use”, and c5’s review-2 says the idle re-read is still owed. measure.sh waits for 60 s of idle, so busy sessions skip it.
- Nothing collects measure.sh’s summary.json into a table; the footprint page is transcribed by hand.
The five questions, over the whole
Our methodology's closing read: useful, clear, precise, necessary, beautiful. Passing all five means ship it.
- Is it useful?yesYes. The pages work on four worlds, light and dark, inside the budget, at fidelity 3 for 28 of 30 drawn components.
- Is it clear?partlyPartly. There are four ways to write a component, half the web names changed, and the slot equivalents are not named anywhere.
- Is it precise?partlyColour is: 509 role reads, 0 mismatches. Type, radius and spacing are not: literals everywhere but devmode.
- Is it necessary?partlyThe second surface is (c130, c131 D18). Some of what came with it is not: three hand copies of the formatters, a Rust port re-ported to TypeScript, the request key in three languages.
- Is it beautiful?yesThe captures say so. The wipes against the web agree at a glance.
What to do
Fifteen course corrections, from the rows above, each with Mark's ruling from review stop 1 (2026-11-12, D1–D14). The code work goes to one qnative cycle after the merge (D14, the hand-on); the dual-surface docs to c141. The cost of two surfaces has its own page, and so do the skills and tools.
Small, after the merge (D13)
- 1Keep the ten port specssmallrows 14
They were the record of how every Inventory page was measured, and lived in a scratchpad under /private/tmp.
Done. Committed in qnative c8 (bc60635) at cycles/c2.expressive-range/specs/, with a README.
Ruled. D13: after the merge, into qnative cycles/c2.expressive-range/specs/, by the qnative cycle (D14).
- 2Sync the bridge fixture before anyone runs sync-bridge.shsmallrows 8
design’s copy lacks qnative c4’s it_contact; the script as written would delete it.
Done. design’s pilot.json and types.ts carry it_contact; the bridge README names design as the source (qnative c8, bc60635).
Ruled. D13: after the merge.
- 3Fix six small flawssmallrows 4, 7, 13, 15
harness.rs:280’s copied .qp("mcp.tool"); the empty box’s 0xE8E8E8 and account.rs’s GRAPE_500 in dark mode; the inbox’s unwrap_or_default (an error drawn as empty); cycle ids in the Sensor page’s copy; the scorecard’s two stale rows.
Done. All six, in qnative c8 (be4545f); 175 tests pass.
Ruled. D13: after the merge, one commit in the qnative cycle (D14).
- 4A token drift check in qnative’s CIsmallrows 7
The bridge is run by hand and its stamp is never verified. A --check mode that fails on a diff turns “matched today” into “cannot drift”.
Done. The bridge moved to qnative with --check and a CI job, tokens, that checks the stamped layer commit and warns on the layer’s main (qnative c8, 8b1af18); design keeps a wrapper.
Ruled. D13: after the merge.
Ruled at review stop 1
- 5The parity register and dual-surface docsmediumrows 8, 18
The Surface entry asks the public docs to track both expressions. The scorecard as shared data, a Surfaces panel per component page, the divergences as flavor or gap.
Ruled. D3, D10: a new small cycle, c141 Surface Docs, opened at Stage 1; the manifest field splits with it.
- 6Type, radius and motion tokens everywhere, and a lintmediumrows 7, 16
433 literal type sizes, 52 literal radii, no motion tokens on either surface. devmode already shows the pattern works.
Ruled. D5: sweep, motion tokens on both surfaces, the motion rule rewritten, and a lint.
- 7The three slot equivalentsmediumrows 3, 6
ParentElement on the containers, children on UxPill, a cell closure on DataTable. It touches every listing page.
Ruled. D6: name the three in the docs now; change the APIs when a component is next touched. DataTable’s silent truncation stays until then.
- 8The keyboard and focus passmediumrows 12, 13
Keyboard users reach the table and nothing else. Six interactive pieces, a focus ring, a test each.
Ruled. D7: after V1 ships. V1 goes out with keyboard access in the table only.
- 9The names: rename to the web’s, or keep and mapsmallrows 19
lib.rs promises the web names and keeps half. Rename with aliases, or add a rust column and accept two vocabularies.
Ruled. D8: rename to the web’s names, with aliases for a release.
- 10c135’s scopesmallrows 18
The cleanup leaves the 36 Rust twins out; it was planned before D18. Widen it, or give the twins their own pass.
Ruled. D4: both surfaces, each group cleaned once on Vue and QGUIkit together.
- 11The second surface’s namesmall
The prompt says QPUIkit and QGUIkit. GPUI Kit is the open-source kit; our components are the layer crate. A ruling for the glossary.
Ruled. D1, D2: QGUIkit, now; the crate layer becomes qguikit.
- 12Split the capture manifest’s surface fieldsmallrows 8
It mixes the medium (app, native) with the kind of page (mock, docs, preview).
Ruled. D10: split into surface (vue · qguikit) and kind, with c141’s register.
- 13Move what does not belong in the component cratemediumrows 1, 17
The tour engine, the harness registry and the git parser out; the seven pantry pieces from the pages in; the demos to a docs crate.
Ruled. D9: all of it, in the qnative cycle (D14).
Leave, and write down
- 14Builders as the way GPUI composessmallrows 3
A flavor of the medium. Record each builder against its Vue prop on the component page instead of fighting it.
- 15Open rows held by the pagesmallrows 4
Better than the web, which closes rows on sort. File the web bug instead.
Ruled. D12: the web bug goes back as a qdash todo (qdash-todos.md).
The records
- c140 Surface Evaluation →the cycle record
- The plan →the rubric and the method, written before the evidence
- The findings →the scores in one table, and what is open
- Review stop 1 →the calls for Mark
- qnative c2, review stop 3 →the port, the scorecard and the budget
- c132 process.md →the two-media loop and the token bridge
- Native App Footprint →every strobe reading, c1 to c5