diff --git a/src/uix/eidos/THEMING.md b/src/uix/eidos/THEMING.md
index 3c2b07b45..40a05d2ba 100644
--- a/src/uix/eidos/THEMING.md
+++ b/src/uix/eidos/THEMING.md
@@ -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 ``, 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.
diff --git a/src/uix/eidos/archetypes.css b/src/uix/eidos/archetypes.css
index 99f439dfa..21e6f5c23 100644
--- a/src/uix/eidos/archetypes.css
+++ b/src/uix/eidos/archetypes.css
@@ -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'] {
diff --git a/src/uix/eidos/components/css-field/css-field.css b/src/uix/eidos/components/css-field/css-field.css
index f5548c732..53ec2d073 100644
--- a/src/uix/eidos/components/css-field/css-field.css
+++ b/src/uix/eidos/components/css-field/css-field.css
@@ -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],
diff --git a/src/uix/eidos/components/number-field/number-field.css b/src/uix/eidos/components/number-field/number-field.css
index 27daf5fb1..c5eb1f2e6 100644
--- a/src/uix/eidos/components/number-field/number-field.css
+++ b/src/uix/eidos/components/number-field/number-field.css
@@ -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],
diff --git a/src/uix/eidos/components/textarea/textarea.css b/src/uix/eidos/components/textarea/textarea.css
index cd8ca494d..fce4a58df 100644
--- a/src/uix/eidos/components/textarea/textarea.css
+++ b/src/uix/eidos/components/textarea/textarea.css
@@ -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] {
diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css
index c65482628..0defa0e4c 100644
--- a/src/uix/eidos/generated/base.css
+++ b/src/uix/eidos/generated/base.css
@@ -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;
diff --git a/src/uix/eidos/lib/config-types.ts b/src/uix/eidos/lib/config-types.ts
index e564b5cb2..5f4402b58 100644
--- a/src/uix/eidos/lib/config-types.ts
+++ b/src/uix/eidos/lib/config-types.ts
@@ -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;
diff --git a/src/uix/eidos/lib/primitives/static.ts b/src/uix/eidos/lib/primitives/static.ts
index 0badae3b5..85bf24e02 100644
--- a/src/uix/eidos/lib/primitives/static.ts
+++ b/src/uix/eidos/lib/primitives/static.ts
@@ -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 = {
diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts
index 94c29962a..4cb05e6d5 100644
--- a/src/uix/eidos/lib/recipes/base.ts
+++ b/src/uix/eidos/lib/recipes/base.ts
@@ -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',
diff --git a/src/uix/eidos/lib/render-css.ts b/src/uix/eidos/lib/render-css.ts
index f847de1b3..d43684875 100644
--- a/src/uix/eidos/lib/render-css.ts
+++ b/src/uix/eidos/lib/render-css.ts
@@ -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)'
)
diff --git a/src/uix/morfo/components/textarea.ts b/src/uix/morfo/components/textarea.ts
index 134d66d83..bb59d78d1 100644
--- a/src/uix/morfo/components/textarea.ts
+++ b/src/uix/morfo/components/textarea.ts
@@ -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: []
},
diff --git a/src/uix/soma/components/textarea/README.md b/src/uix/soma/components/textarea/README.md
index b14928082..833346eec 100644
--- a/src/uix/soma/components/textarea/README.md
+++ b/src/uix/soma/components/textarea/README.md
@@ -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 `