fix(eidos): unify input focus into a parameterised two-ring model

Each input drew its focus ring differently — the foundation [data-archetype]
:focus-visible ring landing on the <input>, ad-hoc per-component input rings
(css-field/number-field), the textarea's own color-mix — so editing showed a
DOUBLE frame (inner + outer), inconsistent across components.

Unify into one theme-parameterised two-ring model:
- New --focus-ring-inner-width (0 by default), folded into --focus-ring and every
  field *-focus-shadow: inner inset ring + outer ring. 0 = single outer frame;
  raise it in a theme to show the inner line everywhere at once.
- Foundation [data-archetype]:focus-visible now excludes form-internal elements
  (:not(input):not(textarea):not(select):not([data-archetype='segment'])) — their
  focus is shown by the wrapping control.
- Removed the ad-hoc input rings (css-field/number-field); textarea uses
  var(--focus-ring).
- Cleaned the now-dead per-data-color _accent-* cascade in css-field/number-field
  (recorded as backlog in THEMING.md §32).

Verified with real keyboard on field/css-field/number-field/date-field/textarea:
single outer frame. Also includes the earlier textarea `resize` fix (morfo
data-resize values + soma stops inlining resize + eidos materialises it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 5e98cc3eb3
commit ded673ad0f

@ -57,6 +57,10 @@
26. [Theme builder en runtime — `eidos.applyColorScheme`](#26-theme-builder-en-runtime--eidosapplycolorscheme-2026-06-04)
27. [Salida wide-gamut OKLCH (default-on)](#27-salida-wide-gamut-oklch-default-on-2026-06-04)
28. [Accesibilidad forced-colors + ramp de bordes](#28-accesibilidad-forced-colors--ramp-de-bordes-2026-06-05)
29. [Profundidad (depth) — canal unificado + eventful](#29-profundidad-depth--canal-unificado--eventful-2026-06-05)
30. [Forma (shape) — continuidad + familias + anidado](#30-forma-shape--continuidad--familias--anidado--eventful-2026-06-05)
31. [Estructura (espacio · densidad · escala)](#31-estructura-espacio--densidad--escala--el-espacio-como-ritmo-2026-06-05)
32. [Focus ring — modelo de dos anillos parametrizado](#32-focus-ring--modelo-de-dos-anillos-parametrizado-2026-06-11)
---
@ -2408,6 +2412,59 @@ compartido), capstone del cuarteto→quinteto de builders.
---
**Última revisión**: 2026-06-05. Si algo en este doc no coincide con
## 32. Focus ring — modelo de dos anillos parametrizado (2026-06-11)
El foco de los inputs estaba implementado **distinto en cada componente** (el anillo del
`[data-archetype]:focus-visible` del foundation sobre el `<input>`, anillos ad-hoc
`[data-x-input]:focus-visible`, el `color-mix` propio del textarea…). Resultado: un **doble
marco** al editar (anillo interior + exterior), inconsistente entre componentes.
**Solución — un único modelo de dos anillos, parametrizado a nivel de tema:**
- **Token nuevo**: `--focus-ring-inner-width` (= `0` por defecto). Definido en
`primitives/static.ts > STATIC_FOCUS_RING.innerWidth`, tipado en `FocusRingPrimitiveSet`
(`config-types.ts`), emitido en `render-css.ts`.
- El **anillo canónico** (`--focus-ring` del foundation **y** todos los `*-focus-shadow` de
los campos en `recipes/base.ts`) es ahora **dos anillos**:
```
inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), /* interior */
0 0 0 var(--focus-ring-offset) var(--color-surface-default), /* hueco */
0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color) /* exterior */
```
Con `inner-width: 0` el anillo interior es invisible → **un solo marco exterior**.
- El anillo del foundation `[data-archetype]:focus-visible` **excluye los elementos internos
de campo** (`:not(input):not(textarea):not(select):not([data-archetype='segment'])`): su
foco lo muestra el control que los envuelve (`archetypes.css`).
- Quitados los anillos interiores ad-hoc: `[data-css-field-input]:focus-visible`,
`[data-number-field-input]:focus-visible`; el textarea pasa a `box-shadow: var(--focus-ring)`.
**Para encender el anillo interior** en un tema: subir `--focus-ring-inner-width` > 0 →
aparece la segunda línea en todos los inputs a la vez, sin tocar componentes.
**Doctrina**: el foco es **un concepto de tema, no de componente**. Dos anillos definidos una
sola vez y parametrizados; los componentes no reinventan su anillo.
### Backlog — tokens retirados en la unificación
Al unificar, la cascada per-`data-color` `--_{css-field,number-field}-accent-*` quedó **sin
uso** (solo la consumía el anillo interior ad-hoc) y se retiró. Quedan registrados aquí por si
se quiere reintroducir que `css-field` / `number-field` tiñan su foco por `data-color` (como
hacen date/time/color-field con sus segmentos):
| Componente | Tokens retirados | Cascada |
| --- | --- | --- |
| `css-field` | `--_css-field-accent-border` · `--_css-field-accent-track` · `--_css-field-accent-text` | `[data-css-field][data-color='…']` × 8 (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) |
| `number-field` | `--_number-field-accent-border` · `--_number-field-accent-track` · `--_number-field-accent-text` | `[data-number-field][data-color='…']` × 8 |
Para reinstaurarlos: re-declarar el trío en el bloque base + la cascada `data-color`, y
consumir `accent-border` en el anillo del campo. **Hoy** ambos usan el `--focus-ring-color`
genérico (consistente con el resto), así que `data-color` no tiñe su foco — decisión
deliberada de la unificación.
---
**Última revisión**: 2026-06-11. Si algo en este doc no coincide con
el código, el código gana — pero abre un issue para que actualicemos
el doc.

@ -103,13 +103,23 @@
* on pills (Switch) and rounded buttons. The `0 0 0` sets no offset / blur,
* so the shadow draws a uniform ring at +offset from the element edge.
*/
[data-archetype]:focus-visible {
[data-archetype]:focus-visible:not(input):not(textarea):not(select):not([data-archetype='segment']) {
box-shadow:
0 0 0 var(--focus-ring-offset) var(--focus-ring-bg, transparent),
0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width))
var(--focus-ring-color);
}
/* Form-internal elements are ALWAYS wrapped in a field control that shows focus
on `:focus-within` (text inputs) or `:has(... :focus-visible)` (date/time/
colour segments). Letting them ALSO draw the foundation ring stacked a second
(inner) frame inside the control's ring — the "double border" when editing.
Excluded above:
- native form controls (input/textarea/select), and
- segments (`data-archetype='segment'`), which keep their own inset focus
highlight.
The control owns the focus affordance; these delegate to it. */
/* ── Item / Option ──────────────────────────────────────────────────────── */
[data-archetype='item'],
[data-archetype='option'] {

@ -3,9 +3,6 @@
--_css-field-px: var(--css-field-px-md);
--_css-field-gap: var(--css-field-gap-md);
--_css-field-font-size: var(--css-field-font-size-md);
--_css-field-accent-border: var(--color-primary-border);
--_css-field-accent-track: var(--color-primary-track);
--_css-field-accent-text: var(--color-primary-text);
display: inline-flex;
align-items: center;
@ -27,48 +24,6 @@
box-shadow var(--css-field-transition-duration) var(--css-field-transition-ease);
}
[data-css-field][data-color='secondary'] {
--_css-field-accent-border: var(--color-secondary-border);
--_css-field-accent-track: var(--color-secondary-track);
--_css-field-accent-text: var(--color-secondary-text);
}
[data-css-field][data-color='neutral'] {
--_css-field-accent-border: var(--color-neutral-border);
--_css-field-accent-track: var(--color-neutral-track);
--_css-field-accent-text: var(--color-neutral-text);
}
[data-css-field][data-color='affirm'] {
--_css-field-accent-border: var(--color-affirm-border);
--_css-field-accent-track: var(--color-affirm-track);
--_css-field-accent-text: var(--color-affirm-text);
}
[data-css-field][data-color='fulfill'] {
--_css-field-accent-border: var(--color-fulfill-border);
--_css-field-accent-track: var(--color-fulfill-track);
--_css-field-accent-text: var(--color-fulfill-text);
}
[data-css-field][data-color='risk'] {
--_css-field-accent-border: var(--color-risk-border);
--_css-field-accent-track: var(--color-risk-track);
--_css-field-accent-text: var(--color-risk-text);
}
[data-css-field][data-color='threat'] {
--_css-field-accent-border: var(--color-threat-border);
--_css-field-accent-track: var(--color-threat-track);
--_css-field-accent-text: var(--color-threat-text);
}
[data-css-field][data-color='loss'] {
--_css-field-accent-border: var(--color-loss-border);
--_css-field-accent-track: var(--color-loss-track);
--_css-field-accent-text: var(--color-loss-text);
}
[data-css-field][data-size='xs'] {
--_css-field-height: var(--css-field-height-xs);
--_css-field-px: var(--css-field-px-xs);
@ -136,12 +91,9 @@
cursor: not-allowed;
}
/* Explicit keyboard focus ring on the input itself. Ensures clear focus
feedback even before the Provider's [data-focused] attribute is applied. */
[data-css-field-input]:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--_css-field-accent-border);
}
/* Focus is shown once, by the root's canonical two-ring focus-shadow
(`[data-css-field][data-focused]`). No separate ring on the input — that
stacked a second (inner) frame inside the root ring. */
[data-css-field-increment-trigger],
[data-css-field-decrement-trigger],

@ -3,9 +3,6 @@
--_number-field-px: var(--number-field-px-md);
--_number-field-gap: var(--number-field-gap-md);
--_number-field-font-size: var(--number-field-font-size-md);
--_number-field-accent-border: var(--color-primary-border);
--_number-field-accent-track: var(--color-primary-track);
--_number-field-accent-text: var(--color-primary-text);
display: inline-flex;
align-items: center;
@ -27,48 +24,6 @@
box-shadow var(--number-field-transition-duration) var(--number-field-transition-ease);
}
[data-number-field][data-color='secondary'] {
--_number-field-accent-border: var(--color-secondary-border);
--_number-field-accent-track: var(--color-secondary-track);
--_number-field-accent-text: var(--color-secondary-text);
}
[data-number-field][data-color='neutral'] {
--_number-field-accent-border: var(--color-neutral-border);
--_number-field-accent-track: var(--color-neutral-track);
--_number-field-accent-text: var(--color-neutral-text);
}
[data-number-field][data-color='affirm'] {
--_number-field-accent-border: var(--color-affirm-border);
--_number-field-accent-track: var(--color-affirm-track);
--_number-field-accent-text: var(--color-affirm-text);
}
[data-number-field][data-color='fulfill'] {
--_number-field-accent-border: var(--color-fulfill-border);
--_number-field-accent-track: var(--color-fulfill-track);
--_number-field-accent-text: var(--color-fulfill-text);
}
[data-number-field][data-color='risk'] {
--_number-field-accent-border: var(--color-risk-border);
--_number-field-accent-track: var(--color-risk-track);
--_number-field-accent-text: var(--color-risk-text);
}
[data-number-field][data-color='threat'] {
--_number-field-accent-border: var(--color-threat-border);
--_number-field-accent-track: var(--color-threat-track);
--_number-field-accent-text: var(--color-threat-text);
}
[data-number-field][data-color='loss'] {
--_number-field-accent-border: var(--color-loss-border);
--_number-field-accent-track: var(--color-loss-track);
--_number-field-accent-text: var(--color-loss-text);
}
[data-number-field][data-size='xs'] {
--_number-field-height: var(--number-field-height-xs);
--_number-field-px: var(--number-field-px-xs);
@ -137,13 +92,9 @@
cursor: not-allowed;
}
/* Explicit keyboard focus ring on the input itself. Ensures clear focus
feedback even before the Provider's [data-focused] attribute is applied
(e.g. very first keydown, or when wrapping rules are overridden). */
[data-number-field-input]:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--_number-field-accent-border);
}
/* Focus is shown once, by the root's canonical two-ring focus-shadow
(`[data-number-field][data-focused]`). No separate ring on the input — that
stacked a second (inner) frame inside the root ring. */
[data-number-field-increment-trigger],
[data-number-field-decrement-trigger],

@ -19,6 +19,9 @@
--_textarea-placeholder: var(--color-content-muted);
--_textarea-count-color: var(--color-content-muted);
--_textarea-count-overflow: var(--color-threat-text);
/* CSS `resize` for the input. Driven by the morfo's `data-resize` enum
(see overrides below); inherits down to `[data-textarea-input]`. */
--_textarea-resize: vertical;
display: inline-flex;
flex-direction: column;
@ -76,11 +79,24 @@
--_textarea-border-focus: var(--color-loss-solid);
}
/* `resize` is owned by eidos (presentation), driven by the morfo's
`data-resize` enum on the provider. `vertical` is the base default. */
[data-textarea][data-resize='none'] {
--_textarea-resize: none;
}
[data-textarea][data-resize='horizontal'] {
--_textarea-resize: horizontal;
}
[data-textarea][data-resize='both'] {
--_textarea-resize: both;
}
/* The real input. */
[data-textarea-input] {
display: block;
inline-size: 100%;
min-inline-size: 0;
resize: var(--_textarea-resize);
padding-block: var(--_textarea-py);
padding-inline: var(--_textarea-px);
border: var(--border-width) solid var(--_textarea-border);
@ -102,8 +118,9 @@
[data-textarea][data-focused] [data-textarea-input] {
border-color: var(--_textarea-border-focus);
box-shadow: 0 0 0 var(--ring-width, 2px)
color-mix(in srgb, var(--_textarea-border-focus) 25%, transparent);
/* Canonical two-ring focus model (inner inset 0 + outer ring) — same as every
other input, so the textarea matches the rest. */
box-shadow: var(--focus-ring);
}
[data-textarea-input][data-invalid] {

@ -169,8 +169,9 @@
--border: var(--border-width) var(--border-style) var(--color-border-default);
--focus-ring-offset: 1px;
--focus-ring-width: 2px;
--focus-ring: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--focus-ring-error: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color-error);
--focus-ring-inner-width: 0px;
--focus-ring: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--focus-ring-error: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color-error), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color-error);
--container-width-sm: 448px;
--container-width-md: 688px;
--container-width-lg: 880px;
@ -768,7 +769,7 @@
--breadcrumb-ellipsis-bg-hover: var(--color-surface-raised);
--breadcrumb-ellipsis-color: var(--color-content-muted);
--breadcrumb-ellipsis-color-hover: var(--color-content-primary);
--breadcrumb-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--breadcrumb-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--breadcrumb-transition-duration: var(--duration-fast);
--breadcrumb-transition-ease: var(--ease-default);
--card-px-xs: var(--space-3);
@ -1038,7 +1039,7 @@
--field-control-bg-disabled: var(--color-surface-muted);
--field-control-color: var(--color-content-primary);
--field-control-placeholder-color: var(--color-content-muted);
--field-control-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--field-control-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--field-affix-color: var(--color-content-secondary);
--field-message-font-size: var(--font-size-xs);
--field-message-line-height: var(--leading-ui);
@ -1246,7 +1247,7 @@
--number-field-border-invalid: var(--color-risk-border);
--number-field-bg: var(--color-surface-default);
--number-field-color: var(--color-content-primary);
--number-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--number-field-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--number-field-control-radius: var(--radius-md);
--number-field-control-border-width: var(--border-width);
--number-field-control-border: transparent;
@ -1258,7 +1259,7 @@
--number-field-control-color: var(--color-content-secondary);
--number-field-control-color-hover: var(--color-content-primary);
--number-field-control-color-active: var(--color-primary-text);
--number-field-control-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--number-field-control-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--number-field-control-disabled-opacity: 0.48;
--number-field-transition-duration: var(--duration-fast);
--number-field-transition-ease: var(--ease-default);
@ -1292,7 +1293,7 @@
--css-field-border-invalid: var(--color-risk-border);
--css-field-bg: var(--color-surface-default);
--css-field-color: var(--color-content-primary);
--css-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--css-field-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--css-field-control-radius: var(--radius-md);
--css-field-control-border-width: var(--border-width);
--css-field-control-border: transparent;
@ -1304,7 +1305,7 @@
--css-field-control-color: var(--color-content-secondary);
--css-field-control-color-hover: var(--color-content-primary);
--css-field-control-color-active: var(--color-primary-text);
--css-field-control-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--css-field-control-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--css-field-control-disabled-opacity: 0.48;
--css-field-transition-duration: var(--duration-fast);
--css-field-transition-ease: var(--ease-default);
@ -1415,7 +1416,7 @@
--date-field-label-font-weight: var(--font-weight-medium);
--date-field-placeholder-color: var(--color-content-muted);
--date-field-literal-color: var(--color-content-muted);
--date-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--date-field-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--date-field-segment-min-width: 1.5ch;
--date-field-segment-height: calc(var(--_date-field-height) - var(--space-2));
--date-field-segment-radius: var(--radius-sm);
@ -1466,7 +1467,7 @@
--time-field-label-font-weight: var(--font-weight-medium);
--time-field-placeholder-color: var(--color-content-muted);
--time-field-literal-color: var(--color-content-muted);
--time-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--time-field-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--time-field-segment-min-width: 1.5ch;
--time-field-segment-height: calc(var(--_time-field-height) - var(--space-2));
--time-field-segment-radius: var(--radius-sm);
@ -1517,7 +1518,7 @@
--color-field-label-font-weight: var(--font-weight-medium);
--color-field-placeholder-color: var(--color-content-muted);
--color-field-literal-color: var(--color-content-muted);
--color-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--color-field-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--color-field-segment-min-width: 1.5ch;
--color-field-segment-height: calc(var(--_color-field-height) - var(--space-2));
--color-field-segment-radius: var(--radius-sm);
@ -1565,7 +1566,7 @@
--color-picker-trigger-swatch-size-lg: calc(var(--control-height-lg) - var(--space-2));
--color-picker-trigger-swatch-radius: var(--radius-full);
--color-picker-trigger-swatch-border: var(--color-border-subtle);
--color-picker-trigger-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--color-picker-trigger-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--color-picker-area-height: 11rem;
--color-picker-area-radius: var(--radius-md);
--color-picker-area-border-width: var(--border-width);
@ -1753,7 +1754,7 @@
--calendar-day-weekend-color: var(--color-content-secondary);
--calendar-day-holiday-shadow: inset 0 -2px 0 var(--_calendar-accent-border);
--calendar-day-unavailable-decoration: var(--color-risk-border);
--calendar-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--calendar-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--calendar-event-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--_calendar-accent-border);
--calendar-transition-duration: var(--duration-fast);
--calendar-transition-ease: var(--ease-default);
@ -1790,7 +1791,7 @@
--pagination-selected-bg-hover: var(--color-primary-solid-hover);
--pagination-selected-color: var(--color-content-on-solid);
--pagination-ellipsis-color: var(--color-content-muted);
--pagination-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--pagination-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--pagination-transition-duration: var(--duration-fast);
--pagination-transition-ease: var(--ease-default);
--pagination-disabled-opacity: 0.56;
@ -1959,7 +1960,7 @@
--rating-group-item-color-partial: var(--color-fulfill-solid);
--rating-group-item-hover-offset: -1px;
--rating-group-focus-radius: var(--radius-md);
--rating-group-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--rating-group-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--rating-group-transition-duration: var(--duration-fast);
--rating-group-transition-ease: var(--ease-default);
--rating-group-disabled-opacity: 0.56;
@ -1993,7 +1994,7 @@
--search-field-bg: var(--color-surface-default);
--search-field-color: var(--color-content-primary);
--search-field-placeholder-color: var(--color-content-muted);
--search-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--search-field-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--search-field-clear-hidden-opacity: 0;
--search-field-icon-size-xs: var(--icon-size-xs);
--search-field-icon-size-sm: var(--icon-size-sm);
@ -2044,7 +2045,7 @@
--password-field-bg: var(--color-surface-default);
--password-field-color: var(--color-content-primary);
--password-field-placeholder-color: var(--color-content-muted);
--password-field-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--password-field-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--password-field-trigger-radius: var(--radius-full);
--password-field-trigger-border-width: var(--border-width);
--password-field-trigger-border: transparent;
@ -2053,7 +2054,7 @@
--password-field-trigger-bg-hover: var(--color-surface-raised);
--password-field-trigger-color: var(--color-content-secondary);
--password-field-trigger-color-hover: var(--color-content-primary);
--password-field-trigger-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--password-field-trigger-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--password-field-icon-size-xs: var(--control-height-xs);
--password-field-icon-size-sm: var(--control-height-xs);
--password-field-icon-size-md: var(--control-height-sm);
@ -2120,7 +2121,7 @@
--select-trigger-indicator-color: var(--color-content-secondary);
--select-trigger-indicator-size: var(--icon-size-sm);
--select-placeholder-color: var(--color-content-muted);
--select-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--select-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--select-disabled-border: var(--color-border-subtle);
--select-disabled-bg: var(--color-surface-raised);
--select-disabled-color: var(--color-content-disabled);
@ -2236,7 +2237,7 @@
--toolbar-control-color: var(--color-content-secondary);
--toolbar-control-color-hover: var(--color-content-primary);
--toolbar-control-color-active: var(--color-primary-text);
--toolbar-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--toolbar-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--toolbar-separator-bg: var(--color-border-subtle);
--toolbar-separator-thickness: var(--border-width);
--toolbar-separator-length: var(--control-height-md);
@ -2271,7 +2272,7 @@
--file-upload-dropzone-radius: var(--radius-lg);
--file-upload-dropzone-bg: var(--color-surface-raised);
--file-upload-dropzone-color: var(--color-content-secondary);
--file-upload-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--file-upload-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--file-upload-button-gap: var(--space-1-5);
--file-upload-button-px: var(--space-3);
--file-upload-button-border-width: var(--border-width);
@ -2378,7 +2379,7 @@
--tag-group-item-shadow: var(--shadow-subtle);
--tag-group-item-color: var(--color-content-secondary);
--tag-group-item-color-hover: var(--color-content-primary);
--tag-group-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--tag-group-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--tag-group-solid-fg: var(--color-content-on-solid);
--tag-group-remove-size: var(--control-height-xs);
--tag-group-remove-radius: var(--radius-full);
@ -2476,7 +2477,7 @@
--editable-control-border-hover: var(--color-border-default);
--editable-control-bg: var(--color-surface-raised);
--editable-control-shadow: var(--shadow-subtle);
--editable-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--editable-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--editable-placeholder-color: var(--color-content-muted);
--editable-trigger-gap: var(--space-1);
--editable-trigger-px: var(--space-2-5);
@ -2518,7 +2519,7 @@
--words-font-size-sm: var(--font-size-xs);
--words-font-size-md: var(--font-size-sm);
--words-font-size-lg: var(--font-size-md);
--words-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--words-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--words-invalid-border: var(--color-risk-border);
--words-disabled-opacity: 0.5;
--words-font-size-xs: 0.6875rem;
@ -2633,7 +2634,7 @@
--tags-input-control-border-hover: var(--color-border-strong);
--tags-input-control-bg: var(--color-surface-raised);
--tags-input-control-shadow: var(--shadow-subtle);
--tags-input-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--tags-input-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--tags-input-input-min-inline-size: 8ch;
--tags-input-placeholder-color: var(--color-content-muted);
--tags-input-item-radius: var(--radius-full);
@ -2727,7 +2728,7 @@
--stepper-nav-bg: var(--color-surface-raised);
--stepper-nav-color: var(--color-content-primary);
--stepper-nav-hover-brightness: 1.04;
--stepper-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--stepper-focus-shadow: inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--stepper-transition-duration: var(--duration-fast);
--stepper-transition-ease: var(--ease-default);
--stepper-disabled-opacity: 0.45;

