diff --git a/src/uix/COMPONENT_COMPLETION_CHECKLIST.md b/src/uix/COMPONENT_COMPLETION_CHECKLIST.md index 664871124..736f8c83e 100644 --- a/src/uix/COMPONENT_COMPLETION_CHECKLIST.md +++ b/src/uix/COMPONENT_COMPLETION_CHECKLIST.md @@ -16,7 +16,7 @@ ## How to read this -Each rule has a **severity** and an **applicability**: +Each rule has a **severity**, an **applicability**, and an **enforcement**: - **Severity**: - `error` — blocks the component from being considered done. @@ -26,6 +26,14 @@ Each rule has a **severity** and an **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). --- @@ -35,47 +43,48 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete. ### A1 · Basics -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| 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 | +| 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 | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| 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 | +| ID | Rule | Severity | Applicability | Enforcement | +| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- | +| 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 -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| M-3.1 | If interactive: `events.length >= 1` | error | interactive | -| M-3.2 | Every event has `name`, `semantic.family`, `semantic.target` (partRef) | error | all | -| M-3.3 | `semantic.family` ∈ `SEMA_FAMILIES` (7 valid) | error | all | -| M-3.4 | `semantic.verb` ∈ `SEMA_VERBS[family]` | error | all | -| M-3.5 | `semantic.sequence` ∈ `'pre' \| 'coincident' \| 'post'` declared explicitly | warn | interactive | -| 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 | +| 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 `{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 | ### A4 · ARIA + keyboard -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| 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 | +| 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 | --- @@ -83,34 +92,34 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete. ### B1 · API shape (Option C disciplined) -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| E-1.1 | Has `{name}.svelte` (root visual) + per-part files `{name}-{part}.svelte` | error | all | -| E-1.2 | `index.ts` does **explicit per-property assignment** (`X.Part = Part`), not `Object.assign(X, { ... })` — see DEMO_AUTHORING §12.3 | error | all | -| E-1.3 | `index.ts` exports `{Name}` named + `default {Name}` | error | all | -| E-1.4 | No exports of `Provider`, `Base`, `Root`, `Parts`, or `Soma{Name}Provider` | error | all | -| E-1.5 | Imports Soma as `import * as {Name} from '$soma/components/{kebab}'` — namespace, not destructured | warn | all | -| E-1.6 | Types: `{Name}Props`, `{Name}Size`, `{Name}Variant` (no `EidosX*` prefixes) | error | all | -| E-1.7 | For single-part components, the default IS the component (Toggle, Switch, Icon) — no fake compound API | error | all | +| 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 | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| 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 | +| 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) | 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 | ### B3 · Wrapper internals -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| E-3.1 | `{name}.svelte` renders Soma's `{Name}.Provider` (or equivalent) — does NOT mount Soma `Trigger`/`Content` directly | error | all | -| E-3.2 | No `$state` re-declaration of bindable props from Soma (use `$bindable` proxy) | warn | all | -| E-3.3 | Snippets receive `children` prop and don't shadow it with `{#snippet children}` in same scope — see DEMO_AUTHORING §12.4 | error | all | -| E-3.4 | No data-*/CSS leaking from other layers: no `data-soma-*`, no `--soma-*`/`--air-*` CSS variables | error | all | -| E-3.5 | All visual props (`size`, `variant`, `color`, `radius`) map to `data-{prop}="value"` on the root for CSS to read | warn | all | +| 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 | warn | all | manual | --- @@ -118,34 +127,35 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete. ### C1 · State coverage -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| R-1.1 | Has root selector `[data-{component}]` defining base layout/spacing | error | all | -| R-1.2 | If morfo declares `data-disabled` on any part: `[data-disabled]` styled | error | interactive | -| R-1.3 | If morfo declares `data-readonly`: `[data-readonly]` styled | warn | interactive | -| R-1.4 | If morfo declares `data-invalid`: `[data-invalid]` styled (using `--color-risk-element` or similar) | warn | interactive | -| R-1.5 | All focusable parts have `:focus-visible` styled | error | interactive | -| R-1.6 | Hover state defined for trigger-like archetypes (`trigger`, `item`, `option`, `close`, `action`) | warn | interactive | -| R-1.7 | Disabled state has `cursor: not-allowed` OR documented exception in README | warn | interactive | +| ID | Rule | Severity | Applicability | Enforcement | +| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- | +| R-1.1 | Has root selector `[data-{component}]` defining base layout/spacing | error | all | audit | +| R-1.2 | If morfo declares `data-disabled` on any part: `[data-disabled]` styled | 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 have `:focus-visible` styled | 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 | ### C2 · Token discipline -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| 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 | +| 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: */` | warn | all | audit | ### C3 · Color resolution -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| R-3.1 | Recipe uses `[data-color='X']` selectors only for the subset declared in component's README | warn | colored | -| R-3.2 | No legacy color names (`success`, `warning`, `danger`, `info`) | error | all | -| R-3.3 | Intent ↔ color resolution implemented: when component receives `intent != 'neutral'`, `data-color` reflects the intent | warn | colored | +| 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 @@ -159,14 +169,14 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete. > WIP tracks `words` / `palabras` / `chronos` are excluded. Escape valves: a same-line > `/* literal: */` (values) or `/* functional: */` (keyframes). -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| 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 | +| 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 | --- @@ -174,89 +184,89 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete. ### D1 · Template compliance -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| D-1.1 | Outer element is `
` | error | all | -| D-1.2 | Tab union is exactly `'live' \| 'api' \| 'morfo' \| 'sema' \| 'recipe' \| 'a11y'` | error | all | -| D-1.3 | Imports `compileMorfo` + the component's morfo | error | all | -| D-1.4 | Imports `getActiveUix` if Sema tab has interactive Play buttons | warn | interactive | -| D-1.5 | Has MutationObserver on `data-event` attribute, populating `trace` state | error | interactive | -| D-1.6 | Has `
` between header and tablist (live always rendered) | error | all | -| D-1.7 | Has `
` with at least the trace strip | error | interactive | +| ID | Rule | Severity | Applicability | Enforcement | +| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- | +| D-1.1 | Outer element is `
` | 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 `
` between header and tablist (live always rendered) | error | all | audit | +| D-1.7 | Has `
` with at least the trace strip | error | interactive | audit | ### D2 · Header -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| D-2.1 | Header has `data-uix-eyebrow` (Category · Name) | error | all | -| D-2.2 | Header has `

