@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,625 @@
1
+ <script lang="ts">
2
+ import { untrack, type Snippet } from 'svelte';
3
+ import { useLocaleContextOptional } from '../../locale-provider/context';
4
+ import type { TimePickerOpenChangeDetails, TimePickerOpenChangeReason } from './context';
5
+ import { setTimePickerContext, type TimePickerContext } from './context';
6
+ import { buildWheelOptions } from '../../clock/root/wheel-options';
7
+ import {
8
+ adjustSegmentWithStep,
9
+ buildTimePartsFromDraft,
10
+ buildTimePickerSegments,
11
+ clampToStep,
12
+ createEmptyTimePickerDraft,
13
+ formatTimePickerValue,
14
+ getEditableSegmentOrder,
15
+ getRequiredSegments,
16
+ getSegmentLabel,
17
+ isSegmentValueEmpty,
18
+ isTimeOutOfRange,
19
+ isValidTimePickerValue,
20
+ normalizeSegmentNumberInput,
21
+ toDraftFromTimeValue,
22
+ type TimePickerDraft,
23
+ type TimePickerEditableSegmentType,
24
+ type TimePickerGranularity,
25
+ type TimePickerHourCycle,
26
+ type TimePickerSegmentType,
27
+ type TimePickerTimeValue
28
+ } from './time-utils';
29
+
30
+ type TimePickerRootProps = {
31
+ id?: string;
32
+ value?: TimePickerTimeValue | null;
33
+ defaultValue?: TimePickerTimeValue | null;
34
+ onChange?: (value: TimePickerTimeValue | null) => void;
35
+ minValue?: TimePickerTimeValue;
36
+ maxValue?: TimePickerTimeValue;
37
+ hourCycle?: TimePickerHourCycle;
38
+ granularity?: TimePickerGranularity;
39
+ hourStep?: number;
40
+ minuteStep?: number;
41
+ secondStep?: number;
42
+ isDisabled?: boolean;
43
+ isReadOnly?: boolean;
44
+ isRequired?: boolean;
45
+ open?: boolean;
46
+ defaultOpen?: boolean;
47
+ onOpenChange?: (open: boolean, details: TimePickerOpenChangeDetails) => void;
48
+ children?: Snippet;
49
+ class?: string;
50
+ 'aria-label'?: string;
51
+ };
52
+
53
+ const generatedInstanceId = $props.id();
54
+
55
+ let {
56
+ id,
57
+ value = $bindable(),
58
+ defaultValue,
59
+ onChange,
60
+ minValue,
61
+ maxValue,
62
+ hourCycle,
63
+ granularity = 'minute',
64
+ hourStep = 1,
65
+ minuteStep = 1,
66
+ secondStep = 1,
67
+ isDisabled = false,
68
+ isReadOnly = false,
69
+ isRequired = false,
70
+ open = $bindable(),
71
+ defaultOpen = false,
72
+ onOpenChange,
73
+ children,
74
+ class: className = '',
75
+ 'aria-label': ariaLabel
76
+ }: TimePickerRootProps = $props();
77
+
78
+ const instanceId = untrack(() => id) ?? generatedInstanceId;
79
+ const localeContext = useLocaleContextOptional();
80
+ const localeStore = localeContext?.locale;
81
+ const localeFromContext = $derived.by(() => {
82
+ if (!localeStore) return undefined;
83
+ return $localeStore;
84
+ });
85
+ const systemLocale = untrack(() => Intl.DateTimeFormat().resolvedOptions().locale);
86
+ const resolvedLocale = $derived(localeFromContext ?? systemLocale);
87
+ const resolvedHourCycle = $derived.by<TimePickerHourCycle>(() => {
88
+ if (hourCycle) return hourCycle;
89
+ const localeCycle = new Intl.DateTimeFormat(resolvedLocale, {
90
+ hour: 'numeric'
91
+ }).resolvedOptions().hourCycle;
92
+ return localeCycle === 'h11' || localeCycle === 'h12' ? 12 : 24;
93
+ });
94
+
95
+ let openInternal = $state((() => defaultOpen)());
96
+ let focusVisible = $state(false);
97
+ let focusWithin = $state(false);
98
+ let activeSegment = $state<Exclude<TimePickerSegmentType, 'literal'> | null>(null);
99
+ let triggerRef: HTMLElement | null = $state(null);
100
+
101
+ const initialValueProp = untrack(() => value);
102
+ const initialDefaultValue = untrack(() =>
103
+ isValidTimePickerValue(defaultValue) ? defaultValue : null
104
+ );
105
+ const initialPropValue =
106
+ initialValueProp === undefined
107
+ ? initialDefaultValue
108
+ : isValidTimePickerValue(initialValueProp)
109
+ ? initialValueProp
110
+ : null;
111
+ const initialHourCycle = untrack<TimePickerHourCycle>(() => {
112
+ if (hourCycle) return hourCycle;
113
+ const localeCycle = new Intl.DateTimeFormat(resolvedLocale, {
114
+ hour: 'numeric'
115
+ }).resolvedOptions().hourCycle;
116
+ return localeCycle === 'h11' || localeCycle === 'h12' ? 12 : 24;
117
+ });
118
+
119
+ let valueInternal = $state<TimePickerTimeValue | null>(initialPropValue);
120
+ let lastPublishedValue = $state<TimePickerTimeValue | null>(initialPropValue);
121
+ let segmentDraft = $state<TimePickerDraft>(
122
+ initialPropValue
123
+ ? toDraftFromTimeValue(initialPropValue, initialHourCycle)
124
+ : createEmptyTimePickerDraft()
125
+ );
126
+ let segmentTypeBuffer = $state<TimePickerDraft>(createEmptyTimePickerDraft());
127
+
128
+ const segmentRefs: Record<TimePickerEditableSegmentType, HTMLElement | null> = {
129
+ hour: null,
130
+ minute: null,
131
+ second: null,
132
+ dayPeriod: null
133
+ };
134
+
135
+ if (initialValueProp === undefined) {
136
+ value = initialPropValue;
137
+ } else if (initialValueProp !== initialPropValue) {
138
+ value = initialPropValue;
139
+ }
140
+
141
+ $effect(() => {
142
+ if (open !== undefined && open !== openInternal) {
143
+ openInternal = open;
144
+ }
145
+ });
146
+
147
+ $effect(() => {
148
+ const nextValue = value === undefined ? null : isValidTimePickerValue(value) ? value : null;
149
+ if (nextValue === lastPublishedValue) return;
150
+ publishCommittedValue(nextValue, false);
151
+ segmentDraft = nextValue
152
+ ? toDraftFromTimeValue(nextValue, resolvedHourCycle)
153
+ : createEmptyTimePickerDraft();
154
+ segmentTypeBuffer = createEmptyTimePickerDraft();
155
+ });
156
+
157
+ const segmentFormatter = $derived.by(
158
+ () =>
159
+ new Intl.DateTimeFormat(resolvedLocale, {
160
+ hour: 'numeric',
161
+ minute: granularity !== 'hour' ? '2-digit' : undefined,
162
+ second: granularity === 'second' ? '2-digit' : undefined,
163
+ hourCycle: resolvedHourCycle === 12 ? 'h12' : 'h23',
164
+ timeZone: 'UTC'
165
+ })
166
+ );
167
+
168
+ const segments = $derived.by(() =>
169
+ buildTimePickerSegments({
170
+ locale: resolvedLocale,
171
+ hourCycle: resolvedHourCycle,
172
+ granularity,
173
+ draft: segmentDraft,
174
+ formatter: segmentFormatter
175
+ })
176
+ );
177
+ const segmentOrder = $derived(getEditableSegmentOrder(segments));
178
+ const requiredSegments = $derived(getRequiredSegments(granularity, resolvedHourCycle));
179
+
180
+ const normalizedMinValue = $derived(isValidTimePickerValue(minValue) ? minValue : undefined);
181
+ const normalizedMaxValue = $derived(isValidTimePickerValue(maxValue) ? maxValue : undefined);
182
+
183
+ const isInvalidDraft = $derived.by(() => {
184
+ const required = requiredSegments;
185
+ const hasAnyValue = required.some((type) => !isSegmentValueEmpty(getSegmentValue(type)));
186
+ if (!hasAnyValue) return false;
187
+ const parts = buildTimePartsFromDraft(segmentDraft, granularity, resolvedHourCycle);
188
+ if (!parts) return true;
189
+ const candidate = formatTimePickerValue(parts, granularity);
190
+ return isTimeOutOfRange(candidate, normalizedMinValue, normalizedMaxValue, granularity);
191
+ });
192
+
193
+ function publishCommittedValue(
194
+ nextValue: TimePickerTimeValue | null,
195
+ emitChange: boolean
196
+ ): boolean {
197
+ const bindableValue = value === undefined ? valueInternal : value;
198
+ const normalizedBindableValue = isValidTimePickerValue(bindableValue) ? bindableValue : null;
199
+ const didInternalChange = valueInternal !== nextValue;
200
+ const didBindableChange = normalizedBindableValue !== nextValue;
201
+ if (!didInternalChange && !didBindableChange) return false;
202
+
203
+ valueInternal = nextValue;
204
+ lastPublishedValue = nextValue;
205
+ if (didBindableChange) {
206
+ value = nextValue;
207
+ }
208
+ if (emitChange && didInternalChange) {
209
+ onChange?.(nextValue);
210
+ }
211
+ return true;
212
+ }
213
+
214
+ function setOpen(
215
+ nextOpen: boolean,
216
+ details?: TimePickerOpenChangeDetails | { reason?: TimePickerOpenChangeReason; event?: Event }
217
+ ) {
218
+ if (openInternal === nextOpen) return;
219
+ let canceled = false;
220
+ const resolvedDetails: TimePickerOpenChangeDetails = {
221
+ reason: details?.reason ?? 'imperative-action',
222
+ event: details?.event,
223
+ cancel: () => {
224
+ canceled = true;
225
+ },
226
+ get isCanceled() {
227
+ return canceled;
228
+ }
229
+ };
230
+
231
+ onOpenChange?.(nextOpen, resolvedDetails);
232
+ if (resolvedDetails.isCanceled) return;
233
+
234
+ openInternal = nextOpen;
235
+ open = nextOpen;
236
+ }
237
+
238
+ function setFocusVisible(visible: boolean) {
239
+ if (focusVisible === visible) return;
240
+ focusVisible = visible;
241
+ }
242
+
243
+ function setTriggerRef(element: HTMLElement | null) {
244
+ if (triggerRef === element) return;
245
+ triggerRef = element;
246
+ }
247
+
248
+ function syncFocusWithin() {
249
+ const root = document.getElementById(instanceId);
250
+ const activeElement = document.activeElement;
251
+ const nextWithin = !!root && !!activeElement && root.contains(activeElement);
252
+ if (!nextWithin && focusVisible) {
253
+ focusVisible = false;
254
+ }
255
+ if (!nextWithin && activeSegment !== null) {
256
+ activeSegment = null;
257
+ segmentTypeBuffer = createEmptyTimePickerDraft();
258
+ }
259
+ if (focusWithin === nextWithin) return;
260
+ focusWithin = nextWithin;
261
+ }
262
+
263
+ function setActiveSegment(segment: Exclude<TimePickerSegmentType, 'literal'> | null) {
264
+ if (activeSegment === segment) return;
265
+ activeSegment = segment;
266
+ segmentTypeBuffer = createEmptyTimePickerDraft();
267
+ }
268
+
269
+ function getSegmentValue(type: Exclude<TimePickerSegmentType, 'literal'>): string {
270
+ if (type === 'hour') return segmentDraft.hour;
271
+ if (type === 'minute') return segmentDraft.minute;
272
+ if (type === 'second') return segmentDraft.second;
273
+ return segmentDraft.dayPeriod;
274
+ }
275
+
276
+ function setSegmentValue(type: Exclude<TimePickerSegmentType, 'literal'>, nextValue: string) {
277
+ if (isDisabled || isReadOnly) return;
278
+ if (type === 'dayPeriod') {
279
+ const normalized = nextValue.trim().toUpperCase();
280
+ if (normalized === '' || normalized === 'AM' || normalized === 'PM') {
281
+ segmentDraft.dayPeriod = normalized;
282
+ } else {
283
+ segmentDraft.dayPeriod = '';
284
+ }
285
+ } else {
286
+ const maxDigits = 2;
287
+ let normalized = normalizeSegmentNumberInput(nextValue, maxDigits);
288
+ if (normalized.length > 0) {
289
+ const numeric = Number(normalized);
290
+ if (type === 'hour') {
291
+ if (resolvedHourCycle === 12) {
292
+ normalized = String(clampToStep(numeric, Math.max(1, hourStep), 1, 12));
293
+ } else {
294
+ normalized = String(clampToStep(numeric, Math.max(1, hourStep), 0, 23));
295
+ }
296
+ }
297
+ if (type === 'minute') {
298
+ normalized = String(clampToStep(numeric, Math.max(1, minuteStep), 0, 59));
299
+ }
300
+ if (type === 'second') {
301
+ normalized = String(clampToStep(numeric, Math.max(1, secondStep), 0, 59));
302
+ }
303
+ }
304
+
305
+ if (type === 'hour') segmentDraft.hour = normalized;
306
+ if (type === 'minute') segmentDraft.minute = normalized;
307
+ if (type === 'second') segmentDraft.second = normalized;
308
+
309
+ if (resolvedHourCycle === 12 && isSegmentValueEmpty(segmentDraft.dayPeriod)) {
310
+ segmentDraft.dayPeriod = 'AM';
311
+ }
312
+ }
313
+
314
+ commitFromDraft();
315
+ }
316
+
317
+ function commitFromDraft() {
318
+ const nextParts = buildTimePartsFromDraft(segmentDraft, granularity, resolvedHourCycle);
319
+ if (!nextParts) {
320
+ publishCommittedValue(null, true);
321
+ return;
322
+ }
323
+
324
+ const candidateValue = formatTimePickerValue(nextParts, granularity);
325
+ if (isTimeOutOfRange(candidateValue, normalizedMinValue, normalizedMaxValue, granularity)) {
326
+ publishCommittedValue(null, true);
327
+ return;
328
+ }
329
+
330
+ publishCommittedValue(candidateValue, true);
331
+ }
332
+
333
+ function openPopover(reason: TimePickerOpenChangeReason = 'imperative-action', event?: Event) {
334
+ if (isDisabled || isReadOnly) return;
335
+ setOpen(true, { reason, event });
336
+ }
337
+
338
+ function closePopover(reason: TimePickerOpenChangeReason = 'imperative-action', event?: Event) {
339
+ setOpen(false, { reason, event });
340
+ }
341
+
342
+ function togglePopover(reason: TimePickerOpenChangeReason = 'trigger-press', event?: Event) {
343
+ if (isDisabled || isReadOnly) return;
344
+ setOpen(!openInternal, { reason, event });
345
+ }
346
+
347
+ function getTypingThreshold(
348
+ type: Exclude<TimePickerSegmentType, 'literal' | 'dayPeriod'>
349
+ ): number {
350
+ if (type === 'hour') {
351
+ return resolvedHourCycle === 12 ? 2 : 3;
352
+ }
353
+ return 6;
354
+ }
355
+
356
+ function typeSegmentDigit(
357
+ type: Exclude<TimePickerSegmentType, 'literal'>,
358
+ digit: string
359
+ ): boolean {
360
+ if (isDisabled || isReadOnly) return false;
361
+ if (!/^\d$/.test(digit)) return false;
362
+ if (type === 'dayPeriod') return false;
363
+
364
+ const currentBuffer = (segmentTypeBuffer[type] || '').slice(0, 2);
365
+ const seededBuffer = currentBuffer.length >= 2 ? '' : currentBuffer;
366
+ let candidate = `${seededBuffer}${digit}`.slice(0, 2);
367
+ if (!candidate) return false;
368
+
369
+ const threshold = getTypingThreshold(type);
370
+ if (candidate.length === 1) {
371
+ setSegmentValue(type, candidate);
372
+ segmentTypeBuffer[type] = candidate;
373
+ const numeric = Number(candidate);
374
+ if (Number.isFinite(numeric) && numeric >= threshold) {
375
+ segmentTypeBuffer[type] = '';
376
+ return true;
377
+ }
378
+ return false;
379
+ }
380
+
381
+ const numericCandidate = Number(candidate);
382
+ if (!Number.isFinite(numericCandidate)) {
383
+ segmentTypeBuffer[type] = '';
384
+ return false;
385
+ }
386
+
387
+ let min = 0;
388
+ let max = 59;
389
+ if (type === 'hour') {
390
+ min = resolvedHourCycle === 12 ? 1 : 0;
391
+ max = resolvedHourCycle === 12 ? 12 : 23;
392
+ }
393
+
394
+ if (numericCandidate < min || numericCandidate > max) {
395
+ candidate = digit;
396
+ setSegmentValue(type, candidate);
397
+ segmentTypeBuffer[type] = candidate;
398
+ const fallbackNumeric = Number(candidate);
399
+ if (fallbackNumeric >= threshold) {
400
+ segmentTypeBuffer[type] = '';
401
+ return true;
402
+ }
403
+ return false;
404
+ }
405
+
406
+ setSegmentValue(type, candidate);
407
+ segmentTypeBuffer[type] = '';
408
+ return true;
409
+ }
410
+
411
+ function adjustSegmentValue(type: Exclude<TimePickerSegmentType, 'literal'>, step: number) {
412
+ if (isDisabled || isReadOnly) return;
413
+ if (type === 'dayPeriod') {
414
+ segmentDraft.dayPeriod = segmentDraft.dayPeriod === 'PM' ? 'AM' : 'PM';
415
+ commitFromDraft();
416
+ return;
417
+ }
418
+
419
+ const current = getSegmentValue(type);
420
+ const next = adjustSegmentWithStep(current, type, step, {
421
+ hourCycle: resolvedHourCycle,
422
+ hourStep,
423
+ minuteStep,
424
+ secondStep
425
+ });
426
+
427
+ setSegmentValue(type, next);
428
+ }
429
+
430
+ function registerSegmentRef(type: TimePickerEditableSegmentType, element: HTMLElement | null) {
431
+ segmentRefs[type] = element;
432
+ }
433
+
434
+ function focusNextSegment(type: TimePickerEditableSegmentType): boolean {
435
+ const index = segmentOrder.indexOf(type);
436
+ if (index < 0) return false;
437
+ for (let cursor = index + 1; cursor < segmentOrder.length; cursor += 1) {
438
+ const nextType = segmentOrder[cursor];
439
+ const nextRef = segmentRefs[nextType];
440
+ if (!nextRef || !nextRef.isConnected) continue;
441
+ nextRef.focus();
442
+ return true;
443
+ }
444
+ return false;
445
+ }
446
+
447
+ function focusPreviousSegment(type: TimePickerEditableSegmentType): boolean {
448
+ const index = segmentOrder.indexOf(type);
449
+ if (index <= 0) return false;
450
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
451
+ const prevType = segmentOrder[cursor];
452
+ const prevRef = segmentRefs[prevType];
453
+ if (!prevRef || !prevRef.isConnected) continue;
454
+ prevRef.focus();
455
+ return true;
456
+ }
457
+ return false;
458
+ }
459
+
460
+ function focusLastSegment(): boolean {
461
+ for (let cursor = segmentOrder.length - 1; cursor >= 0; cursor -= 1) {
462
+ const segmentType = segmentOrder[cursor];
463
+ const ref = segmentRefs[segmentType];
464
+ if (!ref || !ref.isConnected) continue;
465
+ ref.focus();
466
+ return true;
467
+ }
468
+ return false;
469
+ }
470
+
471
+ function focusNextPlaceholderOrLastSegment(): boolean {
472
+ for (const segmentType of segmentOrder) {
473
+ const ref = segmentRefs[segmentType];
474
+ if (!ref || !ref.isConnected) continue;
475
+ const currentValue = getSegmentValue(segmentType);
476
+ if (isSegmentValueEmpty(currentValue)) {
477
+ ref.focus();
478
+ return true;
479
+ }
480
+ }
481
+ return focusLastSegment();
482
+ }
483
+
484
+ function setValue(nextValue: TimePickerTimeValue | null) {
485
+ if (isDisabled || isReadOnly) return;
486
+ if (nextValue !== null && !isValidTimePickerValue(nextValue)) return;
487
+ if (
488
+ nextValue &&
489
+ isTimeOutOfRange(nextValue, normalizedMinValue, normalizedMaxValue, granularity)
490
+ )
491
+ return;
492
+
493
+ publishCommittedValue(nextValue, true);
494
+ segmentDraft = nextValue
495
+ ? toDraftFromTimeValue(nextValue, resolvedHourCycle)
496
+ : createEmptyTimePickerDraft();
497
+ segmentTypeBuffer = createEmptyTimePickerDraft();
498
+ }
499
+
500
+ function getSegmentLabelByType(type: TimePickerEditableSegmentType): string {
501
+ return getSegmentLabel(type, resolvedLocale);
502
+ }
503
+
504
+ function selectWheelValue(type: TimePickerEditableSegmentType, optionValue: string) {
505
+ if (isDisabled || isReadOnly) return;
506
+
507
+ if (type === 'dayPeriod') {
508
+ setSegmentValue(type, optionValue.toUpperCase());
509
+ } else {
510
+ setSegmentValue(type, optionValue);
511
+ }
512
+ }
513
+
514
+ function getSelectedWheelValue(type: TimePickerEditableSegmentType): string | null {
515
+ const selected = getSegmentValue(type);
516
+ return selected.trim().length > 0 ? selected : null;
517
+ }
518
+
519
+ function getWheelOptions(type: TimePickerEditableSegmentType) {
520
+ const hasRangeBounds = normalizedMinValue !== undefined || normalizedMaxValue !== undefined;
521
+
522
+ const getCandidateFromPartial = (
523
+ partial: Partial<TimePickerDraft>
524
+ ): TimePickerTimeValue | null => {
525
+ const candidateDraft: TimePickerDraft = {
526
+ hour: partial.hour ?? segmentDraft.hour,
527
+ minute: partial.minute ?? segmentDraft.minute,
528
+ second: partial.second ?? segmentDraft.second,
529
+ dayPeriod: partial.dayPeriod ?? segmentDraft.dayPeriod
530
+ };
531
+ const parts = buildTimePartsFromDraft(candidateDraft, granularity, resolvedHourCycle);
532
+ if (!parts) return null;
533
+ return formatTimePickerValue(parts, granularity);
534
+ };
535
+
536
+ return buildWheelOptions({
537
+ type,
538
+ granularity,
539
+ hourCycle: resolvedHourCycle,
540
+ hourStep,
541
+ minuteStep,
542
+ secondStep,
543
+ hasRangeBounds,
544
+ getCandidateFromPartial,
545
+ isOutOfRange: (candidate) =>
546
+ isTimeOutOfRange(candidate, normalizedMinValue, normalizedMaxValue, granularity)
547
+ });
548
+ }
549
+
550
+ const context: TimePickerContext = {
551
+ get id() {
552
+ return instanceId;
553
+ },
554
+ get isDisabled() {
555
+ return isDisabled;
556
+ },
557
+ get isReadOnly() {
558
+ return isReadOnly;
559
+ },
560
+ get isRequired() {
561
+ return isRequired;
562
+ },
563
+ get granularity() {
564
+ return granularity;
565
+ },
566
+ get hourCycle() {
567
+ return resolvedHourCycle;
568
+ },
569
+ get open() {
570
+ return openInternal;
571
+ },
572
+ get focusVisible() {
573
+ return focusVisible;
574
+ },
575
+ get focusWithin() {
576
+ return focusWithin;
577
+ },
578
+ get isInvalidDraft() {
579
+ return isInvalidDraft;
580
+ },
581
+ get value() {
582
+ return valueInternal;
583
+ },
584
+ get locale() {
585
+ return resolvedLocale;
586
+ },
587
+ get triggerRef() {
588
+ return triggerRef;
589
+ },
590
+ get activeSegment() {
591
+ return activeSegment;
592
+ },
593
+ setTriggerRef,
594
+ setFocusVisible,
595
+ syncFocusWithin,
596
+ setActiveSegment,
597
+ openPopover,
598
+ closePopover,
599
+ togglePopover,
600
+ onOpenChange: setOpen,
601
+ setValue,
602
+ getSegments: () => segments,
603
+ getSegmentValue,
604
+ setSegmentValue,
605
+ typeSegmentDigit,
606
+ adjustSegmentValue,
607
+ getSegmentLabel: getSegmentLabelByType,
608
+ registerSegmentRef,
609
+ focusNextPlaceholderOrLastSegment,
610
+ focusNextSegment,
611
+ focusPreviousSegment,
612
+ focusLastSegment,
613
+ selectWheelValue,
614
+ getSelectedWheelValue,
615
+ getWheelOptions
616
+ };
617
+
618
+ setTimePickerContext(context);
619
+ </script>
620
+
621
+ <div id={instanceId} class={className} aria-label={ariaLabel}>
622
+ {#if children}
623
+ {@render children()}
624
+ {/if}
625
+ </div>
@@ -0,0 +1,28 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { TimePickerOpenChangeDetails } from './context';
3
+ import { type TimePickerGranularity, type TimePickerHourCycle, type TimePickerTimeValue } from './time-utils';
4
+ type TimePickerRootProps = {
5
+ id?: string;
6
+ value?: TimePickerTimeValue | null;
7
+ defaultValue?: TimePickerTimeValue | null;
8
+ onChange?: (value: TimePickerTimeValue | null) => void;
9
+ minValue?: TimePickerTimeValue;
10
+ maxValue?: TimePickerTimeValue;
11
+ hourCycle?: TimePickerHourCycle;
12
+ granularity?: TimePickerGranularity;
13
+ hourStep?: number;
14
+ minuteStep?: number;
15
+ secondStep?: number;
16
+ isDisabled?: boolean;
17
+ isReadOnly?: boolean;
18
+ isRequired?: boolean;
19
+ open?: boolean;
20
+ defaultOpen?: boolean;
21
+ onOpenChange?: (open: boolean, details: TimePickerOpenChangeDetails) => void;
22
+ children?: Snippet;
23
+ class?: string;
24
+ 'aria-label'?: string;
25
+ };
26
+ declare const TimePickerRoot: import("svelte").Component<TimePickerRootProps, {}, "value" | "open">;
27
+ type TimePickerRoot = ReturnType<typeof TimePickerRoot>;
28
+ export default TimePickerRoot;