Builds on the existing soma TimePicker slider-based clock (HourSlider / MinuteSlider / SecondSlider + DayPeriodToggle). Adds the canonical picker shell: - mode='inline' | 'modal' on the Provider, propagated to popover.modal - Provider helpers commit() / cancel() / clear() with watch(open) snapshot for cancel-revert - Footer composition (TimePicker.Footer / Clear / Cancel / Close as pure-composition Eidos parts; no *Button root props) - New TimePicker.Clock soma component that carries the data-time-picker-clock contract attr, wraps the slider stack - Eidos shell: Provider visual root, Trigger (clock icon), Content (popover content), Clock, Footer + action parts, slider/toggle pass-throughs Renews the time-picker morfo to declare Provider / Trigger / Clock / DayPeriodToggle / DayPeriodItem / Footer / Clear / Cancel / Close, plus all 6 commit/emerge events (open + close-commit/cancel/dismiss/ dismiss-outside + commit-clear). apg=dialog-modal. Demo at /uix/components/time-picker exercises all controls, Footer composition switches, soma + eidos code snippets, morfo / sema / recipe / a11y tabs. README documents the picker contract. 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) <noreply@anthropic.com>active-uix
parent
ff279ae828
commit
391de6e04c
@ -0,0 +1,107 @@
|
||||
# Eidos TimePicker
|
||||
|
||||
Composite popover combining a segmented `TimeField` input + a slider-based
|
||||
`Clock` view (Hour / Minute / Second sliders + AM/PM toggle). Soma owns
|
||||
state coordination (single value + open + helpers `commit / cancel /
|
||||
clear`); Eidos adds visual treatment, the popover content shell and the
|
||||
canonical Footer parts.
|
||||
|
||||
## Baseline
|
||||
|
||||
Air no tenía un `TimePicker` equivalente en la rama anterior. La
|
||||
referencia local real es la composición Soma `time-picker` + `time-field`
|
||||
+ `popover` + `slider`, que ya implementa la edición segmentada y la
|
||||
selección por sliders, y la receta Eidos sólo envuelve esa superficie con
|
||||
tamaños, variante visual y paleta de intents. Referencias externas:
|
||||
React Aria TimeField+Picker, Ark UI Time Picker, Chakra Time Picker.
|
||||
|
||||
## Superficie
|
||||
|
||||
```svelte
|
||||
<TimePicker bind:value bind:open placeholder={defaultTime} mode="inline">
|
||||
<TimePicker.Input>
|
||||
{#snippet children({ segments })}
|
||||
{#each segments as { part, value }, i (`${part}-${i}`)}
|
||||
<TimePicker.Segment {part}>{value}</TimePicker.Segment>
|
||||
{/each}
|
||||
<TimePicker.Trigger />
|
||||
{/snippet}
|
||||
</TimePicker.Input>
|
||||
|
||||
<TimePicker.Portal>
|
||||
<TimePicker.Content>
|
||||
<TimePicker.Clock>
|
||||
<TimePicker.HourSlider />
|
||||
<TimePicker.MinuteSlider />
|
||||
<TimePicker.SecondSlider />
|
||||
<TimePicker.DayPeriodToggle />
|
||||
|
||||
<TimePicker.Footer>
|
||||
<TimePicker.Clear />
|
||||
<TimePicker.Cancel />
|
||||
<TimePicker.Close />
|
||||
</TimePicker.Footer>
|
||||
</TimePicker.Clock>
|
||||
</TimePicker.Content>
|
||||
</TimePicker.Portal>
|
||||
</TimePicker>
|
||||
```
|
||||
|
||||
Partes públicas: `Trigger`, `Content`, `Clock`, `Label`, `Input`,
|
||||
`Segment`, `HiddenInput`, `HourSlider`, `MinuteSlider`, `SecondSlider`,
|
||||
`DayPeriodToggle`, `Footer`, `Clear`, `Cancel`, `Close`, `Anchor`,
|
||||
`Overlay`, `Arrow`, `Portal`.
|
||||
|
||||
## Comparativa
|
||||
|
||||
| Capacidad | Soma/Eidos | React Aria | Ark UI | Chakra |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Input segmentado | Sí | Sí | Sí | Sí |
|
||||
| Popover Clock | Sí (sliders + AM/PM) | Sí (column-wheel) | Sí (columns) | Sí (columns) |
|
||||
| Granularidad hour / minute / second | Sí | Sí | Sí | Sí |
|
||||
| 12h / 24h cycle | Sí | Sí | Sí | Sí |
|
||||
| `mode='inline' \| 'modal'` | Sí | No | No | No |
|
||||
| Footer = composición pura | Sí | No | No | No |
|
||||
| `commit / cancel / clear` helpers | Sí | Manual | Manual | Manual |
|
||||
| Segmentos readonly | Sí | No | No | Sí |
|
||||
| Validación custom | Sí | Sí | Sí | Sí |
|
||||
| Variante / tamaño / paleta visual | Sí, Eidos | CSS externo | CSS externo | CSS externo |
|
||||
|
||||
## Decisiones
|
||||
|
||||
- **Mode = inline | modal** (P-2). `modal` pasa `modal=true` al
|
||||
`PopoverProvider` underlying → outside-click + Escape NO cierran; el
|
||||
consumer debe incluir `<Close>` o `<Cancel>` en el footer.
|
||||
- **Provider helpers** (P-1). `commit()` cierra preservando valor;
|
||||
`cancel()` revierte al snapshot OPEN-edge y cierra; `clear()` deja
|
||||
`value=undefined` sin cerrar. Snapshot capturado vía `watch(open)`
|
||||
cuando `open: false → true`.
|
||||
- **Shell composition** (P-3). `Provider > Input + Trigger > Portal >
|
||||
Content > Clock > Sliders + DayPeriodToggle + Footer`. Mismo esqueleto
|
||||
que `date-picker`.
|
||||
- **Footer = composición pura** (N-7). Sin props `clearButton/...` en el
|
||||
root. Cada part Footer renderiza si se compone; omitir la part la
|
||||
oculta.
|
||||
- **Sliders en lugar de columnas-wheel**. Son accesibles y nativos
|
||||
(rueda + drag + arrows + spinbutton ARIA). La UX wheel-scroll de
|
||||
Chakra/Ark es más vistosa pero requiere más código para la misma
|
||||
precisión. Pendiente walk visual para evaluar trade-off.
|
||||
- **HourCycle** drives los sliders: 12h limita hour [1..12] + muestra
|
||||
DayPeriodToggle; 24h limita hour [0..23] sin toggle.
|
||||
|
||||
## Gaps
|
||||
|
||||
| Gap | Disposición | Detalle |
|
||||
| --- | --- | --- |
|
||||
| Column-wheel scroll view (estilo iOS / Chakra) | **diferir** | Sliders cubren el caso. Wheel requiere intersección-observer + snap CSS. Hacer cuando aparezca demanda real. |
|
||||
| Tests browser-level (Playwright) | **implementar** | Cobertura visual + interacciones (slider drag, AM/PM toggle, modal commit/cancel/clear). En la pasada conjunta de tests. |
|
||||
| Sema sounds/haptic specific al picker | **diferir** | Reutiliza popover sema por ahora; añadir si emerge necesidad perceptiva específica. |
|
||||
|
||||
## Referencias
|
||||
|
||||
- Soma TimePicker: `src/uix/soma/components/time-picker/README.md`
|
||||
- Soma TimeField: `src/uix/soma/components/time-field/README.md`
|
||||
- React Aria TimeField+Picker: https://react-spectrum.adobe.com/react-aria/TimeField.html
|
||||
- Ark UI Time Picker: https://ark-ui.com/docs/components/time-picker
|
||||
- Chakra UI Time Picker: https://chakra-ui.com/docs/components/time-picker
|
||||
- WAI-ARIA APG Dialog (modal): https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
||||
@ -0,0 +1,19 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { TimePickerColor, TimePickerSize, TimePickerVariant } from './types';
|
||||
|
||||
const TIME_PICKER_VISUAL_CONTEXT = Symbol('uix.eidos.time-picker.visual');
|
||||
|
||||
export interface TimePickerVisualContext {
|
||||
getSize(): TimePickerSize;
|
||||
getVariant(): TimePickerVariant;
|
||||
getColor(): TimePickerColor;
|
||||
}
|
||||
|
||||
export function setTimePickerVisualContext(ctx: TimePickerVisualContext): TimePickerVisualContext {
|
||||
setContext(TIME_PICKER_VISUAL_CONTEXT, ctx);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function getTimePickerVisualContext(): TimePickerVisualContext | undefined {
|
||||
return getContext<TimePickerVisualContext | undefined>(TIME_PICKER_VISUAL_CONTEXT);
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
import TimePickerComponent from './time-picker.svelte'
|
||||
import Trigger from './time-picker-trigger.svelte'
|
||||
import Content from './time-picker-content.svelte'
|
||||
import Clock from './time-picker-clock.svelte'
|
||||
import Footer from './time-picker-footer.svelte'
|
||||
import Clear from './time-picker-clear.svelte'
|
||||
import Cancel from './time-picker-cancel.svelte'
|
||||
import Close from './time-picker-close.svelte'
|
||||
import HourSlider from './time-picker-hour-slider.svelte'
|
||||
import MinuteSlider from './time-picker-minute-slider.svelte'
|
||||
import SecondSlider from './time-picker-second-slider.svelte'
|
||||
import DayPeriodToggle from './time-picker-day-period-toggle.svelte'
|
||||
import Label from '../time-field/time-field-label.svelte'
|
||||
import Input from '../time-field/time-field-input.svelte'
|
||||
import Segment from '../time-field/time-field-segment.svelte'
|
||||
import HiddenInput from '../time-field/time-field-hidden-input.svelte'
|
||||
import Anchor from '../popover/popover-anchor.svelte'
|
||||
import Overlay from '../popover/popover-overlay.svelte'
|
||||
import Arrow from '../popover/popover-arrow.svelte'
|
||||
import { Portal } from '$soma/components/internal'
|
||||
|
||||
type TimePickerNamespace = typeof TimePickerComponent & {
|
||||
Trigger: typeof Trigger
|
||||
Portal: typeof Portal
|
||||
Anchor: typeof Anchor
|
||||
Overlay: typeof Overlay
|
||||
Content: typeof Content
|
||||
Arrow: typeof Arrow
|
||||
Clock: typeof Clock
|
||||
Footer: typeof Footer
|
||||
Clear: typeof Clear
|
||||
Cancel: typeof Cancel
|
||||
Close: typeof Close
|
||||
HourSlider: typeof HourSlider
|
||||
MinuteSlider: typeof MinuteSlider
|
||||
SecondSlider: typeof SecondSlider
|
||||
DayPeriodToggle: typeof DayPeriodToggle
|
||||
Label: typeof Label
|
||||
Input: typeof Input
|
||||
Segment: typeof Segment
|
||||
HiddenInput: typeof HiddenInput
|
||||
}
|
||||
|
||||
const TimePicker = TimePickerComponent as TimePickerNamespace
|
||||
TimePicker.Trigger = Trigger
|
||||
TimePicker.Portal = Portal
|
||||
TimePicker.Anchor = Anchor
|
||||
TimePicker.Overlay = Overlay
|
||||
TimePicker.Content = Content
|
||||
TimePicker.Arrow = Arrow
|
||||
TimePicker.Clock = Clock
|
||||
TimePicker.Footer = Footer
|
||||
TimePicker.Clear = Clear
|
||||
TimePicker.Cancel = Cancel
|
||||
TimePicker.Close = Close
|
||||
TimePicker.HourSlider = HourSlider
|
||||
TimePicker.MinuteSlider = MinuteSlider
|
||||
TimePicker.SecondSlider = SecondSlider
|
||||
TimePicker.DayPeriodToggle = DayPeriodToggle
|
||||
TimePicker.Label = Label
|
||||
TimePicker.Input = Input
|
||||
TimePicker.Segment = Segment
|
||||
TimePicker.HiddenInput = HiddenInput
|
||||
|
||||
export { TimePicker }
|
||||
|
||||
export default TimePicker
|
||||
|
||||
export type {
|
||||
TimePickerProps,
|
||||
TimePickerTriggerProps as TriggerProps,
|
||||
TimePickerContentProps as ContentProps,
|
||||
TimePickerClockProps as ClockProps,
|
||||
TimePickerFooterProps as FooterProps,
|
||||
TimePickerClearProps as ClearProps,
|
||||
TimePickerCancelProps as CancelProps,
|
||||
TimePickerCloseProps as CloseProps,
|
||||
TimePickerLabelProps as LabelProps,
|
||||
TimePickerInputProps as InputProps,
|
||||
TimePickerSegmentProps as SegmentProps,
|
||||
TimePickerHiddenInputProps as HiddenInputProps,
|
||||
TimePickerAnchorProps as AnchorProps,
|
||||
TimePickerOverlayProps as OverlayProps,
|
||||
TimePickerArrowProps as ArrowProps,
|
||||
TimePickerSliderProps as SliderProps,
|
||||
InputSnippetProps,
|
||||
TimePickerSize,
|
||||
TimePickerVariant,
|
||||
TimePickerColor
|
||||
} from './types'
|
||||
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { TimePickerProvider } from '$soma/components/time-picker';
|
||||
import { getActiveUix } from '$active-uix';
|
||||
import type { TimePickerCancelProps } from './types';
|
||||
|
||||
// Cancel action — reverts to the value snapshot taken on open and
|
||||
// closes the popover. Renders unconditionally — visibility is the
|
||||
// consumer's responsibility (include the part to show it).
|
||||
const { children, onclick, ...rest }: TimePickerCancelProps = $props();
|
||||
|
||||
const uix = getActiveUix();
|
||||
const provider = TimePickerProvider.require();
|
||||
|
||||
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
|
||||
provider.cancel();
|
||||
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-time-picker-cancel
|
||||
data-action="cancel"
|
||||
aria-label={uix.langs.ts('#?components.time-picker.cancel|Cancel')}
|
||||
onclick={handleClick}
|
||||
{...rest}
|
||||
>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
{uix.langs.ts('#?components.time-picker.cancel|Cancel')}
|
||||
{/if}
|
||||
</button>
|
||||
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { TimePickerProvider } from '$soma/components/time-picker';
|
||||
import { getActiveUix } from '$active-uix';
|
||||
import type { TimePickerClearProps } from './types';
|
||||
|
||||
// Clear action — resets the picker's value (undefined) and keeps the
|
||||
// popover open. Renders unconditionally — visibility is the
|
||||
// consumer's responsibility (include the part to show it).
|
||||
const { children, onclick, ...rest }: TimePickerClearProps = $props();
|
||||
|
||||
const uix = getActiveUix();
|
||||
const provider = TimePickerProvider.require();
|
||||
|
||||
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
|
||||
provider.clear();
|
||||
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-time-picker-clear
|
||||
data-action="clear"
|
||||
aria-label={uix.langs.ts('#?components.time-picker.clear|Clear time')}
|
||||
onclick={handleClick}
|
||||
{...rest}
|
||||
>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
{uix.langs.ts('#?components.time-picker.clear|Clear time')}
|
||||
{/if}
|
||||
</button>
|
||||
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as TimePicker from '$soma/components/time-picker';
|
||||
import { getTimePickerVisualContext } from './context.svelte';
|
||||
import type { TimePickerClockProps } from './types';
|
||||
|
||||
let { size, variant, color, children, ...rest }: TimePickerClockProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const visual = getTimePickerVisualContext();
|
||||
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize() ?? 'md', 'md'));
|
||||
const resolvedVariant = $derived(variant ?? visual?.getVariant() ?? 'ghost');
|
||||
const resolvedColor = $derived(color ?? visual?.getColor() ?? 'primary');
|
||||
</script>
|
||||
|
||||
<TimePicker.Clock
|
||||
{...rest}
|
||||
data-size={resolvedSize}
|
||||
data-variant={resolvedVariant}
|
||||
data-color={resolvedColor}
|
||||
>
|
||||
{@render children?.()}
|
||||
</TimePicker.Clock>
|
||||
@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { TimePickerProvider } from '$soma/components/time-picker';
|
||||
import { getActiveUix } from '$active-uix';
|
||||
import type { TimePickerCloseProps } from './types';
|
||||
|
||||
// Close / Save action — commits the current value and closes the
|
||||
// popover. Renders unconditionally — visibility is the consumer's
|
||||
// responsibility (include the part to show it).
|
||||
//
|
||||
// In `mode='modal'` the consumer MUST include this part (or another
|
||||
// route out) because outside-click and Escape are intentionally
|
||||
// blocked — otherwise the picker has no exit affordance.
|
||||
const { children, onclick, ...rest }: TimePickerCloseProps = $props();
|
||||
|
||||
const uix = getActiveUix();
|
||||
const provider = TimePickerProvider.require();
|
||||
|
||||
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
|
||||
provider.commit();
|
||||
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-time-picker-close
|
||||
data-action="close"
|
||||
aria-label={uix.langs.ts('#?components.time-picker.close|Close')}
|
||||
onclick={handleClick}
|
||||
{...rest}
|
||||
>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
{uix.langs.ts('#?components.time-picker.close|Close')}
|
||||
{/if}
|
||||
</button>
|
||||
@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import { composeInlineStyle } from '$uix/eidos/lib/style';
|
||||
import PopoverContent from '../popover/popover-content.svelte';
|
||||
import { getTimePickerVisualContext } from './context.svelte';
|
||||
import type { TimePickerContentProps } from './types';
|
||||
|
||||
let {
|
||||
size,
|
||||
align = 'end',
|
||||
sideOffset = 10,
|
||||
collisionPadding = 12,
|
||||
width,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
style,
|
||||
children: bodyContent,
|
||||
...rest
|
||||
}: TimePickerContentProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const visual = getTimePickerVisualContext();
|
||||
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize() ?? 'md', 'md'));
|
||||
|
||||
function serializeLength(value: string | number | undefined): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (typeof value === 'number') return Number.isFinite(value) ? `${value}px` : undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function dimensionVar(name: string, value: string | number | undefined): string | undefined {
|
||||
const serialized = serializeLength(value);
|
||||
return serialized ? `${name}: ${serialized};` : undefined;
|
||||
}
|
||||
|
||||
function resolveLength(value: typeof width, fallback: string): string | number | undefined {
|
||||
const resolved = eidos.resolve(value);
|
||||
if (typeof resolved === 'string' || typeof resolved === 'number') return resolved;
|
||||
if (typeof value === 'string' || typeof value === 'number') return value;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const dimensionsStyle = $derived(
|
||||
[
|
||||
dimensionVar('--_popover-content-width-override', resolveLength(width, 'max-content')),
|
||||
dimensionVar('--_popover-content-min-width-override', resolveLength(minWidth, 'max-content')),
|
||||
dimensionVar(
|
||||
'--_popover-content-max-width-override',
|
||||
resolveLength(maxWidth, 'calc(100vw - var(--space-4))')
|
||||
)
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
);
|
||||
|
||||
const composedStyle = $derived(composeInlineStyle(style, dimensionsStyle));
|
||||
</script>
|
||||
|
||||
<PopoverContent
|
||||
{...rest}
|
||||
{align}
|
||||
{sideOffset}
|
||||
{collisionPadding}
|
||||
size={resolvedSize}
|
||||
style={composedStyle}
|
||||
>
|
||||
{#snippet children(snippetProps)}
|
||||
{@render bodyContent?.(snippetProps)}
|
||||
{/snippet}
|
||||
</PopoverContent>
|
||||
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import * as TimePicker from '$soma/components/time-picker';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
children?: Snippet;
|
||||
class?: string;
|
||||
style?: string;
|
||||
};
|
||||
|
||||
let { children, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<TimePicker.DayPeriodToggle {...rest}>
|
||||
{@render children?.()}
|
||||
</TimePicker.DayPeriodToggle>
|
||||
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { TimePickerProvider } from '$soma/components/time-picker';
|
||||
import type { TimePickerFooterProps } from './types';
|
||||
|
||||
// Footer container for the picker clock. Pure composition: render
|
||||
// whatever you want inside it (Clear / Cancel / Close — or arbitrary
|
||||
// children). Visibility is the consumer's responsibility — include
|
||||
// the part to show it, omit it to hide it.
|
||||
//
|
||||
// Carries `data-mode` so the CSS can react to the picker's mode
|
||||
// without re-reading provider context.
|
||||
const { children, ...rest }: TimePickerFooterProps = $props();
|
||||
|
||||
const provider = TimePickerProvider.require();
|
||||
const mode = $derived(provider.opts.mode.current);
|
||||
</script>
|
||||
|
||||
<div data-time-picker-footer data-mode={mode} {...rest}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as TimePicker from '$soma/components/time-picker';
|
||||
import type { TimePickerSliderProps } from './types';
|
||||
|
||||
let { children, ...rest }: TimePickerSliderProps = $props();
|
||||
</script>
|
||||
|
||||
<TimePicker.HourSlider {...rest}>
|
||||
{@render children?.()}
|
||||
</TimePicker.HourSlider>
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as TimePicker from '$soma/components/time-picker';
|
||||
import type { TimePickerSliderProps } from './types';
|
||||
|
||||
let { children, ...rest }: TimePickerSliderProps = $props();
|
||||
</script>
|
||||
|
||||
<TimePicker.MinuteSlider {...rest}>
|
||||
{@render children?.()}
|
||||
</TimePicker.MinuteSlider>
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as TimePicker from '$soma/components/time-picker';
|
||||
import type { TimePickerSliderProps } from './types';
|
||||
|
||||
let { children, ...rest }: TimePickerSliderProps = $props();
|
||||
</script>
|
||||
|
||||
<TimePicker.SecondSlider {...rest}>
|
||||
{@render children?.()}
|
||||
</TimePicker.SecondSlider>
|
||||
@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as TimePicker from '$soma/components/time-picker';
|
||||
import ClockIcon from '../icon/lib/clock.svelte';
|
||||
import { getTimePickerVisualContext } from './context.svelte';
|
||||
import type { TimePickerTriggerProps } from './types';
|
||||
|
||||
let { size, variant, color, children, ...rest }: TimePickerTriggerProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const visual = getTimePickerVisualContext();
|
||||
const resolvedSize = $derived(eidos.resolve(size ?? visual?.getSize() ?? 'md', 'md'));
|
||||
const resolvedVariant = $derived(variant ?? visual?.getVariant() ?? 'surface');
|
||||
const resolvedColor = $derived(color ?? visual?.getColor() ?? 'primary');
|
||||
</script>
|
||||
|
||||
<TimePicker.Trigger
|
||||
{...rest}
|
||||
data-size={resolvedSize}
|
||||
data-variant={resolvedVariant}
|
||||
data-color={resolvedColor}
|
||||
>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<ClockIcon size="1em" decorative />
|
||||
{/if}
|
||||
</TimePicker.Trigger>
|
||||
@ -0,0 +1,237 @@
|
||||
/* TimePicker — visual layer.
|
||||
*
|
||||
* Reuses time-field tokens for the field-shaped portion (Input + Segments)
|
||||
* and the calendar/popover surface tokens for the popover Clock body.
|
||||
*/
|
||||
|
||||
[data-time-picker] {
|
||||
--_time-field-height: var(--time-field-height-md);
|
||||
--_time-field-px: var(--time-field-px-md);
|
||||
--_time-field-gap: var(--time-field-gap-md);
|
||||
--_time-field-segment-px: var(--time-field-segment-px-md);
|
||||
--_time-field-font-size: var(--time-field-font-size-md);
|
||||
--_time-field-accent-border: var(--color-primary-border);
|
||||
--_time-field-accent-track: var(--color-primary-track);
|
||||
--_time-field-accent-text: var(--color-primary-text);
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: var(--time-field-stack-gap);
|
||||
inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
color: var(--time-field-color);
|
||||
font-family: var(--time-field-font-family);
|
||||
font-size: var(--_time-field-font-size);
|
||||
line-height: var(--time-field-line-height);
|
||||
}
|
||||
|
||||
[data-time-picker][data-size='sm'] {
|
||||
--_time-field-height: var(--time-field-height-sm);
|
||||
--_time-field-px: var(--time-field-px-sm);
|
||||
--_time-field-gap: var(--time-field-gap-sm);
|
||||
--_time-field-segment-px: var(--time-field-segment-px-sm);
|
||||
--_time-field-font-size: var(--time-field-font-size-sm);
|
||||
}
|
||||
|
||||
[data-time-picker][data-size='lg'] {
|
||||
--_time-field-height: var(--time-field-height-lg);
|
||||
--_time-field-px: var(--time-field-px-lg);
|
||||
--_time-field-gap: var(--time-field-gap-lg);
|
||||
--_time-field-segment-px: var(--time-field-segment-px-lg);
|
||||
--_time-field-font-size: var(--time-field-font-size-lg);
|
||||
}
|
||||
|
||||
[data-time-picker][data-color='secondary'] {
|
||||
--_time-field-accent-border: var(--color-secondary-border);
|
||||
--_time-field-accent-track: var(--color-secondary-track);
|
||||
--_time-field-accent-text: var(--color-secondary-text);
|
||||
}
|
||||
[data-time-picker][data-color='neutral'] {
|
||||
--_time-field-accent-border: var(--color-neutral-border);
|
||||
--_time-field-accent-track: var(--color-neutral-track);
|
||||
--_time-field-accent-text: var(--color-neutral-text);
|
||||
}
|
||||
[data-time-picker][data-color='affirm'] {
|
||||
--_time-field-accent-border: var(--color-affirm-border);
|
||||
--_time-field-accent-track: var(--color-affirm-track);
|
||||
--_time-field-accent-text: var(--color-affirm-text);
|
||||
}
|
||||
[data-time-picker][data-color='fulfill'] {
|
||||
--_time-field-accent-border: var(--color-fulfill-border);
|
||||
--_time-field-accent-track: var(--color-fulfill-track);
|
||||
--_time-field-accent-text: var(--color-fulfill-text);
|
||||
}
|
||||
[data-time-picker][data-color='risk'] {
|
||||
--_time-field-accent-border: var(--color-risk-border);
|
||||
--_time-field-accent-track: var(--color-risk-track);
|
||||
--_time-field-accent-text: var(--color-risk-text);
|
||||
}
|
||||
[data-time-picker][data-color='threat'] {
|
||||
--_time-field-accent-border: var(--color-threat-border);
|
||||
--_time-field-accent-track: var(--color-threat-track);
|
||||
--_time-field-accent-text: var(--color-threat-text);
|
||||
}
|
||||
[data-time-picker][data-color='loss'] {
|
||||
--_time-field-accent-border: var(--color-loss-border);
|
||||
--_time-field-accent-track: var(--color-loss-track);
|
||||
--_time-field-accent-text: var(--color-loss-text);
|
||||
}
|
||||
|
||||
/* ── Trigger ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
[data-time-picker-trigger] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
inline-size: var(--_time-field-height);
|
||||
block-size: var(--_time-field-height);
|
||||
padding: 0;
|
||||
border: var(--time-field-border-width) solid var(--time-field-border);
|
||||
border-radius: var(--time-field-radius);
|
||||
background: var(--time-field-bg);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--time-field-transition-duration) var(--time-field-transition-ease),
|
||||
border-color var(--time-field-transition-duration) var(--time-field-transition-ease),
|
||||
box-shadow var(--time-field-transition-duration) var(--time-field-transition-ease),
|
||||
color var(--time-field-transition-duration) var(--time-field-transition-ease);
|
||||
}
|
||||
|
||||
[data-time-picker-trigger]:hover {
|
||||
border-color: var(--_time-field-accent-border);
|
||||
background: var(--_time-field-accent-track);
|
||||
color: var(--_time-field-accent-text);
|
||||
}
|
||||
|
||||
[data-time-picker-trigger]:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--_time-field-accent-border);
|
||||
box-shadow: var(--time-field-focus-shadow);
|
||||
}
|
||||
|
||||
[data-time-picker-trigger][data-disabled],
|
||||
[data-time-picker-trigger]:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: var(--time-field-disabled-opacity);
|
||||
}
|
||||
|
||||
/* Embedded trigger inside the input — flush affordance like date-picker. */
|
||||
[data-time-field-input] [data-time-picker-trigger] {
|
||||
flex: 0 0 auto;
|
||||
inline-size: var(--time-field-segment-height);
|
||||
block-size: var(--time-field-segment-height);
|
||||
margin-inline-start: auto;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--time-field-segment-radius);
|
||||
background: transparent;
|
||||
color: var(--time-field-segment-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-time-field-input] [data-time-picker-trigger]:hover {
|
||||
border: none;
|
||||
background: var(--_time-field-accent-track);
|
||||
color: var(--_time-field-accent-text);
|
||||
}
|
||||
|
||||
[data-time-field-input] [data-time-picker-trigger]:focus-visible {
|
||||
outline: none;
|
||||
border: none;
|
||||
background: var(--_time-field-accent-track);
|
||||
color: var(--_time-field-accent-text);
|
||||
box-shadow: var(--time-field-segment-focus-shadow);
|
||||
}
|
||||
|
||||
/* ── Popover content / Clock surface ─────────────────────────────────────── */
|
||||
|
||||
[data-popover-content]:has([data-time-picker-clock]) {
|
||||
--_popover-content-width-override: max-content;
|
||||
--_popover-content-min-width-override: max-content;
|
||||
--_popover-content-max-width-override: calc(100vw - var(--space-4));
|
||||
}
|
||||
|
||||
[data-time-picker-clock] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
min-inline-size: 18rem;
|
||||
color: var(--color-content-primary);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--leading-ui);
|
||||
}
|
||||
|
||||
[data-time-picker-clock] [data-slider] {
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
/* ── Footer (Clear / Cancel / Close action row) ──────────────────────────── */
|
||||
|
||||
[data-time-picker-footer] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding-block-start: var(--space-3);
|
||||
margin-block-start: var(--space-3);
|
||||
border-block-start: var(--border-width) solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
[data-time-picker-clear],
|
||||
[data-time-picker-cancel] {
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
[data-time-picker-clear],
|
||||
[data-time-picker-cancel],
|
||||
[data-time-picker-close] {
|
||||
inline-size: max-content;
|
||||
min-block-size: var(--control-height-sm);
|
||||
padding-inline: var(--space-3);
|
||||
border-radius: var(--radius-md);
|
||||
border: var(--border-width) solid transparent;
|
||||
background: transparent;
|
||||
color: var(--color-content-primary);
|
||||
font: inherit;
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--leading-ui);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--duration-fast) var(--ease-default),
|
||||
border-color var(--duration-fast) var(--ease-default),
|
||||
color var(--duration-fast) var(--ease-default);
|
||||
}
|
||||
|
||||
[data-time-picker-clear]:hover,
|
||||
[data-time-picker-cancel]:hover {
|
||||
background: var(--color-surface-overlay);
|
||||
}
|
||||
|
||||
[data-time-picker-close] {
|
||||
border-color: var(--color-primary-border);
|
||||
background: var(--color-primary-solid);
|
||||
color: var(--color-content-on-solid);
|
||||
}
|
||||
|
||||
[data-time-picker-close]:hover {
|
||||
background: var(--color-primary-solid-hover);
|
||||
}
|
||||
|
||||
[data-time-picker-clear]:focus-visible,
|
||||
[data-time-picker-cancel]:focus-visible,
|
||||
[data-time-picker-close]:focus-visible {
|
||||
outline: var(--focus-ring-width) solid var(--color-primary-border);
|
||||
outline-offset: var(--focus-ring-offset);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-time-picker-trigger],
|
||||
[data-time-picker-clear],
|
||||
[data-time-picker-cancel],
|
||||
[data-time-picker-close] {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as TimePicker from '$soma/components/time-picker';
|
||||
import { setTimePickerVisualContext } from './context.svelte';
|
||||
import type { TimePickerProps } from './types';
|
||||
|
||||
let {
|
||||
size = 'md',
|
||||
variant = 'surface',
|
||||
color = 'primary',
|
||||
value = $bindable(),
|
||||
placeholder = $bindable(),
|
||||
open = $bindable(false),
|
||||
children,
|
||||
...rest
|
||||
}: TimePickerProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const resolvedSize = $derived(eidos.resolve(size, 'md'));
|
||||
|
||||
setTimePickerVisualContext({
|
||||
getSize: () => resolvedSize,
|
||||
getVariant: () => variant,
|
||||
getColor: () => color
|
||||
});
|
||||
</script>
|
||||
|
||||
<TimePicker.Provider
|
||||
{...rest}
|
||||
bind:value
|
||||
bind:placeholder
|
||||
bind:open
|
||||
data-size={resolvedSize}
|
||||
data-variant={variant}
|
||||
data-color={color}
|
||||
>
|
||||
{@render children?.()}
|
||||
</TimePicker.Provider>
|
||||
@ -0,0 +1,92 @@
|
||||
import type {
|
||||
ContentProps as SomaTimePickerContentProps,
|
||||
InputProps as SomaTimePickerInputProps,
|
||||
InputSnippetProps as TimePickerInputSnippetProps,
|
||||
LabelProps as SomaTimePickerLabelProps,
|
||||
ProviderProps,
|
||||
SegmentProps as SomaTimePickerSegmentProps,
|
||||
SliderProps as SomaTimePickerSliderProps,
|
||||
SliderThumbProps as SomaTimePickerSliderThumbProps,
|
||||
SliderRangeProps as SomaTimePickerSliderRangeProps,
|
||||
SliderTickProps as SomaTimePickerSliderTickProps,
|
||||
TriggerProps as SomaTimePickerTriggerProps
|
||||
} from '$soma/components/time-picker'
|
||||
import type {
|
||||
PopoverAnchorProps,
|
||||
PopoverContentProps,
|
||||
PopoverOverlayProps,
|
||||
PopoverArrowProps,
|
||||
PopoverCloseProps
|
||||
} from '../popover/types'
|
||||
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types'
|
||||
import type { Snippet } from 'svelte'
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements'
|
||||
|
||||
export type TimePickerSize = Extract<Size, 'sm' | 'md' | 'lg'>
|
||||
export type TimePickerVariant = ControlVariant
|
||||
export type TimePickerColor = ColorRole
|
||||
|
||||
export type TimePickerProps = ProviderProps & {
|
||||
/** Visual size for the input segments, trigger and clock. @default 'md' */
|
||||
size?: ResponsiveProp<TimePickerSize>
|
||||
/** Visual treatment shared by the field and clock. @default 'surface' */
|
||||
variant?: TimePickerVariant
|
||||
/** Intent palette used for focus and selected affordances. @default 'primary' */
|
||||
color?: TimePickerColor
|
||||
}
|
||||
|
||||
export type TimePickerTriggerProps = SomaTimePickerTriggerProps & {
|
||||
size?: ResponsiveProp<TimePickerSize>
|
||||
variant?: TimePickerVariant
|
||||
color?: TimePickerColor
|
||||
}
|
||||
|
||||
export type TimePickerContentProps = Omit<PopoverContentProps, 'size'> & {
|
||||
size?: ResponsiveProp<TimePickerSize>
|
||||
}
|
||||
|
||||
export type TimePickerClockProps = {
|
||||
id?: string
|
||||
size?: ResponsiveProp<TimePickerSize>
|
||||
variant?: TimePickerVariant
|
||||
color?: TimePickerColor
|
||||
class?: string
|
||||
style?: string
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
export type TimePickerFooterProps = {
|
||||
id?: string
|
||||
class?: string
|
||||
style?: string
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
export type TimePickerClearProps = HTMLButtonAttributes & {
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
export type TimePickerCancelProps = HTMLButtonAttributes & {
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
export type TimePickerCloseProps = HTMLButtonAttributes & {
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
export type TimePickerLabelProps = SomaTimePickerLabelProps
|
||||
export type TimePickerInputProps = SomaTimePickerInputProps
|
||||
export type TimePickerSegmentProps = SomaTimePickerSegmentProps
|
||||
export type TimePickerHiddenInputProps = Record<string, never>
|
||||
|
||||
export type TimePickerAnchorProps = PopoverAnchorProps
|
||||
export type TimePickerOverlayProps = PopoverOverlayProps
|
||||
export type TimePickerArrowProps = PopoverArrowProps
|
||||
export type TimePickerPopoverCloseProps = PopoverCloseProps
|
||||
|
||||
export type TimePickerSliderProps = SomaTimePickerSliderProps
|
||||
export type TimePickerSliderThumbProps = SomaTimePickerSliderThumbProps
|
||||
export type TimePickerSliderRangeProps = SomaTimePickerSliderRangeProps
|
||||
export type TimePickerSliderTickProps = SomaTimePickerSliderTickProps
|
||||
|
||||
export type { TimePickerInputSnippetProps, TimePickerInputSnippetProps as InputSnippetProps }
|
||||
@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from '../../../props';
|
||||
import { createId } from '../../../id';
|
||||
import { TimePickerProvider, timePickerAttrs } from '../time-picker-provider.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
interface ClockProps extends HTMLAttributes<HTMLDivElement> {
|
||||
id?: string;
|
||||
ref?: HTMLElement | null;
|
||||
children?: Snippet;
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
}
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid, 'time-picker-clock'),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: ClockProps = $props();
|
||||
|
||||
const provider = TimePickerProvider.require();
|
||||
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, {
|
||||
id,
|
||||
role: 'dialog' as const,
|
||||
[timePickerAttrs.clock]: '',
|
||||
'data-state': provider.opts.open.current ? 'open' : 'closed'
|
||||
} as Record<string, unknown>)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps} bind:this={ref}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
@ -0,0 +1,666 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
TimePicker,
|
||||
type TimePickerColor,
|
||||
type TimePickerSize,
|
||||
type TimePickerVariant,
|
||||
type InputSnippetProps
|
||||
} from '$uix/eidos/components/time-picker';
|
||||
import { compileMorfo } from '$uix/morfo';
|
||||
import { timePickerMorfo } from '@/uix/morfo/components/time-picker';
|
||||
import { timeFieldMorfo } from '@/uix/morfo/components/time-field';
|
||||
import { popoverMorfo } from '@/uix/morfo/components/popover';
|
||||
import { getActiveUix } from '$active-uix';
|
||||
import {
|
||||
Time,
|
||||
type TimeValue,
|
||||
type TimeGranularity,
|
||||
type HourCycle,
|
||||
type EditableTimeSegmentPart
|
||||
} from '$libs/days';
|
||||
|
||||
const uix = getActiveUix();
|
||||
|
||||
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
||||
type TraceEntry = { event: string; family: string; intent?: string; at: number };
|
||||
|
||||
const startTime: TimeValue = new Time(9, 30, 0);
|
||||
const endTime: TimeValue = new Time(17, 45, 0);
|
||||
|
||||
let tab = $state<Tab>('live');
|
||||
let trace = $state<TraceEntry[]>([]);
|
||||
let stageRef = $state<HTMLElement | null>(null);
|
||||
let value = $state<TimeValue | undefined>(startTime);
|
||||
let placeholder = $state<TimeValue>(startTime);
|
||||
let open = $state(false);
|
||||
let mode = $state<'inline' | 'modal'>('inline');
|
||||
let required = $state(true);
|
||||
let disabled = $state(false);
|
||||
let readonly = $state(false);
|
||||
let lockSeconds = $state(false);
|
||||
let granularity = $state<TimeGranularity>('minute');
|
||||
let hourCycle = $state<HourCycle>(24);
|
||||
let bounds = $state(false);
|
||||
let locale = $state('en-US');
|
||||
let size = $state<TimePickerSize>('md');
|
||||
let variant = $state<TimePickerVariant>('surface');
|
||||
let color = $state<TimePickerColor>('primary');
|
||||
|
||||
// Footer composition toggles
|
||||
let showFooter = $state(true);
|
||||
let showClear = $state(true);
|
||||
let showCancel = $state(true);
|
||||
let showClose = $state(true);
|
||||
|
||||
const compiled = compileMorfo(timePickerMorfo);
|
||||
const timeFieldCompiled = compileMorfo(timeFieldMorfo);
|
||||
const popoverCompiled = compileMorfo(popoverMorfo);
|
||||
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
||||
const events = $derived([...compiled.actions.byName.values()]);
|
||||
const fieldEvents = $derived([...timeFieldCompiled.actions.byName.values()]);
|
||||
const popoverEvents = $derived([...popoverCompiled.actions.byName.values()]);
|
||||
const eventRows = $derived([
|
||||
...events.map((action) => ({ owner: 'TimePicker', action })),
|
||||
...fieldEvents.map((action) => ({ owner: 'TimeField', action })),
|
||||
...popoverEvents.map((action) => ({ owner: 'Popover', action }))
|
||||
]);
|
||||
const readonlySegments = $derived<EditableTimeSegmentPart[]>(lockSeconds ? ['second'] : []);
|
||||
const valueLabel = $derived(value?.toString() ?? 'empty');
|
||||
|
||||
const minValue: TimeValue = new Time(8, 0, 0);
|
||||
const maxValue: TimeValue = new Time(20, 0, 0);
|
||||
|
||||
function sameValue(a: TimeValue | undefined, b: TimeValue): boolean {
|
||||
return a?.toString() === b.toString();
|
||||
}
|
||||
|
||||
function setDemoValue(kind: 'start' | 'end' | 'empty'): void {
|
||||
if (kind === 'empty') {
|
||||
value = undefined;
|
||||
return;
|
||||
}
|
||||
value = kind === 'start' ? startTime : endTime;
|
||||
}
|
||||
|
||||
function fmtTime(at: number): string {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 2
|
||||
}).format(at);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const el = stageRef;
|
||||
if (!el) return;
|
||||
const obs = new MutationObserver((mutations) => {
|
||||
for (const m of mutations) {
|
||||
if (m.attributeName !== 'data-event') continue;
|
||||
const target = m.target as Element;
|
||||
const ev = target.getAttribute('data-event');
|
||||
if (!ev) continue;
|
||||
trace = [
|
||||
{
|
||||
event: ev,
|
||||
family: target.getAttribute('data-event-family') ?? '—',
|
||||
intent: target.getAttribute('data-event-intent') ?? undefined,
|
||||
at: Date.now()
|
||||
},
|
||||
...trace
|
||||
].slice(0, 6);
|
||||
}
|
||||
});
|
||||
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
|
||||
return () => obs.disconnect();
|
||||
});
|
||||
|
||||
const somaSnippet = $derived(
|
||||
[
|
||||
"<script lang='ts'>",
|
||||
" import * as TimePicker from '$soma/components/time-picker';",
|
||||
" import { Time, type TimeValue } from '$libs/days';",
|
||||
'',
|
||||
' let value = $state<TimeValue | undefined>(new Time(9, 30, 0));',
|
||||
' let open = $state(false);',
|
||||
'</' + 'script>',
|
||||
'',
|
||||
'<TimePicker.Provider',
|
||||
' bind:value bind:open',
|
||||
mode !== 'inline' && ` mode="${mode}"`,
|
||||
granularity !== 'minute' && ` granularity="${granularity}"`,
|
||||
hourCycle !== 24 && ` hourCycle={${hourCycle}}`,
|
||||
required && ' required',
|
||||
disabled && ' disabled',
|
||||
readonly && ' readonly',
|
||||
'>',
|
||||
' <TimePicker.Input>',
|
||||
' {#snippet children({ segments })}',
|
||||
' {#each segments as { part, value }, i (`${part}-${i}`)}',
|
||||
' <TimePicker.Segment {part}>{value}</TimePicker.Segment>',
|
||||
' {/each}',
|
||||
' <TimePicker.Trigger>Open</TimePicker.Trigger>',
|
||||
' {/snippet}',
|
||||
' </TimePicker.Input>',
|
||||
' <TimePicker.Content>',
|
||||
' <TimePicker.HourSlider />',
|
||||
' <TimePicker.MinuteSlider />',
|
||||
' </TimePicker.Content>',
|
||||
'</TimePicker.Provider>'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
);
|
||||
|
||||
const eidosSnippet = $derived(
|
||||
[
|
||||
"<script lang='ts'>",
|
||||
" import { TimePicker } from '$uix/eidos/components/time-picker';",
|
||||
" import { Time, type TimeValue } from '$libs/days';",
|
||||
'',
|
||||
' let value = $state<TimeValue | undefined>(new Time(9, 30, 0));',
|
||||
' let open = $state(false);',
|
||||
'</' + 'script>',
|
||||
'',
|
||||
'<TimePicker',
|
||||
' bind:value bind:open',
|
||||
size !== 'md' && ` size="${size}"`,
|
||||
variant !== 'surface' && ` variant="${variant}"`,
|
||||
color !== 'primary' && ` color="${color}"`,
|
||||
mode !== 'inline' && ` mode="${mode}"`,
|
||||
granularity !== 'minute' && ` granularity="${granularity}"`,
|
||||
hourCycle !== 24 && ` hourCycle={${hourCycle}}`,
|
||||
required && ' required',
|
||||
disabled && ' disabled',
|
||||
readonly && ' readonly',
|
||||
lockSeconds && " readonlySegments={['second']}",
|
||||
bounds && ' minValue={new Time(8, 0, 0)} maxValue={new Time(20, 0, 0)}',
|
||||
'>',
|
||||
' <TimePicker.Input>',
|
||||
' {#snippet children({ segments })}',
|
||||
' {#each segments as { part, value }, i (`${part}-${i}`)}',
|
||||
' <TimePicker.Segment {part}>{value}</TimePicker.Segment>',
|
||||
' {/each}',
|
||||
' <TimePicker.Trigger />',
|
||||
' {/snippet}',
|
||||
' </TimePicker.Input>',
|
||||
' <TimePicker.Portal>',
|
||||
' <TimePicker.Content>',
|
||||
' <TimePicker.Clock>',
|
||||
' <TimePicker.HourSlider />',
|
||||
' <TimePicker.MinuteSlider />',
|
||||
granularity === 'second' && ' <TimePicker.SecondSlider />',
|
||||
hourCycle === 12 && ' <TimePicker.DayPeriodToggle />',
|
||||
showFooter && (showClear || showCancel || showClose) && ' <TimePicker.Footer>',
|
||||
showFooter && showClear && ' <TimePicker.Clear />',
|
||||
showFooter && showCancel && ' <TimePicker.Cancel />',
|
||||
showFooter && showClose && ' <TimePicker.Close />',
|
||||
showFooter && (showClear || showCancel || showClose) && ' </TimePicker.Footer>',
|
||||
' </TimePicker.Clock>',
|
||||
' </TimePicker.Content>',
|
||||
' </TimePicker.Portal>',
|
||||
'</TimePicker>'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
);
|
||||
</script>
|
||||
|
||||
<div data-uix-canvas-inner>
|
||||
<header>
|
||||
<div data-uix-eyebrow>Forms · TimePicker</div>
|
||||
<h1 data-uix-page-title>TimePicker</h1>
|
||||
<p data-uix-page-lede>
|
||||
Segmented time input + popover clock. Soma owns value coordination, sliders + AM/PM,
|
||||
validation and form submission; Eidos adds size, variant, intent color, the popover
|
||||
shell and the canonical Footer parts.
|
||||
</p>
|
||||
<div data-uix-page-meta>
|
||||
<span data-uix-meta-pill
|
||||
><span data-uix-meta-key>parts</span>{compiled.parts.order.length}</span
|
||||
>
|
||||
<span data-uix-meta-pill><span data-uix-meta-key>events</span>{eventRows.length}</span>
|
||||
<span data-uix-meta-pill><span data-uix-meta-key>variants</span>3</span>
|
||||
<span data-uix-meta-pill><span data-uix-meta-key>sizes</span>3</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div data-uix-stage>
|
||||
<div data-uix-stage-area bind:this={stageRef}>
|
||||
<div style="inline-size: min(28rem, 100%);">
|
||||
<TimePicker
|
||||
bind:value
|
||||
bind:placeholder
|
||||
bind:open
|
||||
{mode}
|
||||
{required}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{readonlySegments}
|
||||
{granularity}
|
||||
{hourCycle}
|
||||
{locale}
|
||||
{size}
|
||||
{variant}
|
||||
{color}
|
||||
minValue={bounds ? minValue : undefined}
|
||||
maxValue={bounds ? maxValue : undefined}
|
||||
>
|
||||
<TimePicker.Label>Start time</TimePicker.Label>
|
||||
<TimePicker.Input name="start-time" aria-label="Start time">
|
||||
{#snippet children({ segments }: InputSnippetProps)}
|
||||
{#each segments as { part, value }, i (`${part}-${i}`)}
|
||||
<TimePicker.Segment {part}>{value}</TimePicker.Segment>
|
||||
{/each}
|
||||
<TimePicker.Trigger />
|
||||
{/snippet}
|
||||
</TimePicker.Input>
|
||||
|
||||
<TimePicker.Portal>
|
||||
<TimePicker.Content>
|
||||
<TimePicker.Clock>
|
||||
<TimePicker.HourSlider />
|
||||
<TimePicker.MinuteSlider />
|
||||
{#if granularity === 'second'}
|
||||
<TimePicker.SecondSlider />
|
||||
{/if}
|
||||
{#if hourCycle === 12}
|
||||
<TimePicker.DayPeriodToggle />
|
||||
{/if}
|
||||
|
||||
{#if showFooter && (showClear || showCancel || showClose)}
|
||||
<TimePicker.Footer>
|
||||
{#if showClear}<TimePicker.Clear />{/if}
|
||||
{#if showCancel}<TimePicker.Cancel />{/if}
|
||||
{#if showClose}<TimePicker.Close />{/if}
|
||||
</TimePicker.Footer>
|
||||
{/if}
|
||||
</TimePicker.Clock>
|
||||
</TimePicker.Content>
|
||||
</TimePicker.Portal>
|
||||
</TimePicker>
|
||||
</div>
|
||||
</div>
|
||||
<div data-uix-stage-trace>
|
||||
<span data-uix-stage-trace-key>trace</span>
|
||||
{#if trace.length === 0}
|
||||
<span>edit, open the clock or play an event to see sema stamps</span>
|
||||
{:else}
|
||||
{#each trace.slice(0, 3) as entry}
|
||||
<span
|
||||
><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent
|
||||
? ' · ' + entry.intent
|
||||
: ''}</span
|
||||
>
|
||||
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
<span style="margin-inline-start: auto;">
|
||||
<span data-uix-stage-trace-key>value</span>
|
||||
{valueLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-uix-tabs role="tablist">
|
||||
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
|
||||
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>API</button>
|
||||
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
|
||||
<span data-uix-layer-badge="morfo">morfo</span>
|
||||
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
|
||||
</button>
|
||||
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
|
||||
<span data-uix-layer-badge="sema">sema</span>
|
||||
<span data-uix-tab-count>{eventRows.length}</span>
|
||||
</button>
|
||||
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}
|
||||
>Recipe</button
|
||||
>
|
||||
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
|
||||
</div>
|
||||
|
||||
{#if tab === 'live'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Controls</h2>
|
||||
<p data-uix-section-desc>
|
||||
<span data-uix-layer-badge="soma">soma</span> owns value, segment editing,
|
||||
validation and the open state. <span data-uix-layer-badge="eidos">eidos</span> owns
|
||||
visual treatment.
|
||||
</p>
|
||||
|
||||
<div data-uix-subsection-head>
|
||||
<span data-uix-layer-badge="soma">soma</span> props · value + behaviour
|
||||
</div>
|
||||
<div data-uix-controls>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>value</span>
|
||||
<span data-uix-chips role="group">
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={sameValue(value, startTime)}
|
||||
onclick={() => setDemoValue('start')}>start</button
|
||||
>
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={sameValue(value, endTime)}
|
||||
onclick={() => setDemoValue('end')}>end</button
|
||||
>
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={value === undefined}
|
||||
onclick={() => setDemoValue('empty')}>empty</button
|
||||
>
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>mode</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each ['inline', 'modal'] as v}
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={mode === v}
|
||||
onclick={() => (mode = v as 'inline' | 'modal')}>{v}</button
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>granularity</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each ['hour', 'minute', 'second'] as v}
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={granularity === v}
|
||||
onclick={() => (granularity = v as TimeGranularity)}>{v}</button
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>hour cycle</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each [12, 24] as v}
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={hourCycle === v}
|
||||
onclick={() => (hourCycle = v as HourCycle)}>{v}</button
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>locale</span>
|
||||
<select bind:value={locale}>
|
||||
<option value="en-US">en-US</option>
|
||||
<option value="es-ES">es-ES</option>
|
||||
<option value="fr-FR">fr-FR</option>
|
||||
</select>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>states</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={required} />
|
||||
<span data-uix-switch-label>required</span>
|
||||
</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={disabled} />
|
||||
<span data-uix-switch-label>disabled</span>
|
||||
</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={readonly} />
|
||||
<span data-uix-switch-label>readonly</span>
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>constraints</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={bounds} />
|
||||
<span data-uix-switch-label>min 08:00 · max 20:00</span>
|
||||
</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={lockSeconds} />
|
||||
<span data-uix-switch-label>seconds readonly</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-uix-subsection-head>Footer composition</div>
|
||||
<p data-uix-section-desc>
|
||||
Each switch decides whether the part is <em>included</em> in the markup. The parts
|
||||
never read these flags — omitting the part hides it, including it always renders.
|
||||
</p>
|
||||
<div data-uix-controls>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>visibility</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={showFooter} />
|
||||
<span data-uix-switch-label>render footer</span>
|
||||
</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={showClear} disabled={!showFooter} />
|
||||
<span data-uix-switch-label>clear</span>
|
||||
</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={showCancel} disabled={!showFooter} />
|
||||
<span data-uix-switch-label>cancel</span>
|
||||
</span>
|
||||
<span data-uix-switch>
|
||||
<input type="checkbox" bind:checked={showClose} disabled={!showFooter} />
|
||||
<span data-uix-switch-label>close</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-uix-subsection-head>
|
||||
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
|
||||
</div>
|
||||
<div data-uix-controls>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>size</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each ['sm', 'md', 'lg'] as v}
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={size === v}
|
||||
onclick={() => (size = v as TimePickerSize)}>{v}</button
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>variant</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each ['surface', 'outline', 'ghost'] as v}
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={variant === v}
|
||||
onclick={() => (variant = v as TimePickerVariant)}>{v}</button
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
<label data-uix-control>
|
||||
<span data-uix-control-label>color</span>
|
||||
<span data-uix-chips role="radiogroup">
|
||||
{#each ['primary', 'secondary', 'neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as v}
|
||||
<button
|
||||
data-uix-chip
|
||||
data-active={color === v}
|
||||
onclick={() => (color = v as TimePickerColor)}>{v}</button
|
||||
>
|
||||
{/each}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div data-uix-code>
|
||||
<div data-uix-code-head>
|
||||
<span data-uix-layer-badge="soma">soma</span>
|
||||
<span>headless · provider + sliders</span>
|
||||
<span data-uix-code-lang>svelte</span>
|
||||
</div>
|
||||
<pre><code>{somaSnippet}</code></pre>
|
||||
</div>
|
||||
<div data-uix-code style="margin-top: var(--uix-space-3);">
|
||||
<div data-uix-code-head>
|
||||
<span data-uix-layer-badge="eidos">eidos</span>
|
||||
<span>visual · root + attached parts</span>
|
||||
<span data-uix-code-lang>svelte</span>
|
||||
</div>
|
||||
<pre><code>{eidosSnippet}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'api'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>API + reference comparison</h2>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead>
|
||||
<tr><th>Feature</th><th>React Aria</th><th>Ark</th><th>Chakra</th><th>UIX</th><th>Decision</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td class="name">Segmented input</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>Soma owns editable segments.</td></tr>
|
||||
<tr><td class="name">Popover clock</td><td>column wheel</td><td>columns</td><td>columns</td><td>sliders + AM/PM</td><td>Sliders cover the case; wheel-scroll deferred.</td></tr>
|
||||
<tr><td class="name">Granularity hour/min/sec</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>Drives which slider renders.</td></tr>
|
||||
<tr><td class="name">12 / 24 hour cycle</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>12h adds dayPeriod toggle.</td></tr>
|
||||
<tr><td class="name">mode='inline' | 'modal'</td><td>no</td><td>no</td><td>no</td><td>yes</td><td>Same contract as date-picker.</td></tr>
|
||||
<tr><td class="name">Footer = composition</td><td>no</td><td>no</td><td>no</td><td>yes</td><td>No *Button root props — pure composition.</td></tr>
|
||||
<tr><td class="name">commit/cancel/clear helpers</td><td>manual</td><td>manual</td><td>manual</td><td>yes</td><td>Wired by Footer parts.</td></tr>
|
||||
<tr><td class="name">Readonly segments</td><td>no</td><td>no</td><td>yes</td><td>yes</td><td>Per-segment lock without disabling input.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Prop</th><th>Owner</th><th>Default</th><th>Notes</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="name">value / placeholder</td><td>soma</td><td class="default">—</td><td>Bindable TimeValue.</td></tr>
|
||||
<tr><td class="name">open</td><td>soma</td><td class="default">false</td><td>Bindable popover state.</td></tr>
|
||||
<tr><td class="name">mode</td><td>soma</td><td class="default">inline</td><td><code>inline</code> closes on outside-click; <code>modal</code> requires footer action.</td></tr>
|
||||
<tr><td class="name">minValue / maxValue</td><td>soma</td><td class="default">—</td><td>Validation bounds.</td></tr>
|
||||
<tr><td class="name">granularity</td><td>soma</td><td class="default">minute</td><td>Hour / minute / second.</td></tr>
|
||||
<tr><td class="name">hourCycle</td><td>soma</td><td class="default">locale</td><td>12h adds dayPeriod segment + toggle in clock.</td></tr>
|
||||
<tr><td class="name">readonlySegments</td><td>soma</td><td class="default">[]</td><td>Lock individual segments / sliders.</td></tr>
|
||||
<tr><td class="name">size / variant / color</td><td>eidos</td><td class="default">md / surface / primary</td><td>Visual-only data attrs.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'morfo'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Morfo contract</h2>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Optional</th></tr></thead>
|
||||
<tbody>
|
||||
{#each partsList as part}
|
||||
<tr>
|
||||
<td class="name">{part.kebab}</td>
|
||||
<td><code data-uix-part-marker>[{part.marker}]</code></td>
|
||||
<td class="type"><{part.defaultElement}></td>
|
||||
<td class="type">{part.role ?? '—'}</td>
|
||||
<td class="default">{part.optional ? 'yes' : 'no'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'sema'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>
|
||||
<span data-uix-layer-badge="sema">sema</span> · events + perceptual signature
|
||||
</h2>
|
||||
<p data-uix-section-desc>
|
||||
TimePicker declares open / dismiss / commit / clear events on its own surface. The
|
||||
underlying TimeField and Popover contribute their own commit/emerge events too.
|
||||
</p>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Owner</th><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Target</th><th>Play</th></tr></thead>
|
||||
<tbody>
|
||||
{#each eventRows as row}
|
||||
<tr>
|
||||
<td class="default">{row.owner}</td>
|
||||
<td class="name">{row.action.name}</td>
|
||||
<td class="type">{row.action.semantic.family}</td>
|
||||
<td>{row.action.semantic.verb ?? '—'}</td>
|
||||
<td>{row.action.semantic.sequence ?? 'pre'}</td>
|
||||
<td>{row.action.target}</td>
|
||||
<td>
|
||||
<button
|
||||
data-uix-play
|
||||
onclick={() => {
|
||||
const selector =
|
||||
row.owner === 'TimePicker'
|
||||
? '[data-time-picker]'
|
||||
: row.owner === 'TimeField'
|
||||
? '[data-time-field]'
|
||||
: '[data-popover-content]';
|
||||
const target = stageRef?.querySelector(
|
||||
selector
|
||||
) as HTMLElement | null;
|
||||
if (!target) return;
|
||||
void uix.events?.emit({
|
||||
name: row.action.name,
|
||||
family: row.action.semantic.family,
|
||||
target
|
||||
});
|
||||
}}>▶ play</button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'recipe'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Eidos recipe</h2>
|
||||
<p data-uix-section-desc>
|
||||
Selectors live in <code>src/uix/eidos/components/time-picker/time-picker.css</code> and
|
||||
reuse the time-field tokens for the field portion. The clock body is a flex column
|
||||
wrapping slider parts (which carry their own slider recipe).
|
||||
</p>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="name"><code>[data-time-picker]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Root visual scope + size/variant/color tokens.</td></tr>
|
||||
<tr><td class="name"><code>[data-time-picker-trigger]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Open-clock button. Has embedded variant inside the input.</td></tr>
|
||||
<tr><td class="name"><code>[data-time-picker-clock][data-state]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Popover dialog surface. Animates from open/closed state.</td></tr>
|
||||
<tr><td class="name"><code>[data-time-picker-footer][data-mode]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Footer action row. Reflects mode for CSS targeting.</td></tr>
|
||||
<tr><td class="name"><code>[data-time-picker-clear], [data-time-picker-cancel], [data-time-picker-close]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Footer actions. Close has solid emphasis (Save semantics).</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if tab === 'a11y'}
|
||||
<section data-uix-section>
|
||||
<h2 data-uix-section-title>Accessibility</h2>
|
||||
<div data-uix-table-wrap>
|
||||
<table data-uix-table>
|
||||
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="name">Dialog</td><td>Clock exposes <code>role="dialog"</code> with an accessible name.</td></tr>
|
||||
<tr><td class="name">Trigger</td><td><code>aria-haspopup="dialog"</code> + <code>aria-expanded</code> + <code>aria-controls</code>.</td></tr>
|
||||
<tr><td class="name">Segments</td><td>Editable segments expose <code>role="spinbutton"</code> with min/max/value.</td></tr>
|
||||
<tr><td class="name">Sliders</td><td>Native slider semantics — keyboard arrow/page/home/end.</td></tr>
|
||||
<tr><td class="name">Modal</td><td><code>mode='modal'</code> blocks outside-click + Escape; consumer must wire Close/Cancel.</td></tr>
|
||||
<tr><td class="name">Form</td><td>Hidden input submits the ISO time string when <code>name</code> is set on Input.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
Loading…
Reference in new issue