` and `

` (single paragraph summary) | error | all | -| D-2.3 | Header has `data-uix-page-meta` with at minimum `parts` and `events` pills | error | all | +| ID | Rule | Severity | Applicability | Enforcement | +| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- | +| D-2.1 | Header has `data-uix-eyebrow` (Category · Name) | error | all | audit | +| D-2.2 | Header has `

` and `

` (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 §8.1) +### D3 · Snippet parity (DEMO_AUTHORING §12) -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| D-3.1 | Both `somaSnippet` and `eidosSnippet` derived and rendered | error | all | -| D-3.2 | Snippet code reflects current control values (not static placeholders) | warn | all | -| D-3.3 | If live preview uses a schema/options/state, snippet declares the same | warn | all | +| 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 | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| D-4.1 | Sema tab always rendered (even for 0-event components, with explicit empty state) | error | all | -| D-4.2 | Sema tab has events table: `name / family / verb / sequence / intent / play` | error | interactive | -| D-4.3 | Play buttons emit via `uix.events.emit(...)` onto a real DOM target inside the stage | error | interactive | +| 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 | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| D-5.1 | Has header table: name / kebab / scope / apg / parts / events | error | all | -| D-5.2 | Has parts overview table | error | all | -| D-5.3 | For each part with data/aria/keyboard: per-part subsection rendered | warn | all | -| D-5.4 | Events declaration table rendered | error | interactive | +| 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 | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| D-6.1 | A11y tab has keyboard table (from morfo) + ARIA contract table | warn | interactive | -| D-6.2 | Recipe tab lists selectors with their layer source (`morfo` / `eidos`) | warn | all | +| 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 | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| 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 | +| 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 | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| X-1.1 | `npm run morfo:check` PASS for this component | error | all | -| X-1.2 | `npm run perm:check` PASS for this component if it's instrumented | warn | interactive | -| X-1.3 | `eidos-lint-all.ts` invalid count = 0 for this component | error | all | -| X-1.4 | `npm run check` does not produce errors in this component's files | error | all | -| X-1.5 | Component's smoke route loads without console errors (`SMOKE_SCOPE=/uix/components/{kebab} npm run smoke`) | error | all | +| 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 | --- ## F. Documentation completeness (component README) -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| 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 | -| F-1.5 | If component is `passive` (0 events): README explicitly justifies why (`## Passive justification`) | error | passive | +| 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 | --- @@ -264,19 +274,19 @@ The morfo is DNA. If it's incomplete, every downstream layer is incomplete. ### G1 · Subset declaration -| ID | Rule | Severity | Applicability | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| G-1.1 | README declares `## Subset` listing which `color` values + which `intent` values the component accepts (per GUIA_IMPLEMENTACION_SEMAUIX §3) | warn | colored | -| G-1.2 | Types restrict `intent`/`color` props to the declared subset via union types — not free-form string | warn | colored | -| G-1.3 | Recipe CSS only matches `[data-color='X']` for X in the declared subset | warn | colored | +| ID | Rule | Severity | Applicability | Enforcement | +| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | ----------- | +| G-1.1 | README declares `## Subset` listing which `color` values + which `intent` values the component accepts (per GUIA_IMPLEMENTACION_SEMAUIX §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 | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| G-2.1 | Events that animate exit before structural commit use `sequence: 'pre'` (dismiss, close-cancel, etc.) | warn | interactive | -| G-2.2 | Events that confirm a result use `sequence: 'post'` (commit-save, submit, etc.) | warn | interactive | -| G-2.3 | Continuous progress events use `sequence: 'coincident'` (sustain.progress) | warn | passive | +| 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 | --- @@ -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). diff --git a/src/uix/active_architecture.md b/src/uix/active_architecture.md index 501fa97e5..48f86a83a 100644 --- a/src/uix/active_architecture.md +++ b/src/uix/active_architecture.md @@ -608,19 +608,12 @@ hay double-write. Dos vocabulary estables anclan la articulación: -**Archetypes** (`src/uix/morfo/types.ts:ARCHETYPE_VOCABULARY`): - -``` -provider · trigger · content · overlay · viewport -item · option · indicator · thumb · track -label · title · description · close · action -header · image · fallback · arrow · separator -group · input · segment · preview -``` - -24 categorías de parte que aparecen en múltiples componentes. Una `Trigger` -de Dialog, Popover, DropdownMenu y Tooltip son la misma categoría — Eidos -las puede estilar transversalmente con `[data-archetype=trigger]`. +**Archetypes** — categorías de parte que aparecen en múltiples componentes. +El inventario canónico es la const `ARCHETYPE_VOCABULARY` +(`src/uix/morfo/types.ts`) — no se copia aquí: la lista copiada divergió +(quedó en 24 cuando el código ya tenía 26). Una `Trigger` de Dialog, +Popover, DropdownMenu y Tooltip son la misma categoría — Eidos las puede +estilar transversalmente con `[data-archetype=trigger]`. **Verbs** (`src/uix/sema/verbs.ts:SEMA_VERBS`), agrupados por familia: diff --git a/src/uix/morfo/README.md b/src/uix/morfo/README.md index 9e88f32c6..dd0602098 100644 --- a/src/uix/morfo/README.md +++ b/src/uix/morfo/README.md @@ -178,19 +178,14 @@ The runtime emits `data-archetype="..."` on the part's DOM element via `partProps`. Static identity (never mutates), so it ships through `partProps`, not through `dom.apply`. -### Vocabulary (24 archetypes) - -``` -provider · trigger · content · overlay · viewport -item · option · indicator · thumb · track -label · title · description · close · action -header · image · fallback · arrow · separator -group · input · segment · preview -``` - -Defined in [`types.ts:ARCHETYPE_VOCABULARY`](./types.ts) and validated by -the sium schema. Optional field — omit when a part is genuinely unique to -its component (`Slider.Range`, `PinInput.Segment` internals). +### Vocabulary + +The canonical inventory is the `ARCHETYPE_VOCABULARY` const in +[`types.ts`](./types.ts) — the single source; do not copy the list into +prose (a copied list here survived at 24 entries while the code grew to +26). Validated by the sium schema. Optional field — omit when a part is +genuinely unique to its component (`Slider.Range`, `PinInput.Segment` +internals). ### Provider as trigger vs container diff --git a/src/uix/sema/README.md b/src/uix/sema/README.md index b6b91ccae..ae9559639 100644 --- a/src/uix/sema/README.md +++ b/src/uix/sema/README.md @@ -141,7 +141,7 @@ y dispatcha `(signal, effective)` a cada canal. Cada canal lee su slice ignora el signature si no lo usa. Solo el canal visual bloquea al caller con el hold perceptivo; los demás son fire-and-forget. -### Resolver y sema-map — cascada de 5 capas +### Resolver y sema-map — la cascada de resolución El engine, en cada `emit`: @@ -149,23 +149,26 @@ El engine, en cada `emit`: semánticos `data-event`, `data-event-family`, `data-event-intent`, `data-event-phase`, `data-event-id` al `signal.target`. Estos son los tokens que la cascade y la CSS de eidos leen. -2. Llama a `resolveSignature(signal, opts)` que aplica la cascada de 5 - capas (cada una sobreescribe la anterior): +2. Llama a `resolveSignature(signal, opts)` que aplica la cascada + (numeración canónica **1 · 2 · 3 · 4 · 5a · 5b** — la misma en + `engine.ts`, `resolver.ts` y CLAUDE.md; cada capa sobreescribe la + anterior): ``` -1. FAMILY base — SEMA_MAP.families[signal.family].base - sound / haptic + activeChannels + hold -2. INTENT deltas — SEMA_MAP.intents[signal.intent] cuando exista; - numbers ADD por defecto — son modificadores) -3. MORFO overrides — signal.overrides + signal.channels - (numbers REPLACE por defecto — son set values) -4. RUNTIME overrides — engineOpts.overrides.runtime (path-based globals; - baked into el map en el constructor; numbers REPLACE) -5. CASCADE rules — engineOpts.components (per-component packs prepended) - + engineOpts.overrides.cascade (app-level appended; - gana en empate de specificity por declaration order). - Selectors CSS-like contra signal.target con los - data-event-* ya stampados; numbers REPLACE. +1. FAMILY base — SEMA_MAP.families[signal.family].base + sound / haptic + activeChannels + hold +2. INTENT deltas — SEMA_MAP.intents[signal.intent] cuando exista; + numbers ADD por defecto — son modificadores) +3. MORFO overrides — signal.overrides + signal.channels + (numbers REPLACE por defecto — son set values) +4. RUNTIME overrides — engineOpts.overrides.runtime (path-based globals; + baked into el map en el constructor; numbers REPLACE) +5a. PACK cascade — engineOpts.components (per-component packs) +5b. APP cascade — engineOpts.overrides.cascade (appended tras los + packs; gana en empate de specificity por + declaration order). Selectors CSS-like contra + signal.target con los data-event-* ya stampados; + numbers REPLACE. ``` 3. Despacha a cada canal con la signature resuelta. @@ -176,7 +179,7 @@ El engine, en cada `emit`: - **Capa 2 (intent.deltas)**: `pitch: -200` significa "restar 200 al pitch base". Modificadores compositivos. -- **Capas 3, 4, 5 (overrides)**: `pitch: 720` significa "set pitch a +- **Capas 3, 4, 5a/5b (overrides)**: `pitch: 720` significa "set pitch a 720". Como CSS — `gain: 0.4` no añade, asigna. - Para sumar explícitamente desde una capa de override: `{ op: 'add', value: 100 }`. @@ -444,17 +447,17 @@ const semantic = new EngineSemantic({ sound: true, haptic: true, - // Capa 6a — packs de componentes (defaults shipped con cada componente) + // Capa 5a — packs de componentes (defaults shipped con cada componente) components: [dialogSema /* , toastSema, drawerSema, … */], overrides: { - // Capa 5 — edits puntuales del SEMA_MAP, válidos a TODA la app + // Capa 4 — edits puntuales del SEMA_MAP, válidos a TODA la app runtime: { 'families.commit.base.sound.pitch': 850, 'intents.threat.deltas.haptic.intensity': 0.4 }, - // Capa 6b — rules CSS-like de la app, matched contra signal.target + // Capa 5b — rules CSS-like de la app, matched contra signal.target // AFTER de los packs de componente cascade: [ { @@ -474,9 +477,9 @@ const semantic = new EngineSemantic({ ``` ```ts -// Capa 4 — per-event override declarado en la propia morfo del componente. +// Capa 3 — per-event override declarado en la propia morfo del componente. // Propaga vía SomaRuntime → SemanticSignal → resolver. La app puede -// seguir sobreescribiendo desde la cascade (capa 6). +// seguir sobreescribiendo desde la cascade (capas 5a/5b). // src/uix/morfo/components/dialog.ts { diff --git a/src/uix/sema/engine.ts b/src/uix/sema/engine.ts index ee7b881bb..99a6eb4b5 100644 --- a/src/uix/sema/engine.ts +++ b/src/uix/sema/engine.ts @@ -16,8 +16,9 @@ * 3. morfo per-event — signal.overrides + signal.channels * 4. runtime path overrides — engineOpts.overrides.runtime * (applied to map at construction) - * 5. cascade rules — engineOpts.components (per-component - * packs prepended) + engineOpts.overrides.cascade + * 5a. pack cascade — engineOpts.components (per-component + * packs, prepended) + * 5b. app cascade — engineOpts.overrides.cascade * (app-level rules appended; win on tie) */ @@ -68,7 +69,7 @@ export interface EngineSemanticOptions { */ components?: readonly Sema[]; - /** Override layers 5 (runtime path) + 6b (app cascade). */ + /** Override layers 4 (runtime path) + 5b (app cascade). */ overrides?: EngineSemanticOverrides; /** @@ -119,10 +120,10 @@ export class EngineSemantic { constructor(opts: EngineSemanticOptions = {}) { this.logger = opts.logger; - // Layer 5 — runtime path overrides baked into the map at construction. + // Layer 4 — runtime path overrides baked into the map at construction. this.map = applyMapOverrides(SEMA_MAP, opts.overrides?.runtime); - // Layer 6 — flatten per-component packs (lower precedence) + + // Layers 5a/5b — flatten per-component packs (lower precedence) + // app-level cascade (higher precedence on tie). const packCascade: SemaCascadeRule[] = []; const preloadUrls = new Set(); @@ -213,7 +214,7 @@ export class EngineSemantic { const id = signal.id ?? `sig-${this.nextSignalId++}`; const enriched: SemanticSignal = { ...signal, id }; - // Explicit silence (capa 4 morfo override): `signal.channels: []` + // Explicit silence (capa 3 morfo override): `signal.channels: []` // skips EVERYTHING — no projection, no dispatch, no hold. The morfo // declared this event has no perceptual surface. Other paths to an // empty `effective.activeChannels` (e.g. signal with no family — diff --git a/src/uix/sema/resolver.test.ts b/src/uix/sema/resolver.test.ts index 8cc140259..e277b7443 100644 --- a/src/uix/sema/resolver.test.ts +++ b/src/uix/sema/resolver.test.ts @@ -196,7 +196,7 @@ describe('resolveSignature', () => { expect(next.intents).toBe(SEMA_MAP.intents); }); - // ── Layer 6 — cascade rules ──────────────────────────────────────────── + // ── Layers 5a/5b — cascade rules ─────────────────────────────────────── /** * Minimal mock — resolver only calls `target.matches(selector)` and diff --git a/src/uix/sema/resolver.ts b/src/uix/sema/resolver.ts index 50f38edce..67ac2c3dc 100644 --- a/src/uix/sema/resolver.ts +++ b/src/uix/sema/resolver.ts @@ -11,7 +11,11 @@ * 4. RUNTIME overrides — `options.runtimeMap` already has * `engineOpts.overrides.runtime` baked in by the * engine via `applyMapOverrides()` at construction - * 5. CASCADE rules — `options.cascade` — CSS-style rules matched + * 5a. PACK cascade — per-component packs (`engineOpts.components`), + * 5b. APP cascade — app-level rules (`engineOpts.overrides.cascade`), + * appended after the packs so they win on equal + * specificity. Both arrive pre-flattened in + * `options.cascade`: CSS-style rules matched * against `signal.target` AFTER the engine has * stamped `data-event-*` on it (see `stamp.ts`). * Rules are flat: `{ selector, priority?, @@ -158,9 +162,10 @@ export function resolveSignature( signature = applyOverride(signature, signal.overrides); } - // Layer 5 — CSS-style cascade. Rules are matched via target.matches() - // — channel prepare hooks have already projected `data-event-*` on the - // target, so selectors that read those tokens match correctly. + // Layers 5a/5b — CSS-style cascade (packs + app rules, pre-flattened by + // the engine). Rules are matched via target.matches() — channel prepare + // hooks have already projected `data-event-*` on the target, so + // selectors that read those tokens match correctly. if (options.cascade && options.cascade.length > 0 && signal.target) { const matched = collectCascadeMatches(options.cascade, signal.target); for (const rule of matched) { @@ -230,7 +235,7 @@ function ruleAsOverride(rule: SemaCascadeRule): SemaSignatureOverride { return override as SemaSignatureOverride; } -// ── Override application (layers 4 + 6) ─────────────────────────────────── +// ── Override application (layers 3 + 5a/5b) ─────────────────────────────── // // Overrides use REPLACE-by-default for primitive leaves (numbers, strings, // booleans). Matches CSS intuition: `sound: { gain: 0.4 }` SETS gain to @@ -356,7 +361,7 @@ function applyLeaf( return out; } -// ── Runtime path-based map mutation (layer 5) ───────────────────────────── +// ── Runtime path-based map mutation (layer 4) ───────────────────────────── /** * Apply path-based runtime overrides to the SemaMap, producing a new map diff --git a/src/uix/soma/README.md b/src/uix/soma/README.md index cd3cf2492..659fbe22e 100644 --- a/src/uix/soma/README.md +++ b/src/uix/soma/README.md @@ -73,13 +73,19 @@ soma solo depende de: - svelte (runes: $state, $derived, $effect) - runed (Context, watch) -- clsx (class merging) -- @floating-ui (positioning) +- tabbable (focus order) - `$libs/reactive`, `$libs/days`, `$libs/datagrid`, `$libs/forms`, etc. — utilidades puras del repo (no façades) - `$uix/morfo` — el contrato cross-layer (compileMorfo + SomaRuntime) - `$uix/sema` — vocabulario semántico + EngineSemantic +El posicionamiento flotante es motor propio (`layers/floating` + +`$ethereal`); `@floating-ui` quedó como devDependency (demo + parity +tests), no se importa en la librería. `clsx` NO está declarada en +`package.json` pero `props/props.ts` aún la importa (resuelve como +transitiva de svelte — deuda: declararla o inlinearla). La lista +autoritativa es `package.json > dependencies`. + soma NO depende de eidos. La capa visual lee del DOM y de los tipos públicos del soma; la dirección del acoplamiento es eidos → soma, no al revés. diff --git a/src/uix/soma/SOMA_ARCHITECTURE.md b/src/uix/soma/SOMA_ARCHITECTURE.md index 21cfbf66d..6e2488a86 100644 --- a/src/uix/soma/SOMA_ARCHITECTURE.md +++ b/src/uix/soma/SOMA_ARCHITECTURE.md @@ -39,7 +39,7 @@ Cada capa tiene responsabilidades estrictas: - contexto y composicion de partes - servicios de runtime (`langs`, `format`, `logger`) - sistema de animaciones (presence, data-starting/ending-style, onComplete) -- posicionamiento flotante (@floating-ui) +- posicionamiento flotante (motor propio: `layers/floating` + `$ethereal`) ### La capa visual aporta @@ -423,11 +423,16 @@ Dos interfaces de opts: | `TextSelection` | `TextSelection.use(opts)` | Previene selection overflow durante drag. | | `ScrollLock` | `new ScrollLock(initial?, delay?)` | Body scroll lock con refcount. Soporta delay para animaciones. | | `ResizeObserver$` | `new ResizeObserver$(getter, cb)` | ResizeObserver con lifecycle Svelte. | -| `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). | | `Gesture.base` | `Gesture.base(opts)` | Pointer tracking + axis lock + velocity. | | `Gesture.drag` | `Gesture.drag(opts)` | Base + progress + snap points + dismiss. | | `Gesture.resize` | `Gesture.resize(opts)` | Base + delta + min/max constraints. | -| `SafePolygon` | `new SafePolygon(opts)` | Hover-gap corridor between trigger↔content. | +| `SafePolygon` | `new SafePolygon(opts)` (`floating/safe-polygon.ts`) | Hover-gap corridor between trigger↔content. | +| `Stacking` | registry module-level (`stacking.svelte.ts`) | Z-order compartido de superficies movibles (FloatPanel): `bringToFront`, `data-topmost`/`data-behind`. | +| `AxialDrag` | `new AxialDrag(opts)` (`manipulation/`) | Drag axial (un eje) con snap points + release state. | +| `ZoomPan` | `new ZoomPan(config)` | Scale + pan de contenido en viewport fijo (Cropper); estado + math puros. | +| `ImageProvider` | `new ImageProvider(opts)` | Estado de carga de imagen (`idle/loading/loaded/error`) con delay. | +| `ListSelection` | funciones puras (`list-selection.ts`) | Máquina de selección single/multi + `allowDeselect` compartida por Select/Combobox. | `layers/floating/placement.ts` es la fuente unica para `Side`, `Align`, `Boundary`, `SIDE_OPTIONS` y `ALIGN_OPTIONS`. `floating/types.ts` consume esa @@ -894,9 +899,16 @@ import { Portal, Arrow, VisuallyHidden, Soma } from '$soma/components/internal'; `soma` distingue entre: - internos: `layers/`, `reactive/`, `dom/`, `provider/` — helpers propios -- externos: `@floating-ui/dom`, `runed`, `tabbable` — dependencias npm - -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. ## 13. Estructura del directorio diff --git a/src/uix/soma/components/link-preview/README.md b/src/uix/soma/components/link-preview/README.md index 2a6c52815..cb1ef72f9 100644 --- a/src/uix/soma/components/link-preview/README.md +++ b/src/uix/soma/components/link-preview/README.md @@ -1,6 +1,6 @@ # LinkPreview -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 | ---------- | ------- | --------------------------------------------------------------- | | `Provider` | none | Root context. Manages open state, delays, and pointer tracking. | | `Trigger` | `` | Link element. Opens preview on pointer hover. | -| `Content` | `

