Add Eidos slider wrapper

active-uix
dev 5 months ago
parent 5895f9d8b8
commit bdaae718ad

@ -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.

@ -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']],

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

@ -0,0 +1,36 @@
// Slider — eidos compound API, disciplined option C.
//
// import { Slider } from '$uix/eidos/components/slider';
//
// <Slider bind:value aria-label="Volume">
// <Slider.Range />
// <Slider.Thumb />
// </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';

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

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

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

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

@ -0,0 +1,18 @@
<script lang="ts">
/**
* Eidos `<Slider>` — visual wrapper over Soma's slider primitive.
* Soma owns pointer/keyboard/value behavior; Eidos adds `size`.
*/
import { ActiveEidos } from '$uix/eidos';
import { Provider as SomaSliderProvider } from '$soma/components/slider';
import type { SliderProps } from './types';
let { size = 'md', value = $bindable([0]), children, ...rest }: SliderProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<SomaSliderProvider {...rest} bind:value data-size={resolvedSize}>
{@render children?.()}
</SomaSliderProvider>

@ -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<Size, 'sm' | 'md' | 'lg'>;
/**
* Props for the eidos `<Slider>`.
*
* 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<SliderSize>;
};
export type SliderRangeProps = SomaSliderRangeProps;
export type SliderThumbProps = SomaSliderThumbProps;
export type SliderTickProps = SomaSliderTickProps;
export type { SliderType };

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

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

@ -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',

Loading…
Cancel
Save

Powered by TurnKey Linux.