From 2f147897d9e4b3e74f0d55b30757a9a334513da1 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 21 May 2026 18:55:00 +0200 Subject: [PATCH] feat(morfo/time-picker): declare HourSlider/MinuteSlider/SecondSlider as parts + ruler opt-out prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User pointed out that TimePicker exposes / .MinuteSlider / .SecondSlider in its public API but those weren't declared in the time-picker morfo — the contract was implicit (data- slider attrs from the slider morfo). Now made explicit. Morfo additions (all optional, archetype: 'group'): - HourSlider → data-time-picker-hour-slider - MinuteSlider → data-time-picker-minute-slider - SecondSlider → data-time-picker-second-slider Eidos slider wrappers stamp the corresponding data-attr on the outer wrapper div so morfo:check finds the parts in the DOM. The inner slider DOM (data-slider, data-slider-thumb, data-slider-range) keeps its own contract via the slider morfo — composition, not duplication. Also: - Hour ticks now end-inclusive: 24h shows 0/3/6/9/12/15/18/21/23 (was 0..21, missing the upper cap). 12h shows 1/3/6/9/12. - Minute / Second scale now ends with 59 so the upper bound is shown. - New optional `ruler` prop on (default `true`) and matching per-slider `ruler` opt-out. Propagated via visual context. - Demo: new switch to toggle the ruler scale. 67/67 component-audit PASS, 39/39 morfo:check PASS, svelte-check 0 errors / 0 warnings. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/time-picker/context.svelte.ts | 7 +++ .../time-picker-hour-slider.svelte | 42 ++++++++++----- .../time-picker-minute-slider.svelte | 37 ++++++++----- .../time-picker-second-slider.svelte | 34 ++++++++---- .../components/time-picker/time-picker.svelte | 4 +- src/uix/eidos/components/time-picker/types.ts | 5 ++ src/uix/morfo/components/time-picker.ts | 52 +++++++++++++++++++ .../uix/components/time-picker/+page.svelte | 9 ++++ 8 files changed, 153 insertions(+), 37 deletions(-) diff --git a/src/uix/eidos/components/time-picker/context.svelte.ts b/src/uix/eidos/components/time-picker/context.svelte.ts index 85f37d270..0ae22fcbd 100644 --- a/src/uix/eidos/components/time-picker/context.svelte.ts +++ b/src/uix/eidos/components/time-picker/context.svelte.ts @@ -7,6 +7,13 @@ export interface TimePickerVisualContext { getSize(): TimePickerSize; getVariant(): TimePickerVariant; getColor(): TimePickerColor; + /** + * Whether to render the labelled ruler scale next to each slider. + * `true` by default; pass `ruler={false}` on `` to hide + * the rulers entirely, or pass `ruler={false}` on an individual + * slider eidos wrapper to opt that one out. + */ + getRuler(): boolean; } export function setTimePickerVisualContext(ctx: TimePickerVisualContext): TimePickerVisualContext { diff --git a/src/uix/eidos/components/time-picker/time-picker-hour-slider.svelte b/src/uix/eidos/components/time-picker/time-picker-hour-slider.svelte index d60c2024f..577c74576 100644 --- a/src/uix/eidos/components/time-picker/time-picker-hour-slider.svelte +++ b/src/uix/eidos/components/time-picker/time-picker-hour-slider.svelte @@ -2,20 +2,32 @@ import * as TimePicker from '$soma/components/time-picker'; import { TimePickerProvider } from '$soma/components/time-picker'; import * as Slider from '$soma/components/slider'; + import { getTimePickerVisualContext } from './context.svelte'; import type { TimePickerSliderProps } from './types'; - let { children, ...rest }: TimePickerSliderProps = $props(); + type Props = TimePickerSliderProps & { + /** Show the labelled ruler above the track. Falls back to the + * picker-level `ruler` flag (default `true`). */ + ruler?: boolean; + }; + + let { children, ruler, ...rest }: Props = $props(); const provider = TimePickerProvider.require(); + const visual = getTimePickerVisualContext(); const cycle = $derived(provider.resolvedHourCycle); - // Cap-inclusive range. 24h → 0..23 (step 3 ticks): 0,3,6,9,12,15,18,21,23 - // 12h → 1..12 (step 1, every 3): 1,3,6,9,12 + + // Endpoint-inclusive scale. 24h: every 3h plus the upper cap (23) so the + // scale shows the full available range, not just up to 21. 12h: 1, 3, 6, + // 9, 12 — already endpoint-inclusive. const tickValues = $derived.by(() => { if (cycle === 12) return [1, 3, 6, 9, 12]; - return [0, 3, 6, 9, 12, 15, 18, 21]; + return [0, 3, 6, 9, 12, 15, 18, 21, 23]; }); + const min = $derived(provider.hourMin); const max = $derived(provider.hourMax); + const showRuler = $derived(ruler ?? visual?.getRuler() ?? true); function percent(v: number): string { const t = (v - min) / (max - min || 1); @@ -23,15 +35,19 @@ } -
- +
+ {#if showRuler} + + {/if} {#if children} {@render children?.()} diff --git a/src/uix/eidos/components/time-picker/time-picker-minute-slider.svelte b/src/uix/eidos/components/time-picker/time-picker-minute-slider.svelte index a91a0e39f..f8d8e90fb 100644 --- a/src/uix/eidos/components/time-picker/time-picker-minute-slider.svelte +++ b/src/uix/eidos/components/time-picker/time-picker-minute-slider.svelte @@ -1,22 +1,33 @@ -
+
{#if children} {@render children?.()} @@ -25,12 +36,14 @@ {/if} - + {#if showRuler} + + {/if}
diff --git a/src/uix/eidos/components/time-picker/time-picker-second-slider.svelte b/src/uix/eidos/components/time-picker/time-picker-second-slider.svelte index cd4bf7e6e..e1c6ec798 100644 --- a/src/uix/eidos/components/time-picker/time-picker-second-slider.svelte +++ b/src/uix/eidos/components/time-picker/time-picker-second-slider.svelte @@ -1,21 +1,31 @@ -
+
{#if children} {@render children?.()} @@ -24,12 +34,14 @@ {/if} - + {#if showRuler} + + {/if}
diff --git a/src/uix/eidos/components/time-picker/time-picker.svelte b/src/uix/eidos/components/time-picker/time-picker.svelte index 864c8d9fb..fc1ff7ffa 100644 --- a/src/uix/eidos/components/time-picker/time-picker.svelte +++ b/src/uix/eidos/components/time-picker/time-picker.svelte @@ -8,6 +8,7 @@ size = 'md', variant = 'surface', color = 'primary', + ruler = true, value = $bindable(), placeholder = $bindable(), open = $bindable(false), @@ -21,7 +22,8 @@ setTimePickerVisualContext({ getSize: () => resolvedSize, getVariant: () => variant, - getColor: () => color + getColor: () => color, + getRuler: () => ruler }); diff --git a/src/uix/eidos/components/time-picker/types.ts b/src/uix/eidos/components/time-picker/types.ts index 9b9b7e944..a6cb97660 100644 --- a/src/uix/eidos/components/time-picker/types.ts +++ b/src/uix/eidos/components/time-picker/types.ts @@ -33,6 +33,11 @@ export type TimePickerProps = ProviderProps & { variant?: TimePickerVariant /** Intent palette used for focus and selected affordances. @default 'primary' */ color?: TimePickerColor + /** + * Show the labelled ruler scale next to each slider in the clock + * popover. @default true + */ + ruler?: boolean } export type TimePickerTriggerProps = SomaTimePickerTriggerProps & { diff --git a/src/uix/morfo/components/time-picker.ts b/src/uix/morfo/components/time-picker.ts index e65a476b3..60d424c46 100644 --- a/src/uix/morfo/components/time-picker.ts +++ b/src/uix/morfo/components/time-picker.ts @@ -230,6 +230,58 @@ export const timePickerMorfo = { { key: 'Shift+Tab', action: 'focus-prev' } ] }, + { + // Hour / Minute / Second sliders — composite parts. The outer + // wrapper element carries `data-time-picker-{X}-slider`; the + // inner slider DOM is declared by the slider morfo (data-slider, + // data-slider-thumb, data-slider-range). + name: 'HourSlider', + kebab: 'hour-slider', + archetype: 'group', + kind: 'public', + defaultElement: 'div', + optional: true, + data: [{ attr: 'data-disabled', severity: 'optional' }], + aria: [ + { + attr: 'aria-label', + value: v.translationRef('#?components.time-picker.hour|Hour'), + severity: 'optional' + } + ] + }, + { + name: 'MinuteSlider', + kebab: 'minute-slider', + archetype: 'group', + kind: 'public', + defaultElement: 'div', + optional: true, + data: [{ attr: 'data-disabled', severity: 'optional' }], + aria: [ + { + attr: 'aria-label', + value: v.translationRef('#?components.time-picker.minute|Minute'), + severity: 'optional' + } + ] + }, + { + name: 'SecondSlider', + kebab: 'second-slider', + archetype: 'group', + kind: 'public', + defaultElement: 'div', + optional: true, + data: [{ attr: 'data-disabled', severity: 'optional' }], + aria: [ + { + attr: 'aria-label', + value: v.translationRef('#?components.time-picker.second|Second'), + severity: 'optional' + } + ] + }, { name: 'DayPeriodToggle', kebab: 'day-period-toggle', diff --git a/web/routes/uix/components/time-picker/+page.svelte b/web/routes/uix/components/time-picker/+page.svelte index b0ef0fa20..70063487d 100644 --- a/web/routes/uix/components/time-picker/+page.svelte +++ b/web/routes/uix/components/time-picker/+page.svelte @@ -38,6 +38,7 @@ let disabled = $state(false); let readonly = $state(false); let lockSeconds = $state(false); + let ruler = $state(true); let granularity = $state('minute'); let hourCycle = $state(24); let bounds = $state(false); @@ -239,6 +240,7 @@ {granularity} {hourCycle} {locale} + {ruler} {size} {variant} {color} @@ -421,6 +423,13 @@ seconds readonly +
Footer composition