From 4dc09c2897e32047f989403f78e14a8fa94e3013 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 17 May 2026 02:14:31 +0200 Subject: [PATCH] Add Eidos number field wrapper --- continue.md | 15 +- src/uix/eidos/component-visual-attrs.test.ts | 1 + src/uix/eidos/components/README.md | 1 + .../eidos/components/number-field/index.ts | 41 ++++++ .../number-field-decrement-trigger.svelte | 10 ++ .../number-field-increment-trigger.svelte | 10 ++ .../number-field/number-field-input.svelte | 10 ++ .../number-field/number-field-scrubber.svelte | 10 ++ .../components/number-field/number-field.css | 134 ++++++++++++++++++ .../number-field/number-field.svelte | 18 +++ .../eidos/components/number-field/types.ts | 28 ++++ src/uix/eidos/generated/base.css | 39 +++++ src/uix/eidos/index.css | 1 + src/uix/eidos/lib/recipes/base.ts | 43 ++++++ 14 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 src/uix/eidos/components/number-field/index.ts create mode 100644 src/uix/eidos/components/number-field/number-field-decrement-trigger.svelte create mode 100644 src/uix/eidos/components/number-field/number-field-increment-trigger.svelte create mode 100644 src/uix/eidos/components/number-field/number-field-input.svelte create mode 100644 src/uix/eidos/components/number-field/number-field-scrubber.svelte create mode 100644 src/uix/eidos/components/number-field/number-field.css create mode 100644 src/uix/eidos/components/number-field/number-field.svelte create mode 100644 src/uix/eidos/components/number-field/types.ts 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)',