react-panel-layout 0.6.0 → 0.7.0

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 (258) hide show
  1. package/dist/{FloatingPanelFrame-SgYLc6Ud.js → FloatingPanelFrame-3eU9AwPo.js} +2 -2
  2. package/dist/{FloatingPanelFrame-SgYLc6Ud.js.map → FloatingPanelFrame-3eU9AwPo.js.map} +1 -1
  3. package/dist/FloatingWindow-Bw2djgpz.js +1542 -0
  4. package/dist/FloatingWindow-Bw2djgpz.js.map +1 -0
  5. package/dist/FloatingWindow-Cvyokf0m.cjs +2 -0
  6. package/dist/FloatingWindow-Cvyokf0m.cjs.map +1 -0
  7. package/dist/GridLayout-B4aCqSyd.js +947 -0
  8. package/dist/{GridLayout-BltqeCPK.js.map → GridLayout-B4aCqSyd.js.map} +1 -1
  9. package/dist/GridLayout-DNOClFzz.cjs +2 -0
  10. package/dist/{GridLayout-B4VRsC0r.cjs.map → GridLayout-DNOClFzz.cjs.map} +1 -1
  11. package/dist/{HorizontalDivider-WF1k_qND.js → HorizontalDivider-DdxzfV0l.js} +3 -3
  12. package/dist/{HorizontalDivider-WF1k_qND.js.map → HorizontalDivider-DdxzfV0l.js.map} +1 -1
  13. package/dist/{HorizontalDivider-B5Z-KZLk.cjs → HorizontalDivider-_pgV4Mcv.cjs} +2 -2
  14. package/dist/{HorizontalDivider-B5Z-KZLk.cjs.map → HorizontalDivider-_pgV4Mcv.cjs.map} +1 -1
  15. package/dist/PanelSystem-B8Igvnb2.cjs +3 -0
  16. package/dist/PanelSystem-B8Igvnb2.cjs.map +1 -0
  17. package/dist/{PanelSystem-Dr1TBhxM.js → PanelSystem-DDUSFjXD.js} +209 -248
  18. package/dist/PanelSystem-DDUSFjXD.js.map +1 -0
  19. package/dist/ResizeHandle-CBcAS918.cjs +2 -0
  20. package/dist/{ResizeHandle-CScipO5l.cjs.map → ResizeHandle-CBcAS918.cjs.map} +1 -1
  21. package/dist/{ResizeHandle-CdA_JYfN.js → ResizeHandle-CXjc1meV.js} +28 -29
  22. package/dist/{ResizeHandle-CdA_JYfN.js.map → ResizeHandle-CXjc1meV.js.map} +1 -1
  23. package/dist/SwipePivotTabBar-DWrCuwEI.js +411 -0
  24. package/dist/SwipePivotTabBar-DWrCuwEI.js.map +1 -0
  25. package/dist/SwipePivotTabBar-fjjXkpj7.cjs +2 -0
  26. package/dist/SwipePivotTabBar-fjjXkpj7.cjs.map +1 -0
  27. package/dist/components/gesture/SwipeSafeZone.d.ts +40 -0
  28. package/dist/components/window/Drawer.d.ts +4 -1
  29. package/dist/components/window/DrawerLayers.d.ts +1 -1
  30. package/dist/components/window/DrawerRevealContext.d.ts +61 -0
  31. package/dist/components/window/drawerRevealAnimationUtils.d.ts +212 -0
  32. package/dist/components/window/drawerStyles.d.ts +74 -0
  33. package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
  34. package/dist/components/window/useDrawerSwipeTransform.d.ts +29 -0
  35. package/dist/components/window/useDrawerTransform.d.ts +68 -0
  36. package/dist/components/window/useRevealDrawerTransform.d.ts +56 -0
  37. package/dist/config.cjs +1 -1
  38. package/dist/config.cjs.map +1 -1
  39. package/dist/config.js +9 -8
  40. package/dist/config.js.map +1 -1
  41. package/dist/constants/styles.d.ts +17 -0
  42. package/dist/dialog/index.d.ts +69 -0
  43. package/dist/floating.js +1 -1
  44. package/dist/grid.cjs +1 -1
  45. package/dist/grid.js +2 -2
  46. package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +7 -0
  47. package/dist/hooks/gesture/types.d.ts +48 -5
  48. package/dist/hooks/gesture/utils.d.ts +19 -0
  49. package/dist/hooks/useAnimationFrame.d.ts +2 -0
  50. package/dist/hooks/useOperationContinuity.d.ts +64 -0
  51. package/dist/hooks/useResizeObserver.d.ts +33 -1
  52. package/dist/hooks/useSharedElementTransition.d.ts +112 -0
  53. package/dist/hooks/useSwipeContentTransform.d.ts +9 -2
  54. package/dist/index.cjs +1 -1
  55. package/dist/index.js +7 -7
  56. package/dist/modules/dialog/AlertDialog.d.ts +9 -0
  57. package/dist/modules/dialog/DialogContainer.d.ts +37 -0
  58. package/dist/modules/dialog/Modal.d.ts +26 -0
  59. package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
  60. package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
  61. package/dist/modules/dialog/types.d.ts +183 -0
  62. package/dist/modules/dialog/useDialog.d.ts +39 -0
  63. package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
  64. package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
  65. package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
  66. package/dist/modules/drawer/drawerStateMachine.d.ts +168 -0
  67. package/dist/modules/drawer/revealDrawerConstants.d.ts +33 -0
  68. package/dist/modules/drawer/revealDrawerStateMachine.d.ts +146 -0
  69. package/dist/modules/drawer/strategies/index.d.ts +8 -0
  70. package/dist/modules/drawer/strategies/overlayStrategy.d.ts +12 -0
  71. package/dist/modules/drawer/strategies/revealStrategy.d.ts +12 -0
  72. package/dist/modules/drawer/strategies/types.d.ts +116 -0
  73. package/dist/modules/drawer/types.d.ts +74 -0
  74. package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
  75. package/dist/modules/pivot/SwipePivotTabBar.d.ts +3 -0
  76. package/dist/modules/stack/SwipeStackContent.d.ts +6 -3
  77. package/dist/modules/stack/SwipeStackOutlet.d.ts +4 -4
  78. package/dist/modules/stack/computeSwipeStackTransform.d.ts +1 -1
  79. package/dist/panels.cjs +1 -1
  80. package/dist/panels.js +1 -1
  81. package/dist/pivot.cjs +1 -1
  82. package/dist/pivot.js +1 -1
  83. package/dist/resizer.cjs +1 -1
  84. package/dist/resizer.js +2 -2
  85. package/dist/stack.cjs +1 -1
  86. package/dist/stack.cjs.map +1 -1
  87. package/dist/stack.js +480 -780
  88. package/dist/stack.js.map +1 -1
  89. package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
  90. package/dist/sticky-header.cjs +1 -1
  91. package/dist/sticky-header.cjs.map +1 -1
  92. package/dist/sticky-header.js +59 -51
  93. package/dist/sticky-header.js.map +1 -1
  94. package/dist/{styles-DPPuJ0sf.js → styles-NkjuMOVS.js} +13 -13
  95. package/dist/{styles-DPPuJ0sf.js.map → styles-NkjuMOVS.js.map} +1 -1
  96. package/dist/styles-qf6ptVLD.cjs.map +1 -1
  97. package/dist/types.d.ts +30 -0
  98. package/dist/useAnimationFrame-BZ6D2lMq.cjs +2 -0
  99. package/dist/useAnimationFrame-BZ6D2lMq.cjs.map +1 -0
  100. package/dist/useAnimationFrame-Bg4e-H8O.js +394 -0
  101. package/dist/useAnimationFrame-Bg4e-H8O.js.map +1 -0
  102. package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
  103. package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
  104. package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
  105. package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
  106. package/dist/window/index.d.ts +2 -0
  107. package/dist/window.cjs +1 -1
  108. package/dist/window.cjs.map +1 -1
  109. package/dist/window.js +114 -103
  110. package/dist/window.js.map +1 -1
  111. package/package.json +6 -1
  112. package/src/components/gesture/SwipeSafeZone.tsx +70 -0
  113. package/src/components/grid/GridLayout.tsx +110 -38
  114. package/src/components/window/Drawer.tsx +353 -162
  115. package/src/components/window/DrawerLayers.tsx +54 -11
  116. package/src/components/window/DrawerRevealContext.spec.ts +20 -0
  117. package/src/components/window/DrawerRevealContext.tsx +99 -0
  118. package/src/components/window/drawerRevealAnimationUtils.spec.ts +375 -0
  119. package/src/components/window/drawerRevealAnimationUtils.ts +415 -0
  120. package/src/components/window/drawerStyles.spec.ts +302 -0
  121. package/src/components/window/drawerStyles.ts +252 -0
  122. package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
  123. package/src/components/window/drawerSwipeConfig.ts +112 -0
  124. package/src/components/window/useDrawerSwipeTransform.ts +67 -0
  125. package/src/components/window/useDrawerTransform.ts +505 -0
  126. package/src/components/window/useRevealDrawerTransform.spec.ts +1936 -0
  127. package/src/components/window/useRevealDrawerTransform.ts +105 -0
  128. package/src/constants/styles.ts +19 -0
  129. package/src/demo/components/FullscreenDemoPage.tsx +47 -0
  130. package/src/demo/fullscreenRoutes.tsx +32 -0
  131. package/src/demo/index.tsx +5 -0
  132. package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
  133. package/src/demo/pages/Dialog/card/index.tsx +22 -0
  134. package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
  135. package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
  136. package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +219 -0
  137. package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
  138. package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
  139. package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
  140. package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
  141. package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
  142. package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
  143. package/src/demo/pages/Dialog/modal/index.tsx +17 -0
  144. package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
  145. package/src/demo/pages/Drawer/components/DrawerBasics.module.css +6 -1
  146. package/src/demo/pages/Drawer/components/DrawerBasics.tsx +14 -4
  147. package/src/demo/pages/Drawer/components/DrawerReveal.module.css +157 -0
  148. package/src/demo/pages/Drawer/components/DrawerReveal.tsx +128 -0
  149. package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
  150. package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
  151. package/src/demo/pages/Drawer/reveal/index.tsx +17 -0
  152. package/src/demo/pages/Drawer/reveal-fullscreen/index.tsx +135 -0
  153. package/src/demo/pages/Drawer/reveal-fullscreen/styles.module.css +233 -0
  154. package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
  155. package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +54 -23
  156. package/src/demo/pages/Pivot/swipe-debug/index.tsx +1 -1
  157. package/src/demo/pages/Stack/components/StackBasics.spec.tsx +156 -0
  158. package/src/demo/pages/Stack/components/StackBasics.tsx +179 -95
  159. package/src/demo/pages/Stack/components/StackTablet.spec.tsx +110 -0
  160. package/src/demo/pages/Stack/components/StackTablet.tsx +42 -21
  161. package/src/demo/routes.tsx +24 -1
  162. package/src/dialog/index.ts +85 -0
  163. package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +68 -64
  164. package/src/hooks/gesture/testing/createGestureSimulator.ts +113 -37
  165. package/src/hooks/gesture/types.ts +83 -6
  166. package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +22 -14
  167. package/src/hooks/gesture/useNativeGestureGuard.spec.ts +99 -31
  168. package/src/hooks/gesture/useNativeGestureGuard.ts +3 -1
  169. package/src/hooks/gesture/utils.ts +102 -0
  170. package/src/hooks/useAnimatedVisibility.spec.ts +44 -24
  171. package/src/hooks/useAnimatedVisibility.ts +28 -2
  172. package/src/hooks/useAnimationFrame.ts +8 -0
  173. package/src/hooks/useOperationContinuity.spec.ts +394 -0
  174. package/src/hooks/useOperationContinuity.ts +135 -0
  175. package/src/hooks/useResizeObserver.spec.tsx +277 -0
  176. package/src/hooks/useResizeObserver.tsx +108 -39
  177. package/src/hooks/useScrollContainer.ts +4 -10
  178. package/src/hooks/useSharedElementTransition.ts +354 -0
  179. package/src/hooks/useSwipeContentTransform.spec.ts +18 -18
  180. package/src/hooks/useSwipeContentTransform.ts +166 -28
  181. package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
  182. package/src/modules/dialog/AlertDialog.tsx +221 -0
  183. package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
  184. package/src/modules/dialog/DialogContainer.tsx +188 -0
  185. package/src/modules/dialog/Modal.spec.tsx +220 -0
  186. package/src/modules/dialog/Modal.tsx +182 -0
  187. package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
  188. package/src/modules/dialog/dialogAnimationUtils.spec.ts +252 -0
  189. package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
  190. package/src/modules/dialog/types.ts +186 -0
  191. package/src/modules/dialog/useDialog.spec.tsx +447 -0
  192. package/src/modules/dialog/useDialog.ts +214 -0
  193. package/src/modules/dialog/useDialogContainer.spec.ts +339 -0
  194. package/src/modules/dialog/useDialogContainer.ts +150 -0
  195. package/src/modules/dialog/useDialogSwipeInput.spec.ts +178 -0
  196. package/src/modules/dialog/useDialogSwipeInput.ts +350 -0
  197. package/src/modules/dialog/useDialogTransform.spec.ts +403 -0
  198. package/src/modules/dialog/useDialogTransform.ts +407 -0
  199. package/src/modules/drawer/drawerStateMachine.ts +500 -0
  200. package/src/modules/drawer/revealDrawerConstants.ts +38 -0
  201. package/src/modules/drawer/revealDrawerStateMachine.spec.ts +558 -0
  202. package/src/modules/drawer/revealDrawerStateMachine.ts +197 -0
  203. package/src/modules/drawer/strategies/index.ts +9 -0
  204. package/src/modules/drawer/strategies/overlayStrategy.ts +133 -0
  205. package/src/modules/drawer/strategies/revealStrategy.ts +111 -0
  206. package/src/modules/drawer/strategies/types.ts +160 -0
  207. package/src/modules/drawer/types.ts +102 -0
  208. package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
  209. package/src/modules/drawer/useDrawerSwipeInput.ts +402 -0
  210. package/src/modules/panels/rendering/ContentRegistry.spec.tsx +21 -14
  211. package/src/modules/pivot/SwipePivotContent.position.spec.tsx +12 -8
  212. package/src/modules/pivot/SwipePivotContent.spec.tsx +66 -25
  213. package/src/modules/pivot/SwipePivotContent.tsx +2 -2
  214. package/src/modules/pivot/SwipePivotTabBar.spec.tsx +85 -68
  215. package/src/modules/pivot/SwipePivotTabBar.tsx +75 -15
  216. package/src/modules/pivot/scaleInputState.spec.ts +11 -2
  217. package/src/modules/pivot/usePivot.spec.ts +17 -3
  218. package/src/modules/pivot/usePivotSwipeInput.spec.ts +182 -123
  219. package/src/modules/stack/SwipeStackContent.spec.tsx +387 -100
  220. package/src/modules/stack/SwipeStackContent.tsx +43 -33
  221. package/src/modules/stack/SwipeStackOutlet.spec.tsx +14 -16
  222. package/src/modules/stack/SwipeStackOutlet.tsx +6 -6
  223. package/src/modules/stack/computeSwipeStackTransform.spec.ts +5 -5
  224. package/src/modules/stack/computeSwipeStackTransform.ts +3 -3
  225. package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
  226. package/src/modules/stack/useStackAnimationState.spec.ts +3 -1
  227. package/src/modules/stack/useStackAnimationState.ts +18 -13
  228. package/src/modules/stack/useStackNavigation.spec.ts +198 -3
  229. package/src/modules/stack/useStackNavigation.tsx +113 -56
  230. package/src/modules/stack/useStackSwipeInput.spec.ts +65 -32
  231. package/src/modules/stack/useStackSwipeInput.ts +1 -1
  232. package/src/sticky-header/StickyArea.tsx +29 -57
  233. package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
  234. package/src/sticky-header/calculateStickyMetrics.ts +50 -0
  235. package/src/types.ts +33 -0
  236. package/src/window/index.ts +2 -0
  237. package/dist/FloatingWindow-BpdOpg_L.js +0 -400
  238. package/dist/FloatingWindow-BpdOpg_L.js.map +0 -1
  239. package/dist/FloatingWindow-TCDNY5gE.cjs +0 -2
  240. package/dist/FloatingWindow-TCDNY5gE.cjs.map +0 -1
  241. package/dist/GridLayout-B4VRsC0r.cjs +0 -2
  242. package/dist/GridLayout-BltqeCPK.js +0 -927
  243. package/dist/PanelSystem-Bs8bQwQF.cjs +0 -3
  244. package/dist/PanelSystem-Bs8bQwQF.cjs.map +0 -1
  245. package/dist/PanelSystem-Dr1TBhxM.js.map +0 -1
  246. package/dist/ResizeHandle-CScipO5l.cjs +0 -2
  247. package/dist/SwipePivotTabBar-BGO9X94m.js +0 -407
  248. package/dist/SwipePivotTabBar-BGO9X94m.js.map +0 -1
  249. package/dist/SwipePivotTabBar-BrQismcZ.cjs +0 -2
  250. package/dist/SwipePivotTabBar-BrQismcZ.cjs.map +0 -1
  251. package/dist/useDocumentPointerEvents-CKdhGXd0.js +0 -46
  252. package/dist/useDocumentPointerEvents-CKdhGXd0.js.map +0 -1
  253. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs +0 -2
  254. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs.map +0 -1
  255. package/dist/useEffectEvent-Dp7HLCf0.js +0 -13
  256. package/dist/useEffectEvent-Dp7HLCf0.js.map +0 -1
  257. package/dist/useEffectEvent-huSsGUnl.cjs +0 -2
  258. package/dist/useEffectEvent-huSsGUnl.cjs.map +0 -1
