fix(eidos/time-picker): default-compose Range+Thumb and add labelled ruler

Reported: the popover sliders showed a track with no visible thumb,
and there was no indication of the value scale.

- Each TimePicker.{Hour,Minute,Second}Slider eidos wrapper now renders
  <Slider.Range /> + <Slider.Thumb /> by default when no children are
  passed. Consumers can still override with custom children.
- Adds a labelled ruler row next to each slider:
  - HourSlider: scale ABOVE the track. 24h shows 0/3/6/9/12/15/18/21,
    12h shows 1/3/6/9/12. Numbers zero-padded for 24h.
  - MinuteSlider / SecondSlider: scale BELOW the track at 0/15/30/45.
  - Tick positions are computed proportionally and offset inward by
    half the thumb width so the endpoint labels align with the actual
    track endpoints (not the bounding box).
- Reinforces thumb visibility inside the clock popover: ring shadow
  against the popover surface + accented border tied to the picker's
  intent color. Range fill matches the accent border so the filled
  portion of the track is unambiguous.

All checks clean: 67/67 audit PASS, svelte-check 0 errors.

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

@ -1,10 +1,43 @@
<script lang="ts">
import * as TimePicker from '$soma/components/time-picker';
import { TimePickerProvider } from '$soma/components/time-picker';
import * as Slider from '$soma/components/slider';
import type { TimePickerSliderProps } from './types';
let { children, ...rest }: TimePickerSliderProps = $props();
const provider = TimePickerProvider.require();
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
const tickValues = $derived.by(() => {
if (cycle === 12) return [1, 3, 6, 9, 12];
return [0, 3, 6, 9, 12, 15, 18, 21];
});
const min = $derived(provider.hourMin);
const max = $derived(provider.hourMax);
function percent(v: number): string {
const t = (v - min) / (max - min || 1);
return `${Math.max(0, Math.min(1, t)) * 100}%`;
}
</script>
<TimePicker.HourSlider {...rest}>
{@render children?.()}
</TimePicker.HourSlider>
<div class="time-picker-slider time-picker-hour-slider">
<div class="time-picker-scale" aria-hidden="true">
{#each tickValues as v}
<span class="time-picker-scale-tick" style:left={percent(v)}>
<span class="time-picker-scale-mark"></span>
<span class="time-picker-scale-label">{cycle === 12 ? v : String(v).padStart(2, '0')}</span>
</span>
{/each}
</div>
<TimePicker.HourSlider {...rest}>
{#if children}
{@render children?.()}
{:else}
<Slider.Range />
<Slider.Thumb />
{/if}
</TimePicker.HourSlider>
</div>

@ -1,10 +1,36 @@
<script lang="ts">
import * as TimePicker from '$soma/components/time-picker';
import * as Slider from '$soma/components/slider';
import type { TimePickerSliderProps } from './types';
let { children, ...rest }: TimePickerSliderProps = $props();
// Tick every 15 minutes by default: 0, 15, 30, 45 (range 0..59).
const tickValues = [0, 15, 30, 45];
const min = 0;
const max = 59;
function percent(v: number): string {
const t = (v - min) / (max - min);
return `${Math.max(0, Math.min(1, t)) * 100}%`;
}
</script>
<TimePicker.MinuteSlider {...rest}>
{@render children?.()}
</TimePicker.MinuteSlider>
<div class="time-picker-slider time-picker-minute-slider">
<TimePicker.MinuteSlider {...rest}>
{#if children}
{@render children?.()}
{:else}
<Slider.Range />
<Slider.Thumb />
{/if}
</TimePicker.MinuteSlider>
<div class="time-picker-scale time-picker-scale-below" aria-hidden="true">
{#each tickValues as v}
<span class="time-picker-scale-tick" style:left={percent(v)}>
<span class="time-picker-scale-mark"></span>
<span class="time-picker-scale-label">{String(v).padStart(2, '0')}</span>
</span>
{/each}
</div>
</div>

@ -1,10 +1,35 @@
<script lang="ts">
import * as TimePicker from '$soma/components/time-picker';
import * as Slider from '$soma/components/slider';
import type { TimePickerSliderProps } from './types';
let { children, ...rest }: TimePickerSliderProps = $props();
const tickValues = [0, 15, 30, 45];
const min = 0;
const max = 59;
function percent(v: number): string {
const t = (v - min) / (max - min);
return `${Math.max(0, Math.min(1, t)) * 100}%`;
}
</script>
<TimePicker.SecondSlider {...rest}>
{@render children?.()}
</TimePicker.SecondSlider>
<div class="time-picker-slider time-picker-second-slider">
<TimePicker.SecondSlider {...rest}>
{#if children}
{@render children?.()}
{:else}
<Slider.Range />
<Slider.Thumb />
{/if}
</TimePicker.SecondSlider>
<div class="time-picker-scale time-picker-scale-below" aria-hidden="true">
{#each tickValues as v}
<span class="time-picker-scale-tick" style:left={percent(v)}>
<span class="time-picker-scale-mark"></span>
<span class="time-picker-scale-label">{String(v).padStart(2, '0')}</span>
</span>
{/each}
</div>
</div>

@ -169,6 +169,83 @@
inline-size: 100%;
}
/* ── Labelled scale (ruler) above hour slider, below minute/second ───────── */
.time-picker-slider {
display: flex;
flex-direction: column;
gap: var(--space-1);
inline-size: 100%;
}
.time-picker-scale {
position: relative;
block-size: 1.25rem;
inline-size: 100%;
/* Inner inset matches the slider's thumb radius so endpoint labels align
with the actual track endpoints, not the bounding box. */
padding-inline: calc(var(--_slider-thumb-size, 1rem) / 2);
box-sizing: border-box;
}
.time-picker-scale-tick {
position: absolute;
inset-block: 0;
transform: translateX(-50%);
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 1px;
color: var(--color-content-muted);
font-family: var(--font-mono, var(--font-ui));
font-size: var(--font-size-xs);
line-height: 1;
pointer-events: none;
}
.time-picker-scale-mark {
display: block;
inline-size: 1px;
block-size: 6px;
background: var(--color-border-default);
}
.time-picker-scale-label {
display: block;
}
/* Below variant: marks rise to the top of the scale row so the label is
underneath the track. */
.time-picker-scale-below .time-picker-scale-tick {
flex-direction: column-reverse;
}
/* Track-relative offset for scale-above: the position inside the slider
accounts for the thumb's overhang, so the inner padding above keeps the
tick aligned with where the thumb actually centers. */
/* ── Slider thumb / range reinforcement inside the clock ──────────────────
The base slider recipe sets thumb color from --color-surface-default
over the primary range; inside the popover this can blend with the
popover surface. Bump emphasis. */
[data-time-picker-clock] [data-slider-thumb] {
border-color: var(--_time-field-accent-border);
box-shadow:
var(--slider-thumb-shadow),
0 0 0 2px var(--color-surface-default);
}
[data-time-picker-clock] [data-slider-thumb]:focus-visible {
box-shadow:
var(--slider-thumb-focus-shadow),
0 0 0 2px var(--color-surface-default);
}
[data-time-picker-clock] [data-slider-range] {
background: var(--_time-field-accent-border);
}
/* ── Footer (Clear / Cancel / Close action row) ──────────────────────────── */
[data-time-picker-footer] {

Loading…
Cancel
Save

Powered by TurnKey Linux.