@patternmode/stacksheet 1.3.6 → 1.3.7

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 (52) hide show
  1. package/dist/Drag/drag-geometry.d.ts.map +1 -1
  2. package/dist/Drag/use-drag.d.ts.map +1 -1
  3. package/dist/SheetPanel/default-header.d.ts +1 -1
  4. package/dist/SheetPanel/default-header.d.ts.map +1 -1
  5. package/dist/SheetPanel/sheet-panel-content.d.ts +1 -1
  6. package/dist/SheetPanel/sheet-panel-content.d.ts.map +1 -1
  7. package/dist/SheetPanel/sheet-panel-focus.d.ts +1 -1
  8. package/dist/SheetPanel/sheet-panel-focus.d.ts.map +1 -1
  9. package/dist/SheetPanel/sheet-panel-handles.d.ts +2 -2
  10. package/dist/SheetPanel/sheet-panel-handles.d.ts.map +1 -1
  11. package/dist/SheetPanel/sheet-panel-layout.d.ts.map +1 -1
  12. package/dist/SheetPanel/sheet-panel.d.ts +1 -1
  13. package/dist/SheetPanel/sheet-panel.d.ts.map +1 -1
  14. package/dist/SheetParts/sheet-back.d.ts +1 -1
  15. package/dist/SheetParts/sheet-back.d.ts.map +1 -1
  16. package/dist/SheetParts/sheet-body.d.ts +1 -1
  17. package/dist/SheetParts/sheet-body.d.ts.map +1 -1
  18. package/dist/SheetParts/sheet-close.d.ts +1 -1
  19. package/dist/SheetParts/sheet-close.d.ts.map +1 -1
  20. package/dist/SheetParts/sheet-description.d.ts +1 -1
  21. package/dist/SheetParts/sheet-description.d.ts.map +1 -1
  22. package/dist/SheetParts/sheet-footer.d.ts +1 -1
  23. package/dist/SheetParts/sheet-footer.d.ts.map +1 -1
  24. package/dist/SheetParts/sheet-handle.d.ts +1 -1
  25. package/dist/SheetParts/sheet-handle.d.ts.map +1 -1
  26. package/dist/SheetParts/sheet-header.d.ts +1 -1
  27. package/dist/SheetParts/sheet-header.d.ts.map +1 -1
  28. package/dist/SheetParts/sheet-parts.d.ts +8 -8
  29. package/dist/SheetParts/sheet-title.d.ts +1 -1
  30. package/dist/SheetParts/sheet-title.d.ts.map +1 -1
  31. package/dist/Store/create-sheet-store.d.ts.map +1 -1
  32. package/dist/Store/store-args.d.ts +11 -0
  33. package/dist/Store/store-args.d.ts.map +1 -1
  34. package/dist/create.d.ts.map +1 -1
  35. package/dist/icons.d.ts +2 -2
  36. package/dist/icons.d.ts.map +1 -1
  37. package/dist/index.mjs +598 -415
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/media.d.ts.map +1 -1
  40. package/dist/panel-context.d.ts.map +1 -1
  41. package/dist/renderer-effects.d.ts.map +1 -1
  42. package/dist/renderer-helpers.d.ts.map +1 -1
  43. package/dist/renderer.d.ts +2 -2
  44. package/dist/renderer.d.ts.map +1 -1
  45. package/dist/snap-points.d.ts.map +1 -1
  46. package/dist/springs.d.ts +11 -17
  47. package/dist/springs.d.ts.map +1 -1
  48. package/dist/stacking.d.ts +2 -2
  49. package/dist/stacking.d.ts.map +1 -1
  50. package/dist/types.d.ts +26 -32
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +13 -12
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { Portal } from "@radix-ui/react-portal";
3
3
  import { createContext, use, useEffect, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { createStore, useStore } from "zustand";
5
5
  import { useShallow } from "zustand/react/shallow";
6
+ import { springs as springs$1 } from "@howells/motion";
6
7
  import { AnimatePresence, LazyMotion, domMax, m, useReducedMotion } from "motion/react";
7
8
  import { RemoveScroll } from "react-remove-scroll";
8
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -11,29 +12,28 @@ import { joinClassNames } from "@patternmode/system";
11
12
  import { Slot } from "@radix-ui/react-slot";
12
13
  import { Root, Scrollbar, Thumb, Viewport } from "@radix-ui/react-scroll-area";
13
14
  //#region src/springs.ts
15
+ /** Strips the shared token's `type: "spring"` down to Stacksheet's numeric SpringConfig. */
16
+ const toSpringConfig = (spring) => ({
17
+ damping: spring.damping,
18
+ mass: spring.mass,
19
+ stiffness: spring.stiffness
20
+ });
14
21
  /**
15
22
  * Spring presets inspired by iOS animation feel.
16
23
  *
17
- * - `subtle` — Barely noticeable bounce, professional.
18
- * - `snappy` — Quick, responsive for interactions.
19
- * - `stiff` — Very quick, controlled. Panels, drawers. **(default)**
24
+ * - `subtle` — Barely noticeable bounce, professional. (shared @howells/motion token)
25
+ * - `snappy` — Quick, responsive for interactions. (shared @howells/motion token)
26
+ * - `stiff` — Very quick, controlled. Panels, drawers. **(default)** Intentional
27
+ * fork: damping 40 (tighter than the shared `stiff`) for drawer control.
20
28
  */
21
29
  const springs = {
22
- snappy: {
23
- damping: 28,
24
- mass: .8,
25
- stiffness: 400
26
- },
30
+ snappy: toSpringConfig(springs$1.snappy),
27
31
  stiff: {
28
32
  damping: 40,
29
33
  mass: 1,
30
34
  stiffness: 400
31
35
  },
32
- subtle: {
33
- damping: 30,
34
- mass: 1,
35
- stiffness: 300
36
- }
36
+ subtle: toSpringConfig(springs$1.subtle)
37
37
  };
38
38
  //#endregion
39
39
  //#region src/config.ts
@@ -114,9 +114,13 @@ const useIsMobile = (breakpoint) => {
114
114
  useEffect(() => {
115
115
  const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
116
116
  setIsMobile(mql.matches);
117
- const handler = (e) => setIsMobile(e.matches);
117
+ const handler = (e) => {
118
+ setIsMobile(e.matches);
119
+ };
118
120
  mql.addEventListener("change", handler);
119
- return () => mql.removeEventListener("change", handler);
121
+ return () => {
122
+ mql.removeEventListener("change", handler);
123
+ };
120
124
  }, [breakpoint]);
121
125
  return isMobile;
122
126
  };
@@ -130,13 +134,17 @@ const usePanelHeight = (panelRef, hasSnapPoints) => {
130
134
  const [height, setHeight] = useState(0);
131
135
  useEffect(() => {
132
136
  const el = panelRef.current;
133
- if (!(el && hasSnapPoints)) return;
134
- setHeight(el.offsetHeight);
135
- const observer = new ResizeObserver(([entry]) => {
136
- if (entry) setHeight(entry.contentRect.height);
137
- });
138
- observer.observe(el);
139
- return () => observer.disconnect();
137
+ let observer;
138
+ if (el !== null && hasSnapPoints) {
139
+ setHeight(el.offsetHeight);
140
+ observer = new ResizeObserver(([entry]) => {
141
+ if (entry) setHeight(entry.contentRect.height);
142
+ });
143
+ observer.observe(el);
144
+ }
145
+ return () => {
146
+ observer?.disconnect();
147
+ };
140
148
  }, [panelRef, hasSnapPoints]);
141
149
  return height;
142
150
  };
@@ -144,40 +152,56 @@ const getViewportHeight = () => typeof window === "undefined" ? 0 : window.visua
144
152
  const useViewportHeight = (active) => {
145
153
  const [height, setHeight] = useState(() => typeof window === "undefined" ? void 0 : getViewportHeight());
146
154
  useEffect(() => {
147
- if (typeof window === "undefined") return;
148
- const update = () => setHeight(getViewportHeight());
149
- window.addEventListener("resize", update);
150
- window.visualViewport?.addEventListener("resize", update);
155
+ const update = () => {
156
+ setHeight(getViewportHeight());
157
+ };
158
+ const canListen = typeof window !== "undefined";
159
+ if (canListen) {
160
+ window.addEventListener("resize", update);
161
+ window.visualViewport?.addEventListener("resize", update);
162
+ }
151
163
  return () => {
152
- window.removeEventListener("resize", update);
153
- window.visualViewport?.removeEventListener("resize", update);
164
+ if (canListen) {
165
+ window.removeEventListener("resize", update);
166
+ window.visualViewport?.removeEventListener("resize", update);
167
+ }
154
168
  };
155
169
  }, []);
156
170
  return active ? height ?? 0 : 0;
157
171
  };
158
172
  const useBodyScale = (config, isOpen, prefersReducedMotion) => {
159
173
  useEffect(() => {
160
- if (!config.shouldScaleBackground || prefersReducedMotion) return;
161
174
  const wrapper = document.querySelector("[data-stacksheet-wrapper]");
162
- if (!(wrapper && wrapper instanceof HTMLElement)) return;
163
- if (isOpen) {
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) {
164
186
  const scale = config.scaleBackgroundAmount;
165
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)";
166
188
  wrapper.style.transform = `scale(${scale})`;
167
189
  wrapper.style.borderRadius = "8px";
168
190
  wrapper.style.overflow = "hidden";
169
191
  wrapper.style.transformOrigin = "center top";
170
- return;
192
+ return resetWrapper;
171
193
  }
172
- wrapper.style.transform = "";
173
- wrapper.style.borderRadius = "";
174
194
  const handleEnd = () => {
175
- wrapper.style.transition = "";
176
- wrapper.style.overflow = "";
177
- wrapper.style.transformOrigin = "";
195
+ resetWrapper();
196
+ };
197
+ if (canScale) {
198
+ wrapper.style.transform = "";
199
+ wrapper.style.borderRadius = "";
200
+ wrapper.addEventListener("transitionend", handleEnd, { once: true });
201
+ }
202
+ return () => {
203
+ if (wrapper instanceof HTMLElement) wrapper.removeEventListener("transitionend", handleEnd);
178
204
  };
179
- wrapper.addEventListener("transitionend", handleEnd, { once: true });
180
- return () => wrapper.removeEventListener("transitionend", handleEnd);
181
205
  }, [
182
206
  isOpen,
183
207
  config.shouldScaleBackground,
@@ -187,7 +211,14 @@ const useBodyScale = (config, isOpen, prefersReducedMotion) => {
187
211
  };
188
212
  //#endregion
189
213
  //#region src/snap-points.ts
190
- const SNAP_POINT_RE = /^(\d+(?:\.\d+)?)(px|rem|em|vh|%)$/u;
214
+ const SNAP_POINT_RE = /^(?<value>\d+(?:\.\d+)?)(?<unit>px|rem|em|vh|%)$/u;
215
+ const getRootFontSize = () => typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
216
+ const resolveUnitPx = (value, unit, viewportHeight) => {
217
+ if (unit === "px") return value;
218
+ if (unit === "rem" || unit === "em") return value * getRootFontSize();
219
+ if (unit === "vh" || unit === "%") return value / 100 * viewportHeight;
220
+ return 0;
221
+ };
191
222
  /**
192
223
  * Resolve a snap point value to pixels given the viewport height.
193
224
  * - number 0-1: fraction of viewport (0.5 → 50% of vh)
@@ -197,18 +228,11 @@ const SNAP_POINT_RE = /^(\d+(?:\.\d+)?)(px|rem|em|vh|%)$/u;
197
228
  const resolveSnapPointPx = (point, viewportHeight) => {
198
229
  if (typeof point === "number") return point <= 1 ? point * viewportHeight : point;
199
230
  if (typeof point === "string") {
200
- const match = point.match(SNAP_POINT_RE);
201
- if (!match?.[1]) return 0;
202
- const [, rawValue, unit] = match;
203
- const value = Number.parseFloat(rawValue);
204
- switch (unit) {
205
- case "px": return value;
206
- case "rem":
207
- case "em": return value * (typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize));
208
- case "vh":
209
- case "%": return value / 100 * viewportHeight;
210
- default: return 0;
211
- }
231
+ const match = SNAP_POINT_RE.exec(point);
232
+ const rawValue = match?.groups?.value;
233
+ const unit = match?.groups?.unit;
234
+ if (rawValue === void 0 || unit === void 0) return 0;
235
+ return resolveUnitPx(Number.parseFloat(rawValue), unit, viewportHeight);
212
236
  }
213
237
  return 0;
214
238
  };
@@ -397,7 +421,7 @@ const RUBBER_BAND_FACTOR = .6;
397
421
  //#region src/Drag/drag-geometry.ts
398
422
  const isInteractiveElement = (el) => {
399
423
  if (INTERACTIVE_TAGS.has(el.tagName)) return true;
400
- if (el.isContentEditable) return true;
424
+ if (el instanceof HTMLElement && el.isContentEditable) return true;
401
425
  if (el.closest("button, a, input, textarea, select, [contenteditable]")) return true;
402
426
  if (el.closest("[data-stacksheet-no-drag]")) return true;
403
427
  return false;
@@ -473,7 +497,7 @@ const classifyGesture = (dx, dy, axis, sign) => {
473
497
  /** Decide whether a pointer gesture commits as a drag or should be ignored. */
474
498
  const commitGesture = (dx, dy, axis, sign, scrollEl) => {
475
499
  if (classifyGesture(dx, dy, axis, sign) === "none") return "none";
476
- if (scrollEl && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
500
+ if (scrollEl !== null && !isAtScrollEdge(scrollEl, axis, sign)) return "none";
477
501
  return "drag";
478
502
  };
479
503
  const getPanelDimension = (panel, axis) => {
@@ -482,86 +506,64 @@ const getPanelDimension = (panel, axis) => {
482
506
  };
483
507
  //#endregion
484
508
  //#region src/Drag/use-drag.ts
485
- /**
486
- * Hook that manages drag gestures that can dismiss a sheet panel.
487
- *
488
- * Gesture pipeline:
489
- * 1. Dead zone (10px) — ignores micro-movements
490
- * 2. Angle check (35°) — must be roughly aligned with dismiss axis
491
- * 3. Scroll conflict yields to scrollable containers not at edge
492
- * 4. Commit drag is active, applies offset via `onDragUpdate`
493
- * 5. Release — velocity + threshold determine close/snap/bounce-back
494
- *
495
- * Opposite-direction drag uses √(offset) damping for elastic
496
- * rubber-band resistance (same physics as iOS over-scroll).
497
- *
498
- * When `snapHeights` is provided, release targeting uses
499
- * `findSnapTarget()` instead of the simple threshold check.
500
- */
501
- const useDrag = (panelRef, config, onDragUpdate) => {
502
- const startRef = useRef(null);
503
- const committedRef = useRef(null);
504
- const offsetRef = useRef(0);
505
- const scrollTargetRef = useRef(null);
506
- const { axis, sign } = getDismissAxis(config.side);
509
+ const resetDragRefs = ({ committedRef, offsetRef, scrollTargetRef, startRef }) => {
510
+ startRef.current = null;
511
+ committedRef.current = null;
512
+ offsetRef.current = 0;
513
+ scrollTargetRef.current = null;
514
+ };
515
+ const createDragHandlers = ({ axis, config, onDragUpdate, panelRef, refs, sign }) => {
516
+ const dismiss = () => {
517
+ if (config.isNested) config.onPop();
518
+ else config.onClose();
519
+ };
507
520
  const handlePointerDown = (e) => {
508
- if (!config.enabled) return;
509
- if (e.button !== 0) return;
510
- const target = e.target;
511
- if (!target) return;
512
- const isHandle = !!target.closest("[data-stacksheet-handle]");
521
+ if (!config.enabled || e.button !== 0 || !(e.target instanceof Element)) return;
522
+ const { target } = e;
523
+ const isHandle = target.closest("[data-stacksheet-handle]") !== null;
513
524
  if (!isHandle && isInteractiveElement(target)) return;
514
- scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
515
- startRef.current = {
525
+ refs.scrollTargetRef.current = isHandle ? null : findScrollableAncestor(target, axis);
526
+ refs.startRef.current = {
516
527
  time: Date.now(),
517
528
  x: e.clientX,
518
529
  y: e.clientY
519
530
  };
520
- committedRef.current = null;
521
- offsetRef.current = 0;
522
- e.currentTarget?.setPointerCapture?.(e.pointerId);
531
+ refs.committedRef.current = null;
532
+ refs.offsetRef.current = 0;
533
+ if (e.currentTarget instanceof HTMLElement) e.currentTarget.setPointerCapture(e.pointerId);
523
534
  };
524
535
  const handlePointerMove = (e) => {
525
- if (!startRef.current) return;
526
- const dx = e.clientX - startRef.current.x;
527
- const dy = e.clientY - startRef.current.y;
536
+ if (refs.startRef.current === null) return;
537
+ const dx = e.clientX - refs.startRef.current.x;
538
+ const dy = e.clientY - refs.startRef.current.y;
528
539
  const dist = Math.hypot(dx, dy);
529
- if (committedRef.current === null && dist < 10) return;
530
- if (committedRef.current === null) {
531
- committedRef.current = commitGesture(dx, dy, axis, sign, scrollTargetRef.current);
532
- if (committedRef.current !== "drag") {
533
- startRef.current = null;
540
+ if (refs.committedRef.current === null && dist < 10) return;
541
+ if (refs.committedRef.current === null) {
542
+ refs.committedRef.current = commitGesture(dx, dy, axis, sign, refs.scrollTargetRef.current);
543
+ if (refs.committedRef.current !== "drag") {
544
+ refs.startRef.current = null;
534
545
  return;
535
546
  }
536
547
  }
537
- if (committedRef.current !== "drag") return;
548
+ if (refs.committedRef.current !== "drag") return;
538
549
  const directional = (axis === "x" ? dx : dy) * sign;
539
550
  const clampedOffset = directional >= 0 ? directional : -Math.sqrt(Math.abs(directional)) * RUBBER_BAND_FACTOR;
540
- offsetRef.current = clampedOffset;
551
+ refs.offsetRef.current = clampedOffset;
541
552
  onDragUpdate({
542
553
  isDragging: true,
543
554
  offset: clampedOffset
544
555
  });
545
556
  e.preventDefault();
546
557
  };
547
- const dismiss = () => {
548
- if (config.isNested) config.onPop();
549
- else config.onClose();
550
- };
551
- const handlePointerUp = (_e) => {
552
- if (!startRef.current || committedRef.current !== "drag") {
553
- startRef.current = null;
554
- committedRef.current = null;
555
- scrollTargetRef.current = null;
558
+ const handlePointerUp = () => {
559
+ if (refs.startRef.current === null || refs.committedRef.current !== "drag") {
560
+ resetDragRefs(refs);
556
561
  return;
557
562
  }
558
- const offset = Math.max(0, offsetRef.current);
559
- const elapsed = Date.now() - startRef.current.time;
563
+ const offset = Math.max(0, refs.offsetRef.current);
564
+ const elapsed = Date.now() - refs.startRef.current.time;
560
565
  const velocity = elapsed > 0 ? offset / elapsed : 0;
561
- startRef.current = null;
562
- committedRef.current = null;
563
- offsetRef.current = 0;
564
- scrollTargetRef.current = null;
566
+ resetDragRefs(refs);
565
567
  const panelSize = getPanelDimension(panelRef.current, axis);
566
568
  if (config.snapHeights.length > 0) {
567
569
  const targetIndex = findSnapTarget(offset, panelSize, config.snapHeights, velocity, config.activeSnapIndex, config.sequential);
@@ -584,15 +586,54 @@ const useDrag = (panelRef, config, onDragUpdate) => {
584
586
  });
585
587
  };
586
588
  const handlePointerCancel = () => {
587
- startRef.current = null;
588
- committedRef.current = null;
589
- offsetRef.current = 0;
590
- scrollTargetRef.current = null;
589
+ resetDragRefs(refs);
591
590
  onDragUpdate({
592
591
  isDragging: false,
593
592
  offset: 0
594
593
  });
595
594
  };
595
+ return {
596
+ handlePointerCancel,
597
+ handlePointerDown,
598
+ handlePointerMove,
599
+ handlePointerUp
600
+ };
601
+ };
602
+ /**
603
+ * Hook that manages drag gestures that can dismiss a sheet panel.
604
+ *
605
+ * Gesture pipeline:
606
+ * 1. Dead zone (10px) — ignores micro-movements
607
+ * 2. Angle check (35°) — must be roughly aligned with dismiss axis
608
+ * 3. Scroll conflict — yields to scrollable containers not at edge
609
+ * 4. Commit — drag is active, applies offset via `onDragUpdate`
610
+ * 5. Release — velocity + threshold determine close/snap/bounce-back
611
+ *
612
+ * Opposite-direction drag uses √(offset) damping for elastic
613
+ * rubber-band resistance (same physics as iOS over-scroll).
614
+ *
615
+ * When `snapHeights` is provided, release targeting uses
616
+ * `findSnapTarget()` instead of the simple threshold check.
617
+ */
618
+ const useDrag = (panelRef, config, onDragUpdate) => {
619
+ const startRef = useRef(null);
620
+ const committedRef = useRef(null);
621
+ const offsetRef = useRef(0);
622
+ const scrollTargetRef = useRef(null);
623
+ const { axis, sign } = getDismissAxis(config.side);
624
+ const { handlePointerCancel, handlePointerDown, handlePointerMove, handlePointerUp } = createDragHandlers({
625
+ axis,
626
+ config,
627
+ onDragUpdate,
628
+ panelRef,
629
+ refs: {
630
+ committedRef,
631
+ offsetRef,
632
+ scrollTargetRef,
633
+ startRef
634
+ },
635
+ sign
636
+ });
596
637
  const handlersRef = useRef({
597
638
  handlePointerCancel,
598
639
  handlePointerDown,
@@ -607,20 +648,32 @@ const useDrag = (panelRef, config, onDragUpdate) => {
607
648
  };
608
649
  useEffect(() => {
609
650
  const el = panelRef.current;
610
- if (!(el && config.enabled)) return;
611
- const onPointerDown = (event) => handlersRef.current.handlePointerDown(event);
612
- const onPointerMove = (event) => handlersRef.current.handlePointerMove(event);
613
- const onPointerUp = (event) => handlersRef.current.handlePointerUp(event);
614
- const onPointerCancel = () => handlersRef.current.handlePointerCancel();
615
- el.addEventListener("pointerdown", onPointerDown);
616
- el.addEventListener("pointermove", onPointerMove);
617
- el.addEventListener("pointerup", onPointerUp);
618
- el.addEventListener("pointercancel", onPointerCancel);
651
+ const onPointerDown = (event) => {
652
+ handlersRef.current.handlePointerDown(event);
653
+ };
654
+ const onPointerMove = (event) => {
655
+ handlersRef.current.handlePointerMove(event);
656
+ };
657
+ const onPointerUp = () => {
658
+ handlersRef.current.handlePointerUp();
659
+ };
660
+ const onPointerCancel = () => {
661
+ handlersRef.current.handlePointerCancel();
662
+ };
663
+ const canListen = el !== null && config.enabled;
664
+ if (canListen) {
665
+ el.addEventListener("pointerdown", onPointerDown);
666
+ el.addEventListener("pointermove", onPointerMove);
667
+ el.addEventListener("pointerup", onPointerUp);
668
+ el.addEventListener("pointercancel", onPointerCancel);
669
+ }
619
670
  return () => {
620
- el.removeEventListener("pointerdown", onPointerDown);
621
- el.removeEventListener("pointermove", onPointerMove);
622
- el.removeEventListener("pointerup", onPointerUp);
623
- el.removeEventListener("pointercancel", onPointerCancel);
671
+ if (canListen) {
672
+ el.removeEventListener("pointerdown", onPointerDown);
673
+ el.removeEventListener("pointermove", onPointerMove);
674
+ el.removeEventListener("pointerup", onPointerUp);
675
+ el.removeEventListener("pointercancel", onPointerCancel);
676
+ }
624
677
  };
625
678
  }, [panelRef, config.enabled]);
626
679
  };
@@ -795,11 +848,12 @@ const DefaultHeader = ({ isNested, onBack, onClose, className }) => /* @__PURE__
795
848
  //#endregion
796
849
  //#region src/SheetPanel/sheet-panel-content.tsx
797
850
  const PanelInnerContent = ({ isComposable, shouldRender, Content, data, renderHeader, headerProps, headerClassName }) => {
798
- if (isComposable) return shouldRender && Content ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
799
- return /* @__PURE__ */ jsxs(Fragment, { children: [renderHeader ? renderHeader(headerProps) : /* @__PURE__ */ jsx(DefaultHeader, {
851
+ if (isComposable) return shouldRender && Content !== void 0 ? /* @__PURE__ */ jsx(Content, { ...data }) : null;
852
+ const customHeader = renderHeader !== void 0 && renderHeader !== false ? renderHeader : void 0;
853
+ return /* @__PURE__ */ jsxs(Fragment, { children: [customHeader === void 0 ? /* @__PURE__ */ jsx(DefaultHeader, {
800
854
  ...headerProps,
801
855
  className: headerClassName
802
- }), shouldRender && Content && /* @__PURE__ */ jsx("div", {
856
+ }) : customHeader(headerProps), shouldRender && Content !== void 0 && /* @__PURE__ */ jsx("div", {
803
857
  className: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
804
858
  "data-stacksheet-no-drag": "",
805
859
  children: /* @__PURE__ */ jsx(Content, { ...data })
@@ -816,7 +870,9 @@ const LAYERED_MODAL_SELECTORS = [
816
870
  ].join(", ");
817
871
  const subscribeToFocusTarget = (onStoreChange) => {
818
872
  document.addEventListener("focusin", onStoreChange, true);
819
- return () => document.removeEventListener("focusin", onStoreChange, true);
873
+ return () => {
874
+ document.removeEventListener("focusin", onStoreChange, true);
875
+ };
820
876
  };
821
877
  const getServerLayeredModalFocused = () => false;
822
878
  const getLayeredModalFocused = () => {
@@ -837,7 +893,7 @@ const ModalFocusTrap = ({ enabled, active, fallbackRef, children }) => {
837
893
  allowOutsideClick: true,
838
894
  escapeDeactivates: false,
839
895
  fallbackFocus: () => {
840
- if (fallbackRef.current) return fallbackRef.current;
896
+ if (fallbackRef.current !== null) return fallbackRef.current;
841
897
  return document.body;
842
898
  },
843
899
  initialFocus: false,
@@ -890,10 +946,18 @@ const resolvePanelLayout = (layout, renderHeader) => {
890
946
  //#region src/SheetPanel/sheet-panel.tsx
891
947
  const getPanelAriaLabel = (item, fallbackLabel) => item.ariaLabel ?? (typeof item.data?.__ariaLabel === "string" ? item.data.__ariaLabel : void 0) ?? fallbackLabel;
892
948
  const getPanelHoverProps = (enabled, setIsHovered) => enabled ? {
893
- onBlur: () => setIsHovered(false),
894
- onFocus: () => setIsHovered(true),
895
- onMouseEnter: () => setIsHovered(true),
896
- onMouseLeave: () => setIsHovered(false)
949
+ onBlur: () => {
950
+ setIsHovered(false);
951
+ },
952
+ onFocus: () => {
953
+ setIsHovered(true);
954
+ },
955
+ onMouseEnter: () => {
956
+ setIsHovered(true);
957
+ },
958
+ onMouseLeave: () => {
959
+ setIsHovered(false);
960
+ }
897
961
  } : {};
898
962
  const getInactivePanelProps = (isTop) => isTop ? {} : {
899
963
  "aria-hidden": "true",
@@ -927,25 +991,22 @@ const completeOpeningAnimation = (hasEnteredRef, isTop, onOpenCompleteRef) => {
927
991
  onOpenCompleteRef.current?.();
928
992
  }
929
993
  };
930
- const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNames, Content, shouldRender, pop, close, swipeClose, swipePop, snapHeights, activeSnapIndex, onSnap, layout, renderHeader, slideFrom, slideTarget, spring, stackSpring, prefersReducedMotion }) => {
931
- const panelRef = useRef(null);
994
+ const useOpeningCompletion = (isTop, onOpenComplete) => {
932
995
  const hasEnteredRef = useRef(false);
933
- const onOpenCompleteRef = useRef(config.onOpenComplete);
934
- const [dragState, setDragState] = useState({
935
- isDragging: false,
936
- offset: 0
937
- });
938
- const [isHovered, setIsHovered] = useState(false);
939
- const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
940
- const transform = getStackTransform(depth, config.stacking);
941
- const panelStyles = getPanelStyles(side, config, index);
996
+ const onOpenCompleteRef = useRef(onOpenComplete);
942
997
  if (!isTop && hasEnteredRef.current) hasEnteredRef.current = false;
943
998
  useEffect(() => {
944
- onOpenCompleteRef.current = config.onOpenComplete;
945
- }, [config.onOpenComplete]);
946
- const handleAnimationComplete = () => {
999
+ onOpenCompleteRef.current = onOpenComplete;
1000
+ }, [onOpenComplete]);
1001
+ return () => {
947
1002
  completeOpeningAnimation(hasEnteredRef, isTop, onOpenCompleteRef);
948
1003
  };
1004
+ };
1005
+ const useSheetPanelDrag = ({ activeSnapIndex, config, isNested, isTop, onSnap, prefersReducedMotion, side, snapHeights, swipeClose, swipePop }, panelRef) => {
1006
+ const [dragState, setDragState] = useState({
1007
+ isDragging: false,
1008
+ offset: 0
1009
+ });
949
1010
  useDrag(panelRef, {
950
1011
  activeSnapIndex,
951
1012
  closeThreshold: config.closeThreshold,
@@ -959,23 +1020,42 @@ const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNa
959
1020
  snapHeights,
960
1021
  velocityThreshold: config.velocityThreshold
961
1022
  }, setDragState);
962
- const ariaLabel = getPanelAriaLabel(item, config.ariaLabel);
963
- const panelId = `stacksheet-${item.id}`;
1023
+ return dragState;
1024
+ };
1025
+ const useSheetPanelContext = ({ close, isNested, isTop, pop, side }, panelId) => {
964
1026
  const [hasDescription, setHasDescription] = useState(false);
965
1027
  const registerDescription = () => {
966
1028
  setHasDescription(true);
967
- return () => setHasDescription(false);
1029
+ return () => {
1030
+ setHasDescription(false);
1031
+ };
968
1032
  };
969
- const panelContext = getPanelContext({
970
- close,
1033
+ return {
971
1034
  hasDescription,
972
- isNested,
973
- isTop,
974
- panelId,
975
- pop,
976
- registerDescription,
977
- side
978
- });
1035
+ panelContext: getPanelContext({
1036
+ close,
1037
+ hasDescription,
1038
+ isNested,
1039
+ isTop,
1040
+ panelId,
1041
+ pop,
1042
+ registerDescription,
1043
+ side
1044
+ })
1045
+ };
1046
+ };
1047
+ const useSheetPanelModel = (props) => {
1048
+ const { item, index, depth, isTop, isNested, side, config, classNames, pop, close, snapHeights, activeSnapIndex, layout, renderHeader, slideFrom, slideTarget, spring, stackSpring } = props;
1049
+ const panelRef = useRef(null);
1050
+ const [isHovered, setIsHovered] = useState(false);
1051
+ const measuredHeight = usePanelHeight(panelRef, snapHeights.length > 0);
1052
+ const transform = getStackTransform(depth, config.stacking);
1053
+ const panelStyles = getPanelStyles(side, config, index);
1054
+ const handleAnimationComplete = useOpeningCompletion(isTop, config.onOpenComplete);
1055
+ const dragState = useSheetPanelDrag(props, panelRef);
1056
+ const ariaLabel = getPanelAriaLabel(item, config.ariaLabel);
1057
+ const panelId = `stacksheet-${item.id}`;
1058
+ const { hasDescription, panelContext } = useSheetPanelContext(props, panelId);
979
1059
  const isComposable = resolvePanelLayout(layout, renderHeader) === "composable";
980
1060
  const hasPanelClass = classNames.panel !== "";
981
1061
  const dragOffset = getDragTransform(side, dragState.offset);
@@ -1002,9 +1082,26 @@ const SheetPanel = ({ item, index, depth, isTop, isNested, side, config, classNa
1002
1082
  show: showSideHandle,
1003
1083
  side
1004
1084
  });
1005
- const bottomHandle = getOptionalBottomHandle(showBottomHandle, dismiss);
1006
- const hoverProps = getPanelHoverProps(showSideHandle, setIsHovered);
1007
- const inactivePanelProps = getInactivePanelProps(isTop);
1085
+ return {
1086
+ animateTarget,
1087
+ ariaProps,
1088
+ bottomHandle: getOptionalBottomHandle(showBottomHandle, dismiss),
1089
+ handleAnimationComplete,
1090
+ headerProps,
1091
+ hoverProps: getPanelHoverProps(showSideHandle, setIsHovered),
1092
+ inactivePanelProps: getInactivePanelProps(isTop),
1093
+ initialRadius,
1094
+ isComposable,
1095
+ panelContext,
1096
+ panelRef,
1097
+ panelStyle,
1098
+ resolvedSlideFrom,
1099
+ sideHandle
1100
+ };
1101
+ };
1102
+ const SheetPanel = (props) => {
1103
+ const { item, isTop, config, classNames, Content, shouldRender, renderHeader, prefersReducedMotion } = props;
1104
+ const { animateTarget, ariaProps, bottomHandle, handleAnimationComplete, headerProps, hoverProps, inactivePanelProps, initialRadius, isComposable, panelContext, panelRef, panelStyle, resolvedSlideFrom, sideHandle } = useSheetPanelModel(props);
1008
1105
  const panelContent = /* @__PURE__ */ jsxs(m.div, {
1009
1106
  animate: animateTarget,
1010
1107
  className: classNames.panel || void 0,
@@ -1079,21 +1176,27 @@ const getBackdropStyle = (config, hasBackdropClass) => ({
1079
1176
  zIndex: config.zIndex,
1080
1177
  ...hasBackdropClass ? {} : { background: "var(--overlay, rgba(0, 0, 0, 0.15))" }
1081
1178
  });
1082
- /**
1083
- * Root renderer component — manages the backdrop, scroll lock, snap points,
1084
- * close reasons, focus restoration, keyboard/CloseWatcher dismissal, and
1085
- * delegates per-panel rendering to `SheetPanel`.
1086
- *
1087
- * Mounted inside a Portal by `StacksheetProvider`.
1088
- */
1089
- const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) => {
1179
+ const isContentComponent = (content) => typeof content === "function";
1180
+ const getStaticContent = (sheets, type) => {
1181
+ if (sheets === void 0) return;
1182
+ const content = Object.entries(sheets).find(([sheetType]) => sheetType === type)?.[1];
1183
+ return isContentComponent(content) ? content : void 0;
1184
+ };
1185
+ const useRendererStore = (store) => {
1090
1186
  const isOpen = useStore(store, (s) => s.isOpen);
1091
1187
  const stack = useStore(store, (s) => s.stack);
1092
- const rawClose = useStore(store, (s) => s.close);
1093
- const rawPop = useStore(store, (s) => s.pop);
1094
- const side = useResolvedSide(config);
1095
- const prefersReducedMotion = useReducedMotion() ?? false;
1096
- const classNames = resolveClassNames(classNamesProp);
1188
+ const { rawClose, rawPop } = useStore(store, useShallow((s) => ({
1189
+ rawClose: s.close,
1190
+ rawPop: s.pop
1191
+ })));
1192
+ return {
1193
+ isOpen,
1194
+ rawClose,
1195
+ rawPop,
1196
+ stack
1197
+ };
1198
+ };
1199
+ const useSnapState = (config, isOpen, side, stack) => {
1097
1200
  const viewportHeight = useViewportHeight(isOpen && side === "bottom" && config.snapPoints.length > 0);
1098
1201
  const snapHeights = getResolvedSnapHeights(side, config.snapPoints, viewportHeight);
1099
1202
  const snapContext = isOpen ? stack.map((item) => item.id).join("\0") : "";
@@ -1118,6 +1221,13 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1118
1221
  });
1119
1222
  config.onSnapPointChange?.(index);
1120
1223
  };
1224
+ return {
1225
+ activeSnapIndex,
1226
+ handleSnap,
1227
+ snapHeights
1228
+ };
1229
+ };
1230
+ const useCloseControls = (rawClose, rawPop) => {
1121
1231
  const closeReasonRef = useRef("programmatic");
1122
1232
  const closeWith = (reason) => {
1123
1233
  closeReasonRef.current = reason;
@@ -1127,9 +1237,19 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1127
1237
  closeReasonRef.current = reason;
1128
1238
  rawPop();
1129
1239
  };
1130
- const close = () => closeWith("programmatic");
1131
- const pop = () => popWith("programmatic");
1132
- useBodyScale(config, isOpen, prefersReducedMotion);
1240
+ return {
1241
+ close: () => {
1242
+ closeWith("programmatic");
1243
+ },
1244
+ closeReasonRef,
1245
+ closeWith,
1246
+ pop: () => {
1247
+ popWith("programmatic");
1248
+ },
1249
+ popWith
1250
+ };
1251
+ };
1252
+ const useFocusRestore = (isOpen) => {
1133
1253
  const triggerRef = useRef(null);
1134
1254
  const wasOpenRef = useRef(false);
1135
1255
  useEffect(() => {
@@ -1141,56 +1261,96 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1141
1261
  }
1142
1262
  wasOpenRef.current = isOpen;
1143
1263
  }, [isOpen]);
1144
- const stackLengthRef = useRef(stack.length);
1264
+ };
1265
+ const dismissFromEscape = ({ closeReasonRef, rawClose, rawPop, stackLengthRef }) => {
1266
+ closeReasonRef.current = "escape";
1267
+ if (stackLengthRef.current > 1) rawPop();
1268
+ else rawClose();
1269
+ };
1270
+ const useDismissalEffects = ({ closeReasonRef, config, isOpen, rawClose, rawPop, stackLength }) => {
1271
+ const stackLengthRef = useRef(stackLength);
1145
1272
  useEffect(() => {
1146
- stackLengthRef.current = stack.length;
1147
- }, [stack.length]);
1273
+ stackLengthRef.current = stackLength;
1274
+ }, [stackLength]);
1148
1275
  useEffect(() => {
1149
- if (!(isOpen && config.closeOnEscape && config.dismissible)) return;
1276
+ const shouldListen = isOpen && config.closeOnEscape && config.dismissible;
1150
1277
  const handleKeyDown = (e) => {
1151
1278
  if (e.key === "Escape") {
1152
1279
  e.preventDefault();
1153
- if (stackLengthRef.current > 1) {
1154
- closeReasonRef.current = "escape";
1155
- rawPop();
1156
- } else {
1157
- closeReasonRef.current = "escape";
1158
- rawClose();
1159
- }
1280
+ dismissFromEscape({
1281
+ closeReasonRef,
1282
+ rawClose,
1283
+ rawPop,
1284
+ stackLengthRef
1285
+ });
1160
1286
  }
1161
1287
  };
1162
- document.addEventListener("keydown", handleKeyDown);
1163
- return () => document.removeEventListener("keydown", handleKeyDown);
1288
+ if (shouldListen) document.addEventListener("keydown", handleKeyDown);
1289
+ return () => {
1290
+ if (shouldListen) document.removeEventListener("keydown", handleKeyDown);
1291
+ };
1164
1292
  }, [
1165
1293
  isOpen,
1166
1294
  config.closeOnEscape,
1167
1295
  config.dismissible,
1168
1296
  rawPop,
1169
- rawClose
1297
+ rawClose,
1298
+ closeReasonRef
1170
1299
  ]);
1171
1300
  useEffect(() => {
1172
- if (!(isOpen && config.dismissible) || globalThis.CloseWatcher === void 0) return;
1173
- const watcher = new globalThis.CloseWatcher();
1301
+ const CloseWatcherConstructor = globalThis.CloseWatcher;
1302
+ const shouldListen = isOpen && config.dismissible && CloseWatcherConstructor !== void 0;
1303
+ let watcher;
1174
1304
  const handleClose = () => {
1175
- if (stackLengthRef.current > 1) {
1176
- closeReasonRef.current = "escape";
1177
- rawPop();
1178
- } else {
1179
- closeReasonRef.current = "escape";
1180
- rawClose();
1181
- }
1305
+ dismissFromEscape({
1306
+ closeReasonRef,
1307
+ rawClose,
1308
+ rawPop,
1309
+ stackLengthRef
1310
+ });
1182
1311
  };
1183
- watcher.addEventListener("close", handleClose);
1312
+ if (shouldListen) {
1313
+ watcher = new CloseWatcherConstructor();
1314
+ watcher.addEventListener("close", handleClose);
1315
+ }
1184
1316
  return () => {
1185
- watcher.removeEventListener("close", handleClose);
1186
- watcher.destroy();
1317
+ if (watcher !== void 0) {
1318
+ watcher.removeEventListener("close", handleClose);
1319
+ watcher.destroy();
1320
+ }
1187
1321
  };
1188
1322
  }, [
1189
1323
  isOpen,
1190
1324
  config.dismissible,
1191
1325
  rawPop,
1192
- rawClose
1326
+ rawClose,
1327
+ closeReasonRef
1193
1328
  ]);
1329
+ };
1330
+ /**
1331
+ * Root renderer component — manages the backdrop, scroll lock, snap points,
1332
+ * close reasons, focus restoration, keyboard/CloseWatcher dismissal, and
1333
+ * delegates per-panel rendering to `SheetPanel`.
1334
+ *
1335
+ * Mounted inside a Portal by `StacksheetProvider`.
1336
+ */
1337
+ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classNamesProp, layout, renderHeader }) => {
1338
+ const { isOpen, rawClose, rawPop, stack } = useRendererStore(store);
1339
+ const side = useResolvedSide(config);
1340
+ const prefersReducedMotion = useReducedMotion() ?? false;
1341
+ const classNames = resolveClassNames(classNamesProp);
1342
+ const { activeSnapIndex, handleSnap, snapHeights } = useSnapState(config, isOpen, side, stack);
1343
+ const { close, closeReasonRef, closeWith, pop, popWith } = useCloseControls(rawClose, rawPop);
1344
+ useBodyScale(config, isOpen, prefersReducedMotion);
1345
+ useFocusRestore(isOpen);
1346
+ useDismissalEffects({
1347
+ closeReasonRef,
1348
+ config,
1349
+ isOpen,
1350
+ rawClose,
1351
+ rawPop,
1352
+ stackLength: stack.length
1353
+ });
1194
1354
  const slideFrom = getSlideFrom(side);
1195
1355
  const slideTarget = getSlideTarget();
1196
1356
  const spring = getMotionSpring(prefersReducedMotion, config);
@@ -1201,8 +1361,12 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1201
1361
  const handleExitComplete = () => {
1202
1362
  if (stack.length === 0) config.onCloseComplete?.(closeReasonRef.current);
1203
1363
  };
1204
- const swipeClose = () => closeWith("swipe");
1205
- const swipePop = () => popWith("swipe");
1364
+ const swipeClose = () => {
1365
+ closeWith("swipe");
1366
+ };
1367
+ const swipePop = () => {
1368
+ popWith("swipe");
1369
+ };
1206
1370
  const shouldLockScroll = isOpen && isModal && config.lockScroll;
1207
1371
  return /* @__PURE__ */ jsxs(LazyMotion, {
1208
1372
  features: domMax,
@@ -1213,7 +1377,9 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1213
1377
  className: `fixed inset-0 ${classNames.backdrop || ""}`,
1214
1378
  exit: { opacity: 0 },
1215
1379
  initial: { opacity: 0 },
1216
- onClick: config.closeOnBackdrop && config.dismissible ? () => closeWith("backdrop") : void 0,
1380
+ onClick: config.closeOnBackdrop && config.dismissible ? () => {
1381
+ closeWith("backdrop");
1382
+ } : void 0,
1217
1383
  style: backdropStyle,
1218
1384
  transition: spring
1219
1385
  }, "stacksheet-backdrop")
@@ -1232,7 +1398,7 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1232
1398
  const shouldRender = depth <= config.stacking.renderThreshold;
1233
1399
  return /* @__PURE__ */ jsx(SheetPanel, {
1234
1400
  activeSnapIndex,
1235
- Content: componentMap.get(item.type) ?? sheets[item.type],
1401
+ Content: componentMap.get(item.type) ?? getStaticContent(sheets, item.type),
1236
1402
  classNames,
1237
1403
  close,
1238
1404
  config,
@@ -1264,11 +1430,23 @@ const SheetRenderer = ({ store, config, sheets, componentMap, classNames: classN
1264
1430
  };
1265
1431
  //#endregion
1266
1432
  //#region src/Store/store-args.ts
1433
+ const isRecord = (value) => typeof value === "object" && value !== null;
1434
+ const toRecord = (value) => isRecord(value) ? value : {};
1435
+ const isComponent = (value) => typeof value === "function";
1436
+ const getComponentName = (component) => {
1437
+ const name = component.displayName ?? component.name;
1438
+ return name === "" ? void 0 : name;
1439
+ };
1440
+ const getNodeEnv = () => globalThis.process?.env?.NODE_ENV;
1441
+ const getStringArg = (value, name) => {
1442
+ if (typeof value !== "string") throw new TypeError(`Expected ${name} to be a string.`);
1443
+ return value;
1444
+ };
1267
1445
  const resolvePresentationOptions = (value) => {
1268
- if (!(value && typeof value === "object")) return;
1269
- const candidate = value;
1270
- if (candidate.ariaLabel !== void 0 && typeof candidate.ariaLabel !== "string") return;
1271
- return candidate;
1446
+ if (!isRecord(value)) return;
1447
+ const { ariaLabel } = value;
1448
+ if (ariaLabel !== void 0 && typeof ariaLabel !== "string") return;
1449
+ return ariaLabel === void 0 ? {} : { ariaLabel };
1272
1450
  };
1273
1451
  /**
1274
1452
  * Dev-mode warning: detect likely inline arrow functions passed as ad-hoc components.
@@ -1276,52 +1454,202 @@ const resolvePresentationOptions = (value) => {
1276
1454
  * it's almost always an inline arrow being re-created every render.
1277
1455
  */
1278
1456
  const warnInlineComponent = (component, componentRegistry, warnedNames) => {
1279
- if (process === void 0 || process?.env?.NODE_ENV === "production") return;
1280
- const name = component.displayName || component.name;
1281
- if (!name) return;
1457
+ if (getNodeEnv() === "production") return;
1458
+ const name = getComponentName(component);
1459
+ if (name === void 0) return;
1282
1460
  if (warnedNames.has(name)) return;
1283
- for (const [existing, key] of componentRegistry) if ((existing.displayName || existing.name) === name) {
1461
+ for (const [existing, key] of componentRegistry) if (getComponentName(existing) === name) {
1284
1462
  warnedNames.add(name);
1285
1463
  console.warn(`[stacksheet] A new component reference with name "${name}" was registered (key: ${key}), but a different reference with the same name already exists. This usually means you're passing an inline arrow function (e.g. open(() => <MySheet />)). Define the component outside of render to avoid memory leaks and broken navigate() same-type detection.`);
1286
1464
  return;
1287
1465
  }
1288
1466
  };
1467
+ const registerComponent = (component, componentRegistry, componentMap, getNextKey, warnedNames) => {
1468
+ const existingKey = componentRegistry.get(component);
1469
+ if (existingKey !== void 0) return existingKey;
1470
+ warnInlineComponent(component, componentRegistry, warnedNames);
1471
+ const nextKey = getNextKey();
1472
+ componentRegistry.set(component, nextKey);
1473
+ componentMap.set(nextKey, component);
1474
+ return nextKey;
1475
+ };
1289
1476
  /**
1290
1477
  * If `first` is a function (component), register it and return { type, id, data }.
1291
1478
  * Otherwise, pass through the string-based (type, id, data) args unchanged.
1292
1479
  */
1293
1480
  const resolveArgs = (componentRegistry, componentMap, getNextKey, warnedNames, first, second, third, fourth) => {
1294
- if (typeof first === "function") {
1295
- const component = first;
1296
- let typeKey = componentRegistry.get(component);
1297
- if (!typeKey) {
1298
- warnInlineComponent(component, componentRegistry, warnedNames);
1299
- typeKey = getNextKey();
1300
- componentRegistry.set(component, typeKey);
1301
- componentMap.set(typeKey, component);
1302
- }
1481
+ if (isComponent(first)) {
1482
+ const typeKey = registerComponent(first, componentRegistry, componentMap, getNextKey, warnedNames);
1303
1483
  if (typeof second === "string") return {
1304
1484
  ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
1305
- data: third ?? {},
1485
+ data: toRecord(third),
1306
1486
  id: second,
1307
1487
  type: typeKey
1308
1488
  };
1309
1489
  return {
1310
1490
  ariaLabel: resolvePresentationOptions(third)?.ariaLabel,
1311
- data: second ?? {},
1491
+ data: toRecord(second),
1312
1492
  id: crypto.randomUUID(),
1313
1493
  type: typeKey
1314
1494
  };
1315
1495
  }
1316
1496
  return {
1317
1497
  ariaLabel: resolvePresentationOptions(fourth)?.ariaLabel,
1318
- data: third ?? {},
1319
- id: second,
1320
- type: first
1498
+ data: toRecord(third),
1499
+ id: getStringArg(second, "sheet id"),
1500
+ type: getStringArg(first, "sheet type")
1321
1501
  };
1322
1502
  };
1323
1503
  //#endregion
1324
1504
  //#region src/Store/create-sheet-store.ts
1505
+ const createItem = ({ type, id, data, ariaLabel }) => ({
1506
+ ariaLabel,
1507
+ data,
1508
+ id,
1509
+ type
1510
+ });
1511
+ const createResolvedWriters = (set, config) => ({
1512
+ openResolved: (resolved) => {
1513
+ set({
1514
+ isOpen: true,
1515
+ stack: [createItem(resolved)]
1516
+ });
1517
+ },
1518
+ pushResolved: (resolved) => {
1519
+ set((state) => {
1520
+ const item = createItem(resolved);
1521
+ if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
1522
+ isOpen: true,
1523
+ stack: [...state.stack.slice(0, -1), item]
1524
+ };
1525
+ return {
1526
+ isOpen: true,
1527
+ stack: [...state.stack, item]
1528
+ };
1529
+ });
1530
+ },
1531
+ replaceResolved: (resolved) => {
1532
+ set((state) => {
1533
+ const item = createItem(resolved);
1534
+ if (state.stack.length === 0) return {
1535
+ isOpen: true,
1536
+ stack: [item]
1537
+ };
1538
+ return {
1539
+ isOpen: true,
1540
+ stack: [...state.stack.slice(0, -1), item]
1541
+ };
1542
+ });
1543
+ }
1544
+ });
1545
+ const createNavigate = (componentMap, get, resolve, writers) => (first, second, third, fourth) => {
1546
+ const resolved = resolve(first, second, third, fourth);
1547
+ const { stack } = get();
1548
+ const top = stack.at(-1);
1549
+ if (stack.length === 0) {
1550
+ writers.openResolved(resolved);
1551
+ return;
1552
+ }
1553
+ let isSameType = top?.type === resolved.type;
1554
+ if (!isSameType && isComponent(first)) isSameType = componentMap.get(top?.type ?? "") === first;
1555
+ if (isSameType) {
1556
+ writers.replaceResolved(resolved);
1557
+ return;
1558
+ }
1559
+ writers.pushResolved(resolved);
1560
+ };
1561
+ const createPop = (set, pruneRegistry) => () => {
1562
+ set((state) => {
1563
+ if (state.stack.length <= 1) {
1564
+ pruneRegistry([]);
1565
+ return {
1566
+ isOpen: false,
1567
+ stack: []
1568
+ };
1569
+ }
1570
+ const next = state.stack.slice(0, -1);
1571
+ pruneRegistry(next);
1572
+ return {
1573
+ isOpen: true,
1574
+ stack: next
1575
+ };
1576
+ });
1577
+ };
1578
+ const createRemove = (set, pruneRegistry) => (id) => {
1579
+ set((state) => {
1580
+ const next = state.stack.filter((item) => item.id !== id);
1581
+ if (next.length === state.stack.length) return state;
1582
+ pruneRegistry(next);
1583
+ return next.length === 0 ? {
1584
+ isOpen: false,
1585
+ stack: []
1586
+ } : { stack: next };
1587
+ });
1588
+ };
1589
+ const createSetData = (set, resolve) => (first, second, third) => {
1590
+ const { id, data } = resolve(first, second, third);
1591
+ set((state) => {
1592
+ const index = state.stack.findIndex((item) => item.id === id);
1593
+ const existing = state.stack[index];
1594
+ if (existing === void 0) return state;
1595
+ const updated = [...state.stack];
1596
+ updated[index] = {
1597
+ ...existing,
1598
+ data
1599
+ };
1600
+ return { stack: updated };
1601
+ });
1602
+ };
1603
+ const createSwap = (set, internals) => (first, second, third) => {
1604
+ const type = isComponent(first) ? registerComponent(first, internals.componentRegistry, internals.componentMap, internals.getNextKey, internals.warnedNames) : getStringArg(first, "sheet type");
1605
+ const data = toRecord(second);
1606
+ const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
1607
+ set((state) => {
1608
+ const top = state.stack.at(-1);
1609
+ if (top === void 0) return state;
1610
+ const newStack = [...state.stack];
1611
+ newStack[newStack.length - 1] = {
1612
+ ariaLabel: ariaLabel ?? top.ariaLabel,
1613
+ data,
1614
+ id: top.id,
1615
+ type
1616
+ };
1617
+ return { stack: newStack };
1618
+ });
1619
+ };
1620
+ const createStoreState = ({ componentMap, componentRegistry, config, getNextKey, pruneRegistry, resolve, warnedNames }) => (set, get) => {
1621
+ const writers = createResolvedWriters(set, config);
1622
+ return {
1623
+ close: () => {
1624
+ pruneRegistry([]);
1625
+ set({
1626
+ isOpen: false,
1627
+ stack: []
1628
+ });
1629
+ },
1630
+ isOpen: false,
1631
+ navigate: createNavigate(componentMap, get, resolve, writers),
1632
+ open: (first, second, third, fourth) => {
1633
+ writers.openResolved(resolve(first, second, third, fourth));
1634
+ },
1635
+ pop: createPop(set, pruneRegistry),
1636
+ push: (first, second, third, fourth) => {
1637
+ writers.pushResolved(resolve(first, second, third, fourth));
1638
+ },
1639
+ remove: createRemove(set, pruneRegistry),
1640
+ replace: (first, second, third, fourth) => {
1641
+ writers.replaceResolved(resolve(first, second, third, fourth));
1642
+ },
1643
+ setData: createSetData(set, resolve),
1644
+ stack: [],
1645
+ swap: createSwap(set, {
1646
+ componentMap,
1647
+ componentRegistry,
1648
+ getNextKey,
1649
+ warnedNames
1650
+ })
1651
+ };
1652
+ };
1325
1653
  /**
1326
1654
  * Create an isolated Zustand store for a sheet stack instance.
1327
1655
  *
@@ -1355,165 +1683,15 @@ const createSheetStore = (config) => {
1355
1683
  return {
1356
1684
  componentMap,
1357
1685
  componentRegistry,
1358
- store: createStore()((set, get) => {
1359
- const _openResolved = ({ type, id, data, ariaLabel }) => {
1360
- set({
1361
- isOpen: true,
1362
- stack: [{
1363
- ariaLabel,
1364
- data,
1365
- id,
1366
- type
1367
- }]
1368
- });
1369
- };
1370
- const _pushResolved = ({ type, id, data, ariaLabel }) => {
1371
- set((state) => {
1372
- const item = {
1373
- ariaLabel,
1374
- data,
1375
- id,
1376
- type
1377
- };
1378
- if (Number.isFinite(config.maxDepth) && state.stack.length >= config.maxDepth) return {
1379
- isOpen: true,
1380
- stack: [...state.stack.slice(0, -1), item]
1381
- };
1382
- return {
1383
- isOpen: true,
1384
- stack: [...state.stack, item]
1385
- };
1386
- });
1387
- };
1388
- const _replaceResolved = ({ type, id, data, ariaLabel }) => {
1389
- set((state) => {
1390
- const item = {
1391
- ariaLabel,
1392
- data,
1393
- id,
1394
- type
1395
- };
1396
- if (state.stack.length === 0) return {
1397
- isOpen: true,
1398
- stack: [item]
1399
- };
1400
- return {
1401
- isOpen: true,
1402
- stack: [...state.stack.slice(0, -1), item]
1403
- };
1404
- });
1405
- };
1406
- return {
1407
- close: () => {
1408
- pruneRegistry([]);
1409
- set({
1410
- isOpen: false,
1411
- stack: []
1412
- });
1413
- },
1414
- isOpen: false,
1415
- navigate: (first, second, third, fourth) => {
1416
- const resolved = resolve(first, second, third, fourth);
1417
- const { stack } = get();
1418
- const top = stack.at(-1);
1419
- if (stack.length === 0) {
1420
- _openResolved(resolved);
1421
- return;
1422
- }
1423
- let isSameType = top?.type === resolved.type;
1424
- if (!isSameType && typeof first === "function") isSameType = componentMap.get(top?.type ?? "") === first;
1425
- if (isSameType) {
1426
- _replaceResolved(resolved);
1427
- return;
1428
- }
1429
- _pushResolved(resolved);
1430
- },
1431
- open: (first, second, third, fourth) => {
1432
- _openResolved(resolve(first, second, third, fourth));
1433
- },
1434
- pop: () => {
1435
- set((state) => {
1436
- if (state.stack.length <= 1) {
1437
- pruneRegistry([]);
1438
- return {
1439
- isOpen: false,
1440
- stack: []
1441
- };
1442
- }
1443
- const next = state.stack.slice(0, -1);
1444
- pruneRegistry(next);
1445
- return {
1446
- isOpen: true,
1447
- stack: next
1448
- };
1449
- });
1450
- },
1451
- push: (first, second, third, fourth) => {
1452
- _pushResolved(resolve(first, second, third, fourth));
1453
- },
1454
- remove: (id) => {
1455
- set((state) => {
1456
- const next = state.stack.filter((item) => item.id !== id);
1457
- if (next.length === state.stack.length) return state;
1458
- pruneRegistry(next);
1459
- if (next.length === 0) return {
1460
- isOpen: false,
1461
- stack: []
1462
- };
1463
- return { stack: next };
1464
- });
1465
- },
1466
- replace: (first, second, third, fourth) => {
1467
- _replaceResolved(resolve(first, second, third, fourth));
1468
- },
1469
- setData: (first, second, third) => {
1470
- const { id, data } = resolve(first, second, third);
1471
- set((state) => {
1472
- const idx = state.stack.findIndex((item) => item.id === id);
1473
- if (idx === -1) return state;
1474
- const updated = [...state.stack];
1475
- updated[idx] = {
1476
- ...updated[idx],
1477
- data
1478
- };
1479
- return { stack: updated };
1480
- });
1481
- },
1482
- stack: [],
1483
- swap: (first, second, third) => {
1484
- let type;
1485
- let data;
1486
- const ariaLabel = resolvePresentationOptions(third)?.ariaLabel;
1487
- if (typeof first === "function") {
1488
- const component = first;
1489
- let typeKey = componentRegistry.get(component);
1490
- if (!typeKey) {
1491
- warnInlineComponent(component, componentRegistry, warnedNames);
1492
- typeKey = getNextKey();
1493
- componentRegistry.set(component, typeKey);
1494
- componentMap.set(typeKey, component);
1495
- }
1496
- type = typeKey;
1497
- data = second ?? {};
1498
- } else {
1499
- type = first;
1500
- data = second ?? {};
1501
- }
1502
- set((state) => {
1503
- const top = state.stack.at(-1);
1504
- if (!top) return state;
1505
- const newStack = [...state.stack];
1506
- newStack[newStack.length - 1] = {
1507
- ariaLabel: ariaLabel ?? top.ariaLabel,
1508
- data,
1509
- id: top.id,
1510
- type
1511
- };
1512
- return { stack: newStack };
1513
- });
1514
- }
1515
- };
1516
- })
1686
+ store: createStore()(createStoreState({
1687
+ componentMap,
1688
+ componentRegistry,
1689
+ config,
1690
+ getNextKey,
1691
+ pruneRegistry,
1692
+ resolve,
1693
+ warnedNames
1694
+ }))
1517
1695
  };
1518
1696
  };
1519
1697
  //#endregion
@@ -1557,12 +1735,11 @@ const createStacksheet = (config) => {
1557
1735
  if (!ctx) throw new Error("useSheet/useStacksheetState must be used within <StacksheetProvider>");
1558
1736
  return ctx;
1559
1737
  };
1560
- const EMPTY_SHEETS = {};
1561
1738
  const providerValue = {
1562
1739
  config: resolved,
1563
1740
  store
1564
1741
  };
1565
- const StacksheetProvider = ({ sheets = EMPTY_SHEETS, children, classNames, layout, renderHeader }) => /* @__PURE__ */ jsxs(StoreContext.Provider, {
1742
+ const StacksheetProvider = ({ sheets, children, classNames, layout, renderHeader }) => /* @__PURE__ */ jsxs(StoreContext.Provider, {
1566
1743
  value: providerValue,
1567
1744
  children: [children, /* @__PURE__ */ jsx(Portal, {
1568
1745
  asChild: false,
@@ -1611,19 +1788,20 @@ const createStacksheet = (config) => {
1611
1788
  const SheetBack = ({ asChild, className, style, children }) => {
1612
1789
  const { back, isNested } = useSheetPanel();
1613
1790
  if (!isNested) return null;
1614
- return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
1615
- "aria-label": children ? void 0 : "Back",
1616
- className: joinClassNames(asChild ? void 0 : "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-inherit opacity-60 transition-opacity duration-150 hover:opacity-100", className),
1791
+ const isAsChild = asChild === true;
1792
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
1793
+ "aria-label": children === void 0 || children === null ? "Back" : void 0,
1794
+ className: joinClassNames(isAsChild ? void 0 : "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-inherit opacity-60 transition-opacity duration-150 hover:opacity-100", className),
1617
1795
  onClick: back,
1618
1796
  style,
1619
- type: asChild ? void 0 : "button",
1797
+ type: isAsChild ? void 0 : "button",
1620
1798
  children: children ?? /* @__PURE__ */ jsx(ArrowLeftIcon, {})
1621
1799
  });
1622
1800
  };
1623
1801
  //#endregion
1624
1802
  //#region src/SheetParts/sheet-body.tsx
1625
1803
  const SheetBody = ({ asChild, className, style, children }) => {
1626
- if (asChild) return /* @__PURE__ */ jsx(Slot, {
1804
+ if (asChild === true) return /* @__PURE__ */ jsx(Slot, {
1627
1805
  className: joinClassNames("relative min-h-0 flex-1", className),
1628
1806
  "data-stacksheet-no-drag": "",
1629
1807
  style,
@@ -1647,12 +1825,13 @@ const SheetBody = ({ asChild, className, style, children }) => {
1647
1825
  //#region src/SheetParts/sheet-close.tsx
1648
1826
  const SheetClose = ({ asChild, className, style, children }) => {
1649
1827
  const { close } = useSheetPanel();
1650
- return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
1651
- "aria-label": children ? void 0 : "Close",
1652
- className: joinClassNames(asChild ? void 0 : "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-inherit opacity-60 transition-opacity duration-150 hover:opacity-100", className),
1828
+ const isAsChild = asChild === true;
1829
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
1830
+ "aria-label": children === void 0 || children === null ? "Close" : void 0,
1831
+ className: joinClassNames(isAsChild ? void 0 : "flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 text-inherit opacity-60 transition-opacity duration-150 hover:opacity-100", className),
1653
1832
  onClick: close,
1654
1833
  style,
1655
- type: asChild ? void 0 : "button",
1834
+ type: isAsChild ? void 0 : "button",
1656
1835
  children: children ?? /* @__PURE__ */ jsx(XIcon, {})
1657
1836
  });
1658
1837
  };
@@ -1661,7 +1840,7 @@ const SheetClose = ({ asChild, className, style, children }) => {
1661
1840
  const SheetDescription = ({ asChild, className, style, children }) => {
1662
1841
  const { panelId, registerDescription } = useSheetPanel();
1663
1842
  useEffect(() => registerDescription(), [registerDescription]);
1664
- return /* @__PURE__ */ jsx(asChild ? Slot : "p", {
1843
+ return /* @__PURE__ */ jsx(asChild === true ? Slot : "p", {
1665
1844
  className,
1666
1845
  id: `${panelId}-desc`,
1667
1846
  style,
@@ -1671,8 +1850,9 @@ const SheetDescription = ({ asChild, className, style, children }) => {
1671
1850
  //#endregion
1672
1851
  //#region src/SheetParts/sheet-footer.tsx
1673
1852
  const SheetFooter = ({ asChild, className, style, children }) => {
1674
- return /* @__PURE__ */ jsx(asChild ? Slot : "footer", {
1675
- className: joinClassNames(asChild ? "shrink-0" : "flex shrink-0 items-center gap-2 border-t px-6 py-3", className),
1853
+ const isAsChild = asChild === true;
1854
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "footer", {
1855
+ className: joinClassNames(isAsChild ? "shrink-0" : "flex shrink-0 items-center gap-2 border-t px-6 py-3", className),
1676
1856
  style,
1677
1857
  children
1678
1858
  });
@@ -1682,13 +1862,14 @@ const SheetFooter = ({ asChild, className, style, children }) => {
1682
1862
  const SheetHandle = ({ asChild, className, style, children }) => {
1683
1863
  const { close, back, isNested } = useSheetPanel();
1684
1864
  const dismiss = isNested ? back : close;
1685
- return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
1865
+ const isAsChild = asChild === true;
1866
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "button", {
1686
1867
  "aria-label": "Dismiss",
1687
- className: joinClassNames(asChild ? void 0 : "flex shrink-0 cursor-grab touch-none items-center justify-center border-none bg-transparent pt-4 pb-1 text-inherit", className),
1868
+ className: joinClassNames(isAsChild ? void 0 : "flex shrink-0 cursor-grab touch-none items-center justify-center border-none bg-transparent pt-4 pb-1 text-inherit", className),
1688
1869
  "data-stacksheet-handle": "",
1689
1870
  onClick: dismiss,
1690
1871
  style,
1691
- type: asChild ? void 0 : "button",
1872
+ type: isAsChild ? void 0 : "button",
1692
1873
  children: children ?? /* @__PURE__ */ jsx("div", {
1693
1874
  "aria-hidden": "true",
1694
1875
  className: "h-1 w-9 rounded-sm bg-current/25"
@@ -1698,8 +1879,9 @@ const SheetHandle = ({ asChild, className, style, children }) => {
1698
1879
  //#endregion
1699
1880
  //#region src/SheetParts/sheet-header.tsx
1700
1881
  const SheetHeader = ({ asChild, className, style, children }) => {
1701
- return /* @__PURE__ */ jsx(asChild ? Slot : "header", {
1702
- className: joinClassNames(asChild ? "shrink-0" : "flex h-14 shrink-0 items-center justify-between border-b px-6", className),
1882
+ const isAsChild = asChild === true;
1883
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "header", {
1884
+ className: joinClassNames(isAsChild ? "shrink-0" : "flex h-14 shrink-0 items-center justify-between border-b px-6", className),
1703
1885
  style,
1704
1886
  children
1705
1887
  });
@@ -1708,8 +1890,9 @@ const SheetHeader = ({ asChild, className, style, children }) => {
1708
1890
  //#region src/SheetParts/sheet-title.tsx
1709
1891
  const SheetTitle = ({ asChild, className, style, children }) => {
1710
1892
  const { panelId } = useSheetPanel();
1711
- return /* @__PURE__ */ jsx(asChild ? Slot : "h2", {
1712
- className: joinClassNames(asChild ? void 0 : "font-semibold text-sm", className),
1893
+ const isAsChild = asChild === true;
1894
+ return /* @__PURE__ */ jsx(isAsChild ? Slot : "h2", {
1895
+ className: joinClassNames(isAsChild ? void 0 : "font-semibold text-sm", className),
1713
1896
  id: `${panelId}-title`,
1714
1897
  style,
1715
1898
  children