From ebc742e73b39f96c0eca793b481891acedebea43 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 24 Jun 2026 13:52:26 +0200 Subject: [PATCH] =?UTF-8?q?fix(eidos):=20component-coherence=20audit=20FAS?= =?UTF-8?q?E=202=20=E2=80=94=20focus-ring,=20fields,=20segments,=20langs,?= =?UTF-8?q?=20API=20nits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles the uncommitted audit work on this branch (active-uix): - DIM 5A: focus-ring unified to `outline` across the control/field set (kills ~18 box-shadow rings; closes forced-colors a11y). NOTE: spin-field + drawer-close orphan-token loose ends from this migration still trip the eidos contract test — to clean when that migration settles. - DIM 5D: field/input backgrounds homogenized to surface-default; list-surface padding recalibrated 2/4/6/8/10; menu-dial internal vars privatized. - DIM 5E: date/time/color-field + date-range-picker segments delegate the active state to the shared field-segment-state layer; the input transition is reduced to `background` only so the focus border no longer flickers on keyboard increment (border-color is now instant; the ring is an outline). - Langs (cero-hardcodes): IconButton-cascade wrappers (carousel/combobox/ tree-grid/table/search-field/banner) + onion-menu + cropper + time-picker day-period now route aria-labels through morfo langs instead of hardcoded English (the hardcode was overriding soma's already-resolved label). Fixes 2 contract divergences (combobox Clear->Clear value, table toggle label). - DIM 5B: code-block/kbd variants aliased to ControlVariant (no duplicate literals); image-picker default size sm->md. Excludes words/palabras/chronos and the metrics/demos tracks (separate work). Co-Authored-By: Claude Opus 4.8 --- src/uix/eidos/COMPONENT_COHERENCE_AUDIT.md | 276 +++++++++++++++--- .../components/banner/banner-close.svelte | 14 +- src/uix/eidos/components/box/box.css | 12 + src/uix/eidos/components/box/box.svelte | 16 +- .../components/breadcrumb/breadcrumb.css | 6 +- .../eidos/components/calendar/calendar.css | 4 +- .../carousel/carousel-next-trigger.svelte | 9 +- .../carousel/carousel-prev-trigger.svelte | 9 +- .../eidos/components/checkbox/checkbox.css | 3 +- .../components/code-block/code-block.css | 4 +- src/uix/eidos/components/code-block/types.ts | 6 +- .../components/color-field/color-field.css | 15 +- .../components/color-picker/color-picker.css | 69 +++-- .../combobox/combobox-clear-button.svelte | 10 +- .../eidos/components/combobox/combobox.css | 11 +- src/uix/eidos/components/cropper/cropper.css | 4 +- .../eidos/components/cropper/cropper.svelte | 14 +- .../components/date-field/date-field.css | 15 +- .../date-range-picker/date-range-picker.css | 29 +- .../components/drawer/drawer-close.svelte | 60 ++-- src/uix/eidos/components/drawer/drawer.css | 47 +-- .../eidos/components/editable/editable.css | 11 +- .../field/field-control-trigger.css | 4 +- src/uix/eidos/components/field/field.css | 6 +- src/uix/eidos/components/field/field.svelte | 2 +- src/uix/eidos/components/field/types.ts | 2 +- .../components/file-upload/file-upload.css | 3 +- .../image-adjustments/image-adjustments.css | 4 +- .../components/image-picker/image-picker.css | 4 +- .../image-picker/image-picker.svelte | 2 +- .../eidos/components/image-picker/types.ts | 2 +- src/uix/eidos/components/kbd/types.ts | 6 +- src/uix/eidos/components/link/link.css | 4 +- .../menu-dial/menu-dial-action.svelte | 2 +- .../eidos/components/menu-dial/menu-dial.css | 108 +++---- .../components/menu-dial/menu-dial.svelte | 4 +- .../components/month-grid/month-grid.css | 4 +- .../components/onion-menu/onion-menu.svelte | 5 +- .../components/pagination/pagination.css | 3 +- .../password-field/password-field.css | 14 +- .../eidos/components/pin-input/pin-input.css | 4 +- .../components/radio-cards/radio-cards.css | 3 +- .../components/radio-group/radio-group.css | 3 +- .../range-calendar/range-calendar.css | 4 +- .../components/rating-group/rating-group.css | 3 +- .../search-field-clear-trigger.svelte | 10 +- .../components/search-field/search-field.css | 4 +- src/uix/eidos/components/select/select.css | 6 +- src/uix/eidos/components/slider/slider.css | 6 +- .../components/spin-field/spin-field.css | 7 +- src/uix/eidos/components/stepper/stepper.css | 9 +- src/uix/eidos/components/switch/switch.css | 7 +- .../table/table-row-detail-trigger.svelte | 9 +- .../eidos/components/tag-group/tag-group.css | 3 +- .../components/tags-input/tags-input.css | 10 +- .../eidos/components/textarea/textarea.css | 7 +- .../components/time-field/time-field.css | 15 +- .../components/time-picker/time-picker.css | 9 +- .../time-range-picker/time-range-picker.css | 9 +- src/uix/eidos/components/toolbar/toolbar.css | 3 +- .../tree-grid/tree-grid-expand-trigger.svelte | 9 +- .../eidos/components/year-grid/year-grid.css | 4 +- src/uix/eidos/lib/list-surface.css | 6 +- src/uix/langs/common.ts | 1 + src/uix/langs/components/cropper.ts | 16 + .../time-picker-day-period-toggle.svelte | 3 +- src/uix/soma/components/time-picker/langs.ts | 3 +- 67 files changed, 662 insertions(+), 334 deletions(-) diff --git a/src/uix/eidos/COMPONENT_COHERENCE_AUDIT.md b/src/uix/eidos/COMPONENT_COHERENCE_AUDIT.md index 7f1d19659..eb20e1e19 100644 --- a/src/uix/eidos/COMPONENT_COHERENCE_AUDIT.md +++ b/src/uix/eidos/COMPONENT_COHERENCE_AUDIT.md @@ -4,7 +4,7 @@ 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-22" (al final). Hechos: action components (fab/onion-menu/button-group/split-button), font 1:1 en menús, 2A radius bypass, gap canónico trigger→panel. CONTINUAR mañana por el bug popover color-picker (task_5d1a10ec). +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 @@ -265,30 +265,91 @@ El **valor** del foco es casi uniforme (`--color-focus-ring` = alias de | `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 (🔴) +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 ``, 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. @@ -347,16 +408,17 @@ familia, sin opt-out por campo. **Sin acción de código**: el estado actual ya 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 (🟡) +### 5B — Nits de API (🟡) — CERRADO 2026-06-24 (decisiones del usuario, ver hand-off) -- **`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". +- **`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) @@ -472,10 +534,10 @@ paridad visual (hover/focus/disabled) + a11y (foco/teclado) en selects migrados. | Componente | Faults | |---|---| -| **Combobox** | (a) flecha abrir/cerrar es semántica → **emitir evento** · (b) popup **sin padding inferior** · (c) scroll → usar **`ScrollArea`** · (d) **bug**: clic en flecha con popup abierto lo cierra y **reabre** · (e) tipografía md=sm (**no 1:1** — combobox no entró en el sweep de fuente) · (f) popup **pegado al input** (solapa) · (g) **elevación** no "un paso más que el disparador" (escalera canonizada `default` · (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 `` en Lote A), avatar-group conteo visible, date-range-picker year/month soma views ("Select year range"/"Previous decade"). Verificado en navegador (ES resuelve). | 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 @@ -515,9 +577,43 @@ Sesión larga; varios cierres + el gap canónico. Para retomar: doc en THEMING.md. ### Pendiente (orden sugerido) -1. **🔴 Bug popover color-picker** (task chip `task_5d1a10ec`) — EMPEZAR AQUÍ. - Paddings no canonizados + scroll horizontal + los sliders/area solapan el padding - del content (de ahí el scroll). Reportado por el usuario. +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. 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` @@ -531,6 +627,104 @@ Sesión larga; varios cierres + el gap canónico. Para retomar: --- +## 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` → 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 ``** 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 `` 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): closes dialog/drawer/popover/ + banner/toast → `IconButton`+`SvgX`; calendar/range prev/next → `IconButton`+ + `SvgChevron`; **code-block copy → componer ``** (cierra también su + hardcode de langs). Borrar el chrome `--*-control-*`/`--*-close-*` redundante. +2. **DIM 1 — Lote B** (estructural): 6 `` (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`. diff --git a/src/uix/eidos/components/banner/banner-close.svelte b/src/uix/eidos/components/banner/banner-close.svelte index 28008a727..0b39919e0 100644 --- a/src/uix/eidos/components/banner/banner-close.svelte +++ b/src/uix/eidos/components/banner/banner-close.svelte @@ -6,23 +6,29 @@ * event-target role, no keyboard). The consumer wires `onclick` to * their own visibility state — Banner does not own the dismissal. * - * Defaults `aria-label="Dismiss"` so consumers don't have to remember, - * but the consumer can override. + * Defaults to a localized "Dismiss" `aria-label` (via langs) so consumers + * don't have to remember, but the consumer can override. */ + import { ActiveEidos } from '$uix/eidos'; import type { BannerCloseProps } from './types'; let { - 'aria-label': ariaLabel = 'Dismiss', + 'aria-label': ariaLabel, class: className, children, ...restProps }: BannerCloseProps = $props(); + + const eidos = ActiveEidos.require(); + // Accessible name routes through langs — explicit prop wins, else the + // localized default. No hardcoded English. + const resolvedLabel = $derived(ariaLabel ?? eidos.langs.ts('#?common.buttons.dismiss|Dismiss')); + {:else} + + + + {/if} + {/snippet} diff --git a/src/uix/eidos/components/drawer/drawer.css b/src/uix/eidos/components/drawer/drawer.css index 71ae5e30f..e4d822fa5 100644 --- a/src/uix/eidos/components/drawer/drawer.css +++ b/src/uix/eidos/components/drawer/drawer.css @@ -442,47 +442,6 @@ body:has([data-drawer-content][data-dragging]) { } /* ── Close button ───────────────────────────────────────────────────────── */ - -[data-drawer-close] { - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - min-inline-size: var(--drawer-close-size); - block-size: var(--drawer-close-size); - padding-inline: var(--drawer-close-px); - border: var(--drawer-close-border-width) solid var(--drawer-close-border); - border-radius: var(--drawer-close-radius); - background: var(--drawer-close-bg); - color: var(--drawer-close-color); - cursor: pointer; - transition: - background var(--drawer-close-transition-duration) var(--drawer-close-transition-ease), - color var(--drawer-close-transition-duration) var(--drawer-close-transition-ease), - border-color var(--drawer-close-transition-duration) var(--drawer-close-transition-ease), - box-shadow var(--drawer-close-transition-duration) var(--drawer-close-transition-ease); -} - -[data-drawer-close][data-icon-only] { - inline-size: var(--drawer-close-size); - padding-inline: 0; -} - -[data-drawer-close]:hover { - background: var(--drawer-close-bg-hover); - color: var(--drawer-close-color-hover); -} - -[data-drawer-close]:focus-visible { - outline: none; - box-shadow: - 0 0 0 var(--drawer-close-focus-ring-offset) var(--drawer-content-bg), - 0 0 0 calc(var(--drawer-close-focus-ring-offset) + var(--drawer-close-focus-ring-width)) - var(--drawer-close-focus-ring-color); -} - -[data-drawer-close]:disabled, -[data-drawer-close][data-disabled] { - cursor: default; - opacity: var(--drawer-close-disabled-opacity); -} +/* Chrome (size · border · bg · hover · focus-ring · disabled) comes from the + composed `` / `