Add Eidos number field wrapper

active-uix
dev 5 months ago
parent 3c60d33a6c
commit 4dc09c2897

@ -81,9 +81,22 @@ Actualizacion Eidos component migration 2026-05-17:
- `npm run check` -> 0 errores, 0 warnings. - `npm run check` -> 0 errores, 0 warnings.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift - `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
hotspots. 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: - Siguiente bloque sugerido:
- Migrar otro grupo de bajo riesgo desde Soma a Eidos: candidatos reales por - 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`, - Antes de tocar componentes grandes (`select`, `combobox`, `calendar`,
`date-field`) conviene fijar una tabla por componente: partes Soma, `date-field`) conviene fijar una tabla por componente: partes Soma,
props visuales Eidos, recipe tokens y attrs esperados. props visuales Eidos, recipe tokens y attrs esperados.

@ -17,6 +17,7 @@ const VISUAL_ATTRS = [
['drawer/drawer-close.svelte', ['data-icon-only']], ['drawer/drawer-close.svelte', ['data-icon-only']],
['drawer/drawer-content.svelte', ['data-size']], ['drawer/drawer-content.svelte', ['data-size']],
['meter/meter.svelte', ['data-size']], ['meter/meter.svelte', ['data-size']],
['number-field/number-field.svelte', ['data-size']],
['pagination/pagination.svelte', ['data-size']], ['pagination/pagination.svelte', ['data-size']],
['popover/popover-close.svelte', ['data-icon-only']], ['popover/popover-close.svelte', ['data-icon-only']],
['popover/popover-content.svelte', ['data-size']], ['popover/popover-content.svelte', ['data-size']],

@ -355,6 +355,7 @@ su subdirectorio.
| avatar | ✅ multi-part | Image, Fallback (eidos-native) | | avatar | ✅ multi-part | Image, Fallback (eidos-native) |
| icon | ✅ single-part | + 1697 lucide glyphs | | icon | ✅ single-part | + 1697 lucide glyphs |
| meter | ✅ multi-part | Indicator | | meter | ✅ multi-part | Indicator |
| number-field | ✅ multi-part | Input, IncrementTrigger, DecrementTrigger, Scrubber |
| pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis | | pagination | ✅ multi-part | PrevTrigger, NextTrigger, Item, Ellipsis |
| progress | ✅ multi-part | Indicator | | progress | ✅ multi-part | Indicator |
| rating-group | ✅ multi-part | Item | | rating-group | ✅ multi-part | Item |

@ -0,0 +1,41 @@
// NumberField — eidos compound API, disciplined option C.
//
// import { NumberField } from '$uix/eidos/components/number-field';
//
// <NumberField bind:value min={0} max={10}>
// <NumberField.DecrementTrigger>-</NumberField.DecrementTrigger>
// <NumberField.Input />
// <NumberField.IncrementTrigger>+</NumberField.IncrementTrigger>
// </NumberField>
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';

@ -0,0 +1,10 @@
<script lang="ts">
import { DecrementTrigger as SomaNumberFieldDecrementTrigger } from '$soma/components/number-field';
import type { NumberFieldDecrementTriggerProps } from './types';
let { children, ...rest }: NumberFieldDecrementTriggerProps = $props();
</script>
<SomaNumberFieldDecrementTrigger {...rest}>
{@render children?.()}
</SomaNumberFieldDecrementTrigger>

@ -0,0 +1,10 @@
<script lang="ts">
import { IncrementTrigger as SomaNumberFieldIncrementTrigger } from '$soma/components/number-field';
import type { NumberFieldIncrementTriggerProps } from './types';
let { children, ...rest }: NumberFieldIncrementTriggerProps = $props();
</script>
<SomaNumberFieldIncrementTrigger {...rest}>
{@render children?.()}
</SomaNumberFieldIncrementTrigger>

@ -0,0 +1,10 @@
<script lang="ts">
import { Input as SomaNumberFieldInput } from '$soma/components/number-field';
import type { NumberFieldInputProps } from './types';
let { children, ...rest }: NumberFieldInputProps = $props();
</script>
<SomaNumberFieldInput {...rest}>
{@render children?.()}
</SomaNumberFieldInput>

@ -0,0 +1,10 @@
<script lang="ts">
import { Scrubber as SomaNumberFieldScrubber } from '$soma/components/number-field';
import type { NumberFieldScrubberProps } from './types';
let { children, ...rest }: NumberFieldScrubberProps = $props();
</script>
<SomaNumberFieldScrubber {...rest}>
{@render children?.()}
</SomaNumberFieldScrubber>

@ -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;
}
}

@ -0,0 +1,18 @@
<script lang="ts">
/**
* Eidos `<NumberField>` — visual wrapper over Soma's spinbutton primitive.
* Soma owns parsing, formatting, min/max, scrub and keyboard behavior.
*/
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaNumberFieldProvider } from '$soma/components/number-field';
import type { NumberFieldProps } from './types';
let { size = 'md', value = $bindable(), children, ...rest }: NumberFieldProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaNumberFieldProvider {...rest} bind:value data-size={resolvedSize}>
{@render children?.()}
</SomaNumberFieldProvider>

@ -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<Size, 'sm' | 'md' | 'lg'>;
/**
* Props for the eidos `<NumberField>`.
*
* 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<NumberFieldSize>;
};
export type NumberFieldInputProps = SomaNumberFieldInputProps;
export type NumberFieldIncrementTriggerProps = SomaNumberFieldIncrementTriggerProps;
export type NumberFieldDecrementTriggerProps = SomaNumberFieldDecrementTriggerProps;
export type NumberFieldScrubberProps = SomaNumberFieldScrubberProps;
export type { NumberFieldFormatOptions };

@ -602,6 +602,45 @@
--meter-indicator-bg-above: var(--color-threat-solid); --meter-indicator-bg-above: var(--color-threat-solid);
--meter-transition-duration: var(--duration-normal); --meter-transition-duration: var(--duration-normal);
--meter-transition-ease: var(--ease-default); --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-sm: var(--space-1);
--pagination-gap-md: var(--space-1-5); --pagination-gap-md: var(--space-1-5);
--pagination-gap-lg: var(--space-2); --pagination-gap-lg: var(--space-2);

@ -69,6 +69,7 @@
@import './components/tooltip/tooltip.css'; @import './components/tooltip/tooltip.css';
@import './components/progress/progress.css'; @import './components/progress/progress.css';
@import './components/meter/meter.css'; @import './components/meter/meter.css';
@import './components/number-field/number-field.css';
@import './components/pagination/pagination.css'; @import './components/pagination/pagination.css';
@import './components/toggle/toggle.css'; @import './components/toggle/toggle.css';
@import './components/switch/switch.css'; @import './components/switch/switch.css';

@ -347,6 +347,49 @@ export const THEME_BASE_RECIPE_TOKENS = {
'transition-duration': 'var(--duration-normal)', 'transition-duration': 'var(--duration-normal)',
'transition-ease': 'var(--ease-default)' '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: { pagination: {
'gap-sm': 'var(--space-1)', 'gap-sm': 'var(--space-1)',
'gap-md': 'var(--space-1-5)', 'gap-md': 'var(--space-1-5)',

Loading…
Cancel
Save

Powered by TurnKey Linux.