---
title: Auditoría de coherencia de componentes — eidos
type: audit
date: 2026-06-16
scope: src/uix/eidos/components/* (excl. words · palabras · chronos)
method: grep + Read dirigidos (sin agentes, por instrucción del usuario)
status: FASE 1 completa. FASE 2 en curso — ver "hand-off 2026-06-24" (al final). Hechos previos: action components, font 1:1, 2A radius, gap canónico, popover color-picker, faults de selects (combobox/search-field/select). 2026-06-24: barrido de langs (capa eidos — 6 wrappers de la cascada IconButton + 2 divergencias de contrato + onion/cropper/time-picker), DIM 5E (segmentos date/time/color — flicker del borde + estado activo unificado vía 5A), DIM 5B (code-block/kbd→ControlVariant, image-picker md, switch stale, link 'default' se queda). CONTINUAR por: DIM 1 composición (Lote A closes → IconButton+SvgX; Lote B 6 selects nativos → Select).
---
# Auditoría de coherencia de componentes — eidos
Catálogo de incoherencias a nivel de componente: partes cableadas a mano en vez
de componer el ecosistema, literales que se saltan el tema, coherencia de
contrato y verificación de morfo. **Excluidos: words, palabras, chronos.**
116 componentes barridos.
## Resumen ejecutivo
| Dimensión | Veredicto | Hallazgos |
|---|---|---|
| **1. Composición** | 🔴 grande | 6 `<select>` nativos · ~16 componentes reimplementan chrome de botón-comando · ~14 ya correctos (referencia) |
| **2. Literales que se saltan el tema** | 🟡 medio | ~250 literales px/rem; pocos 🔴 bypass real, muchos 🟡 off-scale interno, mayoría 🟢 idiomas legítimos |
| **3. Coherencia de contrato** | 🟢 limpio | `eidos-lint` : **0 invalid en los 116** — el typed-builder + lint funcionan |
| **4. Verificación de morfo** | 🟢 casi limpio | morfos compilan; `unused` informativo; 1 ítem: `corner-shape` documentado-no-materializado |
| **5. Homogeneidad transversal** | 🔴 alto | **focus-ring** 2 mecanismos + a11y forced-colors · **fondo de fields** no homogéneo (editable/tags-input raised, textarea fantasma, combobox hardcode) · **segmentos date/time/color** estado activo divergente + anillo + flicker al teclear · **modelo de anchura** (block-opt-in vs 100%-siempre → fields fijos desproporcionados) · nits API |
El problema real es **composición** (dim. 1), la **higiene de literales** (dim. 2)
y el **focus-ring no homogéneo** (dim. 5). Contrato y morfo (dim. 3/4) están sanos
— no se fabrican problemas ahí.
---
## Decisiones de diseño (cerradas con el usuario, 2026-06-16)
1. **Clave de clasificación = la FAMILIA del evento en el morfo + el ESTADO del
part** (no la impresión visual). Anclado al libro: la familia es intrínseca al
acto y fija su firma perceptual; `<Button>` empaqueta `contact.activate` .
2. **Política ternaria** por parte interactiva:
- **Botón-comando sin estado** (close · clear · prev/next · inc/dec · step ·
download · action) → **componer `<Button>`** (familia `contact.activate` ;
la consecuencia de dominio —`emerge.close`, `shift.navigate` — la cablea el
consumidor).
- **Superficie CON estado** (porta `aria-selected` /`expanded`/`current`;
familia `commit` /`emerge`/`shift` sobre el control) → **identidad
estructural** (`data-button` + tokens del recipe Button), mantiene su
role/aria/keyboard/sema. Precedente: **toggle-group** (`data-toggle`).
- **Chrome propio** (calendar-day = `gridcell` ) → **bespoke** pero
consumiendo tokens.
3. ** `IconButton` es un primitivo público del framework** (no adorno; MUI/Radix/
Chakra lo tienen). Vía (a): **se quita `iconOnly` del API público de `Button`**
(pasa a modo interno), `IconButton` es EL componente icon-only — **compone
`Button` por dentro** (hereda variant/size/intent/color/rounded/loading +
morfo + sema; sin morfo nuevo), **añade `aria-label` obligatorio por tipo** +
el glifo como contenido + el layout cuadrado. Es **agnóstico al glifo** . Se
**migran los 14** `<Button iconOnly>` actuales (corte limpio, sin shim).
NO se hace `CloseButton` ni un componente-por-glifo (el glifo es prop/children).
4. **Fuente de glifos = por animabilidad, no por capa** :
- **`svg/`** = glifos de affordance **propios y animables/acoplables a estado**
(chevron rota, check/dash dibujan vía `pathLength` +`data-svg`, dot, y el `× `
que se animará al cerrar). Tuned, hooks `data-svg` .
- **`icon/` (1696 lucide)** = glifos de **contenido estático** para el
consumidor / chrome estático puntual. No tuneados.
- Regla: *¿se animará o acopla a estado? → `svg/` . ¿Contenido estático? →
`icon/` lucide.* → faltan `SvgX` (+ `SvgPlus` /`SvgMinus`) en `svg/` .
5. **Regla de radio theme-driven** : todo radio —incluido `full` y el default—
resuelve a `--radius-*` ; cero literales (`9999px` → `var(--radius-full)` ).
Aplica a magnitud (`rounded`) y forma (`corner-shape`).
---
## DIMENSIÓN 1 — Composición
### 1A — `<select>` NATIVO → componer `<Select>` (🔴 estructural)
`<select><option>` nativo donde el ecosistema tiene `<Select>` compuesto
(Trigger/Value/Content/Item…). Migrarlo cambia morfo (archetype `input` →
composite), soma (render `<Select.*>` ), DOM y a11y.
| Componente | Parte | Morfo hoy | Fichero soma |
|---|---|---|---|
| calendar | month-select, year-select | archetype `input` , element `select` | `soma/.../calendar-month-select.svelte` + year |
| range-calendar | month-select, year-select | idem | `soma/.../range-calendar-{month,year}-select.svelte` |
| color-field | format-select | `input` /`select` | `soma/.../color-field-format-select.svelte` |
| color-picker | format-select | `input` /`select` | `soma/.../color-picker-format-select.svelte` |
**Síntomas del chrome nativo a resolver al migrar** (observados en color-field, 2026-06-16):
- Con `appearance: auto` , al estar **activo/enfocado** el `<select>` pinta el
**borde/outline negro nativo del navegador** — no tiene sentido visual y rompe
la coherencia con el resto del field. Desaparece al componer `<Select>` (que usa
el focus-ring del sistema, themeable). Vale para los 6 sitios.
- (Histórico) se le había aplicado por error el resaltado de segmento-activo
(`--field-segment-active-*`) en `:focus-visible` ; revertido a su anillo propio
el 2026-06-16. La migración a `<Select>` lo hace moot.
### 1B — Botón-comando que REIMPLEMENTA chrome → componer `<Button>` (🔴)
Parte stateless de comando con border/bg/hover/focus-visible/disabled reescrito
en su CSS, en vez de componer `<Button>` vía soma `child` . Familia del part =
`contact.activate` (la consecuencia de dominio la cablea el consumidor).
| Componente | Parte(s) | Confirmado |
|---|---|---|
| calendar | prev-button, next-button | ✓ leído (`--calendar-control-*` chrome completo) |
| range-calendar | prev/next | ✓ análogo a calendar |
| dialog | close | ✓ no compone Button |
| drawer | close | ✓ no compone Button |
| popover | close | ✓ no compone Button |
| banner | close | ✓ no compone Button |
| toast | close | ✓ no compone Button |
| float-panel | close (+ controles) | ✓ no compone Button |
| pagination | prev/next nav | candidato (page-items van a 1D) |
| stepper | step nav | candidato |
| spin-field | increment / decrement | candidato (ver Lote 0: spin-field es el visual compartido) |
| editable | edit / submit / cancel | candidato |
| file-upload | browse / trigger | candidato |
| image-picker | trigger / controles | candidato |
| color-picker | eyedropper / controles de acción | candidato |
| tags-input | clear (remove-tag → 1D, es chip) | candidato |
| color-field | clear | candidato |
> "candidato" = grep marca chrome de botón sin composición de Button; confirmar
> el part exacto al abrir el wrapper en Lote A.
### 1C — YA correctos (componen `<Button>`) — referencia/plantilla
`alert-dialog` (action/cancel) · `button` · `card` (footer) · `carousel`
(prev/next) · `clipboard` (trigger) · `combobox` (clear) · `dropdown-menu`
(trigger) · `link-preview` (trigger) · `navigation-menu` (link/trigger) ·
`picker-shell` (close/clear/cancel) · `qr-code` (download) · `search-field`
(clear) · `table` (row-detail) · `tree-grid` (expand).
Plantilla canónica: `components/carousel/carousel-prev-trigger.svelte`
(`< Button iconOnly variant = 'ghost' rounded = 'full' > ` + `<SvgChevron>` vía soma
`child` , size desde contexto).
### 1D — Superficie CON estado → identidad estructural (🟡 polish, no bug)
Portan estado de dominio (`aria-selected`/`expanded`/`current`/`pressed`); su
familia es `commit` /`emerge`/`shift` SOBRE el control. **No** componen Button;
si quieren su look → `data-button` + tokens del recipe (vía toggle-group). Si
tienen chrome propio coherente, se quedan bespoke-tokenizados.
| Componente | Parte | Familia | Acción |
|---|---|---|---|
| toggle-group | item | commit/toggle | ✓ ya identidad estructural (`data-toggle`) |
| tabs | trigger | commit/select | identidad estructural (look de Button) — opcional |
| accordion | header/trigger | emerge | bespoke disclosure (chrome propio) |
| pagination | page-item | commit | identidad estructural — opcional |
| calendar / month-grid / year-grid | day/cell | commit/shift | bespoke `gridcell` (tokenizado) |
| listbox / select / combobox / menubar / context-menu / dropdown-menu | item | commit | bespoke item (tokenizado) |
| radio-cards / rating-group | card/item | commit | bespoke (tokenizado) |
| tree-view / tree-grid | node | commit/emerge | bespoke (tokenizado) |
feat(eidos): IconButton primitive + Lote 0 iconOnly->IconButton cascade
Lote 0 of the component-coherence audit (FASE 2):
- New IconButton primitive (composes Button: inherits variant/size/intent/
color/rounded/shape/loading + Button's morfo + sema, no new morfo;
aria-label required; glyph as children). New svg/ glyphs: SvgX, SvgPlus,
SvgMinus.
- Button: rounded='full' 9999px -> var(--radius-full) (theme-driven radius,
value-identical).
- Migrate icon-only Button consumers to IconButton: carousel prev/next
(pilot), tree-grid expand, table row-detail, search-field clear, combobox
clear. Pattern: glyph->children, label->aria-label, drop iconOnly.
- carousel.css: fix arrow positioning (specificity vs Button's base
position:relative, exposed by the IconButton import-order flip) + the
press-squeeze jump (centre via the translate property so the contact
press transform:scale composes instead of replacing the centring).
- Doc: COMPONENT_COHERENCE_AUDIT - indicator-chevron vs discrete IconButton
distinction (tree-view/accordion stay out); 5F width closed (full-width);
Lote 0 hand-off + pending select-family faults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
> **Indicador vs botón discreto (chevron de expand) — verificado 2026-06-18.** El
> chevron de expand es **IconButton SOLO cuando es un botón DISCRETO** (`tree-grid`
> `expand-trigger`: un botoncito aparte en la fila, que compone `<Button iconOnly>`
> → migrado a `<IconButton>`). Cuando es un **indicador DECORATIVO** —`<SvgChevron>`
> que rota por `data-state` dentro de una **superficie clicable mayor**— **NO es
> IconButton**:
> - `tree-view` `BranchIndicator`: chevron suelto; el control es **toda la rama**.
> - `accordion`: `<span data-accordion-indicator aria-hidden><SvgChevron></span>`
> dentro de la cabecera; el control es **toda la `<Accordion.Trigger>`**.
>
> Mismo glifo (`SvgChevron`) en los tres → coherencia de glifo. Lo que cambia es el
> **modelo de interacción** (toggle de superficie entera vs botón discreto). La
> regla: *chevron como indicador de estado ≠ botón icon-only*. Por eso tree-view y
> accordion quedan fuera de la cascada IconButton, correctamente.
---
## DIMENSIÓN 2 — Literales que se saltan el tema
~250 literales px/rem en value-position (excl. `var()` , `0` ). Tres clases:
### 2A — 🔴 Bypass real (el token EXISTE y se ignora)
| Fichero | Literal | Debe ser |
|---|---|---|
| `button/button.css:202` | `--_button-radius: 9999px` (`rounded='full'`) | `var(--radius-full)` |
| `grid-list/grid-list.css:203` | `border-radius: 0.2rem` | `var(--radius-sm)` (o token nombrado) |
| `navigation-menu/navigation-menu.css:292` | `border-radius: 1px` (indicador) | `var(--radius-sm)` o token del indicador |
### 2B — 🟡 Off-scale interno (NO existe token; decidir por doctrina anti-mágicos)
Micro-escalas component-internas sin equivalente en la escala. Opción: snap a
escala existente, o token de recipe nombrado y justificado.
- **Escalas de tamaño internas**: `spinner` (`--_spinner-size` 0.75– 2.5rem +
thickness 1.5– 4px), `carousel` (`--_carousel-indicator-size` 0.375– 0.75rem),
`link-preview` (`--_link-preview-min-width` 11– 22rem), `color-picker`
(`--cp-checker-cell: 6px`).
- **Focus ring offset**: `outline-offset: 1px|2px` recurre en muchos (link,
button, navigation-menu…) → **candidato fuerte a token** `--focus-ring-offset` .
- **Micro-gaps/paddings sub-escala** (`< --space-1 `= 4px ) : `gap: 1px|2px` ,
`padding: 2px` (time-range-picker, command, navigation-menu) → token
`--space-px` /`--space-0.5` o aceptar.
- **Geometría de barra/indicador**: `block-size: 2px` (navigation-menu, command).
- **Underline**: `text-decoration-thickness: 1px` , `text-underline-offset: 2px|3px` .
### 2C — 🟢 Idiomas legítimos (NO tocar)
- `sr-only` : `width:1px; height:1px; margin:-1px` (button, link, command,
time-range-picker…) — patrón canónico de visually-hidden.
- **`em`-relativos**: `0.85em` , `1.125em` , `0.875em` , `0.25em` — escalan con la
fuente local; no son bypass.
- **`50%` círculos**: cropper handle, search-field clear, spinner — idioma de
círculo perfecto sobre elemento cuadrado.
- **Deltas de keyframe** de motion: `translateY(4px)` , `translateX(12px)` ,
`scale(0.985)` — offsets de animación.
- **`box-shadow: 0 0 0 9999px` (cropper mask)** — hack de overlay, no es radio.
### Top offenders por nº de literales (para priorizar lectura en Lote D)
`link-preview` 17 · `navigation-menu` 14 · `time-range-picker` 13 · `spinner`
12 · `link` 11 · `button` 10 · `tooltip` 9 · `carousel` 9 · `command` 8 ·
`color-picker` 8 · `tree-grid` 7 · `password-field` 7 — (la mayoría caen en
2B/2C; el 🔴 real es chico).
---
## DIMENSIÓN 3 — Coherencia de contrato
`scripts/eidos-lint-all.ts` : ** `invalid = 0` en los 116 componentes.** Ningún
selector eidos targetea un attr con valor que el morfo no declara. El
typed-builder (`semaSelector`) + el lint cumplen su función. **Sin hallazgos.**
`eidos-only` (selectores que el morfo no declara pero son legítimos:
pseudo-estados, cross-component como `[data-popover-content] [data-year-grid]` ,
attrs estructurales) y `unused` (el morfo declara attrs que el CSS no estiliza)
son informativos, no bugs.
---
## DIMENSIÓN 4 — Verificación de morfo
- Todos los morfos compilan limpios (el lint los carga sin error).
- **`unused` alto** en algunos (drawer 16, stepper 28, field 22, css-field 18,
number-field 18): el morfo declara attrs que su CSS no estiliza. Casi siempre
legítimo — los consume soma/sema o son state-only. `css-field` /`number-field`
tienen CSS propio vacío porque delegan en el visual compartido `spin-field`
(identidad estructural). 🟡 verificar puntualmente que ninguno sea declaración
muerta — baja prioridad.
- **`corner-shape`** (Button `types.ts` : `rounded` /`continuous`/`cut`/`scoop`):
documentado como prop pero **sin materialización encontrada en `button.css`** .
Posible prop-sin-implementar = incoherencia de contrato. 🟡 verificar/implementar.
---
## DIMENSIÓN 5 — Homogeneidad transversal (API + visual)
Consistencia del vocabulario y del aspecto entre componentes — lo que hace que la
librería se sienta "de una pieza". Verificado por grep de types.ts + CSS.
### 5A — Focus-ring NO homogéneo (🔴, headline)
El **valor** del foco es casi uniforme (`--color-focus-ring` = alias de
`--focus-ring-color` ; los ~18 `--{c}-focus-shadow` son copias del composite
`--focus-ring` ). Lo que rompe es el **mecanismo** :
| Mecanismo | Componentes | Nota |
|---|---|---|
| `outline: var(--focus-ring-width) solid var(--focus-ring-color)` | ~22 (badge, card, carousel, drawer, form, listbox, menubar, popover, table, tree-*, virtual-*…) | **el estándar** |
| `box-shadow: var(--{c}-focus-shadow)` (composite) | ~18 (checkbox, radio-group, radio-cards, select, slider, date-field, time-field, tags-input, toolbar, pagination, stepper, rating-group, breadcrumb, color-field, editable, file-upload, switch…) | **muere en forced-colors (HCM)** = regresión a11y |
| `outline: var(--border-width-medium) solid var(--color-primary-solid)` | button, link, code-block | 3ª convención (ancho + color distintos a los tokens de focus) |
| `outline … var(--color-focus-ring)` | cropper, image-adjustments, image-picker | token-alias redundante |
→ El anillo de foco **se ve y se comporta distinto** según el componente. **Fix** :
unificar todo a `outline: var(--focus-ring-width) solid var(--focus-ring-color)` ;
eliminar los `--{c}-focus-shadow` , la 3ª convención y el alias `--color-focus-ring` .
Cierra además el a11y de forced-colors (deferido en el sprint de theming).
**Familia FIELD migrada 2026-06-23** (a petición del usuario: «solo fields ahora» +
**sin hueco**). El composite tiene `--focus-ring-inner-width: 0` , así que el ring es
visualmente equivalente al outline; el `outline-offset` se deja en **0** (no
`--focus-ring-offset` ) porque con outline el gap es transparente (muestra el fondo) y
el usuario lo vio como hueco — pegado queda limpio. Migrados a
`outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: 0` :
field · date/time/color-field · search · select · combobox · editable · spin-field
(+number/css) · tags-input · pin-input · textarea · password-field (ring en el
`::before` chrome) · color-picker-trigger · y los campos de fecha/hora embebidos en
date-range-picker / time-picker / time-range-picker. **11 tokens `--{c}-focus-shadow`
huérfanos eliminados del recipe** (field-control, spin× 2, date/time/color-field,
color-picker-trigger, search, password× 2, select). orphan test PASA; check sin
errores nuevos; verificado en navegador (field + date-field: anillo pegado sin hueco).
**Huérfanos del contract test (consumed-vars) cerrados 2026-06-23** — al migrar el
focus salieron 3 vars consumidas-no-declaradas (test distinto del orphan):
`scroll-area: --scroll-area-auto-max-block` → declarado en el recipe (default `none` ,
el consumidor —p.ej. command— lo override); `menu-dial: --menu-dial-*` (8 vars: arc-
span/start, count, gap, i, offset, radius, backdrop-bg, stagger-step) → menu-dial es
CSS-puro sin recipe, así que todas sus vars internas se privatizaron a `--_menu-dial-*`
(CSS + los 2 svelte que setean count/i/radius inline). Verificado: stagger calcula
(transition-delay 0.12s = i× 28ms). Único fallo restante del contract = `palabras`
(vedado: words/palabras/chronos no se tocan).
**Resto (NO-field) migrado 2026-06-23** — mismo patrón (outline + offset 0):
breadcrumb, calendar (+month-grid/year-grid/range-calendar reúsan su token),
pagination, rating-group, toolbar, file-upload, tag-group, stepper, editable (input
editing: conserva control-shadow + outline), tags-input (item activo + triggers);
checkbox/radio-group/radio-cards/switch (`--{c}-focus-ring` → outline); button YA era
canónico, code-block + link (3ª convención) → canónico; cropper/image-picker/
image-adjustments (alias `--color-focus-ring` → `--focus-ring-color` ); slider +
time-picker/time-range clock thumbs (outline + conserva el thumb-shadow). **13 tokens
`--{c}-focus-shadow` huérfanos eliminados** del recipe. `color-field-segment` (inset)
NO se toca. **words/palabras/chronos vedados** — words-focus-shadow se rozó por error
de ancla y se restauró. Contract test: pasa salvo `palabras` (vedado).
**Deuda fina CERRADA 2026-06-23**: los composites muertos `--checkbox-focus-ring` /
`--radio-group-focus-ring` + sus 4 sub-tokens cada uno (`-offset/-width/-color/
-surface`), y el privado `--_switch-focus-ring` + sus 4 sub-tokens del recipe — todos
ELIMINADOS (sin consumidor tras migrar a outline; el test no los detectaba por
substring). `field-control-trigger` era otro outlier 3ª-convención (escapado del
sweep) → migrado al canon. El alias ** `--color-focus-ring` NO se elimina**: lo
consume `palabras` (vedado); los `border-focus` que lo usan (field/spin/search/
password/dialog/textarea) son funcionalmente correctos (alias ≡ `--focus-ring-color` ),
así que quedan como están — el alias persiste solo para no romper el track vedado.
### 5D — Fondo de la familia field/input NO homogéneo (🔴) — RESUELTO 2026-06-23
Re-auditado empíricamente (el cuadro original estaba parcialmente STALE):
- **`field` era el ÚNICO outlier de verdad** — default `variant='outline'`
(fondo TRANSPARENTE vía `--field-control-ghost-bg` ) mientras los **otros 12**
(select, textarea, date/time/color-field, search/password/number/css-field,
combobox, editable, tags-input) default `variant='surface'` → surface-default.
Como muchos campos de texto se construyen con `<Field>` , se veían TODOS
transparentes junto a un select con fondo. **Fix** : field default
`'outline'` → `'surface'` (`field.svelte` + `types.ts @default` ). Verificado:
field control bg = `surface-default` , idéntico al select.
- **editable / tags-input**: el audit decía `surface-raised` → STALE. Medido:
`--editable-control-bg` y `--tags-input-control-bg` YA son `surface-default`
(el `trigger-bg` raised que confundía es de un *botón* trigger, no del control).
- **textarea**: el fantasma `--color-surface` YA estaba arreglado (default).
- **Color-field embebido en el color-picker** (reportado por el usuario): el
override `[data-color-picker-content] [data-color-field-input]` lo pintaba
`surface-overlay` + `border-color: transparent` = EXACTAMENTE el fondo del
popover (overlay) → el campo se veía **transparente** (se fundía). **Fix** :
quitado el override de bg/border → usa su propio `--color-field-bg`
(surface-default) + borde, que CONTRASTA con el popover. Verificado
(`nowDistinctFromPopover: true`, screenshot). Era el único input que se fundía
con su contenedor (los demás `surface-overlay` son `:hover` /headings, correctos).
### 5D-bis — Label de la familia field: font-size canónico (RESUELTO) + color (PENDIENTE)
- **font-size**: canon = un paso por debajo del control, escala (field/date/time/
color-field usan `calc(... - (font-md - font-sm))` ). ** `color-picker` era el
outlier**: `font-size: var(--font-size-sm)` FIJO + `font-weight: medium` . Fix:
→ `calc(var(--_color-picker-trigger-font-size) - (font-md - font-sm))` (escala
sm12/md14/lg16) + weight heredado (regular). Verificado.
- **color (PENDIENTE — decisión de canon)**: `field` label = `content-primary` ;
date/time/color-field + color-picker = `content-secondary` . Diverge. Falta
decidir el canon (primary vs secondary) antes de unificar.
### 5E — Segmentos date/time/color: estado activo divergente + anillo + flicker (🔴) — RESUELTO 2026-06-24 (ver hand-off al final)
Afecta `date-field` , `time-field` , `color-field` (estructura idéntica) → se propaga a
`date-picker` /`time-picker`/`date-range-*` que los embeben.
**Estado activo del segmento — divergente y con borde:**
| Componente | Fondo activo | Anillo | Peso |
|---|---|---|---|
| date-field | `accent-element` (fuerte) | inset ring (`--ring-inset-width`) | 600 |
| time-field | `accent-track` (sutil) | `--time-field-segment-focus-shadow` | — |
| color-field | `accent-track` (sutil) | `--color-field-segment-focus-shadow` | — |
→ Los tres llevan anillo; date lo enmascara con fondo fuerte, time/color lo muestran.
**Objetivo (canónico)**: el segmento activo se resalta con **fondo** y **sin borde/anillo** .
**Fix**: quitar el `box-shadow` (inset ring + `--{c}-segment-focus-shadow` ) en los tres;
unificar a UN token de fondo (decidir element vs track en browser) + `color: accent-text`
(+ peso consistente). `outline: none` se queda.
**Flicker del borde del campo al incrementar por teclado (bug):** `[data-{c}-field-input]`
transiciona `border-color` +`box-shadow`, y `:has([segment]:focus-visible)` aplica
borde+shadow al campo entero. Al incrementar, el `:focus-visible` del segmento parpadea
(re-render del segmento en soma → blur/refocus), el `:has()` salta off→on, y la
**transición** desvanece el borde fuera/dentro = el parpadeo. **Fix** : quitar
`border-color` +`box-shadow` de la `transition` del input (foco instantáneo). Si persiste
un blink de 1 frame, raíz secundaria = retención de foco del segmento en soma.
### 5F — Modelo de anchura: dos defaults opuestos + fields desproporcionados (🔴)
**Dos modelos de anchura coexisten:**
- **`block`-opt-in** (~10 componentes: button, toggle, toggle-group, carousel, feed,
grid-list, listbox, table, tree-grid, tree-view): default **content-width** , prop
`block` → `data-block` → `inline-size: 100%` .
- **`100%`-siempre** (familia field: date/time/color-field, number, css, search,
password, combobox, select, textarea, field, + pickers): hard-codean
`inline-size: 100%` en root + input, **sin opt-out** (no hay prop `block` ).
→ Los fields de **contenido casi fijo** (date/time/color/pin) a 100% son
desproporcionados — un `12/31/2026` no necesita la columna entera; los pickers
(date/time) heredan el 100% del contenedor aunque su contenido sea estático. (Los
`max-content` existentes son del popover del calendario, no del trigger.)
**Fix (alinear al modelo del propio sistema)**: la familia field adopta el patrón
`block` —
- **Contenido fijo** (date/time/color-field, pin): default `inline-size: max-content`
(segmentos + icono + padding); `block` → 100%.
- **Contenido variable** (text/search/number/combobox/select/password/textarea):
decidir — mantener un ancho por defecto razonable (no colapsar vacío) + `block` ,
o conservar 100% (convención de columna de formulario).
feat(eidos): IconButton primitive + Lote 0 iconOnly->IconButton cascade
Lote 0 of the component-coherence audit (FASE 2):
- New IconButton primitive (composes Button: inherits variant/size/intent/
color/rounded/shape/loading + Button's morfo + sema, no new morfo;
aria-label required; glyph as children). New svg/ glyphs: SvgX, SvgPlus,
SvgMinus.
- Button: rounded='full' 9999px -> var(--radius-full) (theme-driven radius,
value-identical).
- Migrate icon-only Button consumers to IconButton: carousel prev/next
(pilot), tree-grid expand, table row-detail, search-field clear, combobox
clear. Pattern: glyph->children, label->aria-label, drop iconOnly.
- carousel.css: fix arrow positioning (specificity vs Button's base
position:relative, exposed by the IconButton import-order flip) + the
press-squeeze jump (centre via the translate property so the contact
press transform:scale composes instead of replacing the centring).
- Doc: COMPONENT_COHERENCE_AUDIT - indicator-chevron vs discrete IconButton
distinction (tree-view/accordion stay out); 5F width closed (full-width);
Lote 0 hand-off + pending select-family faults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
**Decisión de diseño — CERRADA (2026-06-18): full-width para TODA la familia.**
El usuario rechazó `max-content` /`block` para los fields. La anchura la gobierna el
**contenedor** (columna de formulario, `Box` /`Stack` con `max-width` ), no el campo —
convención dominante (Chakra/Bootstrap/Radix Themes/shadcn = full-width por defecto;
MUI con `fullWidth` opt-in es el outlier). `inline-size: 100%` se queda en toda la
familia, sin opt-out por campo. **Sin acción de código** : el estado actual ya es
full-width homogéneo (verificado en el audit de anchura del 2026-06-18; color-field
era la única excepción —un `fit-content` temporal— ya revertido).
### 5B — Nits de API (🟡) — CERRADO 2026-06-24 (decisiones del usuario, ver hand-off)
- **`image-picker` default `size` ** — ✅ `'sm'` →`'md'` (decisión: consistencia).
- **`switch`** — ✅ STALE: ya NO declara `SwitchVariant` ni prop `variant` (solo
`SwitchSize` ). Variant-less correcto (color por intent+color). Sin acción.
- **`link` variant `'default'` ** — ✅ se MANTIENE (decisión: es la base con
significado; renombrar = churn de API por bajo valor).
- **code / code-block / kbd**: ✅ code-block + kbd `'surface'|'outline'|'ghost'`
**aliasados a `ControlVariant`** (duplicaban literales → doctrina "no unions
locales con literales duplicados"); **code** queda local (`'plain'|'soft'|'outline'`
no mapea a arquetipo; §19 permite variants component-specific).
### 5C — Sano (verificado, NO findings)
- **Variants**: la mayoría usa alias-arquetipo canónicos (`ControlVariant`,
`SelectionVariant` , `ChipVariant` , `MarkerVariant` ) → homogéneo por construcción.
- **Defaults**: `size='md'` universal (salvo image-picker); variant por familia
consistente (inputs `'surface'` , selección `'solid'` , chips `'soft'` ; field/accordion
`'outline'` = rol de wrapper, defendible).
- **Eje color/intent**: coherente y documentado — `color` (jerarquía) en todos;
`intent` (evaluativo) solo donde el componente **contiene Buttons y se lo reenvía**
(carousel/table/clipboard/dropdown-menu/alert-dialog: "flows to Button", `color`
aplica bajo `intent='neutral'` ). `banner` intent-only = componente de feedback.
NO es deriva.
- **Transición**: 17 consumen `var(--duration-fast)` directo; los
`--{c}-transition-duration` encadenan a `--duration-*` desde el recipe → valor
homogéneo (solo varía la indirección). NO finding.
- **Disabled**: 27 consumen `var(--opacity-disabled)` (unificado en el sprint de
theming); los `0.X` restantes son otras opacidades (muted/overlay) ya cubiertas
en dim. 2. NO finding nuevo.
## PLAN DE REMEDIACIÓN POR LOTES
- **Lote 0 — primitivos compartidos (enabler)**:
1. `svg/` : `SvgX` (+ `SvgPlus` /`SvgMinus` para inc/dec) — animables, `pathLength` ,
`data-svg` , hermanos de chevron/check/dash/dot.
2. `IconButton` (eidos): compone `Button` , `aria-label` requerido, agnóstico de
glifo, layout icon-only (`data-icon-only`). Sin morfo nuevo.
3. `Button` : quitar `iconOnly` del API público (modo interno disparado por
IconButton) + `rounded='full'` → `var(--radius-full)` .
4. Migrar los **14** `<Button iconOnly>` → `<IconButton>` . Piloto verificado:
carousel.
- **Lote A — Button-consumer** (patrón `carousel-prev-trigger` , compone Lote 0):
los 1B. calendar/range-calendar prev/next → `IconButton` +`SvgChevron`; closes
(dialog/drawer/popover/banner/toast/float-panel) → `IconButton` +`SvgX`. Borrar
el chrome `--*-control-*` /`--*-close-*` redundante del CSS.
- **Lote B — `<select>` → `<Select>` ** (1A, estructural): morfo (`input`→composite)
+ soma (render `<Select.*>` ) + a11y + borrar chrome de select. 6 sitios.
- **Lote C — identidad estructural** (1D opcional): tabs/pagination al look de
Button vía `data-button` (precedente toggle-group), manteniendo su sema.
- **Lote D — literales + contrato + doctrina glifos**: 2A 🔴 (button/cropper
`9999px` →`--radius-full`, grid-list, nav-menu); decidir 2B (token de focus-offset,
micro-space, escalas internas → recipe tokens); verificar/implementar
`corner-shape` ; documentar la regla `svg/` -vs-`icon/` en sus READMEs.
- **Lote E — homogeneidad (dim. 5)**:
- **Focus-ring**: unificar a `outline: var(--focus-ring-width) solid
var(--focus-ring-color)` + `outline-offset` en TODO el set (migrar los ~18
box-shadow + la 3ª convención de button/link/code-block + el alias
`--color-focus-ring` ); cierra el a11y de forced-colors.
- **Fondo de fields (5D)**: unificar el `surface` de la familia input a
`--color-surface-default` ; `editable` /`tags-input` raised→default; arreglar
el token fantasma de `textarea` (`--color-surface` → `--color-surface-default` );
combobox vía token de recipe.
- **Segmentos date/time/color (5E)**: quitar el anillo (`box-shadow`) del segmento
activo en date/time/color-field, unificar el fondo de resalte a un token; **quitar
`border-color` +`box-shadow` de la transición del input** (elimina el flicker del
borde al incrementar por teclado).
feat(eidos): IconButton primitive + Lote 0 iconOnly->IconButton cascade
Lote 0 of the component-coherence audit (FASE 2):
- New IconButton primitive (composes Button: inherits variant/size/intent/
color/rounded/shape/loading + Button's morfo + sema, no new morfo;
aria-label required; glyph as children). New svg/ glyphs: SvgX, SvgPlus,
SvgMinus.
- Button: rounded='full' 9999px -> var(--radius-full) (theme-driven radius,
value-identical).
- Migrate icon-only Button consumers to IconButton: carousel prev/next
(pilot), tree-grid expand, table row-detail, search-field clear, combobox
clear. Pattern: glyph->children, label->aria-label, drop iconOnly.
- carousel.css: fix arrow positioning (specificity vs Button's base
position:relative, exposed by the IconButton import-order flip) + the
press-squeeze jump (centre via the translate property so the contact
press transform:scale composes instead of replacing the centring).
- Doc: COMPONENT_COHERENCE_AUDIT - indicator-chevron vs discrete IconButton
distinction (tree-view/accordion stay out); 5F width closed (full-width);
Lote 0 hand-off + pending select-family faults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
- **Modelo de anchura (5F)**: ✅ CERRADO (2026-06-18) — **full-width para toda la
familia** (container-driven). Sin acción de código: el estado actual ya es
homogéneo.
- **Nits API (5B)**: image-picker default `md` , `switch` variant colgante,
renombrar `link` `'default'` , normalizar variants de code/code-block/kbd.
### Verificación por lote
`npm run check` 0 errores · `npx vitest run src/uix/eidos` · `generate:eidos-css`
si cambian recipes · `eidos-lint` sin `invalid` nuevos · browser (Chrome MCP):
paridad visual (hover/focus/disabled) + a11y (foco/teclado) en selects migrados.
---
feat(eidos): IconButton primitive + Lote 0 iconOnly->IconButton cascade
Lote 0 of the component-coherence audit (FASE 2):
- New IconButton primitive (composes Button: inherits variant/size/intent/
color/rounded/shape/loading + Button's morfo + sema, no new morfo;
aria-label required; glyph as children). New svg/ glyphs: SvgX, SvgPlus,
SvgMinus.
- Button: rounded='full' 9999px -> var(--radius-full) (theme-driven radius,
value-identical).
- Migrate icon-only Button consumers to IconButton: carousel prev/next
(pilot), tree-grid expand, table row-detail, search-field clear, combobox
clear. Pattern: glyph->children, label->aria-label, drop iconOnly.
- carousel.css: fix arrow positioning (specificity vs Button's base
position:relative, exposed by the IconButton import-order flip) + the
press-squeeze jump (centre via the translate property so the contact
press transform:scale composes instead of replacing the centring).
- Doc: COMPONENT_COHERENCE_AUDIT - indicator-chevron vs discrete IconButton
distinction (tree-view/accordion stay out); 5F width closed (full-width);
Lote 0 hand-off + pending select-family faults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
## Estado de ejecución — hand-off 2026-06-18
### Lote 0 — IconButton + cascada `iconOnly`→`IconButton`
**Hecho + verificado:**
- **Primitivos**: `svg/SvgX` · `SvgPlus` · `SvgMinus` (animables, `pathLength` ).
** `IconButton` ** (`components/icon-button/`): compone `<Button>` (hereda
variant/size/intent/color/rounded/shape/loading + morfo + sema de Button; **sin
morfo nuevo**), `aria-label` **obligatorio** , glifo como `children` , `iconOnly`
interno. Button: `rounded='full'` `9999px` → `var(--radius-full)` .
- **Migrados** (patrón: glifo→`children`, label→`aria-label`, fuera `iconOnly` ):
carousel (prev/next, piloto) + los **4 de ①** — `tree-grid-expand` ,
`table-row-detail` , `search-field-clear` , `combobox-clear` . Compile 0-nuevos +
medidos en navegador.
**Gotchas (releer antes de seguir la cascada):**
- **Orden de carga CSS**: migrar `Button` →`IconButton` mete una indirección de
import (consumidor→IconButton→Button) que **voltea el orden de carga** → rompe
empates de especificidad con `[data-button]{position:relative}` (las flechas del
carousel se descolocaron, `relative` ganó a `absolute` ). **Fix** : subir la
especificidad del posicionado del consumidor con un ancestro (`[data-carousel]
[data-…-trigger]`, 0,2,0 > 0,1,0). Los clears del campo son **flex-items** → sin
empate.
- **press-squeeze vs centrado por transform**: `contact-activate` (lo dispara TODO
Button) aplica `press-squeeze` (`transform: scale(0.96)`), que **reemplaza** un
`transform: translateY(-50%)` de centrado → **salto** al pulsar. **Fix** : centrar
con la **propiedad `translate`** (compone con el `scale` en vez de borrarlo);
quitar overrides `:active` .
**Pendiente del Lote 0:**
- **② condicionales** (NO icon-only puro): `clipboard-trigger` (default texto
"Copy") y `dropdown-menu-trigger` (trigger genérico) reenvían `iconOnly` opcional
→ no se migran de golpe. **Decisión** : render condicional `IconButton` /`Button`, o
se quedan en Button. **Bloquea el cierre** (quitar `iconOnly` del API público de
Button).
- **corner-shape** (dim 4 / Lote D): Button declara `shape` + emite `data-shape`
pero `button.css` **NO lo materializa** (0 reglas `corner-shape` ) → todo
botón/IconButton es **solo redondo-arco** . `SHAPE_ENGINE_RFC §6` lo difiere
(adoptarlo = adelantar — decisión del usuario).
- **tree-view + accordion** quedan FUERA (chevron = indicador, no botón discreto —
ver la nota en Dimensión 1).
### Faults de los selects (reportados por el usuario — siguiente bloque)
| Componente | Faults |
|---|---|
| ~~**Combobox**~~ ✅ TODOS RESUELTOS — verificado 2026-06-23 (el doc estaba STALE; cerrados en sprints posteriores) | (a) ✅ el provider emite `emerge-open` /`emerge-dismiss` (sema canon) · (b) ✅ `[data-scroll-area-viewport]` tiene `padding-block-end` · (c) ✅ ya compone `<ScrollArea autosize>` · (d) ✅ el soma excluye el trigger del dismiss (líneas 594-611, comentario explícito); verificado en vivo: clic-flecha → `data-state` open→closed en UN solo cambio, NO reabre · (e) ✅ STALE — el default md ya es `--font-size-md` (1:1) · (f) ✅ gap 6px (`data-floating-gap='panel'` o `sideOffset` ), no pegado · (g) ✅ `data-depth='overlay'` + shadow (A1/B2) |
| **SearchField** ⚠️ faults NO aplican a este componente — verificado 2026-06-23 | El search-field eidos es un **input simple** (icon + input + clear + loading): NO tiene popup, scroll ni footer. Los faults "scroll→ScrollArea" / "footer" eran del **command palette** (input + lista + footer), mal-atribuidos aquí. Sin acción. |
| **Select** ✅ mitigado 2026-06-23 | "item seleccionado: borde y resaltado muy pegados" venía del padding de panel de **4px** ; el canon de listas subió a **6px** hoy (`--list-content-padding`), dando margen coherente del token. Verificado en vivo (item a 6px del borde, radius concéntrico 4px sobre panel 10px). |
| **Langs (REGLA transversal)** ✅ eidos cubierto 2026-06-24 | **cero hardcodes** — las strings (aria-labels…) van en el **morfo** del componente vía langs. **Hecho** : 6 wrappers de la cascada IconButton (carousel prev/next · combobox-clear · tree-grid-expand · table-row-detail · search-field-clear · banner-close) hardcodeaban inglés y **pisaban** el `props['aria-label']` ya resuelto por soma → arreglados con el idiom de drawer-close (`resolvedLabel = ariaLabel ?? eidos.langs.ts(idlangref)`; sin default hardcodeado). Cerró además 2 **divergencias del contrato** (combobox 'Clear'→'Clear value', table 'Toggle row details'→'Toggle detail'). +onion-menu surface, cropper zoom (4 entradas nuevas), soma time-picker day-period. **Deferidos** (necesitan superficie langs nueva / solapan con DIM 1): code-block copy (→ componer `<Clipboard>` en Lote A), avatar-group conteo visible, date-range-picker year/month soma views ("Select year range"/"Previous decade"). Verificado en navegador (ES resuelve). |
feat(eidos): IconButton primitive + Lote 0 iconOnly->IconButton cascade
Lote 0 of the component-coherence audit (FASE 2):
- New IconButton primitive (composes Button: inherits variant/size/intent/
color/rounded/shape/loading + Button's morfo + sema, no new morfo;
aria-label required; glyph as children). New svg/ glyphs: SvgX, SvgPlus,
SvgMinus.
- Button: rounded='full' 9999px -> var(--radius-full) (theme-driven radius,
value-identical).
- Migrate icon-only Button consumers to IconButton: carousel prev/next
(pilot), tree-grid expand, table row-detail, search-field clear, combobox
clear. Pattern: glyph->children, label->aria-label, drop iconOnly.
- carousel.css: fix arrow positioning (specificity vs Button's base
position:relative, exposed by the IconButton import-order flip) + the
press-squeeze jump (centre via the translate property so the contact
press transform:scale composes instead of replacing the centring).
- Doc: COMPONENT_COHERENCE_AUDIT - indicator-chevron vs discrete IconButton
distinction (tree-view/accordion stay out); 5F width closed (full-width);
Lote 0 hand-off + pending select-family faults.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
Orden: **combobox → search-field → select → barrido de langs** , uno a uno verificando.
`combobox-arrow` /`content`/`viewport` son thin-wrappers del soma (la lógica de
abrir/cerrar + el scroll viven en soma; el styling en `combobox.css` ).
---
docs(eidos): floating-gap canon (THEMING §36) + audit hand-off 2026-06-22
THEMING.md: new §36 documenting the token-driven floating gap (@property
--floating-gap, the menu/panel archetypes, the conditional stamp, why offset not
margin). Footer date -> 2026-06-22.
COMPONENT_COHERENCE_AUDIT.md: status line + a "hand-off 2026-06-22" section
recording this session's work (action components, 1:1 menu font, 2A radius, the
gap canon) plus the suggested order for next session (color-picker popover bug
task_5d1a10ec, selects faults, focus-ring 5A, field-bg 5D, composition lots).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
## Estado de ejecución — hand-off 2026-06-22 (CONTINUAR AQUÍ MAÑANA)
Sesión larga; varios cierres + el gap canónico. Para retomar:
### Hecho esta sesión
- **Action components shipped** (`c00bac13`): fab, onion-menu, button-group,
split-button (eran untracked). Hardcodes → recipes; pasan el contract test (solo
palabras queda, excluida). fab = escala off-ladder Material en recipe (las
escalas canónicas no la expresan — decisión del user); onion trigger lift/shadow
→ `var(--fab-*)` ; split-button → DropdownMenu `customAnchor` . Memoria
`project_action_components_2026-06-22` .
- **Font 1:1 en menús** (`7cf30287`): el sweep de campos (md=16) se saltó dos
menús + un picker trigger compactos (md→sm). Arreglados: **menubar** ,
**navigation-menu** , **color-picker trigger** (era doble-compacto md→xs) → 1:1.
⚠️ El grep `font-size: var(--font-size-sm)` **sobre-cuenta** (matchea variantes
sm/lg correctas): combobox/command/textarea YA eran 1:1 — la nota "combobox
md=sm" de §5E / faults está STALE. Verificar el bloque DEFAULT md (`[data-X] {`
sin data-size), no el grep crudo. Font de PANEL de pickers + label de menu-dial =
contenido plano (sin eje de size), quedan compactos.
- **2A radius bypass CERRADO** (mismo commit): grid-list `0.2rem` →`--radius-sm`;
navigation-menu indicador `1px` →`--radius-full`. (button `9999px` ya estaba;
cropper `0 0 0 9999px` es el hack de máscara, NO tocar.)
- **Gap canónico trigger→panel** (`1590684c` foundation+piloto + `d229447a`
cascade): el `sideOffset` de floating-ui es un número (no acepta token), así que
cada flotante hardcodeaba el suyo (0/4/6/8). Solución: `@property --floating-gap`
que el posicionador compartido lee como OFFSET (token-driven + arrow-safe; NO el
margin de split-button, que despegaría la flecha). Tokens: menu 0 / panel
`--space-1-5` . Stamp CONDICIONAL (`sideOffset===undefined ? arquetipo : undefined`)
— el override del consumidor gana. Memoria `project_floating_gap_canon_2026-06-22` ;
doc en THEMING.md.
### Pendiente (orden sugerido)
1. ✅ **Bug popover color-picker** (`task_5d1a10ec`) — RESUELTO 2026-06-23. Causa
raíz: el `[data-color-picker-content]` fue escrito para la arquitectura ANTIGUA
del popover (sin viewport interno) y re-declaraba `padding` +`flex`+`gap`+
`overflow-x` +`scrollbar-gutter` en el panel. Tras introducirse el
`[data-popover-viewport]` (padding + scroll + grid) eso causó: doble padding
(content 14px + viewport 12px), el `flex/gap` aplicado al único hijo (el viewport)
en vez de a las filas, los constraints `> *` mal dirigidos, y el grid item sin
`min-inline-size:0` → scroll horizontal. **Fix** : el content delega TODO el
layout al viewport + picker-shell; solo conserva las vars (checker/accent) + los
width-overrides del popover; `min-inline-size:0` en shell/body; `scrollbar-gutter:
stable` en el viewport (donde vive el scroll real).
- **HOMOGENEIZACIÓN (corrección de rumbo del usuario)**: el primer intento
preservó el padding PROPIO del color-picker (14px vía `--_popover-px/py`
override + token de recipe) — un error: mantenía la divergencia. Verificado que
**NINGÚN otro panel flotante** (date/time/range pickers, link-preview, tooltip)
override el padding del popover — todos heredan el canónico `--popover-padding-*`
(12px md, size-aware). El color-picker era el ÚNICO outlier. Fix final: hereda
el padding del popover; tokens `content-padding-{sm,md,lg}` + `content-gap`
BORRADOS del recipe (eran divergentes/huérfanos); var muerta
`--_color-picker-content-padding` eliminada. Verificado en navegador:
`matchesPopoverCanon: true` (12px == popover), `hScroll: false` .
- **DOCTRINA**: hay DOS clases de superficie flotante con canon propio, cada una
internamente homogénea — **lista** (menús/select/combobox/listbox →
`--list-content-padding` + items con su ritmo) y **panel** (popover + todo lo
que se monta en él: pickers/link-preview → `--popover-padding-*` , 12px md). Un
panel flotante NO declara padding propio; hereda el del popover. Cualquier
componente que se monte en `[data-popover-content]` y override `--_popover-px/py`
es un outlier a corregir.
- **Canon de listas recalibrado 2026-06-23** (a petición del usuario tras el
fix): `--list-content-padding` subió de 4→6px en md; progresión completa
**2/4/6/8/10** (xs/sm/md/lg/xl, estrictamente creciente, pasos de 2px). Encaja
con la industria (Radix/shadcn/Mantine/AntD 4px · MUI/Bootstrap 8px → 6px =
punto medio). Verificado en dropdown + select (6px). El padding de **panel** se
deja en **12px** (= Radix Popover size-1, Chakra; 10px quedaría bajo toda la
industria). ** `command` es outlier JUSTIFICADO** (queda fuera del canon de
lista): su padding del scroll-viewport (`0 22px 4px 8px`) alinea los items con
el search input, no con el ritmo de lista — NO tocar.
docs(eidos): floating-gap canon (THEMING §36) + audit hand-off 2026-06-22
THEMING.md: new §36 documenting the token-driven floating gap (@property
--floating-gap, the menu/panel archetypes, the conditional stamp, why offset not
margin). Footer date -> 2026-06-22.
COMPONENT_COHERENCE_AUDIT.md: status line + a "hand-off 2026-06-22" section
recording this session's work (action components, 1:1 menu font, 2A radius, the
gap canon) plus the suggested order for next session (color-picker popover bug
task_5d1a10ec, selects faults, focus-ring 5A, field-bg 5D, composition lots).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
2. **Faults de los selects** (sección arriba): combobox (7 faults) → search-field →
select → barrido langs cero-hardcodes.
3. **DIM 5A focus-ring** (🔴 headline): unificar ~18 `box-shadow` focus → `outline`
+ `outline-offset` ; cierra el a11y de forced-colors.
4. **DIM 5D fondo de fields** : editable/tags-input raised→default (verificar si
deliberado). El fantasma de textarea (`--color-surface`) YA está arreglado
(`--color-surface-default`) — esa parte de §5D está stale.
5. **DIM 1 composición** : Lote A (Button-consumer: closes dialog/drawer/popover/
banner/toast → IconButton+SvgX; calendar/range prev/next) · Lote B (6 selects
nativos → `<Select>` ).
---
## Estado de ejecución — hand-off 2026-06-24
Sesión continuando la FASE 2 (sin agentes, vedados words/palabras/chronos).
Cerrados: barrido de langs (eidos), DIM 5E, DIM 5B. Pendientes reales abajo.
### Hecho + verificado
- **Barrido de langs (cero-hardcodes) — capa eidos cubierta.** El patrón roto lo
dejó la cascada IconButton: el wrapper eidos inyectaba un default **inglés
hardcodeado** en `aria-label` y lo **pisaba sobre** el `props['aria-label']` que
soma ya resolvía desde el morfo (`translationRef`). `props` es
`Record<string, unknown>` → no satisface el `aria-label: string` requerido por
`IconButton` , así que el fix canónico (idiom de **drawer-close** ) re-resuelve:
`const resolvedLabel = $derived(ariaLabel ?? eidos.langs.ts(idlangref))` →
`aria-label={resolvedLabel}` , **sin default hardcodeado** . Aplicado a: carousel
prev/next · combobox-clear · tree-grid-expand · table-row-detail · search-field-clear
· banner-close (este añadió `common.buttons.dismiss` ). Cerró 2 **divergencias de
contrato** (combobox hardcodeaba 'Clear' vs morfo 'Clear value'; table 'Toggle row
details' vs 'Toggle detail'). +onion-menu surface (langs ya existía) · cropper zoom
(4 entradas nuevas en `cropperLangs` ) · soma time-picker day-period (añadió
`DAY_PERIOD` a `TIME_PICKER_LANGS` reusando `common.time.day-period` ). **Verificado
en navegador (ES)**: carousel → "Diapositiva anterior/siguiente", combobox →
"Borrar valor". **Deferidos** (necesitan superficie langs nueva o solapan con DIM 1):
code-block copy ("Copy code to clipboard" → mejor **componer `<Clipboard>`** en
Lote A, que ya posee `clipboardLangs` ), avatar-group "{count} more" (texto visible),
date-range-picker year/month **soma** views ("Select year range"/"Previous decade"
— faltan entradas decade/range en common).
- **DIM 5E (segmentos date/time/color) — RESUELTO.** Dos partes:
1. **Estado activo divergente + anillo + peso** → ya lo resolvió la migración 5A:
existe una capa compartida `field/field-segment-state.css` (selecciona
`[data-field-segment]` , el marcador eidos) que da fondo uniforme
`--field-segment-active-{bg,text}` , **sin anillo** (`outline: none`) y **sin
cambio de peso** (el comentario explica: bold ensancharía el segmento ch-based).
Los 4 componentes con segmentos (date/time/color-field + date-range-picker) la
importan vía su `*-field-segment.svelte` y delegan (date-field ya NO usa
`accent-element` fuerte + 600). Confirmado: tokens resuelven, marcador presente.
2. **Flicker del borde al incrementar por teclado** → **arreglado** : el input
transicionaba `border-color` (+ `box-shadow` **vestigial** , ya no hay focus-shadow
tras 5A) y `:has(segment:focus-visible)` togglea el borde accent; al incrementar,
el `:focus-visible` parpadea (soma re-renderiza → blur/refocus) y la **transición
desvanecía** el borde = flicker. Fix: `transition: background …` **solo**
(border-color instantáneo; el anillo es `outline` , ya instantáneo). Aplicado a
date-field · time-field · color-field · date-range-picker (input **y** group,
ambos con `:has(segment:focus-visible)` ). **time-picker/time-range-picker NO se
tocan**: su `[data-…-trigger]` es un **botón** con `:hover` que transiciona
border/bg/color legítimamente — no es un input segmentado; su time-field embebido
ya quedó por time-field.css. Verificado: `transitionProperty === 'background'` en
los 4; renderizan.
- **Nota de tooling**: el navegador automatizado del preview **no tiene foco de
ventana** (`document.activeElement === segmento` pero `matches(':focus') === false` ),
así que `:focus-visible` no engancha vía eval — no se pudo screenshotear el anillo
de foco real. Verificación: la regla de foco **no la modifiqué** (solo la línea de
transición) + los tokens resuelven; el flicker es mecánicamente imposible
(border-color ya no transiciona).
- **DIM 5B (nits de API) — cerrado con decisiones del usuario.**
- **switch**: STALE — `switch/types.ts` ya NO declara `SwitchVariant` ni prop
`variant` (solo `SwitchSize` ). Correcto variant-less (el color va por intent+color).
Sin acción.
- **code-block + kbd** → **aliasados a `ControlVariant`** : su `surface|outline|ghost`
duplicaba literalmente `ControlVariant` → doctrina components-README ("no unions
locales con literales duplicados"). Cero cambio de valores/CSS/consumidores.
- **code** → queda local (`plain|soft|outline` no mapea a un arquetipo; §19 permite
variants component-specific).
- **image-picker** → default `size` ** 'sm'→'md'** (decisión del usuario: consistencia;
el size fluye al panel `<ImageAdjustments>` embebido).
- **link `'default'` ** → **se mantiene** (decisión del usuario: es la variante base
con significado; renombrar = churn de API por bajo valor).
### Verificación al cierre
`svelte-check` : **0 errores en cualquier fichero que toqué** (total 59 = baseline;
preexistentes en demos untracked / soma file-upload / navigation-menu sideOffset).
Suite eidos: 9 fallas **todas en ficheros fuera de mi conjunto** (spin-field +
drawer-close = working-tree heredado de la 5A en curso; palabras = vedado; icon stroke
+ chronos/metrics = untracked/otros tracks) — **0 atribuibles a esta sesión** (un test
sobre un fichero que no edité tiene input idéntico → resultado idéntico). soma
time-picker + morfo: **65/65** . Navegador: langs ES + transición de los 4 inputs.
### Pendiente (orden sugerido)
1. **DIM 1 composición — Lote A** (Button-consumer) — **EN CURSO** :
- ✅ **popover-close** + **banner-close** → `IconButton` +`SvgX` (slices completos:
wrapper + types + borrado de chrome CSS + eliminación de tokens recipe huérfanos
+ regen; verificados en navegador, consistentes con dialog/drawer-close).
- ✅ **drawer-close** : limpiados los 15 tokens recipe huérfanos que la migración
previa dejó + arreglado el manifiesto de `component-visual-attrs` (drawer +
popover componen IconButton → no estampan `data-icon-only` , sin entrada). Suite
eidos 9→8 fallas (las 8 restantes son heredadas: spin-field, palabras, icon).
- **Gotcha confirmado** ([[iconbutton-lote0]]): migrar a IconButton voltea el orden
de carga CSS → `[data-button]{margin:0}` (igual especificidad, carga después) pisa
el positioning del consumidor. Fix: subir especificidad con ancestro
(`[data-banner] [data-banner-close]`).
- ⏳ **Pendiente del Lote A** : **toast-close** (19 refs CSS — chrome más complejo),
**float-panel-close** (chrome COMPARTIDO con `action` +`stage-trigger` → migrar los
tres o extraer con cuidado), **calendar/range prev/next** → `IconButton` +`SvgChevron`
(+ borrar `--calendar-control-*` ), **code-block copy** → componer `<Clipboard>` .
2. **DIM 1 — Lote B** (estructural): 6 `<select>` nativos → `<Select>` (calendar
month/year, range-calendar month/year, color-field/color-picker format).
3. **Barrido de langs — cola** (deferidos arriba): avatar-group conteo, date-range-picker
soma views (entradas decade/range).
4. **DIM 5D-bis** (color del label de field): decidir canon primary vs secondary
(field usa `content-primary` ; date/time/color-field + color-picker `content-secondary` ).
5. **Higiene del working tree heredado** (NO de esta auditoría, pero bloquea el verde
de la suite): orphans de spin-field + `drawer-close: data-icon-only` de la migración
5A en curso. Resolver cuando se cierre/commitee esa migración.
---
## Apéndice — precedentes / plantillas a reutilizar
- **Button-consumer**: `components/carousel/carousel-prev-trigger.svelte` .
- **Identidad estructural**: toggle-group (`data-toggle` + tokens recipe).
- **Visual compartido inc/dec**: `spin-field` (number-field + css-field).
- **Select compuesto**: `components/select/index.ts` .
- **Primitivos SVG**: `components/svg/` (SvgChevron/Check/Dash/Dot).
- **Doctrina**: `soma/COMPONENT_GUIDE.md` §4 + §12.7; memoria
`feedback_button_consumer_pattern` .