@astryxdesign/lab 0.2.0-canary.8d64648 → 0.2.0-canary.8dfd7cf

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 (63) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +41 -35
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +222 -75
  4. package/dist/BottomSheet/snapOffsets.d.ts +54 -0
  5. package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
  6. package/dist/BottomSheet/snapOffsets.js +101 -0
  7. package/dist/BottomSheet/useSheetGestures.d.ts +55 -12
  8. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  9. package/dist/BottomSheet/useSheetGestures.js +383 -40
  10. package/dist/Chart/index.d.ts +1 -1
  11. package/dist/Chart/index.d.ts.map +1 -1
  12. package/dist/Chart/index.js +1 -1
  13. package/dist/Stepper/Step.d.ts +1 -1
  14. package/dist/Stepper/Step.js +1 -1
  15. package/dist/Stepper/Stepper.d.ts +1 -1
  16. package/dist/Stepper/Stepper.js +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/lab.css +15 -8
  19. package/package.json +3 -2
  20. package/src/BottomSheet/BottomSheet.doc.mjs +13 -13
  21. package/src/BottomSheet/BottomSheet.test.tsx +81 -4
  22. package/src/BottomSheet/BottomSheet.tsx +333 -97
  23. package/src/BottomSheet/snapOffsets.test.ts +146 -0
  24. package/src/BottomSheet/snapOffsets.ts +126 -0
  25. package/src/BottomSheet/useSheetGestures.doc.mjs +7 -6
  26. package/src/BottomSheet/useSheetGestures.test.ts +315 -65
  27. package/src/BottomSheet/useSheetGestures.ts +515 -55
  28. package/src/Chart/ChartArea.doc.mjs +1 -1
  29. package/src/Chart/ChartBar.doc.mjs +1 -1
  30. package/src/Chart/ChartCandlestick.doc.mjs +1 -1
  31. package/src/Chart/ChartDot.doc.mjs +1 -1
  32. package/src/Chart/ChartDotGL.doc.mjs +1 -1
  33. package/src/Chart/ChartDotGLInteractive.doc.mjs +1 -1
  34. package/src/Chart/ChartHeatmapGL.doc.mjs +1 -1
  35. package/src/Chart/ChartLine.doc.mjs +1 -1
  36. package/src/Chart/ChartStreamGL.doc.mjs +1 -1
  37. package/src/Chart/index.ts +0 -1
  38. package/src/Chat/ChatEmojiPicker.doc.mjs +1 -1
  39. package/src/Chat/ChatReactionBar.doc.mjs +1 -1
  40. package/src/Chat/ChatTypingIndicator.doc.mjs +1 -1
  41. package/src/Chat/ChatUnreadDivider.doc.mjs +1 -1
  42. package/src/ChatReasoning/ChatReasoning.doc.mjs +1 -1
  43. package/src/CircularProgress/CircularProgress.doc.mjs +3 -3
  44. package/src/CodeEditor/CodeEditor.doc.mjs +1 -1
  45. package/src/Drawer/Drawer.doc.mjs +3 -3
  46. package/src/InfoTip/InfoTip.doc.mjs +3 -3
  47. package/src/LogStream/LogStream.doc.mjs +1 -1
  48. package/src/Radial/RadialChart.doc.mjs +1 -1
  49. package/src/RichTextEditor/RichTextEditor.doc.mjs +1 -1
  50. package/src/SVGIcon/SVGIcon.doc.mjs +1 -1
  51. package/src/Sankey/SankeyChart.doc.mjs +1 -1
  52. package/src/Schedule/Schedule.doc.mjs +1 -1
  53. package/src/Stat/Stat.doc.mjs +3 -3
  54. package/src/Stepper/Step.doc.mjs +1 -1
  55. package/src/Stepper/Step.tsx +1 -1
  56. package/src/Stepper/Stepper.doc.mjs +3 -3
  57. package/src/Stepper/Stepper.tsx +1 -1
  58. package/src/ThreeD/3DBar.doc.mjs +1 -1
  59. package/src/ThreeD/3DChart.doc.mjs +1 -1
  60. package/src/ThreeD/3DScatter.doc.mjs +1 -1
  61. package/src/ThreeD/3DScatterGL.doc.mjs +1 -1
  62. package/src/ThreeD/3DSurface.doc.mjs +1 -1
  63. package/src/index.ts +1 -1
@@ -8,13 +8,14 @@
8
8
  * @output Exports useSheetGestures hook and its option/result types
9
9
  * @position Internal to BottomSheet; not exported from the lab entry point
10
10
  *
