docs(color): actualiza el handoff de "abrir la jaula del color" con el estado real

Reescribe docs/process/open-color-cage-2026-07.md tras los 7 commits de esta
sesión: familias hechas (field/calendar/time/list-grid/lists + link/mark/
button-group/highlight), los 5 PATRONES de migración (A eidos-wrapper · B
soma-routed · C mini-recipe month-grid · D tinta de contenido · E delegante), las
6 TRAMPAS cazadas (forma declarations[] para `parts`, recipe-exists fiable/no
duplicar clave, declarar solo slots consumidos, `--x:inherit`, cross-portal,
prefijo accent/accent-soft), y la COLA de ~11 standalones heterogéneos con su
patrón + trampa cada uno (incl. color-picker/float-panel intentados y revertidos
por matiz cross-portal/default). Operativa corregida: el total de `check` NO es
baseline estable (76↔94 por caché svelte-check) → verificar CERO en tocados;
índice git compartido + exclusiones (chat-track en soma/virtual-list, palabras,
alpha).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alpha-0.1-sec-dom
dev 3 months ago
parent 28aacf0922
commit ae2a27208e

@ -2,8 +2,8 @@
title: Handoff — Abrir la jaula del color (`color` = sistema completo en todos los componentes)
type: process
audience: human + agent
status: en curso — Fase 0/1/2a/2b ✅ · Fase 3/4/5 pendientes
date: 2026-07-18
status: en curso — Fase 0/1/2a/2b ✅ · Fase 3 mayoría ✅ · cola de ~11 standalones + Fase 4/5 pendientes
date: 2026-07-18 (actualizado tras la sesión de continuación, 7 commits)
---
# Handoff — Abrir la jaula del color
@ -14,117 +14,217 @@ El prop `color` de un componente debe aceptar **todo el sistema de color del
ecosistema — por role, por intent, por paleta (33 escalas donantes) y por valor
(raw CSS) — en TODOS los componentes**. Decisión de diseño del usuario
(2026-07-18): revierte la restricción de THM-2 (2026-07-12) que dejaba los
controles semánticos en subconjuntos; **la jaula se abre siempre**.
controles semánticos en subconjuntos; **la jaula se abre siempre**, sin
excepciones (incluida la tinta de contenido).
Doctrina de respaldo: [`theming/reference.md §25` (THM-2)](../theming/reference.md#L1465)
+ [§39](../theming/reference.md#L1668) — `color` = IDENTIDAD (role/scale/custom),
desacoplada de la evaluación (que vive en `invalid` + el `intent` del evento).
## Estado — HECHO y verificado (type-clean, `check` = 50 errores baseline)
---
## ⚡ Empieza aquí (nueva sesión)
1. Lee este doc entero + la memoria `project-open-color-cage-2026-07-18`.
2. El motor y el patrón están LISTOS y probados. Lo que queda es **~11
componentes standalone HETEROGÉNEOS** (§Cola). NO son un batch uniforme: cada
uno rompe el patrón de una forma distinta y con su propia trampa.
3. Verifica el estado: `npm run check` (⚠️ el total oscila 76↔94, NO es baseline
estable; verifica **CERO errores en los componentes que toques**, no el total).
4. Arranca por los MENOS peligrosos de la cola (skeleton/spinner/textarea), no por
los portalados (color-picker/float-panel).
---
### Fase 0 — Motor (la costura compartida) ✅
## Estado — HECHO
### Motor (Fase 0) ✅ — la costura compartida, LISTA
- **`ComponentColorProp`** (`src/uix/eidos/lib/types.ts`) = `ComponentColor | (string & {})`
— el tipo canónico ABIERTO que todos los componentes deben usar.
- **`resolveComponentColor(color)`** (`src/uix/eidos/lib/component-color.ts`, NUEVO)
— el helper del wrapper: divide un valor en `{ dataColor, isCustom, customStyle }`.
Un nombre canónico (role/intent/escala) → `data-color`; un valor CSS crudo →
`data-color-custom` + el seed `--color-custom`.
— el tipo canónico ABIERTO al que todos apuntan.
- **`resolveComponentColor(color)`** (`src/uix/eidos/lib/component-color.ts`) — el
helper del wrapper: `{ dataColor, isCustom, customStyle }`. Canónico → `data-color`;
valor CSS crudo → `data-color-custom` + seed `--color-custom`.
- **Derivación custom compartida** (`render-css.ts` `renderSharedPaletteLayer`):
bloque `[data-color-custom]` que deriva los 10 slots `--palette-*` desde
`var(--color-custom)` por `color-mix` (fórmula rfc-color-model §5). El forward
`renderRecipePaletteForward` se extendió a `[data-{c}][data-color], [data-{c}][data-color-custom]`.
→ **cualquier receta forward-ready gana el custom con CERO CSS**.
- Guard `recipe-css-contract.test.ts` actualizado (nueva forma del forward + invariante fila custom).
### Fase 1 + 2a — Color eidos-wrapper (14) ✅
El wrapper eidos estampa `data-color` directamente. Patrón: abrir el tipo a
`ComponentColorProp`, wrapper llama `resolveComponentColor` (estampa
`data-color`/`data-color-custom`/`style`), demo con picker completo + input custom.
- **radio-group** (piloto, visual) · checkbox · stepper · toggle-group (sentinels
`absent`/`unknown` preservados; regla hand-authored de su css extendida a
`[data-color-custom]`) · select (por partes trigger+content) · badge · editable ·
file-upload · tag-group · tags-input · surface · avatar (cosmético) · **card**.
- **card**: era eidos-native (no soma-routed); arreglado un **bug real** — su
`isCanonicalColor` usaba solo `COLOR_ROLES`, así una escala como `teal` caía al
path custom y pintaba el CSS `teal` (#008080) en vez de la escala. Fix: incluir
`PALETTE_SCALES`. **PENDIENTE (menor)**: card sigue con custom *narrow* (srgb,
tokens `--card-color-custom`); migrar a la costura genérica `--color-custom`
(oklab) borrando su bloque `[data-card][data-color-custom]` en card.css + los
tokens `color-custom` del recipe.
### Fase 2b — Color soma-routed (button/switch/toggle) ✅
`color` viaja por soma/morfo (eje `intent` + `color`). Patrón morfo+soma limpio
(referencia: **button**):
1. **Soma types**: `color?: ... | (string & {})` (ensanchar) + `colorCustom?: string`.
2. **Soma provider**: el resolver de `color` devuelve `undefined` cuando hay
`colorCustom` (→ `data-color` ausente); y el prop `colorCustom` se **anula con
intent no-neutral** (intent evaluativo gana y suprime el custom).
3. **Soma component** (`components/{c}.svelte`): destructurar `colorCustom` + `bindProps`.
4. **Morfo** (`morfo/components/{c}.ts`): declarar `data-color-custom` con
`value: v.propRef('colorCustom')` + `condition: prop-truthy 'colorCustom'`.
5. **Eidos wrapper**: `resolveComponentColor` → pasa `color={dataColor}` +
`colorCustom={isCustom ? color : undefined}` + `style` al `.Provider`.
- Verificado: escala → `data-color`; custom → `data-color-custom` sin residual;
intent evaluativo → gana y suprime el custom.
### DOS bugs del framework encontrados y arreglados (importante)
1. **`html-presence` con `v.literal('')`**: `html-presence` emite según la verdad
del VALOR (truthy→`''`, falsy→quita). Un literal vacío es falsy → el attr NUNCA
se emite. Card lo tenía igual (dormido, es eidos-native). Fix aplicado en los
morfos soma-routed: `value: v.propRef('colorCustom')` (como `data-disabled`).
**PENDIENTE (menor)**: el morfo de card (`src/uix/morfo/components/card.ts`,
`data-color-custom` con `v.literal('')`) tiene el bug latente — arreglarlo por
correctitud aunque card lo estampe directo.
2. **El custom pisaba al intent evaluativo**: el prop `colorCustom` no se anulaba
con intent no-neutral. Fix: `colorCustom: () => intent === 'neutral' ? opts.colorCustom.current : undefined`.
## PENDIENTE
### Fase 3 — Barrido con cableado (~35 `ColorRole` sin forward)
Componentes `ColorRole` (role+intent, SIN forward → no pintan escalas ni custom
hoy). Por componente: (a) declarar tokens `_palette-{slot}` en el recipe
(`lib/recipes/base.ts`, host default = su token de rol actual → behavior-preserving);
(b) que su `.css` lea `--{c}-palette-*` donde hoy usa `--color-{role}-*`; (c) abrir
el tipo a `ComponentColorProp`; (d) wrapper `resolveComponentColor`; (e) picker en el demo.
**Candidatos** (censo `git grep 'export type \w*Color = ColorRole'`): field, listbox,
link, table, calendar, combobox, mark, feed, highlight, drag-drop, month-grid,
number-field, search-field, textarea, time-field, time-picker, tree-grid, tree-view,
virtual-list, virtual-grid, year-grid, range-calendar, proof-of-human, skeleton,
spinner, clipboard, css-field, date-field, form, field-langs, float-panel,
grid-list, carousel, chronos, banner, + **radio-cards, timeline, button-group**
(reclasificados aquí desde Fase 2 por no ser forward-ready). **Candidato a Workflow**
(pipeline por componente; ficheros disjuntos → paralelo seguro).
### Fase 4 — Tinta de contenido (code, display, heading, label)
`[data-color-custom]` deriva los 10 slots `--palette-*` desde `var(--color-custom)`
por `color-mix`. El forward `renderRecipePaletteForward` emite
`[data-{c}][data-color], [data-{c}][data-color-custom]` → **toda receta con
tokens `_palette-*` gana roles + 33 escalas + custom con CERO CSS extra**.
- Guard `recipe-css-contract.test.ts` con la forma del forward + invariante custom.
### Fase 1/2a/2b ✅ (commit `149c0fef6`, pre-continuación)
- **Eidos-wrapper (14)**: radio-group·checkbox·stepper·toggle-group·select·badge·
editable·file-upload·tag-group·tags-input·surface·avatar·card. Ref: **checkbox**.
- **Soma-routed (button/switch/toggle)**: `color` por soma/morfo, dos ejes
intent+color. Ref: **button**. (2 bugs de framework arreglados: `html-presence`
con `v.literal('')` → usar `v.propRef`; custom pisaba intent evaluativo → anular.)
### Fase 3 — familias (esta sesión, 7 commits) ✅
| Commit | Componentes |
|---|---|
| `861427fe1` | **Field family** (field base + search/password/mask/number/css/color/date/date-range/time-field) + **calendar** + **month-grid** + **time-picker/time-range-picker** + fix regresión runtime + 9 fixes de tipo (tests switch/toggle `colorCustom`) |
| `4adcc4c7f` | sweep custom-threading: 12 sub-partes date/time (trigger/calendar/clock/input/root) por `resolveComponentColor` |
| `889b03dae` | **year-grid** + **range-calendar** |
| `c5699bd01` | **familia lista/data-grid** (10): table·tree-grid·tree-view·virtual-list·virtual-grid·feed·drag-drop·grid-list·clipboard·carousel |
| `528ddaa7f` | **listbox** + **timeline** + **combobox** |
| `1efa03439` | **link** + **mark** (tinta de contenido) |
| `28aacf092` | **button-group** + **highlight** (delegan a Button/Mark) |
Cada familia verificada en Chrome (role · escala · custom → token compartido exacto).
---
## Los PATRONES (elige el correcto por componente)
Antes de tocar un componente, identifica su patrón:
**A. Eidos-wrapper (estampa `data-color` directo).** Ref: `checkbox`. Ya tiene
forward-ready recipe → solo tipo + wrapper `resolveComponentColor`.
**B. Soma-routed (`color` por soma+morfo).** Ref: `button`. Toca soma types/provider/
component + morfo + wrapper. Dos ejes intent+color.
**C. Mini-recipe (cascada `--_X-accent` per-rol → forward).** Ref: `month-grid`. El
patrón dominante de Fase 3:
1. **Recipe** (`lib/recipes/base.ts`): añade tokens `_palette-{slot}` (host =
token de rol actual, normalmente primary → behavior-preserving).
2. **CSS**: quita las decls base `--_X-accent-*` + los bloques per-rol
`[data-X][data-color='rol']`; renombra los consumidores
`var(--_X-accent-*)` → `var(--_X-palette-*)`.
3. **types**: `XColor` → `ComponentColorProp`.
4. **wrapper**: `resolveComponentColor` (estampa el trío + `style`).
5. `npm run generate:eidos-css` + verifica el forward en `generated/base.css`.
**D. Tinta de contenido (`color:`/`background:` per-rol directo).** Ref: `link`
(1 slot text) / `mark` (element+text). Reemplaza la cascada `color: var(--color-{rol}-text)`
por `color: var(--_X-palette-text)` + recipe `_palette-text`.
**E. Delegante (pasa el color a un hijo ya abierto).** Ref: `button-group`
(contexto→Button) / `highlight` (→Mark). Solo tipo + wrapper.
### ⚠️ TRAMPAS (todas cazadas esta sesión — NO repetir)
1. **`parts:` SOLO se lee en la forma `declarations: [...]`**, no en `{ value, scope }`
(`render-css.ts` `expandToDeclarationsWithParts` l.2183). Si la cascada vive en
`[data-X-root]` o `[data-X-content]` (no el provider bare), el recipe necesita
`'_palette-slot': { parts: ['root'], declarations: [{ value, scope: 'host' }] }`.
Verifica con `grep '\[data-X...\]\[data-color=' X.css` DÓNDE se estampa data-color.
2. **Verifica `recipe-exists` FIABLE antes de añadir** (grep `^\t'?X'?: \{`). Varios
componentes YA tienen recipe (z-index/otros): drag-drop, combobox, timeline,
color-picker, float-panel. **FUSIONA en el existente, NO dupliques la clave**
(clave duplicada = el último gana, silencioso; svelte-check lo marca).
3. **Declara SOLO los slots con consumidor real** (`grep 'var(--_X-palette-slot)'`).
Slots sin `var()` consumidor = token de recipe HUÉRFANO → falla el orphan test.
Si TODO estaba muerto (virtual-list/grid) → NO pongas recipe (tipo abierto +
wrapper inerte basta).
4. **Trampa `--x: inherit`**: un custom-prop con valor `inherit` NO guarda el token
`inherit`; hace que el prop herede del padre (→ inválido). Para "default = inherit"
usa una regla `[data-X][data-color],[…custom]{ color: var(--_X-palette-text) }` y
deja la base en `color: inherit` (patrón de `mark`).
5. **Cross-portal**: si el componente RE-DECLARA su accent en 2 scopes (trigger +
contenido portalado), el forward en el root NO alcanza el portal. Requiere data-color
+ forward en el scope del portal (color-picker cae aquí — ver §Cola).
6. **`accent` bare vs `accent-soft`**: renombra `-soft` PRIMERO, luego el bare
`var(--_X-accent)` (con `)` de cierre) → evita el solape de prefijo.
---
## Cola — ~11 standalones HETEROGÉNEOS (lo que falta)
Cada uno es trabajo INDIVIDUAL con su análisis y verificación propia. Orden
sugerido: los menos peligrosos primero.
### Patrón D/simple (menos peligrosos — empieza aquí)
- **skeleton, spinner, textarea, metrics, form** — tienen cascada per-rol pero cada
uno pone una PROPIEDAD distinta (`color`/`background`/`border`) directa. Analiza
qué propiedad tinta cada uno y a qué slot mapea (`grep 'data-color=' + var(--color-`).
spinner además tiene `| 'inherit'` en su tipo (conservar como unión aditiva).
- **field-langs** — `FieldLangsColor = ColorRole`, no estampa data-color (¿compone
Field?); investigar cómo colorea.
- **radio-cards** — `AffirmativeColorRole`; compone RadioGroup (ya abierto). Probable
delegante (patrón E) o mini-recipe.
- **image** — `ImagePlaceholderColor = ColorRole | (string & {})` (ya semi-abierto);
el placeholder. Poco trabajo.
### Patrón C con TRAMPA (compound/portalado — CUIDADO, requieren verificación)
- **color-picker** — cascada 1 slot (accent→border) en `[data-color-picker]`, PERO
**re-declara `--_color-picker-accent` en 2 scopes** (trigger l.28 + contenido
portalado l.99). El forward en el root no alcanza el portal → el contenido
portalado necesita su propio data-color+forward. Compound (root+trigger+
channel-input estampan data-color; las partes threading raw necesitan
resolveComponentColor como el sweep de combobox). Recipe YA existe (fusionar).
**Intentado y revertido esta sesión** por el matiz cross-portal.
- **float-panel** — cascada 1 slot (accent→border) en `[data-float-panel-content]`
(parte → `parts: ['content']`). PERO su default base es
`--_float-panel-accent: var(--float-panel-accent)` y **`--float-panel-accent` NO
existe en el recipe** → el comportamiento actual es "sin acento salvo data-color".
Un host `primary-border` lo cambiaría a "siempre primary" (cambio silencioso).
Verifica primero el default REAL (¿el content-wrapper siempre estampa data-color
vía `ctx?.color`? ¿ctx.color default 'primary'?). Recipe YA existe (fusionar).
**Intentado y revertido esta sesión.**
### Especiales
- **proof-of-human** — fixed-tone. Sus literales de color/font-size en
`clock.css`/`rotate-align.css` YA fallan 2 tests del contract (raw-color +
font-size) — añadir a `FIXED_TONE_COMPONENTS` (recipe-css-contract.test.ts) o
tokenizar. Su `color` es accesorio.
- **chronos** — en `WIP_TRACKS` (excluido del contract); scheduler sin commitear.
- **card-group** — el `.svelte` raíz NO tiene prop `color` (sus items componen Card,
ya abierto). `card-group-item` sí; revisar si necesita apertura.
---
## Fase 4 — Tinta de contenido (code, display, heading, label)
Otro eje (`primary|secondary|muted|disabled|on-solid`). Abrir como unión que
conserva su eje: `ComponentColorProp | 'muted' | 'disabled' | 'on-solid'`; cablear
el color de texto al slot `text` de la paleta.
el color de texto al slot `text` de la paleta (patrón D, como link/mark). Nota:
link/mark YA se hicieron con este patrón — reúsalo.
## Fase 5 — Guard estructural + docs
- **Guard**: test que falla si algún tipo `*Color` es más estrecho que
`ComponentColorProp` (salvo la unión aditiva de tinta: `| 'muted' | ...`). Ancla:
`scripts/component-audit.ts:1327` o `recipe-css-contract.test.ts`. Esto ENFORCEA
"la jaula siempre abierta" y cierra la iniciativa.
- **Docs**: `reference.md §25` + tracker `continue-cleanroom-fixes-2026-07.md §THM-2`
(registrar la reversión) + changelog.
### Fase 5 — Guard estructural + docs
- Test que falla si algún tipo `*Color` es más estrecho que `ComponentColorProp`
(ancla: `scripts/component-audit.ts:1327` o `recipe-css-contract.test.ts`).
- Docs: `reference.md §25` + tracker `continue-cleanroom-fixes-2026-07.md §THM-2`
(registrar la reversión) + changelog + los 2 bugs de morfo runtime.
---
## Operativa (verificada esta sesión)
## Cómo continuar (operativa verificada)
- **Dev server**: `preview_start` con la config `verify` de `.claude/launch.json`
(puerto 5201). El server del otro chat (5180) puede estar caído.
- **Staleness de morfo**: HMR NO recarga `morfo/components/*.ts` fiablemente
(compile cacheado por WeakMap) → **reiniciar el dev server** tras tocar un morfo.
- **Verificación**: `npm run check` (baseline = **50 errores**, todos ajenos —
menubar/alpha/temas/demos; cualquier cifra > 50 es regresión propia). Navegador
con componente real + probe `getComputedStyle` de `--{c}-palette-solid` /
`data-color` / `data-color-custom` (la preview en segundo plano está suspendida;
usar Chrome real y foregroundear para captura). `npm run generate:eidos-css` tras
tocar recipes; NO hace falta tras tocar solo `.svelte`/`.ts`/morfo.
- **Patrones de referencia** (copiar de estos):
- eidos-wrapper: `checkbox` (types + wrapper + demo).
- soma-routed: `button` (soma types/provider/component + morfo + wrapper) — el
más completo, con el intent-gate y el fix de html-presence.
- Picker de demo compartido: `web/routes/uix/lib/PalettePicker.svelte` (ahora con
input custom); los demos que no lo usan inlinean chips + `<input type="color">`.
## Deuda menor anotada
- card custom → genérico (migrar a `--color-custom`/oklab + borrar bloque propio).
- morfo de card: `v.literal('')` → `v.propRef('colorCustom')` (bug latente).
- +2 warnings a11y en los inputs custom de los demos switch/toggle (asociación label).
(:5201). Reiniciar tras tocar un **morfo** (HMR no recarga `morfo/*.ts` fiable —
compile cacheado por WeakMap).
- **⚠️ `check` NO es baseline estable**: la caché incremental de svelte-check aflora
intermitentemente errores `any` PRE-EXISTENTES de demos ajenos (heroscrolling/
animations/palabras/alpha/temas): oscila **76↔94**. La verificación FIABLE es
**CERO errores en los componentes TOCADOS** (`grep 'ERROR "src...components/X'`),
no el total.
- **Tras tocar un recipe**: `npm run generate:eidos-css` + verifica el forward
emite en `generated/base.css` (`grep '\[data-X...\]\[data-color\],'`). NO hace
falta tras solo `.svelte`/`.ts`.
- **Contract test**: `npx vitest run src/uix/eidos/recipe-css-contract.test.ts` —
26/28 pasan; los 2 fallos son proof-of-human (pre-existente). >2 fallos = tu
cambio (orphan/phantom/forward).
- **Navegador**: probe `getComputedStyle` de `--{c}-palette-{slot}` sobre el
elemento que estampa data-color; fuerza `data-color='risk'` y compara con un
`<div data-color='risk'>` probe (`--palette-{slot}`); custom con
`style.setProperty('--color-custom', '#00b894')` → espera `color-mix`.
### ⚠️ ÍNDICE GIT COMPARTIDO
El working tree tiene WIP de OTROS tracks sin commitear. **Al commitear**:
`git reset -q` + `git add` de rutas EXPLÍCITAS + `git diff --cached --name-only`
para verificar ANTES de `git commit -F -` (backticks en el mensaje → usar `-F`, no `-m`).
EXCLUIR siempre:
- `menubar` (eidos/soma/morfo), `dropdown-menu`, `palabras`, `words` — track palabras.
- **`soma/components/virtual-list/*` + `web/routes/uix/components/virtual-list/+page.svelte`**
— el track de chat (`chat-*`) extiende VirtualList ("anchor end/chat mode"), AJENO
al color. Yo solo toco `eidos/components/virtual-list/*`.
- `web/routes/alpha/`, `.claude/settings.local.json`.
- Cambios de comentario "Words"→"Palabras" en css ajenos (p.ej. color-picker.css).
## Referencias de patrón (copiar de estos)
- Mini-recipe (C): **month-grid** (recipe `_palette-*` + css + types + wrapper).
- Con `parts`: **combobox** (`parts: ['control','input','trigger','content']`) /
**table** (`parts: ['root']`).
- Tinta (D): **link** (1 slot) / **mark** (element+text, regla colored-only).
- Delegante (E): **button-group** / **highlight**.
- Sweep de sub-partes que threading raw: workflow con ref `select-trigger.svelte`.

Loading…
Cancel
Save

Powered by TurnKey Linux.