@human-kit/svelte-components 1.0.0-alpha.3 → 1.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/FOCUS_STATE_CONTRACT.md +12 -0
  2. package/dist/calendar/body-cell/README.md +15 -0
  3. package/dist/calendar/grid/README.md +13 -0
  4. package/dist/calendar/grid-body/README.md +13 -0
  5. package/dist/calendar/grid-header/README.md +13 -0
  6. package/dist/calendar/header-cell/README.md +14 -0
  7. package/dist/calendar/heading/README.md +13 -0
  8. package/dist/calendar/root/README.md +24 -0
  9. package/dist/calendar/trigger-next/README.md +14 -0
  10. package/dist/calendar/trigger-previous/README.md +14 -0
  11. package/dist/clock/README.md +75 -0
  12. package/dist/clock/axis/README.md +24 -0
  13. package/dist/clock/axis/clock-axis.svelte +37 -0
  14. package/dist/clock/axis/clock-axis.svelte.d.ts +8 -0
  15. package/dist/clock/hooks/use-wheel-scroll.svelte.d.ts +16 -0
  16. package/dist/clock/hooks/use-wheel-scroll.svelte.js +336 -0
  17. package/dist/clock/index.d.ts +10 -0
  18. package/dist/clock/index.js +10 -0
  19. package/dist/clock/index.parts.d.ts +4 -0
  20. package/dist/clock/index.parts.js +4 -0
  21. package/dist/clock/root/README.md +38 -0
  22. package/dist/clock/root/clock-root-test.svelte +62 -0
  23. package/dist/clock/root/clock-root-test.svelte.d.ts +14 -0
  24. package/dist/clock/root/clock-root.svelte +329 -0
  25. package/dist/clock/root/clock-root.svelte.d.ts +25 -0
  26. package/dist/clock/root/context.d.ts +22 -0
  27. package/dist/clock/root/context.js +15 -0
  28. package/dist/clock/root/resolve-visible-columns.d.ts +7 -0
  29. package/dist/clock/root/resolve-visible-columns.js +16 -0
  30. package/dist/clock/root/time-utils.d.ts +48 -0
  31. package/dist/clock/root/time-utils.js +314 -0
  32. package/dist/clock/root/wheel-options.d.ts +17 -0
  33. package/dist/clock/root/wheel-options.js +63 -0
  34. package/dist/clock/wheel-column/README.md +25 -0
  35. package/dist/clock/wheel-column/clock-wheel-column-bindable-test.svelte +16 -0
  36. package/dist/clock/wheel-column/clock-wheel-column-bindable-test.svelte.d.ts +3 -0
  37. package/dist/clock/wheel-column/clock-wheel-column-custom-snippet-test.svelte +29 -0
  38. package/dist/clock/wheel-column/clock-wheel-column-custom-snippet-test.svelte.d.ts +6 -0
  39. package/dist/clock/wheel-column/clock-wheel-column-default-height-test.svelte +11 -0
  40. package/dist/clock/wheel-column/clock-wheel-column-default-height-test.svelte.d.ts +3 -0
  41. package/dist/clock/wheel-column/clock-wheel-column-test.svelte +38 -0
  42. package/dist/clock/wheel-column/clock-wheel-column-test.svelte.d.ts +12 -0
  43. package/dist/clock/wheel-column/clock-wheel-column-tp-test.svelte +38 -0
  44. package/dist/clock/wheel-column/clock-wheel-column-tp-test.svelte.d.ts +12 -0
  45. package/dist/clock/wheel-column/clock-wheel-column-untagged-snippet-test.svelte +29 -0
  46. package/dist/clock/wheel-column/clock-wheel-column-untagged-snippet-test.svelte.d.ts +6 -0
  47. package/dist/clock/wheel-column/clock-wheel-column.svelte +499 -0
  48. package/dist/clock/wheel-column/clock-wheel-column.svelte.d.ts +17 -0
  49. package/dist/clock/wheel-item/README.md +17 -0
  50. package/dist/clock/wheel-item/clock-wheel-item.svelte +49 -0
  51. package/dist/clock/wheel-item/clock-wheel-item.svelte.d.ts +17 -0
  52. package/dist/datepicker/TODO.md +2 -2
  53. package/dist/datepicker/calendar/README.md +19 -0
  54. package/dist/datepicker/input/README.md +15 -0
  55. package/dist/datepicker/popover/README.md +20 -0
  56. package/dist/datepicker/root/README.md +38 -0
  57. package/dist/datepicker/segment/README.md +14 -0
  58. package/dist/datepicker/trigger/README.md +14 -0
  59. package/dist/index.d.ts +5 -0
  60. package/dist/index.js +5 -0
  61. package/dist/primitives/focus-trap.js +11 -12
  62. package/dist/primitives/input-modality.js +10 -1
  63. package/dist/timepicker/IMPLEMENTATION_PLAN.md +254 -0
  64. package/dist/timepicker/README.md +97 -0
  65. package/dist/timepicker/TODO.md +86 -0
  66. package/dist/timepicker/clock/README.md +14 -0
  67. package/dist/timepicker/clock/time-picker-clock-test.svelte +45 -0
  68. package/dist/timepicker/clock/time-picker-clock-test.svelte.d.ts +11 -0
  69. package/dist/timepicker/clock/time-picker-clock.svelte +65 -0
  70. package/dist/timepicker/clock/time-picker-clock.svelte.d.ts +10 -0
  71. package/dist/timepicker/index.d.ts +14 -0
  72. package/dist/timepicker/index.js +14 -0
  73. package/dist/timepicker/index.parts.d.ts +8 -0
  74. package/dist/timepicker/index.parts.js +8 -0
  75. package/dist/timepicker/input/README.md +15 -0
  76. package/dist/timepicker/input/time-picker-input-forwarding-test.svelte +40 -0
  77. package/dist/timepicker/input/time-picker-input-forwarding-test.svelte.d.ts +3 -0
  78. package/dist/timepicker/input/time-picker-input.svelte +109 -0
  79. package/dist/timepicker/input/time-picker-input.svelte.d.ts +11 -0
  80. package/dist/timepicker/internal/strict-props.d.ts +4 -0
  81. package/dist/timepicker/internal/strict-props.js +51 -0
  82. package/dist/timepicker/popover/README.md +20 -0
  83. package/dist/timepicker/popover/time-picker-popover-unsafe-props-test.svelte +22 -0
  84. package/dist/timepicker/popover/time-picker-popover-unsafe-props-test.svelte.d.ts +3 -0
  85. package/dist/timepicker/popover/time-picker-popover.svelte +89 -0
  86. package/dist/timepicker/popover/time-picker-popover.svelte.d.ts +7 -0
  87. package/dist/timepicker/root/README.md +42 -0
  88. package/dist/timepicker/root/context.d.ts +51 -0
  89. package/dist/timepicker/root/context.js +15 -0
  90. package/dist/timepicker/root/time-picker-12h-test.svelte +22 -0
  91. package/dist/timepicker/root/time-picker-12h-test.svelte.d.ts +3 -0
  92. package/dist/timepicker/root/time-picker-bindable-test.svelte +25 -0
  93. package/dist/timepicker/root/time-picker-bindable-test.svelte.d.ts +3 -0
  94. package/dist/timepicker/root/time-picker-empty-test.svelte +20 -0
  95. package/dist/timepicker/root/time-picker-empty-test.svelte.d.ts +3 -0
  96. package/dist/timepicker/root/time-picker-root.svelte +625 -0
  97. package/dist/timepicker/root/time-picker-root.svelte.d.ts +28 -0
  98. package/dist/timepicker/root/time-picker-test.svelte +72 -0
  99. package/dist/timepicker/root/time-picker-test.svelte.d.ts +15 -0
  100. package/dist/timepicker/root/time-utils.d.ts +1 -0
  101. package/dist/timepicker/root/time-utils.js +3 -0
  102. package/dist/timepicker/segment/README.md +14 -0
  103. package/dist/timepicker/segment/time-picker-segment.svelte +365 -0
  104. package/dist/timepicker/segment/time-picker-segment.svelte.d.ts +9 -0
  105. package/dist/timepicker/trigger/README.md +14 -0
  106. package/dist/timepicker/trigger/time-picker-trigger-forwarding-test.svelte +35 -0
  107. package/dist/timepicker/trigger/time-picker-trigger-forwarding-test.svelte.d.ts +3 -0
  108. package/dist/timepicker/trigger/time-picker-trigger.svelte +122 -0
  109. package/dist/timepicker/trigger/time-picker-trigger.svelte.d.ts +9 -0
  110. package/package.json +11 -1
