--- title: Handoff — Abrir la jaula del color (`color` = sistema completo en todos los componentes) type: process audience: human + agent status: CERRADO ✅ — Fases 0–5 + chart + fix Card/Avatar; 2 guards activos; sin huecos abiertos (qr-code excluido por diseño). Follow-ups menores al final. date: 2026-07-19 (cierre definitivo — cuarta sesión) --- # Handoff — Abrir la jaula del color ## ✅ CERRADO (2026-07-18, tercera sesión) La iniciativa está COMPLETA. En esta sesión de cierre cayeron: - **La cola entera de standalones**: skeleton · spinner (`| 'inherit'` conservado) · textarea · metrics (featured icon, `parts: ['icon']`) · form (submit/reset, `parts: ['submit','reset']`, neutral = no-stamp) · field-langs (delegante) · radio-cards (delegante sobre el forward de radio-group) · image (migrado del mecanismo bespoke al forward — arregla las escalas que caían a named-color CSS) · **color-picker (cross-portal RESUELTO**: forward en `parts: ['trigger','content']`, el content estampa su propio color desde contexto) · float-panel (el "default fantasma" era info obsoleta: la recipe SÍ tenía `accent`) · proof-of-human (apertura + **exención fixed-tone formal** compartida entre los tests de color y font-size) · card-group (ya abierto por delegación, sin cambios). - **Fase 4 — tinta de contenido**: text · heading · display · code · label con RAMA EJE-PRIMERO (el eje `primary…on-solid` conserva su significado de tinta vía var inline sin estampa; todo lo demás va por el forward). Text no estaba en la lista original — incluido por coherencia (mismo eje cerrado). - **Fase 5 — guard estructural**: test "keeps every component `*Color` prop open" en `recipe-css-contract.test.ts` (escaneo de fuente + resolución transitiva de alias; exentos comentados: `OnionColor = string` es MÁS ancho; chronos re-entra con su track). Docs: `reference.md §25` (reversión registrada) · tracker clean-room §THM-2 · `changelog.md §43` · READMEs obsoletos de 13 componentes corregidos. **Huecos RESUELTOS (2026-07-19, tras decisión del usuario):** - **chart** — ABIERTO. Su `color` (series line/area/bar/scatter/bubble, gauge, heatmap-hue, funnel/pie/bar-list/polar per-categoría, smith points) es SVG con resolución JS, así que NO usa `data-color` + capa compartida: se centralizó un resolver en `chart/context.ts` (`seriesColor`/`seriesSurface` + nuevo `seriesContrast`) que mapea rol → `--color-{role}-{solid,surface,contrast}`, escala → `--scale-{name}-{9,a2}` (steps solid/surface-alpha del `PALETTE_SLOT_ STEP`), y valor crudo → verbatim (surface = `color-mix 15%`, contrast = white). Los 3 builders inline (funnel, heatmap, calendar-heatmap) se refactorizaron para pasar por el helper. Los ~14 props `color?: ColorRole` → `ComponentColorProp`. Verificado en Chrome (rol/escala/crudo resuelven a color real; roles behavior- preserving). `MetricsChartProps.color` delega ahí → abierto también. - **qr-code** — queda FUERA por decisión de diseño: `color` es la tinta de los módulos del QR (contraste con el fondo para escaneabilidad), no encaja el sistema de escalas. No se abre. **Verificación adversarial + 2 fugas transitivas cerradas (2026-07-19).** Un workflow de 23 agentes revisó los 16 componentes de la sesión de cierre (todos limpios) y su crítico de completitud cazó 2 fugas en componentes abiertos *transitivamente* (heredan un alias abierto) que nunca se tocaron ni verificaron: - **card-group-item** estampaba `data-color` crudo sin par `data-color-custom` → un valor CSS crudo caía a neutral. FIX: `resolveComponentColor` (ruta genérica). ⚠️ NO usa el split bespoke de Card (ver bug abajo). - **s-text** (`STextColor = TextColor`, abierto gratis en Fase 4) seguía en el puente pre-Fase-4 → escalas/intents/crudo daban vars inexistentes. Migrado a la rama eje-primero + recipe `_palette-text`. - **split-button** revisado: OK (delega crudo en ButtonGroup→Button). - **Guard runtime NUEVO** en `recipe-css-contract.test.ts`: "pairs a dynamic `data-color` stamp with `data-color-custom`" — el guard de TIPOS es ciego al runtime (solo lee `types.ts`); éste falla si un `.svelte` estampa `data-color={…}` dinámico sin el par custom (literal estático exento; WIP excluido). **Contract 30/30.** **🐞→✅ Bug pre-existente descubierto + ARREGLADO: custom-color del FILL de Card + Avatar** (`b93cc6c5c`). Su split bespoke (`--{c}-color-custom` + bloque derivador en su `.css`) quedó ENSOMBRECIDO por el forward compartido de THM-2 (misma especificidad, carga después → gana; para un valor crudo `--palette-*` sin definir porque el shared layer lo deriva de `--color-custom`, no de `--{c}-color-custom` → host neutral). FIX: migrados a `resolveComponentColor` (genérico), borrados los bloques bespoke + 4 tokens de recipe huérfanos. De paso: Card **pisaba su propio seed** (`style=` antes de `{...rest}`) → ambos ahora COMPONEN con `composeInlineStyle`. Avatar RING + BADGE son canales SEPARADOS (namespace propio, no ensombrecidos) → intactos. **Lección: la verificación de CASCADA CSS exige navegador** — el workflow lo dio por bueno leyendo solo código; solo el probe reveló que el forward lo pisa. Los commits de la iniciativa (sesiones 3–4): `60a30c2e6` (skeleton/spinner/ textarea) · `838b01a00` (metrics/form) · `6b37beed6` (field-langs/radio-cards/ image) · `c9db0a8d2` (color-picker) · `18026dfd8` (float-panel) · `04659558d` (proof-of-human + exención) · `67d7dc89c` (Fase 4) · `0b2170d62` (Fase 5 guard + docs) · `6cdba0182` (fugas transitivas + guard runtime) · `72a6569c3` (chart) · `b93cc6c5c` (fix Card/Avatar). Todo verificado en Chrome (probe `getComputedStyle`) y con el contract test 30/30. --- ## Follow-ups (menores, opcionales — para retomar cuando toque) Ninguno bloquea; la iniciativa está cerrada. Por prioridad: 1. **`[data-uix-docs] code` pisa el `` de eidos en las demos** — regla del shell de docs con especificidad (0,1,1) que gana sobre `[data-code]` (0,1,0), pisando la tinta abierta dentro de las páginas de documentación. Fix: acotar a `[data-uix-docs] code:not([data-code])` y revisar reglas equivalentes del shell para h1–h6/label/display. (Pre-existente, no lo introdujo el color; se señaló como tarea aparte.) 2. **`ringColor` de Avatar es roles-only** — acepta rol + valor crudo (por su seed propio `--avatar-ring-color-custom`) pero NO las 33 escalas. Es un canal secundario; abrirlo a escalas exigiría resolución tipo `seriesColor`. Gap menor, no el bug. 3. **Límites de los 2 guards (documentar, no urgente)**: el de tipos no vigila `chart` (usa `color?:` inline en interfaces, sin alias `*Color`); el de runtime solo caza el patrón `data-color={…}` sin par custom — NO caza la clase s-text (var inline sin estampa) ni la clase Card/Avatar (cascada CSS que ensombrece). La verificación de cascada sigue siendo manual/navegador. 4. **`--card-color-custom` en comentarios** — quedan menciones históricas en comentarios (card-group-item, types) que ya no reflejan código vivo; limpieza cosmética. ## ⚠️ Estado del working tree (al cerrar) El árbol tiene WIP SIN COMMITEAR de otros tracks (chat-composer, palabras): `base.ts` + `generated/base.css` + ficheros `chat-*`/`palabras`. NO son míos — no commitear. Todo mi trabajo del color está commiteado. Al operar sobre `base.ts`: `git show HEAD:base.ts` + insertar solo mis bloques + swap→generate→ stage explícito→commit→restore (ver §Operativa). Lo que sigue de este doc es el REGISTRO HISTÓRICO del handoff (patrones A–E y trampas, que siguen siendo la referencia para componentes nuevos). --- ## Objetivo 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**, 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). --- ## ⚡ 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). --- ## Estado — HECHO ### Motor (Fase 0) ✅ — la costura compartida, LISTA - **`ComponentColorProp`** (`src/uix/eidos/lib/types.ts`) = `ComponentColor | (string & {})` — 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`): `[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 (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. --- ## Operativa (verificada esta sesión) - **Dev server**: `preview_start` con la config `verify` de `.claude/launch.json` (: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 `
` 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`.