` | Floating preview card. Positioned via @floating-ui. | +| `Content` | `
` | Floating preview card. Positioned via the floating layer. | | `Arrow` | `` | Optional arrow pointing toward the trigger. | ## ARIA diff --git a/src/uix/soma/components/popover/README.md b/src/uix/soma/components/popover/README.md index 4a9003aad..d5b7482ee 100644 --- a/src/uix/soma/components/popover/README.md +++ b/src/uix/soma/components/popover/README.md @@ -1,6 +1,6 @@ # Popover -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. ## Anatomy diff --git a/src/uix/soma/runtime.svelte.ts b/src/uix/soma/runtime.svelte.ts index 69544c1aa..229b2f97b 100644 --- a/src/uix/soma/runtime.svelte.ts +++ b/src/uix/soma/runtime.svelte.ts @@ -285,7 +285,7 @@ export interface TriggerOptions { * declared in `semantic.overrides`; on key conflict, the per-call value * wins (per-call > morfo-declared). * - * Cascade rules (capa 6) STILL win on conflict — a cascade rule that + * Cascade rules (capas 5a/5b) STILL win on conflict — a cascade rule that * sets `sound.pitch` will override a per-call pitch. To make a per-emit * value the source of truth, the cascade rule should NOT set the same * primitive (only set `channels` to activate the channel). @@ -323,13 +323,14 @@ export interface TriggerOptions { message?: string; /** * Polymorphic event concretion (book §5.3). When the morfo event - * declares `allowedFamilies` + `defaultSemantic`, the caller can - * commit to a concrete shape here. Validated at runtime against + * declares `allowedFamilies` (its regular `family`/`verb` act as the + * default shape — additive design, LIBRO_VARIACIONES D.11), the caller + * can commit to a concrete shape here. Validated at runtime against * `allowedFamilies` — passing a family outside the allowlist raises * `SomaRuntimePolymorphicError`. * - * Ignored for non-polymorphic events (those that declare a concrete - * `family`); pass-through warns via logger if present. + * Silently ignored for non-polymorphic events (those without + * `allowedFamilies`). */ semantic?: { family: SemaFamily; @@ -673,7 +674,7 @@ export function createSomaRuntime(morfo: Morfo, sources: SomaRuntimeSources): So const family = resolvedFamily; // Intent comes from the resolved morfo declaration (concrete // `intent` field for non-polymorphic, or the polymorphic - // override / defaultSemantic for polymorphic). Components that + // override / declared default for polymorphic). Components that // want a consumer prop to flow through declare `intent: // { fromProp: 'intent', … }` on the event. Transitional families // (emerge / shift / sustain) MAY declare intent now (canon