diff --git a/continue.md b/continue.md
index 627612904..82e4b0f8d 100644
--- a/continue.md
+++ b/continue.md
@@ -81,9 +81,22 @@ Actualizacion Eidos component migration 2026-05-17:
- `npm run check` -> 0 errores, 0 warnings.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
hotspots.
+- Sexta tanda del mismo bloque:
+ - `NumberField`: root visual `number-field.svelte`, partes `Input`,
+ `IncrementTrigger`, `DecrementTrigger`, `Scrubber`, `number-field.css`,
+ `types.ts` e `index.ts`.
+ - El wrapper conserva formato/parseo/spinbutton/scrub de Soma y solo anade
+ `size`.
+ - La receta cubre contenedor, input, triggers, scrubber, focused, scrubbing,
+ invalid y disabled.
+- Validado despues de `NumberField`:
+ - `npx vitest run src/uix/eidos` -> 7 archivos, 84 tests OK.
+ - `npm run check` -> 0 errores, 0 warnings.
+ - `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
+ hotspots.
- Siguiente bloque sugerido:
- Migrar otro grupo de bajo riesgo desde Soma a Eidos: candidatos reales por
- morfo/soma existentes son `number-field`, `field` o `toolbar`.
+ morfo/soma existentes son `field`, `toolbar`, `breadcrumb` o `tag-group`.
- Antes de tocar componentes grandes (`select`, `combobox`, `calendar`,
`date-field`) conviene fijar una tabla por componente: partes Soma,
props visuales Eidos, recipe tokens y attrs esperados.
diff --git a/src/uix/eidos/component-visual-attrs.test.ts b/src/uix/eidos/component-visual-attrs.test.ts
index 982d48c74..7605aae4e 100644
--- a/src/uix/eidos/component-visual-attrs.test.ts
+++ b/src/uix/eidos/component-visual-attrs.test.ts
@@ -17,6 +17,7 @@ const VISUAL_ATTRS = [
['drawer/drawer-close.svelte', ['data-icon-only']],
['drawer/drawer-content.svelte', ['data-size']],
['meter/meter.svelte', ['data-size']],
+ ['number-field/number-field.svelte', ['data-size']],
['pagination/pagination.svelte', ['data-size']],
['popover/popover-close.svelte', ['data-icon-only']],
['popover/popover-content.svelte', ['data-size']],
diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md
index 00c4c05a1..459d334b4 100644
--- a/src/uix/eidos/components/README.md
+++ b/src/uix/eidos/components/README.md
@@ -355,6 +355,7 @@ su subdirectorio.
| avatar | ✅ multi-part | Image, Fallback (eidos-native) |
| icon | ✅ single-part | + 1697 lucide glyphs |
| meter | ✅ multi-part | Indicator |
+| number-field | ✅ multi-part | Input, IncrementTrigger, DecrementTrigger, Scrubber |
| pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis |
| progress | ✅ multi-part | Indicator |
| rating-group | ✅ multi-part | Item |
diff --git a/src/uix/eidos/components/number-field/index.ts b/src/uix/eidos/components/number-field/index.ts
new file mode 100644
index 000000000..f555fd86b
--- /dev/null
+++ b/src/uix/eidos/components/number-field/index.ts
@@ -0,0 +1,41 @@
+// NumberField — eidos compound API, disciplined option C.
+//
+// import { NumberField } from '$uix/eidos/components/number-field';
+//
+//
+// -
+//
+// +
+//
+import NumberFieldRoot from './number-field.svelte';
+import Input from './number-field-input.svelte';
+import IncrementTrigger from './number-field-increment-trigger.svelte';
+import DecrementTrigger from './number-field-decrement-trigger.svelte';
+import Scrubber from './number-field-scrubber.svelte';
+
+type NumberFieldNamespace = typeof NumberFieldRoot & {
+ Input: typeof Input;
+ IncrementTrigger: typeof IncrementTrigger;
+ DecrementTrigger: typeof DecrementTrigger;
+ Scrubber: typeof Scrubber;
+};
+
+const NumberField = NumberFieldRoot as NumberFieldNamespace;
+NumberField.Input = Input;
+NumberField.IncrementTrigger = IncrementTrigger;
+NumberField.DecrementTrigger = DecrementTrigger;
+NumberField.Scrubber = Scrubber;
+
+export { NumberField };
+
+export default NumberField;
+
+export type {
+ NumberFieldFormatOptions,
+ NumberFieldProps,
+ NumberFieldInputProps as InputProps,
+ NumberFieldIncrementTriggerProps as IncrementTriggerProps,
+ NumberFieldDecrementTriggerProps as DecrementTriggerProps,
+ NumberFieldScrubberProps as ScrubberProps,
+ NumberFieldSize
+} from './types';
diff --git a/src/uix/eidos/components/number-field/number-field-decrement-trigger.svelte b/src/uix/eidos/components/number-field/number-field-decrement-trigger.svelte
new file mode 100644
index 000000000..a66e266d4
--- /dev/null
+++ b/src/uix/eidos/components/number-field/number-field-decrement-trigger.svelte
@@ -0,0 +1,10 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/number-field/number-field-increment-trigger.svelte b/src/uix/eidos/components/number-field/number-field-increment-trigger.svelte
new file mode 100644
index 000000000..2757c0577
--- /dev/null
+++ b/src/uix/eidos/components/number-field/number-field-increment-trigger.svelte
@@ -0,0 +1,10 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/number-field/number-field-input.svelte b/src/uix/eidos/components/number-field/number-field-input.svelte
new file mode 100644
index 000000000..6fae9acca
--- /dev/null
+++ b/src/uix/eidos/components/number-field/number-field-input.svelte
@@ -0,0 +1,10 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/number-field/number-field-scrubber.svelte b/src/uix/eidos/components/number-field/number-field-scrubber.svelte
new file mode 100644
index 000000000..74f8b731f
--- /dev/null
+++ b/src/uix/eidos/components/number-field/number-field-scrubber.svelte
@@ -0,0 +1,10 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/number-field/number-field.css b/src/uix/eidos/components/number-field/number-field.css
new file mode 100644
index 000000000..84af0e1b2
--- /dev/null
+++ b/src/uix/eidos/components/number-field/number-field.css
@@ -0,0 +1,134 @@
+[data-number-field] {
+ --_number-field-height: var(--number-field-height-md);
+ --_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);
+
+ display: inline-flex;
+ align-items: center;
+ gap: var(--_number-field-gap);
+ inline-size: 100%;
+ min-inline-size: 0;
+ block-size: var(--_number-field-height);
+ padding-inline: var(--_number-field-px);
+ border: var(--number-field-border-width) solid var(--number-field-border);
+ border-radius: var(--number-field-radius);
+ background: var(--number-field-bg);
+ color: var(--number-field-color);
+ font-family: var(--number-field-font-family);
+ font-size: var(--_number-field-font-size);
+ line-height: var(--number-field-line-height);
+ transition:
+ background var(--number-field-transition-duration) var(--number-field-transition-ease),
+ border-color var(--number-field-transition-duration) var(--number-field-transition-ease),
+ box-shadow var(--number-field-transition-duration) var(--number-field-transition-ease);
+}
+
+[data-number-field][data-size='sm'] {
+ --_number-field-height: var(--number-field-height-sm);
+ --_number-field-px: var(--number-field-px-sm);
+ --_number-field-gap: var(--number-field-gap-sm);
+ --_number-field-font-size: var(--number-field-font-size-sm);
+}
+
+[data-number-field][data-size='lg'] {
+ --_number-field-height: var(--number-field-height-lg);
+ --_number-field-px: var(--number-field-px-lg);
+ --_number-field-gap: var(--number-field-gap-lg);
+ --_number-field-font-size: var(--number-field-font-size-lg);
+}
+
+[data-number-field][data-focused],
+[data-number-field][data-scrubbing] {
+ border-color: var(--number-field-border-focus);
+ box-shadow: var(--number-field-focus-shadow);
+}
+
+[data-number-field][data-invalid] {
+ border-color: var(--number-field-border-invalid);
+}
+
+[data-number-field][data-disabled] {
+ opacity: var(--number-field-disabled-opacity);
+}
+
+[data-number-field-input] {
+ flex: 1 1 auto;
+ min-inline-size: 0;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ outline: none;
+}
+
+[data-number-field-input][data-disabled] {
+ cursor: not-allowed;
+}
+
+[data-number-field-increment-trigger],
+[data-number-field-decrement-trigger],
+[data-number-field-scrubber] {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ inline-size: var(--number-field-control-size);
+ block-size: var(--number-field-control-size);
+ border: var(--number-field-control-border-width) solid var(--number-field-control-border);
+ border-radius: var(--number-field-control-radius);
+ background: var(--number-field-control-bg);
+ color: var(--number-field-control-color);
+ font: inherit;
+ outline: none;
+ transition:
+ background var(--number-field-transition-duration) var(--number-field-transition-ease),
+ border-color var(--number-field-transition-duration) var(--number-field-transition-ease),
+ color var(--number-field-transition-duration) var(--number-field-transition-ease),
+ box-shadow var(--number-field-transition-duration) var(--number-field-transition-ease);
+}
+
+[data-number-field-increment-trigger],
+[data-number-field-decrement-trigger] {
+ cursor: pointer;
+}
+
+[data-number-field-scrubber] {
+ cursor: ew-resize;
+}
+
+[data-number-field-increment-trigger]:hover:not([data-disabled]),
+[data-number-field-decrement-trigger]:hover:not([data-disabled]),
+[data-number-field-scrubber]:hover:not([data-disabled]) {
+ border-color: var(--number-field-control-border-hover);
+ background: var(--number-field-control-bg-hover);
+ color: var(--number-field-control-color-hover);
+}
+
+[data-number-field-increment-trigger]:focus-visible,
+[data-number-field-decrement-trigger]:focus-visible,
+[data-number-field-scrubber]:focus-visible {
+ box-shadow: var(--number-field-control-focus-shadow);
+}
+
+[data-number-field-increment-trigger][data-disabled],
+[data-number-field-decrement-trigger][data-disabled],
+[data-number-field-scrubber][data-disabled] {
+ cursor: default;
+ opacity: var(--number-field-control-disabled-opacity);
+}
+
+[data-number-field-scrubber][data-scrubbing] {
+ border-color: var(--number-field-control-border-active);
+ background: var(--number-field-control-bg-active);
+ color: var(--number-field-control-color-active);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ [data-number-field],
+ [data-number-field-increment-trigger],
+ [data-number-field-decrement-trigger],
+ [data-number-field-scrubber] {
+ transition: none;
+ }
+}
diff --git a/src/uix/eidos/components/number-field/number-field.svelte b/src/uix/eidos/components/number-field/number-field.svelte
new file mode 100644
index 000000000..5625292ac
--- /dev/null
+++ b/src/uix/eidos/components/number-field/number-field.svelte
@@ -0,0 +1,18 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/number-field/types.ts b/src/uix/eidos/components/number-field/types.ts
new file mode 100644
index 000000000..ef1824fe6
--- /dev/null
+++ b/src/uix/eidos/components/number-field/types.ts
@@ -0,0 +1,28 @@
+import type {
+ DecrementTriggerProps as SomaNumberFieldDecrementTriggerProps,
+ IncrementTriggerProps as SomaNumberFieldIncrementTriggerProps,
+ InputProps as SomaNumberFieldInputProps,
+ NumberFieldFormatOptions,
+ ProviderProps as SomaNumberFieldProviderProps,
+ ScrubberProps as SomaNumberFieldScrubberProps
+} from '$soma/components/number-field';
+import type { ResponsiveProp, Size } from '$uix/eidos/lib/types';
+
+export type NumberFieldSize = Extract;
+
+/**
+ * Props for the eidos ``.
+ *
+ * Extends Soma's spinbutton with visual sizing only. Formatting, parsing,
+ * scrubbing, keyboard interaction and field integration are owned by Soma.
+ */
+export type NumberFieldProps = SomaNumberFieldProviderProps & {
+ /** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */
+ size?: ResponsiveProp;
+};
+
+export type NumberFieldInputProps = SomaNumberFieldInputProps;
+export type NumberFieldIncrementTriggerProps = SomaNumberFieldIncrementTriggerProps;
+export type NumberFieldDecrementTriggerProps = SomaNumberFieldDecrementTriggerProps;
+export type NumberFieldScrubberProps = SomaNumberFieldScrubberProps;
+export type { NumberFieldFormatOptions };
diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css
index e80cb2df8..8d49d9fab 100644
--- a/src/uix/eidos/generated/base.css
+++ b/src/uix/eidos/generated/base.css
@@ -602,6 +602,45 @@
--meter-indicator-bg-above: var(--color-threat-solid);
--meter-transition-duration: var(--duration-normal);
--meter-transition-ease: var(--ease-default);
+ --number-field-height-sm: var(--control-height-sm);
+ --number-field-height-md: var(--control-height-md);
+ --number-field-height-lg: var(--control-height-lg);
+ --number-field-px-sm: var(--space-2-5);
+ --number-field-px-md: var(--space-3);
+ --number-field-px-lg: var(--space-3-5);
+ --number-field-gap-sm: var(--space-1);
+ --number-field-gap-md: var(--space-1-5);
+ --number-field-gap-lg: var(--space-2);
+ --number-field-font-family: var(--font-ui);
+ --number-field-font-size-sm: var(--font-size-xs);
+ --number-field-font-size-md: var(--font-size-sm);
+ --number-field-font-size-lg: var(--font-size-sm);
+ --number-field-line-height: var(--leading-ui);
+ --number-field-radius: var(--radius-md);
+ --number-field-border-width: var(--border-width);
+ --number-field-border: var(--color-border-default);
+ --number-field-border-focus: var(--color-focus-ring);
+ --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-control-size: var(--control-height-xs);
+ --number-field-control-radius: var(--radius-md);
+ --number-field-control-border-width: var(--border-width);
+ --number-field-control-border: transparent;
+ --number-field-control-border-hover: var(--color-border-subtle);
+ --number-field-control-border-active: var(--color-primary-border);
+ --number-field-control-bg: transparent;
+ --number-field-control-bg-hover: var(--color-surface-raised);
+ --number-field-control-bg-active: var(--color-primary-track);
+ --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-disabled-opacity: 0.48;
+ --number-field-transition-duration: var(--duration-fast);
+ --number-field-transition-ease: var(--ease-default);
+ --number-field-disabled-opacity: 0.56;
--pagination-gap-sm: var(--space-1);
--pagination-gap-md: var(--space-1-5);
--pagination-gap-lg: var(--space-2);
diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css
index fc05f061a..49fb518ac 100644
--- a/src/uix/eidos/index.css
+++ b/src/uix/eidos/index.css
@@ -69,6 +69,7 @@
@import './components/tooltip/tooltip.css';
@import './components/progress/progress.css';
@import './components/meter/meter.css';
+@import './components/number-field/number-field.css';
@import './components/pagination/pagination.css';
@import './components/toggle/toggle.css';
@import './components/switch/switch.css';
diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts
index 8ef4b0fe2..8880a8c6e 100644
--- a/src/uix/eidos/lib/recipes/base.ts
+++ b/src/uix/eidos/lib/recipes/base.ts
@@ -347,6 +347,49 @@ export const THEME_BASE_RECIPE_TOKENS = {
'transition-duration': 'var(--duration-normal)',
'transition-ease': 'var(--ease-default)'
},
+ 'number-field': {
+ 'height-sm': 'var(--control-height-sm)',
+ 'height-md': 'var(--control-height-md)',
+ 'height-lg': 'var(--control-height-lg)',
+ 'px-sm': 'var(--space-2-5)',
+ 'px-md': 'var(--space-3)',
+ 'px-lg': 'var(--space-3-5)',
+ 'gap-sm': 'var(--space-1)',
+ 'gap-md': 'var(--space-1-5)',
+ 'gap-lg': 'var(--space-2)',
+ 'font-family': 'var(--font-ui)',
+ 'font-size-sm': 'var(--font-size-xs)',
+ 'font-size-md': 'var(--font-size-sm)',
+ 'font-size-lg': 'var(--font-size-sm)',
+ 'line-height': 'var(--leading-ui)',
+ 'radius': 'var(--radius-md)',
+ 'border-width': 'var(--border-width)',
+ 'border': 'var(--color-border-default)',
+ 'border-focus': 'var(--color-focus-ring)',
+ 'border-invalid': 'var(--color-risk-border)',
+ 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)',
+ 'control-size': 'var(--control-height-xs)',
+ 'control-radius': 'var(--radius-md)',
+ 'control-border-width': 'var(--border-width)',
+ 'control-border': 'transparent',
+ 'control-border-hover': 'var(--color-border-subtle)',
+ 'control-border-active': 'var(--color-primary-border)',
+ 'control-bg': 'transparent',
+ 'control-bg-hover': 'var(--color-surface-raised)',
+ 'control-bg-active': 'var(--color-primary-track)',
+ 'control-color': 'var(--color-content-secondary)',
+ '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)',
+ 'control-disabled-opacity': '0.48',
+ 'transition-duration': 'var(--duration-fast)',
+ 'transition-ease': 'var(--ease-default)',
+ 'disabled-opacity': '0.56'
+ },
pagination: {
'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)',