@@ -0,0 +1,10 @@
1
+ export * as Clock from './index.parts.ts';
2
+ export { default as ClockRoot } from './root/clock-root.svelte';
3
+ export { default as ClockAxis } from './axis/clock-axis.svelte';
4
+ export { default as ClockWheelColumn } from './wheel-column/clock-wheel-column.svelte';
5
+ export { default as ClockWheelItem } from './wheel-item/clock-wheel-item.svelte';
6
+ export { getClockContext, setClockContext, useClockContext, type ClockContext, type ClockEditableSegmentType } from './root/context.ts';
7
+ export { type TimePickerGranularity as ClockGranularity, type TimePickerHourCycle as ClockHourCycle, type TimePickerTimeValue as ClockTimeValue } from './root/time-utils';
8
+ export { type ClockColumnInfo } from './root/resolve-visible-columns';
9
+ import * as ClockParts from './index.parts.ts';
10
+ export default ClockParts;
@@ -0,0 +1,10 @@
1
+ export * as Clock from './index.parts.ts';
2
+ export { default as ClockRoot } from './root/clock-root.svelte';
3
+ export { default as ClockAxis } from './axis/clock-axis.svelte';
4
+ export { default as ClockWheelColumn } from './wheel-column/clock-wheel-column.svelte';
5
+ export { default as ClockWheelItem } from './wheel-item/clock-wheel-item.svelte';
6
+ export { getClockContext, setClockContext, useClockContext } from './root/context.ts';
7
+ export {} from './root/time-utils';
8
+ export {} from './root/resolve-visible-columns';
9
+ import * as ClockParts from './index.parts.ts';
10
+ export default ClockParts;
@@ -0,0 +1,4 @@
1
+ export { default as Root } from './root/clock-root.svelte';
2
+ export { default as Axis } from './axis/clock-axis.svelte';
3
+ export { default as WheelColumn } from './wheel-column/clock-wheel-column.svelte';
4
+ export { default as WheelItem } from './wheel-item/clock-wheel-item.svelte';
@@ -0,0 +1,4 @@
1
+ export { default as Root } from './root/clock-root.svelte';
2
+ export { default as Axis } from './axis/clock-axis.svelte';
3
+ export { default as WheelColumn } from './wheel-column/clock-wheel-column.svelte';
4
+ export { default as WheelItem } from './wheel-item/clock-wheel-item.svelte';
@@ -0,0 +1,38 @@
1
+ # Clock Root
2
+
3
+ ## API reference
4
+
5
+ ### Clock.Root
6
+
7
+ Name: `Clock.Root`
8
+ Description: Standalone wheel-based time state container that resolves visible columns and publishes selection context for child parts.
9
+
10
+ | Prop | Type | Default | Description |
11
+ | -------------- | --------------------------------- | ----------- | ---------------------------------------------------------- |
12
+ | `value` | `string \| null` | `bindable` | Controlled time value (`HH:mm` or `HH:mm:ss`). |
13
+ | `defaultValue` | `string \| null` | `undefined` | Initial value for uncontrolled usage. |
14
+ | `onChange` | `(value: string \| null) => void` | `undefined` | Called when a committed wheel selection changes the value. |
15
+ | `hourCycle` | `12 \| 24` | `locale` | Hour cycle used for rendering and validation. |
16
+ | `granularity` | `'hour' \| 'minute' \| 'second'` | `'minute'` | Controls visible wheel columns and emitted precision. |
17
+ | `hourStep` | `number` | `1` | Hour increment used by wheel and keyboard navigation. |
18
+ | `minuteStep` | `number` | `1` | Minute increment used by wheel and keyboard navigation. |
19
+ | `secondStep` | `number` | `1` | Second increment used by wheel and keyboard navigation. |
20
+ | `minValue` | `string` | `undefined` | Optional lower bound for selectable values. |
21
+ | `maxValue` | `string` | `undefined` | Optional upper bound for selectable values. |
22
+ | `isDisabled` | `boolean` | `false` | Disables wheel interaction and value updates. |
23
+ | `column` | `Snippet<[ClockColumnInfo]>` | `undefined` | Optional custom renderer for each resolved column. |
24
+ | `children` | `Snippet` | `undefined` | Optional additional content rendered inside the root. |
25
+ | `class` | `string` | `''` | CSS class names for the root element. |
26
+ | `aria-label` | `string` | `undefined` | Accessible label for the root group. |
27
+
28
+ ### Context utilities
29
+
30
+ Name: `setClockContext` / `getClockContext` / `useClockContext`
31
+ Description: Context helpers used by `Clock.Axis`, `Clock.WheelColumn`, and other clock parts.
32
+
33
+ | Prop | Type | Default | Description |
34
+ | ----------------- | --------------------------------- | ------- | -------------------------------------------------------- |
35
+ | `setClockContext` | `(ctx: ClockContext) => void` | `-` | Publishes clock context from root. |
36
+ | `getClockContext` | `() => ClockContext \| undefined` | `-` | Reads clock context when available. |
37
+ | `useClockContext` | `() => ClockContext` | `-` | Reads context and throws outside `Clock.Root`. |
38
+ | `ClockContext` | `type` | `-` | Shared contract for state, labels, and wheel operations. |
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import * as Clock from '../index.parts';
4
+ import type { ClockColumnInfo } from './resolve-visible-columns';
5
+
6
+ type Props = {
7
+ defaultValue?: string | null;
8
+ hourCycle?: 12 | 24;
9
+ granularity?: 'hour' | 'minute' | 'second';
10
+ minValue?: string;
11
+ maxValue?: string;
12
+ isDisabled?: boolean;
13
+ useSnippet?: boolean;
14
+ showAxis?: boolean;
15
+ axisHeight?: number;
16
+ };
17
+
18
+ let {
19
+ defaultValue = '14:30',
20
+ hourCycle = 24,
21
+ granularity = 'minute',
22
+ minValue,
23
+ maxValue,
24
+ isDisabled = false,
25
+ useSnippet = false,
26
+ showAxis = false,
27
+ axisHeight
28
+ }: Props = $props();
29
+
30
+ let value = $state<string | null>(untrack(() => defaultValue ?? null));
31
+ </script>
32
+
33
+ {#if useSnippet}
34
+ <Clock.Root
35
+ bind:value
36
+ {hourCycle}
37
+ {granularity}
38
+ {minValue}
39
+ {maxValue}
40
+ {isDisabled}
41
+ class="flex gap-2"
42
+ >
43
+ {#snippet column(col: ClockColumnInfo)}
44
+ <div data-testid="clock-column" data-type={col.type}>{col.label}</div>
45
+ {/snippet}
46
+ {#if showAxis}
47
+ <Clock.Axis data-testid="clock-axis" height={axisHeight} />
48
+ {/if}
49
+ </Clock.Root>
50
+ {:else}
51
+ <Clock.Root
52
+ bind:value
53
+ {hourCycle}
54
+ {granularity}
55
+ {minValue}
56
+ {maxValue}
57
+ {isDisabled}
58
+ class="flex gap-2"
59
+ />
60
+ {/if}
61
+
62
+ <p data-testid="clock-value">{value}</p>
@@ -0,0 +1,14 @@
1
+ type Props = {
2
+ defaultValue?: string | null;
3
+ hourCycle?: 12 | 24;
4
+ granularity?: 'hour' | 'minute' | 'second';
5
+ minValue?: string;
6
+ maxValue?: string;
7
+ isDisabled?: boolean;
8
+ useSnippet?: boolean;
9
+ showAxis?: boolean;
10
+ axisHeight?: number;
11
+ };
12
+ declare const ClockRootTest: import("svelte").Component<Props, {}, "">;
13
+ type ClockRootTest = ReturnType<typeof ClockRootTest>;
14
+ export default ClockRootTest;
@@ -0,0 +1,329 @@
1
+ <script lang="ts">
2
+ import { untrack, type Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { useLocaleContextOptional } from '../../locale-provider/context';
5
+ import { setClockContext, type ClockContext } from './context';
6
+ import { resolveVisibleColumns, type ClockColumnInfo } from './resolve-visible-columns';
7
+ import { buildWheelOptions } from './wheel-options';
8
+ import ClockWheelColumn from '../wheel-column/clock-wheel-column.svelte';
9
+ import {
10
+ buildTimePartsFromDraft,
11
+ clampToStep,
12
+ createEmptyTimePickerDraft,
13
+ formatTimePickerValue,
14
+ getSegmentLabel,
15
+ isSegmentValueEmpty,
16
+ isTimeOutOfRange,
17
+ isValidTimePickerValue,
18
+ normalizeSegmentNumberInput,
19
+ toDraftFromTimeValue,
20
+ type TimePickerDraft,
21
+ type TimePickerEditableSegmentType,
22
+ type TimePickerGranularity,
23
+ type TimePickerHourCycle,
24
+ type TimePickerTimeValue
25
+ } from './time-utils';
26
+
27
+ type ClockRootProps = Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'> & {
28
+ id?: string;
29
+ value?: TimePickerTimeValue | null;
30
+ defaultValue?: TimePickerTimeValue | null;
31
+ onChange?: (value: TimePickerTimeValue | null) => void;
32
+ minValue?: TimePickerTimeValue;
33
+ maxValue?: TimePickerTimeValue;
34
+ hourCycle?: TimePickerHourCycle;
35
+ granularity?: TimePickerGranularity;
36
+ hourStep?: number;
37
+ minuteStep?: number;
38
+ secondStep?: number;
39
+ isDisabled?: boolean;
40
+ column?: Snippet<[ClockColumnInfo]>;
41
+ children?: Snippet;
42
+ class?: string;
43
+ 'aria-label'?: string;
44
+ };
45
+
46
+ const generatedInstanceId = $props.id();
47
+
48
+ let {
49
+ id,
50
+ value = $bindable(),
51
+ defaultValue,
52
+ onChange,
53
+ minValue,
54
+ maxValue,
55
+ hourCycle,
56
+ granularity = 'minute',
57
+ hourStep = 1,
58
+ minuteStep = 1,
59
+ secondStep = 1,
60
+ isDisabled = false,
61
+ column: columnSnippet,
62
+ children,
63
+ class: className = '',
64
+ 'aria-label': ariaLabel,
65
+ ...restProps
66
+ }: ClockRootProps = $props();
67
+
68
+ function hasExplicitPositionClass(value: string): boolean {
69
+ return /(^|\s)(?:[\w-]+:)*(?:static|fixed|absolute|relative|sticky)(?:\s|$)/.test(value);
70
+ }
71
+
72
+ const resolvedClassName = $derived.by(() => {
73
+ const trimmed = className.trim();
74
+ if (trimmed.length === 0) return 'relative';
75
+ if (hasExplicitPositionClass(trimmed)) return trimmed;
76
+ return `${trimmed} relative`;
77
+ });
78
+
79
+ const instanceId = untrack(() => id) ?? generatedInstanceId;
80
+ const localeContext = useLocaleContextOptional();
81
+ const localeStore = localeContext?.locale;
82
+ const localeFromContext = $derived.by(() => {
83
+ if (!localeStore) return undefined;
84
+ return $localeStore;
85
+ });
86
+ const systemLocale = untrack(() => Intl.DateTimeFormat().resolvedOptions().locale);
87
+ const resolvedLocale = $derived(localeFromContext ?? systemLocale);
88
+ const resolvedHourCycle = $derived.by<TimePickerHourCycle>(() => {
89
+ if (hourCycle) return hourCycle;
90
+ const localeCycle = new Intl.DateTimeFormat(resolvedLocale, {
91
+ hour: 'numeric'
92
+ }).resolvedOptions().hourCycle;
93
+ return localeCycle === 'h11' || localeCycle === 'h12' ? 12 : 24;
94
+ });
95
+
96
+ const initialValueProp = untrack(() => value);
97
+ const initialDefaultValue = untrack(() =>
98
+ isValidTimePickerValue(defaultValue) ? defaultValue : null
99
+ );
100
+ const initialPropValue =
101
+ initialValueProp === undefined
102
+ ? initialDefaultValue
103
+ : isValidTimePickerValue(initialValueProp)
104
+ ? initialValueProp
105
+ : null;
106
+ const initialHourCycle = untrack<TimePickerHourCycle>(() => {
107
+ if (hourCycle) return hourCycle;
108
+ const localeCycle = new Intl.DateTimeFormat(resolvedLocale, {
109
+ hour: 'numeric'
110
+ }).resolvedOptions().hourCycle;
111
+ return localeCycle === 'h11' || localeCycle === 'h12' ? 12 : 24;
112
+ });
113
+
114
+ let valueInternal = $state<TimePickerTimeValue | null>(initialPropValue);
115
+ let lastPublishedValue = $state<TimePickerTimeValue | null>(initialPropValue);
116
+ let segmentDraft = $state<TimePickerDraft>(
117
+ initialPropValue
118
+ ? toDraftFromTimeValue(initialPropValue, initialHourCycle)
119
+ : createEmptyTimePickerDraft()
120
+ );
121
+
122
+ if (initialValueProp === undefined) {
123
+ value = initialPropValue;
124
+ } else if (initialValueProp !== initialPropValue) {
125
+ value = initialPropValue;
126
+ }
127
+
128
+ $effect(() => {
129
+ const nextValue = value === undefined ? null : isValidTimePickerValue(value) ? value : null;
130
+ if (nextValue === lastPublishedValue) return;
131
+ publishCommittedValue(nextValue, false);
132
+ segmentDraft = nextValue
133
+ ? toDraftFromTimeValue(nextValue, resolvedHourCycle)
134
+ : createEmptyTimePickerDraft();
135
+ });
136
+
137
+ const normalizedMinValue = $derived(isValidTimePickerValue(minValue) ? minValue : undefined);
138
+ const normalizedMaxValue = $derived(isValidTimePickerValue(maxValue) ? maxValue : undefined);
139
+
140
+ function publishCommittedValue(
141
+ nextValue: TimePickerTimeValue | null,
142
+ emitChange: boolean
143
+ ): boolean {
144
+ const bindableValue = value === undefined ? valueInternal : value;
145
+ const normalizedBindableValue = isValidTimePickerValue(bindableValue) ? bindableValue : null;
146
+ const didInternalChange = valueInternal !== nextValue;
147
+ const didBindableChange = normalizedBindableValue !== nextValue;
148
+ if (!didInternalChange && !didBindableChange) return false;
149
+
150
+ valueInternal = nextValue;
151
+ lastPublishedValue = nextValue;
152
+ if (didBindableChange) {
153
+ value = nextValue;
154
+ }
155
+ if (emitChange && didInternalChange) {
156
+ onChange?.(nextValue);
157
+ }
158
+ return true;
159
+ }
160
+
161
+ function getSegmentValue(type: TimePickerEditableSegmentType): string {
162
+ if (type === 'hour') return segmentDraft.hour;
163
+ if (type === 'minute') return segmentDraft.minute;
164
+ if (type === 'second') return segmentDraft.second;
165
+ return segmentDraft.dayPeriod;
166
+ }
167
+
168
+ function setSegmentValue(type: TimePickerEditableSegmentType, nextValue: string) {
169
+ if (isDisabled) return;
170
+ if (type === 'dayPeriod') {
171
+ const normalized = nextValue.trim().toUpperCase();
172
+ if (normalized === '' || normalized === 'AM' || normalized === 'PM') {
173
+ segmentDraft.dayPeriod = normalized;
174
+ } else {
175
+ segmentDraft.dayPeriod = '';
176
+ }
177
+ } else {
178
+ const maxDigits = 2;
179
+ let normalized = normalizeSegmentNumberInput(nextValue, maxDigits);
180
+ if (normalized.length > 0) {
181
+ const numeric = Number(normalized);
182
+ if (type === 'hour') {
183
+ if (resolvedHourCycle === 12) {
184
+ normalized = String(clampToStep(numeric, Math.max(1, hourStep), 1, 12));
185
+ } else {
186
+ normalized = String(clampToStep(numeric, Math.max(1, hourStep), 0, 23));
187
+ }
188
+ }
189
+ if (type === 'minute') {
190
+ normalized = String(clampToStep(numeric, Math.max(1, minuteStep), 0, 59));
191
+ }
192
+ if (type === 'second') {
193
+ normalized = String(clampToStep(numeric, Math.max(1, secondStep), 0, 59));
194
+ }
195
+ }
196
+
197
+ if (type === 'hour') segmentDraft.hour = normalized;
198
+ if (type === 'minute') segmentDraft.minute = normalized;
199
+ if (type === 'second') segmentDraft.second = normalized;
200
+
201
+ if (resolvedHourCycle === 12 && isSegmentValueEmpty(segmentDraft.dayPeriod)) {
202
+ segmentDraft.dayPeriod = 'AM';
203
+ }
204
+ }
205
+
206
+ commitFromDraft();
207
+ }
208
+
209
+ function commitFromDraft() {
210
+ const nextParts = buildTimePartsFromDraft(segmentDraft, granularity, resolvedHourCycle);
211
+ if (!nextParts) {
212
+ publishCommittedValue(null, true);
213
+ return;
214
+ }
215
+
216
+ const candidateValue = formatTimePickerValue(nextParts, granularity);
217
+ if (isTimeOutOfRange(candidateValue, normalizedMinValue, normalizedMaxValue, granularity)) {
218
+ publishCommittedValue(null, true);
219
+ return;
220
+ }
221
+
222
+ publishCommittedValue(candidateValue, true);
223
+ }
224
+
225
+ function selectWheelValue(type: TimePickerEditableSegmentType, optionValue: string) {
226
+ if (isDisabled) return;
227
+
228
+ if (type === 'dayPeriod') {
229
+ setSegmentValue(type, optionValue.toUpperCase());
230
+ } else {
231
+ setSegmentValue(type, optionValue);
232
+ }
233
+ }
234
+
235
+ function getSelectedWheelValue(type: TimePickerEditableSegmentType): string | null {
236
+ const selected = getSegmentValue(type);
237
+ return selected.trim().length > 0 ? selected : null;
238
+ }
239
+
240
+ function getWheelOptions(type: TimePickerEditableSegmentType) {
241
+ const hasRangeBounds = normalizedMinValue !== undefined || normalizedMaxValue !== undefined;
242
+
243
+ const getCandidateFromPartial = (
244
+ partial: Partial<TimePickerDraft>
245
+ ): TimePickerTimeValue | null => {
246
+ const candidateDraft: TimePickerDraft = {
247
+ hour: partial.hour ?? segmentDraft.hour,
248
+ minute: partial.minute ?? segmentDraft.minute,
249
+ second: partial.second ?? segmentDraft.second,
250
+ dayPeriod: partial.dayPeriod ?? segmentDraft.dayPeriod
251
+ };
252
+ const parts = buildTimePartsFromDraft(candidateDraft, granularity, resolvedHourCycle);
253
+ if (!parts) return null;
254
+ return formatTimePickerValue(parts, granularity);
255
+ };
256
+
257
+ return buildWheelOptions({
258
+ type,
259
+ granularity,
260
+ hourCycle: resolvedHourCycle,
261
+ hourStep,
262
+ minuteStep,
263
+ secondStep,
264
+ hasRangeBounds,
265
+ getCandidateFromPartial,
266
+ isOutOfRange: (candidate) =>
267
+ isTimeOutOfRange(candidate, normalizedMinValue, normalizedMaxValue, granularity)
268
+ });
269
+ }
270
+
271
+ function getSegmentLabelByType(type: TimePickerEditableSegmentType): string {
272
+ return getSegmentLabel(type, resolvedLocale);
273
+ }
274
+
275
+ const context: ClockContext = {
276
+ get id() {
277
+ return instanceId;
278
+ },
279
+ get locale() {
280
+ return resolvedLocale;
281
+ },
282
+ get isDisabled() {
283
+ return isDisabled;
284
+ },
285
+ get granularity() {
286
+ return granularity;
287
+ },
288
+ get hourCycle() {
289
+ return resolvedHourCycle;
290
+ },
291
+ get open() {
292
+ return true;
293
+ },
294
+ selectWheelValue,
295
+ getSelectedWheelValue,
296
+ getWheelOptions,
297
+ getSegmentLabel: getSegmentLabelByType
298
+ };
299
+
300
+ setClockContext(context);
301
+
302
+ const visibleColumns = $derived.by(() =>
303
+ resolveVisibleColumns(granularity, resolvedHourCycle, getSegmentLabelByType)
304
+ );
305
+ </script>
306
+
307
+ <div
308
+ id={instanceId}
309
+ class={resolvedClassName}
310
+ role="group"
311
+ aria-label={ariaLabel}
312
+ data-clock="true"
313
+ {...restProps}
314
+ >
315
+ {#if columnSnippet}
316
+ {#each visibleColumns as col (col.type)}
317
+ {@render columnSnippet(col)}
318
+ {/each}
319
+ {#if children}
320
+ {@render children()}
321
+ {/if}
322
+ {:else if children}
323
+ {@render children()}
324
+ {:else}
325
+ {#each visibleColumns as col (col.type)}
326
+ <ClockWheelColumn type={col.type} />
327
+ {/each}
328
+ {/if}
329
+ </div>
@@ -0,0 +1,25 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type ClockColumnInfo } from './resolve-visible-columns';
4
+ import { type TimePickerGranularity, type TimePickerHourCycle, type TimePickerTimeValue } from './time-utils';
5
+ type ClockRootProps = Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'> & {
6
+ id?: string;
7
+ value?: TimePickerTimeValue | null;
8
+ defaultValue?: TimePickerTimeValue | null;
9
+ onChange?: (value: TimePickerTimeValue | null) => void;
10
+ minValue?: TimePickerTimeValue;
11
+ maxValue?: TimePickerTimeValue;
12
+ hourCycle?: TimePickerHourCycle;
13
+ granularity?: TimePickerGranularity;
14
+ hourStep?: number;
15
+ minuteStep?: number;
16
+ secondStep?: number;
17
+ isDisabled?: boolean;
18
+ column?: Snippet<[ClockColumnInfo]>;
19
+ children?: Snippet;
20
+ class?: string;
21
+ 'aria-label'?: string;
22
+ };
23
+ declare const ClockRoot: import("svelte").Component<ClockRootProps, {}, "value">;
24
+ type ClockRoot = ReturnType<typeof ClockRoot>;
25
+ export default ClockRoot;
@@ -0,0 +1,22 @@
1
+ import type { TimePickerEditableSegmentType, TimePickerGranularity, TimePickerHourCycle } from './time-utils';
2
+ export type ClockContext = {
3
+ id: string;
4
+ locale: string;
5
+ isDisabled: boolean;
6
+ granularity: TimePickerGranularity;
7
+ hourCycle: TimePickerHourCycle;
8
+ /** Whether the clock is visible and should align scroll positions. */
9
+ open: boolean;
10
+ selectWheelValue: (type: TimePickerEditableSegmentType, value: string) => void;
11
+ getSelectedWheelValue: (type: TimePickerEditableSegmentType) => string | null;
12
+ getWheelOptions: (type: TimePickerEditableSegmentType) => Array<{
13
+ value: string;
14
+ label: string;
15
+ disabled: boolean;
16
+ }>;
17
+ getSegmentLabel: (type: TimePickerEditableSegmentType) => string;
18
+ };
19
+ export declare function setClockContext(context: ClockContext): void;
20
+ export declare function getClockContext(): ClockContext | undefined;
21
+ export declare function useClockContext(): ClockContext;
22
+ export type { TimePickerEditableSegmentType as ClockEditableSegmentType };
@@ -0,0 +1,15 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const KEY = Symbol('clock');
3
+ export function setClockContext(context) {
4
+ setContext(KEY, context);
5
+ }
6
+ export function getClockContext() {
7
+ return getContext(KEY);
8
+ }
9
+ export function useClockContext() {
10
+ const context = getClockContext();
11
+ if (!context) {
12
+ throw new Error('Clock components must be used within Clock.Root.');
13
+ }
14
+ return context;
15
+ }
@@ -0,0 +1,7 @@
1
+ import type { ClockEditableSegmentType } from './context';
2
+ import type { TimePickerGranularity, TimePickerHourCycle } from './time-utils';
3
+ export type ClockColumnInfo = {
4
+ type: ClockEditableSegmentType;
5
+ label?: string;
6
+ };
7
+ export declare function resolveVisibleColumns(granularity: TimePickerGranularity, hourCycle: TimePickerHourCycle, getSegmentLabel?: (type: ClockEditableSegmentType) => string): ClockColumnInfo[];
@@ -0,0 +1,16 @@
1
+ export function resolveVisibleColumns(granularity, hourCycle, getSegmentLabel) {
2
+ const columns = ['hour'];
3
+ if (granularity !== 'hour') {
4
+ columns.push('minute');
5
+ }
6
+ if (granularity === 'second') {
7
+ columns.push('second');
8
+ }
9
+ if (hourCycle === 12) {
10
+ columns.push('dayPeriod');
11
+ }
12
+ return columns.map((type) => ({
13
+ type,
14
+ label: getSegmentLabel?.(type)
15
+ }));
16
+ }
@@ -0,0 +1,48 @@
1
+ export type TimePickerGranularity = 'hour' | 'minute' | 'second';
2
+ export type TimePickerHourCycle = 12 | 24;
3
+ export type TimePickerTimeValue = string;
4
+ export type TimePickerSegmentType = 'hour' | 'minute' | 'second' | 'dayPeriod' | 'literal';
5
+ export type TimePickerEditableSegmentType = Exclude<TimePickerSegmentType, 'literal'>;
6
+ export type TimePickerSegmentPart = {
7
+ type: TimePickerSegmentType;
8
+ text: string;
9
+ isPlaceholder: boolean;
10
+ };
11
+ export type TimePickerDraft = {
12
+ hour: string;
13
+ minute: string;
14
+ second: string;
15
+ dayPeriod: string;
16
+ };
17
+ export type TimeParts = {
18
+ hour: number;
19
+ minute: number;
20
+ second: number;
21
+ };
22
+ export declare function createEmptyTimePickerDraft(): TimePickerDraft;
23
+ export declare function isValidTimePickerValue(value: unknown): value is TimePickerTimeValue;
24
+ export declare function parseTimePickerValue(value: TimePickerTimeValue): TimeParts | null;
25
+ export declare function formatTimePickerValue(parts: TimeParts, granularity: TimePickerGranularity): TimePickerTimeValue;
26
+ export declare function toDraftFromTimeValue(value: TimePickerTimeValue, hourCycle: TimePickerHourCycle): TimePickerDraft;
27
+ export declare function getRequiredSegments(granularity: TimePickerGranularity, hourCycle: TimePickerHourCycle): TimePickerEditableSegmentType[];
28
+ export declare function normalizeSegmentNumberInput(value: string, maxDigits?: number): string;
29
+ export declare function isSegmentValueEmpty(value: string | undefined): boolean;
30
+ export declare function buildTimePartsFromDraft(draft: TimePickerDraft, granularity: TimePickerGranularity, hourCycle: TimePickerHourCycle): TimeParts | null;
31
+ export declare function compareTimeParts(left: TimeParts, right: TimeParts): number;
32
+ export declare function isTimeOutOfRange(value: TimePickerTimeValue, minValue: TimePickerTimeValue | undefined, maxValue: TimePickerTimeValue | undefined, granularity?: TimePickerGranularity): boolean;
33
+ export declare function clampToStep(value: number, step: number, min: number, max: number): number;
34
+ export declare function adjustSegmentWithStep(currentValue: string, type: Exclude<TimePickerSegmentType, 'literal' | 'dayPeriod'>, delta: number, options: {
35
+ hourCycle: TimePickerHourCycle;
36
+ hourStep: number;
37
+ minuteStep: number;
38
+ secondStep: number;
39
+ }): string;
40
+ export declare function buildTimePickerSegments(params: {
41
+ locale: string;
42
+ hourCycle: TimePickerHourCycle;
43
+ granularity: TimePickerGranularity;
44
+ draft: TimePickerDraft;
45
+ formatter?: Intl.DateTimeFormat;
46
+ }): TimePickerSegmentPart[];
47
+ export declare function getEditableSegmentOrder(segments: TimePickerSegmentPart[]): TimePickerEditableSegmentType[];
48
+ export declare function getSegmentLabel(type: TimePickerEditableSegmentType, locale: string): string;