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
@@ -0,0 +1,402 @@
1
+ /**
2
+ * @file Hook for detecting swipe gestures to open/close a drawer.
3
+ *
4
+ * Combines:
5
+ * - Edge swipe detection for opening (Stack pattern)
6
+ * - Drag-to-close within drawer (Dialog pattern)
7
+ * - Native gesture guard for browser back prevention
8
+ */
9
+ import * as React from "react";
10
+ import { useEdgeSwipeInput } from "../../hooks/gesture/useEdgeSwipeInput.js";
11
+ import { useNativeGestureGuard } from "../../hooks/gesture/useNativeGestureGuard.js";
12
+ import { usePointerTracking } from "../../hooks/gesture/usePointerTracking.js";
13
+ import {
14
+ mergeGestureContainerProps,
15
+ isScrollableInDirection,
16
+ } from "../../hooks/gesture/utils.js";
17
+ import { isInSwipeSafeZone } from "../../components/gesture/SwipeSafeZone.js";
18
+ import {
19
+ type ContinuousOperationState,
20
+ IDLE_CONTINUOUS_OPERATION_STATE,
21
+ } from "../../hooks/gesture/types.js";
22
+ import type { UseDrawerSwipeInputOptions, UseDrawerSwipeInputResult } from "./types.js";
23
+ import { getDrawerAnimationAxis, getDrawerCloseSwipeSign, getDrawerOpenSwipeSign } from "./types.js";
24
+
25
+ /**
26
+ * Default dismiss threshold (30% of container size).
27
+ */
28
+ const DEFAULT_DISMISS_THRESHOLD = 0.3;
29
+
30
+ /**
31
+ * Velocity threshold for quick flick dismissal (px/ms).
32
+ */
33
+ const VELOCITY_THRESHOLD = 0.5;
34
+
35
+ // ============================================================================
36
+ // Helper functions (extracted to avoid ternary violations)
37
+ // ============================================================================
38
+
39
+ function getContainerSize(container: HTMLElement, axis: "x" | "y"): number {
40
+ if (axis === "x") {
41
+ return container.clientWidth;
42
+ }
43
+ return container.clientHeight;
44
+ }
45
+
46
+ function getAxisDelta(
47
+ start: { x: number; y: number },
48
+ current: { x: number; y: number },
49
+ axis: "x" | "y",
50
+ ): number {
51
+ if (axis === "x") {
52
+ return current.x - start.x;
53
+ }
54
+ return current.y - start.y;
55
+ }
56
+
57
+ const PHASE_MAP: Record<string, "idle" | "operating" | "ended"> = {
58
+ idle: "idle",
59
+ ended: "ended",
60
+ };
61
+
62
+ function normalizePhase(phase: string): "idle" | "operating" | "ended" {
63
+ return PHASE_MAP[phase] ?? "operating";
64
+ }
65
+
66
+ function computeDisplacementValue(
67
+ closeSwipeSign: 1 | -1,
68
+ axis: "x" | "y",
69
+ closeDisplacement: number,
70
+ ): { x: number; y: number } {
71
+ const signedDisplacement = closeSwipeSign * closeDisplacement;
72
+ if (axis === "x") {
73
+ return { x: signedDisplacement, y: 0 };
74
+ }
75
+ return { x: 0, y: signedDisplacement };
76
+ }
77
+
78
+ function computeAxisDisplacement(
79
+ displacement: { x: number; y: number },
80
+ axis: "x" | "y",
81
+ sign: 1 | -1,
82
+ ): number {
83
+ // Multiply by sign so displacement is positive when moving in open direction
84
+ // Use Math.max(0, ...) to prevent reverse movement from counting as forward
85
+ if (axis === "x") {
86
+ return Math.max(0, sign * displacement.x);
87
+ }
88
+ return Math.max(0, sign * displacement.y);
89
+ }
90
+
91
+ function isEdgeSwipeEnabled(enableEdgeSwipeOpen: boolean, isOpen: boolean): boolean {
92
+ if (!enableEdgeSwipeOpen) {
93
+ return false;
94
+ }
95
+ return !isOpen;
96
+ }
97
+
98
+ function isCloseSwipeEnabled(enableSwipeClose: boolean, isOpen: boolean): boolean {
99
+ if (!enableSwipeClose) {
100
+ return false;
101
+ }
102
+ return isOpen;
103
+ }
104
+
105
+ function isDrawerOpening(isEdgeGesture: boolean, isOpen: boolean): boolean {
106
+ if (!isEdgeGesture) {
107
+ return false;
108
+ }
109
+ return !isOpen;
110
+ }
111
+
112
+ function isDrawerClosing(closePhase: "idle" | "operating" | "ended", isOpen: boolean): boolean {
113
+ if (closePhase === "idle") {
114
+ return false;
115
+ }
116
+ return isOpen;
117
+ }
118
+
119
+ function computeVelocity(
120
+ start: { x: number; y: number; timestamp: number } | null,
121
+ current: { x: number; y: number; timestamp: number } | null,
122
+ displacement: number,
123
+ ): number {
124
+ if (!start || !current) {
125
+ return 0;
126
+ }
127
+ const timeDelta = Math.max(1, current.timestamp - start.timestamp);
128
+ return displacement / timeDelta;
129
+ }
130
+
131
+ /**
132
+ * Hook for detecting swipe gestures to open/close a drawer.
133
+ *
134
+ * When drawer is closed:
135
+ * - Detects edge swipe from the anchor edge to trigger open
136
+ *
137
+ * When drawer is open:
138
+ * - Detects drag gesture within drawer to trigger close
139
+ * - Respects scrollable content boundaries
140
+ *
141
+ * @example
142
+ * ```tsx
143
+ * const { state, edgeContainerProps, drawerContentProps } = useDrawerSwipeInput({
144
+ * edgeContainerRef: gridLayoutRef,
145
+ * drawerContentRef: drawerRef,
146
+ * direction: "left",
147
+ * isOpen,
148
+ * onSwipeOpen: () => setOpen(true),
149
+ * onSwipeClose: () => setOpen(false),
150
+ * });
151
+ * ```
152
+ */
153
+ export function useDrawerSwipeInput(
154
+ options: UseDrawerSwipeInputOptions,
155
+ ): UseDrawerSwipeInputResult {
156
+ const {
157
+ edgeContainerRef,
158
+ drawerContentRef,
159
+ direction,
160
+ isOpen,
161
+ onSwipeOpen,
162
+ onSwipeClose,
163
+ enableEdgeSwipeOpen = true,
164
+ enableSwipeClose = true,
165
+ edgeWidth = 20,
166
+ dismissThreshold = DEFAULT_DISMISS_THRESHOLD,
167
+ } = options;
168
+
169
+ const axis = getDrawerAnimationAxis(direction);
170
+ const closeSwipeSign = getDrawerCloseSwipeSign(direction);
171
+ const openSwipeSign = getDrawerOpenSwipeSign(direction);
172
+
173
+ // Track container size for progress calculation
174
+ const containerSizeRef = React.useRef(0);
175
+
176
+ // Measure drawer content size
177
+ React.useLayoutEffect(() => {
178
+ const container = drawerContentRef.current;
179
+ if (!container) {
180
+ return;
181
+ }
182
+
183
+ const updateSize = () => {
184
+ containerSizeRef.current = getContainerSize(container, axis);
185
+ };
186
+
187
+ updateSize();
188
+
189
+ const observer = new ResizeObserver(updateSize);
190
+ observer.observe(container);
191
+
192
+ return () => observer.disconnect();
193
+ }, [drawerContentRef, axis]);
194
+
195
+ // =========== Edge swipe to OPEN ===========
196
+ const handleOpenSwipeEnd = React.useCallback(
197
+ (state: { direction: 1 | -1 | 0 }) => {
198
+ // Open when swiping in the correct direction (away from edge)
199
+ if (state.direction === openSwipeSign) {
200
+ onSwipeOpen();
201
+ }
202
+ },
203
+ [openSwipeSign, onSwipeOpen],
204
+ );
205
+
206
+ const {
207
+ isEdgeGesture,
208
+ state: edgeSwipeState,
209
+ containerProps: edgeSwipeProps,
210
+ } = useEdgeSwipeInput({
211
+ containerRef: edgeContainerRef,
212
+ edge: direction,
213
+ edgeWidth,
214
+ enabled: isEdgeSwipeEnabled(enableEdgeSwipeOpen, isOpen),
215
+ onSwipeEnd: handleOpenSwipeEnd,
216
+ });
217
+
218
+ // Native gesture guard for edge swipe
219
+ const { containerProps: guardProps } = useNativeGestureGuard({
220
+ containerRef: edgeContainerRef,
221
+ active: isEdgeGesture,
222
+ preventEdgeBack: true,
223
+ preventOverscroll: true,
224
+ edgeWidth,
225
+ });
226
+
227
+ // =========== Drag to CLOSE (Dialog pattern) ===========
228
+ const { state: closeTracking, onPointerDown: baseClosePointerDown } = usePointerTracking({
229
+ enabled: isCloseSwipeEnabled(enableSwipeClose, isOpen),
230
+ });
231
+
232
+ const [closePhase, setClosePhase] = React.useState<"idle" | "operating" | "ended">("idle");
233
+ const lastCloseDisplacementRef = React.useRef(0);
234
+
235
+ // Wrap pointer down with scroll check and safe zone check
236
+ const onClosePointerDown = React.useCallback(
237
+ (event: React.PointerEvent) => {
238
+ if (!enableSwipeClose || !isOpen) {
239
+ return;
240
+ }
241
+
242
+ const container = drawerContentRef.current;
243
+ if (!container) {
244
+ return;
245
+ }
246
+
247
+ const target = event.target as HTMLElement;
248
+
249
+ // Check if target is in a SwipeSafeZone
250
+ if (isInSwipeSafeZone(target, container)) {
251
+ return; // Don't start close swipe if inside safe zone
252
+ }
253
+
254
+ // Check if target is in a scrollable area that would block swipe
255
+ if (isScrollableInDirection(target, container, axis, closeSwipeSign)) {
256
+ return; // Don't start close swipe if inside scrollable content
257
+ }
258
+
259
+ baseClosePointerDown(event);
260
+ },
261
+ [enableSwipeClose, isOpen, drawerContentRef, axis, closeSwipeSign, baseClosePointerDown],
262
+ );
263
+
264
+ // Calculate close displacement
265
+ const closeDisplacement = React.useMemo(() => {
266
+ if (!closeTracking.isDown || !closeTracking.start || !closeTracking.current) {
267
+ return lastCloseDisplacementRef.current;
268
+ }
269
+
270
+ const delta = getAxisDelta(closeTracking.start, closeTracking.current, axis);
271
+
272
+ // Only count movement in close direction
273
+ const signedDelta = delta * closeSwipeSign;
274
+ return Math.max(0, signedDelta);
275
+ }, [closeTracking.isDown, closeTracking.start, closeTracking.current, axis, closeSwipeSign]);
276
+
277
+ // Track displacement while dragging
278
+ React.useEffect(() => {
279
+ if (closeTracking.isDown && closeTracking.current) {
280
+ lastCloseDisplacementRef.current = closeDisplacement;
281
+ }
282
+ }, [closeTracking.isDown, closeTracking.current, closeDisplacement]);
283
+
284
+ // Handle close drag start
285
+ React.useEffect(() => {
286
+ if (closeTracking.isDown && closePhase === "idle") {
287
+ setClosePhase("operating");
288
+ }
289
+ }, [closeTracking.isDown, closePhase]);
290
+
291
+ // Handle close drag end
292
+ React.useEffect(() => {
293
+ if (!closeTracking.isDown && closePhase === "operating") {
294
+ const displacement = lastCloseDisplacementRef.current;
295
+ const hasMovement = displacement > 1;
296
+
297
+ if (hasMovement) {
298
+ setClosePhase("ended");
299
+
300
+ // Check if should close
301
+ const containerSize = containerSizeRef.current;
302
+ if (containerSize > 0) {
303
+ const ratio = displacement / containerSize;
304
+ const velocity = computeVelocity(closeTracking.start, closeTracking.current, displacement);
305
+
306
+ if (ratio >= dismissThreshold || velocity >= VELOCITY_THRESHOLD) {
307
+ onSwipeClose();
308
+ }
309
+ }
310
+ } else {
311
+ setClosePhase("idle");
312
+ lastCloseDisplacementRef.current = 0;
313
+ }
314
+ }
315
+ }, [closeTracking.isDown, closePhase, dismissThreshold, onSwipeClose, closeTracking.start, closeTracking.current]);
316
+
317
+ // Transition from ended to idle
318
+ React.useEffect(() => {
319
+ if (closePhase === "ended") {
320
+ queueMicrotask(() => {
321
+ setClosePhase("idle");
322
+ lastCloseDisplacementRef.current = 0;
323
+ });
324
+ }
325
+ }, [closePhase]);
326
+
327
+ // Reset close state when drawer closes
328
+ React.useEffect(() => {
329
+ if (!isOpen) {
330
+ setClosePhase("idle");
331
+ lastCloseDisplacementRef.current = 0;
332
+ }
333
+ }, [isOpen]);
334
+
335
+ // =========== Combined state ===========
336
+ const isOpening = isDrawerOpening(isEdgeGesture, isOpen);
337
+ const isClosing = isDrawerClosing(closePhase, isOpen);
338
+
339
+ // Determine primary displacement based on current operation
340
+ const displacement = React.useMemo(() => {
341
+ if (isOpening) {
342
+ return computeAxisDisplacement(edgeSwipeState.displacement, axis, openSwipeSign);
343
+ }
344
+ if (isClosing) {
345
+ return closeDisplacement;
346
+ }
347
+ return 0;
348
+ }, [isOpening, isClosing, axis, edgeSwipeState.displacement, closeDisplacement, openSwipeSign]);
349
+
350
+ // Progress calculation
351
+ const progress = React.useMemo(() => {
352
+ const containerSize = containerSizeRef.current;
353
+ if (containerSize <= 0) {
354
+ return 0;
355
+ }
356
+ return Math.min(displacement / containerSize, 1);
357
+ }, [displacement]);
358
+
359
+ // Combined operation state
360
+ const state = React.useMemo<ContinuousOperationState>(() => {
361
+ if (isOpening) {
362
+ return {
363
+ phase: normalizePhase(edgeSwipeState.phase),
364
+ displacement: edgeSwipeState.displacement,
365
+ velocity: edgeSwipeState.velocity,
366
+ };
367
+ }
368
+ if (isClosing) {
369
+ return {
370
+ phase: closePhase,
371
+ displacement: computeDisplacementValue(closeSwipeSign, axis, closeDisplacement),
372
+ velocity: { x: 0, y: 0 },
373
+ };
374
+ }
375
+ return IDLE_CONTINUOUS_OPERATION_STATE;
376
+ }, [isOpening, isClosing, edgeSwipeState, closePhase, closeDisplacement, axis, closeSwipeSign]);
377
+
378
+ // Container props
379
+ const edgeContainerProps = React.useMemo(
380
+ () => mergeGestureContainerProps(edgeSwipeProps, guardProps),
381
+ [edgeSwipeProps, guardProps],
382
+ );
383
+
384
+ const drawerContentProps = React.useMemo(() => ({
385
+ onPointerDown: onClosePointerDown,
386
+ style: {
387
+ touchAction: "none" as const,
388
+ userSelect: "none" as const,
389
+ WebkitUserSelect: "none" as const,
390
+ },
391
+ }), [onClosePointerDown]);
392
+
393
+ return {
394
+ state,
395
+ isOpening,
396
+ isClosing,
397
+ progress,
398
+ displacement,
399
+ edgeContainerProps,
400
+ drawerContentProps,
401
+ };
402
+ }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * @file ContentRegistry tests - state persistence across tab switch, panel move, and split
3
3
  */
