← Design · c140

Two Surfaces, Scored

c1402026-11-12 · the QGUIkit surface against twenty principles

Our 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.

Ruled · 2026-11-12

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.

Principles scored
eight named in the prompt, twelve found in our docs
20
At 4 of 5
clean data boundaries, replicate first, the budget
3
At 3 of 5
mostly holds, with named exceptions
11
At 2 of 5
brand tokens, two surfaces, accessibility by default, states are explicit, documentation architecture, naming
6

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

#PrincipleScoreAgainst VueIn a line
1Modularity3sameThe units are real and the crate boundary is clean, but four of the biggest files are several components and an engine each.
2Clean data boundaries4betterThe strongest row. The layer cannot see a source by construction; the pages mostly go page → repository → nozzle, with four named leaks.
3Composition over configuration3worseDataMachine 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.
4View complexity contained3worseThe 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.
5Low knowledge of context3sameNo component knows the rail or the window. Several know the page: its spacing, its viewport and the theme global.
6Slots3worseGPUI has all three kinds of slot. We use one of them.
7Brand tokens2worseColour travels well and matches today. Type, radius and spacing do not travel at all. Nothing notices when either drifts.
8Two surfaces2—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

#PrincipleScoreAgainst VueIn a line
9Functional, not decorative; colour is semantic3sameVariants, not colour arguments, as on the web. But the variant sets are too small, so pages paint their own.
10Native to the medium3sameThe layer reads as GPUI, with builders, RenderOnce and keyed state. In places it fights the framework instead of using it.
11Progressive enhancement; base and presets3sameThe bases are simple where the web’s are. The missing pieces are small primitives, not big presets.
12Accessibility by default2worseThe 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.
13States are explicit2worseHover 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.
14Replicate first, optimize later4sameApplied 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.
15The demo is the test3sameEvery 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.
16Constrained size and consistent motion3sameSize 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.
17The unified pantry3sameThe vocabulary is in the layer. Small pantry pieces still sit in the pages, and a few app-wired pieces sit in the layer.
18Documentation architecture2worseThe 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.
19Naming2worseThe crate promises the web’s names and keeps about half of them.
20The budget4betterA 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

1

Modularity

3same as Vue

component_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.

What holds
  • 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.
What does not
  • 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.
Raise by oneSplit TableEl into a measured grid, a keyboard-and-roles behaviour and the expand well; move the tour engine and the harness registry out of the component crate (a guide module, a data module). One style for new components: a RenderOnce struct.
RuledD9: all of it, in the qnative cycle (D14).
2

Clean data boundaries

4better than Vue

qnative 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 layer5The pages3
What holds
  • 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.
What does not
  • 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”.
Raise by onePass the clock in (a now: argument, or a Clock global the stories set); move SensorRead and the inbox’s source branches into repositories; route the drill through the nozzle.
3

Composition over configuration

3worse than Vue

component_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.

What holds
  • 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.
What does not
  • 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).
Raise by oneParentElement on the containers (DataMachineGroup, SurfaceSection, DataIndexSection); UxPill takes children with the string as a shorthand; one OnClick type exported once.
RuledD6: name the three in the docs now; change the APIs when a component is next touched. DataTable’s silent truncation stays until then.
flavorBuilders are how GPUI composes, and typed children catch at compile time what Vue catches at runtime. Write the builder for every Vue prop and the seat for every Vue slot on the component’s page.
4

View complexity contained

3worse than Vue

Mark’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.

What holds
  • 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.
What does not
  • 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.
Raise by oneA ListingState in the layer (keyed state the DataListingBar and the DataTable share, the Rust twin of a composable) and a StickyBand that owns its thresholds; make the scroller registration a default.
RuledD13: after the merge, one commit in the qnative cycle (D14).
RuledD12: the web bug goes back as a qdash todo (qdash-todos.md).
flavorThe expand row’s open keys held by the page are better than the web’s per-row refs: a sort or a refresh no longer closes rows. Say so on the expand row’s page.
5

Low knowledge of context

3same as Vue

Mark’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.

What holds
  • 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.
What does not
  • 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.
Raise by oneMargins move to the caller (a page_header_gap the page applies); SurfaceSection measures its container as DataIndexSection does.
6

Slots

3worse than Vue

component_philosophy.md · Principle 2 (“slots for flexible content”)

GPUI has all three kinds of slot. We use one of them.

Named slots4Default slot1Scoped slots1
What holds
  • Named slots: 24 Option<AnyElement> seats set by builders: title_el, toolbar, empty, footer, details. All six of DataMachine’s Vue slots have one.
What does not
  • 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.
Raise by oneName the three equivalents in the layer’s docs and use them: ParentElement for the default slot; Option<AnyElement> for named slots (as now); a render closure, Box<dyn Fn(&Row, &mut Window, &mut App) -> AnyElement>, for a scoped slot, starting with DataTable’s cells.
RuledD6: name the three in the docs now; change the APIs when a component is next touched. DataTable’s silent truncation stays until then.
gapUntil the table takes a cell closure, the QGUIkit DataTable has no per-column slot: the page builds every cell, in column order.
7

Brand tokens

2worse than Vue

design_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.

Colour4Type1Radius and spacing1Light and dark4A third theme2The pipeline2
What holds
  • 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.
What does not
  • 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.
Raise by oneOne drift check in qnative’s CI (the bridge run in --check mode against the layer at a pinned tag, failing on a diff); a lint that refuses text_size(px(literal)) and hex outside tokens.rs; the type and radius tokens used everywhere devmode already uses them.
RuledD13: after the merge, one commit in the qnative cycle (D14).
RuledD13: after the merge.
RuledD5: sweep, motion tokens on both surfaces, the motion rule rewritten, and a lint.
8

Two surfaces