@@ -1,13 +1,18 @@
1
1
  /**
2
2
  * @file Stack Navigation demo - iOS-style hierarchical navigation
3
+ * Uses SwipeStackContent for direct DOM manipulation during swipe gestures.
3
4
  */
4
5
  import * as React from "react";
5
6
  import { useStackNavigation } from "../../../../modules/stack/useStackNavigation.js";
6
7
  import { useStackSwipeInput } from "../../../../modules/stack/useStackSwipeInput.js";
7
- import { StackContent } from "../../../../modules/stack/StackContent.js";
8
+ import { SwipeStackContent } from "../../../../modules/stack/SwipeStackContent.js";
9
+ import { useResizeObserver } from "../../../../hooks/useResizeObserver.js";
10
+ import { toContinuousOperationState } from "../../../../hooks/gesture/types.js";
8
11
  import type { StackPanel } from "../../../../modules/stack/types.js";
9
12
  import styles from "./Stack.module.css";
10
13
 
14
+ const ANIMATION_DURATION = 300;
15
+
11
16
  const panels: StackPanel[] = [
12
17
  {
13
18
  id: "list",
@@ -61,16 +66,78 @@ export const StackBasics: React.FC = () => {
61
66
  const navigation = useStackNavigation({
62
67
  panels,
63
68
  displayMode: "overlay",
64
- transitionMode: "css",
69
+ transitionMode: "none", // Using direct DOM manipulation
65
70
  });
66
71
 
67
- const { isEdgeSwiping, progress, containerProps } = useStackSwipeInput({
72
+ const { isEdgeSwiping, progress, inputState, containerProps } = useStackSwipeInput({
68
73
  containerRef,
69
74
  navigation,
70
75
  edge: "left",
71
76
  edgeWidth: 30,
72
77
  });
73
78
 
79
+ // Track container size for SwipeStackContent
80
+ const { rect } = useResizeObserver(containerRef, { box: "border-box" });
81
+ const containerSize = rect?.width ?? 0;
82
+
83
+ const { stack, depth } = navigation.state;
84
+
85
+ // Track exiting panel when navigating back.
86
+ // CRITICAL: exitingPanelId must be computed synchronously during render
87
+ // to prevent the exiting panel from being unmounted for a frame.
88
+ const prevDepthRef = React.useRef(depth);
89
+ const prevStackRef = React.useRef<ReadonlyArray<string>>(stack);
90
+ const exitingPanelClearTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
91
+ const [exitingPanelState, setExitingPanelState] = React.useState<string | null>(null);
92
+
93
+ // Compute exiting panel ID synchronously during render
94
+ const prevDepth = prevDepthRef.current;
95
+ const prevStack = prevStackRef.current;
96
+ const isNavigatingBack = depth < prevDepth;
97
+ const computedExitingId = isNavigatingBack ? (prevStack[prevDepth] ?? null) : null;
98
+ const exitingPanelId = computedExitingId ?? exitingPanelState;
99
+
100
+ // Update refs and state in effect
101
+ React.useLayoutEffect(() => {
102
+ prevDepthRef.current = depth;
103
+ prevStackRef.current = stack;
104
+
105
+ if (computedExitingId != null) {
106
+ setExitingPanelState(computedExitingId);
107
+
108
+ if (exitingPanelClearTimeoutRef.current != null) {
109
+ clearTimeout(exitingPanelClearTimeoutRef.current);
110
+ }
111
+
112
+ exitingPanelClearTimeoutRef.current = setTimeout(() => {
113
+ setExitingPanelState(null);
114
+ exitingPanelClearTimeoutRef.current = null;
115
+ }, ANIMATION_DURATION);
116
+ }
117
+
118
+ return () => {
119
+ if (exitingPanelClearTimeoutRef.current != null) {
120
+ clearTimeout(exitingPanelClearTimeoutRef.current);
121
+ }
122
+ };
123
+ }, [depth, stack, computedExitingId]);
124
+
125
+ // Get visible panel IDs: active + behind (for swipe reveal) + exiting (for back animation)
126
+ const visiblePanelIds = React.useMemo(() => {
127
+ const ids: string[] = [];
128
+ // Behind panel (if exists)
129
+ if (depth > 0) {
130
+ ids.push(stack[depth - 1]);
131
+ }
132
+ // Active panel
133
+ ids.push(stack[depth]);
134
+ // Include exiting panel if not already in the list
135
+ if (exitingPanelId != null && !ids.includes(exitingPanelId)) {
136
+ ids.push(exitingPanelId);
137
+ }
138
+ return ids;
139
+ }, [stack, depth, exitingPanelId]);
140
+
74
141
  const handleItemClick = (item: typeof listItems[0]) => {
75
142
  setSelectedItem(item);
76
143
  navigation.push("detail");
@@ -98,11 +165,93 @@ export const StackBasics: React.FC = () => {
98
165
  const showBackButton = navigation.state.depth > 0;
99
166
  const showEditButton = navigation.state.depth === 1;
100
167
 
168
+ const renderBackButton = (): React.ReactNode => {
169
+ if (!showBackButton) {
170
+ return null;
171
+ }
172
+ return <BackButton onClick={backButtonProps.onClick} disabled={backButtonProps.disabled} />;
173
+ };
174
+
175
+ const renderPanelContent = (panelId: string): React.ReactNode => {
176
+ if (panelId === "list") {
177
+ return (
178
+ <div className={styles.panel}>
179
+ <ul className={styles.list}>
180
+ {listItems.map((item) => (
181
+ <li key={item.id} className={styles.listItem}>
182
+ <button
183
+ className={styles.listItemButton}
184
+ onClick={() => handleItemClick(item)}
185
+ >
186
+ <span className={styles.listItemName}>{item.name}</span>
187
+ <span className={styles.listItemDesc}>{item.description}</span>
188
+ <span className={styles.chevron}>→</span>
189
+ </button>
190
+ </li>
191
+ ))}
192
+ </ul>
193
+ </div>
194
+ );
195
+ }
196
+
197
+ if (panelId === "detail") {
198
+ return (
199
+ <div className={styles.panel}>
200
+ <div className={styles.detailContent}>
201
+ <h2>{selectedItem?.name}</h2>
202
+ <p>{selectedItem?.description}</p>
203
+ <div className={styles.detailMeta}>
204
+ <span>ID: {selectedItem?.id}</span>
205
+ </div>
206
+ <p className={styles.hint}>
207
+ Swipe from the left edge to go back, or tap the Back button.
208
+ </p>
209
+ </div>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ if (panelId === "edit") {
215
+ return (
216
+ <div className={styles.panel}>
217
+ <div className={styles.editContent}>
218
+ <h2>Edit {selectedItem?.name}</h2>
219
+ <div className={styles.form}>
220
+ <label className={styles.label}>
221
+ Name
222
+ <input
223
+ type="text"
224
+ className={styles.input}
225
+ defaultValue={selectedItem?.name}
226
+ />
227
+ </label>
228
+ <label className={styles.label}>
229
+ Description
230
+ <textarea
231
+ className={styles.textarea}
232
+ defaultValue={selectedItem?.description}
233
+ />
234
+ </label>
235
+ </div>
236
+ <button
237
+ className={styles.saveButton}
238
+ onClick={() => navigation.go(-1)}
239
+ >
240
+ Save Changes
241
+ </button>
242
+ </div>
243
+ </div>
244
+ );
245
+ }
246
+
247
+ return null;
248
+ };
249
+
101
250
  return (
102
251
  <div className={styles.container}>
103
252
  {/* Header */}
104
253
  <header className={styles.header}>
105
- {showBackButton && <BackButton onClick={backButtonProps.onClick} disabled={backButtonProps.disabled} />}
254
+ {renderBackButton()}
106
255
  <h1 className={styles.title}>{getHeaderTitle()}</h1>
107
256
  {showEditButton ? <EditButton onClick={handleEdit} /> : null}
108
257
  </header>
@@ -113,97 +262,32 @@ export const StackBasics: React.FC = () => {
113
262
  className={styles.stackContainer}
114
263
  {...containerProps}
115
264
  >
116
- {/* List Panel */}
117
- <StackContent
118
- id="list"
119
- depth={0}
120
- isActive={navigation.currentPanelId === "list"}
121
- displayMode="overlay"
122
- transitionMode="css"
123
- navigationState={navigation.state}
124
- swipeProgress={navigation.currentPanelId === "list" ? progress : undefined}
125
- >
126
- <div className={styles.panel}>
127
- <ul className={styles.list}>
128
- {listItems.map((item) => (
129
- <li key={item.id} className={styles.listItem}>
130
- <button
131
- className={styles.listItemButton}
132
- onClick={() => handleItemClick(item)}
133
- >
134
- <span className={styles.listItemName}>{item.name}</span>
135
- <span className={styles.listItemDesc}>{item.description}</span>
136
- <span className={styles.chevron}>→</span>
137
- </button>
138
- </li>
139
- ))}
140
- </ul>
141
- </div>
142
- </StackContent>
143
-
144
- {/* Detail Panel */}
145
- <StackContent
146
- id="detail"
147
- depth={1}
148
- isActive={navigation.currentPanelId === "detail"}
149
- displayMode="overlay"
150
- transitionMode="css"
151
- navigationState={navigation.state}
152
- swipeProgress={navigation.currentPanelId === "detail" ? progress : undefined}
153
- >
154
- <div className={styles.panel}>
155
- <div className={styles.detailContent}>
156
- <h2>{selectedItem?.name}</h2>
157
- <p>{selectedItem?.description}</p>
158
- <div className={styles.detailMeta}>
159
- <span>ID: {selectedItem?.id}</span>
160
- </div>
161
- <p className={styles.hint}>
162
- Swipe from the left edge to go back, or tap the Back button.
163
- </p>
164
- </div>
165
- </div>
166
- </StackContent>
167
-
168
- {/* Edit Panel */}
169
- <StackContent
170
- id="edit"
171
- depth={2}
172
- isActive={navigation.currentPanelId === "edit"}
173
- displayMode="overlay"
174
- transitionMode="css"
175
- navigationState={navigation.state}
176
- swipeProgress={navigation.currentPanelId === "edit" ? progress : undefined}
177
- >
178
- <div className={styles.panel}>
179
- <div className={styles.editContent}>
180
- <h2>Edit {selectedItem?.name}</h2>
181
- <div className={styles.form}>
182
- <label className={styles.label}>
183
- Name
184
- <input
185
- type="text"
186
- className={styles.input}
187
- defaultValue={selectedItem?.name}
188
- />
189
- </label>
190
- <label className={styles.label}>
191
- Description
192
- <textarea
193
- className={styles.textarea}
194
- defaultValue={selectedItem?.description}
195
- />
196
- </label>
197
- </div>
198
- <button
199
- className={styles.saveButton}
200
- onClick={() => navigation.go(-1)}
201
- >
202
- Save Changes
203
- </button>
204
- </div>
205
- </div>
206
- </StackContent>
265
+ {visiblePanelIds.map((panelId) => {
266
+ // Panel is only "exiting" if it matches exitingPanelId AND is not in current stack
267
+ // This prevents the bug where a re-pushed panel is incorrectly treated as exiting
268
+ const isInCurrentStack = stack.includes(panelId);
269
+ const isExiting = panelId === exitingPanelId && !isInCurrentStack;
270
+ // For exiting panels, use depth + 1 since they were previously at the active position
271
+ const panelDepth = isExiting ? depth + 1 : stack.indexOf(panelId);
272
+ const isActive = panelDepth === depth && !isExiting;
273
+
274
+ return (
275
+ <SwipeStackContent
276
+ key={panelId}
277
+ id={panelId}
278
+ depth={panelDepth}
279
+ navigationDepth={depth}
280
+ isActive={isActive}
281
+ operationState={toContinuousOperationState(inputState)}
282
+ containerSize={containerSize}
283
+ animateOnMount={true}
284
+ animationDuration={ANIMATION_DURATION}
285
+ displayMode="overlay"
286
+ >
287
+ {renderPanelContent(panelId)}
288
+ </SwipeStackContent>
289
+ );
290
+ })}
207
291
  </div>
208
292
 
209
293
  {/* Debug info */}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @file Tests for StackTablet component logic.
3
+ *
4
+ * These tests focus on the panel visibility and depth calculation logic,
5
+ * particularly around the exitingPanelId handling during rapid navigation.
6
+ */
7
+
8
+ // Empty export to make this file a module (prevents global scope pollution)
9
+ export {};
10
+
11
+ /**
12
+ * Pure function version of the isExiting logic for testing.
13
+ * This matches the logic in StackTablet.tsx.
14
+ */
15
+ function computeIsExiting(
16
+ panelId: string,
17
+ exitingPanelId: string | null,
18
+ stack: readonly string[],
19
+ ): boolean {
20
+ // FIX: Check if panel is in current stack - if so, it's not exiting
21
+ const isInCurrentStack = stack.includes(panelId);
22
+ return panelId === exitingPanelId && !isInCurrentStack;
23
+ }
24
+
25
+ /**
26
+ * Pure function version of panelDepth calculation.
27
+ */
28
+ function computePanelDepth(
29
+ panelId: string,
30
+ isExiting: boolean,
31
+ stack: readonly string[],
32
+ depth: number,
33
+ ): number {
34
+ return isExiting ? depth + 1 : stack.indexOf(panelId);
35
+ }
36
+
37
+ describe("StackTablet panel calculation logic", () => {
38
+ describe("isExiting calculation", () => {
39
+ it("returns false when panel is in current stack even if it matches exitingPanelId", () => {
40
+ // Bug scenario: user pushed panel again before exitingPanelId timeout cleared
41
+ const result = computeIsExiting("general", "general", ["root", "general"]);
42
+ expect(result).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe("panelDepth calculation", () => {
47
+ it("returns correct depth for re-pushed panel (not exiting)", () => {
48
+ // Bug scenario: general was exiting but got re-pushed
49
+ const stack = ["root", "general"];
50
+ const isExiting = computeIsExiting("general", "general", stack);
51
+ const depth = computePanelDepth("general", isExiting, stack, 1);
52
+ expect(depth).toBe(1); // stack.indexOf("general") = 1, NOT depth + 1 = 2
53
+ });
54
+ });
55
+
56
+ describe("rapid navigation scenarios", () => {
57
+ it("handles push → back → push sequence correctly", () => {
58
+ // Helper function to compute stack state for the final step
59
+ const computeFinalState = () => {
60
+ // Steps:
61
+ // 1. Initial: root active (stack: ["root"], depth: 0, exitingPanelId: null)
62
+ // 2. Push general (stack: ["root", "general"], depth: 1)
63
+ // 3. Back to root (stack: ["root"], depth: 0, exitingPanelId: "general")
64
+ // 4. Push general again BEFORE exitingPanelId clears
65
+ return {
66
+ stack: ["root", "general"] as readonly string[],
67
+ depth: 1,
68
+ exitingPanelId: "general",
69
+ };
70
+ };
71
+
72
+ const finalState = computeFinalState();
73
+
74
+ // Now general should NOT be exiting because it's in the stack
75
+ const isExiting = computeIsExiting("general", finalState.exitingPanelId, finalState.stack);
76
+ expect(isExiting).toBe(false);
77
+
78
+ // Panel depth should be correct
79
+ const panelDepth = computePanelDepth("general", isExiting, finalState.stack, finalState.depth);
80
+ expect(panelDepth).toBe(1); // Not 2!
81
+ });
82
+
83
+ it("handles Settings menu rapid navigation", () => {
84
+ // Helper function to compute final state after rapid navigation
85
+ const computeSettingsNavState = () => {
86
+ // Steps:
87
+ // 1. Initial: root (stack: ["root"], depth: 0)
88
+ // 2. Click "General" (stack: ["root", "general"], depth: 1)
89
+ // 3. Click "About" (stack: ["root", "general", "about"], depth: 2)
90
+ // 4. Back to General (stack: ["root", "general"], depth: 1, exitingPanelId: "about")
91
+ // 5. Immediately back to root (stack: ["root"], depth: 0, exitingPanelId: "general")
92
+ // 6. Immediately push General again
93
+ return {
94
+ stack: ["root", "general"] as readonly string[],
95
+ depth: 1,
96
+ exitingPanelId: "general",
97
+ };
98
+ };
99
+
100
+ const finalState = computeSettingsNavState();
101
+
102
+ // general should NOT be exiting
103
+ const isExiting = computeIsExiting("general", finalState.exitingPanelId, finalState.stack);
104
+ expect(isExiting).toBe(false);
105
+
106
+ const panelDepth = computePanelDepth("general", isExiting, finalState.stack, finalState.depth);
107
+ expect(panelDepth).toBe(1);
108
+ });
109
+ });
110
+ });
@@ -8,6 +8,7 @@ import { useStackNavigation } from "../../../../modules/stack/useStackNavigation
8
8
  import { useStackSwipeInput } from "../../../../modules/stack/useStackSwipeInput.js";
9
9
  import { SwipeStackContent } from "../../../../modules/stack/SwipeStackContent.js";
10
10
  import { useResizeObserver } from "../../../../hooks/useResizeObserver.js";
11
+ import { toContinuousOperationState } from "../../../../hooks/gesture/types.js";
11
12
  import type { StackPanel } from "../../../../modules/stack/types.js";
12
13
  import styles from "./StackTablet.module.css";
13
14
  import "../../../styles/stack-themes.css";
@@ -271,36 +272,53 @@ export const StackTablet: React.FC<StackTabletProps> = ({ theme = "ios" }) => {
271
272
  // Get visible panels: active + behind (for swipe reveal) + exiting (for back animation)
272
273
  const { stack, depth } = navigation.state;
273
274
 
274
- // Track exiting panel when navigating back
275
- const [exitingPanelId, setExitingPanelId] = React.useState<string | null>(null);
275
+ // Track exiting panel when navigating back.
276
+ // CRITICAL: exitingPanelId must be computed synchronously during render
277
+ // to prevent the exiting panel from being unmounted for a frame.
278
+ // If the panel unmounts, it loses its position state and causes a visual jump.
276
279
  const prevDepthRef = React.useRef(depth);
277
280
  const prevStackRef = React.useRef<ReadonlyArray<string>>(stack);
281
+ const exitingPanelClearTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
282
+ const [exitingPanelState, setExitingPanelState] = React.useState<string | null>(null);
278
283
 
279
- // Detect when we navigate back and need to animate out
280
- React.useLayoutEffect(() => {
281
- const prevDepth = prevDepthRef.current;
282
- const prevStack = prevStackRef.current;
284
+ // Compute exiting panel ID synchronously during render
285
+ // This ensures the panel stays in the render tree even on the navigation change render
286
+ const prevDepth = prevDepthRef.current;
287
+ const prevStack = prevStackRef.current;
288
+ const isNavigatingBack = depth < prevDepth;
289
+ const computedExitingId = isNavigatingBack ? (prevStack[prevDepth] ?? null) : null;
290
+
291
+ // Use computed value if navigating back, otherwise use state (for timeout-based cleanup)
292
+ const exitingPanelId = computedExitingId ?? exitingPanelState;
283
293
 
294
+ // Update refs and state in effect
295
+ React.useLayoutEffect(() => {
284
296
  // Update refs
285
297
  prevDepthRef.current = depth;
286
298
  prevStackRef.current = stack;
287
299
 
288
- // Check if we went back (depth decreased)
289
- if (depth < prevDepth) {
290
- // The panel at prevDepth is exiting
291
- const exitingId = prevStack[prevDepth];
292
- if (exitingId != null) {
293
- setExitingPanelId(exitingId);
294
-
295
- // Clear exiting panel after animation completes
296
- const timeoutId = setTimeout(() => {
297
- setExitingPanelId(null);
298
- }, THEME_DURATIONS[selectedTheme]);
300
+ // If we just started navigating back, update state and schedule cleanup
301
+ if (computedExitingId != null) {
302
+ setExitingPanelState(computedExitingId);
299
303
 
300
- return () => clearTimeout(timeoutId);
304
+ // Clear any existing timeout
305
+ if (exitingPanelClearTimeoutRef.current != null) {
306
+ clearTimeout(exitingPanelClearTimeoutRef.current);
301
307
  }
308
+
309
+ // Clear exiting panel after animation completes
310
+ exitingPanelClearTimeoutRef.current = setTimeout(() => {
311
+ setExitingPanelState(null);
312
+ exitingPanelClearTimeoutRef.current = null;
313
+ }, THEME_DURATIONS[selectedTheme]);
302
314
  }
303
- }, [depth, stack, selectedTheme]);
315
+
316
+ return () => {
317
+ if (exitingPanelClearTimeoutRef.current != null) {
318
+ clearTimeout(exitingPanelClearTimeoutRef.current);
319
+ }
320
+ };
321
+ }, [depth, stack, selectedTheme, computedExitingId]);
304
322
 
305
323
  const visiblePanelIds = React.useMemo(() => {
306
324
  const ids = [stack[depth]]; // Active panel
@@ -327,7 +345,10 @@ export const StackTablet: React.FC<StackTabletProps> = ({ theme = "ios" }) => {
327
345
  return null;
328
346
  }
329
347
 
330
- const isExiting = panelId === exitingPanelId;
348
+ // Panel is only "exiting" if it matches exitingPanelId AND is not in current stack
349
+ // This prevents the bug where a re-pushed panel is incorrectly treated as exiting
350
+ const isInCurrentStack = stack.includes(panelId);
351
+ const isExiting = panelId === exitingPanelId && !isInCurrentStack;
331
352
  // For exiting panels, use depth + 1 since they were previously at the active position
332
353
  const panelDepth = isExiting ? depth + 1 : stack.indexOf(panelId);
333
354
  const isActive = panelDepth === depth && !isExiting;
@@ -339,7 +360,7 @@ export const StackTablet: React.FC<StackTabletProps> = ({ theme = "ios" }) => {
339
360
  depth={panelDepth}
340
361
  navigationDepth={depth}
341
362
  isActive={isActive}
342
- inputState={inputState}
363
+ operationState={toContinuousOperationState(inputState)}
343
364
  containerSize={containerSize}
344
365
  animateOnMount={true}
345
366
  animationDuration={THEME_DURATIONS[selectedTheme]}
@@ -27,6 +27,14 @@ import PL_Overlays from "./pages/PanelLayout/draggable-overlays";
27
27
  import DR_Basics from "./pages/Drawer/basics";
28
28
  import DR_Menu from "./pages/Drawer/menu";
29
29
  import DR_Animations from "./pages/Drawer/animations";
30
+ import DR_Swipe from "./pages/Drawer/swipe";
31
+ import DR_Reveal from "./pages/Drawer/reveal";
32
+
33
+ import DL_Modal from "./pages/Dialog/modal";
34
+ import DL_Alerts from "./pages/Dialog/alerts";
35
+ import DL_CustomAlert from "./pages/Dialog/custom-alert";
36
+ import DL_Swipe from "./pages/Dialog/swipe";
37
+ import DL_Card from "./pages/Dialog/card";
30
38
 
31
39
  import PS_Preview from "./pages/PanelSystem/preview";
32
40
  import PS_Tabbar from "./pages/PanelSystem/tabbar";
@@ -46,7 +54,7 @@ import ST_Tablet from "./pages/Stack/tablet";
46
54
 
47
55
  import SH_Basics from "./pages/StickyHeader/basics";
48
56
 
49
- import { FiGrid, FiLayers, FiColumns, FiMaximize2, FiBox, FiCpu, FiSmartphone, FiSidebar, FiNavigation, FiImage } from "react-icons/fi";
57
+ import { FiGrid, FiLayers, FiColumns, FiMaximize2, FiBox, FiCpu, FiSmartphone, FiSidebar, FiNavigation, FiImage, FiMessageSquare } from "react-icons/fi";
50
58
 
51
59
  export type DemoPage = {
52
60
  id: string;
@@ -85,6 +93,21 @@ export const demoCategories: DemoCategory[] = [
85
93
  { id: "basics", label: "Basics", path: "basics", element: <DR_Basics /> },
86
94
  { id: "menu", label: "Menu", path: "menu", element: <DR_Menu /> },
87
95
  { id: "animations", label: "Animations", path: "animations", element: <DR_Animations /> },
96
+ { id: "swipe", label: "Swipe Gestures", path: "swipe", element: <DR_Swipe /> },
97
+ { id: "reveal", label: "Reveal Mode", path: "reveal", element: <DR_Reveal /> },
98
+ ],
99
+ },
100
+ {
101
+ id: "dialog",
102
+ label: "Dialog",
103
+ icon: <FiMessageSquare />,
104
+ base: "/components/dialog",
105
+ pages: [
106
+ { id: "modal", label: "Modal", path: "modal", element: <DL_Modal /> },
107
+ { id: "alerts", label: "Alert / Confirm / Prompt", path: "alerts", element: <DL_Alerts /> },
108
+ { id: "custom-alert", label: "Custom Alert Component", path: "custom-alert", element: <DL_CustomAlert /> },
109
+ { id: "swipe", label: "Swipe Dismiss", path: "swipe", element: <DL_Swipe /> },
110
+ { id: "card", label: "Card Expansion", path: "card", element: <DL_Card /> },
88
111
  ],
89
112
  },
90
113
  {
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file Dialog entry point - Modal and alert/confirm/prompt dialogs
3
+ * @packageDocumentation
4
+ *
5
+ * This is a subpath export entry point for `react-panel-layout/dialog`.
6
+ *
7
+ * ## Overview
8
+ * Dialog provides modal dialogs and imperative alert/confirm/prompt APIs
9
+ * using native HTML dialog element for proper accessibility and top-layer rendering.
10
+ *
11
+ * ## Installation
12
+ * ```ts
13
+ * import { Modal, useDialog, DialogContainer } from "react-panel-layout/dialog";
14
+ * ```
15
+ *
16
+ * ## Modal Usage
17
+ * ```tsx
18
+ * const [isOpen, setIsOpen] = useState(false);
19
+ *
20
+ * <Modal
21
+ * visible={isOpen}
22
+ * onClose={() => setIsOpen(false)}
23
+ * header={{ title: "Settings" }}
24
+ * >
25
+ * <form>
26
+ * <input type="text" placeholder="Name" />
27
+ * <button type="submit">Save</button>
28
+ * </form>
29
+ * </Modal>
30
+ * ```
31
+ *
32
+ * ## useDialog Hook Usage
33
+ * ```tsx
34
+ * function MyComponent() {
35
+ * const { alert, confirm, prompt, Outlet } = useDialog();
36
+ *
37
+ * const handleClick = async () => {
38
+ * await alert("Hello!");
39
+ *
40
+ * const confirmed = await confirm({
41
+ * message: "Are you sure?",
42
+ * confirmLabel: "Yes",
43
+ * cancelLabel: "No",
44
+ * });
45
+ *
46
+ * if (confirmed) {
47
+ * const name = await prompt({
48
+ * message: "Enter your name:",
49
+ * defaultValue: "Anonymous",
50
+ * });
51
+ * console.log("Name:", name);
52
+ * }
53
+ * };
54
+ *
55
+ * return (
56
+ * <>
57
+ * <button onClick={handleClick}>Show dialogs</button>
58
+ * <Outlet />
59
+ * </>
60
+ * );
61
+ * }
62
+ * ```
63
+ */
64
+
65
+ // Components
66
+ export { Modal } from "../modules/dialog/Modal.js";
67
+ export { DialogContainer } from "../modules/dialog/DialogContainer.js";
68
+ export { AlertDialog } from "../modules/dialog/AlertDialog.js";
69
+
70
+ // Hooks
71
+ export { useDialog } from "../modules/dialog/useDialog.js";
72
+ export { useDialogContainer } from "../modules/dialog/useDialogContainer.js";
73
+
74
+ // Types
75
+ export type {
76
+ ModalProps,
77
+ ModalHeader,
78
+ DialogContainerProps,
79
+ DialogTransitionMode,
80
+ AlertOptions,
81
+ ConfirmOptions,
82
+ PromptOptions,
83
+ AlertDialogProps,
84
+ UseDialogReturn,
85
+ } from "../modules/dialog/types.js";