Phase 1d of PLAN-docs-reconciliation.
- soma/README s3 + SOMA_ARCHITECTURE s2/s6/s12 + popover/link-preview
READMEs: positioning is the in-house engine (layers/floating +
$ethereal); @floating-ui is devDep-only. Layers table gains Stacking /
AxialDrag / ZoomPan / ImageProvider / ListSelection (popper/ is an empty
leftover dir — not documented). clsx documented as it really is: NOT
declared but still imported by props/props.ts via a svelte transitive
(phantom dep — flagged for a decision, chip spawned).
- COMPONENT_COMPLETION_CHECKLIST: new Enforcement column on every rule
table (audit | tool:X | manual) so declared-but-unchecked rules are
explicit; morfo rule IDs renamed M-* -> A-* to grep-match what
component-audit.ts emits; stale rows fixed against the corrected script
(D-1.2 v2 9-tab, D-3.1 single eidosSnippet, E-2.2 wrapper import);
implemented-but-undeclared rules added (R-2.7 literal typography,
A-3.4b verb/family pairing); A-3.3 family count decopied; sI now
requires an honest Enforcement value for new rules.
- Sema cascade numbering unified to 1 family - 2 intent - 3 morfo -
4 runtime - 5a packs - 5b app across sema/README, engine.ts,
resolver.ts, resolver.test.ts and soma/runtime.svelte.ts (three
divergent schemes coexisted; CLAUDE.md already uses the chosen one).
- '24 archetypes' copied lists replaced by pointers to
ARCHETYPE_VOCABULARY in active_architecture s6 + morfo/README.
- TriggerOptions.semantic JSDoc: phantom defaultSemantic removed
(implemented shape is additive allowedFamilies, D.11); 'warns via
logger' claim corrected — the override is silently ignored on
non-polymorphic events (no warn exists in code).
npm run check: 61 errors = pre-existing baseline, 0 new.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| M-1.1 | Exports a single `{Name}Morfo` const satisfying `Morfo` | error | all |
| M-1.2 | Has `name`, `kebab`, `scope: ['soma', ...]` declared | error | all |
| M-1.3 | Has `texts.label` as a valid idlangref (`#?components.{kebab}.label\|Fallback`), catalog entry in `langs/components/{kebab}.ts` | error | all |
| M-1.4 | If interactive: `apg` URL declared pointing at the relevant W3C ARIA pattern | warn | interactive |
| 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 |
| M-2.1 | Has at least one `Provider` part with `archetype: 'provider'` | error | all |
| M-2.2 | Every part declares `kebab`, `archetype`, `kind: 'public' \| 'internal'`, `defaultElement`, `role` | error | all |
| M-2.3 | Every public part has at least one `data-*` attr OR explicit justification in component README (`A2.3 exception: ...`) | warn | all |
| M-2.4 | Parts with non-trivial state declare `states: [...]` array | warn | interactive |
| M-2.5 | Archetype ∈ `ARCHETYPE_VOCABULARY` (`src/uix/morfo/types.ts`). No invented archetypes. | error | all |
| M-2.6 | Every part with focusable behavior has a `tabindex` or `role` that the browser focuses (no silent unfocusable interactive parts) | warn | interactive |
| A-2.1 | Has at least one `Provider` part with `archetype: 'provider'` | error | all | audit |
| A-2.2 | Every part declares `kebab`, `archetype`, `kind: 'public' \| 'internal'`, `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
| M-3.6 | Event name follows `{verb}-{variant}` or `{family}-{verb}` pattern | warn | interactive |
| M-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 |
| M-3.8 | If component supports value reset/clear: `commit.reset` or `commit.discard` event declared | warn | interactive |
| M-3.9 | If component supports navigation steps (calendar, stepper, pagination): `shift.navigate` event declared with target part | warn | interactive |
| M-3.10 | Intent is declared (`'neutral' \| 'affirm' \| 'fulfill' \| 'risk' \| 'threat' \| 'loss'`) when family is valenced (commit/signal). `target.partRef` always set | warn | interactive |
| A-3.6 | Event name follows `{verb}-{variant}` or `{family}-{verb}` pattern | warn | 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 |
| M-4.1 | Provider part has `aria-label` or `aria-labelledby` declared with `severity: 'recommended'` | warn | interactive |
| M-4.2 | If component has invalid/disabled/readonly/required state: matching `aria-invalid`/`aria-disabled`/`aria-readonly`/`aria-required` declared conditionally | error | interactive |
| M-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 |
| M-4.4 | No reinvented keys (`Spacebar` is `' '`; `Esc` is `'Escape'`; etc.) — must match KeyboardEvent.key values | error | interactive |
| 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 |
---
@ -83,34 +92,34 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
| E-2.1 | `types.ts` exports the public Props + size/variant/color unions | error | all |
| E-2.2 | `{name}.css` exists and is imported from `src/uix/eidos/index.css` | error | all |
| E-2.3 | `README.md` exists with baseline (Air or "no baseline"), external comparison table, decisions, gaps | error | all |
| 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 |
| 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) | 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 |
| 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 |
| R-2.2 | No raw font-size in px/rem. Use `var(--font-size-*)` from Eidos recipe | warn | all |
| R-2.3 | No magic numbers in spacing — use `var(--space-*)` or `var(--{component}-*)` | warn | all |
| R-2.4 | Component-scoped tokens come from `EidosConfig.recipes` (`base.css`) — verify via `ActiveEidos.listRecipes()` | warn | all |
| R-2.5 | No `--eidos-*` or `--soma-*` variable invented in component recipe | error | all |
| 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 |
| 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 |
| 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 |
| R-4.2 | No literal fractional `opacity` outside `@keyframes` — disabled/muted states consume `var(--opacity-*)` | error | all |
| 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 |
| 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 |
| R-4.5 | Local `@keyframes` require a `/* functional: … */` annotation — perceptual signatures live in `EidosConfig.motion`, not in component CSS | error | all |
| R-4.6 | No direct `var(--scale-*)` / `var(--primitive-*)` in component CSS — consume `var(--color-{role}-{slot})` or recipe tokens | error | all |
| 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 |
---
@ -174,89 +184,89 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete.
| 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 |
| D-7.1 | Every control on the Live tab produces a visible change on the stage | warn | all |
| D-7.2 | No demo-only `data-*` attrs hand-stamped to fake morfo selectors | error | all |
| D-7.3 | Soma layer badge `[soma]` and Eidos layer badge `[eidos]` used to group control subsections | warn | all |
| 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 §7.x. | error | all |
| D-7.5 | **Size coverage**: the chip array for `size` matches the component's category in DEMO_AUTHORING §7.x (form controls / text inputs / progress-meter / field-form expose `xs..xl`; nav controls expose `xs..lg`; passive panels keep `sm..lg`). | warn | all |
| 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 |
| F-1.1 | README has section "## Baseline" with Air comparison or explicit "no Air baseline" | error | all |
| 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 |
| F-1.3 | README has section "## Decisiones" with explicit choices made vs alternatives | warn | all |
| F-1.4 | README has section "## Gaps" listing what's deferred / not implemented, each with disposition (implementar / diferir / descartar) | error | all |
@ -296,9 +306,18 @@ A component is **BROKEN** when >5 errors OR any X-1.x script fails.
## I. How to add a new rule
1. Append to the appropriate section table.
2. Update `scripts/component-audit.ts` with a check function that returns `CheckResult`.
3. Add the rule to the doctrinal source if it crosses a layer (`active_architecture.md`, `GUIA_IMPLEMENTACION_SEMAUIX.md`, or `DEMO_AUTHORING_GUIDE.md`).
4. Document the rationale at the top of the new rule's check function.
Rules should be **doctrinally grounded** (cite the source doc) and **machine-checkable** (avoid pure aesthetic criteria — those go in the per-component README review).
1. Append to the appropriate section table **with its Enforcement value**.
2. 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).
3. If `Enforcement: manual` / `tool:X`: no script change, but the value must be
honest — declaring a rule here does not make it checked.
4. Add the rule to the doctrinal source if it crosses a layer
(`active_architecture.md`, `docs/CANON.md`, or `DEMO_AUTHORING_GUIDE.md`).
5. 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).
| `Floating*` | `FloatingProvider.create()`, `FloatingContent.create(opts)`, etc. | Posicionamiento relativo a anchor via @floating-ui. |
| `Floating*` | `FloatingProvider.create()`, `FloatingContent.create(opts)`, etc. | Posicionamiento relativo a anchor — motor propio (`layers/floating` + `$ethereal`; `@floating-ui` es devDep de tests de paridad). |
Si una dependencia tiene API inestable o podria cambiar, se accede a traves de una frontera formal (como `layers/floating/` wrappea @floating-ui). Las dependencias estables (runed, svelte) se importan directamente.
- externos: `runed`, `tabbable` — dependencias npm declaradas (`clsx` se
importa en `props/props.ts` sin declarar — resuelve como transitiva de
svelte; deuda pendiente de decisión)
El posicionamiento flotante dejó de ser dependencia externa: es motor
propio (`layers/floating` + `$ethereal`); `@floating-ui` sobrevive solo
como devDependency para los tests de paridad. Si una dependencia tiene
API inestable o podria cambiar, se accede a traves de una frontera formal
(como `layers/floating/` hace con el motor de posicionamiento). Las
dependencias estables (runed, svelte) se importan directamente.
A floating preview card that appears on hover over a link. Supports configurable open/close delays, positioning via @floating-ui, safe pointer transitions, and dismissal.
A floating preview card that appears on hover over a link. Supports configurable open/close delays, positioning via the in-house floating layer (`soma/layers/floating` + `$ethereal`), safe pointer transitions, and dismissal.
## Anatomy
@ -21,7 +21,7 @@ A floating preview card that appears on hover over a link. Supports configurable
A floating panel anchored to a trigger element. Supports focus management, dismissal, positioning via `@floating-ui`, and an optional overlay.
A floating panel anchored to a trigger element. Supports focus management, dismissal, positioning via the in-house floating layer (`soma/layers/floating` + `$ethereal`), and an optional overlay.