uix(chat-typing): temable — 89 % → 100 %, con UNA clave y siete de sus ocho ya vivas

Protocolo PLAN-theming §7 sobre `chat-typing`. Censo 89 % → **100 %**, contrato
7 → **8** claves, centinela **8/8** con **cero adjudicadas**.

## Lo que entra: una clave, y es la COSTURA

`dot-radius` (`var(--radius-full)`), el único knob que leía el sistema a pelo.
El valor sigue siendo el del sistema; el knob pasa a ser del componente.

**El nombre sale del CATÁLOGO, no del generador.** La ficha proponía
`indicator-radius` —nombra por el primer selector de la regla,
`[data-chat-typing-indicator] > span`— cuando el nodo que PINTA es el punto y el
contrato ya habla `dot-size` / `dot-gap` / `dot-fg`. Es el defecto de
`tree-view` («root-bg» para una guía de indentación) en pequeño.

## Lo que el instrumento no veía: 3 nodos de 8

Los tres puntos son `> span` DESNUDOS del indicador —sin `data-chat-typing-*`,
la clase de la pastilla del separador de `chat-log`— y la demo arranca con NADIE
escribiendo, así que `data-state='idle'` esconde a todos los hijos (presencia =
visibilidad, N-7). Con el selector extra y un typer encendido por `prepareWith`:
**8 nodos**, y el centinela ya daba **7/7 antes de tocar el código**.

## El diff de computed NO cerró en cero, y es RUIDO reproducido

18 diffs, los 18 de `opacity` y los 18 en los tres
`span[data-animation-style=pulse]`. Sus puntos montan el loop `pulse` del
SISTEMA, que anima `opacity` sin parar, así que cada lectura cae en una fase
distinta: corriendo la sonda dos veces sobre el MISMO código salen los mismos 18
(control `after` vs `after2`). Ninguna otra propiedad se mueve, la clave tocada
es `border-radius`, y las capturas 2x antes/después son **idénticas byte a
byte** (1.568 valores · 7 estados · 8 nodos).

## Guards

censo `--only chat-typing` 100 % (global 68 %, no baja; los que llegan al 100 %
pasan de 36 a 42) · `component-audit` PASS · `eidos-lint` 0 invalid (8
morfo-backed, 0 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` (8 claves,
verificada en navegador) y ficha con su veredicto §5.

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

@ -5,18 +5,16 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-24 · **Alcance**: **89%** — 8 de 9 knobs por token público
- **Knobs de apariencia**: 9 — público 8 · privado 0 · global 1 · literal 0 · sistema 0 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 7 pública(s) — `gap`, `font-size`, `fg`, `min-block-size`, `dot-size`, `dot-gap`, `dot-fg`
- **Medido**: 2026-08-24 · **Alcance**: **100%** — 9 de 9 knobs por token público
- **Knobs de apariencia**: 9 — público 9 · privado 0 · global 0 · literal 0 · sistema 0 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 8 pública(s) — `gap`, `font-size`, `fg`, `min-block-size`, `dot-size`, `dot-gap`, `dot-radius`, `dot-fg`
- **Eje `size`**: no · **ficheros**: `chat-typing.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (1)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `chat-typing.css:48` | `[data-chat-typing-indicator] > span` | `border-radius` | `var(--radius-full)` |
_Ninguno._
### 1.2 A través de un privado (0)
@ -46,17 +44,7 @@ _La receta no declara privados propios en su CSS._
## 4. Propuesta de corrección
### 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
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 `⚠`.
| token (`--chat-typing-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `indicator-radius` | `root` | `var(--radius-full)` | 1 |
_Nada que proponer: no hay knobs fuera de alcance._
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
@ -70,6 +58,36 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-24 — 89 % → 100 %.** Contrato 7 → **8** claves; censo
`público 9 · global 0 · literal 0 · sistema 0 · excepción 0`. Centinela **8/8**,
**cero adjudicadas**. `component-audit` PASS · `eidos-lint` 0 invalid (8
morfo-backed, 0 eidos-only) · `rtl:check` 0 · `docs:check` 0 · `check` sin
errores del componente.
**Una sola clave, y es la COSTURA**: `dot-radius` (`var(--radius-full)`), el
único knob que leía el sistema a pelo. El nombre sale del CATÁLOGO, no del
generador: la §4 proponía `indicator-radius` —nombra por el primer selector de
la regla, `[data-chat-typing-indicator] > span`— cuando el nodo que PINTA es el
punto y el contrato ya habla `dot-size` / `dot-gap` / `dot-fg`. Es el defecto de
`tree-view` («root-bg» para una guía de indentación) en pequeño.
**Lo que el instrumento no veía (3 nodos de 8)**: los tres puntos son `> span`
DESNUDOS del indicador —sin `data-chat-typing-*`, la clase de la pastilla del
separador de `chat-log`— y la demo arranca con NADIE escribiendo, así que
`data-state='idle'` esconde a todos los hijos (presencia = visibilidad, N-7).
Con el selector extra y un typer encendido por `prepareWith`: **8 nodos**, y el
centinela ya daba **7/7 antes de tocar el código**.
⚠️ **El diff de computed NO cerró en cero, y es ruido del instrumento
reproducido sobre código idéntico**: 18 diffs, los 18 de `opacity` y los 18 en
los tres `span[data-animation-style=pulse]`. Sus puntos montan el loop `pulse`
del SISTEMA, que anima `opacity` sin parar, así que cada lectura cae en una fase
distinta — corriendo la sonda dos veces sobre el MISMO código salen los mismos
18 (control `after` vs `after2`). Ninguna otra propiedad se mueve, la clave
tocada es `border-radius`, y las capturas 2× antes/después son **idénticas byte
a byte**.
**Lo que queda fuera**: nada. Cero literales, cero globales, cero privados, cero
sistema. El componente está CERRADO en el 100 %.
<!-- veredicto:end -->

@ -110,7 +110,10 @@ const EXTRA_NODES: Record<string, string> = {
// 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'
'[data-chat-message-quick-reactions] [data-popover-viewport], [data-chat-message-reaction-add] > svg',
// Its three dots are bare `> span` children of the indicator: no attr of
// their own, so the four knobs the recipe paints on them had no node.
'chat-typing': '[data-chat-typing-indicator] > span, [data-chat-typing-avatars] > *'
};
/**
@ -181,6 +184,12 @@ const DEMO_VARIANTS: Record<string, string[]> = {
'[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")'
],
// It boots with NOBODY typing, and idle hides every child (presence =
// visibility, N-7): the row measured its three parts with `visibility:
// hidden` and the avatars slot unmounted.
'chat-typing': [
'[data-uix-control]:has([data-uix-control-label]:text-is("typers")) [data-uix-chip]:text-is("Ada")'
]
};

@ -487,6 +487,16 @@ const COMPONENT_OVERRIDES: Record<
{ attr: 'data-emphasized', values: [null, ''] }
]
},
// Its three dots are bare `> span` children of the indicator — no attr of
// their own — and the demo boots with NOBODY typing, which hides every child
// (presence = visibility, N-7). Four of its eight tokens had no node to
// paint (measured 2026-08-24).
'chat-typing': {
prepareWith: [
'[data-uix-control]:has([data-uix-control-label]:text-is("typers")) [data-uix-chip]:text-is("Ada")'
],
extraNodes: '[data-chat-typing-indicator] > span, [data-chat-typing-avatars] > *'
},
// 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

