fix(morfo+docs): approved one-liner pass — delegation/apg declarations + doc drift (P7 close-out)

- natural-time-picker: expression 'delegated' (canon S11; composed children
  own every event — clears A-3.1 truthfully; the Picker-generico event debt
  stays tracked in its dossier Gaps for the S9 package).
- s-text-virtual-list: drop archetype 'item' from Row (0-event display
  surface; item pulled cursor/user-select/hover + a height floor fighting
  the canvas-computed row heights, and flipped the classifier to
  interactive -> spurious R-1.5). Verified live: rows keep canvas heights,
  no overlap, text selectable. Machine now classifies passive (F-1.5
  justification already in the dossier).
- time-range-field / field-langs / link-preview: apg declared (spinbutton /
  C5 'none — rationale' x2) — clears the three A-1.4 warns.
- button: soma README realigned to the real types (loading/intent/color —
  the old pending/pressed/expanded/href surface no longer exists) + the two
  stale docblocks from the audit ficha (types aria-label layer leak, wrapper
  data-state mention).
- password-field: morfo StrengthMeter comment aligned with types (default
  $libs/secs scoring, overridable) + index.ts example updated to the
  post-S6 Control composition.
- Affected dossiers' Gaps tables updated (implemented rows removed).

Verified: component-audit 102 PASS (A-3.1/R-1.5-artifact/A-1.4 x3 cleared),
vocab guard exit 0, type baseline 57, eidos contract tests 7/7, stvl checked
in browser. Note: compile.test.ts 'hoists role into staticAttrs' (dialog)
fails on HEAD pre-existing — unrelated to this pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 20e2e7a1f0
commit dd0637f65b

