@sigmela/router 0.3.7 → 0.4.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 (118) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +2 -1
  3. package/README.md +108 -479
  4. package/dist/Router.d.ts +190 -0
  5. package/dist/Router.d.ts.map +1 -0
  6. package/dist/components/Modal.d.ts +56 -0
  7. package/dist/components/Modal.d.ts.map +1 -0
  8. package/dist/components/OverlayContentScope.d.ts +8 -0
  9. package/dist/components/OverlayContentScope.d.ts.map +1 -0
  10. package/dist/components/RouterCloseButton.d.ts +9 -0
  11. package/dist/components/RouterCloseButton.d.ts.map +1 -0
  12. package/dist/components/Sheet.d.ts +53 -0
  13. package/dist/components/Sheet.d.ts.map +1 -0
  14. package/dist/components/SheetSurface.d.ts +27 -0
  15. package/dist/components/SheetSurface.d.ts.map +1 -0
  16. package/dist/components/index.d.ts +4 -0
  17. package/dist/components/index.d.ts.map +1 -0
  18. package/dist/components/pageScrollLock.d.ts +3 -0
  19. package/dist/components/pageScrollLock.d.ts.map +1 -0
  20. package/dist/components/sheetDragEngine.d.ts +27 -0
  21. package/dist/components/sheetDragEngine.d.ts.map +1 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +3649 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/navigationHistory.d.ts +17 -0
  27. package/dist/navigationHistory.d.ts.map +1 -0
  28. package/dist/router.css +971 -0
  29. package/dist/types.d.ts +3 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/utils/id.d.ts +2 -0
  32. package/dist/utils/id.d.ts.map +1 -0
  33. package/package.json +69 -174
  34. package/lib/module/Drawer/Drawer.js +0 -250
  35. package/lib/module/Drawer/DrawerContext.js +0 -4
  36. package/lib/module/Drawer/DrawerIcon.web.js +0 -47
  37. package/lib/module/Drawer/RenderDrawer.native.js +0 -248
  38. package/lib/module/Drawer/RenderDrawer.web.js +0 -197
  39. package/lib/module/Drawer/useDrawer.js +0 -11
  40. package/lib/module/Navigation.js +0 -80
  41. package/lib/module/NavigationStack.js +0 -254
  42. package/lib/module/Router.js +0 -2171
  43. package/lib/module/RouterContext.js +0 -33
  44. package/lib/module/ScreenStack/ScreenStack.native.js +0 -3
  45. package/lib/module/ScreenStack/ScreenStack.web.js +0 -561
  46. package/lib/module/ScreenStack/ScreenStackContext.js +0 -21
  47. package/lib/module/ScreenStack/animationHelpers.js +0 -87
  48. package/lib/module/ScreenStack/index.js +0 -3
  49. package/lib/module/ScreenStackItem/ScreenStackItem.js +0 -70
  50. package/lib/module/ScreenStackItem/ScreenStackItem.types.js +0 -3
  51. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +0 -171
  52. package/lib/module/ScreenStackItem/index.js +0 -3
  53. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +0 -70
  54. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +0 -5
  55. package/lib/module/ScreenStackSheetItem/index.js +0 -3
  56. package/lib/module/SplitView/RenderSplitView.native.js +0 -101
  57. package/lib/module/SplitView/RenderSplitView.web.js +0 -107
  58. package/lib/module/SplitView/SplitView.js +0 -97
  59. package/lib/module/SplitView/SplitViewContext.js +0 -4
  60. package/lib/module/SplitView/index.js +0 -5
  61. package/lib/module/SplitView/useSplitView.js +0 -11
  62. package/lib/module/StackRenderer.js +0 -42
  63. package/lib/module/TabBar/RenderTabBar.native.js +0 -418
  64. package/lib/module/TabBar/RenderTabBar.web.js +0 -218
  65. package/lib/module/TabBar/TabBar.js +0 -208
  66. package/lib/module/TabBar/TabBarContext.js +0 -4
  67. package/lib/module/TabBar/TabIcon.web.js +0 -47
  68. package/lib/module/TabBar/index.js +0 -6
  69. package/lib/module/TabBar/useTabBar.js +0 -11
  70. package/lib/module/TabBar/useTabBarHeight.js +0 -22
  71. package/lib/module/createController.js +0 -5
  72. package/lib/module/index.js +0 -15
  73. package/lib/module/navigationNode.js +0 -3
  74. package/lib/module/package.json +0 -1
  75. package/lib/module/styles.css +0 -1111
  76. package/lib/module/types.js +0 -13
  77. package/lib/typescript/package.json +0 -1
  78. package/lib/typescript/src/Drawer/Drawer.d.ts +0 -100
  79. package/lib/typescript/src/Drawer/DrawerContext.d.ts +0 -3
  80. package/lib/typescript/src/Drawer/DrawerIcon.web.d.ts +0 -7
  81. package/lib/typescript/src/Drawer/RenderDrawer.native.d.ts +0 -8
  82. package/lib/typescript/src/Drawer/RenderDrawer.web.d.ts +0 -8
  83. package/lib/typescript/src/Drawer/useDrawer.d.ts +0 -2
  84. package/lib/typescript/src/Navigation.d.ts +0 -17
  85. package/lib/typescript/src/NavigationStack.d.ts +0 -46
  86. package/lib/typescript/src/Router.d.ts +0 -232
  87. package/lib/typescript/src/RouterContext.d.ts +0 -18
  88. package/lib/typescript/src/ScreenStack/ScreenStack.native.d.ts +0 -2
  89. package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -9
  90. package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +0 -31
  91. package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +0 -6
  92. package/lib/typescript/src/ScreenStack/index.d.ts +0 -2
  93. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.d.ts +0 -3
  94. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +0 -14
  95. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +0 -3
  96. package/lib/typescript/src/ScreenStackItem/index.d.ts +0 -3
  97. package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.native.d.ts +0 -11
  98. package/lib/typescript/src/ScreenStackSheetItem/ScreenStackSheetItem.web.d.ts +0 -2
  99. package/lib/typescript/src/ScreenStackSheetItem/index.d.ts +0 -2
  100. package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +0 -19
  101. package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +0 -8
  102. package/lib/typescript/src/SplitView/SplitView.d.ts +0 -33
  103. package/lib/typescript/src/SplitView/SplitViewContext.d.ts +0 -3
  104. package/lib/typescript/src/SplitView/index.d.ts +0 -5
  105. package/lib/typescript/src/SplitView/useSplitView.d.ts +0 -2
  106. package/lib/typescript/src/StackRenderer.d.ts +0 -8
  107. package/lib/typescript/src/TabBar/RenderTabBar.native.d.ts +0 -8
  108. package/lib/typescript/src/TabBar/RenderTabBar.web.d.ts +0 -8
  109. package/lib/typescript/src/TabBar/TabBar.d.ts +0 -112
  110. package/lib/typescript/src/TabBar/TabBarContext.d.ts +0 -3
  111. package/lib/typescript/src/TabBar/TabIcon.web.d.ts +0 -7
  112. package/lib/typescript/src/TabBar/index.d.ts +0 -5
  113. package/lib/typescript/src/TabBar/useTabBar.d.ts +0 -2
  114. package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +0 -18
  115. package/lib/typescript/src/createController.d.ts +0 -15
  116. package/lib/typescript/src/index.d.ts +0 -21
  117. package/lib/typescript/src/navigationNode.d.ts +0 -41
  118. package/lib/typescript/src/types.d.ts +0 -140
