@human-kit/svelte-components 1.0.0-alpha.2 → 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.
- package/dist/FOCUS_STATE_CONTRACT.md +63 -0
- package/dist/FOCUS_STATE_REVIEW_TEMPLATE.md +70 -0
- package/dist/calendar/README.md +2 -1
- package/dist/calendar/TODO.md +21 -107
- package/dist/calendar/body-cell/README.md +15 -0
- package/dist/calendar/body-cell/calendar-body-cell.svelte +116 -41
- package/dist/calendar/grid/README.md +13 -0
- package/dist/calendar/grid-body/README.md +13 -0
- package/dist/calendar/grid-header/README.md +13 -0
- package/dist/calendar/header-cell/README.md +14 -0
- package/dist/calendar/heading/README.md +13 -0
- package/dist/calendar/root/README.md +24 -0
- package/dist/calendar/root/calendar-root-test.svelte +4 -0
- package/dist/calendar/root/calendar-root-test.svelte.d.ts +1 -0
- package/dist/calendar/root/calendar-root.svelte +3 -0
- package/dist/calendar/root/calendar-root.svelte.d.ts +1 -0
- package/dist/calendar/root/context.d.ts +4 -0
- package/dist/calendar/root/context.js +28 -25
- package/dist/calendar/root/date-utils.d.ts +1 -1
- package/dist/calendar/root/date-utils.js +16 -26
- package/dist/calendar/trigger-next/README.md +14 -0
- package/dist/calendar/trigger-previous/README.md +14 -0
- package/dist/clock/README.md +75 -0
- package/dist/clock/axis/README.md +24 -0
- package/dist/clock/axis/clock-axis.svelte +37 -0
- package/dist/clock/axis/clock-axis.svelte.d.ts +8 -0
- package/dist/clock/hooks/use-wheel-scroll.svelte.d.ts +16 -0
- package/dist/clock/hooks/use-wheel-scroll.svelte.js +336 -0
- package/dist/clock/index.d.ts +10 -0
- package/dist/clock/index.js +10 -0
- package/dist/clock/index.parts.d.ts +4 -0
- package/dist/clock/index.parts.js +4 -0
- package/dist/clock/root/README.md +38 -0
- package/dist/clock/root/clock-root-test.svelte +62 -0
- package/dist/clock/root/clock-root-test.svelte.d.ts +14 -0
- package/dist/clock/root/clock-root.svelte +329 -0
- package/dist/clock/root/clock-root.svelte.d.ts +25 -0
- package/dist/clock/root/context.d.ts +22 -0
- package/dist/clock/root/context.js +15 -0
- package/dist/clock/root/resolve-visible-columns.d.ts +7 -0
- package/dist/clock/root/resolve-visible-columns.js +16 -0
- package/dist/clock/root/time-utils.d.ts +48 -0
- package/dist/clock/root/time-utils.js +314 -0
- package/dist/clock/root/wheel-options.d.ts +17 -0
- package/dist/clock/root/wheel-options.js +63 -0
- package/dist/clock/wheel-column/README.md +25 -0
- package/dist/clock/wheel-column/clock-wheel-column-bindable-test.svelte +16 -0
- package/dist/clock/wheel-column/clock-wheel-column-bindable-test.svelte.d.ts +3 -0
- package/dist/clock/wheel-column/clock-wheel-column-custom-snippet-test.svelte +29 -0
- package/dist/clock/wheel-column/clock-wheel-column-custom-snippet-test.svelte.d.ts +6 -0
- package/dist/clock/wheel-column/clock-wheel-column-default-height-test.svelte +11 -0
- package/dist/clock/wheel-column/clock-wheel-column-default-height-test.svelte.d.ts +3 -0
- package/dist/clock/wheel-column/clock-wheel-column-test.svelte +38 -0
- package/dist/clock/wheel-column/clock-wheel-column-test.svelte.d.ts +12 -0
- package/dist/clock/wheel-column/clock-wheel-column-tp-test.svelte +38 -0
- package/dist/clock/wheel-column/clock-wheel-column-tp-test.svelte.d.ts +12 -0
- package/dist/clock/wheel-column/clock-wheel-column-untagged-snippet-test.svelte +29 -0
- package/dist/clock/wheel-column/clock-wheel-column-untagged-snippet-test.svelte.d.ts +6 -0
- package/dist/clock/wheel-column/clock-wheel-column.svelte +499 -0
- package/dist/clock/wheel-column/clock-wheel-column.svelte.d.ts +17 -0
- package/dist/clock/wheel-item/README.md +17 -0
- package/dist/clock/wheel-item/clock-wheel-item.svelte +49 -0
- package/dist/clock/wheel-item/clock-wheel-item.svelte.d.ts +17 -0
- package/dist/combobox/TODO.md +28 -175
- package/dist/combobox/button/combobox-button.svelte +2 -0
- package/dist/combobox/root/combobox.svelte +30 -0
- package/dist/datepicker/README.md +100 -0
- package/dist/datepicker/TODO.md +28 -0
- package/dist/datepicker/calendar/README.md +19 -0
- package/dist/datepicker/calendar/date-picker-calendar-unsafe-props-test.svelte +60 -0
- package/dist/datepicker/calendar/date-picker-calendar-unsafe-props-test.svelte.d.ts +3 -0
- package/dist/datepicker/calendar/date-picker-calendar.svelte +65 -0
- package/dist/datepicker/calendar/date-picker-calendar.svelte.d.ts +10 -0
- package/dist/datepicker/index.d.ts +18 -0
- package/dist/datepicker/index.js +18 -0
- package/dist/datepicker/index.parts.d.ts +14 -0
- package/dist/datepicker/index.parts.js +14 -0
- package/dist/datepicker/input/README.md +15 -0
- package/dist/datepicker/input/date-picker-input.svelte +108 -0
- package/dist/datepicker/input/date-picker-input.svelte.d.ts +11 -0
- package/dist/datepicker/internal/strict-props.d.ts +2 -0
- package/dist/datepicker/internal/strict-props.js +28 -0
- package/dist/datepicker/popover/README.md +20 -0
- package/dist/datepicker/popover/date-picker-popover-handler-test.svelte +57 -0
- package/dist/datepicker/popover/date-picker-popover-handler-test.svelte.d.ts +3 -0
- package/dist/datepicker/popover/date-picker-popover-unsafe-props-test.svelte +45 -0
- package/dist/datepicker/popover/date-picker-popover-unsafe-props-test.svelte.d.ts +18 -0
- package/dist/datepicker/popover/date-picker-popover.svelte +87 -0
- package/dist/datepicker/popover/date-picker-popover.svelte.d.ts +7 -0
- package/dist/datepicker/root/README.md +38 -0
- package/dist/datepicker/root/context.d.ts +43 -0
- package/dist/datepicker/root/context.js +15 -0
- package/dist/datepicker/root/date-picker-bindable-empty-test.svelte +24 -0
- package/dist/datepicker/root/date-picker-bindable-empty-test.svelte.d.ts +3 -0
- package/dist/datepicker/root/date-picker-bindable-test.svelte +41 -0
- package/dist/datepicker/root/date-picker-bindable-test.svelte.d.ts +3 -0
- package/dist/datepicker/root/date-picker-empty-test.svelte +47 -0
- package/dist/datepicker/root/date-picker-empty-test.svelte.d.ts +3 -0
- package/dist/datepicker/root/date-picker-locale-typing-test.svelte +47 -0
- package/dist/datepicker/root/date-picker-locale-typing-test.svelte.d.ts +3 -0
- package/dist/datepicker/root/date-picker-open-cancel-test.svelte +54 -0
- package/dist/datepicker/root/date-picker-open-cancel-test.svelte.d.ts +8 -0
- package/dist/datepicker/root/date-picker-root.svelte +495 -0
- package/dist/datepicker/root/date-picker-root.svelte.d.ts +24 -0
- package/dist/datepicker/root/date-picker-test.svelte +86 -0
- package/dist/datepicker/root/date-picker-test.svelte.d.ts +13 -0
- package/dist/datepicker/root/date-utils.d.ts +17 -0
- package/dist/datepicker/root/date-utils.js +138 -0
- package/dist/datepicker/root/draft-evaluation.d.ts +13 -0
- package/dist/datepicker/root/draft-evaluation.js +56 -0
- package/dist/datepicker/root/focus-controller.d.ts +3 -0
- package/dist/datepicker/root/focus-controller.js +15 -0
- package/dist/datepicker/root/open-change.d.ts +5 -0
- package/dist/datepicker/root/open-change.js +13 -0
- package/dist/datepicker/root/open-controller.d.ts +7 -0
- package/dist/datepicker/root/open-controller.js +15 -0
- package/dist/datepicker/root/segment-controller.d.ts +8 -0
- package/dist/datepicker/root/segment-controller.js +53 -0
- package/dist/datepicker/root/segment-state.d.ts +18 -0
- package/dist/datepicker/root/segment-state.js +134 -0
- package/dist/datepicker/root/value-commit.d.ts +4 -0
- package/dist/datepicker/root/value-commit.js +8 -0
- package/dist/datepicker/segment/README.md +14 -0
- package/dist/datepicker/segment/date-picker-segment.svelte +319 -0
- package/dist/datepicker/segment/date-picker-segment.svelte.d.ts +9 -0
- package/dist/datepicker/trigger/README.md +14 -0
- package/dist/datepicker/trigger/date-picker-trigger.svelte +110 -0
- package/dist/datepicker/trigger/date-picker-trigger.svelte.d.ts +9 -0
- package/dist/dialog/content/dialog-content.svelte +6 -6
- package/dist/dialog/root/context.d.ts +2 -1
- package/dist/dialog/root/dialog-root.svelte +9 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/listbox/root/listbox.svelte +44 -0
- package/dist/popover/README.md +10 -0
- package/dist/popover/content/popover-content-standalone-test.svelte +28 -0
- package/dist/popover/content/popover-content-standalone-test.svelte.d.ts +6 -0
- package/dist/popover/content/popover-content-test.svelte +2 -1
- package/dist/popover/content/popover-content-test.svelte.d.ts +2 -1
- package/dist/popover/content/popover-content.svelte +91 -18
- package/dist/popover/content/popover-content.svelte.d.ts +5 -1
- package/dist/popover/index.d.ts +1 -1
- package/dist/popover/index.js +1 -3
- package/dist/popover/root/README.md +10 -15
- package/dist/popover/root/context.d.ts +16 -7
- package/dist/popover/root/context.js +0 -2
- package/dist/popover/root/focus-state.d.ts +4 -0
- package/dist/popover/root/focus-state.js +33 -0
- package/dist/popover/root/popover-root.svelte +90 -17
- package/dist/popover/root/popover-root.svelte.d.ts +2 -1
- package/dist/popover/root/popover-test.svelte +2 -1
- package/dist/popover/root/popover-test.svelte.d.ts +2 -1
- package/dist/popover/trigger/popover-trigger-button.svelte +4 -4
- package/dist/popover/trigger/popover-trigger.svelte +1 -1
- package/dist/portal/portal.svelte +3 -1
- package/dist/primitives/click-outside.d.ts +1 -1
- package/dist/primitives/click-outside.js +1 -1
- package/dist/primitives/focus-trap.d.ts +7 -2
- package/dist/primitives/focus-trap.js +50 -17
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/input-modality.d.ts +7 -0
- package/dist/primitives/input-modality.js +125 -0
- package/dist/test-utils/focus-contract.d.ts +3 -0
- package/dist/test-utils/focus-contract.js +26 -0
- package/dist/timepicker/IMPLEMENTATION_PLAN.md +254 -0
- package/dist/timepicker/README.md +97 -0
- package/dist/timepicker/TODO.md +86 -0
- package/dist/timepicker/clock/README.md +14 -0
- package/dist/timepicker/clock/time-picker-clock-test.svelte +45 -0
- package/dist/timepicker/clock/time-picker-clock-test.svelte.d.ts +11 -0
- package/dist/timepicker/clock/time-picker-clock.svelte +65 -0
- package/dist/timepicker/clock/time-picker-clock.svelte.d.ts +10 -0
- package/dist/timepicker/index.d.ts +14 -0
- package/dist/timepicker/index.js +14 -0
- package/dist/timepicker/index.parts.d.ts +8 -0
- package/dist/timepicker/index.parts.js +8 -0
- package/dist/timepicker/input/README.md +15 -0
- package/dist/timepicker/input/time-picker-input-forwarding-test.svelte +40 -0
- package/dist/timepicker/input/time-picker-input-forwarding-test.svelte.d.ts +3 -0
- package/dist/timepicker/input/time-picker-input.svelte +109 -0
- package/dist/timepicker/input/time-picker-input.svelte.d.ts +11 -0
- package/dist/timepicker/internal/strict-props.d.ts +4 -0
- package/dist/timepicker/internal/strict-props.js +51 -0
- package/dist/timepicker/popover/README.md +20 -0
- package/dist/timepicker/popover/time-picker-popover-unsafe-props-test.svelte +22 -0
- package/dist/timepicker/popover/time-picker-popover-unsafe-props-test.svelte.d.ts +3 -0
- package/dist/timepicker/popover/time-picker-popover.svelte +89 -0
- package/dist/timepicker/popover/time-picker-popover.svelte.d.ts +7 -0
- package/dist/timepicker/root/README.md +42 -0
- package/dist/timepicker/root/context.d.ts +51 -0
- package/dist/timepicker/root/context.js +15 -0
- package/dist/timepicker/root/time-picker-12h-test.svelte +22 -0
- package/dist/timepicker/root/time-picker-12h-test.svelte.d.ts +3 -0
- package/dist/timepicker/root/time-picker-bindable-test.svelte +25 -0
- package/dist/timepicker/root/time-picker-bindable-test.svelte.d.ts +3 -0
- package/dist/timepicker/root/time-picker-empty-test.svelte +20 -0
- package/dist/timepicker/root/time-picker-empty-test.svelte.d.ts +3 -0
- package/dist/timepicker/root/time-picker-root.svelte +625 -0
- package/dist/timepicker/root/time-picker-root.svelte.d.ts +28 -0
- package/dist/timepicker/root/time-picker-test.svelte +72 -0
- package/dist/timepicker/root/time-picker-test.svelte.d.ts +15 -0
- package/dist/timepicker/root/time-utils.d.ts +1 -0
- package/dist/timepicker/root/time-utils.js +3 -0
- package/dist/timepicker/segment/README.md +14 -0
- package/dist/timepicker/segment/time-picker-segment.svelte +365 -0
- package/dist/timepicker/segment/time-picker-segment.svelte.d.ts +9 -0
- package/dist/timepicker/trigger/README.md +14 -0
- package/dist/timepicker/trigger/time-picker-trigger-forwarding-test.svelte +35 -0
- package/dist/timepicker/trigger/time-picker-trigger-forwarding-test.svelte.d.ts +3 -0
- package/dist/timepicker/trigger/time-picker-trigger.svelte +122 -0
- package/dist/timepicker/trigger/time-picker-trigger.svelte.d.ts +9 -0
- package/dist/utils/date-only.d.ts +11 -0
- package/dist/utils/date-only.js +53 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +16 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages scroll-based item selection for a wheel column.
|
|
3
|
+
*
|
|
4
|
+
* All snapping is handled in JavaScript (no CSS `scroll-snap-type`).
|
|
5
|
+
* After scrolling settles (either short inactivity in `scroll` events
|
|
6
|
+
* or browser `scrollend`), we find the nearest centered item and run
|
|
7
|
+
* a fast 120 ms ease-out animation to align it.
|
|
8
|
+
*/
|
|
9
|
+
export function useWheelScroll(container, onSnap) {
|
|
10
|
+
let scrollEndTimer = null;
|
|
11
|
+
let silentScrollTimer = null;
|
|
12
|
+
let snapRafId = null;
|
|
13
|
+
let releaseSnapRafId = null;
|
|
14
|
+
let instantReleaseRafId = null;
|
|
15
|
+
let isSnapping = false;
|
|
16
|
+
let isSilentScroll = false;
|
|
17
|
+
let isPointerInteracting = false;
|
|
18
|
+
let hasPendingPointerReleaseSnap = false;
|
|
19
|
+
const supportsScrollEnd = 'onscrollend' in window;
|
|
20
|
+
const supportsPointerEvents = 'onpointerdown' in window;
|
|
21
|
+
const wheelDebugWindow = window;
|
|
22
|
+
let lastScrollAt = 0;
|
|
23
|
+
let gestureStartAt = 0;
|
|
24
|
+
let scrollEventsInGesture = 0;
|
|
25
|
+
let currentGestureId = 0;
|
|
26
|
+
function isDebugEnabled() {
|
|
27
|
+
return (wheelDebugWindow.__HK_CLOCK_WHEEL_DEBUG__ === true || container.dataset.wheelDebug === 'true');
|
|
28
|
+
}
|
|
29
|
+
function debugLog(event, details) {
|
|
30
|
+
if (!isDebugEnabled())
|
|
31
|
+
return;
|
|
32
|
+
const timestamp = Number(performance.now().toFixed(1));
|
|
33
|
+
console.info('[Clock.WheelDebug]', {
|
|
34
|
+
event,
|
|
35
|
+
t: timestamp,
|
|
36
|
+
gestureId: currentGestureId,
|
|
37
|
+
supportsScrollEnd,
|
|
38
|
+
...details
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function clearSilentScroll() {
|
|
42
|
+
isSilentScroll = false;
|
|
43
|
+
if (silentScrollTimer) {
|
|
44
|
+
clearTimeout(silentScrollTimer);
|
|
45
|
+
silentScrollTimer = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function startSilentScrollWindow() {
|
|
49
|
+
isSilentScroll = true;
|
|
50
|
+
if (silentScrollTimer) {
|
|
51
|
+
clearTimeout(silentScrollTimer);
|
|
52
|
+
}
|
|
53
|
+
silentScrollTimer = setTimeout(() => {
|
|
54
|
+
clearSilentScroll();
|
|
55
|
+
}, 600);
|
|
56
|
+
}
|
|
57
|
+
function getItemElements() {
|
|
58
|
+
const taggedItems = Array.from(container.querySelectorAll('[data-wheel-item]'));
|
|
59
|
+
if (taggedItems.length > 0)
|
|
60
|
+
return taggedItems;
|
|
61
|
+
return Array.from(container.children).filter((child) => {
|
|
62
|
+
if (!(child instanceof HTMLElement))
|
|
63
|
+
return false;
|
|
64
|
+
if (child.matches('[data-wheel-spacer], [data-wheel-highlight], [role="status"], .sr-only')) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function getCenteredIndex() {
|
|
71
|
+
const items = getItemElements();
|
|
72
|
+
if (items.length === 0)
|
|
73
|
+
return -1;
|
|
74
|
+
const centerLine = container.scrollTop + container.clientHeight / 2;
|
|
75
|
+
let closestIndex = -1;
|
|
76
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
77
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
78
|
+
const item = items[index];
|
|
79
|
+
const itemCenter = item.offsetTop + item.offsetHeight / 2;
|
|
80
|
+
const distance = Math.abs(itemCenter - centerLine);
|
|
81
|
+
if (distance < closestDistance) {
|
|
82
|
+
closestDistance = distance;
|
|
83
|
+
closestIndex = index;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return closestIndex;
|
|
87
|
+
}
|
|
88
|
+
/* ── snap animation ─────────────────────────────────────────────── */
|
|
89
|
+
function cancelSnap() {
|
|
90
|
+
if (snapRafId !== null) {
|
|
91
|
+
cancelAnimationFrame(snapRafId);
|
|
92
|
+
snapRafId = null;
|
|
93
|
+
}
|
|
94
|
+
if (releaseSnapRafId !== null) {
|
|
95
|
+
cancelAnimationFrame(releaseSnapRafId);
|
|
96
|
+
releaseSnapRafId = null;
|
|
97
|
+
}
|
|
98
|
+
if (instantReleaseRafId !== null) {
|
|
99
|
+
cancelAnimationFrame(instantReleaseRafId);
|
|
100
|
+
instantReleaseRafId = null;
|
|
101
|
+
}
|
|
102
|
+
isSnapping = false;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run a 120 ms ease-out-cubic animation from the current scrollTop to
|
|
106
|
+
* `target`. During the animation `isSnapping` is true so incoming
|
|
107
|
+
* scroll / scrollend events are ignored.
|
|
108
|
+
*/
|
|
109
|
+
function animateSnapTo(target) {
|
|
110
|
+
cancelSnap();
|
|
111
|
+
const start = container.scrollTop;
|
|
112
|
+
const distance = target - start;
|
|
113
|
+
if (Math.abs(distance) < 1)
|
|
114
|
+
return;
|
|
115
|
+
isSnapping = true;
|
|
116
|
+
const duration = 120;
|
|
117
|
+
const t0 = performance.now();
|
|
118
|
+
debugLog('snap-animation-start', {
|
|
119
|
+
start: Number(start.toFixed(2)),
|
|
120
|
+
target: Number(target.toFixed(2)),
|
|
121
|
+
distance: Number(distance.toFixed(2)),
|
|
122
|
+
durationMs: duration
|
|
123
|
+
});
|
|
124
|
+
function frame(now) {
|
|
125
|
+
const elapsed = now - t0;
|
|
126
|
+
const t = Math.min(elapsed / duration, 1);
|
|
127
|
+
const eased = 1 - (1 - t) ** 3; // ease-out cubic
|
|
128
|
+
container.scrollTop = start + distance * eased;
|
|
129
|
+
if (t < 1) {
|
|
130
|
+
snapRafId = requestAnimationFrame(frame);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
snapRafId = null;
|
|
134
|
+
debugLog('snap-animation-end', {
|
|
135
|
+
elapsedMs: Number((performance.now() - t0).toFixed(1)),
|
|
136
|
+
finalScrollTop: Number(container.scrollTop.toFixed(2))
|
|
137
|
+
});
|
|
138
|
+
// Keep the flag only until next frame so trailing animation
|
|
139
|
+
// events are ignored without creating a visible dead-zone.
|
|
140
|
+
releaseSnapRafId = requestAnimationFrame(() => {
|
|
141
|
+
releaseSnapRafId = null;
|
|
142
|
+
isSnapping = false;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
snapRafId = requestAnimationFrame(frame);
|
|
147
|
+
}
|
|
148
|
+
/* ── scroll settle ──────────────────────────────────────────────── */
|
|
149
|
+
/**
|
|
150
|
+
* Called when the user's scroll gesture has finished.
|
|
151
|
+
* 1. Identify the closest item to the viewport centre.
|
|
152
|
+
* 2. Emit `onSnap` so the consumer can update the selected value.
|
|
153
|
+
* 3. Animate to perfectly centre the item (120 ms).
|
|
154
|
+
*/
|
|
155
|
+
function snapToCenter(source) {
|
|
156
|
+
if (isSnapping || isSilentScroll)
|
|
157
|
+
return;
|
|
158
|
+
const now = performance.now();
|
|
159
|
+
const idleBeforeSnapMs = lastScrollAt > 0 ? now - lastScrollAt : null;
|
|
160
|
+
debugLog('snap-evaluate', {
|
|
161
|
+
source,
|
|
162
|
+
idleBeforeSnapMs: idleBeforeSnapMs === null ? null : Number(idleBeforeSnapMs.toFixed(1)),
|
|
163
|
+
scrollEventsInGesture
|
|
164
|
+
});
|
|
165
|
+
const centeredIndex = getCenteredIndex();
|
|
166
|
+
if (centeredIndex < 0)
|
|
167
|
+
return;
|
|
168
|
+
const resolvedIndex = onSnap(centeredIndex);
|
|
169
|
+
const snapIndex = typeof resolvedIndex === 'number' ? resolvedIndex : centeredIndex;
|
|
170
|
+
if (!Number.isInteger(snapIndex) || snapIndex < 0)
|
|
171
|
+
return;
|
|
172
|
+
const items = getItemElements();
|
|
173
|
+
const target = items[snapIndex];
|
|
174
|
+
if (!target)
|
|
175
|
+
return;
|
|
176
|
+
const idealScrollTop = target.offsetTop - (container.clientHeight - target.offsetHeight) / 2;
|
|
177
|
+
const clamped = Math.max(0, idealScrollTop);
|
|
178
|
+
const diff = Math.abs(container.scrollTop - clamped);
|
|
179
|
+
debugLog('snap-target', {
|
|
180
|
+
centeredIndex,
|
|
181
|
+
snapIndex,
|
|
182
|
+
diffPx: Number(diff.toFixed(2)),
|
|
183
|
+
currentScrollTop: Number(container.scrollTop.toFixed(2)),
|
|
184
|
+
targetScrollTop: Number(clamped.toFixed(2))
|
|
185
|
+
});
|
|
186
|
+
scrollEventsInGesture = 0;
|
|
187
|
+
if (diff >= 2) {
|
|
188
|
+
animateSnapTo(clamped);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/* ── event wiring ───────────────────────────────────────────────── */
|
|
192
|
+
function clearScrollEndTimer() {
|
|
193
|
+
if (scrollEndTimer) {
|
|
194
|
+
clearTimeout(scrollEndTimer);
|
|
195
|
+
scrollEndTimer = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function handleScroll() {
|
|
199
|
+
const now = performance.now();
|
|
200
|
+
if (scrollEventsInGesture === 0) {
|
|
201
|
+
currentGestureId += 1;
|
|
202
|
+
gestureStartAt = now;
|
|
203
|
+
debugLog('gesture-start', {
|
|
204
|
+
scrollTop: Number(container.scrollTop.toFixed(2))
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
scrollEventsInGesture += 1;
|
|
208
|
+
lastScrollAt = now;
|
|
209
|
+
if (isSnapping) {
|
|
210
|
+
debugLog('scroll-ignored-while-snapping');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (isSilentScroll) {
|
|
214
|
+
debugLog('scroll-ignored-while-silent-scroll');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (isPointerInteracting) {
|
|
218
|
+
hasPendingPointerReleaseSnap = true;
|
|
219
|
+
clearScrollEndTimer();
|
|
220
|
+
debugLog('scroll-deferred-until-pointer-release');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Always debounce by scroll inactivity so we don't rely solely on
|
|
224
|
+
// potentially late `scrollend` dispatch.
|
|
225
|
+
clearScrollEndTimer();
|
|
226
|
+
scrollEndTimer = setTimeout(() => snapToCenter(supportsScrollEnd ? 'inactivity-timeout' : 'fallback-timeout'), 120);
|
|
227
|
+
}
|
|
228
|
+
function handleScrollEnd() {
|
|
229
|
+
const now = performance.now();
|
|
230
|
+
if (isSnapping) {
|
|
231
|
+
debugLog('scrollend-ignored-while-snapping');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (isSilentScroll) {
|
|
235
|
+
debugLog('scrollend-ignored-while-silent-scroll');
|
|
236
|
+
clearSilentScroll();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
debugLog('scrollend-received', {
|
|
240
|
+
sinceLastScrollMs: lastScrollAt > 0 ? Number((now - lastScrollAt).toFixed(1)) : null,
|
|
241
|
+
gestureDurationMs: gestureStartAt > 0 ? Number((now - gestureStartAt).toFixed(1)) : null,
|
|
242
|
+
scrollEventsInGesture
|
|
243
|
+
});
|
|
244
|
+
clearScrollEndTimer();
|
|
245
|
+
snapToCenter('scrollend');
|
|
246
|
+
}
|
|
247
|
+
function handlePointerDown(event) {
|
|
248
|
+
if ('pointerType' in event && event.pointerType === 'mouse' && event.button !== 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!('pointerType' in event) && event.button !== 0) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
isPointerInteracting = true;
|
|
255
|
+
hasPendingPointerReleaseSnap = false;
|
|
256
|
+
}
|
|
257
|
+
function handlePointerRelease() {
|
|
258
|
+
if (!isPointerInteracting)
|
|
259
|
+
return;
|
|
260
|
+
isPointerInteracting = false;
|
|
261
|
+
if (hasPendingPointerReleaseSnap) {
|
|
262
|
+
hasPendingPointerReleaseSnap = false;
|
|
263
|
+
clearScrollEndTimer();
|
|
264
|
+
snapToCenter('pointer-release');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
container.addEventListener('scroll', handleScroll, { passive: true });
|
|
268
|
+
if (supportsPointerEvents) {
|
|
269
|
+
container.addEventListener('pointerdown', handlePointerDown, {
|
|
270
|
+
passive: true
|
|
271
|
+
});
|
|
272
|
+
window.addEventListener('pointerup', handlePointerRelease, { passive: true });
|
|
273
|
+
window.addEventListener('pointercancel', handlePointerRelease, { passive: true });
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
container.addEventListener('mousedown', handlePointerDown);
|
|
277
|
+
window.addEventListener('mouseup', handlePointerRelease);
|
|
278
|
+
}
|
|
279
|
+
if (supportsScrollEnd) {
|
|
280
|
+
container.addEventListener('scrollend', handleScrollEnd);
|
|
281
|
+
}
|
|
282
|
+
/* ── public API ─────────────────────────────────────────────────── */
|
|
283
|
+
function scrollToIndex(index, behavior = 'smooth', options) {
|
|
284
|
+
cancelSnap();
|
|
285
|
+
clearScrollEndTimer();
|
|
286
|
+
const shouldUseSilentScroll = options?.silent === true && behavior === 'smooth';
|
|
287
|
+
if (shouldUseSilentScroll) {
|
|
288
|
+
startSilentScrollWindow();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
clearSilentScroll();
|
|
292
|
+
}
|
|
293
|
+
const items = getItemElements();
|
|
294
|
+
if (index < 0 || index >= items.length)
|
|
295
|
+
return;
|
|
296
|
+
const target = items[index];
|
|
297
|
+
const idealScrollTop = target.offsetTop - (container.clientHeight - target.offsetHeight) / 2;
|
|
298
|
+
if (behavior === 'instant') {
|
|
299
|
+
// Direct assignment – no animation. Suppress the ensuing scrollend
|
|
300
|
+
// so it doesn't trigger an unnecessary snapToCenter cycle.
|
|
301
|
+
isSnapping = true;
|
|
302
|
+
container.scrollTop = Math.max(0, idealScrollTop);
|
|
303
|
+
instantReleaseRafId = requestAnimationFrame(() => {
|
|
304
|
+
instantReleaseRafId = null;
|
|
305
|
+
isSnapping = false;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
container.scrollTo({
|
|
310
|
+
top: Math.max(0, idealScrollTop),
|
|
311
|
+
behavior: 'smooth'
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
scrollToIndex,
|
|
317
|
+
destroy: () => {
|
|
318
|
+
cancelSnap();
|
|
319
|
+
clearScrollEndTimer();
|
|
320
|
+
clearSilentScroll();
|
|
321
|
+
container.removeEventListener('scroll', handleScroll);
|
|
322
|
+
if (supportsPointerEvents) {
|
|
323
|
+
container.removeEventListener('pointerdown', handlePointerDown);
|
|
324
|
+
window.removeEventListener('pointerup', handlePointerRelease);
|
|
325
|
+
window.removeEventListener('pointercancel', handlePointerRelease);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
container.removeEventListener('mousedown', handlePointerDown);
|
|
329
|
+
window.removeEventListener('mouseup', handlePointerRelease);
|
|
330
|
+
}
|
|
331
|
+
if (supportsScrollEnd) {
|
|
332
|
+
container.removeEventListener('scrollend', handleScrollEnd);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -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;
|