From 45ae36919a4733e6c81fb3b7e7d948de8293b5df Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 10 Jul 2026 20:31:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(gradient):=20S9=20gradient-*=20=E2=80=94?= =?UTF-8?q?=20graduate=20from=20ACTIVE=5FDEV=5FTRACK=20+=20apg=20+=20dossi?= =?UTF-8?q?ers=20(initiative=20close)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initiative's real remaining scope (verified 2026-07-10 against the repo; the "uncommitted work" memory was stale — builder landed in b3d0e290, picker in f5eb8acb): - contracts.test: remove gradient-builder/gradient-picker from ACTIVE_DEV_TRACK. The catalogue guards found exactly ONE violation — the missing soma barrel exports — now added (GradientBuilder, GradientPicker). Post-graduation: same 13 pre-existing failures (knob/menu-dial/metrics/ onion-menu debris from other WIP), zero gradient mentions. - apg declared on both morfos: builder cites slider (every stop + the angle dial IS a slider thumb — the keyboard-a11y differentiator); picker cites dialog-modal via the composed Popover (same rationale as date-picker/ntp). - Both eidos READMEs upgraded to dossier form (Baseline / Comparativa / Decisiones / Gaps) transplanting the CONTINUE.md competitive research (OKLCH model shared with the token engine; keyboard-editable stops — the field is mouse-only RGBA exporters). Stale "active dev track" recipe notes corrected. - gradient-builder demo: D-1.2 was a false negative — the tab union was the correct v2 9-tab set, just formatted multi-line; joined to one line (canary format). SSR smoke: 200 + 40 data-gradient-builder attrs. Remaining on both: D-1.5/D-3.1/D-4.3 — the catalog-wide demo-census (fails on 10/7 components INCLUDING the button canary), tracked in the dossiers' Gaps, not gradient-specific. Gates: component-audit 102 PASS · contracts 24 pass/13 pre-existing · vocab exit 0 · type baseline 57. Co-Authored-By: Claude Fable 5 --- src/uix/contracts.test.ts | 4 +- .../components/gradient-builder/README.md | 77 +++++++++++++++++-- .../components/gradient-picker/README.md | 59 ++++++++++++-- src/uix/morfo/components/gradient-builder.ts | 4 + src/uix/morfo/components/gradient-picker.ts | 4 + src/uix/soma/components/index.ts | 2 + .../components/gradient-builder/+page.svelte | 11 +-- 7 files changed, 135 insertions(+), 26 deletions(-) diff --git a/src/uix/contracts.test.ts b/src/uix/contracts.test.ts index efdd4b20f..d797b726e 100644 --- a/src/uix/contracts.test.ts +++ b/src/uix/contracts.test.ts @@ -170,9 +170,7 @@ function collectMorfoComponentNamesByScope(scope: string): string[] { const ACTIVE_DEV_TRACK = new Set([ 'words', 'palabras', - 'chronos', - 'gradient-builder', - 'gradient-picker' + 'chronos' ]); // SYS-1 scope-drift guard. `scope` enumerates the layers that IMPLEMENT a diff --git a/src/uix/eidos/components/gradient-builder/README.md b/src/uix/eidos/components/gradient-builder/README.md index fe65bb5d0..053cb9bac 100644 --- a/src/uix/eidos/components/gradient-builder/README.md +++ b/src/uix/eidos/components/gradient-builder/README.md @@ -1,5 +1,7 @@ # Eidos GradientBuilder +Fecha de revisión: 2026-07-10 (dossier S9 — graduado de `ACTIVE_DEV_TRACK`). + The visual layer over the headless [`GradientBuilder`](../../../soma/components/gradient-builder/README.md) (soma). A framed editor card that composes the framework's existing components — never re-implementing a primitive: the embedded **ColorPicker** (the selected @@ -7,6 +9,23 @@ stop's color, inline), **Slider** (the angle), **ToggleGroup** (the kind), **Button** (add stop). Soma owns the state machine + the keyboard-accessible stops; eidos positions, paints and frames. +## Baseline + +Sin baseline air y sin análogo real en el campo: el survey de editores +(documentado en el header del morfo + el research de +`soma/components/gradient-builder/CONTINUE.md`) encontró que TODOS los +incumbentes (Figma, Photoshop, los pickers web, gradient.style) almacenan +RGBA + geometría y exportan un string muerto, y todos son mouse-only. El +diferencial UIX es doble: + +1. **El modelo es el token**: edita el `Gradient` canónico de + `$libs/gradient` — el MISMO modelo que deriva el eje de theming + (`buildGradient`) — así que un gradiente construido aquí es también un + token tematizable (OKLCH, stops role-ref | oklch | css). +2. **Editable desde el teclado**: cada stop y el dial de ángulo son + `role="slider"` reales con `aria-valuetext`, roving focus y anuncios — + nadie más lo publica (apg: slider). + ## Surface ```svelte @@ -61,14 +80,58 @@ selected stop reads as one surface. The seed effect tracks only `selectedIndex` ## Recipe -Self-contained `--_gradient-builder-*` tokens (gradient-builder is on the active -dev track, not yet registered in `recipes/base.ts`). The stop handle centers with -the `translate` property (not `transform`) so a sema press-squeeze can't fight -the centering. Selectors target only morfo-promised attrs + the eidos-only -composition wrappers stamped by the wrappers. +Self-contained `--_gradient-builder-*` tokens — private by choice (graduated +from the dev track 2026-07-10; the R-rules pass with the private set, and a +public `--gradient-builder-*` API waits for real theming demand, see Gaps). +The stop handle centers with the `translate` property (not `transform`) so a +sema press-squeeze can't fight the centering. Selectors target only +morfo-promised attrs + the eidos-only composition wrappers stamped by the +wrappers. + +## Comparativa + +| Capacidad | UIX | Figma | Photoshop | Pickers web / gradient.style | +| --- | --- | --- | --- | --- | +| Modelo compartido con el token engine (OKLCH) | **✓** (`$libs/gradient`) | No (RGBA interno) | No | No (string CSS) | +| Stops editables por teclado (`role=slider`) | **✓** | No | No | No | +| Geometría explícita per-kind (angle / shape+size+at / from+at) | ✓ | Matriz afín | Matriz | Parcial | +| Alpha per-stop | ✓ | ✓ | ✓ | ✓ | +| Round-trip sin pérdida a CSS | ✓ | Export muerto | Export muerto | ✓ (string) | +| Semántica perceptiva del gesto (pack handle+commit) | ✓ | — | — | — | + +Referencias: research competitivo en +[`CONTINUE.md`](../../../soma/components/gradient-builder/CONTINUE.md) (no +re-derivar) · [APG Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) · +[gradient.style](https://gradient.style/). + +## Decisiones + +- **Geometría = campos explícitos per-kind**, no matriz afín: round-trip sin + pérdida a CSS y mapeo trivial a teclado (decisión del research; el morfo la + hereda). +- **Alpha per-stop en v1**; el rail de opacidad separado queda reservado, no + shipped. +- **ColorPicker INLINE** (Area + sliders + hex, sin popover anidado): editar + el stop seleccionado se lee como una sola superficie; el seed effect trackea + solo `selectedIndex` (untrack del color) para no pelear con el picker. +- **Compose-first cumplido**: ColorPicker / Slider / ToggleGroup / Button son + los del sistema; lo único bespoke es el track de stops (que ES el patrón + slider propio). +- **Pack sema**: handle pick/drag sobre el track (la firma del slider) + + commit-set/reset. +- **Sin prop `color`** — GradientBuilder pinta el gradiente del usuario, no un + rol de paleta; el palette-picker de la demo está ausente por diseño. + +## Gaps + +| Gap | Disposición | Detalle | +| --- | --- | --- | +| Mesh editor (puntos 2D) | **diferir** (fast-follow declarado) | El eje ya publica `aurora()`; el editor de mesh reutilizará el patrón stop=slider. Diamond descartado en el research. | +| Midpoints (color hints) + `parseGradient` (paste-to-edit) | **diferir** (Phase-2.5 declarada) | Con el research ya hecho en CONTINUE.md. | +| Gizmo on-canvas (superficie dual barra + lienzo) | **diferir** | v1 = la barra precisa; el gizmo espera el caso mesh. | +| API pública de tokens (`--gradient-builder-*` en recipes/base.ts) | **diferir** | Los privados pasan las R-rules; registrar cuando haya demanda real de theming del editor. | +| Demo v2 (D-1.2/D-3.1 de la máquina) | **implementar** | Pase de demos de esta iniciativa (ver sesión S9). | ## Notes -- **No `color` prop** — GradientBuilder paints the user's gradient, not a palette - role. The demo's palette picker is therefore absent by design. - Demo: `web/routes/uix/components/gradient-builder/+page.svelte`. diff --git a/src/uix/eidos/components/gradient-picker/README.md b/src/uix/eidos/components/gradient-picker/README.md index 5c5ffa167..384a45e88 100644 --- a/src/uix/eidos/components/gradient-picker/README.md +++ b/src/uix/eidos/components/gradient-picker/README.md @@ -1,11 +1,24 @@ # Eidos GradientPicker +Fecha de revisión: 2026-07-10 (dossier S9 — graduado de `ACTIVE_DEV_TRACK`). + The visual layer over the headless [`GradientPicker`](../../../soma/components/gradient-picker/README.md) — a field-shaped trigger carrying a live gradient chip that opens the [`GradientBuilder`](../gradient-builder/README.md) in a Popover, with a Clear / Cancel / Save footer. Identity wrapper over the composed Popover. +## Baseline + +Sin baseline air y sin análogo publicado: los editores de gradiente del campo +viven DENTRO de herramientas de diseño (el fill-popover de Figma), no como +form control con valor comprometido. La forma sigue dos patrones internos ya +canónicos: la **identidad-sobre-Popover §2** (el Popover ES el diálogo — mismo +criterio que date-picker/ntp; apg: dialog-modal) y el **host transaccional del +Picker genérico** (footer Clear/Cancel/Save compartido vía PickerShell — +gradient-picker fue reconstruido sobre él en `f5eb8acb` y es, con ntp, uno de +sus dos consumidores reales). + ## Surface ```svelte @@ -53,12 +66,46 @@ no loop. ## Recipe -Self-contained (gradient-picker is on the active dev track). The trigger is a -field-shaped pill; the chip + standalone swatch paint `--gp-current-gradient` -(stamped inline by soma). The popover content + editor reuse the Popover + -PickerShell + GradientBuilder recipes. +Self-contained (graduated from the dev track 2026-07-10; private tokens by +choice — the chrome is mostly the composed Popover + PickerShell + +GradientBuilder recipes). The trigger is a field-shaped pill; the chip + +standalone swatch paint `--gp-current-gradient` (stamped inline by soma). + +## Comparativa + +| Capacidad | UIX | Figma (fill popover) | Libs web de color con tab gradiente | +| --- | --- | --- | --- | +| Form control con valor comprometido (`bind:value`) | **✓** | No (edición in-place) | Parcial (sin transacción) | +| Footer transaccional (Clear/Cancel/Save + revert) | ✓ (PickerShell) | No | No | +| Editor accesible por teclado dentro | ✓ (builder apg slider) | No | No | +| Chip de valor vivo en el trigger | ✓ (`--gp-current-gradient`) | ✓ | Parcial | +| Overlay delegado al sistema | ✓ (Popover §2) | Propio | Propio | + +Referencias: [`../gradient-builder/README.md`](../gradient-builder/README.md) +(el editor y su research) · patrón §2 en [`../date-picker/`](../date-picker/). + +## Decisiones + +- **Identity wrapper over the composed Popover (§2)**: este morfo no declara + semántica de diálogo (role/trap/open-close) — pertenece al Popover; el + footer es chrome compartido de `picker-shell`. +- **Eventos propios = el desenlace transaccional**: `commit-reset` (Clear) / + `commit-save` (Save) / `commit-remove`, con pack propio; los eventos de + EDICIÓN suenan por el contrato del GradientBuilder embebido. Dos voces, dos + dueños. +- **El value bridge es commit-only + ref guard** (sección arriba): el builder + edita un `draft` local; solo los valores asentados suben al provider; los + resets externos bajan por `$effect` con guard — sin bucle. +- **Sin prop `color`** (como el builder). + +## Gaps + +| Gap | Disposición | Detalle | +| --- | --- | --- | +| Persistencia de presets guardados | **descartar** (nivel componente) | Como ColorPicker: los presets/save son del CONSUMIDOR; el panel de presets ya compone. | +| Migración a `PickerProvider` (deferValue genérico) | **diferir** | Task #31 de la memoria — pasada conjunta de los pickers POST audit-fix. | +| Demo v2 (D-1.5/D-3.1 de la máquina) | **implementar** | Pase de demos de esta iniciativa (ver sesión S9). | ## Notes -- **No `color` prop** (like GradientBuilder). Demo: - `web/routes/uix/components/gradient-picker/+page.svelte`. +- Demo: `web/routes/uix/components/gradient-picker/+page.svelte`. diff --git a/src/uix/morfo/components/gradient-builder.ts b/src/uix/morfo/components/gradient-builder.ts index a5dda6810..b83e2f876 100644 --- a/src/uix/morfo/components/gradient-builder.ts +++ b/src/uix/morfo/components/gradient-builder.ts @@ -19,6 +19,10 @@ export const gradientBuilderMorfo = { name: 'GradientBuilder', kebab: 'gradient-builder', scope: ['soma', 'sema', 'eidos'], + // Every stop (and the angle dial) is a real slider thumb — the pattern the + // keyboard interaction implements; composed pieces (ColorPicker, ToggleGroup, + // Button) carry their own patterns. + apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/slider/', expression: 'pack', events: [ // Grabbing / dragging a stop along the track — same `handle` family as the diff --git a/src/uix/morfo/components/gradient-picker.ts b/src/uix/morfo/components/gradient-picker.ts index 5c9d35a93..2debbe2c9 100644 --- a/src/uix/morfo/components/gradient-picker.ts +++ b/src/uix/morfo/components/gradient-picker.ts @@ -16,6 +16,10 @@ export const gradientPickerMorfo = { name: 'GradientPicker', kebab: 'gradient-picker', scope: ['soma', 'sema', 'eidos'], + // The composed Popover owns the dialog pattern (same as date-picker / + // natural-time-picker §2); the editing surface inside cites slider via + // the gradient-builder morfo. + apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/', // Own commit events (reset / save / remove) tuned by the pack at // sema/components/gradient-picker.ts — the embedded GradientBuilder // additionally sounds through its own contract. diff --git a/src/uix/soma/components/index.ts b/src/uix/soma/components/index.ts index f5d59da2f..f69d44d14 100644 --- a/src/uix/soma/components/index.ts +++ b/src/uix/soma/components/index.ts @@ -30,6 +30,8 @@ export * as FieldLangs from './field-langs'; export * as FileUpload from './file-upload'; export * as FloatPanel from './float-panel'; export * as Form from './form'; +export * as GradientBuilder from './gradient-builder'; +export * as GradientPicker from './gradient-picker'; export * as GridList from './grid-list'; export * as ImageAdjustments from './image-adjustments'; export * as ImagePicker from './image-picker'; diff --git a/web/routes/uix/components/gradient-builder/+page.svelte b/web/routes/uix/components/gradient-builder/+page.svelte index b494f90a4..5169d23bf 100644 --- a/web/routes/uix/components/gradient-builder/+page.svelte +++ b/web/routes/uix/components/gradient-builder/+page.svelte @@ -24,16 +24,7 @@ const uix = getActiveUix(); - type Tab = - | 'live' - | 'system' - | 'motion' - | 'sema' - | 'services' - | 'api' - | 'morfo' - | 'recipe' - | 'a11y'; + type Tab = 'live' | 'system' | 'motion' | 'sema' | 'services' | 'api' | 'morfo' | 'recipe' | 'a11y'; let tab = $state('live'); const sizes: GradientBuilderSize[] = ['sm', 'md', 'lg'];