@ -20,11 +20,39 @@ Label pair;
compose children for custom layouts (e.g. an `Avatar` next to the dots).
`Indicator` ships three dots by default (replaceable via children).
## Talla y tema
**8 claves públicas** en `lib/recipes/base.ts`, sin eje `size` (la fila toma su
altura de la línea que reserva). La pestaña **Tokens** de su demo las resuelve
en vivo.
| grupo | claves |
| --- | --- |
| fila | `gap`, `font-size`, `fg`, `min-block-size` (la línea reservada) |
| puntos | `dot-size`, `dot-gap`, `dot-radius`, `dot-fg` |
Lo que entró el **2026-08-24** (89 % → 100 %): **una** clave, `dot-radius`, la
COSTURA del único knob que leía el sistema a pelo (`var(--radius-full)`). El
valor sigue siendo el del sistema; el knob pasa a ser del componente. El nombre
sale del CATÁLOGO y no del generador: la ficha proponía `indicator-radius`
—nombra por el primer selector de la regla— cuando el nodo que PINTA es el
punto, y el contrato ya habla `dot-size` / `dot-gap` / `dot-fg`.
**Lo que el instrumento no veía**: los tres puntos son `> span` desnudos del
indicador —sin atributo propio— y la demo arranca con NADIE escribiendo, lo que
oculta a todos los hijos (presencia = visibilidad, N-7). La sonda medía 3 nodos
en estado `idle`; con el selector extra y un typer encendido, **8**.
⚠️ **Sus puntos ANIMAN `opacity`** (el loop `pulse` del sistema), así que
cualquier medición a mano de esa propiedad devuelve la fase en que cayó la
lectura: 18 diffs de `opacity` corriendo la sonda dos veces sobre el MISMO
código. Congela `animation`, o mide otra cosa.
## Tokens (`--chat-typing-*`)
Declared in `src/uix/eidos/lib/recipes/base.ts` (block `chat-typing`):
`gap`, `font-size`, `fg`, `min-block-size` (the reserved line),
`dot-size`, `dot-gap`, `dot-fg`.
`dot-size`, `dot-gap`, `dot-radius`, `dot-fg`.
## Comparativa

@ -45,7 +45,7 @@
[data-chat-typing-indicator] > span {
inline-size: var(--chat-typing-dot-size);
block-size: var(--chat-typing-dot-size);
border-radius: var(--radius-full);
border-radius: var(--chat-typing-dot-radius);
background: var(--chat-typing-dot-fg);
}

@ -1068,6 +1068,7 @@
--chat-typing-min-block-size: var(--size-sm-control-height);
--chat-typing-dot-size: var(--space-1);
--chat-typing-dot-gap: var(--space-0-5);
--chat-typing-dot-radius: var(--radius-full);
--chat-typing-dot-fg: var(--color-content-muted);
--emoji-picker-trigger-size: var(--size-md-control-height);
--emoji-picker-open-trigger-fg: var(--color-content-primary);

@ -1031,6 +1031,7 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// ── Dots ──────────────────────────────────────────────────────────
'dot-size': 'var(--space-1)',
'dot-gap': 'var(--space-0-5)',
'dot-radius': 'var(--radius-full)',
'dot-fg': 'var(--color-content-muted)'
},
'emoji-picker': {

@ -14,11 +14,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<Tab>('live');
// ── Typing state (app-land: the transport would feed this) ──────────────
@ -172,6 +183,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>8</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -436,6 +450,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="chat-typing" 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.