diff --git a/continue.md b/continue.md index cd5001178..fccbfa746 100644 --- a/continue.md +++ b/continue.md @@ -33,10 +33,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. +- Segunda tanda del mismo bloque: + - `Slider`: root visual `slider.svelte`, partes `Range`, `Thumb`, `Tick`, + `slider.css`, `types.ts` e `index.ts` con disciplined option C. + - La receta usa las posiciones inline calculadas por Soma y solo define + track, range, thumb, ticks, foco, disabled y size visual. + - `component-visual-attrs.test.ts`, `components/README.md`, + `THEME_BASE_RECIPE_TOKENS`, `index.css` y `generated/base.css` quedan + actualizados. +- Validado despues de `Slider`: + - `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: `separator` no existe - en Soma; candidatos reales por morfo/soma existentes son `slider`, - `number-field`, `pagination` o `rating-group`. + - Migrar otro grupo de bajo riesgo desde Soma a Eidos: candidatos reales por + morfo/soma existentes son `pagination`, `rating-group` o `number-field`. - 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 611213bb9..2d953376d 100644 --- a/src/uix/eidos/component-visual-attrs.test.ts +++ b/src/uix/eidos/component-visual-attrs.test.ts @@ -21,6 +21,7 @@ const VISUAL_ATTRS = [ ['popover/popover-content.svelte', ['data-size']], ['progress/progress.svelte', ['data-size']], ['radio-group/radio-group.svelte', ['data-size', 'data-variant', 'data-color', 'data-columns']], + ['slider/slider.svelte', ['data-size']], ['switch/switch.svelte', ['data-size']], ['tabs/tabs.svelte', ['data-size', 'data-variant', 'data-fitted', 'data-collapsed', 'data-motion']], ['tabs/tabs-content.svelte', ['data-motion']], diff --git a/src/uix/eidos/components/README.md b/src/uix/eidos/components/README.md index ba4ecc1f9..91aae5446 100644 --- a/src/uix/eidos/components/README.md +++ b/src/uix/eidos/components/README.md @@ -356,6 +356,7 @@ su subdirectorio. | icon | ✅ single-part | + 1697 lucide glyphs | | meter | ✅ multi-part | Indicator | | progress | ✅ multi-part | Indicator | +| slider | ✅ multi-part | Range, Thumb, Tick | | tooltip | ✅ multi-part | + Group | | tabs | ✅ multi-part | List, Trigger, Content, Indicator | | checkbox | ✅ multi-part | Indicator, HiddenInput, Group, GroupLabel | diff --git a/src/uix/eidos/components/slider/index.ts b/src/uix/eidos/components/slider/index.ts new file mode 100644 index 000000000..2b3cff505 --- /dev/null +++ b/src/uix/eidos/components/slider/index.ts @@ -0,0 +1,36 @@ +// Slider — eidos compound API, disciplined option C. +// +// import { Slider } from '$uix/eidos/components/slider'; +// +// +// +// +// +import SliderRoot from './slider.svelte'; +import Range from './slider-range.svelte'; +import Thumb from './slider-thumb.svelte'; +import Tick from './slider-tick.svelte'; + +type SliderNamespace = typeof SliderRoot & { + Range: typeof Range; + Thumb: typeof Thumb; + Tick: typeof Tick; +}; + +const Slider = SliderRoot as SliderNamespace; +Slider.Range = Range; +Slider.Thumb = Thumb; +Slider.Tick = Tick; + +export { Slider }; + +export default Slider; + +export type { + SliderProps, + SliderRangeProps as RangeProps, + SliderThumbProps as ThumbProps, + SliderTickProps as TickProps, + SliderSize, + SliderType +} from './types'; diff --git a/src/uix/eidos/components/slider/slider-range.svelte b/src/uix/eidos/components/slider/slider-range.svelte new file mode 100644 index 000000000..e46abac39 --- /dev/null +++ b/src/uix/eidos/components/slider/slider-range.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/slider/slider-thumb.svelte b/src/uix/eidos/components/slider/slider-thumb.svelte new file mode 100644 index 000000000..417c23c77 --- /dev/null +++ b/src/uix/eidos/components/slider/slider-thumb.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/slider/slider-tick.svelte b/src/uix/eidos/components/slider/slider-tick.svelte new file mode 100644 index 000000000..ee0c82e63 --- /dev/null +++ b/src/uix/eidos/components/slider/slider-tick.svelte @@ -0,0 +1,10 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/slider/slider.css b/src/uix/eidos/components/slider/slider.css new file mode 100644 index 000000000..165542730 --- /dev/null +++ b/src/uix/eidos/components/slider/slider.css @@ -0,0 +1,133 @@ +[data-slider] { + --_slider-track-size: var(--slider-track-size-md); + --_slider-thumb-size: var(--slider-thumb-size-md); + --_slider-hit-size: var(--slider-hit-size-md); + --_slider-tick-size: var(--slider-tick-size-md); + + cursor: pointer; +} + +[data-slider][data-size='sm'] { + --_slider-track-size: var(--slider-track-size-sm); + --_slider-thumb-size: var(--slider-thumb-size-sm); + --_slider-hit-size: var(--slider-hit-size-sm); + --_slider-tick-size: var(--slider-tick-size-sm); +} + +[data-slider][data-size='lg'] { + --_slider-track-size: var(--slider-track-size-lg); + --_slider-thumb-size: var(--slider-thumb-size-lg); + --_slider-hit-size: var(--slider-hit-size-lg); + --_slider-tick-size: var(--slider-tick-size-lg); +} + +[data-slider][data-disabled] { + cursor: not-allowed; + opacity: var(--slider-disabled-opacity); +} + +[data-slider][data-orientation='horizontal'] { + inline-size: 100%; + block-size: var(--_slider-hit-size); + min-inline-size: var(--slider-min-inline-size); +} + +[data-slider][data-orientation='vertical'] { + inline-size: var(--_slider-hit-size); + block-size: var(--slider-min-block-size); + min-block-size: var(--slider-min-block-size); +} + +[data-slider]::before { + content: ''; + position: absolute; + border-radius: var(--slider-track-radius); + background: var(--slider-track-bg); +} + +[data-slider][data-orientation='horizontal']::before { + inset-inline: 0; + inset-block-start: 50%; + block-size: var(--_slider-track-size); + transform: translateY(-50%); +} + +[data-slider][data-orientation='vertical']::before { + inset-block: 0; + inset-inline-start: 50%; + inline-size: var(--_slider-track-size); + transform: translateX(-50%); +} + +[data-slider-range] { + border-radius: var(--slider-track-radius); + background: var(--slider-range-bg); + pointer-events: none; +} + +[data-slider-range][data-orientation='horizontal'] { + inset-block-start: 50%; + block-size: var(--_slider-track-size); + transform: translateY(-50%); +} + +[data-slider-range][data-orientation='vertical'] { + inset-inline-start: 50%; + inline-size: var(--_slider-track-size); + transform: translateX(-50%); +} + +[data-slider-thumb] { + inline-size: var(--_slider-thumb-size); + block-size: var(--_slider-thumb-size); + border: var(--slider-thumb-border-width) solid var(--slider-thumb-border); + border-radius: var(--slider-thumb-radius); + background: var(--slider-thumb-bg); + box-shadow: var(--slider-thumb-shadow); + cursor: grab; + outline: none; + transition: + background var(--slider-transition-duration) var(--slider-transition-ease), + border-color var(--slider-transition-duration) var(--slider-transition-ease), + box-shadow var(--slider-transition-duration) var(--slider-transition-ease); +} + +[data-slider-thumb]:active { + cursor: grabbing; +} + +[data-slider-thumb]:focus-visible { + box-shadow: var(--slider-thumb-focus-shadow); +} + +[data-slider-thumb][data-disabled] { + cursor: not-allowed; +} + +[data-slider-tick] { + inline-size: var(--_slider-tick-size); + block-size: var(--_slider-tick-size); + border-radius: var(--slider-tick-radius); + background: var(--slider-tick-bg); + pointer-events: none; +} + +[data-slider-tick][data-orientation='horizontal'] { + inset-block-start: 50%; + margin-block-start: calc(var(--_slider-tick-size) / -2); +} + +[data-slider-tick][data-orientation='vertical'] { + inset-inline-start: 50%; + margin-inline-start: calc(var(--_slider-tick-size) / -2); +} + +[data-slider-tick][data-active] { + background: var(--slider-tick-bg-active); +} + +@media (prefers-reduced-motion: reduce) { + [data-slider-thumb] { + transition: none; + } +} diff --git a/src/uix/eidos/components/slider/slider.svelte b/src/uix/eidos/components/slider/slider.svelte new file mode 100644 index 000000000..539140498 --- /dev/null +++ b/src/uix/eidos/components/slider/slider.svelte @@ -0,0 +1,18 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/slider/types.ts b/src/uix/eidos/components/slider/types.ts new file mode 100644 index 000000000..d5bbc4e4a --- /dev/null +++ b/src/uix/eidos/components/slider/types.ts @@ -0,0 +1,26 @@ +import type { + ProviderProps as SomaSliderProviderProps, + RangeProps as SomaSliderRangeProps, + SliderType, + ThumbProps as SomaSliderThumbProps, + TickProps as SomaSliderTickProps +} from '$soma/components/slider'; +import type { ResponsiveProp, Size } from '$uix/eidos/lib/types'; + +export type SliderSize = Extract; + +/** + * Props for the eidos ``. + * + * Extends Soma's slider with visual sizing only. Orientation, disabled state, + * range position, thumb position and ARIA are owned by Soma. + */ +export type SliderProps = SomaSliderProviderProps & { + /** Visual size. Accepts a responsive map resolved by ActiveEidos. @default 'md' */ + size?: ResponsiveProp; +}; + +export type SliderRangeProps = SomaSliderRangeProps; +export type SliderThumbProps = SomaSliderThumbProps; +export type SliderTickProps = SomaSliderTickProps; +export type { SliderType }; diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 3b7600017..0e01ce8ad 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -703,6 +703,35 @@ --radio-group-label-font-weight: var(--font-weight-medium); --radio-group-label-line-height: var(--leading-ui); --radio-group-label-color: var(--color-content-primary); + --slider-track-size-sm: 4px; + --slider-track-size-md: 6px; + --slider-track-size-lg: 8px; + --slider-thumb-size-sm: 16px; + --slider-thumb-size-md: 20px; + --slider-thumb-size-lg: 24px; + --slider-hit-size-sm: 28px; + --slider-hit-size-md: 32px; + --slider-hit-size-lg: 36px; + --slider-tick-size-sm: 4px; + --slider-tick-size-md: 5px; + --slider-tick-size-lg: 6px; + --slider-min-inline-size: 12rem; + --slider-min-block-size: 10rem; + --slider-track-radius: var(--radius-full); + --slider-track-bg: var(--color-neutral-track); + --slider-range-bg: var(--color-primary-solid); + --slider-thumb-radius: var(--radius-full); + --slider-thumb-border-width: var(--border-width); + --slider-thumb-border: var(--color-border-default); + --slider-thumb-bg: var(--color-surface-default); + --slider-thumb-shadow: var(--shadow-subtle); + --slider-thumb-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), var(--slider-thumb-shadow); + --slider-tick-radius: var(--radius-full); + --slider-tick-bg: var(--color-neutral-border); + --slider-tick-bg-active: var(--color-primary-solid); + --slider-transition-duration: var(--duration-fast); + --slider-transition-ease: var(--ease-default); + --slider-disabled-opacity: 0.56; --switch-size-sm-track-width: 32px; --switch-size-sm-track-height: 18px; --switch-size-sm-thumb-size: 14px; diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index cf29d269d..bfcb43739 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -76,3 +76,4 @@ @import './components/tabs/tabs.css'; @import './components/checkbox/checkbox.css'; @import './components/radio-group/radio-group.css'; +@import './components/slider/slider.css'; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index ec2bc08d3..278aeabb8 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -459,6 +459,38 @@ export const THEME_BASE_RECIPE_TOKENS = { 'label-line-height': 'var(--leading-ui)', 'label-color': 'var(--color-content-primary)' }, + slider: { + 'track-size-sm': '4px', + 'track-size-md': '6px', + 'track-size-lg': '8px', + 'thumb-size-sm': '16px', + 'thumb-size-md': '20px', + 'thumb-size-lg': '24px', + 'hit-size-sm': '28px', + 'hit-size-md': '32px', + 'hit-size-lg': '36px', + 'tick-size-sm': '4px', + 'tick-size-md': '5px', + 'tick-size-lg': '6px', + 'min-inline-size': '12rem', + 'min-block-size': '10rem', + 'track-radius': 'var(--radius-full)', + 'track-bg': 'var(--color-neutral-track)', + 'range-bg': 'var(--color-primary-solid)', + 'thumb-radius': 'var(--radius-full)', + 'thumb-border-width': 'var(--border-width)', + 'thumb-border': 'var(--color-border-default)', + 'thumb-bg': 'var(--color-surface-default)', + 'thumb-shadow': 'var(--shadow-subtle)', + 'thumb-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), var(--slider-thumb-shadow)', + 'tick-radius': 'var(--radius-full)', + 'tick-bg': 'var(--color-neutral-border)', + 'tick-bg-active': 'var(--color-primary-solid)', + 'transition-duration': 'var(--duration-fast)', + 'transition-ease': 'var(--ease-default)', + 'disabled-opacity': '0.56' + }, switch: { 'size-sm-track-width': '32px', 'size-sm-track-height': '18px',