21 KiB
| title | type | audience | authority | status | source |
|---|---|---|---|---|---|
| Component audit guide | guide | human + agent | binding — the pre-flight checklist before touching any component | current | migrated from web/routes/uix/lib/COMPONENT_AUDIT_GUIDE.md (2026-07-02, docs-book F7.5) |
Component audit guide
STOP. READ THIS BEFORE TOUCHING ANY COMPONENT.
This file is a binding pre-flight checklist. Every time you create, port, or modify a UIX component you MUST walk this guide front to back. Skipping steps produced the broken Layout Batch 1 (commit
9ec2a57a) that had to be redone — a full day lost. Don't repeat the mistake.
0. The inviolable rule
Before touching src/uix/{morfo,soma,sema,eidos}/components/{name} or
web/routes/uix/components/{name}/+page.svelte:
- Read this entire file. Yes, every time.
- Read
demo-authoring.md— the demo template is locked. - Read
src/uix/eidos/components/README.md— the eidos contract. - Run the pre-flight audit in §3 against at least 4 reference libraries. Tabular output, not prose.
- Get the user's sign-off on scope before writing any code.
Agents you delegate to MUST be given this file in their brief. Their brief must reference §3 (audit template) and §6 (anti-patterns) explicitly.
The minimum package for a component-building brief is this exact file list (titles are not enough — a package missing any of these produced a non-conforming build in the Knob exercise, STUMBLES.md #8):
docs/building-a-component.md— the routedocs/guides/component-guide.md— build steps + rules A1–A37docs/guides/completion-checklist.md— acceptancedocs/guides/demo-authoring.md— the LOCKED demo template (D-1.x is error-level)docs/guides/component-audit.md— this filedocs/CANON.md— the ruling semantic vocabularydocs/architecture/morfo.md+sema.md+soma.md— the layer contractsdocs/canon/vocabularies.md— the generated closed sets (archetypes, families+holds, verbs, intents, haptic kinds, scales, sizes, variants) you draw fromsrc/uix/eidos/components/README.md— the eidos pattern
1. Reference libraries — who to consult and why
Every component must be audited against the relevant references below. Pick at least 3, including Radix Themes when it's a visual primitive and Ark UI / React Aria when it's a headless behaviour.
| Library | URL | Use for |
|---|---|---|
| Radix Themes | https://radix-ui.com/themes | Visual primitives (Box / Flex / Grid / Container / Text). Canonical "Box-style split" architecture. |
| Radix Primitives | https://radix-ui.com/primitives | Headless behaviour (Dialog / Popover / Toast / DropdownMenu). ARIA reference. |
| Chakra UI | https://chakra-ui.com | Style-props heavy. Useful to spot props users expect on Box that we deliberately moved to Flex/Grid. |
| Mantine | https://mantine.dev | Strong typography + layout primitives (Stack, Group, Container, Text, Title). |
| MUI | https://mui.com | Reference for sx prop, transitions, theme system. |
| React Aria | https://react-spectrum.adobe.com/react-aria | Industry-strength a11y. Use when keyboard or screen-reader contract is non-trivial. |
| Ark UI | https://ark-ui.com | Headless state machines (combobox, picker, tags-input). Mirror their event names when possible. |
| Bits UI | https://bits-ui.com | Svelte-native parallel to Radix Primitives — useful sanity check on component shape. |
| shadcn/ui | https://ui.shadcn.com | Mostly visual recipes layered on Radix. Useful for opinionated styling defaults. |
| WAI-ARIA APG | https://www.w3.org/WAI/ARIA/apg/patterns/ | The contract. Required reading for any role / aria audit. |
Anti-rule: "I'll just look at how air did it" is NOT a reference audit. Air's port reflects the old architecture and the gaps from the previous era. Audit fresh.
2. Layer responsibilities
Before deciding where a feature lives, recall the 4-layer architecture:
| Layer | Owns | Examples |
|---|---|---|
| morfo | declarative contract: parts, data-attrs, ARIA, keyboard, events | src/uix/morfo/components/{name}.ts |
| soma | headless behaviour: state, effects, runtime providers, focus, ARIA wiring | src/uix/soma/components/{name}/ |
| sema | perceptual side-effects: sound, motion, haptic projections of semantic events | src/uix/sema/components/{name}/ |
| eidos | pure CSS visual recipe + *Provider wrappers that pipe data-* from morfo into the cascade |
src/uix/eidos/components/{name}/ |
The 2-of-3 rule: a morfo extension is only justified if at least
2 of 3 consumer layers (soma / sema / eidos) need it. (feedback_2of3_rule.)
Visual-only primitives (avatar, icon, all layout/typography
primitives) get scope: ['eidos'] in their morfo — no soma, no sema.
Justify the 0-event surface in a header comment on the morfo file.
3. Pre-flight audit template
Before writing or porting a component, fill out this table. Don't proceed without user sign-off.
### {ComponentName} audit — {YYYY-MM-DD}
#### Feature parity matrix
| Feature | Radix Themes | Chakra | Mantine | UIX (port plan) | Decision |
| --- | --- | --- | --- | --- | --- |
| {prop or feature} | {how their X handles it} | {…} | {…} | {what we plan} | implement / defer / drop |
#### Architectural choices
- Layer ownership (morfo / soma / sema / eidos): {…}
- Container vs item split: {if applicable, where each prop lives}
- Composition vs single-component: {does this share root with another primitive?}
- Sizes covered: {xs … xxl subset — declared in the recipe + types; parity rule in demo-authoring.md §6}
- Variants: {surface / outline / ghost / soft …}
- Color intent palette: {full / narrowed — justify}
#### Reference comparison summary
| Library | Closest equivalent | Difference vs UIX | Why we differ |
| --- | --- | --- | --- |
| {…} | {…} | {…} | {…} |
#### Decision log
- {gap}: {implement / defer / drop} — {reason}
Sign-off line at the bottom: User signed scope on YYYY-MM-DD.
4. Architectural rules (project-wide)
These are non-negotiable. Violations trigger a rewrite.
4.1 Radix-style item / container split
For layout primitives:
- Container props (
alignItems,justifyContent,flexDirection,flexWrap,templateColumns,gap) live on<Flex>/<Grid>containers. - Item props (
flex,grow,shrink,basis,order,alignSelf,justifySelf,placeSelf,gridColumn,gridRow,gridArea) live on<Box>.
Chakra puts everything on <Box>. We don't. Document the split in the
demo's lede + cross-references between primitives.
4.2 Composition over visibility props
Optional parts (Footer, Clear, Close, Header sub-items) must not
be controlled by *Button boolean props on the root. Visibility is
owned by composition — include the part to render it, omit to hide.
(Doctrine: eidos/README.md picker conventions, presence = visibility.)
4.3 Chip parity
Every chip-group control in a demo enumerates the full type union.
Truncated arrays are a contract bug. If the component narrows the
union deliberately (Extract<…>), the narrowing must be justified in
the component README. (demo-authoring.md §6.)
4.4 Size category cheatsheet
The shared Size scale is xxs · xs · sm · md · lg · xl · xxl · full.
Each component declares which steps it maps in the recipe + types —
that declaration IS the source. Demos reflect that narrowing 1:1
(parity rule: demo-authoring.md §6).
4.5 Per-event intent
Each morfo event's intent reflects its OWN evaluative load, not the
component's overall context. close-save = fulfill, close-cancel = absent, open = fromProp. (feedback_per_event_intent_intrinsic.)
4.6 No re-export façades
feedback_no_reexport_facades + feedback_no_reexport_shims. If
$libs/days exposes a date util, the consumer imports $libs/days
directly. Do not add re-exports across layers.
4.7 Persistent label registries
When a component's contract requires displaying a label for a value
that may be unmounted (combobox after popover closes, multi-select
chips), the soma provider's value→label map MUST NOT be cleaned up on
item unmount — keep the entry around. Re-registration is idempotent
via SvelteMap.set. (Combobox SelectedTags bug from commit
30e9517a.)
4.8 Floating layer pattern
Anchored popovers default to intrinsic width + align="start" +
sideOffset=6. Do NOT set matchAnchorWidth unless the component
contract explicitly requires it. The visual gap from forcing
anchor-width is worse than the marginal alignment win. (Decision from
2026-05-22.)
4.9 Combobox-style keyboard
<input> keeps DOM focus; highlight moves virtually via
aria-activedescendant. Arrow keys cycle, Enter commits, Escape
closes, Tab moves out of the component (does NOT enter the listbox).
Pattern: WAI-ARIA combobox. (feedback_combobox_keyboard, SearchField
demo 2026-05-22.)
4.10 Chakra-style multi-select chips
When a combobox / multi-select renders selected values as chips, the
chips go ABOVE the input control (Chakra v3 multi pattern). Reason:
the popover opens downward and would cover chips that sit below the
input. Do NOT inline chips inside the control — MUI Autocomplete's
inline pattern fights wrap behavior. (Decision from 20709caf.)
4.11 No literal typography in recipes (R-2.7)
Component CSS recipes consume tokens, never literal values, for
font-size / font-weight / line-height / letter-spacing /
font-family. The audit script flags violations with warn
severity.
Why: the system vertebrates via the foundation token chain:
typography.ts styles → --style-{name}-* → consumed directly by recipes
(+ --leading-ui, config data)
→ --field-*-* (recipe tokens)
→ component CSS
A literal in a recipe breaks the chain — changing the foundation
no longer propagates. See architecture/eidos.md
§ "Typographic vertebration" for the full architecture (why two anchors,
why recipes don't read --style-{name}-* directly, comparison vs Radix
Themes / Chakra / Mantine / MUI).
Allowed escape valves:
var(...)wrapping the value- numeric identities:
0,0px,0em,0rem,1 - keywords:
inherit,initial,unset - explicit opt-out via trailing
/* literal: <reason> */comment
4.12 Run the contract audit script
The repo ships scripts/component-audit.ts (alias npm run component:audit). It is the canonical verification tool for
contract compliance — it walks every component, parses morfo declarations,
cross-checks against eidos recipes + demos, and emits a markdown report
at tmp/component-audit.md.
npm run component:audit # full run
node --import tsx/esm scripts/component-audit.ts --only NAME # single component
node --import tsx/esm scripts/component-audit.ts --severity error
A component is only "done" when its row in the scoreboard reads PASS with 0 errors. Warnings should be triaged in the audit log (§8). The audit script enforces:
E-2.xmorfo / eidos / demo / README presenceD-7.4demo chip-array parity vs type unionA-3.xkeyboard ↔ events declarative consistencyR-1.xmorfodata-*attrs styled in eidos recipeF-1.xREADME structure (Baseline / Comparativa / Decisiones / Gaps)
Run it before every commit that touches a component. Block the merge on any error.
5. The demo template is locked
Read demo-authoring.md. The full template is
mandatory — the tab set (v2: 9 tabs), the always-on stage + trace, the
harness modules, layer-grouped controls, snippet parity, and the
always-rendered contract tabs are all defined THERE; this guide does not
copy the list (copies drift — the v1 6-tab enumeration that used to live
here went stale against the v2 template).
The canary is button (web/routes/uix/components/button/+page.svelte,
the v2 worked reference). Layout primitives have their own canaries —
box and flex (see §7).
6. Anti-patterns (failed approaches — don't repeat)
These produced rework or rejected commits. Don't propose them again without naming the reason.
| Anti-pattern | Where it failed | Why |
|---|---|---|
| Shallow 100-line demo "marketing page" | Layout Batch 1 v1 (9ec2a57a) |
Violates DEMO_AUTHORING_GUIDE. Reverted + redone. |
matchAnchorWidth on date-picker popover |
2026-05-22 morning | Visual gap when popover content narrower than input. User explicitly abandoned. |
Visibility via *Button boolean props |
DatePicker pre-69, removed |
Inverts composition. Use {#if showX} around the part instead. |
| Inline chips inside Combobox control | Combobox v1–v3 (2026-05-22) | Fights flex-wrap; popover covers chips. Switched to band-above-Control (Chakra). |
onpointerdown + preventDefault for item picks |
SearchField v1 | Eats the synthetic click — onclick never fires. Use plain <button onclick>. |
flex: 1 1 100% on input inside flex-wrap container |
Combobox v2 | Forces wrap to new row even with chips fitting. Use flex: 1 1 4rem and let it share rows. |
| Re-registering child labels and unregistering on unmount | Combobox SelectedTags v1 | Labels disappear when popover closes. Register on mount, never unregister. |
| Auto-rendering everything in a wrapper | Combobox.SelectedTags v1 | Less customizable than explicit composition. Provide a default snippet, allow override. |
inputRef.focus() after picking without guard |
SearchField popover v1 | Re-fires onfocusin → reopens popover. Use justCommitted flag + microtask reset. |
Agent runs git reset mid-session |
Layout Batch 2 v1 worktree | Lost reported work. Brief MUST say "DO NOT git reset, stash, or checkout files." |
| Skipping reference audit before porting | Layout Batch 1 v1 | Missed alignContent, columns/rows shorthand, grow, divider slot. Always audit first. |
7. Canonical canaries
When unsure how to shape a new component, copy and adapt from these:
| Domain | Canary | Path |
|---|---|---|
| Overlay with state | Drawer | web/routes/uix/components/drawer/+page.svelte |
| Form input with composite parts | Search field | web/routes/uix/components/search-field/+page.svelte |
| Visual-only primitive (Box-style) | Box | web/routes/uix/components/box/+page.svelte |
| Visual-only container with own props | Flex | web/routes/uix/components/flex/+page.svelte |
| Multi-state composite (header / body / footer) | Date picker | web/routes/uix/components/date-picker/+page.svelte |
| Passive headless (image / svg) | Avatar | web/routes/uix/components/avatar/+page.svelte |
8. Audit log
Running log of completed reference audits. Add a row when you sign off scope with the user. Use the §3 template; link the commit that closed the work.
| Date | Component | Audited against | Commit | Notes |
|---|---|---|---|---|
| 2026-05-22 | Combobox (multi tags) | radix-themes, chakra, mui (Autocomplete), mantine | 20709caf |
Chose Chakra v3 band-above-control after rejecting MUI inline |
| 2026-05-22 | Layout Batch 1 (Box) | radix-themes, chakra, mantine | 66c6897c |
Moved gridArea/Column/Row from Grid → Box (Radix split). Added placeSelf. |
| 2026-05-22 | Layout Batch 1 (Flex + 6 others) | radix-themes, chakra, mantine | 591b0885 |
Surface-level only — see "Known gaps" below. Re-audit before next round. |
Known gaps from Layout Batch 1 (HISTORICAL — 2026-05-22 snapshot)
Superseded by the 2026-07 component re-audit programme (the full matrix went green —
docs/audit/components/_cierre.md). The gaps below are the frozen record of that moment; CURRENT gaps live in each component's dossier (## Gaps, with disposition).
| Primitive | Missing feature | Reference | Priority |
|---|---|---|---|
| Flex | alignContent (multi-line cross-axis align) |
Radix, Chakra | should |
| Grid | columns / rows shorthand (e.g. columns="3" → repeat(3, 1fr)) |
Radix Themes | should |
| Grid | inline boolean (display: inline-grid) |
parity with Flex.inline | nice |
| Grid | alignContent |
Radix, Chakra | should |
| Stack | divider slot (separator between items) |
Chakra Stack | nice |
| Stack | HStack / VStack helpers |
Chakra ergonomics | nice |
| Group | grow boolean (children fill width equally) |
Mantine Group | should |
| Group | preventGrowOverflow |
Mantine Group | nice |
| Wrap | shouldWrapChildren (auto-wrap each child in WrapItem) |
Chakra Wrap | nice |
| Container | fluid mode (no max-width) |
Mantine | should |
| Section | — | none missing | — |
9. Pre-port checklist (copy this into your task plan)
[ ] Read component-audit.md (this file)
[ ] Read demo-authoring.md
[ ] Read eidos/components/README.md
[ ] Identify reference libraries to audit against (§1)
[ ] Fill out the §3 audit template
[ ] Present scope table + decisions to user
[ ] Get user sign-off in writing
[ ] Implement: morfo → soma (if needed) → eidos → demo
[ ] Write src/uix/eidos/components/{name}/README.md with the
required sections (Baseline / Comparativa / Decisiones / Gaps)
[ ] Run `npm run component:audit` — verdict MUST be PASS
[ ] Run `npm run check` — 0 errors required
[ ] Visual walk: every Live control changes the stage; every Sema
`▶ play` button fires; snippets reflect control values
[ ] Add row to §8 audit log with commit hash
[ ] Update §6 anti-patterns if a new failure mode emerged
10. When in doubt
Stop and ask the user. The cost of a clarifying question is always lower than the cost of redoing 8 components.