@sigmela/router 0.1.3 → 0.2.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 (52) hide show
  1. package/README.md +177 -833
  2. package/lib/module/Navigation.js +1 -10
  3. package/lib/module/NavigationStack.js +168 -19
  4. package/lib/module/Router.js +1523 -501
  5. package/lib/module/RouterContext.js +1 -1
  6. package/lib/module/ScreenStack/ScreenStack.web.js +388 -117
  7. package/lib/module/ScreenStack/ScreenStackContext.js +21 -0
  8. package/lib/module/ScreenStack/animationHelpers.js +72 -0
  9. package/lib/module/ScreenStackItem/ScreenStackItem.js +2 -1
  10. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +76 -16
  11. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +2 -1
  12. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +1 -1
  13. package/lib/module/SplitView/RenderSplitView.native.js +85 -0
  14. package/lib/module/SplitView/RenderSplitView.web.js +109 -0
  15. package/lib/module/SplitView/SplitView.js +89 -0
  16. package/lib/module/SplitView/SplitViewContext.js +4 -0
  17. package/lib/module/SplitView/index.js +5 -0
  18. package/lib/module/SplitView/useSplitView.js +11 -0
  19. package/lib/module/StackRenderer.js +4 -2
  20. package/lib/module/TabBar/RenderTabBar.native.js +118 -33
  21. package/lib/module/TabBar/RenderTabBar.web.js +52 -47
  22. package/lib/module/TabBar/TabBar.js +116 -3
  23. package/lib/module/TabBar/index.js +4 -1
  24. package/lib/module/TabBar/useTabBarHeight.js +22 -0
  25. package/lib/module/index.js +3 -4
  26. package/lib/module/navigationNode.js +3 -0
  27. package/lib/module/styles.css +693 -28
  28. package/lib/typescript/src/NavigationStack.d.ts +25 -13
  29. package/lib/typescript/src/Router.d.ts +147 -34
  30. package/lib/typescript/src/RouterContext.d.ts +1 -1
  31. package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -2
  32. package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +31 -0
  33. package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +6 -0
  34. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +5 -1
  35. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +1 -1
  36. package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +8 -0
  37. package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +8 -0
  38. package/lib/typescript/src/SplitView/SplitView.d.ts +31 -0
  39. package/lib/typescript/src/SplitView/SplitViewContext.d.ts +3 -0
  40. package/lib/typescript/src/SplitView/index.d.ts +5 -0
  41. package/lib/typescript/src/SplitView/useSplitView.d.ts +2 -0
  42. package/lib/typescript/src/StackRenderer.d.ts +2 -1
  43. package/lib/typescript/src/TabBar/TabBar.d.ts +27 -3
  44. package/lib/typescript/src/TabBar/index.d.ts +3 -0
  45. package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +18 -0
  46. package/lib/typescript/src/createController.d.ts +1 -0
  47. package/lib/typescript/src/index.d.ts +4 -3
  48. package/lib/typescript/src/navigationNode.d.ts +41 -0
  49. package/lib/typescript/src/types.d.ts +29 -32
  50. package/package.json +6 -5
  51. package/lib/module/web/TransitionStack.js +0 -227
  52. package/lib/typescript/src/web/TransitionStack.d.ts +0 -21