11
- * Drag-to-dismiss machinery for the bottom sheet. Tracks a pointer drag
12
- * down the block axis, translates the sliding surface live, and on release
13
- * either dismisses (past a distance or velocity threshold) or springs back
14
- * to fully open. Pointer-events based so one code path covers mouse + touch.
11
+ * Drag + snap machinery for the bottom sheet. Tracks a pointer drag down the
12
+ * block axis, translates the sliding surface live, and on release either
13
+ * settles to the nearest snap detent (a slow drag) or dismisses (a fast flick
14
+ * down). A fast flick up expands to the tallest detent. This is the core
15
+ * behavior split the sheet needs: DRAG places, SWIPE closes.
15
16
  *
16
- * Kept private to BottomSheet: the behavior is sheet-specific (a dismiss
17
- * edge on a bottom-anchored surface). It is not a general primitive and is
17
+ * Kept private to BottomSheet: a dismiss edge + detents on a bottom-anchored
18
+ * surface are inherently sheet concepts. It is not a general primitive and is
18
19
  * intentionally not exported.
19
20
  *
20
21
  * SSR-safe: no window/document access at module scope; all measurement
@@ -26,12 +27,61 @@
26
27
  * - /packages/lab/src/BottomSheet/useSheetGestures.test.ts
27
28
  */
28
29
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
30
+ import { computeDetentOffsets, resolveSettleOffset, scrimOpacityForOffset } from "./snapOffsets.js";
29
31
 