@@ -1,33 +0,0 @@
1
- "use strict";
2
-
3
- import React, { createContext, useContext } from 'react';
4
- export const RouterContext = /*#__PURE__*/createContext(null);
5
- export const RouteLocalContext = /*#__PURE__*/createContext(null);
6
- export const useRouter = () => {
7
- const ctx = useContext(RouterContext);
8
- if (ctx == null) {
9
- throw new Error('useRouter must be used within RouterContext.Provider');
10
- }
11
- return ctx;
12
- };
13
- export const useCurrentRoute = () => {
14
- const router = useRouter();
15
- const subscribe = React.useCallback(cb => router.subscribeActiveRoute(cb), [router]);
16
- const get = React.useCallback(() => router.getActiveRoute(), [router]);
17
- return React.useSyncExternalStore(subscribe, get, get);
18
- };
19
- export function useParams() {
20
- const local = React.useContext(RouteLocalContext);
21
- return local?.params ?? {};
22
- }
23
- export function useQueryParams() {
24
- const local = React.useContext(RouteLocalContext);
25
- return local?.query ?? {};
26
- }
27
- export function useRoute() {
28
- const local = React.useContext(RouteLocalContext);
29
- if (!local) {
30
- throw new Error('useRoute must be used within RouterLocalContext.Provider');
31
- }
32
- return local;
33
- }
@@ -1,3 +0,0 @@
1
- "use strict";
2
-
3
- export { ScreenStack } from 'react-native-screens';
@@ -1,561 +0,0 @@
1
- "use strict";
2
-
3
- import { memo, useRef, useLayoutEffect, useMemo, useEffect, Children, isValidElement, Fragment, useCallback, useContext } from 'react';
4
- import { useTransitionMap } from 'react-transition-state';
5
- import { ScreenStackItemsContext, ScreenStackAnimatingContext, useScreenStackConfig } from "./ScreenStackContext.js";
6
- import { getPresentationTypeClass, computeAnimationType } from "./animationHelpers.js";
7
- import { RouterContext } from "../RouterContext.js";
8
- import { isModalLikePresentation } from "../types.js";
9
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
- const isScreenStackItemElement = child => {
11
- if (! /*#__PURE__*/isValidElement(child)) return false;
12
- const anyProps = child.props;
13
- return anyProps && typeof anyProps === 'object' && 'item' in anyProps;
14
- };
15
- const getItemKey = child => {
16
- const anyChild = child;
17
- const reactKey = anyChild.key ?? null;
18
- if (typeof reactKey === 'string' && reactKey.length > 0 && !reactKey.startsWith('.')) {
19
- return reactKey;
20
- }
21
- const item = anyChild.props?.item;
22
- if (item?.key && typeof item.key === 'string') {
23
- return item.key;
24
- }
25
- if (item?.routeId) {
26
- return String(item.routeId);
27
- }
28
- throw new Error('[ScreenStack] ScreenStackItem is missing a stable key');
29
- };
30
- const computeDirection = (prev, current) => {
31
- if (prev.length === 0 && current.length > 0) {
32
- return 'forward';
33
- }
34
- if (current.length > prev.length) {
35
- return 'forward';
36
- }
37
- if (current.length < prev.length) {
38
- return 'back';
39
- }
40
- const prevTop = prev[prev.length - 1];
41
- const currentTop = current[current.length - 1];
42
- if (prevTop === currentTop) {
43
- return 'forward';
44
- }
45
- const prevIndexOfCurrentTop = prev.indexOf(currentTop);
46
- const prevIndexOfPrevTop = prev.indexOf(prevTop);
47
- if (prevIndexOfCurrentTop !== -1 && prevIndexOfPrevTop !== -1 && prevIndexOfCurrentTop < prevIndexOfPrevTop) {
48
- return 'back';
49
- }
50
- return 'forward';
51
- };
52
- export const ScreenStack = /*#__PURE__*/memo(props => {
53
- const {
54
- children,
55
- transitionTime = 250,
56
- animated = true
57
- } = props;
58
- const router = useContext(RouterContext);
59
- const debugEnabled = router?.isDebugEnabled() ?? false;
60
- const devLog = useCallback((msg, data) => {
61
- if (typeof __DEV__ !== 'undefined' && __DEV__ && debugEnabled) {
62
- console.log(msg, data !== undefined ? JSON.stringify(data) : '');
63
- }
64
- }, [debugEnabled]);
65
- devLog('[ScreenStack] Render', {
66
- transitionTime,
67
- animated,
68
- childrenExists: !!children
69
- });
70
- const containerRef = useRef(null);
71
- const isInitialMountRef = useRef(true);
72
- const suppressEnterAfterEmptyRef = useRef(false);
73
- const suppressedEnterKeyRef = useRef(null);
74
- const isBulkRemovalRef = useRef(false);
75
- const prevKeysRef = useRef([]);
76
- const lastStackIdRef = useRef(null);
77
- const dismissInProgressRef = useRef(false);
78
- const lastDirectionRef = useRef('forward');
79
- const childMapRef = useRef(new Map());
80
- const stackChildren = useMemo(() => {
81
- const stackItems = [];
82
- Children.forEach(children, child => {
83
- if (isScreenStackItemElement(child)) {
84
- stackItems.push(child);
85
- } else if (child != null) {
86
- devLog('[ScreenStack] Non-ScreenStackItem child ignored', {
87
- child
88
- });
89
- }
90
- });
91
- devLog('[ScreenStack] Parsed children', {
92
- stackChildrenLength: stackItems.length
93
- });
94
- return stackItems;
95
- }, [children, devLog]);
96
- const currentStackId = useMemo(() => {
97
- let found;
98
- for (const child of stackChildren) {
99
- const childStackId = child.props.stackId ?? child.props.item?.stackId ?? undefined;
100
- if (childStackId) {
101
- found = childStackId;
102
- break;
103
- }
104
- }
105
- if (found) {
106
- lastStackIdRef.current = found;
107
- }
108
- return found ?? lastStackIdRef.current;
109
- }, [stackChildren]);
110
- const routeKeys = useMemo(() => {
111
- const keys = stackChildren.map(child => {
112
- const item = child.props.item;
113
- return item?.key || getItemKey(child);
114
- });
115
- devLog('[ScreenStack] routeKeys', keys);
116
- return keys;
117
- }, [devLog, stackChildren]);
118
- const childMap = useMemo(() => {
119
- const map = new Map(childMapRef.current);
120
- for (const child of stackChildren) {
121
- const item = child.props.item;
122
- const key = item?.key || getItemKey(child);
123
- map.set(key, child);
124
- }
125
- devLog('[ScreenStack] childMap updated', {
126
- size: map.size,
127
- keys: Array.from(map.keys())
128
- });
129
- return map;
130
- }, [devLog, stackChildren]);
131
- useEffect(() => {
132
- childMapRef.current = childMap;
133
- }, [childMap]);
134
- const {
135
- stateMap,
136
- toggle,
137
- setItem,
138
- deleteItem
139
- } = useTransitionMap({
140
- timeout: transitionTime,
141
- preEnter: true,
142
- mountOnEnter: true,
143
- unmountOnExit: false,
144
- enter: animated,
145
- exit: animated,
146
- allowMultiple: true,
147
- onStateChange: ({
148
- key,
149
- current
150
- }) => {
151
- devLog(`[ScreenStack] Transition state change for key ${key}:`, {
152
- status: current.status,
153
- isMounted: current.isMounted,
154
- isEnter: current.isEnter,
155
- isResolved: current.isResolved
156
- });
157
- }
158
- });
159
- devLog('[ScreenStack] Current transition states:', Array.from(stateMap.entries()).map(([key, state]) => ({
160
- key,
161
- status: state.status,
162
- isMounted: state.isMounted,
163
- isEnter: state.isEnter,
164
- isResolved: state.isResolved
165
- })));
166
- const stateMapEntries = useMemo(() => Array.from(stateMap.entries()), [stateMap]);
167
- const prevKeysForDirection = prevKeysRef.current;
168
- const direction = useMemo(() => {
169
- const computed = computeDirection(prevKeysForDirection, routeKeys);
170
- return computed;
171
- }, [routeKeys, prevKeysForDirection]);
172
- devLog('[ScreenStack] Computed direction', {
173
- prevKeys: prevKeysRef.current,
174
- routeKeys,
175
- direction
176
- });
177
- const screenStackConfig = useScreenStackConfig();
178
- const animateFirstScreenAfterEmpty = screenStackConfig.animateFirstScreenAfterEmpty ?? true;
179
- const isInitialPhase = isInitialMountRef.current;
180
- const keysToRender = useMemo(() => {
181
- const routeKeySet = new Set(routeKeys);
182
- const exitingKeys = [];
183
- for (const [key, state] of stateMapEntries) {
184
- if (!state.isMounted) continue;
185
- if (!routeKeySet.has(key)) {
186
- exitingKeys.push(key);
187
- }
188
- }
189
- const result = [...routeKeys, ...exitingKeys];
190
- devLog('[ScreenStack] Keys to render:', {
191
- result,
192
- exitingKeys
193
- });
194
- return result;
195
- }, [devLog, routeKeys, stateMapEntries]);
196
- const containerClassName = 'screen-stack';
197
-
198
- // CRITICAL: Calculate bulk removal BEFORE useMemo for itemsContextValue
199
- // so the flag is available when computing animation types
200
- // Use prevKeysForDirection (captured before direction useMemo) to detect removals
201
- const removedCount = prevKeysForDirection.filter(key => !routeKeys.includes(key)).length;
202
- const isBulkRemoval = removedCount > 1 || routeKeys.length === 0 && prevKeysForDirection.length > 1;
203
- isBulkRemovalRef.current = isBulkRemoval;
204
- useLayoutEffect(() => {
205
- devLog('[ScreenStack] === LIFECYCLE EFFECT START ===', {
206
- prevKeys: prevKeysRef.current,
207
- routeKeys,
208
- direction
209
- });
210
- const routeKeySet = new Set(routeKeys);
211
- const existingKeySet = new Set();
212
- for (const [key] of stateMapEntries) {
213
- existingKeySet.add(key);
214
- }
215
- const newKeys = routeKeys.filter(key => !existingKeySet.has(key));
216
- const removedKeys = [...existingKeySet].filter(key => !routeKeySet.has(key));
217
-
218
- // Track "became empty" state to suppress the next enter animation when configured.
219
- // This is SplitView-secondary-only (via ScreenStackConfigContext), not global behavior.
220
- if (!animateFirstScreenAfterEmpty) {
221
- if (routeKeys.length === 0) {
222
- suppressEnterAfterEmptyRef.current = true;
223
- suppressedEnterKeyRef.current = null;
224
- }
225
- }
226
- devLog('[ScreenStack] Lifecycle diff', {
227
- newKeys,
228
- removedKeys
229
- });
230
-
231
- // Bulk removal was already computed before useMemo (see above)
232
- devLog('[ScreenStack] Bulk removal detected', {
233
- isBulkRemoval: isBulkRemovalRef.current,
234
- removedCount: removedKeys.length,
235
- prevLength: prevKeysForDirection.length,
236
- currentLength: routeKeys.length
237
- });
238
-
239
- // If this is the first pushed key after the stack was empty, remember its key so we can
240
- // suppress only its enter animation (without affecting exit animations).
241
- if (!animateFirstScreenAfterEmpty && suppressEnterAfterEmptyRef.current && routeKeys.length > 0 && newKeys.length > 0) {
242
- const candidate = newKeys[newKeys.length - 1] ?? null;
243
- suppressedEnterKeyRef.current = candidate;
244
- suppressEnterAfterEmptyRef.current = false;
245
- }
246
- for (const key of newKeys) {
247
- devLog(`[ScreenStack] Adding item: ${key}`);
248
- setItem(key);
249
- devLog(`[ScreenStack] Entering item: ${key}`);
250
- toggle(key, true);
251
- }
252
- for (const key of removedKeys) {
253
- const state = stateMap.get(key);
254
- if (state && state.isEnter) {
255
- devLog(`[ScreenStack] Starting exit for item: ${key}`, {
256
- status: state.status
257
- });
258
- toggle(key, false);
259
- } else {
260
- devLog(`[ScreenStack] Skip exit for item (not entered or missing): ${key}`, {
261
- hasState: !!state,
262
- status: state?.status,
263
- isEnter: state?.isEnter
264
- });
265
- }
266
- }
267
- lastDirectionRef.current = direction;
268
- devLog('[ScreenStack] === LIFECYCLE EFFECT END ===');
269
- }, [routeKeys, direction, prevKeysForDirection.length, setItem, toggle, stateMapEntries, stateMap, animateFirstScreenAfterEmpty, devLog]);
270
- useLayoutEffect(() => {
271
- devLog('[ScreenStack] === CLEANUP EFFECT START ===');
272
- const routeKeySet = new Set(routeKeys);
273
- for (const [key, state] of stateMapEntries) {
274
- if (!state.isMounted) {
275
- devLog(`[ScreenStack] Cleanup unmounted item: ${key}`, {
276
- status: state.status,
277
- isResolved: state.isResolved
278
- });
279
- deleteItem(key);
280
- childMapRef.current.delete(key);
281
- continue;
282
- }
283
- const isInStack = routeKeySet.has(key);
284
- const canCleanup = !isInStack && state.status === 'exited' && state.isResolved === true;
285
- if (canCleanup) {
286
- devLog(`[ScreenStack] Cleanup exited item: ${key}`, {
287
- status: state.status,
288
- isResolved: state.isResolved
289
- });
290
- deleteItem(key);
291
- childMapRef.current.delete(key);
292
- }
293
- }
294
- devLog('[ScreenStack] === CLEANUP EFFECT END ===');
295
- }, [routeKeys, stateMapEntries, deleteItem, devLog]);
296
-
297
- // Update the previous keys after all layout effects so direction calculations
298
- // always compare against the last committed stack.
299
- useLayoutEffect(() => {
300
- prevKeysRef.current = routeKeys;
301
- }, [routeKeys]);
302
- useEffect(() => {
303
- if (!isInitialMountRef.current) return;
304
- const hasMountedItem = stateMapEntries.some(([, st]) => st.isMounted);
305
-
306
- // If the stack mounts empty, we still want the first pushed screen to animate.
307
- // Mark initial mount as completed immediately in that case.
308
- if (!hasMountedItem && routeKeys.length === 0) {
309
- if (animateFirstScreenAfterEmpty) {
310
- isInitialMountRef.current = false;
311
- devLog('[ScreenStack] Initial mount completed (empty stack)');
312
- }
313
- return;
314
- }
315
- if (hasMountedItem) {
316
- isInitialMountRef.current = false;
317
- devLog('[ScreenStack] Initial mount completed');
318
- }
319
- }, [stateMapEntries, routeKeys.length, animateFirstScreenAfterEmpty, devLog]);
320
-
321
- // Clear suppression key once it is no longer the top screen (so it can animate normally as
322
- // a background when new screens are pushed).
323
- useLayoutEffect(() => {
324
- if (animateFirstScreenAfterEmpty) return;
325
- const topKey = routeKeys[routeKeys.length - 1] ?? null;
326
- if (!topKey) {
327
- suppressedEnterKeyRef.current = null;
328
- return;
329
- }
330
- if (suppressedEnterKeyRef.current && suppressedEnterKeyRef.current !== topKey) {
331
- suppressedEnterKeyRef.current = null;
332
- }
333
- }, [routeKeys, animateFirstScreenAfterEmpty]);
334
- useEffect(() => {
335
- if (!debugEnabled) return;
336
- if (!containerRef.current) return;
337
- const items = containerRef.current.querySelectorAll('.screen-stack-item');
338
- if (items.length === 0) return;
339
- devLog('[ScreenStack] DOM State after render:', {
340
- containerClasses: containerRef.current.className,
341
- containerDataAnimation: containerRef.current.dataset.animation,
342
- containerDataDirection: containerRef.current.dataset.direction,
343
- itemCount: items.length
344
- });
345
- }, [debugEnabled, devLog]);
346
- const topKey = routeKeys[routeKeys.length - 1] ?? null;
347
- const routeKeySet = useMemo(() => new Set(routeKeys), [routeKeys]);
348
- const hasExitingItems = useMemo(() => {
349
- return stateMapEntries.some(([key, state]) => state.isMounted && !routeKeySet.has(key));
350
- }, [stateMapEntries, routeKeySet]);
351
- const hasExitingModalLike = useMemo(() => {
352
- return stateMapEntries.some(([key, state]) => {
353
- if (!state.isMounted || routeKeySet.has(key)) return false;
354
- const item = childMap.get(key)?.props.item;
355
- return isModalLikePresentation(item?.options?.stackPresentation);
356
- });
357
- }, [childMap, routeKeySet, stateMapEntries]);
358
-
359
- // D4: Compute active modal/sheet state for data attributes
360
- const hasActiveModal = useMemo(() => {
361
- for (const key of routeKeys) {
362
- const child = childMap.get(key);
363
- if (!child) continue;
364
- const presentation = child.props.item?.options?.stackPresentation;
365
- if (presentation && presentation !== 'push' && presentation !== 'formSheet' && presentation !== 'pageSheet' && presentation !== 'sheet' && isModalLikePresentation(presentation)) {
366
- return true;
367
- }
368
- }
369
- return false;
370
- }, [routeKeys, childMap]);
371
- const hasActiveSheet = useMemo(() => {
372
- for (const key of routeKeys) {
373
- const child = childMap.get(key);
374
- if (!child) continue;
375
- const presentation = child.props.item?.options?.stackPresentation;
376
- if (presentation === 'formSheet' || presentation === 'pageSheet' || presentation === 'sheet') {
377
- return true;
378
- }
379
- }
380
- return false;
381
- }, [routeKeys, childMap]);
382
-
383
- // D5: Scroll lock during active modal (ref-counted for nested stacks)
384
- useEffect(() => {
385
- if (!hasActiveModal) return;
386
- const g = globalThis;
387
- g.__scrollLockCount = (g.__scrollLockCount ?? 0) + 1;
388
- if (g.__scrollLockCount === 1) {
389
- g.__scrollLockPrev = document.body.style.overflow;
390
- document.body.style.overflow = 'hidden';
391
- }
392
- return () => {
393
- g.__scrollLockCount = Math.max(0, (g.__scrollLockCount ?? 1) - 1);
394
- if (g.__scrollLockCount === 0) {
395
- document.body.style.overflow = g.__scrollLockPrev ?? '';
396
- }
397
- };
398
- }, [hasActiveModal]);
399
- const animationDirection = hasExitingItems ? lastDirectionRef.current : direction;
400
- const stackDismissedByRouter = router?.isStackBeingDismissed?.(currentStackId ?? undefined) ?? false;
401
- if (stackDismissedByRouter) {
402
- dismissInProgressRef.current = true;
403
- }
404
- const isStackBeingDismissed = dismissInProgressRef.current;
405
- devLog('[ScreenStack] Dismiss state', {
406
- stackId: currentStackId,
407
- isStackBeingDismissed
408
- });
409
- useLayoutEffect(() => {
410
- if (!isStackBeingDismissed) return;
411
- if (hasExitingItems) return;
412
- dismissInProgressRef.current = false;
413
- router?.clearStackDismissed?.(currentStackId ?? undefined);
414
- }, [hasExitingItems, isStackBeingDismissed, currentStackId, router]);
415
- const itemsContextValue = useMemo(() => {
416
- const items = {};
417
- for (let index = 0; index < keysToRender.length; index++) {
418
- const key = keysToRender[index];
419
- if (!key) continue;
420
- const transitionState = stateMap.get(key);
421
- const child = childMap.get(key);
422
- if (!child) continue;
423
- const item = child.props.item;
424
- if (!item) continue;
425
- const presentation = item.options?.stackPresentation ?? 'push';
426
- const animated = item.options?.animated ?? true;
427
- const isInStack = routeKeySet.has(key);
428
- const isTop = isInStack && topKey !== null && key === topKey;
429
- let phase;
430
- if (!isInStack) {
431
- phase = 'exiting';
432
- } else if (isTop) {
433
- phase = 'active';
434
- } else {
435
- phase = 'inactive';
436
- }
437
- const rawStatus = transitionState?.status || 'preEnter';
438
- const status = isInitialPhase && (rawStatus === 'preEnter' || rawStatus === 'entering') ? 'entered' : rawStatus;
439
- const routeIndex = routeKeys.indexOf(key);
440
- const zIndex = routeIndex >= 0 ? routeIndex + 1 : keysToRender.length + index + 1;
441
- const presentationType = getPresentationTypeClass(presentation);
442
- let animationType = computeAnimationType(key, isInStack, isTop, animationDirection, presentation, isInitialPhase, animated, isBulkRemovalRef.current);
443
- if (hasExitingModalLike && isTop && isInStack) {
444
- animationType = 'none';
445
- }
446
- if (isStackBeingDismissed && !isInStack) {
447
- animationType = 'no-animate';
448
- }
449
-
450
- // SplitView-secondary-only: suppress enter animation for the first screen after empty.
451
- if (!animateFirstScreenAfterEmpty && isTop && direction === 'forward' && suppressedEnterKeyRef.current === key) {
452
- animationType = 'none';
453
- }
454
- items[key] = {
455
- presentationType,
456
- animationType,
457
- phase,
458
- transitionStatus: status,
459
- zIndex
460
- };
461
- }
462
- for (let index = 0; index < routeKeys.length; index++) {
463
- const key = routeKeys[index];
464
- if (!key || items[key]) continue;
465
- const child = childMap.get(key);
466
- if (!child) continue;
467
- const item = child.props.item;
468
- if (!item) continue;
469
- const presentation = item.options?.stackPresentation ?? 'push';
470
- const animated = item.options?.animated ?? true;
471
- const isInStack = routeKeySet.has(key);
472
- const isTop = isInStack && topKey !== null && key === topKey;
473
- let phase;
474
- if (isTop) {
475
- phase = 'active';
476
- } else {
477
- phase = 'inactive';
478
- }
479
- const presentationType = getPresentationTypeClass(presentation);
480
- let animationType = isInitialPhase ? 'none' : computeAnimationType(key, isInStack, isTop, animationDirection, presentation, isInitialPhase, animated, isBulkRemovalRef.current);
481
- if (hasExitingModalLike && isTop && isInStack) {
482
- animationType = 'none';
483
- }
484
- if (isStackBeingDismissed && !isInStack) {
485
- animationType = 'no-animate';
486
- }
487
- if (!animateFirstScreenAfterEmpty && isTop && direction === 'forward' && suppressedEnterKeyRef.current === key) {
488
- animationType = 'none';
489
- }
490
- items[key] = {
491
- presentationType,
492
- animationType,
493
- phase,
494
- transitionStatus: 'preEnter',
495
- zIndex: index + 1
496
- };
497
- }
498
- return {
499
- items
500
- };
501
- }, [keysToRender, stateMap, childMap, routeKeySet, topKey, isInitialPhase, routeKeys, animationDirection, direction, animateFirstScreenAfterEmpty, isStackBeingDismissed, hasExitingModalLike]);
502
- const animating = useMemo(() => {
503
- return stateMapEntries.some(([, state]) => state.isMounted && (state.status === 'entering' || state.status === 'exiting' || state.status === 'preEnter' || state.status === 'preExit'));
504
- }, [stateMapEntries]);
505
-
506
- // D6: Compute current screen label for accessibility
507
- const currentScreenLabel = useMemo(() => {
508
- if (!topKey) return '';
509
- const child = childMap.get(topKey);
510
- if (!child) return '';
511
- const item = child.props.item;
512
- return item?.routeId || topKey;
513
- }, [topKey, childMap]);
514
- return /*#__PURE__*/_jsx(ScreenStackItemsContext.Provider, {
515
- value: itemsContextValue,
516
- children: /*#__PURE__*/_jsx(ScreenStackAnimatingContext.Provider, {
517
- value: animating,
518
- children: /*#__PURE__*/_jsxs("div", {
519
- ref: containerRef,
520
- className: containerClassName + (animating ? ' animating' : ''),
521
- "data-has-active-modal": hasActiveModal ? 'true' : undefined,
522
- "data-has-active-sheet": hasActiveSheet ? 'true' : undefined,
523
- "data-animation": animating ? 'true' : undefined,
524
- style: {
525
- '--stack-transition-time': `${transitionTime}ms`
526
- },
527
- children: [/*#__PURE__*/_jsx("div", {
528
- "aria-live": "polite",
529
- role: "status",
530
- style: {
531
- position: 'absolute',
532
- width: 1,
533
- height: 1,
534
- overflow: 'hidden',
535
- clip: 'rect(0,0,0,0)'
536
- },
537
- children: currentScreenLabel
538
- }), keysToRender.map(key => {
539
- const transitionState = stateMap.get(key);
540
- if (!transitionState || !transitionState.isMounted) {
541
- devLog(`[ScreenStack] Skipping ${key} - no state or not mounted`, {
542
- hasState: !!transitionState,
543
- isMounted: transitionState?.isMounted
544
- });
545
- return null;
546
- }
547
- const child = childMap.get(key);
548
- if (!child) {
549
- devLog(`[ScreenStack] No child element for ${key}`, {
550
- availableKeys: Array.from(childMap.keys())
551
- });
552
- return null;
553
- }
554
- return /*#__PURE__*/_jsx(Fragment, {
555
- children: child
556
- }, child.key || key);
557
- })]
558
- })
559
- })
560
- });
561
- });
@@ -1,21 +0,0 @@
1
- "use strict";
2
-
3
- import { createContext, useContext } from 'react';
4
- export const ScreenStackItemsContext = /*#__PURE__*/createContext(null);
5
- export const ScreenStackAnimatingContext = /*#__PURE__*/createContext(false);
6
- export const ScreenStackConfigContext = /*#__PURE__*/createContext({
7
- animateFirstScreenAfterEmpty: true
8
- });
9
- export const useScreenStackConfig = () => {
10
- return useContext(ScreenStackConfigContext);
11
- };
12
- export const useScreenStackItemsContext = () => {
13
- const ctx = useContext(ScreenStackItemsContext);
14
- if (!ctx) {
15
- throw new Error('useScreenStackItemsContext must be used within ScreenStack');
16
- }
17
- return ctx;
18
- };
19
- export const useScreenStackAnimatingContext = () => {
20
- return useContext(ScreenStackAnimatingContext);
21
- };