@@ -13,7 +13,7 @@ export const useRouter = () => {
13
13
  export const useCurrentRoute = () => {
14
14
  const router = useRouter();
15
15
  const subscribe = React.useCallback(cb => router.subscribe(cb), [router]);
16
- const get = React.useCallback(() => router.getVisibleRoute(), [router]);
16
+ const get = React.useCallback(() => router.getActiveRoute(), [router]);
17
17
  return React.useSyncExternalStore(subscribe, get, get);
18
18
  };
19
19
  export function useParams() {
@@ -1,139 +1,410 @@
1
1
  "use strict";
2
2
 
3
- import { Fragment, memo, useRef } from 'react';
4
- import TransitionStack from "../web/TransitionStack.js";
5
- import { Children, cloneElement, useLayoutEffect, useMemo, useState } from 'react';
3
+ import { memo, useRef, useLayoutEffect, useMemo, useEffect, Children, isValidElement, Fragment } from 'react';
4
+ import { useTransitionMap } from 'react-transition-state';
5
+ import { ScreenStackItemsContext, ScreenStackAnimatingContext, useScreenStackConfig } from "./ScreenStackContext.js";
6
+ import { getPresentationTypeClass, computeAnimationType } from "./animationHelpers.js";
6
7
  import { jsx as _jsx } from "react/jsx-runtime";
8
+ const devLog = (_, __) => {};
9
+ const isScreenStackItemElement = child => {
10
+ if (! /*#__PURE__*/isValidElement(child)) return false;
11
+ const anyProps = child.props;
12
+ return anyProps && typeof anyProps === 'object' && 'item' in anyProps;
13
+ };
14
+ const getItemKey = child => {
15
+ const anyChild = child;
16
+ const reactKey = anyChild.key ?? null;
17
+ if (typeof reactKey === 'string' && reactKey.length > 0 && !reactKey.startsWith('.')) {
18
+ return reactKey;
19
+ }
20
+ const item = anyChild.props?.item;
21
+ if (item?.key && typeof item.key === 'string') {
22
+ return item.key;
23
+ }
24
+ if (item?.routeId) {
25
+ return String(item.routeId);
26
+ }
27
+ throw new Error('[ScreenStack] ScreenStackItem is missing a stable key');
28
+ };
29
+ const computeDirection = (prev, current) => {
30
+ if (prev.length === 0 && current.length > 0) {
31
+ return 'forward';
32
+ }
33
+ if (current.length > prev.length) {
34
+ return 'forward';
35
+ }
36
+ if (current.length < prev.length) {
37
+ return 'back';
38
+ }
39
+ const prevTop = prev[prev.length - 1];
40
+ const currentTop = current[current.length - 1];
41
+ if (prevTop === currentTop) {
42
+ return 'forward';
43
+ }
44
+ const prevIndexOfCurrentTop = prev.indexOf(currentTop);
45
+ const prevIndexOfPrevTop = prev.indexOf(prevTop);
46
+ if (prevIndexOfCurrentTop !== -1 && prevIndexOfPrevTop !== -1 && prevIndexOfCurrentTop < prevIndexOfPrevTop) {
47
+ return 'back';
48
+ }
49
+ return 'forward';
50
+ };
7
51
  export const ScreenStack = /*#__PURE__*/memo(props => {
8
52
  const {
9
53
  children,
10
54
  transitionTime = 250,
11
- type,
12
55
  animated = true
13
56
  } = props;
14
- const [records, setRecords] = useState([]);
57
+ devLog('[ScreenStack] Render', {
58
+ transitionTime,
59
+ animated,
60
+ childrenExists: !!children
61
+ });
15
62
  const containerRef = useRef(null);
16
- const transitionRef = useRef(null);
17
- const prevSelectedRef = useRef(-1);
18
- const lastExitingIndexRef = useRef(null);
19
- const prevSignatureRef = useRef(null);
20
- const childArray = useMemo(() => Children.toArray(children).filter(Boolean), [children]);
21
- const nextKeys = useMemo(() => childArray.map(c => c.key), [childArray]);
22
- useLayoutEffect(() => {
23
- setRecords(prev => {
24
- const prevKeys = prev.map(r => r.key);
25
- if (nextKeys.length === prevKeys.length + 1 && prevKeys.every((k, i) => k === nextKeys[i])) {
26
- const child = childArray[childArray.length - 1];
27
- if (!child) return prev;
28
- const key = child.key;
29
- const element = /*#__PURE__*/cloneElement(child, {
30
- phase: 'active'
63
+ const isInitialMountRef = useRef(true);
64
+ const suppressEnterAfterEmptyRef = useRef(false);
65
+ const suppressedEnterKeyRef = useRef(null);
66
+ const prevKeysRef = useRef([]);
67
+ const lastDirectionRef = useRef('forward');
68
+ const childMapRef = useRef(new Map());
69
+ const stackChildren = useMemo(() => {
70
+ const stackItems = [];
71
+ Children.forEach(children, child => {
72
+ if (isScreenStackItemElement(child)) {
73
+ stackItems.push(child);
74
+ } else if (child != null) {
75
+ devLog('[ScreenStack] Non-ScreenStackItem child ignored', {
76
+ child
31
77
  });
32
- return [...prev, {
33
- key,
34
- phase: 'active',
35
- element
36
- }];
37
78
  }
38
- if (prevKeys.length === nextKeys.length + 1 && nextKeys.every((k, i) => k === prevKeys[i])) {
39
- const last = prev[prev.length - 1];
40
- if (!last) return prev;
41
- if (last.phase === 'exiting') return prev;
42
- return [...prev.slice(0, -1), {
43
- key: last.key,
44
- phase: 'exiting',
45
- element: /*#__PURE__*/cloneElement(last.element, {
46
- phase: 'exiting'
47
- })
48
- }];
49
- }
50
- const nextByKey = new Map();
51
- for (const ch of childArray) nextByKey.set(ch.key, ch);
52
- const prevByKey = new Map();
53
- for (const r of prev) prevByKey.set(r.key, r);
54
- const result = [];
55
- for (const ch of childArray) {
56
- const key = ch.key;
57
- const existed = prevByKey.get(key);
58
- const nextEl = /*#__PURE__*/cloneElement(ch, {
59
- phase: 'active'
60
- });
61
- if (existed) {
62
- const sameActive = existed.phase === 'active';
63
- result.push(sameActive ? {
64
- key,
65
- phase: 'active',
66
- element: nextEl
67
- } : {
68
- key,
69
- phase: 'active',
70
- element: nextEl
71
- });
72
- } else {
73
- result.push({
74
- key,
75
- phase: 'active',
76
- element: nextEl
77
- });
78
- }
79
+ });
80
+ devLog('[ScreenStack] Parsed children', {
81
+ stackChildrenLength: stackItems.length
82
+ });
83
+ return stackItems;
84
+ }, [children]);
85
+ const routeKeys = useMemo(() => {
86
+ const keys = stackChildren.map(child => {
87
+ const item = child.props.item;
88
+ return item?.key || getItemKey(child);
89
+ });
90
+ devLog('[ScreenStack] routeKeys', keys);
91
+ return keys;
92
+ }, [stackChildren]);
93
+ const childMap = useMemo(() => {
94
+ const map = new Map(childMapRef.current);
95
+ for (const child of stackChildren) {
96
+ const item = child.props.item;
97
+ const key = item?.key || getItemKey(child);
98
+ map.set(key, child);
99
+ }
100
+ childMapRef.current = map;
101
+ devLog('[ScreenStack] childMap updated', {
102
+ size: map.size,
103
+ keys: Array.from(map.keys())
104
+ });
105
+ return map;
106
+ }, [stackChildren]);
107
+ const {
108
+ stateMap,
109
+ toggle,
110
+ setItem,
111
+ deleteItem
112
+ } = useTransitionMap({
113
+ timeout: transitionTime,
114
+ preEnter: true,
115
+ mountOnEnter: true,
116
+ unmountOnExit: false,
117
+ enter: animated,
118
+ exit: animated,
119
+ allowMultiple: true,
120
+ onStateChange: ({
121
+ key,
122
+ current
123
+ }) => {
124
+ devLog(`[ScreenStack] Transition state change for key ${key}:`, {
125
+ status: current.status,
126
+ isMounted: current.isMounted,
127
+ isEnter: current.isEnter,
128
+ isResolved: current.isResolved
129
+ });
130
+ }
131
+ });
132
+ devLog('[ScreenStack] Current transition states:', Array.from(stateMap.entries()).map(([key, state]) => ({
133
+ key,
134
+ status: state.status,
135
+ isMounted: state.isMounted,
136
+ isEnter: state.isEnter,
137
+ isResolved: state.isResolved
138
+ })));
139
+ const stateMapEntries = Array.from(stateMap.entries());
140
+ const direction = useMemo(() => {
141
+ const prevKeys = prevKeysRef.current;
142
+ const computed = computeDirection(prevKeys, routeKeys);
143
+ prevKeysRef.current = routeKeys;
144
+ return computed;
145
+ }, [routeKeys]);
146
+ devLog('[ScreenStack] Computed direction', {
147
+ prevKeys: prevKeysRef.current,
148
+ routeKeys,
149
+ direction
150
+ });
151
+ const screenStackConfig = useScreenStackConfig();
152
+ const animateFirstScreenAfterEmpty = screenStackConfig.animateFirstScreenAfterEmpty ?? true;
153
+ const isInitialPhase = isInitialMountRef.current;
154
+ const keysToRender = useMemo(() => {
155
+ const routeKeySet = new Set(routeKeys);
156
+ const exitingKeys = [];
157
+ for (const [key, state] of stateMapEntries) {
158
+ if (!state.isMounted) continue;
159
+ if (!routeKeySet.has(key)) {
160
+ exitingKeys.push(key);
79
161
  }
80
- for (const r of prev) {
81
- if (!nextByKey.has(r.key)) {
82
- const exitingEl = /*#__PURE__*/cloneElement(r.element, {
83
- phase: 'exiting'
84
- });
85
- result.push({
86
- key: r.key,
87
- phase: 'exiting',
88
- element: exitingEl
89
- });
90
- }
162
+ }
163
+ const result = [...routeKeys, ...exitingKeys];
164
+ devLog('[ScreenStack] Keys to render:', {
165
+ result,
166
+ exitingKeys
167
+ });
168
+ return result;
169
+ }, [routeKeys, stateMapEntries]);
170
+ const containerClassName = useMemo(() => {
171
+ return 'screen-stack';
172
+ }, []);
173
+ useLayoutEffect(() => {
174
+ devLog('[ScreenStack] === LIFECYCLE EFFECT START ===', {
175
+ prevKeys: prevKeysRef.current,
176
+ routeKeys,
177
+ direction
178
+ });
179
+ const routeKeySet = new Set(routeKeys);
180
+ const existingKeySet = new Set();
181
+ for (const [key] of stateMapEntries) {
182
+ existingKeySet.add(key);
183
+ }
184
+ const newKeys = routeKeys.filter(key => !existingKeySet.has(key));
185
+ const removedKeys = [...existingKeySet].filter(key => !routeKeySet.has(key));
186
+
187
+ // Track "became empty" state to suppress the next enter animation when configured.
188
+ // This is SplitView-secondary-only (via ScreenStackConfigContext), not global behavior.
189
+ if (!animateFirstScreenAfterEmpty) {
190
+ if (routeKeys.length === 0) {
191
+ suppressEnterAfterEmptyRef.current = true;
192
+ suppressedEnterKeyRef.current = null;
91
193
  }
92
- return result;
194
+ }
195
+ devLog('[ScreenStack] Lifecycle diff', {
196
+ newKeys,
197
+ removedKeys
93
198
  });
94
- }, [childArray, nextKeys]);
199
+
200
+ // If this is the first pushed key after the stack was empty, remember its key so we can
201
+ // suppress only its enter animation (without affecting exit animations).
202
+ if (!animateFirstScreenAfterEmpty && suppressEnterAfterEmptyRef.current && routeKeys.length > 0 && newKeys.length > 0) {
203
+ const candidate = newKeys[newKeys.length - 1] ?? null;
204
+ suppressedEnterKeyRef.current = candidate;
205
+ suppressEnterAfterEmptyRef.current = false;
206
+ }
207
+ for (const key of newKeys) {
208
+ devLog(`[ScreenStack] Adding item: ${key}`);
209
+ setItem(key);
210
+ devLog(`[ScreenStack] Entering item: ${key}`);
211
+ toggle(key, true);
212
+ }
213
+ for (const key of removedKeys) {
214
+ const state = stateMap.get(key);
215
+ if (state && state.isEnter) {
216
+ devLog(`[ScreenStack] Starting exit for item: ${key}`, {
217
+ status: state.status
218
+ });
219
+ toggle(key, false);
220
+ } else {
221
+ devLog(`[ScreenStack] Skip exit for item (not entered or missing): ${key}`, {
222
+ hasState: !!state,
223
+ status: state?.status,
224
+ isEnter: state?.isEnter
225
+ });
226
+ }
227
+ }
228
+ lastDirectionRef.current = direction;
229
+ devLog('[ScreenStack] === LIFECYCLE EFFECT END ===');
230
+ }, [routeKeys, direction, setItem, toggle, stateMapEntries, stateMap, animateFirstScreenAfterEmpty]);
95
231
  useLayoutEffect(() => {
96
- if (!containerRef.current || transitionRef.current) return;
97
- transitionRef.current = TransitionStack({
98
- content: containerRef.current,
99
- type: type,
100
- transitionTime,
101
- withAnimationListener: animated,
102
- onTransitionEnd: () => {
103
- setRecords(prev => {
104
- const idx = lastExitingIndexRef.current ?? -1;
105
- if (idx < 0 || idx >= prev.length) return prev;
106
- const candidate = prev[idx];
107
- if (candidate?.phase !== 'exiting') return prev;
108
- return [...prev.slice(0, idx), ...prev.slice(idx + 1)];
232
+ devLog('[ScreenStack] === CLEANUP EFFECT START ===');
233
+ const routeKeySet = new Set(routeKeys);
234
+ for (const [key, state] of stateMapEntries) {
235
+ if (!state.isMounted) {
236
+ devLog(`[ScreenStack] Cleanup unmounted item: ${key}`, {
237
+ status: state.status,
238
+ isResolved: state.isResolved
109
239
  });
240
+ deleteItem(key);
241
+ childMapRef.current.delete(key);
242
+ continue;
110
243
  }
111
- });
112
- }, [type, transitionTime, animated]);
244
+ const isInStack = routeKeySet.has(key);
245
+ const canCleanup = !isInStack && state.status === 'exited' && state.isResolved === true;
246
+ if (canCleanup) {
247
+ devLog(`[ScreenStack] Cleanup exited item: ${key}`, {
248
+ status: state.status,
249
+ isResolved: state.isResolved
250
+ });
251
+ deleteItem(key);
252
+ childMapRef.current.delete(key);
253
+ }
254
+ }
255
+ devLog('[ScreenStack] === CLEANUP EFFECT END ===');
256
+ }, [routeKeys, stateMapEntries, deleteItem]);
257
+ useEffect(() => {
258
+ if (!isInitialMountRef.current) return;
259
+ const hasMountedItem = stateMapEntries.some(([, st]) => st.isMounted);
260
+
261
+ // If the stack mounts empty, we still want the first pushed screen to animate.
262
+ // Mark initial mount as completed immediately in that case.
263
+ if (!hasMountedItem && routeKeys.length === 0) {
264
+ if (animateFirstScreenAfterEmpty) {
265
+ isInitialMountRef.current = false;
266
+ devLog('[ScreenStack] Initial mount completed (empty stack)');
267
+ }
268
+ return;
269
+ }
270
+ if (hasMountedItem) {
271
+ isInitialMountRef.current = false;
272
+ devLog('[ScreenStack] Initial mount completed');
273
+ }
274
+ }, [stateMapEntries, routeKeys.length, animateFirstScreenAfterEmpty]);
275
+
276
+ // Clear suppression key once it is no longer the top screen (so it can animate normally as
277
+ // a background when new screens are pushed).
113
278
  useLayoutEffect(() => {
114
- if (!transitionRef.current) return;
115
- const targetIndex = (() => {
116
- for (let i = records.length - 1; i >= 0; i--) {
117
- const rec = records[i];
118
- if (rec && rec.phase === 'active') return i;
279
+ if (animateFirstScreenAfterEmpty) return;
280
+ const topKey = routeKeys[routeKeys.length - 1] ?? null;
281
+ if (!topKey) {
282
+ suppressedEnterKeyRef.current = null;
283
+ return;
284
+ }
285
+ if (suppressedEnterKeyRef.current && suppressedEnterKeyRef.current !== topKey) {
286
+ suppressedEnterKeyRef.current = null;
287
+ }
288
+ }, [routeKeys, animateFirstScreenAfterEmpty]);
289
+ useEffect(() => {
290
+ if (!containerRef.current) return;
291
+ const items = containerRef.current.querySelectorAll('.screen-stack-item');
292
+ if (items.length === 0) return;
293
+ devLog('[ScreenStack] DOM State after render:', {
294
+ containerClasses: containerRef.current.className,
295
+ containerDataAnimation: containerRef.current.dataset.animation,
296
+ containerDataDirection: containerRef.current.dataset.direction,
297
+ itemCount: items.length
298
+ });
299
+ });
300
+ const topKey = routeKeys[routeKeys.length - 1] ?? null;
301
+ const routeKeySet = useMemo(() => new Set(routeKeys), [routeKeys]);
302
+ const itemsContextValue = useMemo(() => {
303
+ const items = {};
304
+ for (let index = 0; index < keysToRender.length; index++) {
305
+ const key = keysToRender[index];
306
+ if (!key) continue;
307
+ const transitionState = stateMap.get(key);
308
+ const child = childMap.get(key);
309
+ if (!child) continue;
310
+ const item = child.props.item;
311
+ if (!item) continue;
312
+ const presentation = item.options?.stackPresentation ?? 'push';
313
+ const animated = item.options?.animated ?? true;
314
+ const isInStack = routeKeySet.has(key);
315
+ const isTop = isInStack && topKey !== null && key === topKey;
316
+ let phase;
317
+ if (!isInStack) {
318
+ phase = 'exiting';
319
+ } else if (isTop) {
320
+ phase = 'active';
321
+ } else {
322
+ phase = 'inactive';
323
+ }
324
+ const rawStatus = transitionState?.status || 'preEnter';
325
+ const status = isInitialPhase && (rawStatus === 'preEnter' || rawStatus === 'entering') ? 'entered' : rawStatus;
326
+ const routeIndex = routeKeys.indexOf(key);
327
+ const zIndex = routeIndex >= 0 ? routeIndex + 1 : keysToRender.length + index + 1;
328
+ const presentationType = getPresentationTypeClass(presentation);
329
+ let animationType = computeAnimationType(key, isInStack, isTop, direction, presentation, isInitialPhase, animated);
330
+
331
+ // SplitView-secondary-only: suppress enter animation for the first screen after empty.
332
+ if (!animateFirstScreenAfterEmpty && isTop && direction === 'forward' && suppressedEnterKeyRef.current === key) {
333
+ animationType = 'none';
334
+ }
335
+ items[key] = {
336
+ presentationType,
337
+ animationType,
338
+ phase,
339
+ transitionStatus: status,
340
+ zIndex
341
+ };
342
+ }
343
+ for (let index = 0; index < routeKeys.length; index++) {
344
+ const key = routeKeys[index];
345
+ if (!key || items[key]) continue;
346
+ const child = childMap.get(key);
347
+ if (!child) continue;
348
+ const item = child.props.item;
349
+ if (!item) continue;
350
+ const presentation = item.options?.stackPresentation ?? 'push';
351
+ const animated = item.options?.animated ?? true;
352
+ const isInStack = routeKeySet.has(key);
353
+ const isTop = isInStack && topKey !== null && key === topKey;
354
+ let phase;
355
+ if (isTop) {
356
+ phase = 'active';
357
+ } else {
358
+ phase = 'inactive';
359
+ }
360
+ const presentationType = getPresentationTypeClass(presentation);
361
+ let animationType = isInitialPhase ? 'none' : computeAnimationType(key, isInStack, isTop, direction, presentation, isInitialPhase, animated);
362
+ if (!animateFirstScreenAfterEmpty && isTop && direction === 'forward' && suppressedEnterKeyRef.current === key) {
363
+ animationType = 'none';
119
364
  }
120
- return -1;
121
- })();
122
- if (targetIndex === -1) return;
123
- const outgoingIndex = records.findIndex(it => it.phase === 'exiting');
124
- lastExitingIndexRef.current = outgoingIndex !== -1 ? outgoingIndex : null;
125
- const signature = records.map(r => `${String(r.key)}:${r.phase}`).join('|');
126
- const stackChanged = prevSignatureRef.current !== null && prevSignatureRef.current !== signature;
127
- const animate = animated && stackChanged;
128
- transitionRef.current(targetIndex, animate);
129
- prevSignatureRef.current = signature;
130
- prevSelectedRef.current = targetIndex;
131
- }, [records, animated]);
132
- return /*#__PURE__*/_jsx("div", {
133
- ref: containerRef,
134
- className: `screen-stack${type ? ` ${type}` : ''}`,
135
- children: records.map(r => /*#__PURE__*/_jsx(Fragment, {
136
- children: r.element
137
- }, r.key))
365
+ items[key] = {
366
+ presentationType,
367
+ animationType,
368
+ phase,
369
+ transitionStatus: 'preEnter',
370
+ zIndex: index + 1
371
+ };
372
+ }
373
+ return {
374
+ items
375
+ };
376
+ }, [keysToRender, stateMap, childMap, routeKeySet, topKey, isInitialPhase, routeKeys, direction, animateFirstScreenAfterEmpty]);
377
+ const animating = useMemo(() => {
378
+ return stateMapEntries.some(([, state]) => state.isMounted && (state.status === 'entering' || state.status === 'exiting' || state.status === 'preEnter' || state.status === 'preExit'));
379
+ }, [stateMapEntries]);
380
+ return /*#__PURE__*/_jsx(ScreenStackItemsContext.Provider, {
381
+ value: itemsContextValue,
382
+ children: /*#__PURE__*/_jsx(ScreenStackAnimatingContext.Provider, {
383
+ value: animating,
384
+ children: /*#__PURE__*/_jsx("div", {
385
+ ref: containerRef,
386
+ className: containerClassName + (animating ? ' animating' : ''),
387
+ children: keysToRender.map(key => {
388
+ const transitionState = stateMap.get(key);
389
+ if (!transitionState || !transitionState.isMounted) {
390
+ devLog(`[ScreenStack] Skipping ${key} - no state or not mounted`, {
391
+ hasState: !!transitionState,
392
+ isMounted: transitionState?.isMounted
393
+ });
394
+ return null;
395
+ }
396
+ const child = childMap.get(key);
397
+ if (!child) {
398
+ devLog(`[ScreenStack] No child element for ${key}`, {
399
+ availableKeys: Array.from(childMap.keys())
400
+ });
401
+ return null;
402
+ }
403
+ return /*#__PURE__*/_jsx(Fragment, {
404
+ children: child
405
+ }, child.key || key);
406
+ })
407
+ })
408
+ })
138
409
  });
139
410
  });
@@ -0,0 +1,21 @@
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
+ };
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ export function getPresentationTypeClass(presentation) {
4
+ switch (presentation) {
5
+ case 'push':
6
+ return 'push';
7
+ case 'modal':
8
+ return 'modal';
9
+ case 'transparentModal':
10
+ return 'transparent-modal';
11
+ case 'containedModal':
12
+ return 'contained-modal';
13
+ case 'containedTransparentModal':
14
+ return 'contained-transparent-modal';
15
+ case 'fullScreenModal':
16
+ return 'fullscreen-modal';
17
+ case 'formSheet':
18
+ return 'formsheet';
19
+ case 'pageSheet':
20
+ return 'pagesheet';
21
+ case 'sheet':
22
+ return 'sheet';
23
+ default:
24
+ return 'push';
25
+ }
26
+ }
27
+ export function getAnimationTypeForPresentation(presentation, isEntering, direction) {
28
+ const suffix = isEntering ? 'enter' : 'exit';
29
+ const presentationClass = getPresentationTypeClass(presentation);
30
+ if (presentation === 'push') {
31
+ return direction === 'forward' ? `push-${suffix}` : `pop-${suffix}`;
32
+ }
33
+ return `${presentationClass}-${suffix}`;
34
+ }
35
+ export function computeAnimationType(_key, isInStack, isTop, direction, presentation, isInitialPhase, animated = true) {
36
+ if (!animated) {
37
+ return 'no-animate';
38
+ }
39
+ if (isInitialPhase) {
40
+ return 'none';
41
+ }
42
+ const isEntering = isInStack && isTop;
43
+ const isModalLike = ['modal', 'transparentModal', 'containedModal', 'containedTransparentModal', 'fullScreenModal', 'formSheet', 'pageSheet', 'sheet'].includes(presentation);
44
+ if (isModalLike) {
45
+ if (!isInStack) {
46
+ return getAnimationTypeForPresentation(presentation, false, direction);
47
+ }
48
+ if (isEntering) {
49
+ return getAnimationTypeForPresentation(presentation, true, direction);
50
+ }
51
+ return 'none';
52
+ }
53
+ if (!isInStack) {
54
+ if (direction === 'forward') {
55
+ return 'push-exit';
56
+ } else {
57
+ return 'pop-exit';
58
+ }
59
+ }
60
+ if (isTop) {
61
+ if (direction === 'forward') {
62
+ return 'push-enter';
63
+ } else {
64
+ return 'pop-enter';
65
+ }
66
+ }
67
+ if (direction === 'forward') {
68
+ return 'push-background';
69
+ } else {
70
+ return 'pop-background';
71
+ }
72
+ }
@@ -59,7 +59,8 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
59
59
  children: /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
60
60
  value: route,
61
61
  children: /*#__PURE__*/_jsx(item.component, {
62
- ...item.passProps
62
+ ...item.passProps,
63
+ appearance: appearance
63
64
  })
64
65
  })
65
66
  }, item.key);