2not comparable

Mark’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.

Data contracts (types, worlds, stream)4Components and pages2Divergences written down1
What holds
  • 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).
What does not
  • 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.
Raise by oneOne parity register both surfaces read: the scorecard as data (JSON beside the registry), with the Rust symbol, fidelity, divergences and the web file’s SHA when the row was checked; a check that flags a row when its Vue file changes after that SHA. Extend live-vectors.sh to the formatters and rollups so the three copies are held by tests.
RuledD13: after the merge.
RuledD3, D10: a new small cycle, c141 Surface Docs, opened at Stage 1; the manifest field splits with it.
RuledD10: split into surface (vue · qguikit) and kind, with c141’s register.
gapNo surface tells its operators where it differs from the other. That is the Surface glossary entry’s ask, and the course correction above is its first half.

What our docs add

9

Functional, not decorative; colour is semantic

3same as Vue

component_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.

What holds
  • UxPill takes a Tone (Grape, Leaf, Grey, Warn) and a PillSize, never a colour, which mirrors the web exactly.
What does not
  • 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.
Raise by oneAdd the severity tones (info, attention, required) to UxPill on both surfaces, from the severity roles tokens.rs already carries; give the tour’s cyan a role.
10

Native to the medium

3same as Vue

component_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.

What holds
  • 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.
What does not
  • 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.
Raise by oneReplace the five traits with .when() and .when_some(); one motion module with the one easing.
11

Progressive enhancement; base and presets

3same as Vue

component_philosophy.md · Principle 4

The bases are simple where the web’s are. The missing pieces are small primitives, not big presets.

What holds
  • Behaviour arrives through callbacks and builders the consumer adds, never through a dependency the layer forces (the layer’s one dependency is the kit).
What does not
  • 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.
Raise by oneLift the lede, the text link, the not-found state and the section heading into the layer as four small components.
12

Accessibility by default

2worse than Vue

component_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.

What holds
  • 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.
What does not
  • 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.
Raise by oneA focus-and-role pass over the six interactive pieces, with a focus ring from the primary role; one headless test per piece that tabs to it and presses Enter.
RuledD7: after V1 ships. V1 goes out with keyboard access in the table only.
gapUntil the pass lands, keyboard users reach the table on the QGUIkit surface and nothing else.
13

States are explicit

2worse than Vue

component_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.

Interactive states2Honest data states3
What holds
  • 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.
What does not
  • 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.
Raise by oneA page-state matrix: every route × Healthy, Slow, Unavailable, ShapeMismatch as stories, on the scenario conditions that already exist; pressed and disabled on stroke_button.
RuledD13: after the merge, one commit in the qnative cycle (D14).
RuledD7: after V1 ships. V1 goes out with keyboard access in the table only.
14

Replicate first, optimize later

4same as Vue

component_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.

What holds
  • 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.
What does not
  • 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.
Raise by oneCommit the specs where the port they describe lives (qnative cycles/c2.expressive-range/specs/), and file the web bugs the port fixed back to the web.
RuledD13: after the merge, into qnative cycles/c2.expressive-range/specs/, by the qnative cycle (D14).
flavorEvery decided oddity is a flavor: list them on the component’s and the page’s documentation, not only in a cycle record.
15

The demo is the test

3same as Vue

component_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.

What holds
  • 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.
What does not
  • 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.
Raise by oneThe region-text assertion c5 uses, added to the Inventory and inbox stories; a doc page and a scorecard row as the condition of a new layer component.
RuledD13: after the merge, one commit in the qnative cycle (D14).
16

Constrained size and consistent motion

3same as Vue

component_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.

What holds
  • PillSize Sm, Md and Lg (ux.rs:69–72), MarkerSize in the tour: named steps, never a free size argument.
What does not
  • 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.
Raise by oneA motion token set (fast, base, slow, and the one easing) in tokens.json, emitted to both surfaces by the bridge; the rule in component_philosophy.md rewritten to what the web actually does.
RuledD5: sweep, motion tokens on both surfaces, the motion rule rewritten, and a lint.
17

The unified pantry

3same as Vue

component_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.

What holds
  • 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).
What does not
  • 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.
Raise by oneOne sweep: lift the seven page pieces into the layer and the helpers into layer::data::format, and move the registry out.
RuledD9: all of it, in the qnative cycle (D14).
18

Documentation architecture

2worse than Vue

component_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.

What holds
  • 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.
What does not
  • 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.
Raise by oneA surfaces field in component-registry.ts and a QGUIkit panel on each component page (the native capture, the Rust symbol, fidelity, the divergences as flavor or gap), read from the parity register; the demos moved out of the library into a docs crate that devmode and stories depend on.
RuledD3, D10: a new small cycle, c141 Surface Docs, opened at Stage 1; the manifest field splits with it.
RuledD4: both surfaces, each group cleaned once on Vue and QGUIkit together.
flavorDocs that run inside the app they document (Dev › Components) are a good idea the web does not have. Keep them, and mirror them in public.
19

Naming

2worse than Vue

component_philosophy.md · Naming

The crate promises the web’s names and keeps about half of them.

What holds
  • DataMachine, DataMachineGroup, DataMachineMicro, DataTable, DataListingBar, DataIndexSection and UxPill carry their web names.
What does not
  • 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.
Raise by oneRename to the web names, the Data and Ux prefixes included (a one-time sweep with aliases), and add a rust column to the scorecard so the map is a lookup, not a search.
RuledD8: rename to the web’s names, with aliases for a release.
20

The budget

4better than Vue

new 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.

What holds
  • 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.
What does not
  • 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.
Raise by oneA queue of owed readings that runs measure.sh when the machine is idle, and writes the footprint page’s data file from the summaries.

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

Qpoint Brand Style Guide