react-panel-layout 0.6.0 → 0.6.1

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 (216) 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-CUXnEtrb.js +827 -0
  4. package/dist/FloatingWindow-CUXnEtrb.js.map +1 -0
  5. package/dist/FloatingWindow-DMwyK0eK.cjs +2 -0
  6. package/dist/FloatingWindow-DMwyK0eK.cjs.map +1 -0
  7. package/dist/GridLayout-DKTg_N61.cjs +2 -0
  8. package/dist/{GridLayout-B4VRsC0r.cjs.map → GridLayout-DKTg_N61.cjs.map} +1 -1
  9. package/dist/{GridLayout-BltqeCPK.js → GridLayout-UWNxXw77.js} +34 -35
  10. package/dist/{GridLayout-BltqeCPK.js.map → GridLayout-UWNxXw77.js.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-Dr1TBhxM.js → PanelSystem-BqUzNtf2.js} +5 -5
  16. package/dist/{PanelSystem-Dr1TBhxM.js.map → PanelSystem-BqUzNtf2.js.map} +1 -1
  17. package/dist/{PanelSystem-Bs8bQwQF.cjs → PanelSystem-D603LKKv.cjs} +2 -2
  18. package/dist/{PanelSystem-Bs8bQwQF.cjs.map → PanelSystem-D603LKKv.cjs.map} +1 -1
  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 +3 -1
  29. package/dist/components/window/DrawerLayers.d.ts +1 -1
  30. package/dist/components/window/drawerStyles.d.ts +69 -0
  31. package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
  32. package/dist/components/window/useDrawerSwipeTransform.d.ts +23 -0
  33. package/dist/config.cjs +1 -1
  34. package/dist/config.js +3 -3
  35. package/dist/constants/styles.d.ts +17 -0
  36. package/dist/dialog/index.d.ts +69 -0
  37. package/dist/floating.js +1 -1
  38. package/dist/grid.cjs +1 -1
  39. package/dist/grid.js +2 -2
  40. package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +7 -0
  41. package/dist/hooks/gesture/types.d.ts +48 -5
  42. package/dist/hooks/gesture/utils.d.ts +19 -0
  43. package/dist/hooks/useAnimationFrame.d.ts +2 -0
  44. package/dist/hooks/useOperationContinuity.d.ts +64 -0
  45. package/dist/hooks/useResizeObserver.d.ts +33 -1
  46. package/dist/hooks/useSharedElementTransition.d.ts +112 -0
  47. package/dist/hooks/useSwipeContentTransform.d.ts +9 -2
  48. package/dist/index.cjs +1 -1
  49. package/dist/index.js +7 -7
  50. package/dist/modules/dialog/AlertDialog.d.ts +9 -0
  51. package/dist/modules/dialog/DialogContainer.d.ts +37 -0
  52. package/dist/modules/dialog/Modal.d.ts +26 -0
  53. package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
  54. package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
  55. package/dist/modules/dialog/types.d.ts +183 -0
  56. package/dist/modules/dialog/useDialog.d.ts +39 -0
  57. package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
  58. package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
  59. package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
  60. package/dist/modules/drawer/types.d.ts +74 -0
  61. package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
  62. package/dist/modules/pivot/SwipePivotTabBar.d.ts +3 -0
  63. package/dist/modules/stack/SwipeStackContent.d.ts +6 -3
  64. package/dist/modules/stack/SwipeStackOutlet.d.ts +4 -4
  65. package/dist/modules/stack/computeSwipeStackTransform.d.ts +1 -1
  66. package/dist/panels.cjs +1 -1
  67. package/dist/panels.js +1 -1
  68. package/dist/pivot.cjs +1 -1
  69. package/dist/pivot.js +1 -1
  70. package/dist/resizer.cjs +1 -1
  71. package/dist/resizer.js +2 -2
  72. package/dist/stack.cjs +1 -1
  73. package/dist/stack.cjs.map +1 -1
  74. package/dist/stack.js +503 -762
  75. package/dist/stack.js.map +1 -1
  76. package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
  77. package/dist/sticky-header.cjs +1 -1
  78. package/dist/sticky-header.cjs.map +1 -1
  79. package/dist/sticky-header.js +59 -51
  80. package/dist/sticky-header.js.map +1 -1
  81. package/dist/{styles-DPPuJ0sf.js → styles-NkjuMOVS.js} +13 -13
  82. package/dist/{styles-DPPuJ0sf.js.map → styles-NkjuMOVS.js.map} +1 -1
  83. package/dist/styles-qf6ptVLD.cjs.map +1 -1
  84. package/dist/types.d.ts +16 -0
  85. package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
  86. package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
  87. package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
  88. package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
  89. package/dist/useNativeGestureGuard-C7TSqEkr.cjs +2 -0
  90. package/dist/useNativeGestureGuard-C7TSqEkr.cjs.map +1 -0
  91. package/dist/useNativeGestureGuard-CGYo6O0r.js +347 -0
  92. package/dist/useNativeGestureGuard-CGYo6O0r.js.map +1 -0
  93. package/dist/window/index.d.ts +2 -0
  94. package/dist/window.cjs +1 -1
  95. package/dist/window.cjs.map +1 -1
  96. package/dist/window.js +114 -103
  97. package/dist/window.js.map +1 -1
  98. package/package.json +6 -1
  99. package/src/components/gesture/SwipeSafeZone.tsx +69 -0
  100. package/src/components/window/Drawer.tsx +249 -162
  101. package/src/components/window/DrawerLayers.tsx +13 -3
  102. package/src/components/window/drawerStyles.spec.ts +263 -0
  103. package/src/components/window/drawerStyles.ts +228 -0
  104. package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
  105. package/src/components/window/drawerSwipeConfig.ts +112 -0
  106. package/src/components/window/useDrawerSwipeTransform.spec.ts +234 -0
  107. package/src/components/window/useDrawerSwipeTransform.ts +129 -0
  108. package/src/constants/styles.ts +19 -0
  109. package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
  110. package/src/demo/pages/Dialog/card/index.tsx +22 -0
  111. package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
  112. package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
  113. package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +204 -0
  114. package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
  115. package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
  116. package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
  117. package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
  118. package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
  119. package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
  120. package/src/demo/pages/Dialog/modal/index.tsx +17 -0
  121. package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
  122. package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
  123. package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
  124. package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
  125. package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +54 -23
  126. package/src/demo/pages/Pivot/swipe-debug/index.tsx +1 -1
  127. package/src/demo/pages/Stack/components/StackBasics.spec.tsx +152 -0
  128. package/src/demo/pages/Stack/components/StackBasics.tsx +179 -95
  129. package/src/demo/pages/Stack/components/StackTablet.spec.tsx +120 -0
  130. package/src/demo/pages/Stack/components/StackTablet.tsx +42 -21
  131. package/src/demo/routes.tsx +22 -1
  132. package/src/dialog/index.ts +85 -0
  133. package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +68 -64
  134. package/src/hooks/gesture/testing/createGestureSimulator.ts +112 -37
  135. package/src/hooks/gesture/types.ts +83 -6
  136. package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +22 -14
  137. package/src/hooks/gesture/useNativeGestureGuard.spec.ts +91 -31
  138. package/src/hooks/gesture/useNativeGestureGuard.ts +3 -1
  139. package/src/hooks/gesture/utils.ts +91 -0
  140. package/src/hooks/useAnimatedVisibility.spec.ts +44 -24
  141. package/src/hooks/useAnimatedVisibility.ts +28 -2
  142. package/src/hooks/useAnimationFrame.ts +8 -0
  143. package/src/hooks/useOperationContinuity.spec.ts +387 -0
  144. package/src/hooks/useOperationContinuity.ts +135 -0
  145. package/src/hooks/useResizeObserver.spec.tsx +277 -0
  146. package/src/hooks/useResizeObserver.tsx +108 -39
  147. package/src/hooks/useScrollContainer.ts +4 -10
  148. package/src/hooks/useSharedElementTransition.ts +333 -0
  149. package/src/hooks/useSwipeContentTransform.spec.ts +18 -18
  150. package/src/hooks/useSwipeContentTransform.ts +166 -28
  151. package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
  152. package/src/modules/dialog/AlertDialog.tsx +221 -0
  153. package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
  154. package/src/modules/dialog/DialogContainer.tsx +188 -0
  155. package/src/modules/dialog/Modal.spec.tsx +220 -0
  156. package/src/modules/dialog/Modal.tsx +182 -0
  157. package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
  158. package/src/modules/dialog/dialogAnimationUtils.spec.ts +253 -0
  159. package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
  160. package/src/modules/dialog/types.ts +186 -0
  161. package/src/modules/dialog/useDialog.spec.tsx +447 -0
  162. package/src/modules/dialog/useDialog.ts +214 -0
  163. package/src/modules/dialog/useDialogContainer.spec.ts +331 -0
  164. package/src/modules/dialog/useDialogContainer.ts +150 -0
  165. package/src/modules/dialog/useDialogSwipeInput.spec.ts +157 -0
  166. package/src/modules/dialog/useDialogSwipeInput.ts +319 -0
  167. package/src/modules/dialog/useDialogTransform.spec.ts +370 -0
  168. package/src/modules/dialog/useDialogTransform.ts +407 -0
  169. package/src/modules/drawer/types.ts +102 -0
  170. package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
  171. package/src/modules/drawer/useDrawerSwipeInput.ts +399 -0
  172. package/src/modules/panels/rendering/ContentRegistry.spec.tsx +21 -14
  173. package/src/modules/pivot/SwipePivotContent.position.spec.tsx +12 -8
  174. package/src/modules/pivot/SwipePivotContent.spec.tsx +55 -25
  175. package/src/modules/pivot/SwipePivotContent.tsx +2 -2
  176. package/src/modules/pivot/SwipePivotTabBar.spec.tsx +85 -68
  177. package/src/modules/pivot/SwipePivotTabBar.tsx +75 -15
  178. package/src/modules/pivot/scaleInputState.spec.ts +11 -2
  179. package/src/modules/pivot/usePivot.spec.ts +17 -3
  180. package/src/modules/pivot/usePivotSwipeInput.spec.ts +182 -123
  181. package/src/modules/stack/SwipeStackContent.spec.tsx +387 -100
  182. package/src/modules/stack/SwipeStackContent.tsx +43 -33
  183. package/src/modules/stack/SwipeStackOutlet.spec.tsx +14 -16
  184. package/src/modules/stack/SwipeStackOutlet.tsx +6 -6
  185. package/src/modules/stack/computeSwipeStackTransform.spec.ts +5 -5
  186. package/src/modules/stack/computeSwipeStackTransform.ts +3 -3
  187. package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
  188. package/src/modules/stack/useStackAnimationState.spec.ts +3 -1
  189. package/src/modules/stack/useStackAnimationState.ts +18 -13
  190. package/src/modules/stack/useStackNavigation.spec.ts +198 -3
  191. package/src/modules/stack/useStackNavigation.tsx +113 -56
  192. package/src/modules/stack/useStackSwipeInput.spec.ts +65 -32
  193. package/src/modules/stack/useStackSwipeInput.ts +1 -1
  194. package/src/sticky-header/StickyArea.tsx +29 -57
  195. package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
  196. package/src/sticky-header/calculateStickyMetrics.ts +50 -0
  197. package/src/types.ts +18 -0
  198. package/src/window/index.ts +2 -0
  199. package/dist/FloatingWindow-BpdOpg_L.js +0 -400
  200. package/dist/FloatingWindow-BpdOpg_L.js.map +0 -1
  201. package/dist/FloatingWindow-TCDNY5gE.cjs +0 -2
  202. package/dist/FloatingWindow-TCDNY5gE.cjs.map +0 -1
  203. package/dist/GridLayout-B4VRsC0r.cjs +0 -2
  204. package/dist/ResizeHandle-CScipO5l.cjs +0 -2
  205. package/dist/SwipePivotTabBar-BGO9X94m.js +0 -407
  206. package/dist/SwipePivotTabBar-BGO9X94m.js.map +0 -1
  207. package/dist/SwipePivotTabBar-BrQismcZ.cjs +0 -2
  208. package/dist/SwipePivotTabBar-BrQismcZ.cjs.map +0 -1
  209. package/dist/useDocumentPointerEvents-CKdhGXd0.js +0 -46
  210. package/dist/useDocumentPointerEvents-CKdhGXd0.js.map +0 -1
  211. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs +0 -2
  212. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs.map +0 -1
  213. package/dist/useEffectEvent-Dp7HLCf0.js +0 -13
  214. package/dist/useEffectEvent-Dp7HLCf0.js.map +0 -1
  215. package/dist/useEffectEvent-huSsGUnl.cjs +0 -2
  216. package/dist/useEffectEvent-huSsGUnl.cjs.map +0 -1
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @file Tests for dialog animation utilities
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ computeCloseTransform,
7
+ computeOpenTransform,
8
+ computeSwipeTransform,
9
+ shouldDismiss,
10
+ getAnimationAxis,
11
+ getDirectionSign,
12
+ getCloseDirectionFromSwipe,
13
+ supportsViewTransitions,
14
+ buildTransformString,
15
+ } from "./dialogAnimationUtils.js";
16
+
17
+ describe("dialogAnimationUtils", () => {
18
+ describe("getAnimationAxis", () => {
19
+ it("returns 'y' for center direction", () => {
20
+ expect(getAnimationAxis("center")).toBe("y");
21
+ });
22
+
23
+ it("returns 'y' for bottom direction", () => {
24
+ expect(getAnimationAxis("bottom")).toBe("y");
25
+ });
26
+
27
+ it("returns 'y' for top direction", () => {
28
+ expect(getAnimationAxis("top")).toBe("y");
29
+ });
30
+
31
+ it("returns 'x' for left direction", () => {
32
+ expect(getAnimationAxis("left")).toBe("x");
33
+ });
34
+
35
+ it("returns 'x' for right direction", () => {
36
+ expect(getAnimationAxis("right")).toBe("x");
37
+ });
38
+ });
39
+
40
+ describe("getDirectionSign", () => {
41
+ it("returns 1 for center (default down)", () => {
42
+ expect(getDirectionSign("center")).toBe(1);
43
+ });
44
+
45
+ it("returns 1 for bottom", () => {
46
+ expect(getDirectionSign("bottom")).toBe(1);
47
+ });
48
+
49
+ it("returns -1 for top", () => {
50
+ expect(getDirectionSign("top")).toBe(-1);
51
+ });
52
+
53
+ it("returns -1 for left", () => {
54
+ expect(getDirectionSign("left")).toBe(-1);
55
+ });
56
+
57
+ it("returns 1 for right", () => {
58
+ expect(getDirectionSign("right")).toBe(1);
59
+ });
60
+ });
61
+
62
+ describe("computeCloseTransform", () => {
63
+ const containerSize = 800;
64
+
65
+ it("returns identity transform at progress 0", () => {
66
+ const result = computeCloseTransform(0, containerSize, "bottom");
67
+ expect(result.translate).toBe(0);
68
+ expect(result.scale).toBe(1);
69
+ expect(result.backdropOpacity).toBe(1);
70
+ });
71
+
72
+ it("returns final transform at progress 1", () => {
73
+ const result = computeCloseTransform(1, containerSize, "bottom");
74
+ // 50% of container = 400px translate
75
+ expect(result.translate).toBeCloseTo(400);
76
+ // Final scale is 0.85
77
+ expect(result.scale).toBeCloseTo(0.85);
78
+ // Backdrop should be faded
79
+ expect(result.backdropOpacity).toBe(0);
80
+ });
81
+
82
+ it("phase 1 only applies translate", () => {
83
+ const result = computeCloseTransform(0.5, containerSize, "bottom");
84
+ // Still in phase 1 (before 0.7)
85
+ expect(result.scale).toBe(1);
86
+ expect(result.translate).toBeGreaterThan(0);
87
+ expect(result.translate).toBeLessThan(400);
88
+ });
89
+
90
+ it("phase 2 applies scale reduction", () => {
91
+ const result = computeCloseTransform(0.85, containerSize, "bottom");
92
+ // In phase 2 (after 0.7)
93
+ expect(result.scale).toBeLessThan(1);
94
+ expect(result.scale).toBeGreaterThan(0.85);
95
+ });
96
+
97
+ it("uses negative translate for top direction", () => {
98
+ const result = computeCloseTransform(1, containerSize, "top");
99
+ expect(result.translate).toBeLessThan(0);
100
+ });
101
+
102
+ it("uses negative translate for left direction", () => {
103
+ const result = computeCloseTransform(1, containerSize, "left");
104
+ expect(result.translate).toBeLessThan(0);
105
+ });
106
+ });
107
+
108
+ describe("computeOpenTransform", () => {
109
+ const containerSize = 800;
110
+
111
+ it("returns closed state at progress 0", () => {
112
+ const result = computeOpenTransform(0, containerSize, "bottom");
113
+ expect(result.translate).toBeCloseTo(400);
114
+ expect(result.scale).toBeCloseTo(0.85);
115
+ });
116
+
117
+ it("returns identity transform at progress 1", () => {
118
+ const result = computeOpenTransform(1, containerSize, "bottom");
119
+ expect(result.translate).toBe(0);
120
+ expect(result.scale).toBe(1);
121
+ });
122
+
123
+ it("is the reverse of computeCloseTransform", () => {
124
+ const closeAt30 = computeCloseTransform(0.3, containerSize, "bottom");
125
+ const openAt70 = computeOpenTransform(0.7, containerSize, "bottom");
126
+
127
+ expect(closeAt30.translate).toBeCloseTo(openAt70.translate, 5);
128
+ expect(closeAt30.scale).toBeCloseTo(openAt70.scale, 5);
129
+ });
130
+ });
131
+
132
+ describe("computeSwipeTransform", () => {
133
+ const containerSize = 400;
134
+
135
+ it("returns identity transform at displacement 0", () => {
136
+ const result = computeSwipeTransform(0, containerSize);
137
+ expect(result.translate).toBe(0);
138
+ expect(result.scale).toBe(1);
139
+ expect(result.backdropOpacity).toBe(1);
140
+ });
141
+
142
+ it("translates by displacement amount", () => {
143
+ const result = computeSwipeTransform(100, containerSize);
144
+ expect(result.translate).toBe(100);
145
+ });
146
+
147
+ it("applies light scale feedback during swipe", () => {
148
+ const result = computeSwipeTransform(200, containerSize);
149
+ // 50% progress, 2% max reduction = 1% reduction
150
+ expect(result.scale).toBeCloseTo(0.99);
151
+ });
152
+
153
+ it("fades backdrop with swipe progress", () => {
154
+ const result = computeSwipeTransform(200, containerSize);
155
+ // 50% progress, 80% max fade = 40% fade
156
+ expect(result.backdropOpacity).toBeCloseTo(0.6);
157
+ });
158
+
159
+ it("clamps progress at 1", () => {
160
+ const result = computeSwipeTransform(800, containerSize);
161
+ expect(result.scale).toBeCloseTo(0.98); // Max 2% reduction
162
+ expect(result.backdropOpacity).toBeCloseTo(0.2); // 80% fade
163
+ });
164
+ });
165
+
166
+ describe("shouldDismiss", () => {
167
+ const containerSize = 400;
168
+
169
+ it("returns true when displacement exceeds threshold", () => {
170
+ // 30% threshold = 120px
171
+ const result = shouldDismiss(130, 0, containerSize);
172
+ expect(result).toBe(true);
173
+ });
174
+
175
+ it("returns false when displacement is below threshold", () => {
176
+ const result = shouldDismiss(100, 0, containerSize);
177
+ expect(result).toBe(false);
178
+ });
179
+
180
+ it("returns true when velocity is high", () => {
181
+ // 0.5 px/ms threshold
182
+ const result = shouldDismiss(50, 0.6, containerSize);
183
+ expect(result).toBe(true);
184
+ });
185
+
186
+ it("returns false when velocity is low", () => {
187
+ const result = shouldDismiss(50, 0.3, containerSize);
188
+ expect(result).toBe(false);
189
+ });
190
+
191
+ it("respects custom threshold", () => {
192
+ // Custom 50% threshold = 200px
193
+ const belowThreshold = shouldDismiss(190, 0, containerSize, 0.5);
194
+ const aboveThreshold = shouldDismiss(210, 0, containerSize, 0.5);
195
+
196
+ expect(belowThreshold).toBe(false);
197
+ expect(aboveThreshold).toBe(true);
198
+ });
199
+ });
200
+
201
+ describe("getCloseDirectionFromSwipe", () => {
202
+ it("returns bottom for positive Y displacement", () => {
203
+ const result = getCloseDirectionFromSwipe(0, 100);
204
+ expect(result).toBe("bottom");
205
+ });
206
+
207
+ it("returns top for negative Y displacement", () => {
208
+ const result = getCloseDirectionFromSwipe(0, -100);
209
+ expect(result).toBe("top");
210
+ });
211
+
212
+ it("returns right for positive X displacement", () => {
213
+ const result = getCloseDirectionFromSwipe(100, 0);
214
+ expect(result).toBe("right");
215
+ });
216
+
217
+ it("returns left for negative X displacement", () => {
218
+ const result = getCloseDirectionFromSwipe(-100, 0);
219
+ expect(result).toBe("left");
220
+ });
221
+
222
+ it("prefers Y over X when equal", () => {
223
+ const result = getCloseDirectionFromSwipe(100, 100);
224
+ expect(result).toBe("bottom");
225
+ });
226
+
227
+ it("returns default when both are zero", () => {
228
+ const result = getCloseDirectionFromSwipe(0, 0, "left");
229
+ expect(result).toBe("left");
230
+ });
231
+ });
232
+
233
+ describe("supportsViewTransitions", () => {
234
+ it("returns a boolean", () => {
235
+ const result = supportsViewTransitions();
236
+ expect(typeof result).toBe("boolean");
237
+ });
238
+ });
239
+
240
+ describe("buildTransformString", () => {
241
+ it("builds translateY for y axis", () => {
242
+ const transform = { translate: 100, scale: 0.9, backdropOpacity: 0.5 };
243
+ const result = buildTransformString(transform, "y");
244
+ expect(result).toBe("translateY(100px) scale(0.9)");
245
+ });
246
+
247
+ it("builds translateX for x axis", () => {
248
+ const transform = { translate: -50, scale: 1, backdropOpacity: 1 };
249
+ const result = buildTransformString(transform, "x");
250
+ expect(result).toBe("translateX(-50px) scale(1)");
251
+ });
252
+ });
253
+ });
@@ -0,0 +1,297 @@
1
+ /**
2
+ * @file Dialog animation utilities
3
+ *
4
+ * Provides multi-phase animation calculations and viewTransition support
5
+ * for dialog open/close animations with "suck in" effect.
6
+ */
7
+
8
+ import { easings } from "../../hooks/useAnimationFrame.js";
9
+
10
+ /**
11
+ * Direction from which the dialog opens.
12
+ * The close animation uses the opposite direction.
13
+ */
14
+ export type DialogOpenDirection = "center" | "top" | "bottom" | "left" | "right";
15
+
16
+ /**
17
+ * Phase transition point for multi-phase animation.
18
+ * Phase 1: 0 to PHASE_TRANSITION (translate movement)
19
+ * Phase 2: PHASE_TRANSITION to 1 (scale + accelerated translate)
20
+ */
21
+ const PHASE_TRANSITION = 0.7;
22
+
23
+ /**
24
+ * Final scale value for "suck in" effect.
25
+ */
26
+ const FINAL_SCALE = 0.85;
27
+
28
+ /**
29
+ * Transform values for dialog animation.
30
+ */
31
+ export type DialogTransform = {
32
+ /** Translate value in pixels (x or y depending on direction) */
33
+ translate: number;
34
+ /** Scale value (1 = normal, 0.85 = final suck-in) */
35
+ scale: number;
36
+ /** Backdrop opacity (1 = fully visible, 0 = transparent) */
37
+ backdropOpacity: number;
38
+ };
39
+
40
+ /**
41
+ * Axis for the dialog animation based on direction.
42
+ */
43
+ export type DialogAnimationAxis = "x" | "y";
44
+
45
+ /**
46
+ * Get the animation axis for a given direction.
47
+ */
48
+ export function getAnimationAxis(direction: DialogOpenDirection): DialogAnimationAxis {
49
+ switch (direction) {
50
+ case "left":
51
+ case "right":
52
+ return "x";
53
+ case "top":
54
+ case "bottom":
55
+ case "center":
56
+ return "y";
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get the sign multiplier for translate based on direction.
62
+ * Positive means moving in the positive axis direction.
63
+ */
64
+ export function getDirectionSign(direction: DialogOpenDirection): number {
65
+ switch (direction) {
66
+ case "top":
67
+ case "left":
68
+ return -1;
69
+ case "bottom":
70
+ case "right":
71
+ case "center":
72
+ return 1;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Compute the close animation transform values.
78
+ *
79
+ * The close animation has two phases:
80
+ * - Phase 1 (0-70%): Translate movement with easeOutExpo (natural deceleration)
81
+ * - Phase 2 (70-100%): Scale shrink + accelerated translate with easeInExpo ("suck in" effect)
82
+ *
83
+ * @param progress - Animation progress (0 = open, 1 = fully closed)
84
+ * @param containerSize - Size of the container in the animation direction (width or height)
85
+ * @param direction - Direction the dialog is closing towards
86
+ * @returns Transform values for the current progress
87
+ */
88
+ export function computeCloseTransform(
89
+ progress: number,
90
+ containerSize: number,
91
+ direction: DialogOpenDirection = "bottom",
92
+ ): DialogTransform {
93
+ const sign = getDirectionSign(direction);
94
+ const translateTarget = containerSize * 0.5; // Move 50% of container size
95
+
96
+ if (progress < PHASE_TRANSITION) {
97
+ // Phase 1: translate only, no scale change
98
+ const phase1Progress = progress / PHASE_TRANSITION;
99
+ const easedProgress = easings.easeOutExpo(phase1Progress);
100
+
101
+ return {
102
+ translate: sign * translateTarget * easedProgress * 0.7, // 70% of translate in phase 1
103
+ scale: 1.0,
104
+ backdropOpacity: 1.0 - progress * 0.5, // Fade backdrop gradually
105
+ };
106
+ }
107
+
108
+ // Phase 2: scale + accelerated translate
109
+ const phase2Progress = (progress - PHASE_TRANSITION) / (1 - PHASE_TRANSITION);
110
+ const easedProgress = easeInExpo(phase2Progress);
111
+
112
+ return {
113
+ translate: sign * translateTarget * (0.7 + 0.3 * easedProgress),
114
+ scale: 1.0 - (1.0 - FINAL_SCALE) * easedProgress,
115
+ backdropOpacity: 0.5 - 0.5 * easedProgress, // Finish fading
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Compute the open animation transform values.
121
+ * This is the reverse of the close animation.
122
+ *
123
+ * @param progress - Animation progress (0 = closed, 1 = fully open)
124
+ * @param containerSize - Size of the container in the animation direction
125
+ * @param direction - Direction the dialog is opening from
126
+ * @returns Transform values for the current progress
127
+ */
128
+ export function computeOpenTransform(
129
+ progress: number,
130
+ containerSize: number,
131
+ direction: DialogOpenDirection = "bottom",
132
+ ): DialogTransform {
133
+ // Reverse the close animation
134
+ return computeCloseTransform(1 - progress, containerSize, direction);
135
+ }
136
+
137
+ /**
138
+ * Compute transform values during a swipe gesture.
139
+ * This provides real-time feedback as the user swipes.
140
+ *
141
+ * @param displacement - Current swipe displacement in pixels
142
+ * @param containerSize - Size of the container in the animation direction
143
+ * @returns Transform values for current swipe state
144
+ */
145
+ export function computeSwipeTransform(
146
+ displacement: number,
147
+ containerSize: number,
148
+ ): DialogTransform {
149
+ const progress = Math.min(Math.abs(displacement) / containerSize, 1);
150
+
151
+ // Light scale feedback during swipe (max 2% reduction)
152
+ const scale = 1.0 - 0.02 * progress;
153
+
154
+ // Backdrop fades with swipe progress
155
+ const backdropOpacity = 1.0 - progress * 0.8;
156
+
157
+ return {
158
+ translate: displacement,
159
+ scale,
160
+ backdropOpacity,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Check if the swipe should trigger a dismiss action.
166
+ *
167
+ * @param displacement - Final swipe displacement in pixels
168
+ * @param velocity - Final swipe velocity in px/ms
169
+ * @param containerSize - Size of the container
170
+ * @param threshold - Threshold ratio (0-1) for dismiss (default: 0.3)
171
+ * @returns Whether the dialog should be dismissed
172
+ */
173
+ export function shouldDismiss(
174
+ displacement: number,
175
+ velocity: number,
176
+ containerSize: number,
177
+ threshold: number = 0.3,
178
+ ): boolean {
179
+ const absDisplacement = Math.abs(displacement);
180
+ const absVelocity = Math.abs(velocity);
181
+
182
+ // Dismiss if displacement exceeds threshold
183
+ if (absDisplacement >= containerSize * threshold) {
184
+ return true;
185
+ }
186
+
187
+ // Dismiss if velocity is high enough (quick flick)
188
+ if (absVelocity >= 0.5) {
189
+ // 0.5 px/ms = 500 px/s
190
+ return true;
191
+ }
192
+
193
+ return false;
194
+ }
195
+
196
+ /**
197
+ * Get the close direction based on swipe displacement.
198
+ * The dialog closes in the direction of the swipe.
199
+ *
200
+ * @param displacementX - Horizontal displacement
201
+ * @param displacementY - Vertical displacement
202
+ * @param defaultDirection - Default direction if no clear winner
203
+ * @returns The determined close direction
204
+ */
205
+ export function getCloseDirectionFromSwipe(
206
+ displacementX: number,
207
+ displacementY: number,
208
+ defaultDirection: DialogOpenDirection = "bottom",
209
+ ): DialogOpenDirection {
210
+ const absX = Math.abs(displacementX);
211
+ const absY = Math.abs(displacementY);
212
+
213
+ if (absY > absX) {
214
+ return displacementY > 0 ? "bottom" : "top";
215
+ }
216
+ if (absX > absY) {
217
+ return displacementX > 0 ? "right" : "left";
218
+ }
219
+
220
+ return defaultDirection;
221
+ }
222
+
223
+ /**
224
+ * Check if View Transitions API is supported.
225
+ */
226
+ export function supportsViewTransitions(): boolean {
227
+ if (typeof document === "undefined") {
228
+ return false;
229
+ }
230
+ return "startViewTransition" in document;
231
+ }
232
+
233
+ /**
234
+ * Execute a callback with View Transitions API, with fallback.
235
+ *
236
+ * @param callback - The DOM-changing callback to wrap
237
+ * @returns Promise<boolean> - true if viewTransition was used, false if fallback
238
+ */
239
+ export async function safeViewTransition(callback: () => void): Promise<boolean> {
240
+ if (!supportsViewTransitions()) {
241
+ return false;
242
+ }
243
+
244
+ try {
245
+ const transition = (document as Document & { startViewTransition: (cb: () => void) => ViewTransition }).startViewTransition(callback);
246
+ await transition.finished;
247
+ return true;
248
+ } catch (error) {
249
+ console.warn("viewTransition failed, using JS fallback:", error);
250
+ return false;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * ViewTransition type (for TypeScript).
256
+ */
257
+ type ViewTransition = {
258
+ finished: Promise<void>;
259
+ ready: Promise<void>;
260
+ updateCallbackDone: Promise<void>;
261
+ skipTransition: () => void;
262
+ };
263
+
264
+ /**
265
+ * Ease-in exponential function for "suck in" effect.
266
+ * Accelerates towards the end.
267
+ */
268
+ function easeInExpo(t: number): number {
269
+ if (t === 0) {
270
+ return 0;
271
+ }
272
+ return Math.pow(2, 10 * t - 10);
273
+ }
274
+
275
+ /**
276
+ * Build CSS transform string from transform values.
277
+ *
278
+ * @param transform - Transform values
279
+ * @param axis - Animation axis ('x' or 'y')
280
+ * @returns CSS transform string
281
+ */
282
+ export function buildTransformString(transform: DialogTransform, axis: DialogAnimationAxis): string {
283
+ const translateFn = axis === "x" ? "translateX" : "translateY";
284
+ return `${translateFn}(${transform.translate}px) scale(${transform.scale})`;
285
+ }
286
+
287
+ /**
288
+ * Build CSS for backdrop based on transform values.
289
+ *
290
+ * @param transform - Transform values
291
+ * @returns CSS properties for backdrop
292
+ */
293
+ export function buildBackdropStyle(transform: DialogTransform): React.CSSProperties {
294
+ return {
295
+ opacity: transform.backdropOpacity,
296
+ };
297
+ }