uix(chat-log): temable — 77 % → 100 %, y el instrumento daba DIECISÉIS falsos muertos de 31

Protocolo PLAN-theming §7 sobre `chat-log`. Censo 77 % → **100 %**, contrato
31 → **35** claves, centinela **34/35** con 1 adjudicada. El default NO se
mueve: diff de computed VACÍO (1.920 valores · 7 estados · 9 nodos), capturas
2x antes/después idénticas.

## Lo caro no fue acuñar: fue VER

El guard leía **15/31** antes de tocar una línea de código, y las dieciséis
"muertas" estaban las dieciséis VIVAS. Tres cegueras, ninguna del componente:

1. **El viewport que scrollea es del componente COMPUESTO.** El log pinta su
   `padding-inline` / `padding-block` sobre `[data-virtual-list-viewport]`, un
   nodo sin un solo `data-chat-log-*`: fuera del filtro de la sonda y del
   guard. Cuarta forma del mismo punto ciego (prose · navigation-menu ·
   waveform).
2. **La demo no montaba TRES de las seis partes.** Separador de día y divisor
   de no-leídos no existían en el escenario, y la fila de evento vivía en el
   índice 0 de una lista virtualizada anclada al FINAL — fuera de la ventana.
   Once claves sin nodo que pintar. La demo los monta ya cerca del final
   (instrumento: lo que la demo no monta, nadie lo mide).
3. **La pastilla del separador es un `> span` sin atributo** — seis claves más.

Con las tres arregladas, **31/31 antes de acuñar nada**.

## Lo que entra (4 claves, la COSTURA de los cuatro globales)

`divider-padding-block` · `event-radius` · `pill-gap` · `to-latest-radius`.
El valor sigue siendo el del sistema; el knob pasa a ser del componente.

Los nombres salen del CATÁLOGO, no del generador: la ficha proponía
`divider-unread-padding-block` y `pill-new-gap` —nombra por el atributo de la
parte— cuando el contrato lleva `divider-fg` / `-line` / `-font-size` / `-gap`
y `pill-bg` / `-fg` / `-radius`. La familia manda.

## Lo que SALE: una regla que no ha pintado nunca

`[data-chat-log-to-latest] svg { block-size: 1em; inline-size: 1em }` no puede
ganar: el `Icon` compuesto emite `style="width: var(--icon-size-sm); height:
..."` EN LÍNEA, y un estilo en línea gana a todo selector. El glifo mide 16 px
(paso `sm` del icono), no los 14 px que `1em` daría sobre la fuente heredada
del botón. **Retirada: 0 diffs**, que es la prueba de que estaba muerta. Es la
clase `carousel.item-gap` / `drag-drop.preview-z`, con una vuelta más: aquí el
inline lo escribe un COMPONENTE COMPUESTO, no soma.

## Identidades firmadas (3, fuera del ratio)

`block-size: 100%` del shell y del viewport (el log ocupa la caja que la app le
da) e `inline-size: fit-content` de la pill (la dimensiona su contenido).

## Adjudicada (1)

`event-radius` — sólo pinta bajo `:focus-visible` y el guard hace blur tras
abrir. Medido enfocando con TECLADO la fila real, con `transition` congelada:
0 px en reposo, 4 px enfocada, 1234 px con el centinela.

## Guards

censo `--only chat-log` 100 % (global 67 %, no baja) · `component-audit` PASS ·
`eidos-lint` 0 invalid · `vitest run src/uix/eidos` sin rojos nuevos (los dos
vivos son ajenos: `skin-media-player` y `radio-group.row-min-height-touch`) ·
`rtl:check` 0 · `docs:check` 0 · `check` sin un solo error del componente ·
prettier limpio sobre el contenido normalizado a LF.

README con su sección «Talla y tema», demo con pestaña `Tokens` verificada en
navegador, ficha con su veredicto §5 y registro en PLAN §8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent bf90fc62b1
commit fd8f01bace

