| title |
type |
audience |
authority |
status |
source |
| Component completion checklist |
guide |
human + agent |
canonical — the acceptance matrix for a component being done |
current |
migrated from src/uix/COMPONENT_COMPLETION_CHECKLIST.md (2026-07-02, docs-book F7.5) |
Component completion checklist
Doctrinal criteria for considering a UIX component done across all four
layers (Morfo · Soma · Sema · Eidos), its recipe CSS, and its demo page.
Source of truth — architecture/active-architecture.md,
decisions/guia-semantica-historica.md (historical seed),
demo-authoring.md.
Machine-validated by scripts/component-audit.ts. Run via
npm run component:audit [name]?. Outputs a markdown report at
tmp/component-audit.md.
This is the acceptance matrix — the criteria for done, not a build
guide. For HOW to build a component (the ordered authoring steps + rationale
rules A1–A37), see component-guide.md. The
two are a complementary pair, not duplicate checklists.
How to read this
Each rule has a severity, an applicability, and an enforcement:
- Severity:
error — blocks the component from being considered done.
warn — should be fixed but not blocking.
info — informational, no remediation expected.
- Applicability:
all — every public component.
interactive — components with user actions (most). Identified by morfo.events.length > 0 OR morfo.parts.*.keyboard.length > 0.
passive — purely structural / display components (icon, avatar, breadcrumb, meter, progress). Allowed 0 events only after README justifies it.
- Enforcement — who verifies the rule. Declaring a rule here does NOT
imply the audit script checks it; this column makes the gap explicit:
audit — implemented in scripts/component-audit.ts (the report prints
the same rule ID).
tool:{name} — enforced by another script/test (e.g. tool:morfo:check,
tool:smoke, tool:check for tsc/svelte-check).
manual — human review; no mechanical check exists yet. Candidates for
promotion to audit are welcome (see §I).
A. Morfo declaration
The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
A1 · Basics
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| A-1.1 |
Exports a single {Name}Morfo const satisfying Morfo |
error |
all |
audit |
| A-1.2 |
Has name, kebab, scope: ['soma', ...] declared |
error |
all |
audit |
| A-1.3 |
Has texts.label as a valid idlangref (#?components.{kebab}.label|Fallback), catalog entry in langs/components/{kebab}.ts |
error |
all |
audit |
| A-1.4 |
If interactive: apg URL declared pointing at the relevant W3C ARIA pattern |
warn |
interactive |
audit |
A2 · Parts
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| A-2.1 |
Has at least one part with kebab: 'provider' (the audit checks the kebab only — the archetype may vary: trigger when the Provider IS the interactive element (Toggle/Switch), image for icon, …) |
error |
all |
audit |
| A-2.2 |
Every part declares kebab, archetype, kind: 'public' | 'private' | 'virtual' (MorfoPartKind), defaultElement, role |
error |
all |
manual |
| A-2.3 |
Every public part has at least one data-* attr OR explicit justification in component README (A2.3 exception: ...) |
warn |
all |
manual |
| A-2.4 |
Parts with non-trivial state declare states: [...] array |
warn |
interactive |
manual |
| A-2.5 |
Archetype ∈ ARCHETYPE_VOCABULARY (src/uix/morfo/types.ts). No invented archetypes. |
error |
all |
audit |
| A-2.6 |
Every part with focusable behavior has a tabindex or role that the browser focuses (no silent unfocusable interactive parts) |
warn |
interactive |
manual |
A3 · Events — the part where current components leak
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| A-3.1 |
If interactive: events.length >= 1 |
error |
interactive |
audit |
| A-3.2 |
Every event has name, semantic.family, semantic.target (partRef) |
error |
all |
tool:check |
| A-3.3 |
semantic.family ∈ SEMA_FAMILIES (source: src/uix/sema/types.ts — do not copy the list) |
error |
all |
audit |
| A-3.4 |
semantic.verb ∈ SEMA_VERBS[family] |
error |
all |
audit |
| A-3.4b |
Per-event family.verb pairing is canonical (no verb borrowed from another family) |
warn |
all |
audit |
| A-3.5 |
semantic.sequence ∈ 'pre' | 'coincident' | 'post' declared explicitly |
warn |
interactive |
audit |
| A-3.6 |
Event name follows the {family}-{verb}[-{nuance}] pattern (validateMorfo throws otherwise) |
error |
interactive |
audit |
| A-3.7 |
Event/keyboard coverage: every distinct keyboard action that mutates state has a corresponding semantic event. Pure focus moves don't need an event. |
error |
interactive |
audit |
| A-3.8 |
If component supports value reset/clear: commit.reset or commit.discard event declared |
warn |
interactive |
manual |
| A-3.9 |
If component supports navigation steps (calendar, stepper, pagination): shift.navigate event declared with target part |
warn |
interactive |
manual |
| A-3.10 |
Intent is declared when family requires it per SEMA_FAMILY_POLICY (src/uix/sema/types.ts). target.partRef always set |
warn |
interactive |
audit |
A4 · ARIA + keyboard
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| A-4.1 |
Provider part has aria-label or aria-labelledby declared with severity: 'recommended' |
warn |
interactive |
manual |
| A-4.2 |
If component has invalid/disabled/readonly/required state: matching aria-invalid/aria-disabled/aria-readonly/aria-required declared conditionally |
error |
interactive |
manual |
| A-4.3 |
If APG pattern requires specific keys (e.g., Grid: Arrow×4 + Home/End/PageUp/PageDown), all are declared in part keyboard |
warn |
interactive |
manual |
| A-4.4 |
No reinvented keys (Spacebar is ' '; Esc is 'Escape'; etc.) — must match KeyboardEvent.key values |
error |
interactive |
audit |
B. Eidos wrapper
B1 · API shape (Option C disciplined)
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| E-1.1 |
Has {name}.svelte (root visual) + per-part files {name}-{part}.svelte |
error |
all |
audit |
| E-1.2 |
index.ts does explicit per-property assignment (X.Part = Part), not Object.assign(X, { ... }) |
error |
all |
audit |
| E-1.3 |
index.ts exports {Name} named + default {Name} |
error |
all |
audit |
| E-1.4 |
No exports of Provider, Base, Root, Parts, or Soma{Name}Provider |
error |
all |
audit |
| E-1.5 |
Imports Soma as import * as {Name} from '$soma/components/{kebab}' — namespace, not destructured |
warn |
all |
manual |
| E-1.6 |
Types: {Name}Props, {Name}Size, {Name}Variant (no EidosX* prefixes) |
error |
all |
manual |
| E-1.7 |
For single-part components, the default IS the component (Toggle, Switch, Icon) — no fake compound API |
error |
all |
manual |
B2 · Files + structure
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| E-2.1 |
types.ts exports the public Props + size/variant/color unions |
error |
all |
audit |
| E-2.2 |
{name}.css exists and is wired: imported by the component's own wrapper (current, code-split pattern) OR from eidos/index.css (layout primitives + shared visuals like spin-field), OR a documented E-2.2 exception: in README (headless components with no visual recipe) |
error |
all |
audit |
| E-2.3 |
README.md exists with baseline (Air or "no baseline"), external comparison table, decisions, gaps |
error |
all |
audit |
| E-2.4 |
Every part declared in morfo (kind: 'public') has either a wrapper file or an explicit README note explaining why it's not exposed |
warn |
all |
manual |
| E-2.5 |
If the component has any direction-dependent behaviour or paint: public dir prop declared as dir?: Direction — the alias, never a hand-written 'ltr' | 'rtl' union (canon/direction-contract.md §1) |
error |
all |
manual |
B3 · Wrapper internals
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| E-3.1 |
{name}.svelte renders Soma's {Name}.Provider (or equivalent) — does NOT mount Soma Trigger/Content directly |
error |
all |
manual |
| E-3.2 |
No $state re-declaration of bindable props from Soma (use $bindable proxy) |
warn |
all |
manual |
| E-3.3 |
Snippets receive children prop and don't shadow it with {#snippet children} in same scope |
error |
all |
manual |
| E-3.4 |
No data-_/CSS leaking from other layers: no data-soma-_, no --soma-_/--air-_ CSS variables |
error |
all |
manual |
| E-3.5 |
All visual props (size, variant, color, radius) map to data-{prop}="value" on the root for CSS to read. dir is exempt: it is native and stamped raw — data-dir is a legitimate extra (the resolved value, always present wherever a component opts into stamping it), never a substitute, since :dir() cannot see it |
warn |
all |
manual |
| E-3.6 |
If the component declares dir: the wrapper runs activeDir(() => dir, soma) at Provider.create(…) and the provider defaults once, in resolvedDir — it never re-implements the chain (no second defaulting, no prefs lookup, no DOM read). A further link (a submenu inheriting its parent menu) is composed at the call site, never added inside the provider |
error |
all |
manual |
| E-3.7 |
No silent internal write (catalogue invariant): every callback reporting writes to a bindable (onValueChange beside value) fires INSIDE that key's { get, set } setter, and the provider only writes — it never invokes the callback itself (that double-fires). A path that writes the bindable without notifying is a defect: bind: and the callback would see different histories. ONE named exception: a coalesced (debounced) notification, which must flush on clear/submit and emit from a single private method — component-guide.md §Callback conventions |
error |
components with a bindable + change callback |
manual |
| E-3.8 |
The wrapper builds its bag with bindProps<XOpts> (target-typed — the provider EXPORTS its Opts) or, when the whole bag is { id, ref }, with partOpts. No cast at the call site: a cast silences the whole bag |
warn |
all |
manual |
C. Recipe CSS
C1 · State coverage
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| R-1.1 |
Has root selector [data-{component}] defining base layout/spacing, OR a documented R-1.1 exception: in README (foundation-riding components with no recipe of their own) |
error |
all |
audit |
| R-1.2 |
If morfo declares data-disabled on any part: [data-disabled] styled, OR a documented R-1.2 exception: in README (state owned by a composed primitive's recipe) |
error |
interactive |
audit |
| R-1.3 |
If morfo declares data-readonly: [data-readonly] styled |
warn |
interactive |
audit |
| R-1.4 |
If morfo declares data-invalid: [data-invalid] styled (using --color-risk-element or similar) |
warn |
interactive |
audit |
| R-1.5 |
All focusable parts show a focus treatment — :focus-visible, the field shell's :focus-within, a :has(…:focus…) rule, or the canonical [data-focused] state ring — OR a documented R-1.5 exception: in README (foundation archetype ring / shared layer / composed primitive / no focusable part) |
error |
interactive |
audit |
| R-1.6 |
Hover state defined for trigger-like archetypes (trigger, item, option, close, action) |
warn |
interactive |
manual |
| R-1.7 |
Disabled state has cursor: not-allowed OR documented exception in README |
warn |
interactive |
manual |
| R-1.8 |
If the recipe branches with :dir(…): the provider stamps the raw opts.dir.current as the native dir attribute — an unstamped assertion moves the maths and leaves the paint behind (canon/direction-contract.md §2) |
error |
all |
manual |
C2 · Token discipline
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| R-2.1 |
No raw colors (hex/rgb/named). All colors come from var(--color-*) or var(--{component}-*). Applies to recipes/base.ts, archetypes.css, events.css and every component *.css — no exceptions. |
error |
all |
audit |
| R-2.2 |
No raw font-size in px/rem. Use var(--font-size-*) from Eidos recipe |
warn |
all |
audit (via R-2.7) |
| R-2.3 |
No magic numbers in spacing — use var(--space-*) or var(--{component}-*) |
warn |
all |
manual |
| R-2.4 |
Component-scoped tokens come from EidosConfig.recipes (base.css) — verify via ActiveEidos.listRecipes() |
warn |
all |
manual |
| R-2.5 |
No --eidos-* or --soma-* variable invented in component recipe |
error |
all |
audit |
| R-2.6 |
Every var(--color-X) referenced in recipes or component CSS is declared in generated/base.css. The theme contract (SurfaceColorRoles, ContentColorRoles, BorderColorRoles, FocusColorRoles + intent role maps) is the closed set; new tokens go through themes/base.ts + regen. |
error |
all |
audit |
| R-2.7 |
No literal typography in recipes (font-size, line-height, letter-spacing, font-weight raw values) — consume the foundation's type anchor via tokens. Escape valves: CSS keywords, numeric identities (0, 1), or a same-line /* literal: <reason> */ |
warn |
all |
audit |
C3 · Color resolution
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| R-3.1 |
Recipe uses [data-color='X'] selectors only for the subset declared in component's README |
warn |
colored |
manual |
| R-3.2 |
No legacy color names (success, warning, danger, info) |
error |
all |
audit |
| R-3.3 |
Intent ↔ color resolution implemented: when component receives intent != 'neutral', data-color reflects the intent |
warn |
colored |
manual |
C4 · Recipe Contract — transversal systems
Canon: canon/recipe-contract.md. These
rules enforce that every recipe consumes the theming's transversal systems
(state-layer, tokenized elevation, opacity token, logical axes, motion channel)
instead of hand-rolling its own idiom. All R-4.x are error: R-4.1/4.2/4.3/4.4/4.6
graduated after the 2026-07-02 mechanical backfill; R-4.5 after the motion migration
emptied its backlog (recipes consume the channel via preset stamp, signatures, or
registered keyframes — the trigger-vs-materials doctrine is in the contract). The
WIP tracks words / palabras / chronos are excluded. Escape valves: a same-line
/* literal: <reason> */ (values), /* functional: <reason> */ (keyframes) or
/* important: <reason> */ (!important).
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| R-4.1 |
No literal box-shadow — elevation goes through var(--shadow-*) / var(--depth-{plane}-*) (or the inset-ring pattern, which carries var()) |
error |
all |
audit |
| R-4.2 |
No literal fractional opacity outside @keyframes — disabled/muted states consume var(--opacity-*) |
error |
all |
audit |
| R-4.3 |
:hover backgrounds are the state-layer (var(--state-*)) or a palette token — no raw values, no hand-rolled color-mix(… currentColor …) |
error |
interactive |
audit |
| R-4.4 |
No physical-axis token keys (padding-x/-y, margin-x/-y) in lib/recipes/base.ts — logical axes (padding-inline/-block) are the canon |
error |
all |
audit |
| R-4.5 |
Local @keyframes require a /* functional: … */ annotation — perceptual signatures live in EidosConfig.motion, not in component CSS |
error |
all |
audit |
| R-4.6 |
No direct var(--scale-*) / var(--primitive-*) in component CSS — consume var(--color-{role}-{slot}) or recipe tokens |
error |
all |
audit |
| R-4.7 |
No !important without a same-line /* important: <reason> */ annotation — the declaration wins every cascade fight, so the reason lives where it happens |
error |
all |
audit |
| R-5.1 |
Every appearance declaration carries an act: a public var(--{c}-…) token, a /* literal: <reason> */ annotation, or an entry in the debt ledger (scripts/theming-census-debt.ts). New debt outside the ledger fails; a ledger entry whose knob is no longer debt is STALE and fails until the line is deleted |
error |
all |
audit |
| R-5.2 |
A recipe with themeable knobs declares them in lib/recipes/base.ts — unless it has nothing to declare (all system / structural) or every one of its knobs is registered in the debt ledger |
error |
all |
audit |
| R-5.3 |
Recipe token keys follow the naming grammar: the ink slot is fg (never color), and the interactive modifier goes IN FRONT (hover-bg) while size, orientation and context stay behind (control-height-md) — recipe-contract §1 |
error |
all |
audit |
D. Demo page (web/routes/uix/components/{kebab}/+page.svelte)
D1 · Template compliance
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| D-1.1 |
Outer element is <div data-uix-canvas-inner> |
error |
all |
audit |
| D-1.2 |
Tab union matches the canonical v2 9-tab template ('live' | 'system' | 'motion' | 'sema' | 'services' | 'api' | 'morfo' | 'recipe' | 'a11y'); the v1 6-tab union is accepted only pending migration |
error |
all |
audit |
| D-1.3 |
Imports compileMorfo + the component's morfo |
error |
all |
audit |
| D-1.4 |
Imports getActiveUix if Sema tab has interactive Play buttons |
warn |
interactive |
manual |
| D-1.5 |
Has MutationObserver on data-event attribute, populating trace state |
error |
interactive |
audit |
| D-1.6 |
Has <div data-uix-stage> between header and tablist (live always rendered) |
error |
all |
audit |
| D-1.7 |
Has <div data-uix-stage-trace> with at least the trace strip |
error |
interactive |
audit |
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| D-2.1 |
Header has data-uix-eyebrow (Category · Name) |
error |
all |
audit |
| D-2.2 |
Header has <h1 data-uix-page-title> and <p data-uix-page-lede> (single paragraph summary) |
error |
all |
audit |
| D-2.3 |
Header has data-uix-page-meta with at minimum parts and events pills |
error |
all |
audit |
D3 · Snippet parity (DEMO_AUTHORING §12)
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| D-3.1 |
eidosSnippet derived and rendered (the v2 template's single snippet; v1 demos may additionally carry somaSnippet) |
error |
all |
audit |
| D-3.2 |
Snippet code reflects current control values (not static placeholders) |
warn |
all |
manual |
| D-3.3 |
If live preview uses a schema/options/state, snippet declares the same |
warn |
all |
manual |
D4 · Sema tab
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| D-4.1 |
Sema tab always rendered (even for 0-event components, with explicit empty state) |
error |
all |
manual |
| D-4.2 |
Sema tab has events table: name / family / verb / sequence / intent / play |
error |
interactive |
manual |
| D-4.3 |
Play buttons emit via uix.events.emit(...) onto a real DOM target inside the stage |
error |
interactive |
audit |
D5 · Morfo tab (declarative contract surface)
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| D-5.1 |
Has header table: name / kebab / scope / apg / parts / events |
error |
all |
manual |
| D-5.2 |
Has parts overview table |
error |
all |
manual |
| D-5.3 |
For each part with data/aria/keyboard: per-part subsection rendered |
warn |
all |
manual |
| D-5.4 |
Events declaration table rendered |
error |
interactive |
manual |
D6 · A11y tab + Recipe tab
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| D-6.1 |
A11y tab has keyboard table (from morfo) + ARIA contract table |
warn |
interactive |
manual |
| D-6.2 |
Recipe tab lists selectors with their layer source (morfo / eidos) |
warn |
all |
manual |
D7 · Visible controls discipline (DEMO_AUTHORING §12.6)
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| D-7.1 |
Every control on the Live tab produces a visible change on the stage |
warn |
all |
manual |
| D-7.2 |
No demo-only data-* attrs hand-stamped to fake morfo selectors |
error |
all |
manual |
| D-7.3 |
Soma layer badge [soma] and Eidos layer badge [eidos] used to group control subsections |
warn |
all |
manual |
| D-7.4 |
Chip parity: every chip-group control (size, variant, color) enumerates the full union of the component's type — no truncated arrays. Theme defines 3 variants → all 3 are selectable. See DEMO_AUTHORING §6. |
error |
all |
audit |
| D-7.5 |
Size coverage: the chip array for size matches the component's declared union in recipe + types, 1:1 (form controls / text inputs / progress-meter / field-form expose xs..xl; nav controls expose xs..lg; passive panels keep sm..lg). |
warn |
all |
manual |
E. Cross-layer integrity
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| X-1.1 |
npm run morfo:check PASS for this component |
error |
all |
tool:morfo:check |
| X-1.2 |
npm run perm:check PASS for this component if it's instrumented |
warn |
interactive |
tool:perm:check |
| X-1.3 |
eidos-lint-all.ts invalid count = 0 for this component |
error |
all |
tool:eidos-lint |
| X-1.4 |
npm run check does not produce errors in this component's files |
error |
all |
tool:check |
| X-1.5 |
Component's smoke route loads without console errors (SMOKE_SCOPE=/uix/components/{kebab} npm run smoke) |
error |
all |
tool:smoke |
| X-1.6 |
npm run rtl:check PASS (RTL-1 — logical inline anchor paired with a physical inline translate; RTL-2 — a :dir() rule that turns one logical face off and repaints the other, cancelling a mirror the property had already made). The run walks all of src/uix/eidos; there is no per-component scope |
error |
all |
tool:rtl:check |
F. Documentation completeness (component README)
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| F-1.1 |
README has section "## Baseline" with Air comparison or explicit "no Air baseline" |
error |
all |
audit |
| F-1.2 |
README has section "## Comparativa" with at least 3 external references (Ark UI, Bits UI, Radix/shadcn, React Aria, MUI, or similar) |
error |
all |
audit |
| F-1.3 |
README has section "## Decisiones" with explicit choices made vs alternatives |
warn |
all |
audit |
| F-1.4 |
README has section "## Gaps" listing what's deferred / not implemented, each with disposition (implementar / diferir / descartar) |
error |
all |
audit |
| F-1.5 |
If component is passive (0 events): README explicitly justifies why (## Passive justification) |
error |
passive |
audit |
G. Doctrinal canon (intent + color + sequence)
G1 · Subset declaration
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| G-1.1 |
README declares ## Subset listing which color values + which intent values the component accepts (per decisions/guia-semantica-historica.md §3) |
warn |
colored |
manual |
| G-1.2 |
Types restrict intent/color props to the declared subset via union types — not free-form string |
warn |
colored |
manual |
| G-1.3 |
Recipe CSS only matches [data-color='X'] for X in the declared subset |
warn |
colored |
manual |
G2 · Sequence canon
| ID |
Rule |
Severity |
Applicability |
Enforcement |
| G-2.1 |
Events that animate exit before structural commit use sequence: 'pre' (dismiss, close-cancel, etc.) |
warn |
interactive |
manual |
| G-2.2 |
Events that confirm a result use sequence: 'post' (commit-save, submit, etc.) |
warn |
interactive |
manual |
| G-2.3 |
Continuous progress events use sequence: 'coincident' (sustain.progress) |
warn |
passive |
manual |
H. Severity summary table
A component is PASS when:
- 0 errors across A–H
- ≤3 warnings, each justified in README "## Audit exceptions"
- All required scripts (X-1.x) pass
A component is NEEDS-WORK when 1-5 errors or >3 unjustified warnings.
A component is BROKEN when >5 errors OR any X-1.x script fails.
I. How to add a new rule
- Append to the appropriate section table with its Enforcement value.
- If
Enforcement: audit: implement it in scripts/component-audit.ts with a
check function that returns CheckResult, using the SAME rule ID the table
declares (the report and this doc must grep-match).
- If
Enforcement: manual / tool:X: no script change, but the value must be
honest — declaring a rule here does not make it checked.
- Add the rule to the doctrinal source if it crosses a layer
(
architecture/active-architecture.md,
CANON.md, or demo-authoring.md).
- Document the rationale at the top of the new rule's check function.
Rules should be doctrinally grounded (cite the source doc) and, when
audit, machine-checkable (avoid pure aesthetic criteria — those go in the
per-component README review). Keep this table and the script in sync: every
audit rule ID exists in the script, and every rule ID the script emits exists
here (npm run docs:check verifies both directions).