30
- // Fraction of the measured sheet height a drag must cross (with low
31
- // velocity) before release dismisses instead of springing back.
32
- const DISMISS_DISTANCE_RATIO = 0.25;
33
- // A quick flick past this speed (px/ms) dismisses regardless of distance.
34
- const DISMISS_VELOCITY = 0.5;
32
+ // A flick (fast throw) dismisses (down) or expands (up) regardless of where
33
+ // it ends. Requires both a speed and a distance floor so a small nudge
34
+ // doesn't trigger it.
35
+ const FLICK_VELOCITY = 1.2; // px/ms
36
+ const FLICK_MIN_DISTANCE = 48; // px traveled during the gesture
37
+ // On a slow drag below the shortest detent, dismiss once dragged past it by
38
+ // more than this fraction of that detent's height; otherwise snap back to it.
39
+ const DISMISS_OVERSHOOT_RATIO = 0.4;
40
+ // Within this many px of a detent, the live drag is magnetically eased toward
41
+ // it so it "clicks" into place instead of hovering just off the mark.
42
+ const MAGNET_RANGE = 40;
43
+ // Rubber-band factor for dragging up past fully-open, capped at OVERSCROLL_MAX
44
+ // (the sheet reserves that much bottom padding for the lift to reveal).
45
+ const OVERSCROLL_RESISTANCE = 0.35;
46
+ // SYNC: must match OVERSCROLL_PADDING in BottomSheet.tsx (the reserved bottom
47
+ // padding the lift reveals). Kept as a local const rather than a shared import
48
+ // so it can be used inside stylex.create there.
49
+ const OVERSCROLL_MAX = 48;
50
+
51
+ // Short haptic tick on detent settle where supported. iOS Safari doesn't
52
+ // expose navigator.vibrate, so this is a no-op there; skipped under
53
+ // reduced-motion.
54
+ function hapticTick() {
55
+ if (typeof navigator === 'undefined' || typeof navigator.vibrate !== 'function') {
56
+ return;
57
+ }
58
+ if (prefersReducedMotion()) {
59
+ return;
60
+ }
61
+ navigator.vibrate(8);
62
+ }
63
+
64
+ // Pull a value toward the nearest of `targets` when within MAGNET_RANGE, easing
65
+ // the last stretch so the surface settles crisply onto a detent while dragging.
66
+ function magnetize(value, targets) {
67
+ let nearestTarget = targets[0];
68
+ let nearestDist = Math.abs(value - nearestTarget);
69
+ for (const t of targets) {
70
+ const d = Math.abs(value - t);
71
+ if (d < nearestDist) {
72
+ nearestDist = d;
73
+ nearestTarget = t;
74
+ }
75
+ }
76
+ if (nearestDist >= MAGNET_RANGE) {
77
+ return value;
78
+ }
79
+ // Ease-in over the range: pull grows as you approach (t^2), so the click
80
+ // feels magnetic near the detent but doesn't fight a deliberate drag-through.
81
+ const t = nearestDist / MAGNET_RANGE;
82
+ const pull = 1 - t * t;
83
+ return value + (nearestTarget - value) * pull;
84
+ }
35
85
  function prefersReducedMotion() {
36
86
  if (typeof window === 'undefined' || !window.matchMedia) {
37
87
  return false;
@@ -40,15 +90,17 @@ function prefersReducedMotion() {
40
90
  }
41
91
 
42
92
  /**
43
- * Drag-to-dismiss machinery for the bottom sheet. Returns props to spread on
44
- * the grab handle and the sliding surface, plus live drag state for callers
45
- * that want to drive their own animation.
93
+ * Drag + snap machinery for the bottom sheet. Returns props to spread on the
94
+ * grab handle and the sliding surface, plus live drag state. A slow drag
95
+ * settles to the nearest detent; a fast downward flick dismisses; a fast
96
+ * upward flick expands to the tallest detent.
46
97
  *
47
98
  * @example
48
99
  * ```
49
100
  * const {contentProps, handleProps} = useSheetGestures({
50
101
  * isOpen,
51
102
  * onDismiss: () => onOpenChange(false),
103
+ * snapHeights: () => [240, 0.5 * (window.visualViewport?.height ?? 0)],
52
104
  * });
53
105
  * <div {...contentProps}>
54
106
  * <div {...handleProps} />
@@ -58,60 +110,146 @@ function prefersReducedMotion() {
58
110
  */
59
111
  export function useSheetGestures({
60
112
  isOpen,
61
- onDismiss
113
+ onDismiss,
114
+ snapHeights,
115
+ onSnap,
116
+ onScrimOpacity
62
117
  }) {
63
118
  const [dragOffset, setDragOffset] = useState(0);
119
+ const [settledOffset, setSettledOffset] = useState(0);
64
120
  const [isDragging, setIsDragging] = useState(false);
65
-
66
- // Latest callback ref so pointer handlers stay stable across renders.
67
121
  const onDismissRef = useRef(onDismiss);
122
+ const onSnapRef = useRef(onSnap);
123
+ const onScrimOpacityRef = useRef(onScrimOpacity);
124
+ const snapHeightsRef = useRef(snapHeights);
68
125
  useEffect(() => {
69
126
  onDismissRef.current = onDismiss;
127
+ onSnapRef.current = onSnap;
128
+ onScrimOpacityRef.current = onScrimOpacity;
129
+ snapHeightsRef.current = snapHeights;
70
130
  });
71
131
 
72
132
  // Live drag bookkeeping (refs so pointermove doesn't churn renders).
73
133
  const dragStateRef = useRef(null);
74
134
 
75
- // Reset drag state each time the sheet re-opens.
135
+ // Fully-open height, tracked by a ResizeObserver (see sheetRef) so detents
136
+ // stay correct across rotation / viewport changes without re-measuring.
137
+ const sheetHeightRef = useRef(0);
138
+ const sheetElRef = useRef(null);
139
+ const observerRef = useRef(null);
140
+ const sheetRef = useCallback(node => {
141
+ observerRef.current?.disconnect();
142
+ observerRef.current = null;
143
+ sheetElRef.current = node;
144
+ if (!node || typeof ResizeObserver === 'undefined') {
145
+ if (node) {
146
+ sheetHeightRef.current = node.getBoundingClientRect().height;
147
+ }
148
+ return;
149
+ }
150
+ sheetHeightRef.current = node.getBoundingClientRect().height;
151
+ const ro = new ResizeObserver(entries => {
152
+ const box = entries[0]?.contentRect;
153
+ if (box && box.height > 0) {
154
+ sheetHeightRef.current = box.height;
155
+ }
156
+ });
157
+ ro.observe(node);
158
+ observerRef.current = ro;
159
+ }, []);
160
+ useEffect(() => () => observerRef.current?.disconnect(), []);
161
+
162
+ // Reset to the tallest detent each time the sheet re-opens.
76
163
  useEffect(() => {
77
164
  if (isOpen) {
78
165
  setDragOffset(0);
166
+ setSettledOffset(0);
79
167
  setIsDragging(false);
80
168
  }
81
169
  }, [isOpen]);
82
- const measureHeight = useCallback(el => {
83
- const sheet = el?.closest('[data-astryx-sheet]') ?? el;
84
- return sheet?.getBoundingClientRect().height ?? 0;
170
+
171
+ // The fully-open height for detent math. Prefer the ResizeObserver-locked
172
+ // value; fall back to a live measure of the tracked sheet element if the
173
+ // observer hasn't reported yet.
174
+ const measureHeight = useCallback(() => {
175
+ if (sheetHeightRef.current > 0) {
176
+ return sheetHeightRef.current;
177
+ }
178
+ return sheetElRef.current?.getBoundingClientRect().height ?? 0;
179
+ }, []);
180
+
181
+ // Detent translate offsets (px) from the tallest detent, ascending. The
182
+ // tallest detent is offset 0; shorter detents translate further down.
183
+ // Snap heights are resolved lazily here (at drag start) so they reflect the
184
+ // current viewport without a persistent listener.
185
+ const detentOffsets = useCallback(height => {
186
+ return computeDetentOffsets(height, snapHeightsRef.current?.() ?? []);
85
187
  }, []);
86
- const settleFromDrag = useCallback((offset, velocity, height) => {
87
- const dismiss = offset > height * DISMISS_DISTANCE_RATIO || velocity > DISMISS_VELOCITY;
88
- if (dismiss) {
188
+ const settleFromDrag = useCallback((offset, velocity, height, dir, travel, baseOffset) => {
189
+ const offsets = detentOffsets(height);
190
+ const maxOffset = offsets[offsets.length - 1];
191
+ const shortestDetentHeight = height - maxOffset;
192
+ const speed = Math.abs(velocity);
193
+ const isFlick = speed > FLICK_VELOCITY && travel > FLICK_MIN_DISTANCE;
194
+ if (dir > 0 && isFlick) {
89
195
  onDismissRef.current();
90
196
  return;
91
197
  }
92
- // Below threshold: spring back to fully open.
93
- setDragOffset(0);
94
- }, []);
95
- const handlePointerDown = useCallback(event => {
198
+ // Fast upward flick = expand to the tallest detent (the sheet's full
199
+ // provided height).
200
+ if (dir < 0 && isFlick) {
201
+ setSettledOffset(0);
202
+ onSnapRef.current?.(height);
203
+ onScrimOpacityRef.current?.(1);
204
+ hapticTick();
205
+ return;
206
+ }
207
+ if (offset > maxOffset + shortestDetentHeight * DISMISS_OVERSHOOT_RATIO) {
208
+ onDismissRef.current();
209
+ return;
210
+ }
211
+ // Settle to the nearest detent in the drag direction (never back past
212
+ // the starting detent), de-duped and direction-clamped by the util.
213
+ const target = resolveSettleOffset(offset, offsets, dir, baseOffset);
214
+ setSettledOffset(target);
215
+ onSnapRef.current?.(height - target);
216
+ // Keep the scrim in sync with the resting detent (hidden at the peek).
217
+ const dismissOffset = maxOffset + shortestDetentHeight * DISMISS_OVERSHOOT_RATIO;
218
+ onScrimOpacityRef.current?.(scrimOpacityForOffset(target, offsets, dismissOffset));
219
+ // Haptic "tick" when landing on a different detent (where supported).
220
+ if (target !== baseOffset) {
221
+ hapticTick();
222
+ }
223
+ }, [detentOffsets]);
224
+ const beginDrag = useCallback((event, sheetHeight, startCoord) => {
96
225
  const target = event.currentTarget;
97
226
  target.setPointerCapture?.(event.pointerId);
227
+ // `startCoord` lets a body-overscroll drag anchor at the original
228
+ // pointer-down position (not the promotion point), so the first frame's
229
+ // delta reflects the full pull distance.
230
+ const start = startCoord ?? event.clientY;
98
231
  dragStateRef.current = {
99
232
  pointerId: event.pointerId,
100
- startCoord: event.clientY,
233
+ startCoord: start,
101
234
  lastCoord: event.clientY,
102
235
  lastTime: event.timeStamp,
103
236
  velocity: 0,
104
- height: measureHeight(target)
237
+ height: sheetHeight,
238
+ baseOffset: settledOffset
105
239
  };
240
+ // Seed dragOffset at the resting detent so flipping isDragging doesn't
241
+ // jump the sheet to fully-open for one frame.
242
+ setDragOffset(settledOffset);
106
243
  setIsDragging(true);
107
- }, [measureHeight]);
244
+ }, [settledOffset]);
245
+ const handlePointerDown = useCallback(event => {
246
+ beginDrag(event, measureHeight());
247
+ }, [beginDrag, measureHeight]);
108
248
  const handlePointerMove = useCallback(event => {
109
249
  const state = dragStateRef.current;
110
250
  if (!state || state.pointerId !== event.pointerId) {
111
251
  return;
112
252
  }
113
- // Dragging DOWN (increasing Y) collapses/dismisses. Only allow drag in
114
- // the closing direction past the fully-open position (no overscroll).
115
253
  const delta = event.clientY - state.startCoord;
116
254
  const dt = event.timeStamp - state.lastTime;
117
255
  if (dt > 0) {
@@ -119,8 +257,30 @@ export function useSheetGestures({
119
257
  state.lastCoord = event.clientY;
120
258
  state.lastTime = event.timeStamp;
121
259
  }
122
- setDragOffset(Math.max(0, delta));
123
- }, []);
260
+ const offsets = detentOffsets(state.height);
261
+ const raw = state.baseOffset + delta;
262
+ const maxDetentOffset = offsets[offsets.length - 1];
263
+ let next;
264
+ if (raw < 0) {
265
+ // Up past fully-open: damped + capped rubber-band; springs back on release.
266
+ next = Math.max(-OVERSCROLL_MAX, raw * OVERSCROLL_RESISTANCE);
267
+ } else if (raw > maxDetentOffset) {
268
+ // In the dismiss zone: no magnet, so it doesn't fight a drag-to-close.
269
+ next = raw;
270
+ } else {
271
+ // Between detents: magnetically ease toward a nearby one.
272
+ next = magnetize(raw, offsets);
273
+ }
274
+ setDragOffset(next);
275
+
276
+ // Mirror the scrim to the live drag: full at/above the mid detent,
277
+ // fading to hidden as it collapses onto the peek detent and through the
278
+ // dismiss overshoot.
279
+ const floorOffset = offsets[offsets.length - 1];
280
+ const shortestDetentHeight = state.height - floorOffset;
281
+ const dismissOffset = floorOffset + shortestDetentHeight * DISMISS_OVERSHOOT_RATIO;
282
+ onScrimOpacityRef.current?.(scrimOpacityForOffset(next, offsets, dismissOffset));
283
+ }, [detentOffsets]);
124
284
  const endDrag = useCallback(event => {
125
285
  const state = dragStateRef.current;
126
286
  if (!state || state.pointerId !== event.pointerId) {
@@ -128,22 +288,195 @@ export function useSheetGestures({
128
288
  }
129
289
  const target = event.currentTarget;
130
290
  target.releasePointerCapture?.(event.pointerId);
131
- const offset = Math.max(0, event.clientY - state.startCoord);
291
+ const delta = event.clientY - state.startCoord;
292
+ const offset = Math.max(0, state.baseOffset + delta);
293
+ const dir = delta === 0 ? 0 : delta > 0 ? 1 : -1;
132
294
  dragStateRef.current = null;
133
295
  setIsDragging(false);
134
- settleFromDrag(offset, state.velocity, state.height || 1);
296
+ settleFromDrag(offset, state.velocity, state.height || 1, dir, Math.abs(delta), state.baseOffset);
135
297
  }, [settleFromDrag]);
136
298
 
137
- // Reduced motion skips the settle transition (snaps instantly).
299
+ // Pointer path for the body at-top pull-down (desktop / mouse). Touch uses
300
+ // the non-passive listener below, since pointer events are cancelled once a
301
+ // native pan starts.
302
+ const armedBodyRef = useRef(null);
303
+ const handleBodyPointerDown = useCallback(event => {
304
+ const scroller = event.currentTarget;
305
+ if (scroller.scrollTop > 0) {
306
+ armedBodyRef.current = null;
307
+ return;
308
+ }
309
+ armedBodyRef.current = {
310
+ pointerId: event.pointerId,
311
+ startCoord: event.clientY,
312
+ scroller
313
+ };
314
+ }, []);
315
+ const handleBodyPointerMove = useCallback(event => {
316
+ if (dragStateRef.current) {
317
+ handlePointerMove(event);
318
+ return;
319
+ }
320
+ const armed = armedBodyRef.current;
321
+ if (!armed || armed.pointerId !== event.pointerId) {
322
+ return;
323
+ }
324
+ const delta = event.clientY - armed.startCoord;
325
+ if (delta > 0 && armed.scroller.scrollTop <= 0) {
326
+ // Downward pull at the top: promote to a sheet drag, anchored at the
327
+ // original pointer-down position so the pull distance carries over.
328
+ armedBodyRef.current = null;
329
+ beginDrag(event, measureHeight(), armed.startCoord);
330
+ handlePointerMove(event);
331
+ } else if (delta < 0) {
332
+ // Upward move = the user is scrolling; disarm so we don't hijack it.
333
+ armedBodyRef.current = null;
334
+ }
335
+ }, [beginDrag, handlePointerMove, measureHeight]);
336
+ const handleBodyEnd = useCallback(event => {
337
+ armedBodyRef.current = null;
338
+ if (dragStateRef.current) {
339
+ endDrag(event);
340
+ }
341
+ }, [endDrag]);
342
+
343
+ // Non-passive touchmove is the reliable scroll<->drag handoff on touch:
344
+ // preventDefault() at a scroll edge stops the native scroll and drives the
345
+ // sheet drag through the pointer math (Touch adapted to the fields it reads).
346
+ const bodyNodeRef = useRef(null);
347
+ const touchDragRef = useRef(null);
348
+ const prevTouchHandlers = useRef(null);
349
+ const beginDragRef = useRef(beginDrag);
350
+ const pointerMoveRef = useRef(handlePointerMove);
351
+ const endDragRef = useRef(endDrag);
352
+ const measureHeightRef = useRef(measureHeight);
353
+ useEffect(() => {
354
+ beginDragRef.current = beginDrag;
355
+ pointerMoveRef.current = handlePointerMove;
356
+ endDragRef.current = endDrag;
357
+ measureHeightRef.current = measureHeight;
358
+ });
359
+ const bodyRef = useCallback(node => {
360
+ const asPointer = (touch, target) => ({
361
+ pointerId: touch.identifier,
362
+ clientY: touch.clientY,
363
+ timeStamp: Date.now(),
364
+ currentTarget: target,
365
+ setPointerCapture: () => {},
366
+ releasePointerCapture: () => {}
367
+ });
368
+ const atTop = el => el.scrollTop <= 0;
369
+ const atBottom = el => el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
370
+ const onTouchStart = event => {
371
+ const scroller = event.currentTarget;
372
+ const touch = event.changedTouches[0];
373
+ // Arm only at a scroll edge; from mid-content this is an ordinary scroll.
374
+ // At the top, a pull DOWN hands off (collapse); at the bottom, a pull UP
375
+ // hands off (expand). Record the edge so the move handler matches it.
376
+ if (!touch) {
377
+ touchDragRef.current = null;
378
+ return;
379
+ }
380
+ const top = atTop(scroller);
381
+ const bottom = atBottom(scroller);
382
+ if (!top && !bottom) {
383
+ touchDragRef.current = null;
384
+ return;
385
+ }
386
+ touchDragRef.current = {
387
+ id: touch.identifier,
388
+ startY: touch.clientY,
389
+ top,
390
+ bottom
391
+ };
392
+ };
393
+ const onTouchMove = event => {
394
+ const scroller = event.currentTarget;
395
+ if (dragStateRef.current) {
396
+ const t = [...event.changedTouches].find(x => x.identifier === dragStateRef.current?.pointerId);
397
+ if (t) {
398
+ event.preventDefault();
399
+ pointerMoveRef.current(asPointer(t, scroller));
400
+ }
401
+ return;
402
+ }
403
+ const armed = touchDragRef.current;
404
+ if (!armed) {
405
+ return;
406
+ }
407
+ const t = [...event.changedTouches].find(x => x.identifier === armed.id);
408
+ if (!t) {
409
+ return;
410
+ }
411
+ const delta = t.clientY - armed.startY;
412
+ // Promote to a sheet drag on a pull that opposes the armed edge and can
413
+ // no longer scroll that way: at the top, a downward pull (delta > 0)
414
+ // collapses; at the bottom, an upward pull (delta < 0) expands. The
415
+ // opposite direction is a real scroll, so disarm and let it through.
416
+ const pullDownAtTop = armed.top && delta > 0 && atTop(scroller);
417
+ const pullUpAtBottom = armed.bottom && delta < 0 && atBottom(scroller);
418
+ if (pullDownAtTop || pullUpAtBottom) {
419
+ event.preventDefault();
420
+ touchDragRef.current = null;
421
+ beginDragRef.current(asPointer(t, scroller), measureHeightRef.current(), armed.startY);
422
+ pointerMoveRef.current(asPointer(t, scroller));
423
+ } else if (armed.top && delta < 0 || armed.bottom && delta > 0) {
424
+ // Scrolling away from the armed edge; hand back to native scroll.
425
+ touchDragRef.current = null;
426
+ }
427
+ };
428
+ const onTouchEnd = event => {
429
+ touchDragRef.current = null;
430
+ if (dragStateRef.current) {
431
+ const t = event.changedTouches[0];
432
+ if (t) {
433
+ endDragRef.current(asPointer(t, event.currentTarget));
434
+ }
435
+ }
436
+ };
437
+ const prev = bodyNodeRef.current;
438
+ if (prev && prevTouchHandlers.current) {
439
+ const h = prevTouchHandlers.current;
440
+ prev.removeEventListener('touchstart', h.start);
441
+ prev.removeEventListener('touchmove', h.move);
442
+ prev.removeEventListener('touchend', h.end);
443
+ prev.removeEventListener('touchcancel', h.end);
444
+ }
445
+ bodyNodeRef.current = node;
446
+ if (node) {
447
+ node.addEventListener('touchstart', onTouchStart, {
448
+ passive: true
449
+ });
450
+ node.addEventListener('touchmove', onTouchMove, {
451
+ passive: false
452
+ });
453
+ node.addEventListener('touchend', onTouchEnd, {
454
+ passive: true
455
+ });
456
+ node.addEventListener('touchcancel', onTouchEnd, {
457
+ passive: true
458
+ });
459
+ prevTouchHandlers.current = {
460
+ start: onTouchStart,
461
+ move: onTouchMove,
462
+ end: onTouchEnd
463
+ };
464
+ } else {
465
+ prevTouchHandlers.current = null;
466
+ }
467
+ }, []);
138
468
  const reducedMotion = useMemo(prefersReducedMotion, [isOpen]);
469
+
470
+ // While dragging, follow the finger; otherwise rest at the settled detent.
471
+ const activeOffset = isDragging ? dragOffset : settledOffset;
139
472
  const contentProps = useMemo(() => ({
140
473
  style: {
141
- transform: dragOffset !== 0 ? `translateY(${dragOffset}px)` : undefined,
474
+ transform: activeOffset !== 0 ? `translateY(${activeOffset}px)` : undefined,
142
475
  transition: isDragging || reducedMotion ? 'none' : undefined,
143
476
  touchAction: 'none',
144
477
  overscrollBehavior: 'contain'
145
478
  }
146
- }), [dragOffset, isDragging, reducedMotion]);
479
+ }), [activeOffset, isDragging, reducedMotion]);
147
480
  const handleProps = useMemo(() => ({
148
481
  style: {
149
482
  touchAction: 'none',
@@ -154,10 +487,20 @@ export function useSheetGestures({
154
487
  onPointerUp: endDrag,
155
488
  onPointerCancel: endDrag
156
489
  }), [handlePointerDown, handlePointerMove, endDrag]);
490
+ const bodyProps = useMemo(() => ({
491
+ ref: bodyRef,
492
+ onPointerDown: handleBodyPointerDown,
493
+ onPointerMove: handleBodyPointerMove,
494
+ onPointerUp: handleBodyEnd,
495
+ onPointerCancel: handleBodyEnd
496
+ }), [bodyRef, handleBodyPointerDown, handleBodyPointerMove, handleBodyEnd]);
157
497
  return {
498
+ sheetRef,
158
499
  contentProps,
159
500
  handleProps,
501
+ bodyProps,
160
502
  dragOffset,
503
+ settledOffset,
161
504
  isDragging
162
505
  };
163
506
  }
@@ -21,7 +21,7 @@ export { isBandScale, xPixel } from './utils';
21
21
  export { useChartColors } from './useChartColors';
22
22
  export { getChartColors, getChartColorsFromResolver, type ChartColorsAPI, type SequentialHue, type TokenResolver, } from './getChartColors';
23
23
  export { useChartRange, type UseChartRangeOptions, type UseChartRangeReturn, } from './useChartRange';
24
- export { hexToGL, getCanvasDPR, getWebGLContext, setupGLState, sizeCanvas, mountCanvasOverSVG, compileShader, createProgram as createGLProgram, CIRCLE_FRAG_BODY, POINT_SIZE_COMPENSATION, } from './webgl';
24
+ export { hexToGL, getCanvasDPR, getWebGLContext, setupGLState, sizeCanvas, mountCanvasOverSVG, compileShader, CIRCLE_FRAG_BODY, POINT_SIZE_COMPENSATION, } from './webgl';
25
25
  export { ChartBrush, type ChartBrushProps, type BrushMode, type BrushRange, } from './ChartBrush';
26
26
  export { ChartZoom, type ChartZoomProps, type ZoomToolbarPosition, } from './ChartZoom';
27
27
  export { ChartSelect, type ChartSelectProps } from './ChartSelect';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Chart/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,KAAK,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAC,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,aAAa,CAAC;AAC3D,OAAO,EACL,aAAa,EACb,QAAQ,EACR,OAAO,EACP,SAAS,EACT,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAC,QAAQ,EAAE,KAAK,aAAa,EAAC,MAAM,YAAY,CAAC;AACxD,OAAO,EAAC,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAC,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAC,aAAa,EAAE,KAAK,kBAAkB,EAAC,MAAM,iBAAiB,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,QAAQ,EAAE,KAAK,aAAa,EAAC,MAAM,YAAY,CAAC;AACxD,OAAO,EAAC,UAAU,EAAE,KAAK,eAAe,EAAC,MAAM,cAAc,CAAC;AAC9D,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAC,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AACxC,YAAY,EAAC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAC9E,OAAO,EAAC,QAAQ,EAAE,KAAK,OAAO,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,EAAC,WAAW,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,OAAO,EACP,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,aAAa,IAAI,eAAe,EAChC,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAC,WAAW,EAAE,KAAK,gBAAgB,EAAC,MAAM,eAAe,CAAC;AACjE,OAAO,EACL,kBAAkB,EAClB,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Chart/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,KAAK,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAC,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,aAAa,CAAC;AAC3D,OAAO,EACL,aAAa,EACb,QAAQ,EACR,OAAO,EACP,SAAS,EACT,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAC,QAAQ,EAAE,KAAK,aAAa,EAAC,MAAM,YAAY,CAAC;AACxD,OAAO,EAAC,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAC,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAC,aAAa,EAAE,KAAK,kBAAkB,EAAC,MAAM,iBAAiB,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,QAAQ,EAAE,KAAK,aAAa,EAAC,MAAM,YAAY,CAAC;AACxD,OAAO,EAAC,UAAU,EAAE,KAAK,eAAe,EAAC,MAAM,cAAc,CAAC;AAC9D,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAC,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AACxC,YAAY,EAAC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAC9E,OAAO,EAAC,QAAQ,EAAE,KAAK,OAAO,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,EAAC,WAAW,EAAE,MAAM,EAAC,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,cAAc,EACd,0BAA0B,EAC1B,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,OAAO,EACP,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAC,WAAW,EAAE,KAAK,gBAAgB,EAAC,MAAM,eAAe,CAAC;AACjE,OAAO,EACL,kBAAkB,EAClB,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAC"}
@@ -22,7 +22,7 @@ export { isBandScale, xPixel } from "./utils.js";
22
22
  export { useChartColors } from "./useChartColors.js";
23
23
  export { getChartColors, getChartColorsFromResolver } from "./getChartColors.js";
24
24
  export { useChartRange } from "./useChartRange.js";
25
- export { hexToGL, getCanvasDPR, getWebGLContext, setupGLState, sizeCanvas, mountCanvasOverSVG, compileShader, createProgram as createGLProgram, CIRCLE_FRAG_BODY, POINT_SIZE_COMPENSATION } from "./webgl.js";
25
+ export { hexToGL, getCanvasDPR, getWebGLContext, setupGLState, sizeCanvas, mountCanvasOverSVG, compileShader, CIRCLE_FRAG_BODY, POINT_SIZE_COMPENSATION } from "./webgl.js";
26
26
  export { ChartBrush } from "./ChartBrush.js";
27
27
  export { ChartZoom } from "./ChartZoom.js";
28
28
  export { ChartSelect } from "./ChartSelect.js";
@@ -9,7 +9,7 @@
9
9
  * - /packages/lab/src/Stepper/Stepper.test.tsx
10
10
  * - /packages/lab/src/Stepper/index.ts
11
11
  * - /apps/storybook/stories/Stepper.stories.tsx
12
- * - /packages/cli/templates/blocks/components/Stepper/ (showcase blocks)
12
+ * - /packages/cli/assets/templates/blocks/components/Stepper/ (showcase blocks)
13
13
  */
14
14
  import { type ReactNode } from 'react';
15
15
  import type { BaseProps } from '@astryxdesign/core';
@@ -13,7 +13,7 @@
13
13
  * - /packages/lab/src/Stepper/Stepper.test.tsx
14
14
  * - /packages/lab/src/Stepper/index.ts
15
15
  * - /apps/storybook/stories/Stepper.stories.tsx
16
- * - /packages/cli/templates/blocks/components/Stepper/ (showcase blocks)
16
+ * - /packages/cli/assets/templates/blocks/components/Stepper/ (showcase blocks)
17
17
  */
18
18
  import * as stylex from '@stylexjs/stylex';
19
19
  import '@astryxdesign/core/theme/tokens.stylex';
@@ -9,7 +9,7 @@
9
9
  * - /packages/lab/src/Stepper/Stepper.test.tsx (tests for new/changed behavior)
10
10
  * - /packages/lab/src/Stepper/index.ts (exports if types change)
11
11
  * - /apps/storybook/stories/Stepper.stories.tsx (storybook stories)
12
- * - /packages/cli/templates/blocks/components/Stepper/ (showcase blocks)
12
+ * - /packages/cli/assets/templates/blocks/components/Stepper/ (showcase blocks)
13
13
  */
14
14
  import { type ReactNode } from 'react';
15
15
  import type { BaseProps } from '@astryxdesign/core';
@@ -13,7 +13,7 @@
13
13
  * - /packages/lab/src/Stepper/Stepper.test.tsx (tests for new/changed behavior)
14
14
  * - /packages/lab/src/Stepper/index.ts (exports if types change)
15
15
  * - /apps/storybook/stories/Stepper.stories.tsx (storybook stories)
16
- * - /packages/cli/templates/blocks/components/Stepper/ (showcase blocks)
16
+ * - /packages/cli/assets/templates/blocks/components/Stepper/ (showcase blocks)
17
17
  */
18
18
  import { Children, useMemo } from 'react';
19
19
  import * as stylex from '@stylexjs/stylex';
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ export * from "./Chat/index.js";
27
27
  // Drawer — experimental overlay panel
28
28
  export { Drawer } from "./Drawer/index.js";
29
29
 
30
- // BottomSheet — mobile touch sheet built on the Drawer <dialog> engine
30
+ // BottomSheet — mobile touch sheet on a native modal <dialog>
31
31
  export { BottomSheet } from "./BottomSheet/index.js";
32
32
 
33
33
  // Stat — experimental KPI/metric display