@patternmode/stacksheet 1.3.7 → 1.3.8

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.
@@ -0,0 +1,24 @@
1
+ /** A single pointer sample recorded during a drag gesture. */
2
+ export interface VelocitySample {
3
+ /** Drag offset in the dismiss direction (px) at sample time */
4
+ offset: number;
5
+ /** Timestamp in ms (Date.now()) */
6
+ time: number;
7
+ }
8
+ /** Sliding window (ms) of samples used to compute release velocity. */
9
+ export declare const VELOCITY_WINDOW_MS = 100;
10
+ /**
11
+ * Append a pointer sample, pruning entries that fall outside the sliding
12
+ * window (plus a hard cap as a memory guard). Mutates and returns `samples`.
13
+ */
14
+ export declare const appendVelocitySample: (samples: VelocitySample[], sample: VelocitySample) => VelocitySample[];
15
+ /**
16
+ * Compute release velocity (px/ms, positive = dismiss direction) from the
17
+ * samples recorded within the sliding window before `releaseTime`.
18
+ *
19
+ * Using only recent samples means a pause followed by a flick reports the
20
+ * flick's velocity — not the whole-gesture average, which would dilute it
21
+ * to near zero.
22
+ */
23
+ export declare const getReleaseVelocity: (samples: VelocitySample[], releaseTime: number) => number;
24
+ //# sourceMappingURL=drag-velocity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drag-velocity.d.ts","sourceRoot":"","sources":["../../src/Drag/drag-velocity.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAKtC;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAC/B,SAAS,cAAc,EAAE,EACzB,QAAQ,cAAc,KACrB,cAAc,EAOhB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,cAAc,EAAE,EAAE,aAAa,MAAM,KAAG,MASnF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-drag.d.ts","sourceRoot":"","sources":["../../src/Drag/use-drag.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAUvC,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAmI1D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,OAAO,GAClB,UAAU,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,EAC1C,QAAQ,UAAU,EAClB,cAAc,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,SAyDzC,CAAC"}
1
+ {"version":3,"file":"use-drag.d.ts","sourceRoot":"","sources":["../../src/Drag/use-drag.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAUvC,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAqJ1D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,OAAO,GAClB,UAAU,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,EAC1C,QAAQ,UAAU,EAClB,cAAc,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,SAyDzC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sheet-panel-focus.d.ts","sourceRoot":"","sources":["../../src/SheetPanel/sheet-panel-focus.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAsClD,eAAO,MAAM,cAAc,GAAI,6CAK5B;IACD,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC3C,QAAQ,EAAE,SAAS,CAAC;CACrB,KAAG,SAyBH,CAAC"}
1
+ {"version":3,"file":"sheet-panel-focus.d.ts","sourceRoot":"","sources":["../../src/SheetPanel/sheet-panel-focus.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAsClD,eAAO,MAAM,cAAc,GAAI,6CAK5B;IACD,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC3C,QAAQ,EAAE,SAAS,CAAC;CACrB,KAAG,SA2BH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sheet-panel.d.ts","sourceRoot":"","sources":["../../src/SheetPanel/sheet-panel.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAqR3D,eAAO,MAAM,UAAU,GAAI,OAAO,eAAe,gCAiFhD,CAAC"}
1
+ {"version":3,"file":"sheet-panel.d.ts","sourceRoot":"","sources":["../../src/SheetPanel/sheet-panel.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAyT3D,eAAO,MAAM,UAAU,GAAI,OAAO,eAAe,gCAiFhD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sheet-title.d.ts","sourceRoot":"","sources":["../../src/SheetParts/sheet-title.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,eAAO,MAAM,UAAU,GAAI,yCAAyC,cAAc,gCAUjF,CAAC"}
1
+ {"version":3,"file":"sheet-title.d.ts","sourceRoot":"","sources":["../../src/SheetParts/sheet-title.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,eAAO,MAAM,UAAU,GAAI,yCAAyC,cAAc,gCAWjF,CAAC"}
@@ -9,6 +9,12 @@ declare global {
9
9
  }
10
10
  export declare const toRecord: (value: unknown) => Record<string, unknown>;
11
11
  export declare const isComponent: (value: unknown) => value is AnyComponent;
12
+ /**
13
+ * Generate a unique sheet id. `crypto.randomUUID` is only available in
14
+ * secure contexts, so fall back to a timestamp + random suffix on
15
+ * non-secure origins (e.g. plain-HTTP LAN dev servers).
16
+ */
17
+ export declare const generateSheetId: () => string;
12
18
  export declare const getStringArg: (value: unknown, name: string) => string;
13
19
  export declare const resolvePresentationOptions: (value: unknown) => SheetPresentationOptions | undefined;
14
20
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"store-args.d.ts","sourceRoot":"","sources":["../../src/Store/store-args.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,OAAO,EACP,SAAS,GACT;QACE,GAAG,CAAC,EAAE;YACJ,QAAQ,CAAC,EAAE,MAAM,CAAC;SACnB,CAAC;KACH,CAAC;CACP;AAKD,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAmC,CAAC;AAEpG,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,YAA2C,CAAC;AASlG,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,EAAE,MAAM,MAAM,KAAG,MAK3D,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACrC,OAAO,OAAO,KACb,wBAAwB,GAAG,SAS7B,CAAC;AACF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,YAAY,EACvB,mBAAmB,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,EAC5C,aAAa,GAAG,CAAC,MAAM,CAAC,KACvB,IAyBF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,YAAY,EACvB,mBAAmB,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,EAC5C,cAAc,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACvC,YAAY,MAAM,MAAM,EACxB,aAAa,GAAG,CAAC,MAAM,CAAC,KACvB,MAWF,CAAC;AACF;;;GAGG;AACH,eAAO,MAAM,WAAW,GACtB,mBAAmB,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,EAC5C,cAAc,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACvC,YAAY,MAAM,MAAM,EACxB,aAAa,GAAG,CAAC,MAAM,CAAC,EACxB,OAAO,OAAO,EACd,QAAQ,OAAO,EACf,OAAO,OAAO,EACd,SAAS,OAAO,KACf,YA8BF,CAAC"}
1
+ {"version":3,"file":"store-args.d.ts","sourceRoot":"","sources":["../../src/Store/store-args.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,OAAO,EACP,SAAS,GACT;QACE,GAAG,CAAC,EAAE;YACJ,QAAQ,CAAC,EAAE,MAAM,CAAC;SACnB,CAAC;KACH,CAAC;CACP;AAKD,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAmC,CAAC;AAEpG,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,YAA2C,CAAC;AASlG;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAO,MAKlC,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,EAAE,MAAM,MAAM,KAAG,MAK3D,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACrC,OAAO,OAAO,KACb,wBAAwB,GAAG,SAS7B,CAAC;AACF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,YAAY,EACvB,mBAAmB,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,EAC5C,aAAa,GAAG,CAAC,MAAM,CAAC,KACvB,IAyBF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,WAAW,YAAY,EACvB,mBAAmB,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,EAC5C,cAAc,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACvC,YAAY,MAAM,MAAM,EACxB,aAAa,GAAG,CAAC,MAAM,CAAC,KACvB,MAWF,CAAC;AACF;;;GAGG;AACH,eAAO,MAAM,WAAW,GACtB,mBAAmB,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,EAC5C,cAAc,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACvC,YAAY,MAAM,MAAM,EACxB,aAAa,GAAG,CAAC,MAAM,CAAC,EACxB,OAAO,OAAO,EACd,QAAQ,OAAO,EACf,OAAO,OAAO,EACd,SAAS,OAAO,KACf,YA8BF,CAAC"}
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Portal } from "@radix-ui/react-portal";
3
- import { createContext, use, useEffect, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createContext, use, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { createStore, useStore } from "zustand";
5
5
  import { useShallow } from "zustand/react/shallow";
6
6
  import { springs as springs$1 } from "@howells/motion";
@@ -169,38 +169,50 @@ const useViewportHeight = (active) => {
169
169
  }, []);
170
170
  return active ? height ?? 0 : 0;
171
171
  };
172
+ const BODY_SCALE_TRANSITION = "transform 500ms cubic-bezier(0.32, 0.72, 0, 1), border-radius 500ms cubic-bezier(0.32, 0.72, 0, 1)";
173
+ /** Fallback delay (transition duration + margin) if `transitionend` never fires. */
174
+ const BODY_SCALE_RESET_FALLBACK_MS = 600;
175
+ /** Cancels a pending un-scale reset when the sheet reopens mid-animation. */
176
+ let cancelPendingBodyScaleReset;
172
177
  const useBodyScale = (config, isOpen, prefersReducedMotion) => {
173
178
  useEffect(() => {
174
179
  const wrapper = document.querySelector("[data-stacksheet-wrapper]");
175
- const resetWrapper = () => {
176
- if (wrapper instanceof HTMLElement) {
177
- wrapper.style.transform = "";
178
- wrapper.style.borderRadius = "";
179
- wrapper.style.transition = "";
180
- wrapper.style.overflow = "";
181
- wrapper.style.transformOrigin = "";
182
- }
183
- };
184
- const canScale = config.shouldScaleBackground && !prefersReducedMotion && wrapper instanceof HTMLElement;
185
- if (canScale && isOpen) {
186
- const scale = config.scaleBackgroundAmount;
187
- wrapper.style.transition = "transform 500ms cubic-bezier(0.32, 0.72, 0, 1), border-radius 500ms cubic-bezier(0.32, 0.72, 0, 1)";
188
- wrapper.style.transform = `scale(${scale})`;
189
- wrapper.style.borderRadius = "8px";
190
- wrapper.style.overflow = "hidden";
191
- wrapper.style.transformOrigin = "center top";
192
- return resetWrapper;
193
- }
194
- const handleEnd = () => {
195
- resetWrapper();
196
- };
197
- if (canScale) {
198
- wrapper.style.transform = "";
199
- wrapper.style.borderRadius = "";
200
- wrapper.addEventListener("transitionend", handleEnd, { once: true });
180
+ const scalable = config.shouldScaleBackground && !prefersReducedMotion && wrapper instanceof HTMLElement && isOpen ? wrapper : null;
181
+ if (scalable !== null) {
182
+ cancelPendingBodyScaleReset?.();
183
+ scalable.style.transition = BODY_SCALE_TRANSITION;
184
+ scalable.style.transform = `scale(${config.scaleBackgroundAmount})`;
185
+ scalable.style.borderRadius = "8px";
186
+ scalable.style.overflow = "hidden";
187
+ scalable.style.transformOrigin = "center top";
201
188
  }
202
189
  return () => {
203
- if (wrapper instanceof HTMLElement) wrapper.removeEventListener("transitionend", handleEnd);
190
+ if (scalable === null) return;
191
+ scalable.style.transition = BODY_SCALE_TRANSITION;
192
+ scalable.style.transform = "";
193
+ scalable.style.borderRadius = "";
194
+ const controller = new AbortController();
195
+ const { signal } = controller;
196
+ const finish = () => {
197
+ controller.abort();
198
+ cancelPendingBodyScaleReset = void 0;
199
+ scalable.style.transition = "";
200
+ scalable.style.overflow = "";
201
+ scalable.style.transformOrigin = "";
202
+ };
203
+ scalable.addEventListener("transitionend", (event) => {
204
+ if (event.target === scalable && event.propertyName === "transform") finish();
205
+ }, { signal });
206
+ const timeoutId = setTimeout(() => {
207
+ if (!signal.aborted) finish();
208
+ }, BODY_SCALE_RESET_FALLBACK_MS);
209
+ signal.addEventListener("abort", () => {
210
+ clearTimeout(timeoutId);
211
+ });
212
+ cancelPendingBodyScaleReset = () => {
213
+ controller.abort();
214
+ cancelPendingBodyScaleReset = void 0;
215
+ };
204
216
  };
205
217
  }, [
206
218
  isOpen,
@@ -318,12 +330,13 @@ const resolveClassNames = (cn) => {
318
330
  panel: cn.panel ?? ""
319
331
  };
320
332
  };
321
- const buildAriaProps = (isTop, isModal, isComposable, ariaLabel, panelId, hasDescription) => {
333
+ const buildAriaProps = ({ ariaLabel, hasDescription, hasTitle, isComposable, isModal, isTop, panelId }) => {
322
334
  if (!isTop) return {};
323
335
  const props = { role: "dialog" };
324
336
  if (isModal) props["aria-modal"] = "true";
325
337
  if (isComposable) {
326
- props["aria-labelledby"] = `${panelId}-title`;
338
+ if (hasTitle) props["aria-labelledby"] = `${panelId}-title`;
339
+ else props["aria-label"] = ariaLabel;
327
340
  if (hasDescription) props["aria-describedby"] = `${panelId}-desc`;
328
341
  } else props["aria-label"] = ariaLabel;
329
342
  return props;
@@ -504,12 +517,41 @@ const getPanelDimension = (panel, axis) => {
504
517
  if (!panel) return 300;
505
518
  return axis === "x" ? panel.offsetWidth : panel.offsetHeight;
506
519
  };
520
+ /** Upper bound on retained samples — plenty for a 100ms window at 120Hz. */
521
+ const MAX_SAMPLES = 20;
522
+ /**
523
+ * Append a pointer sample, pruning entries that fall outside the sliding
524
+ * window (plus a hard cap as a memory guard). Mutates and returns `samples`.
525
+ */
526
+ const appendVelocitySample = (samples, sample) => {
527
+ samples.push(sample);
528
+ const cutoff = sample.time - 100;
529
+ while (samples.length > MAX_SAMPLES || samples.length > 2 && (samples[0]?.time ?? 0) < cutoff) samples.shift();
530
+ return samples;
531
+ };
532
+ /**
533
+ * Compute release velocity (px/ms, positive = dismiss direction) from the
534
+ * samples recorded within the sliding window before `releaseTime`.
535
+ *
536
+ * Using only recent samples means a pause followed by a flick reports the
537
+ * flick's velocity — not the whole-gesture average, which would dilute it
538
+ * to near zero.
539
+ */
540
+ const getReleaseVelocity = (samples, releaseTime) => {
541
+ const cutoff = releaseTime - 100;
542
+ const recent = samples.filter((sample) => sample.time >= cutoff);
543
+ const [first] = recent;
544
+ const last = recent.at(-1);
545
+ if (first === void 0 || last === void 0 || last.time <= first.time) return 0;
546
+ return (last.offset - first.offset) / (last.time - first.time);
547
+ };
507
548
  //#endregion
508
549
  //#region src/Drag/use-drag.ts
509
- const resetDragRefs = ({ committedRef, offsetRef, scrollTargetRef, startRef }) => {
550
+ const resetDragRefs = ({ committedRef, offsetRef, samplesRef, scrollTargetRef, startRef }) => {
510
551
  startRef.current = null;
511
552
  committedRef.current = null;
512
553
  offsetRef.current = 0;
554
+ samplesRef.current = [];
513
555
  scrollTargetRef.current = null;
514
556
  };
515
557
  const createDragHandlers = ({ axis, config, onDragUpdate, panelRef, refs, sign }) => {
@@ -524,13 +566,15 @@ const createDragHandlers = ({ axis, config, onDragUpdate, panelRef, refs, sign }
524
566
  if (!isHandle && isInteractiveElement(target)) return;
525
567
  refs.scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
526
568
  refs.startRef.current = {
527
- time: Date.now(),
528
569
  x: e.clientX,
529
570
  y: e.clientY
530
571
  };
531
572
  refs.committedRef.current = null;
532
573
  refs.offsetRef.current = 0;
533
- if (e.currentTarget instanceof HTMLElement) e.currentTarget.setPointerCapture(e.pointerId);
574
+ refs.samplesRef.current = [{
575
+ offset: 0,
576
+ time: Date.now()
577
+ }];
534
578
  };
535
579
  const handlePointerMove = (e) => {
536
580
  if (refs.startRef.current === null) return;
@@ -544,11 +588,16 @@ const createDragHandlers = ({ axis, config, onDragUpdate, panelRef, refs, sign }
544
588
  refs.startRef.current = null;
545
589
  return;
546
590
  }
591
+ if (e.currentTarget instanceof HTMLElement) e.currentTarget.setPointerCapture(e.pointerId);
547
592
  }
548
593
  if (refs.committedRef.current !== "drag") return;
549
594
  const directional = (axis === "x" ? dx : dy) * sign;
550
595
  const clampedOffset = directional >= 0 ? directional : -Math.sqrt(Math.abs(directional)) * RUBBER_BAND_FACTOR;
551
596
  refs.offsetRef.current = clampedOffset;
597
+ appendVelocitySample(refs.samplesRef.current, {
598
+ offset: clampedOffset,
599
+ time: Date.now()
600
+ });
552
601
  onDragUpdate({
553
602
  isDragging: true,
554
603
  offset: clampedOffset
@@ -561,8 +610,7 @@ const createDragHandlers = ({ axis, config, onDragUpdate, panelRef, refs, sign }
561
610
  return;
562
611
  }
563
612
  const offset = Math.max(0, refs.offsetRef.current);
564
- const elapsed = Date.now() - refs.startRef.current.time;
565
- const velocity = elapsed > 0 ? offset / elapsed : 0;
613
+ const velocity = getReleaseVelocity(refs.samplesRef.current, Date.now());
566
614
  resetDragRefs(refs);
567
615
  const panelSize = getPanelDimension(panelRef.current, axis);
568
616
  if (config.snapHeights.length > 0) {
@@ -619,6 +667,7 @@ const useDrag = (panelRef, config, onDragUpdate) => {
619
667
  const startRef = useRef(null);
620
668
  const committedRef = useRef(null);
621
669
  const offsetRef = useRef(0);
670
+ const samplesRef = useRef([]);
622
671
  const scrollTargetRef = useRef(null);
623
672
  const { axis, sign } = getDismissAxis(config.side);
624
673
  const { handlePointerCancel, handlePointerDown, handlePointerMove, handlePointerUp } = createDragHandlers({
@@ -629,6 +678,7 @@ const useDrag = (panelRef, config, onDragUpdate) => {
629
678
  refs: {
630
679
  committedRef,
631
680
  offsetRef,
681
+ samplesRef,
632
682
  scrollTargetRef,
633
683
  startRef
634
684
  },
@@ -896,7 +946,7 @@ const ModalFocusTrap = ({ enabled, active, fallbackRef, children }) => {
896
946
  if (fallbackRef.current !== null) return fallbackRef.current;
897
947
  return document.body;
898
948
  },
899
- initialFocus: false,
949
+ initialFocus: () => fallbackRef.current ?? void 0,
900
950
  returnFocusOnDeactivate: true
901
951
  },
902
952
  paused,
@@ -969,14 +1019,16 @@ const getHeaderProps = ({ close, isNested, pop, side }) => ({
969
1019
  onClose: close,
970
1020
  side
971
1021
  });
972
- const getPanelContext = ({ close, hasDescription, isNested, isTop, panelId, pop, registerDescription, side }) => ({
1022
+ const getPanelContext = ({ close, hasDescription, hasTitle, isNested, isTop, panelId, pop, registerDescription, registerTitle, side }) => ({
973
1023
  back: pop,
974
1024
  close,
975
1025
  hasDescription,
1026
+ hasTitle,
976
1027
  isNested,
977
1028
  isTop,
978
1029
  panelId,
979
1030
  registerDescription,
1031
+ registerTitle,
980
1032
  side
981
1033
  });
982
1034
  const getOptionalSideHandle = ({ isHovered, onDismiss, show, side }) => show ? /* @__PURE__ */ jsx(SideHandle, {
@@ -1024,24 +1076,45 @@ const useSheetPanelDrag = ({ activeSnapIndex, config, isNested, isTop, onSnap, p
1024
1076
  };
1025
1077
  const useSheetPanelContext = ({ close, isNested, isTop, pop, side }, panelId) => {
1026
1078
  const [hasDescription, setHasDescription] = useState(false);
1027
- const registerDescription = () => {
1079
+ const [hasTitle, setHasTitle] = useState(false);
1080
+ const registerDescription = useCallback(() => {
1028
1081
  setHasDescription(true);
1029
1082
  return () => {
1030
1083
  setHasDescription(false);
1031
1084
  };
1032
- };
1085
+ }, []);
1086
+ const registerTitle = useCallback(() => {
1087
+ setHasTitle(true);
1088
+ return () => {
1089
+ setHasTitle(false);
1090
+ };
1091
+ }, []);
1033
1092
  return {
1034
1093
  hasDescription,
1035
- panelContext: getPanelContext({
1094
+ hasTitle,
1095
+ panelContext: useMemo(() => getPanelContext({
1036
1096
  close,
1037
1097
  hasDescription,
1098
+ hasTitle,
1038
1099
  isNested,
1039
1100
  isTop,
1040
1101
  panelId,
1041
1102
  pop,
1042
1103
  registerDescription,
1104
+ registerTitle,
1043
1105
  side
1044
- })
1106
+ }), [
1107
+ close,
1108
+ hasDescription,
1109
+ hasTitle,
1110
+ isNested,
1111
+ isTop,
1112
+ panelId,
1113
+ pop,
1114
+ registerDescription,
1115
+ registerTitle,
1116
+ side
1117
+ ])
1045
1118
  };
1046
1119
  };
1047
1120
  const useSheetPanelModel = (props) => {
@@ -1055,7 +1128,7 @@ const useSheetPanelModel = (props) => {
1055
1128
  const dragState = useSheetPanelDrag(props, panelRef);
1056
1129
  const ariaLabel = getPanelAriaLabel(item, config.ariaLabel);
1057
1130
  const panelId = `stacksheet-${item.id}`;
1058
- const { hasDescription, panelContext } = useSheetPanelContext(props, panelId);
1131
+ const { hasDescription, hasTitle, panelContext } = useSheetPanelContext(props, panelId);
1059
1132
  const isComposable = resolvePanelLayout(layout, renderHeader) === "composable";
1060
1133
  const hasPanelClass = classNames.panel !== "";
1061
1134
  const dragOffset = getDragTransform(side, dragState.offset);
@@ -1066,7 +1139,15 @@ const useSheetPanelModel = (props) => {
1066
1139
  pop,
1067
1140
  side
1068
1141
  });
1069
- const ariaProps = buildAriaProps(isTop, config.modal, isComposable, ariaLabel, panelId, hasDescription);
1142
+ const ariaProps = buildAriaProps({
1143
+ ariaLabel,
1144
+ hasDescription,
1145
+ hasTitle,
1146
+ isComposable,
1147
+ isModal: config.modal,
1148
+ isTop,
1149
+ panelId
1150
+ });
1070
1151
  const transition = buildPanelTransition(dragState.isDragging, isTop, spring, stackSpring);
1071
1152
  const animatedRadius = getAnimatedBorderRadius(side, depth, config.stacking);
1072
1153
  const snapYOffset = computeSnapYOffset(side, snapHeights, activeSnapIndex, measuredHeight);
@@ -1275,15 +1356,14 @@ const useDismissalEffects = ({ closeReasonRef, config, isOpen, rawClose, rawPop,
1275
1356
  useEffect(() => {
1276
1357
  const shouldListen = isOpen && config.closeOnEscape && config.dismissible;
1277
1358
  const handleKeyDown = (e) => {
1278
- if (e.key === "Escape") {
1279
- e.preventDefault();
1280
- dismissFromEscape({
1281
- closeReasonRef,
1282
- rawClose,
1283
- rawPop,
1284
- stackLengthRef
1285
- });
1286
- }
1359
+ if (e.key !== "Escape" || e.defaultPrevented) return;
1360
+ e.preventDefault();
1361
+ dismissFromEscape({
1362
+ closeReasonRef,
1363
+ rawClose,
1364
+ rawPop,
1365
+ stackLengthRef
1366
+ });
1287
1367
  };
1288
1368
  if (shouldListen) document.addEventListener("keydown", handleKeyDown);
1289
1369
  return () => {
@@ -1299,7 +1379,7 @@ const useDismissalEffects = ({ closeReasonRef, config, isOpen, rawClose, rawPop,
1299
1379
  ]);
1300
1380
  useEffect(() => {
1301
1381
  const CloseWatcherConstructor = globalThis.CloseWatcher;
1302
- const shouldListen = isOpen && config.dismissible && CloseWatcherConstructor !== void 0;
1382
+ const shouldListen = isOpen && config.closeOnEscape && config.dismissible && CloseWatcherConstructor !== void 0;
1303
1383
  let watcher;
1304
1384
  const handleClose = () => {
1305
1385
  dismissFromEscape({
@@ -1321,6 +1401,7 @@ const useDismissalEffects = ({ closeReasonRef, config, isOpen, rawClose, rawPop,
1321
1401
  };
1322
1402
  }, [
1323
1403
  isOpen,
1404
+ config.closeOnEscape,
1324
1405
  config.dismissible,
1325
1406
  rawPop,
1326
1407
  rawClose,
@@ -1438,6 +1519,15 @@ const getComponentName = (component) => {
1438
1519
  return name === "" ? void 0 : name;
1439
1520
  };
1440
1521
  const getNodeEnv = () => globalThis.process?.env?.NODE_ENV;
1522
+ /**
1523
+ * Generate a unique sheet id. `crypto.randomUUID` is only available in
1524
+ * secure contexts, so fall back to a timestamp + random suffix on
1525
+ * non-secure origins (e.g. plain-HTTP LAN dev servers).
1526
+ */
1527
+ const generateSheetId = () => {
1528
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
1529
+ return `sheet-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1530
+ };
1441
1531
  const getStringArg = (value, name) => {
1442
1532
  if (typeof value !== "string") throw new TypeError(`Expected ${name} to be a string.`);
1443
1533
  return value;
@@ -1489,7 +1579,7 @@ const resolveArgs = (componentRegistry, componentMap, getNextKey, warnedNames, f
1489
1579
  return {
1490
1580
  ariaLabel: resolvePresentationOptions(third)?.ariaLabel,
1491
1581
  data: toRecord(second),
1492
- id: crypto.randomUUID(),
1582
+ id: generateSheetId(),
1493
1583
  type: typeKey
1494
1584
  };
1495
1585
  }
@@ -1889,7 +1979,8 @@ const SheetHeader = ({ asChild, className, style, children }) => {
1889
1979
  //#endregion
1890
1980
  //#region src/SheetParts/sheet-title.tsx
1891
1981
  const SheetTitle = ({ asChild, className, style, children }) => {
1892
- const { panelId } = useSheetPanel();
1982
+ const { panelId, registerTitle } = useSheetPanel();
1983
+ useEffect(() => registerTitle(), [registerTitle]);
1893
1984
  const isAsChild = asChild === true;
1894
1985
  return /* @__PURE__ */ jsx(isAsChild ? Slot : "h2", {
1895
1986
  className: joinClassNames(isAsChild ? void 0 : "font-semibold text-sm", className),