Componente nuevo del bloque chat v2: el panel de lista de salas/DMs.
Compone el Listbox del ecosistema (doble registro id/ref compartido):
selección única + roving-tabindex + typeahead + role=option vienen de
Listbox; chat-list aporta la disposición de fila (avatar · título/preview ·
meta) y el estado visual data-unread (negrita + Badge compuesto).
- morfo: expression 'delegated', sin eventos propios (D.1 — el actor es el
Listbox compuesto); parts provider/item(data-unread)/avatar/title/preview/meta.
- soma: ChatListProvider crea ListboxProvider dentro (value string <-> string[]);
ChatListItemProvider per-instance (unread = boolean crudo); 4 clases de
contenido explicitas.
- eidos: rejilla avatar | (titulo/preview) | meta, avatar y meta abarcan las
dos filas; truncado; unread -> titulo en negrita. No neutraliza el arquetipo
item (la conversacion ES una opcion seleccionable). Sin @keyframes; ejes
logicos.
- demo: 5 salas, seleccion limpia unread, typing-en-lista (compone ChatTyping),
Badge de no-leidos; tabs live/system/api/morfo/a11y.
- READMEs dobles (eidos con Passive justification, delegated); next-features 7
marca chat-list SHIPPED.
Verificacion: check baseline 76 (0 nuevos) · component:audit PASS ·
eidos-lint invalid=0 · recipe-contract 30/30 · morfo:vocabulary 0 · Playwright
PASS (role=listbox, 5 salas, aria-selected, unread=2, badges=2, typing-en-lista,
click + teclado seleccionan).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Corrige la conclusión de la investigación (la hipótesis DOM-src era errónea; el
CPU profile dio samePalabrasDocument = 34%). imagenes-base64.md §Causa REAL +
handoff §2026-07-19: fast-path d96e4cde7 (20→12ms) + blob 013ef5afc (12→8ms).
Lección anotada: perfilar ANTES de teorizar en atascos de perf.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan de ejecucion para sesion aparte, VERIFICADO adversarialmente (workflow de
7 agentes contra codigo y docs) antes de escribirse — corrige 5 afirmaciones
de la respuesta que lo origino:
- "por delante de Radix (hex sRGB)": FALSO — el propio RFC §14 registra Radix
con APCA y P3 ("Radix style"); la posicion del repo es "on par"; nuestra
paleta enviada es sRGB-equivalente (oklch = siblings de hex).
- "el solver satisface la tabla §40": el UNICO floor duro ratificado es
text-strong·12 ≥ 4.5; text·11 quedo tier blando (decision D1); solid·9 esta
anclado exacto a la semilla; el script de auditoria aun codifica la tabla
PRE-veredicto (consumirla contradiria los veredictos → F0 = tabla como datos).
- "los tipos ya existen en parte": ColorScaleSeed/Source son solo prosa del
RFC; config-seeds imposibles hoy (solo applyColorScheme roles-only +
generatePalette full-33 como bancos).
- "792 hex en base.ts": exacto en total pero en DOS ficheros (288 base.ts +
504 color-scales.ts); 4 escalas ya autoradas desde semillas offline.
- "nada downstream cambia": nombres si (contract.ts), pero hay pins de VALOR
disenados para dispararse (hex literales, flip-set de polaridad,
generated/base.css byte-exacto) + persistencia rechaza version ≠ 1.
El plan: F0 tabla ratificada como datos + harness sobre output generado ·
F1 solver (biseccion contra sRGB gamut-mapeado, salida hex, desviacion minima
del donante) · F2 validacion en bancos de semilla + navegador · F3 (gated)
migracion base→seeds con re-ratificacion de pins y pase visual. 6 decisiones
de usuario ABIERTAS al frente (target text·11, semantica base+ΔE, modo
on-solid, colocacion/alcance, tipos Fase 0, normalizacion de la tabla).
next-features §1 enlaza el plan. (El hunk ajeno de chat-list en next-features
queda fuera del commit — danza clean-desde-HEAD.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Utilidad transversal + su primer consumidor (palabras). Win SECUNDARIO sobre el
fast-path de d96e4cde7 (no es el fix del atasco; ese era el deep-compare).
- `$adom.DataUrlBlobUrls` (NUEVO, `arts/adom/data-url-blobs.ts`): caché stateful
que resuelve `data:` URLs a `blob:` object URLs (decodifica base64/percent),
cachea por identidad, `dispose()` revoca. Realm same-origin. 5 tests.
DECISIÓN de ubicación (valorada): NO va en `<Image>` — ningún consumidor de
`<Image>` pasa base64 (image-picker/file-upload ya usan createObjectURL de un
File; el resto URLs reales) → sería abstracción prematura. Es una utilidad
reutilizable que `<Image>`/ImageProvider pueden adoptar si algún día aparece un
consumidor base64.
- `palabras-image-blobs.svelte` (NUEVO): tras cada render swapea el `src` de los
`<img data:…>` del contenteditable por el blob (el MODELO conserva el base64 →
persistencia; el DOM queda ligero). Re-run por `blocks`; el `<img>` memoizado
mantiene el blob entre teclas.
Medido (base64 de 2MB, con el fast-path ya puesto): tecleo 12ms/char → 8ms/char
(el `src` de MB en el DOM sí añadía ~2-4ms). check 0 errores; Playwright: `src`
= `blob:` tras cargar, tecleo 8.1ms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CAUSA REAL del «atasco» de imágenes base64 (hallada por CPU profile: 34% de las
muestras). El sync del `bind:value` (`syncExternalState`) comparaba el `value`
bindeado contra el doc interno con `samePalabrasDocument` — una comparación DEEP
O(documento) — EN CADA cambio. Como el editor escribe `value` con la MISMA
referencia del doc interno (publishState → `this.document`), esa comparación
recorría el string base64 (MB) por tecla para nada.
Fix: un check de igualdad por REFERENCIA antes del deep-compare — el caso común
(nuestras propias ediciones) se resuelve en O(1); el `samePalabrasDocument` solo
corre cuando la referencia difiere de verdad (una escritura externa).
Medido (imagen base64 de 2MB en el modelo): tecleo 20ms/char → 12ms/char. NO era
el `src` grande en el DOM (mi hipótesis inicial, errónea) — era el deep-compare.
Verificado: provider 53 tests verdes; check 0 errores palabras.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Actualiza el handoff en el design-record del bloque: v1 completo (incl.
onMessageSeen), los 3 remates de v2 shipped (typing avatars, read-by,
jump-to-message highlight con su decisión de visual-puro-sin-sema y el
gotcha del boolean en attrs de presencia), y los frentes v2 restantes
(chat-list, emoji-picker, threads, menciones, rangos bidireccionales) por
valor/independencia. docs:check 0 errores.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tercer y último remate de v2 (batch elegido por el usuario). chat-message
gana el prop `emphasized` → attr `data-emphasized`: al saltar a una cita
(reply.onJump → scrollToIndex) la app enciende `emphasized` ~1.4s y la fila
objetivo hace un flash suave que se desvanece.
VISUAL PURO — sin evento de sema (desvía a propósito del signal-emphasize
-target del plan): las apps de referencia hacen un flash SILENCIOSO; un
signal que pitara por defecto sería molesto. Cumple 2-de-3 (soma escribe el
attr, eidos lo lee). El pulso = toggle de dos estados + transition (cero
@keyframes, R-4.5); reduced-motion = instantáneo.
Morfo attr + soma prop/provider (devuelve el booleano al resolver, no '')
+ eidos recipe (wash color-mix 14% de toda la fila) + demo componente
(botón flash) + escaparate (reply → jump → emphasize). next-features §7
marca los 3 remates shipped.
Verificado (Playwright): flash on → bg tintado → auto-clear a 1.4s;
escaparate intacto. audit PASS, eidos-lint invalid=0, recipe-contract
30/30, check en baseline (76).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- imagenes-base64.md (NUEVO): investigación completa del atasco (raíz base64-en-
modelo sin adapter; descartado historia/render; fix de perf 79a7e4dea html→blocks;
residual ~4ms/MB intrínseco del navegador) + 4 opciones de fix de raíz (PENDIENTE
de decisión) + método de repro. Para retomar mañana sin re-investigar.
- continuar-palabras.md: bitácora del día (copyBlock + investigación + perf fix) +
«Próximo» apunta el base64 a DECIDIR+implementar (ver el doc).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Investigando el atasco de imágenes base64 se destapó un anti-patrón general:
SEIS effects del chrome (bubble/slash reposition · activate re-stamp · grip/
column-inserter/table-resize remeasure) dependían de `provider.html` como señal
de «cambió el contenido». Pero `html` es el `join('')` O(documento) de TODOS los
htmls de bloque, así que cada tecla lo recomputaba + reasignaba el string entero
(con imágenes base64 en el modelo, MB por tecla; con docs de texto grandes, O(texto)).
Migrados a `provider.blocks` / `api.blocks` — la MISMA señal (el array que el
render ya computa, referencia nueva por cambio, cubre contenido + decoraciones)
pero O(1). `html` deja de computarse en el hot path (solo en export/tests).
Comentarios en cada sitio para que no vuelva a `html`.
NOTA: este NO es el coste dominante del atasco base64 (medido: ~igual antes/
después). El residual (~4ms/MB/char) parece intrínseco del navegador — un
atributo `src` de varios MB en el DOM del contenteditable ralentiza el tecleo.
La raíz (base64 en el modelo por falta de adapter `onUploadImage`) se decide
aparte. Este commit es una mejora de hot path independiente, correcta para docs
grandes en general.
Verificado: check 0 errores palabras; Playwright — grip aparece al activar,
bubble aparece y se posiciona al seleccionar (top 363), 0 errores de consola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Segundo remate de v2. chat-message gana la parte opcional `ReadBy`: una
fila de avatares "visto por N" que la app compone (AvatarGroup), típica
de mensajes propios; el emisor la alimenta con los acuses recolectados
vía el onMessageSeen (v1). DECORATIVA (aria-hidden): el indicador Status
ya transmite "read" a AT; las caras son realce visual.
Morfo part + soma provider/wrapper + eidos wrapper/recipe (fila de
avatares muted, ejes lógicos in/out) + demo con toggle. next-features §7
marca read-by y typing-avatars como shipped.
Verificado (Playwright): la fila aparece con 3 avatares + "Visto por 3",
aria-hidden, en la columna del cuerpo. component:audit PASS, eidos-lint
invalid=0, recipe-contract 30/30, check en baseline (76).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
continuar-palabras.md §2026-07-19: bullet de copyBlock (8215a9f41) en «Lo hecho»;
quitado de la deuda del motor en «Próximo».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra la deuda del motor «copy = hack de outerHTML». El chrome scrapeaba el
`outerHTML` renderizado del bloque activo (con sus `data-palabras-*`) al
clipboard; ahora hay un comando simétrico que serializa desde el MODELO.
- palabras-provider.svelte.ts: método `copyBlock(index)` (async) — serializa el
bloque top-level envuelto en un doc de 1 (`serializeHtml` → text/html limpio +
`renderPalabrasPlainText` → text/plain) y lo escribe al clipboard realm-aware.
Guard `typeof ClipboardItem` (fallback writeText en realm sin él). Non-mutating
→ funciona read-only. Expuesto en el api snippet + ProviderSnippetProps (types).
- palabras-menubar.svelte: `doCopy` usa `api.copyBlock(loc.path[0])` (fuera el
`new ClipboardItem/Blob` + outerHTML); gate `!loc`; announce «Bloque copiado».
- palabras-announce.ts: `PALABRAS_ANNOUNCE.copied`.
La serialización (doc de 1 bloque) ya la cubren los tests de serialize-html /
plain-text. Langs es/en de `announce.copied` DIFERIDO (entanglement; ts fallback).
Verificado Playwright (permisos de clipboard): copiar el H1 → clipboard
`text/html` = `<h1>Cómo migrar sin downtime</h1>` LIMPIO (0 `data-palabras-*` →
es serialización del modelo, no scrape), `text/plain` correcto, announce «Bloque
copiado»; 0 errores. Ese HTML limpio es justo lo que pasteAfterBlock parsea →
round-trip simétrico. check 0 errores palabras.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra Stage 1 de la iniciativa next-features §1 con los veredictos del usuario:
- reference.md §40 (doctrina standing): contrato de contraste por pares de
slots. Texto = dos tiers (text·11 secundario ≈APCA 60 / text-strong·12 AA
garantizado). Bordes = dos tiers WCAG 1.4.11 (decorativo exento: accent·7,
subtle·4/default·6, reposo-sobre-fondo · portador 3:1: solid·9 lo cumple, el
resto con señal redundante). Foco = eje de config (primitives.focusRing +
color.focus, un solo `outline` por §32); default suave, endurecer = valores
por config, no código.
- changelog.md §44: el chronicle datado (drift medido + veredictos + la
herramienta scripts/contrast-audit.ts).
- next-features.md §1: Stage 1 marcado DONE; Stage 2 (generador
by-construction) sigue abierto, gated en la migración base→seeds.
Sin cambios de código (el hallazgo del foco es una decisión de valores-por-
defecto vía config; defaults mantenidos por decisión del usuario). El script
de auditoría (commiteado antes) queda como herramienta de medición.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Primer remate de v2 (elegido por el usuario). chat-typing gana una parte
opcional `Avatars`: un slot antes de los dots donde la app compone
`AvatarGroup` / `Avatar` para quienes escriben. Decorativo (aria-hidden:
la label ya los nombra). Morfo part + soma provider/wrapper + eidos
wrapper/recipe (layout mínimo; el AvatarGroup ya apila) + demo con toggle.
Verificado (Playwright): slot presente con 2 avatares al activar 2 typers,
aria-hidden, antes de los dots; ausente en idle; toggle off = solo dots.
component:audit PASS, eidos-lint invalid=0, check en baseline (76).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El handoff listaba «aria-live announce» como próximo, pero se hizo en 5150bfb38.
- continuar-palabras.md §2026-07-19: bullet de F3.6 announce en «Lo hecho»;
quitado de «Próximo»; sus keys de langs añadidos a la deuda diferida.
- PLAN-EJECUCION.md item 6 (A11y del chrome) → ✅ (Ctrl+. a8a75b196 + announce
5150bfb38 + focus-trap/breadcrumb del framework).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extiende el script de Stage 1 con el análisis que pidió el veredicto de
usuario: mide TODOS los tokens semantic-border (subtle·4 / default·6 /
strong·8 / focus-ring / focus-ring-error / invalid·7 / checked·9), cada uno
contra las 3 superficies (default·1 / raised·2 / muted·3) en ambos modos,
marcando load-bearing (3:1 requerido) vs decorativo (exento). Añade
compositing sRGB para los focus rings translúcidos (color-mix @ ~50% alpha).
Fix: oklchToGammaRgb devuelve [0,1] en tupla [r,g,b] (no {r,g,b} ni [0,255]).
Hallazgo: todo el vocabulario de border vive en el rango sutil 4-8 (contrato
Radix); el único que cruza 3:1 es el solid·9 (checked/selected). El resto es
correcto como decorativo salvo el FOCO (indicador único, WCAG 2.4.11) → se
verifica en navegador aparte.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El único item del alcance v1 acordado (plan §4.2, "lectura por visibilidad")
que se había quedado fuera al cerrar F3. chat-log gana el hook
onMessageSeen(id): observa las filas renderizadas con IntersectionObserver
(root = viewport) y dispara UNA vez por id al entrar en vista (dedup interno
vía seenIds); la app manda el acuse e ignora los ids que no son mensajes
(los eventos de sistema también disparan).
Verificado en navegador (Playwright): 8 filas vistas al montar → 12 al
revelar historial arriba → 12 al volver (dedup; nunca supera las filas
totales). Provider test (harness) + lector "seen" en la demo + README +
tabla API. component:audit PASS, check en baseline (76).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cierra la pata de a11y pendiente de F3.6 (announce). El chrome anuncia sus
acciones estructurales por una única región viva compartida, para lectores de
pantalla.
- palabras-announce.ts (NUEVO): contexto ligero `announce(ref)` (`ts()` ref, el
shell lo resuelve → los llamadores quedan locale-agnósticos) + vocabulario
`PALABRAS_ANNOUNCE` (mover arriba/abajo · eliminar · insertar · pegar ·
combinar · dividir), compartido por menú y panel. Vía CONTEXT (no props)
porque los anunciadores están repartidos por menubar/panel/docPanel, todos
descendientes del único shell que posee la región.
- palabras.svelte: región `[data-palabras-live]` (role=status, aria-live=polite,
aria-atomic) al pie del shell + `setPalabrasAnnounce`. Clear-then-set en el
frame siguiente (`dom.requestFrame`) para que el SR re-anuncie un mensaje
idéntico consecutivo (solo dispara al cambiar el TEXTO).
- palabras-chrome.css: región visualmente oculta (clip/1px).
- menubar + panel: `announce(...)` en mover/eliminar/insertar/pegar + merge/split
(handlers `doMerge`/`doSplit` extraídos, dedup del inline).
Langs es/en de los mensajes DIFERIDOS (fichero de langs entangled; `ts()` por
fallback castellano).
Verificado Playwright headless: región role=status/aria-live=polite/oculta;
«Mover abajo»→«Bloque movido abajo», Insertar→«Bloque insertado» (texto real de
la región); 0 errores. Delete/merge/split = mismo `announce()` de una línea en
el mismo patrón de handler (verificados por consistencia; el harness no conduce
fiable el menú Borrar por gating/hidratación).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pase de medición de la iniciativa #1 de next-features: comprueba si las
promesas de contraste por par de slots se cumplen en las 33 escalas × 2 modos,
reutilizando la matemática de `$color` (apcaLc · wcagContrastRatio ·
oklchToGammaRgb · safeParseColor). WCAG 2 = gate normativo (4.5:1 texto /
3:1 no-texto); APCA Lc reportado al lado. Tabla de pares = las promesas
declaradas (texto·11/12 sobre track·1/bg·2/element·3; border·7 sobre track/bg).
Solo mide — no cambia emisión. Correr: `node --import tsx/esm scripts/contrast-audit.ts`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La entrada #6 (THM-2 rollout) decía que los controles semánticos (checkbox/
radio/stepper/select/file-upload) y `Avatar.Badge` quedaban role-restricted
"por diseño". La iniciativa open-color-cage (2026-07-18/19) revirtió justo eso:
`color` = sistema completo en TODOS los componentes. Se añade la nota de
reversión con puntero al handoff; qr-code queda fuera por diseño. Sin cambios
de código.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documentación del bloque chat-* recién shippeado, siguiendo la convención
del corpus (por-componente = READMEs dobles; por-lib = README; doctrina de
bloque = design record en decisions).
- docs/decisions/design-chat-block.md: la doctrina cross-cutting que abarca
los 4 componentes — la tesis comparativa (5 fallos del sector = nuestras
oportunidades), la topología de composición (triple registro Feed +
VirtualList anclado, Textarea, FileUpload, Icon lucide), el anclaje
(sticky monótono, pin contra DOM real), la doctrina visual del rediseño
de referencia y los 2 fixes de framework que destapó. Indexado en
decisions.md (sección "Component families — design records").
- src/libs/chat/README.md: los helpers puros (groupIntoRuns ventana 8min,
aggregateTypers, límites de día) — convención de README por lib.
docs:check 0 errores (503 docs). Los READMEs por componente y next-features
§7 ya iban en el commit del bloque (4c4878f13).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Familia chat-* completa, componiendo el ecosistema (Feed + VirtualList
anclado, Textarea, FileUpload, Avatar, Popover, Icon). Transporte-agnóstica:
el modelo de datos es de la app, los componentes reciben props + snippets.
- chat-log: Feed + VirtualList(anchor:'end') en triple registro; day
separators, unread divider, filas de sistema (Feed.Article), pill "N
nuevos" + notifyIncoming (announce a live-region), to-latest distance-aware.
- chat-message: raíz Feed.Article; runs ($libs/chat), ticks de entrega,
reply embebido, reacciones (chip pressed + tapback panel), acciones hover.
- chat-composer: Textarea (doble registro) + FileUpload; teclado
Enter/Shift+Enter/Esc/ArrowUp; typing saliente debounced (uix.timers);
barra reply/edit.
- chat-typing: sustain-streaming (visual-only, loop del sistema);
aggregateTypers 1/2/3+ con plantillas plurales.
- $libs/chat: groupIntoRuns (ventana 8 min estilo Discord) + aggregateTypers.
- VirtualList: extensión anchor:'end' (followOutput, prepend por clave
estable, sticky monótono, onAtEndChange) — sin scroller paralelo.
Diseño de referencia (WhatsApp/Telegram, pase lado-a-lado): burbujas suaves
(out = primary-element + texto normal; in = surface card + sombra) sobre
lienzo muted del log, meta dentro de la burbuja, tapback bar horizontal,
iconos lucide del ecosistema (nada de SVG a mano). Contrato de tamaños:
size="md" = 16px canónico.
Fixes de framework (afectaban más allá de chat):
- EngineLangs.t() ahora pela el prefijo '#?': los idlangrefs imperativos
(pagination/file-upload/chronos/chat) caían al fallback EN en locale es.
Fix parseLangRef ?? parsePathFallback + test de regresión (132/132).
- virtual-list commit-set-resize con channels:[]: es bookkeeping de
ResizeObserver, no gesto → dejaba de emitir beep/vibrate pre-interacción
(interventions de navigator.vibrate / AudioContext).
Escaparate messenger en /uix/demos/chat (bot de eco con uix.timers;
variante $connection señalada). Gaps v2 registrados en next-features §7.
Pase: component:audit 4×PASS · eidos-lint invalid=0 · recipe-contract
29/29 · Playwright escaparate PASS end-to-end · consola limpia ·
npm run check en baseline (76, cero errores nuevos).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Actualiza el handoff y el changelog §43 al estado FINAL de la iniciativa
(cerrada, cuarta sesión):
- open-color-cage-2026-07.md: añade las 2 fugas transitivas cerradas
(card-group-item · s-text) + el guard runtime, el fix del bug de Card/Avatar
(`b93cc6c5c`), la lista completa de commits (sesiones 3–4), y una sección
«Follow-ups» con lo menor que queda (override docs-chrome sobre `<Code>`,
ringColor roles-only, límites de los 2 guards, comentarios históricos) +
nota del estado del working tree (WIP de chat/palabras sin commitear, ajeno).
- changelog.md §43: el bug de Card/Avatar pasa de «flageado, NO arreglado» a
ARREGLADO con el resumen del fix (migración a resolveComponentColor + compose
del seed + canales ring/badge separados intactos).
Sin cambios de código. La iniciativa queda CERRADA sin huecos abiertos
(qr-code excluido por diseño). Contract 30/30.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BUG (descubierto en la verificación adversarial de la iniciativa): un color
CSS crudo (p.ej. `color="#3b82f6"`) NO pintaba el FILL de Card ni Avatar —
caía a neutral. Card/Avatar (Fase 2a) usaban un split bespoke pre-helper
(`isCanonicalColor` + seed `--{c}-color-custom` + bloque derivador
`[data-{c}][data-color-custom]{--_{c}-palette-*: …}` en su .css). Ese bloque
quedó ENSOMBRECIDO por el forward compartido de THM-2 en `generated/base.css`
(misma especificidad (0,2,0), carga después → gana); para un valor crudo
`--palette-*` está sin definir (el shared layer lo deriva de `--color-custom`,
NO de `--{c}-color-custom`) → host neutral.
Fix (los alinea con TODO el resto de la iniciativa):
- card.svelte / avatar.svelte: el split bespoke → `resolveComponentColor`
(genérico: canónico→`data-color`; crudo→`data-color-custom` + seed
`--color-custom`, que el forward YA consume). Ambos ahora COMPONEN el seed
con el `style` del consumidor (`composeInlineStyle`) para que `...rest` no lo
pise — Card lo pisaba (style antes de rest), por eso el custom se perdía
incluso con el seed correcto.
- card.css / avatar.css: borrados los bloques bespoke `[data-{c}][data-color-
custom]{…}` (muertos + ensombrecidos). Las variant slices leen
`--_{c}-palette-*`, que el forward llena para rol/escala/custom uniformemente.
- Avatar RING y BADGE son canales de color SEPARADOS (`--avatar-ring-color-
custom` / `--avatar-badge-color-custom`): NO se tocan — usan su propio
namespace de tokens, el forward no los ensombrece y funcionan (verificado).
- base.ts: quitados los 4 tokens de recipe muertos (`card`/`avatar` ·
`color-custom` + `color-custom-contrast`); ring/badge se conservan.
Verificado en Chrome: Card real (picker del demo) + Avatar sintético pintan el
custom en soft/solid/outline/ghost, componiendo el style del consumidor;
roles/escalas siguen exactos (behavior-preserving); ring + badge intactos.
Orphan test resuelto (los `-contrast` ya no huérfanos). Los 2 fallos residuales
del contract son WIP de chat-composer (otro track, sin commitear), ajenos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cierra la UI de F4 (las tres piezas elegidas por el usuario: listas · merge/split
· count/TOC · resize). Alcance v1 acordado: solo COLUMNAS (sin filas ni snap).
palabras-table-resize.svelte (NUEVO): overlay de tiradores en el borde superior
de cada tabla top-level (uno por borde interno de columna), materializados solo
al pasar el puntero por la tabla (mirror de palabras-column-inserter: medición
frame-relative, remeasure en api.html + scroll/resize). Arrastrar un tirador
redimensiona la columna a su IZQUIERDA:
- feedback EN VIVO por inline `width` en la celda de la 1ª fila durante el drag
(el navegador dimensiona la columna por la celda más ancha),
- commit a `cell.width` en UN paso de historia al soltar (nada durante el drag),
- doble-clic = auto (limpia el width).
`cell.width` es una LONGITUD CSS en string (el campo `length` del panel commitea
p.ej. "314px") — un número crudo renderiza `width:314` inválido; el commit usa
`${w}px`. Restringido a tablas TOP-LEVEL (path del commit `[table,0,col]`).
Filas + snap a fracciones (1/2·1/3·2/3) diferidos a v1.1. Langs `resize-column`
diferido (entanglement; ts() por fallback).
Verificado Playwright headless: hover tabla → 2 tiradores (3 columnas); arrastrar
+80px → col0 234→314 (las otras se ajustan 183/196), commit width:314px; doble-
clic → auto restaurado (234/221/238); 0 errores consola. Screenshot MIRADO
(tirador de acento en el borde hover). check 0 errores palabras.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra F4.7. Los derivados wordCount/charCount/tableOfContents ya vivían en el
snippet del provider; faltaba su affordance. Decisión del usuario (2026-07-19):
barra de estado al pie con el recuento (readout pasivo) + botón «Índice» que
abre un popover de encabezados; clic hace scroll al heading.
- palabras-status.svelte (NUEVO): «N palabras · M caracteres» + `<Popover>` con
`tableOfContents` (sangrado por `level` vía data-level); jumpTo hace
scrollIntoView del `[data-palabras-id]` del encabezado y cierra el popover.
El trigger se deshabilita sin encabezados.
- palabras.svelte: montada al pie del shell (los 3 modos — el recuento es un
readout del documento, no una propiedad; el usuario lo quitó del panel).
- palabras-chrome.css: sección [data-palabras-status] + outline (tokens).
El índice va en affordance PROPIA (no el panel de propiedades — decisión del
usuario 2026-07-18: es navegación, no una propiedad de página).
Langs es/en (status.words/chars/outline/untitled) DIFERIDOS (fichero de langs
entangled con el merge pendiente; los `ts()` renderizan por fallback).
Verificado: check 0 errores palabras; Playwright headless: «45 palabras · 253
caracteres», popover con 2 encabezados (niveles 1/2), clic en «Estrategias» →
popover cierra + h2 en vista; 0 errores consola. Screenshot MIRADO.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chart era el último hueco de la iniciativa. Su `color` NO tinta labels/ejes:
tinta la MARCA de datos (stroke de línea, fill de área/barra/burbuja, hue de
heatmap, arco de gauge, porción de pie/funnel/bar-list/polar, punto de smith).
Es SVG con resolución JS, así que NO usa el `data-color` + capa de paleta
compartida (eso es para cascadas CSS en HTML) — el mecanismo es propio.
Resolver central en `chart/context.ts`:
- `seriesColor` / `seriesSurface` reescritos + `seriesContrast` nuevo, todos
`number | ComponentColorProp`. Un índice cicla `SERIES_ROLES` (multi-serie,
sin cambios). Un valor explícito pasa por `resolveChartColor`:
· rol/intent → `var(--color-{role}-{solid,surface,contrast})`
· una de las 33 escalas → `var(--scale-{name}-{9,a2})` (steps solid /
surface-alpha del `PALETTE_SLOT_STEP`); contrast → white (no hay token)
· valor CSS crudo → verbatim (surface = `color-mix 15%`, contrast = white;
sin contraste garantizado, el trade-off de cualquier custom).
- `ChartContext.color` widenado a `number | ComponentColorProp`.
Todos los props `color?: ColorRole` (~14: ChartSeriesProps, Bubble, Sparkline,
ChartCategory, BarList, Funnel, Calendar/Heatmap, Smith×2, Gauge) →
`ComponentColorProp`. Los 3 builders que armaban el fill inline (funnel:
solid+contrast, heatmap:56, calendar-heatmap:118) refactorizados para pasar
por el helper — funnel pierde su `roleOf` local.
qr-code queda FUERA por decisión de diseño (color = tinta de módulos del QR,
contraste con el fondo, no encaja escalas).
Verificado en Chrome: las 3 ramas del resolver resuelven a color real
(--scale-teal-9→oklch, --scale-teal-a2→rgba translúcido, #3b82f6→verbatim,
mix translúcido ok); marcas existentes = primary-solid (roles behavior-
preserving). check chart limpio; contract 30/30. Docs: changelog §43 +
handoff actualizados (hueco chart cerrado, qr-code excluido por diseño).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expone los comandos mergeTableCells/splitTableCell del motor en el chrome,
gateados a celda activa de tabla top-level.
Desviación declarada respecto a "solo UI": los comandos resolvían la celda vía
`state.selection`, que el chrome PIERDE al abrirse fuera del editor (el menú
Editar / el panel). Ajuste mínimo del motor, consistente con mover/eliminar
(que ya operan por `loc.path`): mergeTableCells/splitTableCell aceptan un `at`
explícito `[block,row,cell]` (fallback a la selección). Test nuevo con
`selection: null`.
UI (eidos):
- palabras-block-actions.ts: `tableCellActions(loc, activeEl)` deriva inCell +
`at` (desde loc) + canMergeRight/Down/Split (geometría del <td> renderizado) +
dispatch `mergeActiveCells`/`splitActiveCell`.
- Menú Editar (barra): 3 items gateados (Combinar derecha `cols:2` / abajo
`rows:2` / Dividir).
- Panel: fila de acciones de celda PROPIA — el `blockFoot` (mover/insertar) se
oculta para celdas structural, así que las celdas tienen su pie.
Restringido a tablas top-level (el resolver del motor direcciona
[block,row,cell]). Langs es/en de las 3 acciones DIFERIDOS (fichero de langs
entangled con el merge pendiente; los `ts()` renderizan por fallback).
Verificado: motor 37 tests (2 nuevos del `at`); check 0 errores palabras/table;
Playwright headless en AMBAS superficies: barra Editar → Combinar derecha →
celda colspan=2 (B1 absorbida) → Dividir la restaura; pie del panel acoplado
igual. 0 errores de consola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Los campos start/numbering del panel de lista (motor + escritura ya cableados
desde 5411becb5) aparecían para viñetas y tareas, donde no tienen sentido.
Añade kinds:['ordered'] a ambos — mismo filtro que ya gobierna marker/box.
Verificado en navegador: el modelo start:3/numbering:'i' proyecta a
<ol start="3" type="i"> y el navegador pinta lower-roman (iii, iv).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verificacion adversarial multi-agente (17 componentes de la sesion de cierre,
23 agentes) cazo 2 fugas que la verificacion individual en Chrome no cubrio —
componentes abiertos TRANSITIVAMENTE (heredan un alias abierto) y nunca
tocados ni verificados:
- **card-group-item**: estampaba `data-color={color}` CRUDO en ambas ramas
(button + div) sin `data-color-custom` ni seed → un valor CSS crudo (que el
tipo abierto `CardColor=ComponentColorProp` permite) caia a neutral y se
descartaba en silencio. Ahora pasa por `resolveComponentColor` (ruta
GENERICA `--color-custom`, que el forward compartido `[data-card][data-color-
custom]` consume). NOTA: NO usa el split bespoke de Card (`--card-color-
custom`) a proposito — esa ruta esta ENSOMBRECIDA por el forward de THM-2 y
no pinta (bug pre-existente de Card/Avatar, flageado aparte). Verificado en
Chrome: teal=escala exacta, #3b82f6=palette-solid exacto, neutral=sin estampa.
- **s-text**: `STextColor = TextColor` se abrio gratis al ensanchar TextColor
en Fase 4, pero su .svelte seguia en el puente pre-Fase-4
(`var(--color-content-${colorKey})` para TODO valor) → escalas/intents/crudo
producian vars inexistentes. Migrado a la rama eje-primero de Fase 4 (espejo
de Text): ink axis inline sin estampa; resto por el forward. Recipe
`_palette-text` + regla colored-only + stamp. Verificado en Chrome
(risk/teal exactos, custom aplicado, default sin estampa).
- **split-button**: revisado, SIN cambios — delega `{color}` crudo en
ButtonGroup→Button (ya verificado); no re-estampa.
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), asi que un stamp dinamico sin el par custom pasa
inadvertido (justo la clase de card-group-item). Este falla si algun .svelte
estampa `data-color={…}` dinamico sin `data-color-custom` en el mismo fichero;
literal estatico (`data-color="neutral"` del badge +N de avatar-group) exento;
WIP excluido. 30/30 verde.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El fix crítico de la auditoría (insertLineBreak enruta code/table a \n a
CUALQUIER profundidad, no solo top-level) iba sin test. Añade 5 casos en
paragraph-ops.test.ts:
- code top-level → \n, sin nodo break (fija la rama).
- code anidado en callout → \n, sin break (DISCRIMINA).
- code anidado en columna → \n, sin break (DISCRIMINA).
- celda de tabla → \n vía Shift+Enter, no solo Enter (fija la rama).
- tabla anidada en callout → \n en la celda (DISCRIMINA).
Discriminación demostrada revirtiendo el fix al check solo-top-level: caen
EXACTAMENTE los 3 casos anidados y los 2 top-level siguen verdes. Con el fix
puesto, 30/30 verde; check con 0 errores en palabras.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Los 3 tests recomendados por la auditoría (autorados + verificados
adversarialmente: cada uno falla si se revierte el invariante que fija).
- normalize.test.ts (nuevo): fija el flanqueo de texto de los break. 5 casos
vía normalizeDocument con factories reales: break al inicio / al final / entre
dos breaks / junto a un link → siempre queda un text inline a ambos lados;
break ya flanqueado → sin empties espurios. Caen si se borra el paso de
normalize.ts:299-317.
- palabras-provider.svelte.test.ts (+2): (a) slashMenuItems filtra por la
allowlist insertableBlocks (['heading','paragraph'] excluye code-block; []
vacía; undefined = todo) — cae si se quita el filtro del motor. (b) paridad
canonicalSlashType (soma) === canonicalBlockType (eidos) sobre el set real de
ids del registry — cae si las dos copias byte-idénticas del cruce de capa
divergen.
- palabras-provider.svelte.ts: canonicalSlashType pasa a exportarse (única
fuente del cambio) para poder compararla contra la de eidos.
Verificado: 58 tests verdes (5 + 53); check con 0 errores en palabras.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard: test nuevo "keeps every component `*Color` prop open (THM-2 — the
cage stays open)" en recipe-css-contract.test.ts — escanea los alias
`export type XColor = …` de components/*/types.ts y FALLA si alguno se
estrecha por debajo de ComponentColorProp. Uniones aditivas ('muted',
'inherit', 'absent'…) pasan; alias resuelven transitivamente
(DatePickerColor = CalendarColor); WIP_TRACKS (chronos) re-entra con su
track; exento comentado: OnionColor = string (MAS ancho que la jaula,
normalizacion pendiente). Verde con el inventario real: 60+ alias abiertos.
Docs (todo lo aplazado del track):
- reference.md §25: reversion de los subconjuntos THM-2 registrada como
doctrina (color = sistema completo en TODOS; identidad ≠ evaluacion;
enforcement = el guard).
- Tracker clean-room §THM-2: nota de reversion (los narrows que THM-2 dejo
"por diseño" quedan abiertos, Avatar.Badge incluido).
- changelog.md §43: cronica de la iniciativa completa (mecanica, fases,
huecos señalados).
- Handoff open-color-cage-2026-07.md → CERRADO con el resumen de la tercera
sesion (cola completa, F4, F5, commits) y los 2 huecos pendientes de
decision: chart (11 props ColorRole) y qr-code (color inline semi-abierto,
invisible para el guard). El cuerpo queda como referencia de patrones A–E.
- 13 READMEs con afirmaciones ya falsas corregidos (roles-only, subsets,
"Locking to ColorRole", tablas de props): badge, css-field, field-langs,
float-panel, link, listbox, mark, metrics (solo el icon; Chart delega en
chart y queda como esta), password-field, proof-of-human (incl. la fila de
decision "descartar" → "hecho (reversion)"), range-calendar, textarea,
timeline. banner NO se toca (su ColorRole es del eje intent, otra cosa).
Contract test: 29/29 verde.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Auditoría multi-agente (hallazgos verificados adversarialmente). Arreglos:
- CRÍTICO: insertLineBreak detectaba code/table solo en el bloque TOP-LEVEL, así
que un code block ANIDADO (dentro de una columna o callout) recibía un nodo
break (ilegal en code text-only — validate lo rechaza) en vez de un \n literal.
Ahora camina el container path: el code block es la hoja, la table un ancestro
de la celda.
- El parser de NOTACIÓN (parsePalabrasScheme) no conocía marks/blocks, así que un
esquema en forma de notación no podía gobernar (marks=/blocks= caían en el
scale-shortcut como NaN y se descartaban). Añadido el parseo (coma-lista; vacío
= ninguno), restaurando la paridad notación<->objeto (ESQUEMA §9).
- Bubble: el Select de familia se renderizaba VACÍO cuando el esquema declara
fonts:[] (dropdown muerto). showFamily ahora exige allFonts.length > 0.
- Doc: marks NO gobierna color/background (el color va por editable + la paleta)
— corregido en types.ts + ESQUEMA §6b, que lo documentaban por error.
Tests nuevos: parseo de notación marks/blocks + lock-down (marks:[]/blocks:[] =
nada permitido, que sobrevive al merge).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Los 5 primitivos tipograficos tenian OTRO eje de color: la tinta de contenido
(primary|secondary|muted|disabled|on-solid, donde primary = --color-content-
primary, NO el rol), interpolada inline por el wrapper (--_x-color) sin
data-color ni cascada CSS. Abrir el tipo a pelo rompia: color='teal' →
var(--color-content-teal) inexistente; 'muted' via resolveComponentColor →
--color-custom: muted invalido.
Apertura con RAMA EJE-PRIMERO (identica en los 5):
- tipo: `ComponentColorProp | 'muted' | 'disabled' | 'on-solid'` (label sin
on-solid — conserva su union exacta de 4). primary/secondary ya estan en
ComponentColorProp pero la rama los captura ANTES y les conserva el
significado de TINTA ("conserva su eje", handoff §Fase 4).
- wrapper: valor ∈ eje → var inline como siempre, SIN estampa; resto →
resolveComponentColor (canonico → data-color; raw → data-color-custom +
seed). Los dos caminos son mutuamente excluyentes.
- css: regla colored-only `--_x-color: var(--_x-palette-text)` — no pelea con
el eje porque el eje nunca estampa.
- recipe: `_palette-text` nueva por componente (host = fallback-only).
Text NO estaba en la lista del handoff (code/display/heading/label) pero tiene
la MISMA union cerrada y es el primitivo modelo de la familia — incluido y
declarado (dejarlo fuera era una incoherencia de la propia fase).
Verificado en Chrome: heading con las DOS ramas y prop real (chip muted →
inline var sin estampa; risk/teal/custom = probe exacto); label idem (la demo
monta color='primary' real → inline, y risk exacto sin el inline); text/
display risk/custom exactos; code el token fluye exacto — su pisoton visible
es una colision PRE-EXISTENTE del chrome de docs ([data-uix-docs] code, ya
pisaba el eje viejo; señalada como tarea aparte). Contract test 28/28 —
primera vez todo verde.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apertura (era el ultimo standalone con cascada):
- El accent del reto (2 slots: solid + element-soft) pasa de 7 bloques
per-rol al forward compartido en la parte `root` (display:contents →
cascada a todos los retos, verificado que el skin hereda). Recipe NUEVA
(su comentario "needs no recipes/base.ts entry" quedaba obsoleto).
- `ProofOfHumanColor = ComponentColorProp` (era ColorRole, README decia
"8 roles-only"). Root + los 3 retos (clock/path-trace/rotate-align)
estampan via resolveComponentColor — sus stamps son CSS-muertos hoy, pero
con el tipo abierto habrian estampado raw strings como data-color.
Exencion fixed-tone (los "2 fallos pre-existentes" del contract):
- FIXED_TONE_COMPONENTS se hoista y comparte entre el test de color Y el de
font-size, añadiendo proof-of-human: la paloma mensajera + carta son
paleta identitaria del objeto dibujado (anotada linea a linea) y los
font-size 13px/11px son unidades de usuario SVG del viewBox, no escala
tipografica (anotacion añadida al .ra-card que no la tenia). Nota: esto
tambien exime a natural-time-picker/color-picker del test de font-size
(hoy limpios; su doctrina fixed-tone ya esta documentada).
card-group: revisado, SIN cambios — card-group-item ya usa `CardColor`
(= ComponentColorProp desde Fase 1): abierto por delegacion.
Verificado en Chrome: default primary = probe exacto (2 slots), risk/jade
exactos, skin hereda por display:contents, custom → seed + mix element.
Contract: mis 2 pre-existentes ELIMINADOS; unico fallo restante =
chat-message `--radius-xs` (WIP vivo de otra sesion, ajeno).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El esquema (fijado por el desarrollador, NO editable por el usuario) ahora gobierna qué
marcas y qué tipos de bloque puede usar el usuario, no solo el panel. Allowlists top-level
`marks` + `blocks` en PalabrasScheme (doctrina «sin declarar = todo permitido», como
`editable`) + lectores `schemeAllowsMark` / `schemeAllowsBlock`.
- Bubble: filtra las 5 marcas booleanas + gatea sub/sup/tamaño/familia por `marks` (los
valores paramétricos siguen saliendo de fonts/sizes/palette).
- Slash: filtrado en el MOTOR vía nuevo opt `insertableBlocks` del provider (para que el
índice de teclado no derive) — filterSlashCommandItems(query, allowed) con mapeo canónico
id→tipo.
- Menús eidos (menubar / panel / column-inserter): filtran vía insertableBlockTypes(scheme).
ESQUEMA.md §6b documentado. Verificado: 64 tests (gobernanza + provider con el opt), check
0 errores en palabras, repro Playwright default (bubble 5 marcas + Aa, slash 14 items, 0
errores de consola).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El accent (regla del header, focus ring, grip de resize) pasa de 8 bloques
per-rol al forward compartido en la parte `content` (el panel portala y
estampa su propio `data-color` via ctx). El default real NO era el "fantasma"
que temia el handoff: la recipe SI tiene `accent: neutral-border` y el root
siempre estampa `color='neutral'` por defecto → estampa-siempre + regla
generica es fiel (host del forward = neutral-border, el valor del default).
El fallback sin-ctx (`var(--float-panel-accent)`) queda intacto.
Tipos: `FloatPanelColor = ComponentColorProp` (era ColorRole); el Content
resuelve via resolveComponentColor con seed `--color-custom` para raw values.
Verificado en Chrome: default neutral = probe exacto; threat/iris = token
compartido; sin estampa → fallback neutral-border; custom → color-mix del
seed. Contract test sin fallos nuevos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La trampa: el accent vivia en DOS scopes — cascada per-rol en el root y una
re-declaracion fija `primary-border` en el contenido portalado. El forward del
root nunca alcanzaba el portal: el prop `color` solo tintaba el ring del
trigger; los rings del portal (area-thumb, slider-thumb, eye-dropper) eran
SIEMPRE primary.
Solucion (espejo de select): el forward apunta a las DOS partes que estampan —
recipe `_palette-border` con `parts: ['trigger', 'content']` (fusionado en la
recipe existente). El trigger consume el token directo en su ring; el content
re-deriva su `--_color-picker-accent` del token EN SU scope (sus descendientes
lo heredan como antes). Root: fuera la decl base + la cascada de 7 roles; el
provider solo estampa el marcador canonico y el color RAW viaja por contexto
(Trigger/Content/ChannelInput lo resuelven cada uno via resolveComponentColor,
con seed `--color-custom` para valores raw). El marker div del ChannelInput
enruta ademas el forward de color-field, como un ColorField standalone.
Tipos: `ColorPickerColor = ComponentColorProp` (era ColorRole).
Verificado en Chrome con el popover abierto: content portalado estampa su
propio data-color; default primary = probe exacto en trigger y content;
risk/teal en el scope del portal = token compartido; custom → color-mix del
seed; el area-thumb hereda el accent. Contract test: mis cambios no añaden
fallos (los 2 de chat-composer son WIP de otra sesion con recipe fuera del
tree; 2 de proof-of-human pre-existentes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tres aperturas de bajo peso:
- field-langs: delegante puro (patron E) — pasa `color` al `<Field>` compuesto
(abierto en Fase 3). Solo tipo: `FieldLangsColor = ComponentColorProp`.
- radio-cards: comparte elemento provider con radio-group, cuyo forward ya
enruta la paleta que las reglas de card consumen. Tipo abierto (era
AffirmativeColorRole) + ruta custom en el wrapper (seed `--color-custom`).
- image: migra el mecanismo bespoke pre-helper (`data-placeholder-color` +
seed propio) al forward compartido, paridad con avatar (su gemelo declarado).
Arregla de paso las escalas: `placeholderColor='teal'` caia a la rama custom
y pintaba el NAMED COLOR css `teal` (hue equivocado) y `'iris'` era invalido;
ahora resuelven a la escala canonica. `'neutral'` (default) no estampa y
conserva el fallback neutral-track. Fuera el token de recipe
`placeholder-color-custom` (huerfano tras la migracion).
Verificado en Chrome (probe): radio-cards risk/mint/custom en el forward y el
item checked consume borde+fondo (el gris inicial era la transition congelada
del pane suspendido, caso conocido, verificado con transition:none); image
default=neutral-track exacto, risk/teal=escala, custom=color-mix; field-langs
estampa via Field. check: 0 errores en tocados; total 77 = 76 baseline + 1 de
web/routes/uix/demos/chat (WIP de otra sesion). Contract test 26/28.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El fix anterior (`value={customColor || '#000000'}`) NO servía: el warning
"specified value '' does not conform to #rrggbb" NO viene del valor, sino de que
Svelte pone `""` en el input MOMENTÁNEAMENTE durante la HIDRATACIÓN cuando hay un
`value=`/`bind:value` reactivo (el navegador lo rechaza y resetea el input a
#000000). Diagnóstico ya documentado en `web/routes/temas/color/+page.svelte`.
Fix correcto: action `colorValue` (nueva en `lib/harness.svelte.ts`) que setea el
valor imperativamente (`node.value = hex || '#000000'`), sin atributo `value`
gestionado por Svelte → no hay `""` en hidratación. Los 21 demos con picker custom
pasan de `value={...}` a `use:colorValue={customColor}`.
Verificado tras reiniciar el dev server (el HMR de Vite servía código viejo): el
input conserva #8e4ec6, sin reset a #000000, sin warning. check: 76 (0 en tocados).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dos aperturas por partes (`parts:` en recipe, forma `declarations:`):
- metrics-icon: los 5 slots del featured icon (track/solid/text/contrast/
border) pasan de 8 bloques per-rol al forward compartido en la parte `icon`.
Fuera el guard roles-only del wrapper (caia a neutral con cualquier valor
no-rol); `plain` sigue sin estampar. Default primary estampado = identico.
El flash de valor NO se toca: gradua por `data-event-intent` (sema), no por
el prop `color`. `MetricsChartProps.color` queda cerrado: delega en la
familia chart, que NO esta en la cola del handoff (hueco señalado aparte).
- form submit/reset: 4 slots (solid/solid-hover/border/contrast) en las DOS
partes via el mismo forward. `'neutral'` (default de reset) NO estampa: la
base action look (element bg + text ink) ES el neutral — estamparlo habria
cambiado reset a solid grey. Submit default primary = identico.
Verificado en Chrome (probe getComputedStyle): metrics-icon risk 5/5 slots +
crimson + custom (contrast blanco derivado); form threat 4/4 + iris + custom;
reset sin estampa con look base intacto. check: 0 errores en tocados (total
76 = baseline); contract test 26/28 (2 pre-existentes proof-of-human).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renombra el motor headless soma `words` a `palabras` y FUSIONA sus dos contratos morfo
(motor `words` + chrome `palabras`) en uno solo `palabras`: carpeta soma/components/
words→palabras, ~2500 refs (`Words*`→`Palabras*`, `data-words-*`→`data-palabras-*`,
`WORDS_`→`PALABRAS_`, `$soma/components/words`→palabras), morfo unificado (12 eventos +
partes de chrome, scope soma/sema/eidos), vars CSS `--_?words-`→`--palabras-`. Barrel
soma actualizado.
Además dos cambios de comportamiento:
- Panel Documento: quitada la sección Paleta (es del esquema, no editable por el usuario;
los colores se aplican desde los controles de color, no como ajuste de documento).
- Shift+Enter: nuevo nodo inline atómico `PalabrasBreak` que renderiza <br> (antes metía
un \n crudo que colapsaba, invisible). Costuras: render/split (fix crash)/offsets
(ancho 1)/validate/normalize (break flanqueado por texto)/serialize (html·md·txt)/parse
(<br>↔break + guarda anti-<p><br></p> de office). Code/table siguen con \n literal. El
slash-menu ya no traga Shift+Enter.
Verificado: motor 490 tests verdes (+ nuevos de break) · check 0 errores en palabras ·
repro Playwright real (salto en medio Y al final pintan 2 líneas, Backspace limpio, DOM
100% data-palabras-*, 0 data-words-*, sin errores de consola) · eidos-lint invalid:0.
Nota: el merge de langs y las exenciones de auditoria (ACTIVE_DEV_TRACK/WIP_TRACKS) quedan
sin commitear por enredo con el trabajo de chat en curso en los barrels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`0886bfe53` aplico el fallback `|| '#000000'` uniforme a los 21 demos, pero
card era el unico con `bind:value` (two-way) y `bind:` no admite expresiones →
error de compilacion de Svelte al visitar /uix/components/card. Se pasa al
patron one-way de los otros 20 (`value={...}` + `oninput`), conservando el
boton "apply" propio de card. Verificado: la pagina compila y renderiza.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tres standalones al forward de paleta compartido (`resolveComponentColor` +
recipe `_palette-*`):
- skeleton: tinta el wash (slot track, 50% alpha) + el highlight del shimmer.
Colored-only: el default `'neutral'` NO estampa data-color y conserva su
mezcla base cross-mode (oklab + 16% solid). Delta declarado: affirm/fulfill/
risk/threat/loss ganan highlight tintado en el shimmer (antes solo primary/
secondary lo definian; la regla generica uniformiza la cobertura).
- spinner: patron C puro — la base consume `--_spinner-palette-{text,track}`
(host = primary, behavior-preserving); fuera los 7 bloques per-rol.
`'inherit'` se conserva como union aditiva con su regla literal
(currentColor); no puede viajar por el seed custom (trampa `--x: inherit`).
- textarea: tinta el borde de focus (slot solid). Colored-only: el default
`'primary'` no estampa y conserva `--focus-ring-color` (que NO es
primary-solid: es un mix al 52% — estampar habria cambiado el default).
Verificado en Chrome (probe getComputedStyle): default intacto en los tres;
risk/teal/amber/jade = token compartido exacto; custom hex → color-mix del
seed. check: 0 errores en los componentes tocados; contract test 26/28 (2
pre-existentes de proof-of-human).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
El input de color custom de los demos usaba `value={customColor}` y `customColor`
arranca vacío (''), que NO es un valor válido para `<input type="color">` (exige
`#rrggbb`) → warning en consola: "The specified value "" does not conform to the
required format".
Fix uniforme en los 21 demos con picker custom: `value={customColor || '#000000'}`.
PalettePicker.svelte ya estaba bien (input sin `value`, no controlado). Verificado
en el demo de toggle: el warning desaparece.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Ambos delegan el color a un componente ya abierto, así que es solo tipo + wrapper
(sin recipe/css):
- button-group: pasa `color` a los <Button> hijos por contexto (cada Button lo
resuelve, incl. custom). Tipo 'primary'|'secondary'|'neutral' → ComponentColorProp;
el `data-color` del <Group> (presentacional, sin consumidor) pasa por
`resolveComponentColor` para no estampar un hex crudo inválido.
- highlight: envuelve coincidencias en `<mark data-mark>` (ya forward-ready). Tipo
→ ComponentColorProp; wrapper estampa el trío en el shell `[data-highlight]` +
cada `<mark>`, con el seed `--color-custom` en el shell que los marks heredan.
check: 0 errores en componentes tocados (el total oscila 76↔94 por la caché de
svelte-check aflorando errores `any` de demos ajenos).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migra los dos content-ink al forward de paleta compartido:
- link: `color` tinta el texto. Base pasa a `color: var(--_link-palette-text)`
(host = primary-text); se quita la cascada per-rol de 8 bloques. hover/active
siguen en primary (comportamiento pre-existente). Recipe nuevo `_palette-text`.
- mark (resaltado): `element` tinta el fondo (default = fulfill, el resalte
clásico), `text` tinta la tinta SOLO cuando hay color (la base sigue
`color: inherit`; no se puede poner `inherit` como host de custom-prop — trampa
del `--x: inherit`). Regla añadida `[data-mark][data-color],[…custom]{color:…}`.
Ambos wrappers pasan por `resolveComponentColor` (custom hex → data-color-custom
+ seed). Verificado en Chrome: link risk→texto risk, custom→color-mix; mark
fulfill por defecto, risk→fondo risk, custom→color-mix.
Nota: `check` marca 94 (no 76) por la caché incremental de svelte-check aflorando
errores `any` pre-existentes en demos ajenos (heroscrolling/animations/palabras/
alpha/temas); CERO errores en componentes tocados (link/mark limpios).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>