@ -117,7 +117,6 @@ Referencias: [APG Button](https://www.w3.org/WAI/ARIA/apg/patterns/button/) ·
| Gap | Disposición | Detalle |
| --- | --- | --- |
| `Spinner` declara `archetype: 'item'` siendo parte display (ficha F-1: item arrastra estilo interactivo) | **implementar** | Quitar el archetype (doctrina de partes display) + verificación visual de los 3 placements. |
| README soma drifted (documenta `pending`/`pressed`/`expanded`/`href`; los types reales son `loading`/`intent`/`color`/…) + docblocks stale (soma `aria-label` cita `iconOnly` de eidos — F-2; wrapper cita `data-state` — F-3) | **implementar** | Realinear README soma + los 2 docblocks a los types actuales. |
| Cero tests de provider/wrapper (ficha F-7: el buque insignia sin suite) | **implementar** | Suite browser: click→contact-activate, gate disabled/loading, Field-merge, cascada intent/color, placements del spinner — pasada SYS-2. |
| API extendida sobre las referencias (más variantes, `href`→`<a>`) | **diferir** | `Link` posee la navegación; más variantes requieren caso real. |

@ -3,8 +3,9 @@
/**
* Eidos `<Button>` — visual wrapper over Soma's headless button.
*
* Soma owns the click flow, `data-state`, `aria-busy`, `aria-disabled`
* and the `contact-activate` perceptual event (book cap. 22 §10).
* Soma owns the click flow, the `data-loading` / `data-disabled`
* presence flags, `aria-busy`, `aria-disabled` and the
* `contact-activate` perceptual event (book cap. 22 §10).
* Eidos adds the visual API on top — `variant`, `size`, `rounded`,
* `block`, `iconOnly`, the `icon` / `endIcon` snippet slots, and the
* loading Spinner — and wires those to the `data-*` selectors that

@ -155,6 +155,6 @@ Presented for scope sign-off — each row below carries an explicit disposition.
- `R-1.4 exception:` the invalid visual is owned by the composed `Field`
(FieldLangs passes `invalid`); there is no FieldLangs-level `[data-invalid]`
rule to add.
- `A-1.4 exception:` FieldLangs is a composite CMS pattern with no single APG
pattern; the composed `Field` (textbox) and the switcher (a WAI-ARIA
radiogroup, in both modes) carry the formal patterns.
- ~~`A-1.4 exception:`~~ superseded 2026-07-10: A-1.4 no longer honors README
exceptions (checkpoint C5) — the rationale now lives in the morfo itself as
`apg: 'none — composite CMS pattern …'`.

@ -66,7 +66,6 @@ Referencias: [Bits UI Link Preview](https://bits-ui.com/docs/components/link-pre
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Sin cita `apg:` (warn A-1.4) | **implementar** | One-liner en forma C5: `apg: 'none — pointer-only enhancement; the anchor carries the link semantics'` — mismo cajón que el apg de trf. |
| Fetch/caché de metadatos de URL integrado | **descartar** | El contenido del preview es del consumidor; UIX no hace red. |
| Touch (long-press para abrir) | **diferir** | En coarse el preview no aplica hoy (hover-only); esperar caso real. |

@ -97,7 +97,7 @@ Referencias: [MUI X TimePicker](https://mui.com/x/react-date-pickers/time-picker
| Gap | Disposición | Detalle |
| --- | --- | --- |
| El footer transaccional no tiene semántica propia: delega en el `Picker` genérico cuyo morfo declara 0 eventos (deuda RECONOCIDA in-place en `picker.ts`) — el "Listo" de un picker suena a Button genérico (A-3.1 de la máquina) | **implementar** (en el Picker genérico, iniciativa S9 "ntp integral" + veredicto picker) | `commit-set` fulfill en Accept / `commit-cancel` neutral / `commit-clear` neutral EN el Picker; ntp y los 5 pickers a migrar lo heredan gratis. NO declarar eventos propios aquí — rompería la delegación correcta. Mientras: valorar el one-liner `expression: 'delegated'` (canon S11) que silenciaría A-3.1 diciendo la verdad. |
| El footer transaccional no tiene semántica propia: delega en el `Picker` genérico cuyo morfo declara 0 eventos (deuda RECONOCIDA in-place en `picker.ts`) — el "Listo" de un picker suena a Button genérico | **implementar** (en el Picker genérico, iniciativa S9 "ntp integral" + veredicto picker) | `commit-set` fulfill en Accept / `commit-cancel` neutral / `commit-clear` neutral EN el Picker; ntp y los 5 pickers a migrar lo heredan gratis. NO declarar eventos propios aquí — rompería la delegación correcta. (El morfo ya declara `expression: 'delegated'` — canon S11, 2026-07-10 — así que A-3.1 ya no flaggea; la deuda de fondo sigue siendo del Picker.) |
| 9 hex sin anotación (R-2.1) — los cielos physically-fixed | **implementar** (S9 ntp integral) | Anotar cada línea `/* literal: physically-fixed sky/astro color */`; NO tokenizar (ficha F-2). |
| `data-readonly`/`data-invalid` declarados sin estilo (R-1.3/R-1.4) | **implementar** | Censo de familia pickers. |
| README soma ausente + 0 tests | **implementar** (S9 ntp integral — "iniciativa a medias", SYS-9) | Contrato headless + suite (banda↔valor, chips, defer commit/cancel). |

@ -114,8 +114,6 @@ Input.Password (survey del morfo).
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Cero tests del contrato más rico de la familia (stateBound del caps + clearSignal, toggle+aria-pressed, strength plugable) | **implementar** | Suite prioritaria (pasada SYS-2): el stateBound del caps es exactamente lo que una regresión rompería en silencio. |
| Comentario del morfo stale ("UIX does not bundle a strength algorithm" — los types documentan default `$libs/secs`) | **implementar** | Alinear el comentario del morfo con los types (una línea). |
| Docblock de `index.ts` pre-S6 (ejemplo sin `Control`; el morfo lo declara `optional: false`) | **implementar** | Actualizar el ejemplo al canónico con `Control` (como la demo). |
| API extendida sobre las referencias | **diferir** | Superficie estable; consenso 5-refs ya cubierto. |
## Audit exceptions

@ -4,11 +4,17 @@
// import { PasswordField } from '$uix/eidos/components/password-field';
//
// <PasswordField bind:value purpose="new">
// <PasswordField.Input placeholder="At least 8 characters" />
// <PasswordField.VisibilityTrigger />
// <PasswordField.CapsLockIndicator />
// <PasswordField.StrengthMeter strength={score} />
// <PasswordField.Control>
// <PasswordField.Input placeholder="At least 8 characters" />
// <PasswordField.CapsLockIndicator />
// <PasswordField.VisibilityTrigger />
// </PasswordField.Control>
// <PasswordField.StrengthMeter />
// </PasswordField>
//
// The Control is the bordered field box (S6: password-field IS-A Field);
// the StrengthMeter sits below it as a stack sibling and reads its score
// from the provider.
import PasswordFieldComponent from './password-field.svelte';
import Control from './password-field-control.svelte';
import Input from './password-field-input.svelte';

@ -18,8 +18,10 @@ Superficie de 0 eventos (header del morfo: "0-event surface") y
`scope: ['eidos']` — composición display de dos piezas del sistema (SText ×
windowing). El scroll del usuario no se modela aquí: no hay semántica que
delegar porque las filas son texto plano (`role="list"`/`"listitem"`), no
ítems accionables. *(Nota: la máquina hoy la clasifica "interactive" por el
`archetype: 'item'` de Row — ver Gaps.)*
ítems accionables. La Row NO declara archetype a propósito (razón in-place
en el morfo, 2026-07-10): `item` arrastraría cursor pointer, user-select
none, acento de hover y un suelo de altura que pelearía con la altura
computada por canvas.
## Baseline
@ -72,7 +74,6 @@ Engine propio: [`../../lib/canvas-text/`](../../lib/canvas-text/).
| Gap | Disposición | Detalle |
| --- | --- | --- |
| `Row` declara `archetype: 'item'` siendo display (`role="listitem"`, 0 eventos) — el gotcha conocido: item arrastra estilo interactivo Y vuelca el clasificador de la máquina a "interactive", lo que a su vez exige el focus R-1.5 que un display no tiene | **implementar** | One-liner: quitar el archetype de Row (doctrina de partes display). Con ello la clasificación pasa a passive (este README ya trae la justification) y el R-1.5 deja de aplicar. Decisión de morfo — mismo cajón que los one-liners de trf/ntp. |
| Selección/copia de filas (log viewer) | **diferir** | Convertiría la superficie en interactiva de verdad — sería otro componente o una composición; esperar caso. |
| Sticky headers de sección | **diferir** | Con virtual-list (pass conjunto si llega). |

@ -102,7 +102,6 @@ delegación per-endpoint, misma Label compartida, mismo separador.
| Gap | Disposición | Detalle |
| --- | --- | --- |
| El `data-invalid` de RANGO no pinta (end < start valida en soma y marca el root, pero los endpoints solo reciben `disabled/readonly/required` — no el invalid de rango) | **implementar** | Espejo del gap del gemelo drf ("per-endpoint coloring"): propagar el invalid de rango a los endpoints o tintar desde el root. Mismo fix para ambos ranges (ficha F-3/F-1). |
| Sin cita `apg:` en el morfo (el gemelo y el single citan spinbutton) — warn A-1.4 | **implementar** | Una línea: `apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/'`. |
| Sin evento de rango-completo (ambos endpoints fijados) | **diferir** | Decisión conjunta con date-range-field (ficha F-1 compartida); hoy el commit per-endpoint es el contrato. |
| Tests del wrapper de composición | **implementar** | Pasada SYS-2 (rango inválido, readonly segments por endpoint, placeholder compartido). |

@ -31,6 +31,7 @@ export const fieldLangsMorfo = {
// The switch-press tap comes from the composed ToggleGroup's own pack — no
// per-component tuning needed here.
expression: 'family-default',
apg: 'none — composite CMS pattern with no single APG entry; the composed Field carries textbox and the switcher is a WAI-ARIA radiogroup in both modes',
texts: {
label: '#?components.field-langs.label|Localized field',
'switcher-label': '#?components.field-langs.switcher-label|Select language'

@ -9,6 +9,7 @@ export const linkPreviewMorfo = {
// would be noise. The trigger is a native anchor — its semantics (and any
// Link expression) own the interaction (checkpoint fix, 2026-07-07).
expression: 'delegated',
apg: 'none — pointer-only enhancement with no APG pattern (hover card); the native anchor carries the link semantics and stays fully functional without the preview',
texts: {
label: '#?components.link-preview.label|Link Preview'
},

@ -21,6 +21,10 @@ export const naturalTimePickerMorfo = {
name: 'NaturalTimePicker',
kebab: 'natural-time-picker',
scope: ['soma', 'eidos'],
// Composed children own every event (header above): TimeField commits the
// value, the generic Picker hosts the transactional footer, Popover owns
// open/close. Canon S11 — same declaration as the two field-ranges.
expression: 'delegated',
// The composed Popover owns the dialog pattern (same as date-picker §2);
// the daylight band follows the composed Slider's conventions.
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/',

@ -233,9 +233,10 @@ export const passwordFieldMorfo = {
aria: [{ attr: 'aria-live', value: v.literal('polite') }]
},
{
// Composable strength meter — consumer provides a 0–4 score.
// UIX does not bundle a strength algorithm (zxcvbn etc.) to stay
// dependency-free; consumer wires whichever library they trust.
// Composable strength meter. Default scoring is the framework's own
// `passwordStrength` from `$libs/secs` (dependency-free); consumers
// override via `calculateStrength` (zxcvbn, OWASP, server-side) —
// see soma types.
name: 'StrengthMeter',
kebab: 'strength-meter',
archetype: 'indicator',

@ -54,9 +54,13 @@ export const sTextVirtualListMorfo = {
aria: []
},
{
// Display row — NO archetype on purpose: `item` would pull the
// transversal interactive styling (cursor: pointer, user-select:
// none, hover accent, control-height floor) onto plain text rows
// whose height the canvas engine already computed, and it flips
// the audit classifier to "interactive" (0-event display surface).
name: 'Row',
kebab: 'row',
archetype: 'item',
kind: 'public',
defaultElement: 'div',
role: 'listitem',

@ -8,6 +8,9 @@ export const timeRangeFieldMorfo = {
// Sema events forwarded by the composed TimeField instances (one per
// endpoint) — same delegation as date-range-field.
expression: 'delegated',
// Per-segment spinbutton — the same pattern the single time-field and the
// date-range-field twin cite.
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/',
texts: {
label: '#?components.time-range-field.label|Time Range Field',
'start-label': '#?components.time-range-field.start-label|Start time',

@ -1,53 +1,63 @@
# Button
Single-action `<button type="button">` (or `<a>` when `href` is provided) with pressed / loading / disabled state and pending UI helpers. Soma owns behavior; the eidos wrapper layers icons + visual recipes on top.
Single-press action surface — a native `<button>` with loading / disabled
state, a click flow gated on both, and the `contact-activate` perceptual
event. Soma owns behavior; the eidos wrapper layers icons + visual recipes
on top. (Links are `<Link>`'s job — Button has no `href` form; compose via
the `child` snippet when another element must carry the behavior.)
## Anatomy
```svelte
<Button.Provider intent="primary" onclick={save}>
<Button.Provider color="primary" onclick={save}>
Save
</Button.Provider>
<Button.Provider intent="threat" pending={isSaving}>
<Button.Spinner />
Save
<Button.Provider intent="threat" loading={isDeleting} loadingText="Deleting…">
Delete
</Button.Provider>
```
## Parts
| Part | Element | Description |
| ---------- | ------------------- | ---------------------------------------------------------------------- |
| `Provider` | `<button>` / `<a>` | Root. Switches to `<a>` when `href` is set; otherwise stays `<button>`. |
| `Spinner` | `<span>` | Loading indicator. Render alongside children; visible while `pending`. |
| Part | Element | Description |
| ---------- | ---------- | --------------------------------------------------------------------------- |
| `Provider` | `<button>` | Root. Native button; Enter/Space activate natively. `child` for asChild. |
| `Spinner` | `<span>` | Loading indicator. Auto-rendered by the Provider while `loading`; render your own for full control. |
## Props
### `Provider`
| Prop | Type | Default | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------- |
| `intent` | `'primary' \| 'secondary' \| 'neutral' \| 'affirm' \| 'fulfill' \| 'risk' \| 'threat' \| 'loss'` | `'primary'` | Semantic intent. Drives sema event family and the eidos colour recipe. |
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type. Ignored when `href` is set. |
| `href` | `string` | — | When set, renders as `<a>` instead of `<button>`. |
| `disabled` | `boolean` | `false` | Disables interaction (sets `aria-disabled` on `<a>`, `disabled` on button). |
| `pending` | `boolean` | `false` | Loading state. Sets `aria-busy="true"` and `data-pending=""`. |
| `pressed` | `boolean` | — | Toggle pressed state. Sets `aria-pressed`. |
| `expanded` | `boolean` | — | For disclosure buttons. Sets `aria-expanded`. |
| `controls` | `string` | — | DOM id controlled by the button. Sets `aria-controls`. |
| `aria-label` | `string` | — | Accessible name when the button has no visible text (e.g. icon-only). |
| `onclick` | `(e: MouseEvent) => void` | — | Click handler. Standard DOM event. |
## ARIA
| Attribute | Value |
| --------------- | ----------------------------------------------------------- |
| `aria-busy` | `"true"` while `pending` |
| `aria-disabled` | When `<a>` form + `disabled` |
| `aria-pressed` | When `pressed` is boolean |
| `aria-expanded` | When `expanded` is boolean |
| `aria-controls` | From `controls` prop |
| Prop | Type | Default | Description |
| ------------------ | --------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------- |
| `intent` | `'neutral' \| 'affirm' \| 'fulfill' \| 'risk' \| 'threat' \| 'loss'` | `'neutral'` | Visual evaluative tint (chip + `data-color`). Does NOT load the sema event — see below. |
| `color` | `'primary' \| 'secondary' \| 'tertiary' \| 'neutral' \| (string & {})` | `'primary'` visual | Hierarchical accent; only applies under `intent='neutral'`. Any donor palette scale allowed (THEMING §25.5). |
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type (default avoids implicit form submit). |
| `disabled` | `boolean` | `false` | OR-merged with `Field.Provider`. Click flow gated. |
| `loading` | `boolean` | `false` | Gates the click flow (no `contact-activate`), sets `aria-busy`, renders the Spinner. |
| `loadingText` | `string` | — | Localised swap for children while loading (children stay sr-only). |
| `spinnerPlacement` | `'start' \| 'end'` | `'start'` | Where the spinner renders relative to children. |
| `aria-label` | `string` | — | Accessible-name override; required when the button has no visible text. |
| `onPress` | `OnChangeFn<{ intent: ButtonIntent }>` | — | Diagnostic feedback that `contact-activate` fired; the side effect is `onclick`. |
| `onclick` | `(e: MouseEvent) => void` | — | Click handler. Standard DOM event. |
Snippet props (`children` / `child`): `{ loading, disabled }`.
## ARIA & data attributes
| Attribute | Value |
| --------------- | ------------------------------------------------------------ |
| `aria-busy` | `"true"` while `loading` |
| `aria-disabled` | `"true"` while `disabled` |
| `aria-label` | From the `aria-label` prop when set |
| `data-color` | Resolved visual color (intent wins when evaluative) |
| `data-loading` | Presence flag while `loading` |
| `data-disabled` | Presence flag while `disabled` |
No `data-state`: the enum was removed after it clobbered host disclosures'
`data-state="open|closed"` when Button chromed their triggers (audit
2026-05-27) — binary states are presence flags.
## Sema events

@ -108,7 +108,10 @@ export type ButtonProps = WithChild<
* @default 'start'
*/
spinnerPlacement?: 'start' | 'end';
/** Override accessible name. Required when `iconOnly` is true. */
/**
* Accessible-name override. Required whenever the button renders no
* visible text (icon-only chrome, glyph-only triggers).
*/
'aria-label'?: string;
/**
* Called on every press for diagnostic / analytics purposes. The

Loading…
Cancel
Save

Powered by TurnKey Linux.