@ -5,44 +5,46 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-23 · **Alcance**: **77%** — 30 de 39 knobs por token público
- **Knobs de apariencia**: 42 — público 30 · privado 0 · global 4 · literal 5 · sistema 3 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 31 pública(s) — `bg`, `padding-inline`, `padding-block`, `separator-gap`, `separator-font-size`, `separator-fg`, `separator-bg`, `separator-padding-inline`, `separator-padding-block`, `separator-radius`, `divider-fg`, `divider-line`, `divider-font-size`, `divider-gap`, `event-font-size`, `event-fg`, `event-padding-block`, `pill-bg`, `pill-fg`, `pill-font-size`, `pill-padding-inline`, `pill-height`, `pill-radius`, `pill-shadow`, `pill-offset`, `to-latest-size`, `to-latest-bg`, `to-latest-fg`, `to-latest-border`, `to-latest-shadow`, `to-latest-offset`
- **Medido**: 2026-08-23 (tras el commit) · **Alcance**: **100%** — 34 de 34 knobs por token público
- **Knobs de apariencia**: 37 — público 34 · privado 0 · global 0 · literal 0 · sistema 3 · excepción 3 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 35 pública(s) — `bg`, `padding-inline`, `padding-block`, `separator-gap`, `separator-font-size`, `separator-fg`, `separator-bg`, `separator-padding-inline`, `separator-padding-block`, `separator-radius`, `divider-fg`, `divider-line`, `divider-font-size`, `divider-gap`, `divider-padding-block`, `event-font-size`, `event-fg`, `event-padding-block`, `event-radius`, `pill-bg`, `pill-fg`, `pill-font-size`, `pill-padding-inline`, `pill-height`, `pill-radius`, `pill-shadow`, `pill-offset`, `pill-gap`, `to-latest-size`, `to-latest-bg`, `to-latest-fg`, `to-latest-border`, `to-latest-shadow`, `to-latest-offset`, `to-latest-radius`
- **Eje `size`**: no · **ficheros**: `chat-log.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (4)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `chat-log.css:70` | `[data-chat-log-divider-unread]` | `padding-block` | `var(--space-1)` |
| 2 | `chat-log.css:94` | `[data-chat-log-event]:focus-visible` | `border-radius` | `var(--radius-sm)` |
| 3 | `chat-log.css:122` | `[data-chat-log-pill-new]` | `gap` | `var(--space-1)` |
| 4 | `chat-log.css:143` | `[data-chat-log-to-latest]` | `border-radius` | `var(--radius-full)` |
_Ninguno_ — los cuatro que había (el `padding-block` del divisor, el radio de
foco de la fila de evento, el `gap` de la pill y el radio del to-latest) están
cosidos: la clave es del componente, el valor sigue siendo el del sistema.
### 1.2 A través de un privado (0)
_Ninguno._
### 1.3 Literales (5)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `chat-log.css:10` | `[data-chat-log]` | `block-size` | `100%` |
| 2 | `chat-log.css:18` | `[data-chat-log] [data-virtual-list-viewport]` | `block-size` | `100%` |
| 3 | `chat-log.css:119` | `[data-chat-log-pill-new]` | `inline-size` | `fit-content` |
| 4 | `chat-log.css:148` | `[data-chat-log-to-latest] svg` | `block-size` | `1em` |
| 5 | `chat-log.css:149` | `[data-chat-log-to-latest] svg` | `inline-size` | `1em` |
_Ninguno sin firmar._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (3) — fuera del ratio
Literales que llevan su anotación `/* literal: <razón> */` en la propia
declaración: la válvula de recipe-contract §3, la misma que honra
`component-audit`. **Una desviación firmada no es deuda** — se listan para que la
razón se lea, no para acuñarlas.
_Ninguno._
| # | fichero:línea | selector | propiedad | valor | razón |
| ---: | --- | --- | --- | --- | --- |
| 1 | `chat-log.css:10` | `[data-chat-log]` | `block-size` | `100%` | el log ocupa la caja que la app le da — identidad |
| 2 | `chat-log.css:18` | `[data-chat-log] [data-virtual-list-viewport]` | `block-size` | `100%` | el viewport ocupa el log — identidad |
| 3 | `chat-log.css:119` | `[data-chat-log-pill-new]` | `inline-size` | `fit-content` | la pill la dimensiona su contenido — identidad |
**Y dos declaraciones RETIRADAS por muertas** (no son excepción: dejaron de
existir): `[data-chat-log-to-latest] svg { block-size: 1em; inline-size: 1em }`.
El `Icon` compuesto escribe `style="width: var(--icon-size-sm)"` EN LÍNEA y
ningún selector gana a un estilo en línea — el glifo medía 16 px, nunca los
14 px que la regla creía pintar. Retirarla da **0 diffs**.
## 2. Sistema transversal (3) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -50,8 +52,8 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `chat-log.css:92` | `[data-chat-log-event]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 2 | `chat-log.css:163` | `[data-chat-log-pill-new]:hover, [data-chat-log-to-latest]:hover` | `background-image` | `linear-gradient(var(--state-hover), var(--state-hover))` |
| 3 | `chat-log.css:168` | `[data-chat-log-pill-new]:focus-visible, [data-chat-log-to-latest]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 2 | `chat-log.css:158` | `[data-chat-log-pill-new]:hover, [data-chat-log-to-latest]:hover` | `background-image` | `linear-gradient(var(--state-hover), var(--state-hover))` |
| 3 | `chat-log.css:163` | `[data-chat-log-pill-new]:focus-visible, [data-chat-log-to-latest]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
@ -59,27 +61,23 @@ _La receta no declara privados propios en su CSS._
## 4. Propuesta de corrección
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (7)
### 4.1 EJECUTADA (2026-08-23) — 4 claves acuñadas, 2 declaraciones retiradas
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
al final) y theming §6.7 (slots de color, modificador delante). Un token con
DOS valores distintos es una colisión de nombre: son dos knobs, o el nombre
no distingue lo que debería — se marca `⚠`.
La propuesta generada pedía 7 claves. Entraron **4**, con los nombres del
CATÁLOGO y no los del generador:
| token (`--chat-log-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `divider-unread-padding-block` | `root` | `var(--space-1)` | 1 |
| `event-radius` | `root` | `var(--radius-sm)` | 1 |
| `pill-new-width` | `root` | `fit-content` | 1 |
| `pill-new-gap` | `root` | `var(--space-1)` | 1 |
| `to-latest-radius` | `root` | `var(--radius-full)` | 1 |
| `to-latest-height` | `root` | `1em` | 1 |
| `to-latest-width` | `root` | `1em` | 1 |
| propuesto | acuñado | por qué |
| --- | --- | --- |
| `divider-unread-padding-block` | **`divider-padding-block`** | la familia del contrato ya dice `divider-fg` / `-line` / `-font-size` / `-gap`; el generador nombra por el atributo de la parte, el catálogo por el grupo |
| `event-radius` | **`event-radius`** | correcto |
| `pill-new-gap` | **`pill-gap`** | misma razón: la familia es `pill-*` |
| `to-latest-radius` | **`to-latest-radius`** | correcto |
| `pill-new-width` (`fit-content`) | — | identidad: la pill la dimensiona su contenido; firmada `literal:` |
| `to-latest-height` / `-width` (`1em`) | — | la regla estaba MUERTA (el `Icon` compuesto escribe su tamaño en línea): retirada |
### 4.2 Sin nombre mecánico (2)
- **⚠ decisión: `100%` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 2: `block-size`.
- Los dos `block-size: 100%` son identidad (§1.4), firmados con su anotación.
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
@ -93,6 +91,49 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-23 — 77 % → 100 %.** Contrato 31 → **35** claves; censo
`público 34 · global 0 · literal 0 · sistema 3 · excepción 3`. Diff de computed
**VACÍO** (1.920 valores, 7 estados, 9 nodos). Centinela **34/35**, una
adjudicada. `component-audit` PASS · `eidos-lint` 0 invalid · `rtl:check` 0 ·
`docs:check` 0 · `check` sin errores del componente.
**Lo caro no fue acuñar: fue VER.** El centinela daba 15/31 antes de tocar una
línea, y las 16 «muertas» estaban las 16 VIVAS. Tres cegueras, ninguna del
código:
1. **El viewport que scrollea es del componente COMPUESTO.** El log pinta su
`padding-inline` / `padding-block` sobre `[data-virtual-list-viewport]`, un
nodo sin un solo `data-chat-log-*`: fuera del filtro de sonda y guard. Es la
cuarta forma del mismo punto ciego (prose, navigation-menu, waveform).
2. **Tres de las seis partes no las montaba la demo** — separador de día y
divisor de no-leídos no existían en el escenario, y la fila de evento vivía
en el índice 0 de una lista virtualizada anclada al FINAL, o sea fuera de la
ventana. Once claves sin nodo que pintar. La demo las monta ya, cerca del
final (instrumento, §3.bis del CONTINUE).
3. **La pastilla del separador es un `> span` sin atributo** — seis claves más
sin nodo.
Con las tres arregladas: **31/31 antes de tocar el código**. La lección es la
de siempre con un número pequeño: 9 nodos son pocos para seis partes; contarlos
es lo que destapó las tres.
**El hallazgo real: una regla que no ha pintado nunca.**
`[data-chat-log-to-latest] svg { block-size: 1em; inline-size: 1em }` no puede
ganar: el `Icon` compuesto emite `style="width: var(--icon-size-sm); height: …"`
INLINE, y un estilo en línea gana a todo selector. El glifo mide 16 px (paso
`sm` del icono), no los 14 px que `1em` daría sobre la fuente heredada del
botón. **Retirada, 0 diffs** — que es la prueba de que estaba muerta. Es la
clase `carousel.item-gap` / `drag-drop.preview-z`, pero con el estilo en línea
puesto por un COMPONENTE COMPUESTO, no por soma: lo que otro componente escribe
inline tampoco es tema.
**Adjudicada (1)**: `event-radius` — sólo pinta bajo `:focus-visible` y el
guard hace blur tras abrir. Medido enfocando con teclado la fila REAL con
`transition` congelada: 0 px en reposo, 4 px enfocada, **1234 px** con el
centinela.
**Lo que queda fuera y por qué**: tres identidades firmadas (§1.4) y tres knobs
de sistema transversal (§2). No hay deuda: el componente está CERRADO en su
número.
<!-- veredicto:end -->

@ -486,6 +486,31 @@ commits de corrección.
## 8. Registro
- 2026-08-23 — **`chat-log` 77 % → 100 %** (COMMIT), contrato 31 → **35**
claves, centinela **34/35** con 1 adjudicada. **El instrumento daba 15/31
antes de tocar el código, y las dieciséis «muertas» estaban VIVAS las
dieciséis.** Tres cegueras, ninguna del componente: (1) el viewport que
scrollea es del VirtualList compuesto (`data-virtual-list-viewport`, cero
`data-chat-log-*`), donde el log pinta sus dos paddings — la cuarta forma del
punto ciego de prose / navigation-menu / waveform; (2) la demo no montaba
TRES de las seis partes (separador de día y divisor de no-leídos no existían;
la fila de evento vivía en el índice 0 de una lista virtualizada anclada al
final, o sea fuera de la ventana) — once claves sin nodo; (3) la pastilla del
separador es un `> span` sin atributo, seis claves más. Con las tres
arregladas: **31/31 antes de acuñar nada**. Lo acuñado es la COSTURA de los
cuatro globales: `divider-padding-block`, `event-radius`, `pill-gap`,
`to-latest-radius` — con los nombres del CATÁLOGO (`divider-*` / `pill-*`),
no los del generador (`divider-unread-*` / `pill-new-*`), que nombra por el
atributo de la parte. **Y una regla RETIRADA por muerta**:
`[data-chat-log-to-latest] svg { block-size: 1em }` nunca ha pintado porque
el `Icon` compuesto escribe `style="width: var(--icon-size-sm)"` EN LÍNEA —
el glifo mide 16 px, no los 14 px de `1em`; retirarla da 0 diffs, la prueba.
Es la clase `carousel.item-gap`, pero el inline lo pone un COMPONENTE
COMPUESTO, no soma. Tres identidades firmadas (`block-size: 100%` ×2,
`inline-size: fit-content`). Adjudicada: `event-radius`, que sólo pinta bajo
`:focus-visible` (medido con teclado sobre la fila real: 0 → 4 → 1234 px).
Diff de computed VACÍO (1.920 valores · 7 estados · 9 nodos).
- 2026-08-23 — **`editable` 75 % → 82 %** (COMMIT), contrato 56 claves,
centinela **42/56** con 14 adjudicadas. **Su apertura es frágil AL REVÉS**:
salir del modo edición es lo que hace un `blur` —da igual lo que diga

@ -91,7 +91,12 @@ const EXTRA_NODES: Record<string, string> = {
// re-tints (the thumb becomes the playhead): those nodes carry
// `data-slider*`, so the attribute filter saw 4 nodes and none of them was
// the playhead the recipe paints.
waveform: '[data-waveform] [data-slider], [data-waveform] [data-slider] *'
waveform: '[data-waveform] [data-slider], [data-waveform] [data-slider] *',
// The scrolling viewport is the composed VirtualList's (`data-virtual-list-viewport`),
// so the two knobs the log paints on it — its inline/block padding — had no
// node in the filter; the to-latest glyph is a bare `<svg>`.
'chat-log':
'[data-chat-log] [data-virtual-list-viewport], [data-chat-log-to-latest] svg, [data-chat-log-separator-day] > *'
};
/**

@ -99,6 +99,13 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'pause-offset': 'the pause control is not mounted in the demo (0 nodes on the page)',
'pause-z': 'the pause control is not mounted in the demo (0 nodes on the page)'
},
'chat-log': {
// 2026-08-23. ONE key, and only because the guard blurs after opening: the
// event row is a tabbable feed article whose corner exists only under the
// focus ring (it is square at rest).
'event-radius':
'only paints under :focus-visible, and the guard blurs the active element after opening; keyboard-focused the REAL row with transitions frozen -> reaches (rest 0px, focused 4px -> 1234px)'
},
badge: {
// 2026-08-23. The demo mounts ONE chip, and its `rounded` axis paints only
// the step in force — measured forcing each on the real chip.

@ -363,6 +363,14 @@ const COMPONENT_OVERRIDES: Record<
// The ring is an SVG: its arc and halo are `<circle>` children with no
// `data-aura*` attr, so three live tokens read dead (measured 2026-08-22).
aura: { extraNodes: '[data-aura-ring] svg *' },
// The scrolling viewport is the composed VirtualList's — it carries
// `data-virtual-list-viewport`, not a `data-chat-log-*` attr — so the shell's
// own inline/block padding, painted on it, had no node in the filter; the
// to-latest glyph is a bare `<svg>` (measured 2026-08-23).
'chat-log': {
extraNodes:
'[data-chat-log] [data-virtual-list-viewport], [data-chat-log-to-latest] svg, [data-chat-log-separator-day] > *'
},
// The FIFTH canon component with no route of its own
// (`/uix/components/color-swatch` is a 404, like picker-shell / mockup /
// surface / audio-player): it is measured where it actually renders — the

@ -35,14 +35,49 @@ localized "{{count}} new messages" + a ↓ glyph by default (snippet receives
`{ unseenCount }`); `DividerUnread` renders the translated label by default;
`ToLatest` renders a ↓ glyph (its aria-label comes from the catalog).
## Talla y tema
**35 claves públicas** en `lib/recipes/base.ts`, sin eje `size` (el log ocupa
la caja que la app le da). La pestaña **Tokens** de su demo las resuelve en
vivo.
| grupo | claves |
| --- | --- |
| lienzo | `bg` (el fondo apagado sobre el que flotan las burbujas — rol wallpaper de WhatsApp), `padding-inline`, `padding-block` |
| separador de día | `separator-gap`, `separator-font-size`, `separator-fg`, `separator-bg`, `separator-padding-inline`, `separator-padding-block`, `separator-radius` |
| divisor de no-leídos | `divider-fg`, `divider-line`, `divider-font-size`, `divider-gap`, `divider-padding-block` |
| fila de evento | `event-font-size`, `event-fg`, `event-padding-block`, `event-radius` |
| pill flotante | `pill-bg`, `pill-fg`, `pill-font-size`, `pill-padding-inline`, `pill-height`, `pill-radius`, `pill-shadow`, `pill-offset`, `pill-gap` |
| botón to-latest | `to-latest-size`, `to-latest-bg`, `to-latest-fg`, `to-latest-border`, `to-latest-shadow`, `to-latest-offset`, `to-latest-radius` |
Lo que entró el **2026-08-23** (77 % → 100 %): la COSTURA de los cuatro knobs
que leían el sistema a pelo — `divider-padding-block`, `event-radius`
(el radio sólo existe bajo el anillo de foco: la fila es cuadrada en reposo),
`pill-gap` y `to-latest-radius`.
**Y una regla RETIRADA por muerta**: `[data-chat-log-to-latest] svg` fijaba
`1em` en los dos ejes, pero el `Icon` compuesto escribe
`style="width: var(--icon-size-sm)"` **en línea**, y ningún selector gana a un
estilo en línea. El glifo medía 16 px (el paso `sm` del icono), nunca los 14 px
que la regla creía pintar; retirarla da **0 diffs**. Lo que un componente
compuesto escribe inline es canal de valor, no tema.
**Identidades firmadas** (fuera del ratio, con su anotación `literal:`): el
`block-size: 100%` del shell y del viewport — el log ocupa la caja que recibe —
y el `inline-size: fit-content` de la pill, que la dimensiona su contenido.
**Lo que el guard da por silencioso**: `event-radius`, porque sólo pinta bajo
`:focus-visible` y el centinela hace blur tras abrir. Medido enfocando con
teclado la fila REAL: 0 px en reposo, 4 px enfocada, 1234 px con el centinela.
## Tokens (`--chat-log-*`)
Declared in `lib/recipes/base.ts` (~30): shell `bg` (the muted canvas the
Declared in `lib/recipes/base.ts` (35): shell `bg` (the muted canvas the
surface bubbles float over — WhatsApp-wallpaper role, redesign 2026-07-18)
+ padding, separator pill (bg/color/radius/padding), unread divider
(color/line/font), event row, floating pill (primary solid + contrast,
`--shadow-3`, offset, sm control height) and to-latest (element bg, border,
`--shadow-2`, md control size).
(color/line/font/padding), event row (+ its focus corner), floating pill
(primary solid + contrast, `--shadow-3`, offset, gap, sm control height) and
to-latest (element bg, border, radius, `--shadow-2`, md control size).
Visibility rules: `[data-has-new]:not([data-at-latest])` shows the pill;
`:not([data-at-latest])` shows to-latest (the pill supersedes it).
The recipe also NEUTRALIZES the global archetype-`item` hover/highlight

@ -7,7 +7,7 @@
[data-chat-log] {
position: relative;
display: block;
block-size: 100%;
block-size: 100%; /* literal: the log fills the box the app gives it — identity, not a knob */
min-block-size: 0;
/* Muted canvas so surface bubbles float over it (WhatsApp wallpaper /
Telegram backdrop role). */
@ -15,7 +15,7 @@
}
[data-chat-log] [data-virtual-list-viewport] {
block-size: 100%;
block-size: 100%; /* literal: the viewport fills the log — identity, not a knob */
padding-inline: var(--chat-log-padding-inline);
padding-block: var(--chat-log-padding-block);
}
@ -67,7 +67,7 @@
gap: var(--chat-log-divider-gap);
color: var(--chat-log-divider-fg);
font-size: var(--chat-log-divider-font-size);
padding-block: var(--space-1);
padding-block: var(--chat-log-divider-padding-block);
}
[data-chat-log-divider-unread]::before,
@ -91,7 +91,7 @@
[data-chat-log-event]:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
border-radius: var(--radius-sm);
border-radius: var(--chat-log-event-radius);
}
/* ── Floating actions ────────────────────────────────────────────────── */
@ -116,10 +116,10 @@
physical `translate: -50%` centred in LTR only. */
inset-inline: 0;
margin-inline: auto;
inline-size: fit-content;
inline-size: fit-content; /* literal: the pill is sized by its content — identity, not a knob */
block-size: var(--chat-log-pill-height);
padding-inline: var(--chat-log-pill-padding-inline);
gap: var(--space-1);
gap: var(--chat-log-pill-gap);
background: var(--chat-log-pill-bg);
color: var(--chat-log-pill-fg);
font-size: var(--chat-log-pill-font-size);
@ -140,15 +140,10 @@
background: var(--chat-log-to-latest-bg);
color: var(--chat-log-to-latest-fg);
border: var(--border-width) solid var(--chat-log-to-latest-border);
border-radius: var(--radius-full);
border-radius: var(--chat-log-to-latest-radius);
box-shadow: var(--chat-log-to-latest-shadow);
}
[data-chat-log-to-latest] svg {
block-size: 1em;
inline-size: 1em;
}
/* Shown while scrolled up (the pill supersedes it when there are unseen). */
[data-chat-log]:not([data-at-latest]) [data-chat-log-to-latest] {
display: inline-flex;

@ -957,9 +957,11 @@
--chat-log-divider-line: var(--color-primary-border);
--chat-log-divider-font-size: var(--size-xs-font-size);
--chat-log-divider-gap: var(--space-2);
--chat-log-divider-padding-block: var(--space-1);
--chat-log-event-font-size: var(--size-sm-font-size);
--chat-log-event-fg: var(--color-content-muted);
--chat-log-event-padding-block: var(--space-1);
--chat-log-event-radius: var(--radius-sm);
--chat-log-pill-bg: var(--color-primary-solid);
--chat-log-pill-fg: var(--color-primary-contrast);
--chat-log-pill-font-size: var(--size-sm-font-size);
@ -968,12 +970,14 @@
--chat-log-pill-radius: var(--radius-full);
--chat-log-pill-shadow: var(--shadow-3);
--chat-log-pill-offset: var(--space-3);
--chat-log-pill-gap: var(--space-1);
--chat-log-to-latest-size: var(--size-md-control-height);
--chat-log-to-latest-bg: var(--color-neutral-element);
--chat-log-to-latest-fg: var(--color-content-primary);
--chat-log-to-latest-border: var(--color-neutral-border);
--chat-log-to-latest-shadow: var(--shadow-2);
--chat-log-to-latest-offset: var(--space-3);
--chat-log-to-latest-radius: var(--radius-full);
--chat-message-gap: var(--space-2);
--chat-message-row-gap-run: var(--space-0-5);
--chat-message-row-gap-group: var(--space-3);

@ -866,10 +866,13 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'divider-line': 'var(--color-primary-border)',
'divider-font-size': 'var(--size-xs-font-size)',
'divider-gap': 'var(--space-2)',
'divider-padding-block': 'var(--space-1)',
// ── System event row ─────────────────────────────────────────────
'event-font-size': 'var(--size-sm-font-size)',
'event-fg': 'var(--color-content-muted)',
'event-padding-block': 'var(--space-1)',
// The row is only round while focused — the ring follows this corner.
'event-radius': 'var(--radius-sm)',
// ── Floating pill ("N new messages") ─────────────────────────────
'pill-bg': 'var(--color-primary-solid)',
'pill-fg': 'var(--color-primary-contrast)',
@ -879,13 +882,15 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'pill-radius': 'var(--radius-full)',
'pill-shadow': 'var(--shadow-3)',
'pill-offset': 'var(--space-3)',
'pill-gap': 'var(--space-1)',
// ── To-latest button ─────────────────────────────────────────────
'to-latest-size': 'var(--size-md-control-height)',
'to-latest-bg': 'var(--color-neutral-element)',
'to-latest-fg': 'var(--color-content-primary)',
'to-latest-border': 'var(--color-neutral-border)',
'to-latest-shadow': 'var(--shadow-2)',
'to-latest-offset': 'var(--space-3)'
'to-latest-offset': 'var(--space-3)',
'to-latest-radius': 'var(--radius-full)'
},
'chat-message': {
// ── Row layout ────────────────────────────────────────────────────

@ -17,17 +17,30 @@
import SystemAxes from '../../lib/SystemAxes.svelte';
import MotionPanel from '../../lib/MotionPanel.svelte';
import SemaPanel from '../../lib/SemaPanel.svelte';
import TokensPanel from '../../lib/TokensPanel.svelte';
import { DemoTrace } from '../../lib/harness.svelte';
const uix = getActiveUix();
type Tab = 'live' | 'system' | 'motion' | 'sema' | 'services' | 'api' | 'morfo' | 'recipe' | 'a11y';
type Tab =
| 'live'
| 'system'
| 'motion'
| 'sema'
| 'services'
| 'api'
| 'morfo'
| 'recipe'
| 'tokens'
| 'a11y';
let tab = $state<Tab>('live');
// ── Conversation data (app-land) ────────────────────────────────────────
type Row =
| { kind: 'message'; id: string; author: string; self: boolean; text: string; at: number }
| { kind: 'event'; id: string; text: string; at: number };
| { kind: 'event'; id: string; text: string; at: number }
| { kind: 'separator'; id: string; text: string; at: number }
| { kind: 'divider'; id: string; at: number };
const T0 = Date.now() - 3_600_000;
let nextId = 0;
@ -40,23 +53,38 @@
at
});
const filler = (i: number) =>
mkMsg(
i % 3 === 2 ? 'Tú' : 'Ada',
i % 3 === 2,
`Mensaje nº ${i + 1} del historial — relleno para que el anclaje y el scroll trabajen.`,
T0 + i * 90_000
);
// The day separator, a system event and the unread divider ride near the END,
// where the end-anchored viewport shows them: three of the six parts this demo
// advertises only existed in the prose (the joining event sits at index 0, a
// screenful of history above the anchor).
let rows = $state<Row[]>([
{ kind: 'event', id: `m${nextId++}`, text: 'Ada se unió a la conversación', at: T0 },
...Array.from({ length: 28 }, (_, i) =>
mkMsg(
i % 3 === 2 ? 'Tú' : 'Ada',
i % 3 === 2,
`Mensaje nº ${i + 1} del historial — relleno para que el anclaje y el scroll trabajen.`,
T0 + i * 90_000
)
)
...Array.from({ length: 24 }, (_, i) => filler(i)),
{ kind: 'separator', id: `m${nextId++}`, text: 'Hoy', at: T0 + 24 * 90_000 },
...Array.from({ length: 2 }, (_, i) => filler(24 + i)),
{
kind: 'event',
id: `m${nextId++}`,
text: 'Ada cambió el asunto de la sala',
at: T0 + 26 * 90_000
},
{ kind: 'divider', id: `m${nextId++}`, at: T0 + 26 * 90_000 },
...Array.from({ length: 2 }, (_, i) => filler(26 + i))
]);
const runs = $derived(
groupIntoRuns(rows, {
authorId: (r) => (r.kind === 'message' ? r.author : `event-${r.id}`),
authorId: (r) => (r.kind === 'message' ? r.author : `${r.kind}-${r.id}`),
timestamp: (r) => r.at,
breaks: (r) => r.kind === 'event'
breaks: (r) => r.kind !== 'message'
})
);
@ -222,6 +250,10 @@
<ChatLog.Item index={v.index} start={v.start} size={v.size}>
{#if row?.kind === 'event'}
<ChatLog.Event posinset={v.index + 1}>{row.text}</ChatLog.Event>
{:else if row?.kind === 'separator'}
<ChatLog.SeparatorDay>{row.text}</ChatLog.SeparatorDay>
{:else if row?.kind === 'divider'}
<ChatLog.DividerUnread />
{:else if row}
<ChatMessage
direction={row.self ? 'out' : 'in'}
@ -297,6 +329,9 @@
>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>35</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -613,6 +648,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="chat-log" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.