|
|
|
|
@ -0,0 +1,416 @@
|
|
|
|
|
---
|
|
|
|
|
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 (catálogo) — completa. FASE 2 (remediación) pendiente de lotes.
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 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) |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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)` +
|
|
|
|
|
`outline-offset: var(--focus-ring-offset)`; eliminar los 18 `--{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).
|
|
|
|
|
|
|
|
|
|
### 5D — Fondo de la familia field/input NO homogéneo (🔴)
|
|
|
|
|
|
|
|
|
|
Toda la familia default `variant='surface'` → **debería compartir fondo**. Casi
|
|
|
|
|
todos resuelven a `--color-surface-default` (= `--primitive-neutral-1`): field,
|
|
|
|
|
spin-field (number/css-field), date/time/color-field, search-field, password-field,
|
|
|
|
|
select-trigger, pin-input-cell. **Divergentes:**
|
|
|
|
|
|
|
|
|
|
| Componente | bg hoy | Debe ser |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| **editable** | `--color-surface-raised` (neutral-2) | `--color-surface-default` (o documentar si es deliberado) |
|
|
|
|
|
| **tags-input** | `--color-surface-raised` (neutral-2) | `--color-surface-default` |
|
|
|
|
|
| **textarea** | `var(--color-surface)` — **token FANTASMA** (no existe; solo `-default/-raised/-muted/-overlay`) → fondo roto/transparente | `var(--color-surface-default)` |
|
|
|
|
|
| **combobox** | hardcodea `var(--color-surface-default)` en CSS | token `--combobox-trigger-bg` (valor OK, patrón) |
|
|
|
|
|
|
|
|
|
|
→ Es lo que se ve: `editable`/`tags-input` con fondo más elevado, `textarea` sin
|
|
|
|
|
fondo. **Fix**: unificar el `surface` de la familia a `--color-surface-default`;
|
|
|
|
|
arreglar el fantasma de textarea; combobox vía token.
|
|
|
|
|
|
|
|
|
|
### 5E — Segmentos date/time/color: estado activo divergente + anillo + flicker (🔴)
|
|
|
|
|
|
|
|
|
|
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).
|
|
|
|
|
|
|
|
|
|
**Decisión de diseño** (implicación de layout de formularios): ¿alineamos TODA la
|
|
|
|
|
familia al modelo `block`, o solo los de contenido fijo (date/time/color/pin)?
|
|
|
|
|
|
|
|
|
|
### 5B — Nits de API (🟡)
|
|
|
|
|
|
|
|
|
|
- **`image-picker` default `size='sm'`** — todos los demás controles default `'md'`. Outlier.
|
|
|
|
|
- **`switch`** define el tipo `SwitchVariant` (= `SelectionVariant`) pero **no expone
|
|
|
|
|
prop `variant`** (sin `data-variant`) — tipo colgante o variant-less intencional
|
|
|
|
|
(verificar; checkbox/radio/toggle sí exponen variant `'solid'`).
|
|
|
|
|
- **`link` variant `'default'`** — valor no-semántico (el resto usa nombres como
|
|
|
|
|
`subtle`/`plain`/`soft`). Renombrar.
|
|
|
|
|
- **code / code-block / kbd**: subsets de variant ad-hoc (`'surface'|'outline'|'ghost'`,
|
|
|
|
|
`'plain'|'soft'|'outline'`) en vez de un alias-arquetipo. Familia "texto/superficie".
|
|
|
|
|
|
|
|
|
|
### 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).
|
|
|
|
|
- **Modelo de anchura (5F)**: alinear la familia field al patrón `block` — fields
|
|
|
|
|
de contenido fijo (date/time/color/pin) → `inline-size: max-content` + `block` para
|
|
|
|
|
100%. _(Pendiente decisión: ¿toda la familia o solo contenido-fijo?)_
|
|
|
|
|
- **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.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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`.
|