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 `