From f882e1b9d992ae6d911eebf8b1cf4b17a50c7028 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 24 Aug 2026 03:10:16 +0200 Subject: [PATCH] =?UTF-8?q?uix(chat-message):=20temable=20=E2=80=94=2084?= =?UTF-8?q?=20%=20=E2=86=92=20100=20%,=20y=20las=20TREINTA=20muertas=20era?= =?UTF-8?q?n=20ejes=20que=20la=20demo=20no=20enciende?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Protocolo PLAN-theming §7 sobre `chat-message`, el mayor de la familia chat. Censo 84 % → **100 %**, contrato 75 → **81** claves, centinela **77/81** con 4 adjudicadas. El default NO se mueve: diff de computed VACÍO (4.192 valores · 8 estados · 16 nodos) y capturas 2x antes/después **idénticas byte a byte**, en reposo y con la barra tapback abierta. ## Otra vez, lo caro fue VER El guard leía **45/75** antes de tocar una línea de código. Una fila de chat es una PILA de superficies opt-in (reply citado, read-by, estado de entrega, mención) y de ejes de PRESENTACIÓN (`run`, `direction`, `emphasized`) que la demo arranca en su posición más pobre: `solo` + `in`, sin ninguna de las cuatro. Con las superficies encendidas por `prepareWith` y los cinco ejes barridos como PRODUCTO: **71/75 antes de acuñar nada**. La sonda pasó de 12 nodos a 16 en reposo, y a 24 con el panel abierto. ## Lo que entra (6 claves, la COSTURA de los seis globales) `focus-radius` · `mentioned-fg` · `reply-fg` · `reaction-fg` · `reaction-inner-gap` · `quick-react-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 `radius` (colisiona con `emphasize-radius`, que ES la esquina en reposo de la MISMA fila), `bubble-fg` (el knob es de la MENCIÓN: `mentioned-bg` y `mentioned-accent` ya existían), `reaction-width` + `reaction-height` para el glifo (`reaction-height` ya existe y es la altura del CHIP) y `quick-reactions-width` para un `max-content` que es identidad. ## El caso que obligó a decidir: `reaction-gap` ya existía con otro sentido La receta la pinta en la fila `reactions` — el hueco ENTRE chips — mientras el knob nuevo es el ritmo INTERNO del chip, el emoji y su cuenta. Mismo valor (`--space-1`), dos papeles: un chip más denso no es una fila más apretada. Reusar la clave habría cambiado en silencio lo que mueve un tema que ya la usa, así que entra como `reaction-inner-gap`. El resto del bloque `{parte}-gap` de esta receta (`meta-gap`, `read-by-gap`, `header-gap`, `actions-gap`) significa «dentro de la parte»: la anómala es la vieja, y renombrarla es churn con riesgo. ## Lo que SALE: una regla que no ha pintado nunca `[data-chat-message-reaction] svg, [data-chat-message-reaction-add] svg` fijaba `1.1em` en los dos ejes «porque un svg de sólo viewBox computa 0x0». El glifo por defecto es el `Icon` compuesto, que emite `style="width: var(--icon-size-sm); height: ..."` EN LÍNEA, y un estilo en línea gana a todo selector. Medido: **16 px** (el paso `sm` del icono), no los 15,4 px que `1.1em` daría sobre los 14 px heredados. La otra mitad del selector no tiene nodo: los chips llevan un emoji de texto. **Retirada: 0 diffs.** Tercera vez en la familia (chat-log y chat-composer, ayer). ## Identidades firmadas (4, fuera del ratio) `inline-size: max-content` de la barra tapback (la barra ES su fila de emoji), `line-height: 1` de la celda (la celda es la caja del glifo) y los dos `opacity: 1` de la pastilla de acciones (el final del fundido 0 → 1). ## Adjudicadas (4) — las cuatro son límites del INSTRUMENTO, medidas a mano - `bubble-tail-radius` — el guard fotografía UNA esquina (`borderTopLeftRadius`) y la cola es la de ABAJO. Medido en las 8 combinaciones dirección x run: 4 px → 1234 px en `solo` y `last`, en las dos direcciones. Su hermana `bubble-radius-run` lee viva porque `middle` aplana la esquina START, que sí está en la lista. - `emphasize-duration` — ES la transición que el guard congela para medir todo lo demás. Sin congelar: 0,24 s → 4,321 s. - `focus-radius` — la fila sólo es tabulable compuesta DENTRO de un Feed (soma fusiona la identidad de `Feed.Article` cuando hay Feed ancestro) y su propia demo la monta suelta: `:focus-visible` no tiene nodo ahí. Medido en la demo de `chat-log`, donde las filas SÍ son artículos de feed (`tabindex=0`): reposo 6 px, enfocada 4 px → 1234 px, por foco programático y por recorrido con Tab. - `quick-react-hover-scale` — `transform` no está en la lista de propiedades del guard (punto ciego ya adjudicado en background / rating-group / card) y la celda es un botón PORTALADO con regla `:hover`. Con puntero real: `matrix(1.18…)` → `matrix(7.77…)`. ## Un defecto real medido y NO corregido aquí (mueve píxel) El chip de añadir reacción lleva también `data-popover-trigger`, y `[data-popover-trigger]:not([data-archetype='field-trigger'])` pesa (0,2,0) contra los (0,1,0) de `[data-chat-message-reaction-add]`: su fondo, borde, radio, tinta, altura, padding y tamaño de letra los pinta `popover.css`. Se ve en la captura — los dos chips de reacción son píldoras redondas y el de añadir es un cuadrado gris. Los `reaction-*` no mienten (alcanzan sobre los chips normales), así que no hay nada que retirar: es una decisión de diseño. Clase `gradient-picker` / `emoji-picker`. Queda REGISTRADO, no tocado (D-TH.5). ## Guards censo `--only chat-message` 100 % (global 68 %, no baja) · `component-audit` PASS · `eidos-lint` 0 invalid (37 morfo-backed / 41 eidos-only) · `vitest run src/uix/eidos` sin rojos nuevos (el único vivo es el conocido `skin-media-player`) · `rtl:check` 0 · `docs:check` 0 · `check` COMPLETED 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` (81 claves, verificada en navegador) y ficha con su veredicto §5. Co-Authored-By: Claude Opus 5 --- docs/audit/theming/chat-message.md | 141 +++++++++++++----- scripts/__theming-probe.ts | 22 ++- scripts/theming-sentinel-exceptions.ts | 12 ++ scripts/theming-sentinel.ts | 28 ++++ .../eidos/components/chat-message/README.md | 67 ++++++++- .../components/chat-message/chat-message.css | 28 ++-- src/uix/eidos/generated/base.css | 6 + src/uix/eidos/lib/recipes/base.ts | 12 ++ .../uix/components/chat-message/+page.svelte | 20 ++- 9 files changed, 277 insertions(+), 59 deletions(-) diff --git a/docs/audit/theming/chat-message.md b/docs/audit/theming/chat-message.md index 2a1c6d627..89c9ae9f0 100644 --- a/docs/audit/theming/chat-message.md +++ b/docs/audit/theming/chat-message.md @@ -5,47 +5,38 @@ > 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**: **84%** — 65 de 77 knobs por token público -- **Knobs de apariencia**: 84 — público 65 · privado 0 · global 6 · literal 6 · sistema 7 · excepción 0 _(los dos últimos, fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): 75 pública(s) — `gap`, `row-gap-run`, `row-gap-group`, `gutter-inline-size`, `bubble-padding-inline-sm`, `bubble-padding-inline-md`, `bubble-padding-inline-lg`, `bubble-padding-block-sm`, `bubble-padding-block-md`, `bubble-padding-block-lg`, `bubble-radius`, `bubble-radius-run`, `bubble-tail-radius`, `bubble-max-inline-size`, `bubble-font-size-sm`, `bubble-font-size-md`, `bubble-font-size-lg`, `bubble-line-height`, `bubble-bg-in`, `bubble-fg-in`, `bubble-bg-out`, `bubble-fg-out`, `bubble-shadow`, `header-gap`, `header-font-size`, `header-font-weight`, `header-fg`, `meta-gap`, `meta-font-size`, `meta-fg`, `status-size`, `status-fg`, `read-status-fg`, `failed-status-fg`, `read-by-gap`, `read-by-font-size`, `read-by-fg`, `read-by-margin`, `emphasize-bg`, `emphasize-radius`, `emphasize-duration`, `reply-padding-inline`, `reply-padding-block`, `reply-accent-size`, `reply-radius`, `reply-bg`, `reply-accent`, `reply-font-size`, `reaction-gap`, `reaction-height`, `reaction-padding-inline`, `reaction-radius`, `reaction-font-size`, `reaction-bg`, `reaction-border`, `reaction-shadow`, `reaction-overlap`, `reaction-pressed-bg`, `reaction-pressed-border`, `reaction-pressed-fg`, `quick-react-gap`, `quick-react-pad`, `quick-react-size`, `quick-react-emoji-size`, `quick-react-hover-bg`, `quick-react-hover-scale`, `actions-bg`, `actions-border`, `actions-shadow`, `actions-radius`, `actions-padding`, `mentioned-bg`, `mentioned-accent`, `actions-gap`, `actions-offset` +- **Medido**: 2026-08-24 · **Alcance**: **100%** — 71 de 71 knobs por token público +- **Knobs de apariencia**: 78 — público 71 · privado 0 · global 0 · literal 0 · sistema 7 · excepción 4 _(los dos últimos, fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 81 pública(s) — `gap`, `row-gap-run`, `row-gap-group`, `focus-radius`, `gutter-inline-size`, `bubble-padding-inline-sm`, `bubble-padding-inline-md`, `bubble-padding-inline-lg`, `bubble-padding-block-sm`, `bubble-padding-block-md`, `bubble-padding-block-lg`, `bubble-radius`, `bubble-radius-run`, `bubble-tail-radius`, `bubble-max-inline-size`, `bubble-font-size-sm`, `bubble-font-size-md`, `bubble-font-size-lg`, `bubble-line-height`, `bubble-bg-in`, `bubble-fg-in`, `bubble-bg-out`, `bubble-fg-out`, `bubble-shadow`, `header-gap`, `header-font-size`, `header-font-weight`, `header-fg`, `meta-gap`, `meta-font-size`, `meta-fg`, `status-size`, `status-fg`, `read-status-fg`, `failed-status-fg`, `read-by-gap`, `read-by-font-size`, `read-by-fg`, `read-by-margin`, `emphasize-bg`, `emphasize-radius`, `emphasize-duration`, `reply-padding-inline`, `reply-padding-block`, `reply-accent-size`, `reply-radius`, `reply-bg`, `reply-fg`, `reply-accent`, `reply-font-size`, `reaction-gap`, `reaction-inner-gap`, `reaction-height`, `reaction-padding-inline`, `reaction-radius`, `reaction-font-size`, `reaction-bg`, `reaction-fg`, `reaction-border`, `reaction-shadow`, `reaction-overlap`, `reaction-pressed-bg`, `reaction-pressed-border`, `reaction-pressed-fg`, `quick-react-gap`, `quick-react-pad`, `quick-react-size`, `quick-react-radius`, `quick-react-emoji-size`, `quick-react-hover-bg`, `quick-react-hover-scale`, `actions-bg`, `actions-border`, `actions-shadow`, `actions-radius`, `actions-padding`, `mentioned-bg`, `mentioned-fg`, `mentioned-accent`, `actions-gap`, `actions-offset` - **Eje `size`**: sí · **ficheros**: `chat-message.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (6) +### 1.1 Directo a primitivo global (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `chat-message.css:38` | `[data-chat-message]:focus-visible` | `border-radius` | `var(--radius-sm)` | -| 2 | `chat-message.css:226` | `[data-chat-message][data-mentioned] [data-chat-message-bubble]` | `color` | `var(--color-content-primary)` | -| 3 | `chat-message.css:289` | `[data-chat-message-reply]` | `color` | `var(--color-content-secondary)` | -| 4 | `chat-message.css:328` | `[data-chat-message-reaction], [data-chat-message-reaction-add]` | `gap` | `var(--space-1)` | -| 5 | `chat-message.css:334` | `[data-chat-message-reaction], [data-chat-message-reaction-add]` | `color` | `var(--color-content-primary)` | -| 6 | `chat-message.css:407` | `[data-chat-message-quick-react]` | `border-radius` | `var(--radius-full)` | +_Ninguno._ ### 1.2 A través de un privado (0) _Ninguno._ -### 1.3 Literales (6) +### 1.3 Literales (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `chat-message.css:355` | `[data-chat-message-reaction] svg, [data-chat-message-reaction-add] svg` | `inline-size` | `1.1em` | -| 2 | `chat-message.css:356` | `[data-chat-message-reaction] svg, [data-chat-message-reaction-add] svg` | `block-size` | `1.1em` | -| 3 | `chat-message.css:379` | `[data-popover-content][data-chat-message-quick-reactions]` | `inline-size` | `max-content` | -| 4 | `chat-message.css:409` | `[data-chat-message-quick-react]` | `line-height` | `1` | -| 5 | `chat-message.css:478` | `[data-chat-message]:hover [data-chat-message-actions], [data-chat-message]:focus-within [data-chat-message-actions]` | `opacity` | `1` | -| 6 | `chat-message.css:484` | `[data-chat-message-actions]` | `opacity` | `1` | +_Ninguno._ -### 1.4 Excepciones firmadas (0) — fuera del ratio +### 1.4 Excepciones firmadas (4) — fuera del ratio Literales que llevan su anotación `/* literal: */` 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 | +| ---: | --- | --- | --- | --- | +| 1 | `chat-message.css:371` | `[data-popover-content][data-chat-message-quick-reactions]` | `inline-size` | `max-content` | +| 2 | `chat-message.css:401` | `[data-chat-message-quick-react]` | `line-height` | `1` | +| 3 | `chat-message.css:470` | `[data-chat-message]:hover [data-chat-message-actions], [data-chat-message]:focus-within [data-chat-message-actions]` | `opacity` | `1` | +| 4 | `chat-message.css:476` | `[data-chat-message-actions]` | `opacity` | `1` | ## 2. Sistema transversal (7) — informativo, fuera del ratio Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). @@ -58,7 +49,7 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). | 4 | `chat-message.css:300` | `[data-chat-message-reply]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | | 5 | `chat-message.css:342` | `[data-chat-message-reaction]:hover, [data-chat-message-reaction-add]:hover` | `background-image` | `linear-gradient(var(--state-hover), var(--state-hover))` | | 6 | `chat-message.css:347` | `[data-chat-message-reaction]:focus-visible, [data-chat-message-reaction-add]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | -| 7 | `chat-message.css:424` | `[data-chat-message-quick-react]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | +| 7 | `chat-message.css:416` | `[data-chat-message-quick-react]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | ## 3. Privados de la receta — ¿de dónde sale su valor? @@ -71,7 +62,7 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). - **Tiene eje `size`**: los tokens dimensionales van por talla (`{part}-{eje}-{k}`) apuntando al bundle `--size-{k}-*`, nunca al primitivo crudo (theming §5; el guard `recipe-css-contract` prohíbe el primitivo). -### 4.1 Tokens a declarar en `lib/recipes/base.ts` (9) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1) 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 @@ -81,15 +72,7 @@ no distingue lo que debería — se marca `⚠`. | token (`--chat-message-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `radius` | `root` | `var(--radius-sm)` | 1 | -| `bubble-fg` | `root` | `var(--color-content-primary)` | 1 | -| `reply-fg` | `root` | `var(--color-content-secondary)` | 1 | -| `reaction-gap` _(ya existe)_ | `root` | `var(--space-1)` | 1 | -| `reaction-fg` | `root` | `var(--color-content-primary)` | 1 | -| `reaction-width` | `root` | `1.1em` | 1 | -| `reaction-height` _(ya existe)_ | `root` | `1.1em` | 1 | | `quick-reactions-width` | `root` | `max-content` | 1 | -| `quick-react-radius` | `root` | `var(--radius-full)` | 1 | ### 4.2 Sin nombre mecánico (3) @@ -107,6 +90,96 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**EJECUTADO 2026-08-24 — 84 % → 100 %.** Contrato 75 → **81** claves; censo +`público 71 · global 0 · literal 0 · sistema 7 · excepción 4`. Diff de computed +**VACÍO** (4.192 valores, 8 estados, 16 nodos) y capturas 2× **idénticas byte a +byte**, en reposo y con la barra tapback abierta. Centinela **77/81**, cuatro +adjudicadas. `component-audit` PASS · `eidos-lint` 0 invalid · `rtl:check` 0 · +`docs:check` 0 · `check` sin errores del componente. + +**Otra vez, lo caro fue VER.** El guard daba **45/75** antes de tocar una línea +y las treinta «muertas» estaban las treinta VIVAS: una fila de chat es una PILA +de superficies opt-in (reply citado, read-by, estado de entrega, mención) y de +ejes de PRESENTACIÓN (`run`, `direction`, `emphasized`) que la demo arranca en +su posición más pobre. Con las cuatro superficies encendidas por `prepareWith` +y los cinco ejes barridos como producto: **71/75 antes de acuñar nada**. La +sonda pasó de 12 a 16 nodos en reposo y a 24 con la barra abierta. + +**Lo que entra (6 claves, la COSTURA de los seis globales)**: `focus-radius`, +`mentioned-fg`, `reply-fg`, `reaction-fg`, `reaction-inner-gap` y +`quick-react-radius`. El valor sigue siendo el del sistema; el knob pasa a ser +del componente. + +**Los nombres salen del CATÁLOGO.** La ficha proponía `radius` (colisiona con +`emphasize-radius`, que ES la esquina en reposo de la MISMA fila: la de foco +lleva su modificador delante, `focus-radius`), `bubble-fg` (el knob es de la +MENCIÓN — `mentioned-bg` / `mentioned-accent` ya existen, faltaba +`mentioned-fg`), `reaction-width` + `reaction-height` para el glifo (dos +literales de una regla muerta, y `reaction-height` ya existe con otro sentido: +la altura del CHIP) y `quick-reactions-width` para un `max-content` que es +identidad. + +**Y el caso que obligó a decidir: `reaction-gap` ya existía con otro sentido.** +La receta la pinta en la fila `reactions` (el hueco ENTRE chips) mientras el +knob nuevo es el ritmo INTERNO del chip — el emoji y su cuenta. Mismo valor +(`--space-1`), dos papeles: un chip más denso no es una fila más apretada. +Reusar la clave habría cambiado en silencio lo que mueve un tema que ya la usa, +así que entra como `reaction-inner-gap` y `reaction-gap` se queda como está. El +resto del bloque `{parte}-gap` de esta receta (`meta-gap`, `read-by-gap`, +`header-gap`, `actions-gap`) significa «dentro de la parte»: la anómala es la +vieja, y renombrarla es churn con riesgo. + +**Lo que SALE: una regla que no ha pintado nunca.** +`[data-chat-message-reaction] svg, [data-chat-message-reaction-add] svg` fijaba +`1.1em` en los dos ejes «porque un svg de sólo viewBox computa 0×0». El glifo +por defecto es el `Icon` compuesto, que emite +`style="width: var(--icon-size-sm); height: …"` INLINE — y un estilo en línea +gana a todo selector. Medido: **16 px** (el paso `sm` del icono), no los 15,4 px +que `1.1em` daría sobre los 14 px heredados. La otra mitad del selector no tiene +nodo: los chips llevan un emoji de texto. **Retirada: 0 diffs.** Tercera vez en +la familia (chat-log, chat-composer, y aquí). + +**Identidades firmadas (4, fuera del ratio)**: `inline-size: max-content` de la +barra tapback (la barra ES su fila de emoji), `line-height: 1` de la celda (la +celda es la caja del glifo) y los dos `opacity: 1` de la pastilla de acciones +(el final del fundido 0 → 1). + +**Adjudicadas (4), todas límites del INSTRUMENTO y medidas a mano**: + +1. `bubble-tail-radius` — el guard fotografía UNA esquina + (`borderTopLeftRadius`) y la cola es la de ABAJO (`border-end-start` en `in`, + `border-end-end` en `out`). Medido en las 8 combinaciones dirección × run: + 4 px → 1234 px en `solo` y `last` en las dos direcciones. Su hermana + `bubble-radius-run` lee viva porque `middle` aplana la esquina START, que sí + está en la lista. +2. `emphasize-duration` — ES la transición que el guard congela para poder medir + todo lo demás. Sin congelar: 0,24 s → 4,321 s. +3. `focus-radius` — la fila sólo es tabulable compuesta DENTRO de un Feed (soma + fusiona la identidad de `Feed.Article` cuando hay un Feed ancestro) y su + propia demo la monta suelta, así que `:focus-visible` no tiene nodo ahí. + Medido en `/uix/components/chat-log`, donde las filas SÍ son artículos de + feed (`tabindex=0`): reposo 6 px (la esquina de `emphasize`), enfocada 4 px → + 1234 px, por foco programático y por recorrido real con Tab. +4. `quick-react-hover-scale` — dos límites a la vez: `transform` no está en la + lista de propiedades del guard (el punto ciego ya adjudicado en `background`, + `rating-group` y `card`) y la celda es un botón PORTALADO con regla `:hover`. + Con puntero real y transiciones congeladas: `matrix(1.18…)` → + `matrix(7.77…)`. + +**Defecto real encontrado y NO corregido aquí (mueve píxel)**: el chip de añadir +reacción lleva también `data-popover-trigger`, y +`[data-popover-trigger]:not([data-archetype='field-trigger'])` pesa (0,2,0) +contra los (0,1,0) de `[data-chat-message-reaction-add]` — así que su fondo, su +borde, su radio, su tinta, su altura, su padding y su tamaño de letra los pinta +`popover.css`. Se ve en la captura: los dos chips de reacción son píldoras +redondas y el de añadir es un cuadrado gris. Es la clase `gradient-picker` (36 +de 45) / `emoji-picker` (3). Los `reaction-*` NO mienten —alcanzan sobre los +chips normales—, así que no hay nada que retirar; lo que hay es una decisión de +diseño pendiente. Registrado, no tocado (D-TH.5). + +**Lo que queda fuera y por qué**: cuatro identidades firmadas (§1.4) y siete +knobs de sistema transversal (§2, anillo de foco × 4, velo de estado × 2, +`--opacity-muted` del `sending`). No hay deuda: el componente está CERRADO en su +número. diff --git a/scripts/__theming-probe.ts b/scripts/__theming-probe.ts index 1abf7525f..a46780d98 100644 --- a/scripts/__theming-probe.ts +++ b/scripts/__theming-probe.ts @@ -105,7 +105,12 @@ const EXTRA_NODES: Record = { // painted surface is the composed Button's icon slot, which the recipe sizes // by descendant selector (`[data-fab] [data-button-icon]`) and which carries // the Button's attr, not the FAB's. - fab: '[data-fab] [data-button-icon]' + fab: '[data-fab] [data-button-icon]', + // Its quick-reaction panel is PORTALED (the bar rides on `Popover.Content` + // and the gap it paints sits on the composed `[data-popover-viewport]`), and + // the add-reaction glyph is the composed `Icon` — a bare svg with `data-icon`. + 'chat-message': + '[data-chat-message-quick-reactions] [data-popover-viewport], [data-chat-message-reaction-add] > svg' }; /** @@ -122,7 +127,10 @@ const OPEN_TRIGGER: Record = { // demo, whose stage mounts exactly ONE swatch (the trigger chip). Every other // swatch — presets and saved swatches, a dozen of them — lives in the // PORTALED panel, so the open pass is where the node count stops being 1. - 'color-swatch': '[data-color-picker-trigger]' + 'color-swatch': '[data-color-picker-trigger]', + // Its only openable surface is the quick-reaction tapback bar, hanging off + // the add-reaction chip — the default `[data-{c}-trigger]` matches nothing. + 'chat-message': '[data-chat-message-reaction-add]' }; const DEMO_VARIANTS: Record = { @@ -163,6 +171,16 @@ const DEMO_VARIANTS: Record = { 'chat-composer': [ '[data-uix-control]:has([data-uix-control-label]:text-is("context")) [data-uix-chip]:text-is("reply")', '[data-uix-control]:has([data-uix-control-label]:text-is("simulate")) button:has-text("attach file")' + ], + // Four of its twelve parts are OPT-IN and boot off: the quoted reply, the + // read-receipt row, the delivery status (only rendered when `delivery` is + // set) and the mention accent. Without them the probe saw 12 nodes and none + // of the four surfaces the recipe paints there. + 'chat-message': [ + '[data-uix-control]:has([data-uix-control-label]:text-is("reply (quote)")) input[type=checkbox]', + '[data-uix-control]:has([data-uix-control-label]:text-is("read-by")) input[type=checkbox]', + '[data-uix-control]:has([data-uix-control-label]:text-is("mentioned")) input[type=checkbox]', + '[data-uix-control]:has([data-uix-control-label]:text-is("delivery")) [data-uix-chip]:text-is("read")' ] }; diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 84ead7616..87a6fb08c 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -124,6 +124,18 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'disabled-opacity': 'file-upload sweeps DESCENDANTS: `[data-file-upload] [data-disabled]` is (0,2,0), the same weight as `[data-chat-composer-send][data-disabled]`, and its recipe is emitted LATER, so it wins over every disabled node inside a FileUpload — which is where the demo (and the documented composition) puts the composer; measured on the REAL nodes with the ancestor attr removed -> reaches (0.4 -> 0.123 on the send AND on the shell). Both defaults are --opacity-disabled, so the collision is invisible until a theme moves one' }, + 'chat-message': { + // 2026-08-24. Four, and every one of them is a limit of the INSTRUMENT — + // measured on the real nodes, one by one. + 'bubble-tail-radius': + 'the guard snapshots exactly ONE corner (`borderTopLeftRadius`) and the tail is the BOTTOM one — `border-end-start-radius` incoming, `border-end-end-radius` outgoing. Measured on the real bubble across the 8 direction x run combinations: 4px -> 1234px on solo and last in BOTH directions (10px/10px/10px/4px -> 10px/10px/10px/1234px in, 10px/10px/4px/10px -> 10px/10px/1234px/10px out), while the corner the guard reads never moves. Its sibling `bubble-radius-run` reads alive because `middle` flattens the START corner, which IS in the read set', + 'emphasize-duration': + 'the guard freezes transitions and this token IS the transition (the jump-to-message flash is a two-state toggle + a transition, R-4.5 — there are no keyframes); measured unfrozen on the real row -> reaches (0.24s -> 4.321s)', + 'focus-radius': + 'the row is tabbable ONLY when composed into a Feed — soma merges `Feed.Article`s identity (tabindex, keyboard) into the provider props when a Feed ancestor exists — and its own demo mounts it standalone, so `:focus-visible` has no node there. Measured on /uix/components/chat-log, where the rows ARE feed articles (tabindex=0), with transitions frozen: rest 6px (the emphasize corner), focused 4px -> 1234px, both by programmatic focus and by a real Tab walk', + 'quick-react-hover-scale': + 'two limits at once: the guard does not snapshot `transform` (the same blind spot adjudicated for background / rating-group / card) and the cell is a PORTALED tapback button whose rule is `:hover, :focus-visible`. Measured with a real pointer on the open bar, transitions frozen -> reaches (matrix(1.18, 0, 0, 1.18, 0, 0) -> matrix(7.77, 0, 0, 7.77, 0, 0))' + }, '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 diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index 54a09cca7..6ae7507ae 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -459,6 +459,34 @@ const COMPONENT_OVERRIDES: Record< '[data-uix-control]:has([data-uix-control-label]:text-is("simulate")) button:has-text("attach file")' ] }, + // A conversation row is a pile of OPT-IN surfaces and PRESENTATION axes, and + // the demo boots the plainest of them: no quoted reply, no read receipts, no + // delivery status, no mention accent, `run='solo'` and `direction='in'`. + // Thirty of its seventy-five tokens read dead on that stage — every one of + // them alive on a row that carries the axis. The four surfaces come from the + // demo controls; the five axes are swept as their product (the run corners, + // the outgoing palette, the two delivery tints, the mention and the + // jump-to-message flash). The quick-reaction tapback bar is PORTALED off the + // add-reaction chip, and the gap it paints sits on the composed + // `[data-popover-viewport]` (measured 2026-08-24). + 'chat-message': { + prepareWith: [ + '[data-uix-control]:has([data-uix-control-label]:text-is("reply (quote)")) input[type=checkbox]', + '[data-uix-control]:has([data-uix-control-label]:text-is("read-by")) input[type=checkbox]', + '[data-uix-control]:has([data-uix-control-label]:text-is("delivery")) [data-uix-chip]:text-is("read")' + ], + openWith: ['[data-chat-message-reaction-add]'], + openMarker: '[data-chat-message-quick-reactions]', + extraNodes: + '[data-chat-message-quick-reactions] [data-popover-viewport], [data-chat-message-reaction-add] > svg', + sweepAttr: [ + { attr: 'data-run', values: ['solo', 'middle', 'last'] }, + { attr: 'data-direction', values: ['in', 'out'] }, + { attr: 'data-delivery', values: ['delivered', 'read', 'failed'] }, + { attr: 'data-mentioned', values: [null, ''] }, + { attr: 'data-emphasized', values: [null, ''] } + ] + }, // 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 diff --git a/src/uix/eidos/components/chat-message/README.md b/src/uix/eidos/components/chat-message/README.md index a8924dcf2..306b7102e 100644 --- a/src/uix/eidos/components/chat-message/README.md +++ b/src/uix/eidos/components/chat-message/README.md @@ -57,17 +57,76 @@ never hand-rolled SVG paths. theme retints. No `intent` prop: the row is a frame; the evaluative load lives in the events (CANON §5). +## Talla y tema + +**81 claves públicas** en `lib/recipes/base.ts`, con eje `size` (`sm · md · lg`) +en las dos coordenadas que densifican la burbuja — su tipografía y sus dos +paddings lógicos. La pestaña **Tokens** de su demo las resuelve en vivo. + +| grupo | claves | +| --- | --- | +| fila | `gap`, `row-gap-run`, `row-gap-group`, `focus-radius`, `gutter-inline-size` | +| burbuja | `bubble-padding-inline-{sm,md,lg}`, `bubble-padding-block-{sm,md,lg}`, `bubble-font-size-{sm,md,lg}`, `bubble-line-height`, `bubble-radius`, `bubble-radius-run`, `bubble-tail-radius`, `bubble-max-inline-size`, `bubble-bg-in`, `bubble-fg-in`, `bubble-bg-out`, `bubble-fg-out`, `bubble-shadow` | +| cabecera y meta | `header-gap`, `header-font-size`, `header-font-weight`, `header-fg`, `meta-gap`, `meta-font-size`, `meta-fg` | +| entrega | `status-size`, `status-fg`, `read-status-fg`, `failed-status-fg` | +| reply citado | `reply-padding-inline`, `reply-padding-block`, `reply-accent-size`, `reply-accent`, `reply-radius`, `reply-bg`, `reply-fg`, `reply-font-size` | +| chips de reacción | `reaction-gap`, `reaction-inner-gap`, `reaction-height`, `reaction-padding-inline`, `reaction-radius`, `reaction-font-size`, `reaction-bg`, `reaction-fg`, `reaction-border`, `reaction-shadow`, `reaction-overlap`, `reaction-pressed-bg`, `reaction-pressed-border`, `reaction-pressed-fg` | +| barra tapback | `quick-react-gap`, `quick-react-pad`, `quick-react-size`, `quick-react-radius`, `quick-react-emoji-size`, `quick-react-hover-bg`, `quick-react-hover-scale` | +| pastilla de acciones | `actions-bg`, `actions-border`, `actions-shadow`, `actions-radius`, `actions-padding`, `actions-gap`, `actions-offset` | +| mención y salto | `mentioned-bg`, `mentioned-fg`, `mentioned-accent`, `emphasize-bg`, `emphasize-radius`, `emphasize-duration` | +| read-by | `read-by-gap`, `read-by-font-size`, `read-by-fg`, `read-by-margin` | + +Lo que entró el **2026-08-24** (84 % → 100 %): la COSTURA de los seis knobs que +leían el sistema a pelo — `focus-radius` (la esquina que sigue el anillo de +foco; NO es la de `emphasize`, que va un paso más ancha), `mentioned-fg`, +`reply-fg`, `reaction-fg`, `reaction-inner-gap` y `quick-react-radius`. + +**`reaction-gap` y `reaction-inner-gap` son dos knobs, no uno**: el primero +separa los chips (se pinta en la fila `reactions`), el segundo es el ritmo +INTERNO del chip — el emoji y su cuenta. Comparten valor por defecto y no +comparten papel: un chip más denso no es una fila más apretada. + +**Y una regla RETIRADA por muerta**: `[data-chat-message-reaction] svg, +[data-chat-message-reaction-add] svg` fijaba `1.1em` en los dos ejes, pero el +glifo por defecto es el `Icon` compuesto, que escribe +`style="width: var(--icon-size-sm); height: …"` **en línea** — y ningún selector +gana a un estilo en línea. Medido: el glifo computa 16 px (el paso `sm` del +icono), nunca los 15,4 px que la regla creía pintar. Retirarla da **0 diffs**. +Es la misma lección que `chat-log` y `chat-composer` (2026-08-23). + +**Identidades firmadas** (fuera del ratio, con su anotación `literal:`): el +`inline-size: max-content` de la barra tapback (la barra ES su fila de emoji), +el `line-height: 1` de la celda (la celda es la caja del glifo) y los dos +`opacity: 1` de la pastilla de acciones (el final del fundido 0 → 1). + +**Lo que el guard da por silencioso** (4, todas medidas a mano y en el ledger): +`bubble-tail-radius` — el centinela fotografía UNA esquina (`borderTopLeftRadius`) +y la cola es la de abajo; `emphasize-duration` — ES la transición que el guard +congela para poder medir lo demás; `focus-radius` — la fila sólo es tabulable +compuesta DENTRO de un Feed, y su propia demo la monta suelta (medida en la de +`chat-log`); y `quick-react-hover-scale` — `transform` no está en la lista de +propiedades del guard y la celda es un botón portalado con regla `:hover`. + +**Defecto conocido, NO tocado aquí**: el chip de añadir reacción lleva también +`data-popover-trigger`, y `[data-popover-trigger]:not([data-archetype='field-trigger'])` +pesa (0,2,0) contra los (0,1,0) de esta receta — así que su fondo, su borde, su +radio y su tinta los pinta `popover.css`, no `reaction-*`. Se ve en la demo: los +dos chips de reacción son píldoras redondas y el de añadir, un cuadrado gris. Es +la clase `gradient-picker` / `emoji-picker`; arreglarlo mueve píxel, así que +queda registrado, no corregido. + ## Tokens (`--chat-message-*`) Declared in `lib/recipes/base.ts`: row layout (`gap`, `row-gap-run`, `row-gap-group`, `gutter-inline-size`), bubble (`bubble-bg/fg-in|out`, `bubble-shadow`, paddings per size, `bubble-radius[-run]`, `bubble-tail-radius`, `bubble-max-inline-size`, typography), header/meta, -status (`status-color[-read|-failed]`, `status-size`), reply (`reply-*` -incl. logical accent bar), reaction chips (`reaction-*` incl. pressed -palette, `reaction-shadow`, `reaction-overlap`), `mentioned-bg/accent`, +status (`status-fg`, `read-status-fg`, `failed-status-fg`, `status-size`), +reply (`reply-*` incl. logical accent bar and its ink), reaction chips +(`reaction-*` incl. the chip's own `reaction-inner-gap`, its ink, the pressed +palette, `reaction-shadow`, `reaction-overlap`), `mentioned-bg/fg/accent`, actions pill (`actions-bg/border/shadow/radius/padding` + `gap/offset`), -read-by row (`read-by-gap/font-size/color/margin`), jump-highlight +read-by row (`read-by-gap/font-size/fg/margin`), jump-highlight (`emphasize-bg/radius/duration`). Recipe-contract compliant: logical axes, size-bundle coordinates, state-layer hovers, `--opacity-muted` while sending, outline focus, no local keyframes. diff --git a/src/uix/eidos/components/chat-message/chat-message.css b/src/uix/eidos/components/chat-message/chat-message.css index c318b3f13..9f85094b9 100644 --- a/src/uix/eidos/components/chat-message/chat-message.css +++ b/src/uix/eidos/components/chat-message/chat-message.css @@ -35,7 +35,7 @@ [data-chat-message]: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-message-focus-radius); } /* The double-registered Feed.Article stamps archetype 'item', whose global @@ -223,7 +223,7 @@ /* Mention highlight — logical accent bar + soft tint. */ [data-chat-message][data-mentioned] [data-chat-message-bubble] { background: var(--chat-message-mentioned-bg); - color: var(--color-content-primary); + color: var(--chat-message-mentioned-fg); border-inline-start: var(--chat-message-reply-accent-size) solid var(--chat-message-mentioned-accent); } @@ -286,7 +286,7 @@ var(--chat-message-reply-accent); border-radius: var(--chat-message-reply-radius); background: var(--chat-message-reply-bg); - color: var(--color-content-secondary); + color: var(--chat-message-reply-fg); font-size: var(--chat-message-reply-font-size); margin-block-end: var(--space-0-5); cursor: pointer; @@ -325,13 +325,13 @@ font: inherit; display: inline-flex; align-items: center; - gap: var(--space-1); + gap: var(--chat-message-reaction-inner-gap); block-size: var(--chat-message-reaction-height); padding-inline: var(--chat-message-reaction-padding-inline); border: var(--border-width) solid var(--chat-message-reaction-border); border-radius: var(--chat-message-reaction-radius); background: var(--chat-message-reaction-bg); - color: var(--color-content-primary); + color: var(--chat-message-reaction-fg); font-size: var(--chat-message-reaction-font-size); box-shadow: var(--chat-message-reaction-shadow); cursor: pointer; @@ -348,14 +348,6 @@ outline-offset: var(--focus-ring-offset); } -/* Icon children (the default smiley) need explicit box — a viewBox-only - inline svg computes to 0×0 inside the flex chip. */ -[data-chat-message-reaction] svg, -[data-chat-message-reaction-add] svg { - inline-size: 1.1em; - block-size: 1.1em; -} - [data-chat-message-reaction][data-pressed] { background: var(--chat-message-reaction-pressed-bg); border-color: var(--chat-message-reaction-pressed-border); @@ -376,7 +368,7 @@ horizontal flex row. `inline-size: max-content` stands alone (not inside `min()`, where an intrinsic keyword fails to compute). */ [data-popover-content][data-chat-message-quick-reactions] { - inline-size: max-content; + inline-size: max-content; /* literal: intrinsic sizing — the bar IS its emoji row; identity, not a knob */ min-inline-size: 0; max-inline-size: min(var(--popover-max-width), calc(100vw - var(--space-4))); --_popover-padding-block: var(--chat-message-quick-react-pad); @@ -404,9 +396,9 @@ flex: none; inline-size: var(--chat-message-quick-react-size); block-size: var(--chat-message-quick-react-size); - border-radius: var(--radius-full); + border-radius: var(--chat-message-quick-react-radius); font-size: var(--chat-message-quick-react-emoji-size); - line-height: 1; + line-height: 1; /* literal: the cell is the glyph's box — identity, not a knob */ cursor: pointer; transition: background-color var(--duration-fast) var(--ease-out), @@ -475,13 +467,13 @@ [data-chat-message]:hover [data-chat-message-actions], [data-chat-message]:focus-within [data-chat-message-actions] { - opacity: 1; + opacity: 1; /* literal: fully revealed — the end of the 0 → 1 fade, identity */ } /* Touch: hover does not exist — keep the cluster reachable. */ @media (pointer: coarse) { [data-chat-message-actions] { - opacity: 1; + opacity: 1; /* literal: fully revealed — no hover to reveal it, identity */ } } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 04f258e0f..ed5cf5d19 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -984,6 +984,7 @@ --chat-message-gap: var(--space-2); --chat-message-row-gap-run: var(--space-0-5); --chat-message-row-gap-group: var(--space-3); + --chat-message-focus-radius: var(--radius-sm); --chat-message-gutter-inline-size: var(--avatar-size-md, var(--space-8)); --chat-message-bubble-padding-inline-sm: var(--space-2-5); --chat-message-bubble-padding-inline-md: var(--space-3); @@ -1027,14 +1028,17 @@ --chat-message-reply-accent-size: var(--space-0-5); --chat-message-reply-radius: var(--radius-sm); --chat-message-reply-bg: var(--color-neutral-track); + --chat-message-reply-fg: var(--color-content-secondary); --chat-message-reply-accent: var(--color-primary-solid); --chat-message-reply-font-size: var(--size-xs-font-size); --chat-message-reaction-gap: var(--space-1); + --chat-message-reaction-inner-gap: var(--space-1); --chat-message-reaction-height: var(--size-xs-control-height); --chat-message-reaction-padding-inline: var(--space-2); --chat-message-reaction-radius: var(--radius-full); --chat-message-reaction-font-size: var(--size-xs-font-size); --chat-message-reaction-bg: var(--color-surface-default); + --chat-message-reaction-fg: var(--color-content-primary); --chat-message-reaction-border: var(--color-neutral-separator); --chat-message-reaction-shadow: var(--shadow-1); --chat-message-reaction-overlap: var(--space-1); @@ -1044,6 +1048,7 @@ --chat-message-quick-react-gap: var(--space-0-5); --chat-message-quick-react-pad: var(--space-1); --chat-message-quick-react-size: var(--size-lg-control-height); + --chat-message-quick-react-radius: var(--radius-full); --chat-message-quick-react-emoji-size: var(--size-xl-font-size); --chat-message-quick-react-hover-bg: var(--color-neutral-hover); --chat-message-quick-react-hover-scale: 1.18; @@ -1053,6 +1058,7 @@ --chat-message-actions-radius: var(--radius-full); --chat-message-actions-padding: var(--space-0-5); --chat-message-mentioned-bg: var(--color-primary-surface); + --chat-message-mentioned-fg: var(--color-content-primary); --chat-message-mentioned-accent: var(--color-primary-solid); --chat-message-actions-gap: var(--space-0-5); --chat-message-actions-offset: var(--space-1); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index ee8f83306..425b28e5b 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -909,6 +909,10 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ gap: 'var(--space-2)', 'row-gap-run': 'var(--space-0-5)', 'row-gap-group': 'var(--space-3)', + // The corner the focus ring follows. It is NOT the emphasize corner: the + // jump-highlight wash rides one step wider, and the two coexist on the + // same node (the focus rule out-ranks the resting one while focused). + 'focus-radius': 'var(--radius-sm)', // Avatar column reserve — follows the composed Avatar's md diameter. 'gutter-inline-size': 'var(--avatar-size-md, var(--space-8))', // ── Bubble ──────────────────────────────────────────────────────── @@ -972,15 +976,21 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'reply-accent-size': 'var(--space-0-5)', 'reply-radius': 'var(--radius-sm)', 'reply-bg': 'var(--color-neutral-track)', + 'reply-fg': 'var(--color-content-secondary)', 'reply-accent': 'var(--color-primary-solid)', 'reply-font-size': 'var(--size-xs-font-size)', // ── Reaction chips — floating pills lapping the bubble edge ────── + // `reaction-gap` separates the chips (it is painted on the `reactions` + // row); `reaction-inner-gap` is the chip's OWN rhythm — the emoji and its + // count. Two roles, two knobs: a denser chip is not a tighter row. 'reaction-gap': 'var(--space-1)', + 'reaction-inner-gap': 'var(--space-1)', 'reaction-height': 'var(--size-xs-control-height)', 'reaction-padding-inline': 'var(--space-2)', 'reaction-radius': 'var(--radius-full)', 'reaction-font-size': 'var(--size-xs-font-size)', 'reaction-bg': 'var(--color-surface-default)', + 'reaction-fg': 'var(--color-content-primary)', 'reaction-border': 'var(--color-neutral-separator)', 'reaction-shadow': 'var(--shadow-1)', 'reaction-overlap': 'var(--space-1)', @@ -992,6 +1002,7 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'quick-react-gap': 'var(--space-0-5)', 'quick-react-pad': 'var(--space-1)', 'quick-react-size': 'var(--size-lg-control-height)', + 'quick-react-radius': 'var(--radius-full)', 'quick-react-emoji-size': 'var(--size-xl-font-size)', 'quick-react-hover-bg': 'var(--color-neutral-hover)', 'quick-react-hover-scale': '1.18', @@ -1003,6 +1014,7 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'actions-padding': 'var(--space-0-5)', // ── Mention highlight ──────────────────────────────────────────── 'mentioned-bg': 'var(--color-primary-surface)', + 'mentioned-fg': 'var(--color-content-primary)', 'mentioned-accent': 'var(--color-primary-solid)', // ── Actions cluster ────────────────────────────────────────────── 'actions-gap': 'var(--space-0-5)', diff --git a/web/routes/uix/components/chat-message/+page.svelte b/web/routes/uix/components/chat-message/+page.svelte index 5b08709ae..063a1ff98 100644 --- a/web/routes/uix/components/chat-message/+page.svelte +++ b/web/routes/uix/components/chat-message/+page.svelte @@ -21,11 +21,22 @@ 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('live'); const directions: ChatMessageDirection[] = ['in', 'out']; @@ -314,6 +325,9 @@ > + @@ -725,6 +739,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'a11y'}

Accessibility