@ -219,6 +219,8 @@ export interface BorderPrimitiveSet {
export interface FocusRingPrimitiveSet {
readonly offset: string;
readonly width: string;
/** Inner (inset) ring width. `0` = single outer ring (default). */
readonly innerWidth: string;
}
export const SHADOW_PHYSICAL_KEYS = ['1', '2', '3', '4', '5', '6'] as const;

@ -66,7 +66,13 @@ export const STATIC_BORDER = {
export const STATIC_FOCUS_RING = {
offset: '1px',
width: '2px'
width: '2px',
/* Inner ring width. The focus affordance is TWO rings — an outer ring
(offset + width, around the element) and an inner `inset` ring. The inner
one is 0 by default (invisible); a theme can raise it to draw a second
line inside the field edge. Both are parameterised at theme level so every
input component shares ONE focus model. */
innerWidth: '0px'
} as const
export const STATIC_LAYOUT = {

@ -459,7 +459,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'ellipsis-color': 'var(--color-content-muted)',
'ellipsis-color-hover': 'var(--color-content-primary)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)'
},
@ -841,7 +841,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'control-color': 'var(--color-content-primary)',
'control-placeholder-color': 'var(--color-content-muted)',
'control-focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'affix-color': 'var(--color-content-secondary)',
'message-font-size': 'var(--font-size-xs)',
'message-line-height': 'var(--leading-ui)',
@ -1073,7 +1073,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
bg: 'var(--color-surface-default)',
color: 'var(--color-content-primary)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'transparent',
@ -1086,7 +1086,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'control-color-hover': 'var(--color-content-primary)',
'control-color-active': 'var(--color-primary-text)',
'control-focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'control-disabled-opacity': '0.48',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
@ -1123,7 +1123,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
bg: 'var(--color-surface-default)',
color: 'var(--color-content-primary)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'control-radius': 'var(--radius-md)',
'control-border-width': 'var(--border-width)',
'control-border': 'transparent',
@ -1136,7 +1136,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'control-color-hover': 'var(--color-content-primary)',
'control-color-active': 'var(--color-primary-text)',
'control-focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'control-disabled-opacity': '0.48',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
@ -1278,7 +1278,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'placeholder-color': 'var(--color-content-muted)',
'literal-color': 'var(--color-content-muted)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'segment-min-width': '1.5ch',
'segment-height': 'calc(var(--_date-field-height) - var(--space-2))',
'segment-radius': 'var(--radius-sm)',
@ -1332,7 +1332,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'placeholder-color': 'var(--color-content-muted)',
'literal-color': 'var(--color-content-muted)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'segment-min-width': '1.5ch',
'segment-height': 'calc(var(--_time-field-height) - var(--space-2))',
'segment-radius': 'var(--radius-sm)',
@ -1386,7 +1386,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'placeholder-color': 'var(--color-content-muted)',
'literal-color': 'var(--color-content-muted)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'segment-min-width': '1.5ch',
'segment-height': 'calc(var(--_color-field-height) - var(--space-2))',
'segment-radius': 'var(--radius-sm)',
@ -1440,7 +1440,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'trigger-swatch-radius': 'var(--radius-full)',
'trigger-swatch-border': 'var(--color-border-subtle)',
'trigger-focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
// 2D area
'area-height': '11rem',
@ -1746,7 +1746,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'day-holiday-shadow': 'inset 0 -2px 0 var(--_calendar-accent-border)',
'day-unavailable-decoration': 'var(--color-risk-border)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'event-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--_calendar-accent-border)',
'transition-duration': 'var(--duration-fast)',
@ -1787,7 +1787,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'selected-color': 'var(--color-content-on-solid)',
'ellipsis-color': 'var(--color-content-muted)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
@ -1993,7 +1993,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'item-hover-offset': '-1px',
'focus-radius': 'var(--radius-md)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.56'
@ -2030,7 +2030,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
color: 'var(--color-content-primary)',
'placeholder-color': 'var(--color-content-muted)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'clear-hidden-opacity': '0',
'icon-size-xs': 'var(--icon-size-xs)',
'icon-size-sm': 'var(--icon-size-sm)',
@ -2084,7 +2084,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
color: 'var(--color-content-primary)',
'placeholder-color': 'var(--color-content-muted)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'trigger-radius': 'var(--radius-full)',
'trigger-border-width': 'var(--border-width)',
'trigger-border': 'transparent',
@ -2094,7 +2094,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'trigger-color': 'var(--color-content-secondary)',
'trigger-color-hover': 'var(--color-content-primary)',
'trigger-focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'icon-size-xs': 'var(--control-height-xs)',
'icon-size-sm': 'var(--control-height-xs)',
'icon-size-md': 'var(--control-height-sm)',
@ -2168,7 +2168,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'trigger-indicator-size': 'var(--icon-size-sm)',
'placeholder-color': 'var(--color-content-muted)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'disabled-border': 'var(--color-border-subtle)',
'disabled-bg': 'var(--color-surface-raised)',
'disabled-color': 'var(--color-content-disabled)',
@ -2340,7 +2340,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'control-color-hover': 'var(--color-content-primary)',
'control-color-active': 'var(--color-primary-text)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'separator-bg': 'var(--color-border-subtle)',
'separator-thickness': 'var(--border-width)',
'separator-length': 'var(--control-height-md)',
@ -2378,7 +2378,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'dropzone-bg': 'var(--color-surface-raised)',
'dropzone-color': 'var(--color-content-secondary)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'button-gap': 'var(--space-1-5)',
'button-px': 'var(--space-3)',
'button-border-width': 'var(--border-width)',
@ -2550,7 +2550,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'item-color': 'var(--color-content-secondary)',
'item-color-hover': 'var(--color-content-primary)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'solid-fg': 'var(--color-content-on-solid)',
'remove-size': 'var(--control-height-xs)',
'remove-radius': 'var(--radius-full)',
@ -2740,7 +2740,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'control-bg': 'var(--color-surface-raised)',
'control-shadow': 'var(--shadow-subtle)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'placeholder-color': 'var(--color-content-muted)',
'trigger-gap': 'var(--space-1)',
'trigger-px': 'var(--space-2-5)',
@ -2821,7 +2821,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'font-size-md': 'var(--font-size-sm)',
'font-size-lg': 'var(--font-size-md)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'invalid-border': 'var(--color-risk-border)',
'disabled-opacity': '0.5',
// Smallest doc-chrome size (breadcrumb, cell labels) — one step under `sm`.
@ -2961,7 +2961,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'control-bg': 'var(--color-surface-raised)',
'control-shadow': 'var(--shadow-subtle)',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'input-min-inline-size': '8ch',
'placeholder-color': 'var(--color-content-muted)',
'item-radius': 'var(--radius-full)',
@ -3098,7 +3098,7 @@ export const THEME_BASE_RECIPE_TOKENS = {
'nav-color': 'var(--color-content-primary)',
'nav-hover-brightness': '1.04',
'focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',
'disabled-opacity': '0.45',

@ -163,10 +163,15 @@ export function renderStaticCss(options: EidosConfig): string {
if (primitives.focusRing) {
declarations.push(cssVar('focus-ring-offset', primitives.focusRing.offset))
declarations.push(cssVar('focus-ring-width', primitives.focusRing.width))
declarations.push(cssVar('focus-ring-inner-width', primitives.focusRing.innerWidth))
// Two-ring focus model: inner `inset` ring (width from token, 0 by
// default → invisible) + the outer ring. Every input component shares
// this so the focus affordance is uniform and theme-parameterised.
declarations.push(
cssVar(
'focus-ring',
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), ' +
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color), ' +
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), ' +
'0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) ' +
'var(--focus-ring-color)'
)
@ -174,7 +179,8 @@ export function renderStaticCss(options: EidosConfig): string {
declarations.push(
cssVar(
'focus-ring-error',
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), ' +
'inset 0 0 0 var(--focus-ring-inner-width) var(--focus-ring-color-error), ' +
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), ' +
'0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) ' +
'var(--focus-ring-color-error)'
)

@ -81,7 +81,15 @@ export const textareaMorfo = {
{ attr: 'data-focused', value: v.propRef('focused'), severity: 'optional' },
{ attr: 'data-empty', value: v.propRef('empty'), severity: 'optional' },
{ attr: 'data-autosize', value: v.propRef('autosize'), severity: 'optional' },
{ attr: 'data-resize', value: v.propRef('resize'), severity: 'optional' }
{
// Enum-valued: eidos selects `[data-resize='none']` to set the CSS
// `resize`. Without `values` the compiler treats this as a presence
// flag and emits `data-resize=""`, so the value never reaches eidos.
attr: 'data-resize',
values: ['none', 'vertical', 'horizontal', 'both'],
value: v.propRef('resize'),
severity: 'optional'
}
],
aria: []
},

@ -32,6 +32,7 @@ Multi-line text input with optional autosize, character counter, and submit-on-s
| `autosize` | `boolean` | `false` | Resize height to content between `minRows` and `maxRows`. |
| `minRows` | `number` | `2` | Floor for autosize height. |
| `maxRows` | `number` | — | Ceiling for autosize height. `undefined` = unbounded. |
| `resize` | `'none' \| 'vertical' \| 'horizontal' \| 'both'` | `'vertical'` | CSS resize handle. Forced to `none` under `autosize`. Materialised by eidos from `data-resize`. |
| `minLength` | `number` | — | Forwarded to the textarea. |
| `maxLength` | `number` | — | Clamps the value AND surfaces `signal-warn-count-overflow` events. |
| `disabled` | `boolean` | `false` | OR-merged with `Field.disabled`. |
@ -61,6 +62,44 @@ Multi-line text input with optional autosize, character counter, and submit-on-s
When `autosize` is enabled, the provider's `measureAutosize()` computes the natural content height (using `scrollHeight` + computed line-height + chrome) and writes it to the textarea via `this.soma.dom.apply({ target, attrs: { style: 'height: ...' } })`. All DOM writes route through `ActiveDom` to preserve the soma "no direct mutable DOM writes" contract.
## Backlog · correcciones
### 2026-06-11 — `resize` no se aplicaba (fix por capas)
**Síntoma**: `resize="none"` (y el resto de valores) no llegaban al `<textarea>`; el
navegador mostraba siempre `resize: both`.
**Causa raíz** (dos fallos encadenados):
1. **morfo** — `data-resize` se declaraba con `v.propRef('resize')` sin `values`, así que
el compilador (`pickDataMode`) lo emitía como *flag de presencia* (`data-resize=""`) en
vez del valor (`data-resize="none"`). El contrato quedaba inerte para eidos.
2. **soma** — el `resize` se inyectaba como inline-style en el `Input`, pero
`measureAutosize()` reescribe el atributo `style` entero con la altura, machacando el
`resize` inline.
**Corrección** (respetando las capas, sin tocar el DOM):
- **morfo**: `data-resize` declara `values: ['none','vertical','horizontal','both']` → emite
el valor real.
- **soma**: deja de inyectar `resize` en el inline-style; el runtime sigue proyectando
`data-resize` desde `resolvedResize`.
- **eidos**: materializa el `resize` desde `data-resize` con el token `--_textarea-resize`
(`[data-textarea-input] { resize: var(--_textarea-resize) }`).
**Verificado**: autosize on → `data-resize="none"` + `resize:none`; autosize off → cada
valor (`none`/`vertical`/`horizontal`/`both`) computa correctamente, sin inline-style en el
input.
### Pendiente — doble-escritura del `style` en autosize
`measureAutosize()` escribe la altura con `dom.apply({ attrs: { style: 'height: …' } })`, que
**reemplaza el atributo `style` completo**. Hoy esto descarta el `overflow: auto` que el
provider declara para el modo `autosize` + `maxRows` (el scroll interno no aparece al llegar
al tope). Cerrarlo: mover ese `overflow` a eidos (`[data-textarea][data-autosize]`) o escribir
la altura como propiedad individual del `style` en vez de reemplazar el atributo entero. No
bloqueante; deuda heredada, ortogonal al fix de `resize`.
## See also
- Eidos visual: `src/uix/eidos/components/textarea/README.md`

@ -343,11 +343,14 @@ export class TextAreaInputProvider {
readonly props = $derived.by(() => {
const p = this.provider;
// `resize` is presentation: eidos materialises it from the morfo's
// `data-resize` attr (projected by the runtime). It must NOT be inlined
// here — `measureAutosize` rewrites the whole `style` attr with the
// height, which would clobber an inline `resize`.
const heightStyle =
p.opts.autosize.current && p.autosizeHeight !== undefined
? { height: `${p.autosizeHeight}px`, overflow: 'auto' }
: undefined;
const resizeStyle = { resize: p.resolvedResize };
return this.runtimePart.assert({
...this.runtimePart.props,
@ -364,7 +367,7 @@ export class TextAreaInputProvider {
'data-disabled': boolToEmptyStrOrUndef(p.isDisabled),
'data-readonly': boolToEmptyStrOrUndef(p.isReadonly),
'data-invalid': boolToEmptyStrOrUndef(p.isInvalid),
style: { ...resizeStyle, ...heightStyle },
style: heightStyle,
oninput: this.handleInput,
onfocus: this.handleFocus,
onblur: this.handleBlur,

Loading…
Cancel
Save

Powered by TurnKey Linux.