refactor(time-picker): promote HourScale ruler to soma part (audit 5.3 P2)

Eliminates `Provider.require()` from eidos hour-sliders by promoting
the tick scale to canonical soma parts:

  - `<TimePicker.HourScale>` — exposes `{ ticks, percent, label, cycle,
    min, max }` via a snippet so eidos can paint the ruler without
    importing TimePickerProvider.
  - `<TimeRangePicker.HourScale>` — same shape for the range variant.
    Scale is global (both endpoints share the cycle), so no endpoint
    prop.

Eidos hour-sliders become pure composition + paint via snippet props.
No JS coupling between eidos and the soma provider class.

Both new parts declared in their morfos as `kind: public, optional`,
with `kebab: 'hour-scale'`. Stamp `data-time-picker-hour-scale` /
`data-time-range-picker-hour-scale` plus `aria-hidden="true"`.

Test result unchanged: 2391/2397 passing. No regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 74ccfead70
commit a5243168e5

@ -1,6 +1,12 @@
<script lang="ts"> <script lang="ts">
/**
* Eidos `<TimePicker.HourSlider>` — pure paint wrapper composing
* soma's `<TimePicker.HourSlider>` (slider behavior) with the
* optional `<TimePicker.HourScale>` (tick data). The eidos paints
* the ruler chrome via the scale's snippet — no `Provider.require()`
* on the eidos side.
*/
import * as TimePicker from '$soma/components/time-picker'; import * as TimePicker from '$soma/components/time-picker';
import { TimePickerProvider } from '$soma/components/time-picker';
import * as Slider from '$soma/components/slider'; import * as Slider from '$soma/components/slider';
import { getTimePickerVisualContext } from './context.svelte'; import { getTimePickerVisualContext } from './context.svelte';
import type { TimePickerSliderProps } from './types'; import type { TimePickerSliderProps } from './types';
@ -13,40 +19,22 @@
let { children, ruler, ...rest }: Props = $props(); let { children, ruler, ...rest }: Props = $props();
const provider = TimePickerProvider.require();
const visual = getTimePickerVisualContext(); const visual = getTimePickerVisualContext();
const cycle = $derived(provider.resolvedHourCycle);
// Endpoint-inclusive scale. Step 6h for 24-hour cycle gives clean
// 0/6/12/18/23 ticks without the awkward "21 → 23" close pair that
// step=3 produces. 12h cycle keeps the classic clock face hours.
const tickValues = $derived.by(() => {
if (cycle === 12) return [1, 3, 6, 9, 12];
return [0, 6, 12, 18, 23];
});
const min = $derived(provider.hourMin);
const max = $derived(provider.hourMax);
const showRuler = $derived(ruler ?? visual?.getRuler() ?? true); const showRuler = $derived(ruler ?? visual?.getRuler() ?? true);
function percent(v: number): string {
const t = (v - min) / (max - min || 1);
return `${Math.max(0, Math.min(1, t)) * 100}%`;
}
</script> </script>
<div data-time-picker-hour-slider class="time-picker-slider time-picker-hour-slider"> <div class="time-picker-slider time-picker-hour-slider">
{#if showRuler} {#if showRuler}
<div class="time-picker-scale" aria-hidden="true"> <TimePicker.HourScale class="time-picker-scale">
{#each tickValues as v} {#snippet children({ ticks, percent, label })}
<span class="time-picker-scale-tick" style:left={percent(v)}> {#each ticks as v}
<span class="time-picker-scale-mark"></span> <span class="time-picker-scale-tick" style:left={percent(v)}>
<span class="time-picker-scale-label" <span class="time-picker-scale-mark"></span>
>{cycle === 12 ? v : String(v).padStart(2, '0')}</span <span class="time-picker-scale-label">{label(v)}</span>
> </span>
</span> {/each}
{/each} {/snippet}
</div> </TimePicker.HourScale>
{/if} {/if}
<TimePicker.HourSlider {...rest}> <TimePicker.HourSlider {...rest}>
{#if children} {#if children}

@ -1,6 +1,12 @@
<script lang="ts"> <script lang="ts">
/**
* Eidos `<TimeRangePicker.HourSlider>` — pure paint wrapper. Same
* pattern as the single TimePicker variant: composes soma's
* `<TimeRangePicker.HourSlider>` (per-endpoint slider behavior) with
* the optional `<TimeRangePicker.HourScale>` (tick data). Scale is
* global to the range picker (both endpoints share the same cycle).
*/
import * as TimeRangePicker from '$soma/components/time-range-picker'; import * as TimeRangePicker from '$soma/components/time-range-picker';
import { TimeRangePickerProvider } from '$soma/components/time-range-picker';
import * as Slider from '$soma/components/slider'; import * as Slider from '$soma/components/slider';
import { getTimeRangePickerVisualContext } from './context.svelte'; import { getTimeRangePickerVisualContext } from './context.svelte';
import type { TimeRangePickerSliderProps } from './types'; import type { TimeRangePickerSliderProps } from './types';
@ -13,44 +19,25 @@
let { children, ruler, type, ...rest }: Props = $props(); let { children, ruler, type, ...rest }: Props = $props();
const provider = TimeRangePickerProvider.require();
const visual = getTimeRangePickerVisualContext(); const visual = getTimeRangePickerVisualContext();
const cycle = $derived(provider.resolvedHourCycle);
// Endpoint-inclusive scale. Step 6 hours for 24h cycle gives clean
// 0/6/12/18/23 ticks without the awkward "21 → 23" close pair that
// step=3 produces. 12h cycle uses the classic clock face hours.
const tickValues = $derived.by(() => {
if (cycle === 12) return [1, 3, 6, 9, 12];
return [0, 6, 12, 18, 23];
});
const min = $derived(provider.hourMin);
const max = $derived(provider.hourMax);
const showRuler = $derived(ruler ?? visual?.getRuler() ?? true); const showRuler = $derived(ruler ?? visual?.getRuler() ?? true);
function percent(v: number): string {
const t = (v - min) / (max - min || 1);
return `${Math.max(0, Math.min(1, t)) * 100}%`;
}
</script> </script>
<div <div
data-time-range-picker-hour-slider
data-endpoint={type} data-endpoint={type}
class="time-range-picker-slider time-range-picker-hour-slider" class="time-range-picker-slider time-range-picker-hour-slider"
> >
{#if showRuler} {#if showRuler}
<div class="time-range-picker-scale" aria-hidden="true"> <TimeRangePicker.HourScale class="time-range-picker-scale">
{#each tickValues as v} {#snippet children({ ticks, percent, label })}
<span class="time-range-picker-scale-tick" style:left={percent(v)}> {#each ticks as v}
<span class="time-range-picker-scale-mark"></span> <span class="time-range-picker-scale-tick" style:left={percent(v)}>
<span class="time-range-picker-scale-label" <span class="time-range-picker-scale-mark"></span>
>{cycle === 12 ? v : String(v).padStart(2, '0')}</span <span class="time-range-picker-scale-label">{label(v)}</span>
> </span>
</span> {/each}
{/each} {/snippet}
</div> </TimeRangePicker.HourScale>
{/if} {/if}
<TimeRangePicker.HourSlider {type} {...rest}> <TimeRangePicker.HourSlider {type} {...rest}>
{#if children} {#if children}

@ -233,6 +233,16 @@ export const timePickerMorfo = {
{ {
// Hour / Minute / Second sliders — composite parts. The outer // Hour / Minute / Second sliders — composite parts. The outer
// wrapper element carries `data-time-picker-{X}-slider`; the // wrapper element carries `data-time-picker-{X}-slider`; the
name: 'HourScale',
kebab: 'hour-scale',
archetype: 'group',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
// inner slider DOM is declared by the slider morfo (data-slider, // inner slider DOM is declared by the slider morfo (data-slider,
// data-slider-thumb, data-slider-range). // data-slider-thumb, data-slider-range).
name: 'HourSlider', name: 'HourSlider',

@ -231,6 +231,19 @@ export const timeRangePickerMorfo = {
{ key: 'Shift+Tab', action: 'focus-prev' } { key: 'Shift+Tab', action: 'focus-prev' }
] ]
}, },
{
// Hour scale — decorative ruler exposing tick data to eidos.
// No per-endpoint state; the scale is global to the range
// picker (both endpoints share the same hour cycle).
name: 'HourScale',
kebab: 'hour-scale',
archetype: 'group',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{ {
// Hour / Minute / Second sliders — composite parts. The outer // Hour / Minute / Second sliders — composite parts. The outer
// wrapper carries `data-time-range-picker-{X}-slider` plus // wrapper carries `data-time-range-picker-{X}-slider` plus

@ -0,0 +1,63 @@
<script lang="ts">
/**
* Soma `<TimePicker.HourScale>` — exposes the hour-axis tick data
* (cycle / min / max / positions) via a `children` snippet so visual
* layers (eidos) can paint a ruler / scale above the slider without
* importing the TimePickerProvider class.
*
* <TimePicker.HourScale>
* {#snippet children({ ticks, percent, label })}
* {#each ticks as v}
* <span style:left={percent(v)}>{label(v)}</span>
* {/each}
* {/snippet}
* </TimePicker.HourScale>
*
* If `children` is omitted, the scale renders an empty wrapper —
* useful as a positioning anchor when visuals are stamped via CSS
* pseudo-elements.
*/
import type { Snippet } from 'svelte';
import { TimePickerProvider } from '../time-picker-provider.svelte';
import type { HTMLAttributes } from 'svelte/elements';
interface TimePickerHourScaleSnippetProps {
ticks: readonly number[];
percent: (v: number) => string;
label: (v: number) => string;
cycle: 12 | 24;
min: number;
max: number;
}
type TimePickerHourScaleProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
children?: Snippet<[TimePickerHourScaleSnippetProps]>;
};
let { children, ...restProps }: TimePickerHourScaleProps = $props();
const provider = TimePickerProvider.require();
const cycle = $derived<12 | 24>(provider.resolvedHourCycle);
const min = $derived(provider.hourMin);
const max = $derived(provider.hourMax);
// Endpoint-inclusive ticks. 6-hour step on 24-cycle gives clean
// 0/6/12/18/23 without the awkward 21→23 close pair. 12-cycle keeps
// the classic analog clock face hours.
const ticks = $derived<readonly number[]>(
cycle === 12 ? [1, 3, 6, 9, 12] : [0, 6, 12, 18, 23]
);
function percent(v: number): string {
const t = (v - min) / (max - min || 1);
return `${Math.max(0, Math.min(1, t)) * 100}%`;
}
function label(v: number): string {
return cycle === 12 ? String(v) : String(v).padStart(2, '0');
}
</script>
<div data-time-picker-hour-scale aria-hidden="true" {...restProps}>
{@render children?.({ ticks, percent, label, cycle, min, max })}
</div>

@ -2,6 +2,7 @@
export { default as Provider } from './components/time-picker.svelte'; export { default as Provider } from './components/time-picker.svelte';
export { default as Trigger } from './components/time-picker-trigger.svelte'; export { default as Trigger } from './components/time-picker-trigger.svelte';
export { default as Clock } from './components/time-picker-clock.svelte'; export { default as Clock } from './components/time-picker-clock.svelte';
export { default as HourScale } from './components/time-picker-hour-scale.svelte';
export { default as HourSlider } from './components/time-picker-hour-slider.svelte'; export { default as HourSlider } from './components/time-picker-hour-slider.svelte';
export { default as MinuteSlider } from './components/time-picker-minute-slider.svelte'; export { default as MinuteSlider } from './components/time-picker-minute-slider.svelte';
export { default as SecondSlider } from './components/time-picker-second-slider.svelte'; export { default as SecondSlider } from './components/time-picker-second-slider.svelte';

@ -0,0 +1,51 @@
<script lang="ts">
/**
* Soma `<TimeRangePicker.HourScale>` — exposes the hour-axis tick data
* (cycle / min / max / positions) via a `children` snippet so visual
* layers (eidos) can paint a ruler above each endpoint's slider
* without importing the TimeRangePickerProvider class.
*
* Scale is global to the range picker (both endpoints share the same
* cycle / hour range), so this part does NOT take an `endpoint` prop.
*/
import type { Snippet } from 'svelte';
import { TimeRangePickerProvider } from '../time-range-picker-provider.svelte';
import type { HTMLAttributes } from 'svelte/elements';
interface TimeRangePickerHourScaleSnippetProps {
ticks: readonly number[];
percent: (v: number) => string;
label: (v: number) => string;
cycle: 12 | 24;
min: number;
max: number;
}
type TimeRangePickerHourScaleProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
children?: Snippet<[TimeRangePickerHourScaleSnippetProps]>;
};
let { children, ...restProps }: TimeRangePickerHourScaleProps = $props();
const provider = TimeRangePickerProvider.require();
const cycle = $derived<12 | 24>(provider.resolvedHourCycle);
const min = $derived(provider.hourMin);
const max = $derived(provider.hourMax);
const ticks = $derived<readonly number[]>(
cycle === 12 ? [1, 3, 6, 9, 12] : [0, 6, 12, 18, 23]
);
function percent(v: number): string {
const t = (v - min) / (max - min || 1);
return `${Math.max(0, Math.min(1, t)) * 100}%`;
}
function label(v: number): string {
return cycle === 12 ? String(v) : String(v).padStart(2, '0');
}
</script>
<div data-time-range-picker-hour-scale aria-hidden="true" {...restProps}>
{@render children?.({ ticks, percent, label, cycle, min, max })}
</div>

@ -2,6 +2,7 @@
export { default as Provider } from './components/time-range-picker.svelte'; export { default as Provider } from './components/time-range-picker.svelte';
export { default as Trigger } from './components/time-range-picker-trigger.svelte'; export { default as Trigger } from './components/time-range-picker-trigger.svelte';
export { default as Clock } from './components/time-range-picker-clock.svelte'; export { default as Clock } from './components/time-range-picker-clock.svelte';
export { default as HourScale } from './components/time-range-picker-hour-scale.svelte';
export { default as HourSlider } from './components/time-range-picker-hour-slider.svelte'; export { default as HourSlider } from './components/time-range-picker-hour-slider.svelte';
export { default as MinuteSlider } from './components/time-range-picker-minute-slider.svelte'; export { default as MinuteSlider } from './components/time-range-picker-minute-slider.svelte';
export { default as SecondSlider } from './components/time-range-picker-second-slider.svelte'; export { default as SecondSlider } from './components/time-range-picker-second-slider.svelte';

Loading…
Cancel
Save

Powered by TurnKey Linux.