4
- /* eslint-disable no-restricted-imports, no-restricted-properties, no-restricted-syntax -- integration test */
5
4
  import { render, screen, fireEvent } from "@testing-library/react";
6
5
  import * as React from "react";
7
6
  import { ContentRegistryProvider, useContentRegistry } from "./ContentRegistry";
@@ -49,6 +48,21 @@ const TestHarness: React.FC<{ state: TestState }> = ({ state }) => {
49
48
  };
50
49
 
51
50
  describe("ContentRegistry", () => {
51
+ const defaultRect = {
52
+ top: 0,
53
+ left: 0,
54
+ width: 100,
55
+ height: 100,
56
+ right: 100,
57
+ bottom: 100,
58
+ x: 0,
59
+ y: 0,
60
+ toJSON: () => ({}),
61
+ } as DOMRect;
62
+ const originalPointerCapture = Element.prototype.setPointerCapture;
63
+ const originalReleasePointerCapture = Element.prototype.releasePointerCapture;
64
+ const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
65
+
52
66
  const createPanel = (id: string): TabDefinition => ({
53
67
  id,
54
68
  title: `Panel ${id}`,
@@ -59,25 +73,18 @@ describe("ContentRegistry", () => {
59
73
  // Mock ResizeObserver (polyfill provided in vitest.setup.ts)
60
74
 
61
75
  // Mock pointer capture methods
62
- Element.prototype.setPointerCapture = vi.fn();
63
- Element.prototype.releasePointerCapture = vi.fn();
76
+ Element.prototype.setPointerCapture = () => {};
77
+ Element.prototype.releasePointerCapture = () => {};
64
78
  // Mock getBoundingClientRect
65
- Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({
66
- top: 0,
67
- left: 0,
68
- width: 100,
69
- height: 100,
70
- right: 100,
71
- bottom: 100,
72
- x: 0,
73
- y: 0,
74
- toJSON: () => ({}),
75
- });
79
+ Element.prototype.getBoundingClientRect = () => defaultRect;
76
80
  });
77
81
 
78
82
  afterEach(() => {
79
83
  // Clean up any portal containers
80
84
  document.querySelectorAll("[data-panel-content-root]").forEach((el) => el.remove());
85
+ Element.prototype.setPointerCapture = originalPointerCapture;
86
+ Element.prototype.releasePointerCapture = originalReleasePointerCapture;
87
+ Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
81
88
  });
82
89
 
83
90
  it("should render content inside the registered container element", () => {
@@ -31,8 +31,12 @@ const shouldRenderItem = (offset: number): boolean => {
31
31
  * Helper to convert offset to display position
32
32
  */
33
33
  const toDisplayPosition = (offset: number): -1 | 0 | 1 => {
34
- if (offset < 0) return -1;
35
- if (offset > 0) return 1;
34
+ if (offset < 0) {
35
+ return -1;
36
+ }
37
+ if (offset > 0) {
38
+ return 1;
39
+ }
36
40
  return 0;
37
41
  };
38
42
 
@@ -76,9 +80,9 @@ describe("SwipePivotContent position handling", () => {
76
80
  const activeIndex = 0;
77
81
 
78
82
  // Filter to only adjacent items BEFORE rendering
79
- const itemsToRender = items.filter(item =>
80
- shouldRenderItem(getPositionOffset(item.index, activeIndex))
81
- );
83
+ const itemsToRender = items.filter((item) => {
84
+ return shouldRenderItem(getPositionOffset(item.index, activeIndex));
85
+ });
82
86
 
83
87
  render(
84
88
  <>
@@ -114,9 +118,9 @@ describe("SwipePivotContent position handling", () => {
114
118
  ];
115
119
  const activeIndex = 1; // On Page 2
116
120
 
117
- const itemsToRender = items.filter(item =>
118
- shouldRenderItem(getPositionOffset(item.index, activeIndex))
119
- );
121
+ const itemsToRender = items.filter((item) => {
122
+ return shouldRenderItem(getPositionOffset(item.index, activeIndex));
123
+ });
120
124
 
121
125
  render(
122
126
  <>
@@ -6,41 +6,82 @@ import * as React from "react";
6
6
  import { SwipePivotContent } from "./SwipePivotContent.js";
7
7
  import type { SwipeInputState } from "../../hooks/gesture/types.js";
8
8
 
9
- // Mock Web Animations API for JSDOM
10
- class MockAnimation {
11
- finished: Promise<Animation>;
12
- private resolveFinished!: () => void;
13
- playState: AnimationPlayState = "running";
14
-
15
- constructor() {
16
- this.finished = new Promise((resolve) => {
17
- this.resolveFinished = () => resolve(this as unknown as Animation);
18
- });
19
- }
9
+ /**
10
+ * Mock Animation that implements the full Animation interface.
11
+ * Used to polyfill Web Animations API for JSDOM testing.
12
+ */
13
+ const createMockAnimation = (): Animation => {
14
+ const animationState = {
15
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- required for Promise resolve callback
16
+ resolveFinished: (_anim: Animation): void => {},
17
+ playState: "running" as AnimationPlayState,
18
+ };
20
19
 
21
- cancel() {
22
- this.playState = "idle";
23
- }
20
+ const animation: Animation = {
21
+ currentTime: 0,
22
+ effect: null,
23
+ id: "",
24
+ oncancel: null,
25
+ onfinish: null,
26
+ onremove: null,
27
+ get pending() {
28
+ return false;
29
+ },
30
+ get playState() {
31
+ return animationState.playState;
32
+ },
33
+ playbackRate: 1,
34
+ get replaceState() {
35
+ return "active" as AnimationReplaceState;
36
+ },
37
+ startTime: 0,
38
+ timeline: null,
39
+ get finished() {
40
+ return new Promise<Animation>((resolve) => {
41
+ animationState.resolveFinished = resolve;
42
+ });
43
+ },
44
+ get ready() {
45
+ return Promise.resolve(animation);
46
+ },
47
+ cancel() {
48
+ animationState.playState = "idle";
49
+ },
50
+ finish() {
51
+ animationState.playState = "finished";
52
+ animationState.resolveFinished(animation);
53
+ },
54
+ commitStyles() {},
55
+ pause() {},
56
+ persist() {},
57
+ play() {},
58
+ reverse() {},
59
+ updatePlaybackRate() {},
60
+ addEventListener() {},
61
+ removeEventListener() {},
62
+ dispatchEvent() {
63
+ return true;
64
+ },
65
+ };
24
66
 
25
- finish() {
26
- this.playState = "finished";
27
- this.resolveFinished();
28
- }
29
- }
67
+ return animation;
68
+ };
30
69
 
31
70
  describe("SwipePivotContent", () => {
32
- let originalAnimate: typeof Element.prototype.animate | undefined;
71
+ const animationState = {
72
+ originalAnimate: Element.prototype.animate as typeof Element.prototype.animate | undefined,
73
+ };
33
74
 
34
75
  beforeAll(() => {
35
- originalAnimate = Element.prototype.animate;
36
- Element.prototype.animate = function () {
37
- return new MockAnimation() as unknown as Animation;
76
+ animationState.originalAnimate = Element.prototype.animate;
77
+ Element.prototype.animate = (): Animation => {
78
+ return createMockAnimation();
38
79
  };
39
80
  });
40
81
 
41
82
  afterAll(() => {
42
- if (originalAnimate) {
43
- Element.prototype.animate = originalAnimate;
83
+ if (animationState.originalAnimate) {
84
+ Element.prototype.animate = animationState.originalAnimate;
44
85
  }
45
86
  });
46
87
  const idleState: SwipeInputState = {
@@ -140,14 +140,14 @@ export const SwipePivotContent: React.FC<SwipePivotContentProps> = React.memo(({
140
140
 
141
141
  const targetPx = position * containerSize;
142
142
  const displacement = getAxisDisplacement(inputState, axis);
143
- const isSwiping = inputState.phase === "swiping" || inputState.phase === "tracking";
143
+ const isOperating = inputState.phase === "swiping" || inputState.phase === "tracking";
144
144
 
145
145
  // Use shared transform hook for DOM manipulation
146
146
  const { animationDirection } = useSwipeContentTransform({
147
147
  elementRef,
148
148
  targetPx,
149
149
  displacement,
150
- isSwiping,
150
+ isOperating,
151
151
  axis,
152
152
  animationDuration,
153
153
  containerSize,