@sigmela/router 0.2.3 → 0.2.4
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.
- package/README.md +65 -9
- package/lib/module/Navigation.js +13 -10
- package/lib/module/NavigationStack.js +13 -1
- package/lib/module/Router.js +115 -6
- package/lib/module/ScreenStack/ScreenStack.web.js +15 -9
- package/lib/module/ScreenStack/animationHelpers.js +12 -2
- package/lib/module/ScreenStackItem/ScreenStackItem.js +4 -1
- package/lib/module/ScreenStackItem/ScreenStackItem.web.js +19 -5
- package/lib/module/SplitView/RenderSplitView.native.js +57 -64
- package/lib/module/TabBar/RenderTabBar.native.js +76 -4
- package/lib/module/styles.css +91 -16
- package/lib/module/types.js +11 -1
- package/lib/typescript/src/Navigation.d.ts +8 -0
- package/lib/typescript/src/NavigationStack.d.ts +3 -2
- package/lib/typescript/src/Router.d.ts +12 -1
- package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +2 -2
- package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +7 -0
- package/lib/typescript/src/types.d.ts +14 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,10 +148,60 @@ const stack = new NavigationStack({ header: { largeTitle: true } })
|
|
|
148
148
|
|
|
149
149
|
Key methods:
|
|
150
150
|
- `addScreen(pathPattern, componentOrNode, options?)`
|
|
151
|
-
- `addModal(pathPattern,
|
|
152
|
-
- `addSheet(pathPattern,
|
|
151
|
+
- `addModal(pathPattern, componentOrStack, options?)` (shorthand for `stackPresentation: 'modal'`)
|
|
152
|
+
- `addSheet(pathPattern, componentOrStack, options?)` (shorthand for `stackPresentation: 'sheet'`)
|
|
153
153
|
- `addStack(prefixOrStack, maybeStack?)` — compose nested stacks under a prefix
|
|
154
154
|
|
|
155
|
+
### Modal Stacks (Stack in Stack)
|
|
156
|
+
|
|
157
|
+
You can pass an entire `NavigationStack` to `addModal()` or `addSheet()` to create a multi-screen flow inside a modal:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
// Define a flow with multiple screens
|
|
161
|
+
const emailVerifyStack = new NavigationStack()
|
|
162
|
+
.addScreen('/verify', EmailInputScreen)
|
|
163
|
+
.addScreen('/verify/sent', EmailSentScreen);
|
|
164
|
+
|
|
165
|
+
// Mount the entire stack as a modal
|
|
166
|
+
const rootStack = new NavigationStack()
|
|
167
|
+
.addScreen('/', HomeScreen)
|
|
168
|
+
.addModal('/verify', emailVerifyStack);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**How it works:**
|
|
172
|
+
- Navigating to `/verify` opens the modal with `EmailInputScreen`
|
|
173
|
+
- Inside the modal, `router.navigate('/verify/sent')` pushes `EmailSentScreen` within the same modal
|
|
174
|
+
- `router.goBack()` navigates back inside the modal stack
|
|
175
|
+
- `router.dismiss()` closes the entire modal from any depth
|
|
176
|
+
|
|
177
|
+
**Example screen with navigation inside modal:**
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
function EmailInputScreen() {
|
|
181
|
+
const router = useRouter();
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<View>
|
|
185
|
+
<Button title="Next" onPress={() => router.navigate('/verify/sent')} />
|
|
186
|
+
<Button title="Close" onPress={() => router.dismiss()} />
|
|
187
|
+
</View>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function EmailSentScreen() {
|
|
192
|
+
const router = useRouter();
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<View>
|
|
196
|
+
<Button title="Back" onPress={() => router.goBack()} />
|
|
197
|
+
<Button title="Done" onPress={() => router.dismiss()} />
|
|
198
|
+
</View>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This pattern works recursively — you can nest stacks inside stacks to any depth.
|
|
204
|
+
|
|
155
205
|
### `Router`
|
|
156
206
|
|
|
157
207
|
The `Router` holds navigation state and performs path matching.
|
|
@@ -169,6 +219,7 @@ Navigation:
|
|
|
169
219
|
- `router.navigate(path)` — push
|
|
170
220
|
- `router.replace(path, dedupe?)` — replace top of the active stack
|
|
171
221
|
- `router.goBack()` — pop top of the active stack
|
|
222
|
+
- `router.dismiss()` — close the nearest modal or sheet (including all screens in a modal stack)
|
|
172
223
|
- `router.reset(path)` — **web-only**: rebuild Router state as if app loaded at `path`
|
|
173
224
|
- `router.setRoot(rootKey, { transition? })` — swap root at runtime (`rootKey` from `config.roots`)
|
|
174
225
|
|
|
@@ -180,8 +231,6 @@ State/subscriptions:
|
|
|
180
231
|
- `router.subscribeRoot(cb)` — notify when root is replaced via `setRoot`
|
|
181
232
|
- `router.getStackHistory(stackId)` — slice of history for a stack
|
|
182
233
|
|
|
183
|
-
> Note: `router.getGlobalStackId()` exists but currently returns `undefined`.
|
|
184
|
-
|
|
185
234
|
### `TabBar`
|
|
186
235
|
|
|
187
236
|
`TabBar` is a container node that renders one tab at a time.
|
|
@@ -193,12 +242,16 @@ const tabBar = new TabBar({ component: CustomTabBar, initialIndex: 0 })
|
|
|
193
242
|
```
|
|
194
243
|
|
|
195
244
|
Key methods:
|
|
196
|
-
- `addTab({ key, stack?, screen?, title?, icon?, selectedIcon?, ... })`
|
|
245
|
+
- `addTab({ key, stack?, node?, screen?, prefix?, title?, icon?, selectedIcon?, ... })`
|
|
197
246
|
- `onIndexChange(index)` — switch active tab
|
|
198
247
|
- `setBadge(index, badge | null)`
|
|
199
248
|
- `setTabBarConfig(partialConfig)`
|
|
200
249
|
- `getState()` and `subscribe(cb)`
|
|
201
250
|
|
|
251
|
+
Notes:
|
|
252
|
+
- Exactly one of `stack`, `node`, `screen` must be provided.
|
|
253
|
+
- Use `prefix` to mount a tab's routes under a base path (e.g. `/mail`).
|
|
254
|
+
|
|
202
255
|
Web behavior note:
|
|
203
256
|
- The built-in **web** tab bar renderer resets Router history on tab switch (to keep URL and Router state consistent) using `router.reset(firstRoutePath)`.
|
|
204
257
|
|
|
@@ -207,22 +260,25 @@ Web behavior note:
|
|
|
207
260
|
`SplitView` renders **two stacks**: `primary` and `secondary`.
|
|
208
261
|
|
|
209
262
|
- On **native**, `secondary` overlays `primary` when it has at least one screen in its history.
|
|
210
|
-
- On **web**, the layout becomes side-by-side at a fixed breakpoint (
|
|
263
|
+
- On **web**, the layout becomes side-by-side at a fixed breakpoint (`minWidth`, default `640px`).
|
|
211
264
|
|
|
212
265
|
```tsx
|
|
213
|
-
import { NavigationStack, SplitView } from '@sigmela/router';
|
|
266
|
+
import { NavigationStack, SplitView, TabBar } from '@sigmela/router';
|
|
214
267
|
|
|
215
268
|
const master = new NavigationStack().addScreen('/', ThreadsScreen);
|
|
216
269
|
const detail = new NavigationStack().addScreen('/:threadId', ThreadScreen);
|
|
217
270
|
|
|
218
271
|
const splitView = new SplitView({
|
|
219
|
-
minWidth: 640,
|
|
272
|
+
minWidth: 640,
|
|
220
273
|
primary: master,
|
|
221
274
|
secondary: detail,
|
|
222
275
|
primaryMaxWidth: 390,
|
|
223
276
|
});
|
|
224
277
|
|
|
225
|
-
|
|
278
|
+
// Mount SplitView directly as a tab (no wrapper stack needed).
|
|
279
|
+
const tabBar = new TabBar()
|
|
280
|
+
.addTab({ key: 'mail', node: splitView, prefix: '/mail', title: 'Mail' })
|
|
281
|
+
.addTab({ key: 'settings', stack: settingsStack, title: 'Settings' });
|
|
226
282
|
```
|
|
227
283
|
|
|
228
284
|
## Controllers
|
package/lib/module/Navigation.js
CHANGED
|
@@ -5,13 +5,22 @@ import { RouterContext } from "./RouterContext.js";
|
|
|
5
5
|
import { ScreenStack } from "./ScreenStack/index.js";
|
|
6
6
|
import { StyleSheet } from 'react-native';
|
|
7
7
|
import { useSyncExternalStore, memo, useCallback, useEffect, useState } from 'react';
|
|
8
|
-
import { jsx as _jsx
|
|
8
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
const EMPTY_HISTORY = [];
|
|
10
10
|
function useStackHistory(router, stackId) {
|
|
11
11
|
const subscribe = useCallback(cb => stackId ? router.subscribeStack(stackId, cb) : () => {}, [router, stackId]);
|
|
12
12
|
const get = useCallback(() => stackId ? router.getStackHistory(stackId) : EMPTY_HISTORY, [router, stackId]);
|
|
13
13
|
return useSyncExternalStore(subscribe, get, get);
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Navigation component renders the root and global stacks.
|
|
18
|
+
*
|
|
19
|
+
* Modal stacks (NavigationStack added via addModal) are rendered as regular ScreenStackItems
|
|
20
|
+
* with their component being the StackRenderer that subscribes to its own stack history.
|
|
21
|
+
* This creates a clean recursive structure: stacks render their items, nested stacks
|
|
22
|
+
* (via childNode) render their own items through StackRenderer.
|
|
23
|
+
*/
|
|
15
24
|
export const Navigation = /*#__PURE__*/memo(({
|
|
16
25
|
router,
|
|
17
26
|
appearance
|
|
@@ -30,23 +39,17 @@ export const Navigation = /*#__PURE__*/memo(({
|
|
|
30
39
|
rootId
|
|
31
40
|
} = root;
|
|
32
41
|
const rootTransition = router.getRootTransition();
|
|
33
|
-
const globalId = router.getGlobalStackId();
|
|
34
42
|
const rootItems = useStackHistory(router, rootId);
|
|
35
|
-
const globalItems = useStackHistory(router, globalId);
|
|
36
43
|
return /*#__PURE__*/_jsx(RouterContext.Provider, {
|
|
37
44
|
value: router,
|
|
38
|
-
children: /*#__PURE__*/
|
|
45
|
+
children: /*#__PURE__*/_jsx(ScreenStack, {
|
|
39
46
|
style: styles.flex,
|
|
40
|
-
children:
|
|
47
|
+
children: rootItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
41
48
|
stackId: rootId,
|
|
42
49
|
item: item,
|
|
43
50
|
stackAnimation: rootTransition,
|
|
44
51
|
appearance: appearance
|
|
45
|
-
}, `root-${item.key}`))
|
|
46
|
-
appearance: appearance,
|
|
47
|
-
stackId: globalId,
|
|
48
|
-
item: item
|
|
49
|
-
}, `global-${item.key}`))]
|
|
52
|
+
}, `root-${item.key}`))
|
|
50
53
|
}, rootId ?? 'root')
|
|
51
54
|
});
|
|
52
55
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { nanoid } from 'nanoid/non-secure';
|
|
4
4
|
import { match as pathMatchFactory } from 'path-to-regexp';
|
|
5
5
|
import qs from 'query-string';
|
|
6
|
+
import React from 'react';
|
|
6
7
|
export class NavigationStack {
|
|
7
8
|
routes = [];
|
|
8
9
|
children = [];
|
|
@@ -130,7 +131,18 @@ export class NavigationStack {
|
|
|
130
131
|
return this.children.slice();
|
|
131
132
|
}
|
|
132
133
|
getRenderer() {
|
|
133
|
-
|
|
134
|
+
// eslint-disable-next-line consistent-this
|
|
135
|
+
const stackInstance = this;
|
|
136
|
+
return function NavigationStackRenderer(props) {
|
|
137
|
+
// Lazy require to avoid circular dependency (StackRenderer imports NavigationStack)
|
|
138
|
+
const {
|
|
139
|
+
StackRenderer
|
|
140
|
+
} = require('./StackRenderer');
|
|
141
|
+
return /*#__PURE__*/React.createElement(StackRenderer, {
|
|
142
|
+
stack: stackInstance,
|
|
143
|
+
appearance: props.appearance
|
|
144
|
+
});
|
|
145
|
+
};
|
|
134
146
|
}
|
|
135
147
|
seed() {
|
|
136
148
|
const first = this.getFirstRoute();
|
package/lib/module/Router.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { nanoid } from 'nanoid/non-secure';
|
|
4
4
|
import { Platform } from 'react-native';
|
|
5
5
|
import qs from 'query-string';
|
|
6
|
+
import { isModalLikePresentation } from "./types.js";
|
|
6
7
|
function canSwitchToRoute(node) {
|
|
7
8
|
return node !== undefined && typeof node.switchToRoute === 'function';
|
|
8
9
|
}
|
|
@@ -56,6 +57,9 @@ export class Router {
|
|
|
56
57
|
}
|
|
57
58
|
this.recomputeActiveRoute();
|
|
58
59
|
}
|
|
60
|
+
isDebugEnabled() {
|
|
61
|
+
return this.debugEnabled;
|
|
62
|
+
}
|
|
59
63
|
log(message, data) {
|
|
60
64
|
if (this.debugEnabled) {
|
|
61
65
|
if (data !== undefined) {
|
|
@@ -136,6 +140,72 @@ export class Router {
|
|
|
136
140
|
}
|
|
137
141
|
this.popFromActiveStack();
|
|
138
142
|
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Closes the nearest modal or sheet, regardless of navigation depth inside it.
|
|
146
|
+
* Useful when a NavigationStack is rendered inside a modal and you want to
|
|
147
|
+
* close the entire modal from any screen within it.
|
|
148
|
+
*/
|
|
149
|
+
dismiss = () => {
|
|
150
|
+
// Find the nearest modal/sheet item in history (searching from end)
|
|
151
|
+
let modalItem = null;
|
|
152
|
+
for (let i = this.state.history.length - 1; i >= 0; i--) {
|
|
153
|
+
const item = this.state.history[i];
|
|
154
|
+
if (item && isModalLikePresentation(item.options?.stackPresentation)) {
|
|
155
|
+
modalItem = item;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!modalItem) {
|
|
160
|
+
this.log('dismiss: no modal found in history');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.log('dismiss: closing modal', {
|
|
164
|
+
key: modalItem.key,
|
|
165
|
+
routeId: modalItem.routeId,
|
|
166
|
+
stackId: modalItem.stackId,
|
|
167
|
+
presentation: modalItem.options?.stackPresentation
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Handle sheet dismisser if registered
|
|
171
|
+
if (modalItem.options?.stackPresentation === 'sheet') {
|
|
172
|
+
const dismisser = this.sheetDismissers.get(modalItem.key);
|
|
173
|
+
if (dismisser) {
|
|
174
|
+
this.unregisterSheetDismisser(modalItem.key);
|
|
175
|
+
dismisser();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check if modal has a childNode (NavigationStack added via addModal)
|
|
181
|
+
const compiled = this.registry.find(r => r.routeId === modalItem.routeId);
|
|
182
|
+
const childNode = compiled?.childNode;
|
|
183
|
+
if (childNode) {
|
|
184
|
+
// Modal stack: remove all items from child stack AND the modal wrapper item
|
|
185
|
+
const childStackId = childNode.getId();
|
|
186
|
+
this.log('dismiss: closing modal stack', {
|
|
187
|
+
childStackId,
|
|
188
|
+
modalKey: modalItem.key
|
|
189
|
+
});
|
|
190
|
+
const newHistory = this.state.history.filter(item => item.stackId !== childStackId && item.key !== modalItem.key);
|
|
191
|
+
this.setState({
|
|
192
|
+
history: newHistory
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Clear child stack's history cache
|
|
196
|
+
this.stackHistories.delete(childStackId);
|
|
197
|
+
} else {
|
|
198
|
+
// Simple modal: just pop the modal item
|
|
199
|
+
this.applyHistoryChange('pop', modalItem);
|
|
200
|
+
}
|
|
201
|
+
this.recomputeActiveRoute();
|
|
202
|
+
this.emit(this.listeners);
|
|
203
|
+
|
|
204
|
+
// Sync URL on web
|
|
205
|
+
if (this.isWebEnv()) {
|
|
206
|
+
this.syncUrlAfterInternalPop(modalItem);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
139
209
|
getState = () => {
|
|
140
210
|
return this.state;
|
|
141
211
|
};
|
|
@@ -154,6 +224,14 @@ export class Router {
|
|
|
154
224
|
}
|
|
155
225
|
return this.stackHistories.get(stackId) ?? EMPTY_ARRAY;
|
|
156
226
|
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns all history items in navigation order.
|
|
230
|
+
* Useful for rendering all screens including modal stacks.
|
|
231
|
+
*/
|
|
232
|
+
getFullHistory = () => {
|
|
233
|
+
return this.state.history;
|
|
234
|
+
};
|
|
157
235
|
subscribeStack = (stackId, cb) => {
|
|
158
236
|
if (!stackId) return () => {};
|
|
159
237
|
let set = this.stackListeners.get(stackId);
|
|
@@ -172,9 +250,6 @@ export class Router {
|
|
|
172
250
|
getRootStackId() {
|
|
173
251
|
return this.root?.getId();
|
|
174
252
|
}
|
|
175
|
-
getGlobalStackId() {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
253
|
subscribeRoot(listener) {
|
|
179
254
|
this.rootListeners.add(listener);
|
|
180
255
|
return () => this.rootListeners.delete(listener);
|
|
@@ -508,12 +583,23 @@ export class Router {
|
|
|
508
583
|
}
|
|
509
584
|
const newItem = this.createHistoryItem(base, params, query, pathname, passProps);
|
|
510
585
|
this.applyHistoryChange(action, newItem);
|
|
586
|
+
|
|
587
|
+
// Seed child node if present
|
|
588
|
+
if (base.childNode) {
|
|
589
|
+
this.addChildNodeSeedsToHistory(base.routeId);
|
|
590
|
+
}
|
|
511
591
|
};
|
|
512
592
|
base.controller(controllerInput, present);
|
|
513
593
|
return;
|
|
514
594
|
}
|
|
515
595
|
const newItem = this.createHistoryItem(base, params, query, pathname);
|
|
516
596
|
this.applyHistoryChange(action, newItem);
|
|
597
|
+
|
|
598
|
+
// If the matched route has a childNode (e.g., NavigationStack added via addModal),
|
|
599
|
+
// seed the child stack's history so StackRenderer has items to render.
|
|
600
|
+
if (base.childNode) {
|
|
601
|
+
this.addChildNodeSeedsToHistory(base.routeId);
|
|
602
|
+
}
|
|
517
603
|
}
|
|
518
604
|
createHistoryItem(matched, params, query, pathname, passProps) {
|
|
519
605
|
const normalizedParams = params && Object.keys(params).length > 0 ? params : undefined;
|
|
@@ -862,7 +948,7 @@ export class Router {
|
|
|
862
948
|
}
|
|
863
949
|
buildRegistry() {
|
|
864
950
|
this.registry.length = 0;
|
|
865
|
-
const addFromNode = (node, basePath) => {
|
|
951
|
+
const addFromNode = (node, basePath, inheritedOptions) => {
|
|
866
952
|
const normalizedBasePath = this.normalizeBasePath(basePath);
|
|
867
953
|
const baseSpecificity = this.computeBasePathSpecificity(normalizedBasePath);
|
|
868
954
|
const routes = node.getNodeRoutes();
|
|
@@ -870,7 +956,16 @@ export class Router {
|
|
|
870
956
|
if (stackId) {
|
|
871
957
|
this.stackById.set(stackId, node);
|
|
872
958
|
}
|
|
959
|
+
let isFirstRoute = true;
|
|
873
960
|
for (const r of routes) {
|
|
961
|
+
// Merge options: first route inherits parent options (e.g., for nested stacks)
|
|
962
|
+
const mergedOptions = isFirstRoute && inheritedOptions ? {
|
|
963
|
+
...inheritedOptions,
|
|
964
|
+
...r.options
|
|
965
|
+
} : r.options;
|
|
966
|
+
|
|
967
|
+
// Always register the route.
|
|
968
|
+
// If it has a childNode, r.component is already childNode.getRenderer() (set by extractComponent).
|
|
874
969
|
const compiled = {
|
|
875
970
|
routeId: r.routeId,
|
|
876
971
|
path: this.combinePathWithBase(r.path, normalizedBasePath),
|
|
@@ -891,7 +986,7 @@ export class Router {
|
|
|
891
986
|
},
|
|
892
987
|
component: r.component,
|
|
893
988
|
controller: r.controller,
|
|
894
|
-
options:
|
|
989
|
+
options: mergedOptions,
|
|
895
990
|
stackId,
|
|
896
991
|
childNode: r.childNode
|
|
897
992
|
};
|
|
@@ -908,10 +1003,15 @@ export class Router {
|
|
|
908
1003
|
pathnamePattern: compiled.pathnamePattern,
|
|
909
1004
|
isWildcardPath: compiled.isWildcardPath,
|
|
910
1005
|
baseSpecificity: compiled.baseSpecificity,
|
|
911
|
-
stackId
|
|
1006
|
+
stackId,
|
|
1007
|
+
hasChildNode: !!compiled.childNode
|
|
912
1008
|
});
|
|
1009
|
+
isFirstRoute = false;
|
|
1010
|
+
|
|
1011
|
+
// Also register routes from childNode (for navigation inside the nested stack)
|
|
913
1012
|
if (r.childNode) {
|
|
914
1013
|
const nextBaseForChild = r.isWildcardPath ? normalizedBasePath : this.joinPaths(normalizedBasePath, r.pathnamePattern);
|
|
1014
|
+
// Child routes don't inherit parent options - they use their own
|
|
915
1015
|
addFromNode(r.childNode, nextBaseForChild);
|
|
916
1016
|
}
|
|
917
1017
|
}
|
|
@@ -1172,12 +1272,21 @@ export class Router {
|
|
|
1172
1272
|
if (hasQueryPattern) {
|
|
1173
1273
|
spec += 1000;
|
|
1174
1274
|
}
|
|
1275
|
+
|
|
1276
|
+
// Routes with childNode AND modal/sheet presentation are "wrapper" routes
|
|
1277
|
+
// that should take priority over the child stack's own routes when both match.
|
|
1278
|
+
// This ensures addModal('/path', NavigationStack) renders the wrapper modal
|
|
1279
|
+
// and not the child stack's first screen directly.
|
|
1280
|
+
if (r.childNode && isModalLikePresentation(r.options?.stackPresentation)) {
|
|
1281
|
+
spec += 1;
|
|
1282
|
+
}
|
|
1175
1283
|
this.log('matchBaseRoute candidate', {
|
|
1176
1284
|
routeId: r.routeId,
|
|
1177
1285
|
path: r.path,
|
|
1178
1286
|
baseSpecificity: r.baseSpecificity,
|
|
1179
1287
|
adjustedSpecificity: spec,
|
|
1180
1288
|
hasQueryPattern,
|
|
1289
|
+
hasChildNode: !!r.childNode,
|
|
1181
1290
|
stackId: r.stackId
|
|
1182
1291
|
});
|
|
1183
1292
|
if (!best || spec > best.specificity) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { memo, useRef, useLayoutEffect, useMemo, useEffect, Children, isValidElement, Fragment } from 'react';
|
|
3
|
+
import { memo, useRef, useLayoutEffect, useMemo, useEffect, Children, isValidElement, Fragment, useCallback, useContext } from 'react';
|
|
4
4
|
import { useTransitionMap } from 'react-transition-state';
|
|
5
5
|
import { ScreenStackItemsContext, ScreenStackAnimatingContext, useScreenStackConfig } from "./ScreenStackContext.js";
|
|
6
6
|
import { getPresentationTypeClass, computeAnimationType } from "./animationHelpers.js";
|
|
7
|
+
import { RouterContext } from "../RouterContext.js";
|
|
7
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
-
const devLog = (_, __) => {};
|
|
9
9
|
const isScreenStackItemElement = child => {
|
|
10
10
|
if (! /*#__PURE__*/isValidElement(child)) return false;
|
|
11
11
|
const anyProps = child.props;
|
|
@@ -54,6 +54,12 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
54
54
|
transitionTime = 250,
|
|
55
55
|
animated = true
|
|
56
56
|
} = props;
|
|
57
|
+
const router = useContext(RouterContext);
|
|
58
|
+
const debugEnabled = router?.isDebugEnabled() ?? false;
|
|
59
|
+
const devLog = useCallback((msg, data) => {
|
|
60
|
+
if (!debugEnabled) return;
|
|
61
|
+
console.log(msg, data !== undefined ? JSON.stringify(data) : '');
|
|
62
|
+
}, [debugEnabled]);
|
|
57
63
|
devLog('[ScreenStack] Render', {
|
|
58
64
|
transitionTime,
|
|
59
65
|
animated,
|
|
@@ -81,7 +87,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
81
87
|
stackChildrenLength: stackItems.length
|
|
82
88
|
});
|
|
83
89
|
return stackItems;
|
|
84
|
-
}, [children]);
|
|
90
|
+
}, [children, devLog]);
|
|
85
91
|
const routeKeys = useMemo(() => {
|
|
86
92
|
const keys = stackChildren.map(child => {
|
|
87
93
|
const item = child.props.item;
|
|
@@ -89,7 +95,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
89
95
|
});
|
|
90
96
|
devLog('[ScreenStack] routeKeys', keys);
|
|
91
97
|
return keys;
|
|
92
|
-
}, [stackChildren]);
|
|
98
|
+
}, [devLog, stackChildren]);
|
|
93
99
|
const childMap = useMemo(() => {
|
|
94
100
|
const map = new Map(childMapRef.current);
|
|
95
101
|
for (const child of stackChildren) {
|
|
@@ -103,7 +109,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
103
109
|
keys: Array.from(map.keys())
|
|
104
110
|
});
|
|
105
111
|
return map;
|
|
106
|
-
}, [stackChildren]);
|
|
112
|
+
}, [devLog, stackChildren]);
|
|
107
113
|
const {
|
|
108
114
|
stateMap,
|
|
109
115
|
toggle,
|
|
@@ -166,7 +172,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
166
172
|
exitingKeys
|
|
167
173
|
});
|
|
168
174
|
return result;
|
|
169
|
-
}, [routeKeys, stateMapEntries]);
|
|
175
|
+
}, [devLog, routeKeys, stateMapEntries]);
|
|
170
176
|
const containerClassName = useMemo(() => {
|
|
171
177
|
return 'screen-stack';
|
|
172
178
|
}, []);
|
|
@@ -227,7 +233,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
227
233
|
}
|
|
228
234
|
lastDirectionRef.current = direction;
|
|
229
235
|
devLog('[ScreenStack] === LIFECYCLE EFFECT END ===');
|
|
230
|
-
}, [routeKeys, direction, setItem, toggle, stateMapEntries, stateMap, animateFirstScreenAfterEmpty]);
|
|
236
|
+
}, [routeKeys, direction, setItem, toggle, stateMapEntries, stateMap, animateFirstScreenAfterEmpty, devLog]);
|
|
231
237
|
useLayoutEffect(() => {
|
|
232
238
|
devLog('[ScreenStack] === CLEANUP EFFECT START ===');
|
|
233
239
|
const routeKeySet = new Set(routeKeys);
|
|
@@ -253,7 +259,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
devLog('[ScreenStack] === CLEANUP EFFECT END ===');
|
|
256
|
-
}, [routeKeys, stateMapEntries, deleteItem]);
|
|
262
|
+
}, [routeKeys, stateMapEntries, deleteItem, devLog]);
|
|
257
263
|
useEffect(() => {
|
|
258
264
|
if (!isInitialMountRef.current) return;
|
|
259
265
|
const hasMountedItem = stateMapEntries.some(([, st]) => st.isMounted);
|
|
@@ -271,7 +277,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
271
277
|
isInitialMountRef.current = false;
|
|
272
278
|
devLog('[ScreenStack] Initial mount completed');
|
|
273
279
|
}
|
|
274
|
-
}, [stateMapEntries, routeKeys.length, animateFirstScreenAfterEmpty]);
|
|
280
|
+
}, [stateMapEntries, routeKeys.length, animateFirstScreenAfterEmpty, devLog]);
|
|
275
281
|
|
|
276
282
|
// Clear suppression key once it is no longer the top screen (so it can animate normally as
|
|
277
283
|
// a background when new screens are pushed).
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { isModalLikePresentation } from "../types.js";
|
|
3
4
|
export function getPresentationTypeClass(presentation) {
|
|
4
5
|
switch (presentation) {
|
|
5
6
|
case 'push':
|
|
6
7
|
return 'push';
|
|
7
8
|
case 'modal':
|
|
8
9
|
return 'modal';
|
|
10
|
+
case 'modalRight':
|
|
11
|
+
return 'modal-right';
|
|
9
12
|
case 'transparentModal':
|
|
10
13
|
return 'transparent-modal';
|
|
11
14
|
case 'containedModal':
|
|
@@ -40,7 +43,7 @@ export function computeAnimationType(_key, isInStack, isTop, direction, presenta
|
|
|
40
43
|
return 'none';
|
|
41
44
|
}
|
|
42
45
|
const isEntering = isInStack && isTop;
|
|
43
|
-
const isModalLike =
|
|
46
|
+
const isModalLike = isModalLikePresentation(presentation);
|
|
44
47
|
if (isModalLike) {
|
|
45
48
|
if (!isInStack) {
|
|
46
49
|
return getAnimationTypeForPresentation(presentation, false, direction);
|
|
@@ -48,7 +51,14 @@ export function computeAnimationType(_key, isInStack, isTop, direction, presenta
|
|
|
48
51
|
if (isEntering) {
|
|
49
52
|
return getAnimationTypeForPresentation(presentation, true, direction);
|
|
50
53
|
}
|
|
51
|
-
|
|
54
|
+
|
|
55
|
+
// Modal-like screen that's NOT top (background) - animate like push
|
|
56
|
+
// This happens when navigating inside a modal stack
|
|
57
|
+
if (direction === 'forward') {
|
|
58
|
+
return 'push-background';
|
|
59
|
+
} else {
|
|
60
|
+
return 'pop-background';
|
|
61
|
+
}
|
|
52
62
|
}
|
|
53
63
|
if (!isInStack) {
|
|
54
64
|
if (direction === 'forward') {
|
|
@@ -17,6 +17,9 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
17
17
|
stackPresentation,
|
|
18
18
|
...screenProps
|
|
19
19
|
} = item.options || {};
|
|
20
|
+
|
|
21
|
+
// On native, modalRight behaves as regular modal
|
|
22
|
+
const nativePresentation = stackPresentation === 'modalRight' ? 'modal' : stackPresentation;
|
|
20
23
|
const route = {
|
|
21
24
|
presentation: stackPresentation ?? 'push',
|
|
22
25
|
params: item.params,
|
|
@@ -54,7 +57,7 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
54
57
|
style: StyleSheet.absoluteFill,
|
|
55
58
|
contentStyle: appearance?.screen,
|
|
56
59
|
headerConfig: headerConfig,
|
|
57
|
-
stackPresentation:
|
|
60
|
+
stackPresentation: nativePresentation,
|
|
58
61
|
stackAnimation: stackAnimation ?? item.options?.stackAnimation,
|
|
59
62
|
children: /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
|
|
60
63
|
value: route,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { RouteLocalContext, useRouter } from "../RouterContext.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isModalLikePresentation } from "../types.js";
|
|
5
|
+
import { memo, useMemo, useCallback } from 'react';
|
|
5
6
|
import { StyleSheet, View } from 'react-native';
|
|
6
7
|
import { useScreenStackItemsContext } from "../ScreenStack/ScreenStackContext.js";
|
|
7
8
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
-
const devLog = (_, __) => {};
|
|
9
9
|
export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
10
10
|
item,
|
|
11
11
|
appearance,
|
|
@@ -13,6 +13,11 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
13
13
|
}) => {
|
|
14
14
|
const itemsContext = useScreenStackItemsContext();
|
|
15
15
|
const router = useRouter();
|
|
16
|
+
const debugEnabled = router.isDebugEnabled();
|
|
17
|
+
const devLog = useCallback((msg, data) => {
|
|
18
|
+
if (!debugEnabled) return;
|
|
19
|
+
console.log(msg, data !== undefined ? JSON.stringify(data) : '');
|
|
20
|
+
}, [debugEnabled]);
|
|
16
21
|
const key = item.key;
|
|
17
22
|
const itemState = itemsContext.items[key];
|
|
18
23
|
const presentationType = itemState?.presentationType;
|
|
@@ -21,7 +26,7 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
21
26
|
const transitionStatus = itemState?.transitionStatus;
|
|
22
27
|
const zIndex = itemState?.zIndex ?? 0;
|
|
23
28
|
const presentation = item.options?.stackPresentation ?? 'push';
|
|
24
|
-
const isModalLike =
|
|
29
|
+
const isModalLike = isModalLikePresentation(presentation);
|
|
25
30
|
const className = useMemo(() => {
|
|
26
31
|
const classes = ['screen-stack-item'];
|
|
27
32
|
if (presentationType) {
|
|
@@ -46,12 +51,20 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
46
51
|
className: classes.join(' ')
|
|
47
52
|
});
|
|
48
53
|
return classes.join(' ');
|
|
49
|
-
}, [presentationType, animationType, transitionStatus, phase, item.key, item.path]);
|
|
54
|
+
}, [presentationType, animationType, transitionStatus, phase, devLog, item.key, item.path]);
|
|
50
55
|
const mergedStyle = useMemo(() => ({
|
|
51
56
|
flex: 1,
|
|
52
57
|
...style,
|
|
53
58
|
zIndex
|
|
54
59
|
}), [style, zIndex]);
|
|
60
|
+
const modalContainerStyle = useMemo(() => {
|
|
61
|
+
if (!isModalLike || !item.options?.maxWidth) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
maxWidth: `${item.options.maxWidth}px`
|
|
66
|
+
};
|
|
67
|
+
}, [isModalLike, item.options?.maxWidth]);
|
|
55
68
|
const value = {
|
|
56
69
|
presentation,
|
|
57
70
|
params: item.params,
|
|
@@ -67,9 +80,10 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
|
67
80
|
className: className,
|
|
68
81
|
children: [isModalLike && /*#__PURE__*/_jsx("div", {
|
|
69
82
|
className: "stack-modal-overlay",
|
|
70
|
-
onClick: () => router.
|
|
83
|
+
onClick: () => router.dismiss()
|
|
71
84
|
}), /*#__PURE__*/_jsx("div", {
|
|
72
85
|
className: isModalLike ? 'stack-modal-container' : 'stack-screen-container',
|
|
86
|
+
style: modalContainerStyle,
|
|
73
87
|
children: appearance?.screen ? /*#__PURE__*/_jsx(View, {
|
|
74
88
|
style: [appearance?.screen, styles.flex],
|
|
75
89
|
children: /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
|
|
@@ -1,85 +1,78 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ScreenStackItem } from "../ScreenStackItem/index.js";
|
|
4
|
+
import { ScreenStack } from "../ScreenStack/index.js";
|
|
4
5
|
import { SplitViewContext } from "./SplitViewContext.js";
|
|
5
6
|
import { useRouter } from "../RouterContext.js";
|
|
6
|
-
import { memo, useCallback, useSyncExternalStore } from 'react';
|
|
7
|
-
import { StyleSheet
|
|
8
|
-
import { jsx as _jsx
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const subscribe = useCallback(cb => router.subscribeStack(stackId, cb), [router, stackId]);
|
|
17
|
-
const get = useCallback(() => router.getStackHistory(stackId), [router, stackId]);
|
|
18
|
-
const history = useSyncExternalStore(subscribe, get, get);
|
|
19
|
-
let historyToRender = history;
|
|
20
|
-
if (fallbackToFirstRoute && historyToRender.length === 0) {
|
|
21
|
-
const first = stack.getFirstRoute();
|
|
22
|
-
if (first) {
|
|
23
|
-
const activePath = router.getActiveRoute()?.path;
|
|
24
|
-
historyToRender = [{
|
|
25
|
-
key: `splitview-seed-${stackId}`,
|
|
26
|
-
routeId: first.routeId,
|
|
27
|
-
component: first.component,
|
|
28
|
-
options: first.options,
|
|
29
|
-
stackId,
|
|
30
|
-
pattern: first.path,
|
|
31
|
-
path: activePath ?? first.path
|
|
32
|
-
}];
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return /*#__PURE__*/_jsx(StackRenderer, {
|
|
36
|
-
appearance: appearance,
|
|
37
|
-
stack: stack,
|
|
38
|
-
history: historyToRender
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
StackSliceRenderer.displayName = 'SplitViewStackSliceRendererNative';
|
|
7
|
+
import { memo, useCallback, useSyncExternalStore, useMemo } from 'react';
|
|
8
|
+
import { StyleSheet } from 'react-native';
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
/**
|
|
11
|
+
* On native (iPhone), SplitView renders primary and secondary screens
|
|
12
|
+
* in a SINGLE ScreenStack to get native push/pop animations.
|
|
13
|
+
*
|
|
14
|
+
* The combined history is: [...primaryHistory, ...secondaryHistory]
|
|
15
|
+
* This way, navigating from primary to secondary is a native push.
|
|
16
|
+
*/
|
|
42
17
|
export const RenderSplitView = /*#__PURE__*/memo(({
|
|
43
18
|
splitView,
|
|
44
19
|
appearance
|
|
45
20
|
}) => {
|
|
46
21
|
const router = useRouter();
|
|
22
|
+
|
|
23
|
+
// Subscribe to primary stack
|
|
24
|
+
const primaryId = splitView.primary.getId();
|
|
25
|
+
const subscribePrimary = useCallback(cb => router.subscribeStack(primaryId, cb), [router, primaryId]);
|
|
26
|
+
const getPrimary = useCallback(() => router.getStackHistory(primaryId), [router, primaryId]);
|
|
27
|
+
const primaryHistory = useSyncExternalStore(subscribePrimary, getPrimary, getPrimary);
|
|
28
|
+
|
|
29
|
+
// Subscribe to secondary stack
|
|
47
30
|
const secondaryId = splitView.secondary.getId();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const secondaryHistory = useSyncExternalStore(
|
|
51
|
-
|
|
31
|
+
const subscribeSecondary = useCallback(cb => router.subscribeStack(secondaryId, cb), [router, secondaryId]);
|
|
32
|
+
const getSecondary = useCallback(() => router.getStackHistory(secondaryId), [router, secondaryId]);
|
|
33
|
+
const secondaryHistory = useSyncExternalStore(subscribeSecondary, getSecondary, getSecondary);
|
|
34
|
+
|
|
35
|
+
// Fallback: if primary is empty, seed with first route
|
|
36
|
+
const primaryHistoryToRender = useMemo(() => {
|
|
37
|
+
if (primaryHistory.length > 0) {
|
|
38
|
+
return primaryHistory;
|
|
39
|
+
}
|
|
40
|
+
const first = splitView.primary.getFirstRoute();
|
|
41
|
+
if (!first) return [];
|
|
42
|
+
const activePath = router.getActiveRoute()?.path;
|
|
43
|
+
return [{
|
|
44
|
+
key: `splitview-seed-${primaryId}`,
|
|
45
|
+
routeId: first.routeId,
|
|
46
|
+
component: first.component,
|
|
47
|
+
options: first.options,
|
|
48
|
+
stackId: primaryId,
|
|
49
|
+
pattern: first.path,
|
|
50
|
+
path: activePath ?? first.path
|
|
51
|
+
}];
|
|
52
|
+
}, [primaryHistory, splitView.primary, primaryId, router]);
|
|
53
|
+
|
|
54
|
+
// Combine histories: primary screens first, then secondary screens on top
|
|
55
|
+
// This gives native push animation when navigating from primary to secondary
|
|
56
|
+
const combinedHistory = useMemo(() => {
|
|
57
|
+
return [...primaryHistoryToRender, ...secondaryHistory];
|
|
58
|
+
}, [primaryHistoryToRender, secondaryHistory]);
|
|
59
|
+
|
|
60
|
+
// Use primary stack ID for the combined ScreenStack
|
|
61
|
+
// (secondary items will animate as if pushed onto this stack)
|
|
52
62
|
return /*#__PURE__*/_jsx(SplitViewContext.Provider, {
|
|
53
63
|
value: splitView,
|
|
54
|
-
children: /*#__PURE__*/
|
|
64
|
+
children: /*#__PURE__*/_jsx(ScreenStack, {
|
|
55
65
|
style: styles.container,
|
|
56
|
-
children:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
stack: splitView.primary,
|
|
62
|
-
fallbackToFirstRoute: true
|
|
63
|
-
})
|
|
64
|
-
}), hasSecondary ? /*#__PURE__*/_jsx(View, {
|
|
65
|
-
style: styles.secondary,
|
|
66
|
-
children: /*#__PURE__*/_jsx(StackSliceRenderer, {
|
|
67
|
-
appearance: appearance,
|
|
68
|
-
stack: splitView.secondary
|
|
69
|
-
})
|
|
70
|
-
}) : null]
|
|
66
|
+
children: combinedHistory.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
67
|
+
appearance: appearance,
|
|
68
|
+
stackId: item.stackId,
|
|
69
|
+
item: item
|
|
70
|
+
}, `splitview-${item.key}`))
|
|
71
71
|
})
|
|
72
72
|
});
|
|
73
73
|
});
|
|
74
74
|
const styles = StyleSheet.create({
|
|
75
75
|
container: {
|
|
76
76
|
flex: 1
|
|
77
|
-
},
|
|
78
|
-
primary: {
|
|
79
|
-
flex: 1
|
|
80
|
-
},
|
|
81
|
-
secondary: {
|
|
82
|
-
...StyleSheet.absoluteFillObject,
|
|
83
|
-
zIndex: 2
|
|
84
77
|
}
|
|
85
78
|
});
|
|
@@ -160,6 +160,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
160
160
|
tabBar,
|
|
161
161
|
appearance = {}
|
|
162
162
|
}) => {
|
|
163
|
+
const router = useRouter();
|
|
163
164
|
const subscribe = useCallback(cb => tabBar.subscribe(cb), [tabBar]);
|
|
164
165
|
const snapshot = useSyncExternalStore(subscribe, tabBar.getState, tabBar.getState);
|
|
165
166
|
const {
|
|
@@ -182,11 +183,82 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
182
183
|
const onNativeFocusChange = useCallback(event => {
|
|
183
184
|
const tabKey = event.nativeEvent.tabKey;
|
|
184
185
|
const tabIndex = tabs.findIndex(route => route.tabKey === tabKey);
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
if (tabIndex === -1) return;
|
|
187
|
+
const targetTab = tabs[tabIndex];
|
|
188
|
+
if (!targetTab) return;
|
|
189
|
+
const targetStack = tabBar.stacks[targetTab.tabKey];
|
|
190
|
+
const targetNode = tabBar.nodes[targetTab.tabKey];
|
|
191
|
+
|
|
192
|
+
// Update TabBar UI state
|
|
193
|
+
if (tabIndex !== index) {
|
|
194
|
+
tabBar.onIndexChange(tabIndex);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Navigate to the target stack's first route if needed
|
|
198
|
+
if (targetStack) {
|
|
199
|
+
const stackId = targetStack.getId();
|
|
200
|
+
const stackHistory = router.getStackHistory(stackId);
|
|
201
|
+
// Only navigate if stack is empty (first visit)
|
|
202
|
+
if (stackHistory.length === 0) {
|
|
203
|
+
const firstRoute = targetStack.getFirstRoute();
|
|
204
|
+
if (firstRoute?.path) {
|
|
205
|
+
router.navigate(firstRoute.path);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (targetNode) {
|
|
209
|
+
// For nodes like SplitView, check if we need to seed it
|
|
210
|
+
const nodeId = targetNode.getId?.();
|
|
211
|
+
if (nodeId) {
|
|
212
|
+
const nodeHistory = router.getStackHistory(nodeId);
|
|
213
|
+
if (nodeHistory.length === 0) {
|
|
214
|
+
const seed = targetNode.seed?.();
|
|
215
|
+
if (seed?.path) {
|
|
216
|
+
const prefix = targetTab.tabPrefix ?? '';
|
|
217
|
+
const fullPath = prefix && !seed.path.startsWith(prefix) ? `${prefix}${seed.path.startsWith('/') ? '' : '/'}${seed.path}` : seed.path;
|
|
218
|
+
router.navigate(fullPath);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}, [tabs, tabBar, index, router]);
|
|
187
224
|
const onTabPress = useCallback(nextIndex => {
|
|
188
|
-
|
|
189
|
-
|
|
225
|
+
const targetTab = tabs[nextIndex];
|
|
226
|
+
if (!targetTab) return;
|
|
227
|
+
const targetStack = tabBar.stacks[targetTab.tabKey];
|
|
228
|
+
const targetNode = tabBar.nodes[targetTab.tabKey];
|
|
229
|
+
|
|
230
|
+
// Update TabBar UI state
|
|
231
|
+
if (nextIndex !== index) {
|
|
232
|
+
tabBar.onIndexChange(nextIndex);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Navigate to the target stack's first route if needed
|
|
236
|
+
if (targetStack) {
|
|
237
|
+
const stackId = targetStack.getId();
|
|
238
|
+
const stackHistory = router.getStackHistory(stackId);
|
|
239
|
+
// Only navigate if stack is empty (first visit)
|
|
240
|
+
if (stackHistory.length === 0) {
|
|
241
|
+
const firstRoute = targetStack.getFirstRoute();
|
|
242
|
+
if (firstRoute?.path) {
|
|
243
|
+
router.navigate(firstRoute.path);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else if (targetNode) {
|
|
247
|
+
// For nodes like SplitView, check if we need to seed it
|
|
248
|
+
const nodeId = targetNode.getId?.();
|
|
249
|
+
if (nodeId) {
|
|
250
|
+
const nodeHistory = router.getStackHistory(nodeId);
|
|
251
|
+
if (nodeHistory.length === 0) {
|
|
252
|
+
const seed = targetNode.seed?.();
|
|
253
|
+
if (seed?.path) {
|
|
254
|
+
const prefix = targetTab.tabPrefix ?? '';
|
|
255
|
+
const fullPath = prefix && !seed.path.startsWith(prefix) ? `${prefix}${seed.path.startsWith('/') ? '' : '/'}${seed.path}` : seed.path;
|
|
256
|
+
router.navigate(fullPath);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}, [tabs, tabBar, index, router]);
|
|
190
262
|
const containerProps = {
|
|
191
263
|
tabBarBackgroundColor: backgroundColor,
|
|
192
264
|
tabBarItemTitleFontFamily: title?.fontFamily,
|
package/lib/module/styles.css
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
/* Show overlay only for modal-like presentations.
|
|
49
49
|
IMPORTANT: use direct child (>) to avoid collapsing overlay from nested stacks,
|
|
50
50
|
when modal ScreenStackItem is inside push ScreenStackItem container. */
|
|
51
|
-
.screen-stack-item:not(.modal):not(.contained-modal):not(.fullscreen-modal):not(.formsheet):not(.pagesheet):not(.sheet)
|
|
51
|
+
.screen-stack-item:not(.modal):not(.modal-right):not(.contained-modal):not(.fullscreen-modal):not(.formsheet):not(.pagesheet):not(.sheet)
|
|
52
52
|
> .stack-modal-overlay {
|
|
53
53
|
display: none;
|
|
54
54
|
}
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
|
|
76
76
|
/* Overlay in initial state — transparent */
|
|
77
77
|
.screen-stack-item.modal.transition-preEnter > .stack-modal-overlay,
|
|
78
|
+
.screen-stack-item.modal-right.transition-preEnter > .stack-modal-overlay,
|
|
78
79
|
.screen-stack-item.contained-modal.transition-preEnter > .stack-modal-overlay,
|
|
79
80
|
.screen-stack-item.fullscreen-modal.transition-preEnter > .stack-modal-overlay,
|
|
80
81
|
.screen-stack-item.formsheet.transition-preEnter > .stack-modal-overlay,
|
|
@@ -88,6 +89,8 @@
|
|
|
88
89
|
/* Overlay on enter — start animation */
|
|
89
90
|
.screen-stack-item.modal.transition-entering > .stack-modal-overlay,
|
|
90
91
|
.screen-stack-item.modal.phase-active.transition-preEnter > .stack-modal-overlay,
|
|
92
|
+
.screen-stack-item.modal-right.transition-entering > .stack-modal-overlay,
|
|
93
|
+
.screen-stack-item.modal-right.phase-active.transition-preEnter > .stack-modal-overlay,
|
|
91
94
|
.screen-stack-item.contained-modal.transition-entering > .stack-modal-overlay,
|
|
92
95
|
.screen-stack-item.contained-modal.phase-active.transition-preEnter > .stack-modal-overlay,
|
|
93
96
|
.screen-stack-item.fullscreen-modal.transition-entering > .stack-modal-overlay,
|
|
@@ -106,6 +109,8 @@
|
|
|
106
109
|
/* For modal-like in active / entered states — fully visible */
|
|
107
110
|
.screen-stack-item.modal.phase-active > .stack-modal-overlay,
|
|
108
111
|
.screen-stack-item.modal.transition-entered > .stack-modal-overlay,
|
|
112
|
+
.screen-stack-item.modal-right.phase-active > .stack-modal-overlay,
|
|
113
|
+
.screen-stack-item.modal-right.transition-entered > .stack-modal-overlay,
|
|
109
114
|
.screen-stack-item.contained-modal.phase-active > .stack-modal-overlay,
|
|
110
115
|
.screen-stack-item.contained-modal.transition-entered > .stack-modal-overlay,
|
|
111
116
|
.screen-stack-item.fullscreen-modal.phase-active > .stack-modal-overlay,
|
|
@@ -124,6 +129,8 @@
|
|
|
124
129
|
/* Overlay on modal-like close — disappearance animation */
|
|
125
130
|
.screen-stack-item.modal.phase-exiting > .stack-modal-overlay,
|
|
126
131
|
.screen-stack-item.modal.transition-exiting > .stack-modal-overlay,
|
|
132
|
+
.screen-stack-item.modal-right.phase-exiting > .stack-modal-overlay,
|
|
133
|
+
.screen-stack-item.modal-right.transition-exiting > .stack-modal-overlay,
|
|
127
134
|
.screen-stack-item.contained-modal.phase-exiting > .stack-modal-overlay,
|
|
128
135
|
.screen-stack-item.contained-modal.transition-exiting > .stack-modal-overlay,
|
|
129
136
|
.screen-stack-item.fullscreen-modal.phase-exiting > .stack-modal-overlay,
|
|
@@ -153,6 +160,7 @@
|
|
|
153
160
|
|
|
154
161
|
/* Modal-like content container always above overlay */
|
|
155
162
|
.screen-stack-item.modal .stack-modal-container,
|
|
163
|
+
.screen-stack-item.modal-right .stack-modal-container,
|
|
156
164
|
.screen-stack-item.contained-modal .stack-modal-container,
|
|
157
165
|
.screen-stack-item.fullscreen-modal .stack-modal-container,
|
|
158
166
|
.screen-stack-item.formsheet .stack-modal-container,
|
|
@@ -202,6 +210,7 @@
|
|
|
202
210
|
|
|
203
211
|
/* IMPORTANT: modal elements should not move like regular screens */
|
|
204
212
|
.screen-stack > .screen-stack-item.modal,
|
|
213
|
+
.screen-stack > .screen-stack-item.modal-right,
|
|
205
214
|
.screen-stack > .screen-stack-item.sheet {
|
|
206
215
|
transform: translate3d(0, 0, 0) !important;
|
|
207
216
|
}
|
|
@@ -229,7 +238,12 @@
|
|
|
229
238
|
|
|
230
239
|
/* PUSH BACKGROUND - background screen shifts left */
|
|
231
240
|
/* Important: use !important to override general rules for inactive */
|
|
232
|
-
.screen-stack-item.push.push-background
|
|
241
|
+
.screen-stack-item.push.push-background,
|
|
242
|
+
.screen-stack-item.modal.push-background,
|
|
243
|
+
.screen-stack-item.modal-right.push-background,
|
|
244
|
+
.screen-stack-item.sheet.push-background,
|
|
245
|
+
.screen-stack-item.formsheet.push-background,
|
|
246
|
+
.screen-stack-item.pagesheet.push-background {
|
|
233
247
|
transform: translateX(-25%) !important;
|
|
234
248
|
transition:
|
|
235
249
|
transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
@@ -238,7 +252,12 @@
|
|
|
238
252
|
|
|
239
253
|
/* POP BACKGROUND - background screen on pop stays at -25% */
|
|
240
254
|
/* Background elements on pop get pop-background instead of none, to preserve -25% position */
|
|
241
|
-
.screen-stack-item.push.pop-background
|
|
255
|
+
.screen-stack-item.push.pop-background,
|
|
256
|
+
.screen-stack-item.modal.pop-background,
|
|
257
|
+
.screen-stack-item.modal-right.pop-background,
|
|
258
|
+
.screen-stack-item.sheet.pop-background,
|
|
259
|
+
.screen-stack-item.formsheet.pop-background,
|
|
260
|
+
.screen-stack-item.pagesheet.pop-background {
|
|
242
261
|
transform: translateX(-25%) !important;
|
|
243
262
|
/* Don't set transition, so element stays in place without animation */
|
|
244
263
|
filter: none;
|
|
@@ -294,7 +313,8 @@
|
|
|
294
313
|
/* ==================== MOBILE MODAL (<= 639px) — bottom sheet ==================== */
|
|
295
314
|
@media (max-width: 639px) {
|
|
296
315
|
/* Inner container for modal — bottom sheet full width with top border radius */
|
|
297
|
-
.screen-stack-item.modal .stack-modal-container
|
|
316
|
+
.screen-stack-item.modal .stack-modal-container,
|
|
317
|
+
.screen-stack-item.modal-right .stack-modal-container {
|
|
298
318
|
width: 100%;
|
|
299
319
|
margin: 0;
|
|
300
320
|
border-radius: 0;
|
|
@@ -311,19 +331,23 @@
|
|
|
311
331
|
}
|
|
312
332
|
|
|
313
333
|
/* MODAL ENTER - modal enters (bottom to top) */
|
|
314
|
-
.screen-stack-item.modal.modal-enter.transition-preEnter .stack-modal-container
|
|
334
|
+
.screen-stack-item.modal.modal-enter.transition-preEnter .stack-modal-container,
|
|
335
|
+
.screen-stack-item.modal-right.modal-right-enter.transition-preEnter .stack-modal-container {
|
|
315
336
|
transform: translateY(100%);
|
|
316
337
|
filter: none;
|
|
317
338
|
}
|
|
318
339
|
|
|
319
340
|
.screen-stack-item.modal.modal-enter.transition-entering .stack-modal-container,
|
|
320
|
-
.screen-stack-item.modal.modal-enter.transition-entered .stack-modal-container
|
|
341
|
+
.screen-stack-item.modal.modal-enter.transition-entered .stack-modal-container,
|
|
342
|
+
.screen-stack-item.modal-right.modal-right-enter.transition-entering .stack-modal-container,
|
|
343
|
+
.screen-stack-item.modal-right.modal-right-enter.transition-entered .stack-modal-container {
|
|
321
344
|
transform: translateY(0);
|
|
322
345
|
filter: none;
|
|
323
346
|
}
|
|
324
347
|
|
|
325
348
|
/* MODAL EXIT - modal closes (exits downward) */
|
|
326
|
-
.screen-stack-item.modal.modal-exit.transition-exiting .stack-modal-container
|
|
349
|
+
.screen-stack-item.modal.modal-exit.transition-exiting .stack-modal-container,
|
|
350
|
+
.screen-stack-item.modal-right.modal-right-exit.transition-exiting .stack-modal-container {
|
|
327
351
|
transform: translateY(100%);
|
|
328
352
|
filter: none;
|
|
329
353
|
transition:
|
|
@@ -370,10 +394,57 @@
|
|
|
370
394
|
}
|
|
371
395
|
}
|
|
372
396
|
|
|
373
|
-
/* ==================== DESKTOP MODAL (>= 640px) —
|
|
397
|
+
/* ==================== DESKTOP MODAL (>= 640px) — centered card ==================== */
|
|
374
398
|
@media (min-width: 640px) {
|
|
375
|
-
/* Inner container for modal —
|
|
399
|
+
/* Inner container for modal — centered card 600px width, 75% height */
|
|
376
400
|
.screen-stack-item.modal .stack-modal-container {
|
|
401
|
+
width: 600px;
|
|
402
|
+
max-width: calc(100% - 48px);
|
|
403
|
+
height: 75vh;
|
|
404
|
+
max-height: calc(100% - 48px);
|
|
405
|
+
margin: auto; /* center in parent */
|
|
406
|
+
border-radius: 38px;
|
|
407
|
+
overflow: hidden;
|
|
408
|
+
background: #fff;
|
|
409
|
+
|
|
410
|
+
transform: translateY(0);
|
|
411
|
+
transition:
|
|
412
|
+
transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
413
|
+
filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* Center the modal container using flexbox on parent */
|
|
417
|
+
.screen-stack-item.modal {
|
|
418
|
+
display: flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
justify-content: center;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* MODAL ENTER - modal enters (bottom to center) */
|
|
424
|
+
.screen-stack-item.modal.modal-enter.transition-preEnter .stack-modal-container {
|
|
425
|
+
transform: translateY(100vh);
|
|
426
|
+
filter: none;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.screen-stack-item.modal.modal-enter.transition-entering .stack-modal-container,
|
|
430
|
+
.screen-stack-item.modal.modal-enter.transition-entered .stack-modal-container {
|
|
431
|
+
transform: translateY(0);
|
|
432
|
+
filter: none;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* MODAL EXIT - modal closes (exits downward) */
|
|
436
|
+
.screen-stack-item.modal.modal-exit.transition-exiting .stack-modal-container {
|
|
437
|
+
transform: translateY(100vh);
|
|
438
|
+
filter: none;
|
|
439
|
+
transition:
|
|
440
|
+
transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
441
|
+
filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* ==================== MODAL-RIGHT — slide from right (previous modal behavior) ==================== */
|
|
445
|
+
|
|
446
|
+
/* Inner container for modal-right — right sheet 393px */
|
|
447
|
+
.screen-stack-item.modal-right .stack-modal-container {
|
|
377
448
|
width: 393px;
|
|
378
449
|
max-width: calc(100% - 36px);
|
|
379
450
|
height: calc(100% - 36px);
|
|
@@ -381,7 +452,7 @@
|
|
|
381
452
|
margin-left: auto; /* align to right edge */
|
|
382
453
|
border-radius: 16px;
|
|
383
454
|
overflow: hidden;
|
|
384
|
-
background: #fff;
|
|
455
|
+
background: #fff;
|
|
385
456
|
|
|
386
457
|
transform: translateX(0);
|
|
387
458
|
transition:
|
|
@@ -389,20 +460,20 @@
|
|
|
389
460
|
filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
390
461
|
}
|
|
391
462
|
|
|
392
|
-
/* MODAL ENTER - modal enters (right to left) */
|
|
393
|
-
.screen-stack-item.modal.modal-enter.transition-preEnter .stack-modal-container {
|
|
463
|
+
/* MODAL-RIGHT ENTER - modal enters (right to left) */
|
|
464
|
+
.screen-stack-item.modal-right.modal-right-enter.transition-preEnter .stack-modal-container {
|
|
394
465
|
transform: translateX(100%);
|
|
395
466
|
filter: none;
|
|
396
467
|
}
|
|
397
468
|
|
|
398
|
-
.screen-stack-item.modal.modal-enter.transition-entering .stack-modal-container,
|
|
399
|
-
.screen-stack-item.modal.modal-enter.transition-entered .stack-modal-container {
|
|
469
|
+
.screen-stack-item.modal-right.modal-right-enter.transition-entering .stack-modal-container,
|
|
470
|
+
.screen-stack-item.modal-right.modal-right-enter.transition-entered .stack-modal-container {
|
|
400
471
|
transform: translateX(0);
|
|
401
472
|
filter: none;
|
|
402
473
|
}
|
|
403
474
|
|
|
404
|
-
/* MODAL EXIT - modal closes (exits to right) */
|
|
405
|
-
.screen-stack-item.modal.modal-exit.transition-exiting .stack-modal-container {
|
|
475
|
+
/* MODAL-RIGHT EXIT - modal closes (exits to right) */
|
|
476
|
+
.screen-stack-item.modal-right.modal-right-exit.transition-exiting .stack-modal-container {
|
|
406
477
|
transform: translateX(100%);
|
|
407
478
|
filter: none;
|
|
408
479
|
transition:
|
|
@@ -439,6 +510,10 @@
|
|
|
439
510
|
.screen-stack:has(> .screen-stack-item.modal.phase-active)
|
|
440
511
|
> .screen-stack-item.push.phase-inactive,
|
|
441
512
|
.screen-stack:has(> .screen-stack-item.modal.phase-active)
|
|
513
|
+
> .screen-stack-item.push.phase-inactive.transition-entered,
|
|
514
|
+
.screen-stack:has(> .screen-stack-item.modal-right.phase-active)
|
|
515
|
+
> .screen-stack-item.push.phase-inactive,
|
|
516
|
+
.screen-stack:has(> .screen-stack-item.modal-right.phase-active)
|
|
442
517
|
> .screen-stack-item.push.phase-inactive.transition-entered {
|
|
443
518
|
transform: translate3d(0, 0, 0) !important;
|
|
444
519
|
filter: none;
|
package/lib/module/types.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Presentations that behave like modals (overlay on top of content).
|
|
5
|
+
*/
|
|
6
|
+
export const MODAL_LIKE_PRESENTATIONS = new Set(['modal', 'modalRight', 'transparentModal', 'containedModal', 'containedTransparentModal', 'fullScreenModal', 'formSheet', 'pageSheet', 'sheet']);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a presentation type is modal-like (renders as overlay).
|
|
10
|
+
*/
|
|
11
|
+
export function isModalLikePresentation(presentation) {
|
|
12
|
+
return presentation !== undefined && MODAL_LIKE_PRESENTATIONS.has(presentation);
|
|
13
|
+
}
|
|
@@ -4,6 +4,14 @@ interface NavigationProps {
|
|
|
4
4
|
router: Router;
|
|
5
5
|
appearance?: NavigationAppearance;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Navigation component renders the root and global stacks.
|
|
9
|
+
*
|
|
10
|
+
* Modal stacks (NavigationStack added via addModal) are rendered as regular ScreenStackItems
|
|
11
|
+
* with their component being the StackRenderer that subscribes to its own stack history.
|
|
12
|
+
* This creates a clean recursive structure: stacks render their items, nested stacks
|
|
13
|
+
* (via childNode) render their own items through StackRenderer.
|
|
14
|
+
*/
|
|
7
15
|
export declare const Navigation: import("react").NamedExoticComponent<NavigationProps>;
|
|
8
16
|
export {};
|
|
9
17
|
//# sourceMappingURL=Navigation.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ScreenOptions } from './types';
|
|
2
|
+
import React from 'react';
|
|
2
3
|
import { type MixedComponent } from './createController';
|
|
3
4
|
import type { NavigationNode, NodeRoute, NodeChild } from './navigationNode';
|
|
4
5
|
type BuiltRoute = NodeRoute;
|
|
@@ -17,8 +18,8 @@ export declare class NavigationStack implements NavigationNode {
|
|
|
17
18
|
private log;
|
|
18
19
|
getId(): string;
|
|
19
20
|
addScreen(pathPattern: string, mixedComponent: MixedComponent | NavigationNode, options?: ScreenOptions): NavigationStack;
|
|
20
|
-
addModal(path: string, mixedComponent: MixedComponent, options?: ScreenOptions): NavigationStack;
|
|
21
|
-
addSheet(path: string, mixedComponent: MixedComponent, options?: ScreenOptions): NavigationStack;
|
|
21
|
+
addModal(path: string, mixedComponent: MixedComponent | NavigationNode, options?: ScreenOptions): NavigationStack;
|
|
22
|
+
addSheet(path: string, mixedComponent: MixedComponent | NavigationNode, options?: ScreenOptions): NavigationStack;
|
|
22
23
|
addStack(prefixOrStack: string | NavigationStack, maybeStack?: NavigationStack): NavigationStack;
|
|
23
24
|
getChildren(): ChildNode[];
|
|
24
25
|
getRoutes(): BuiltRoute[];
|
|
@@ -34,6 +34,7 @@ export declare class Router {
|
|
|
34
34
|
private suppressHistorySyncCount;
|
|
35
35
|
private navigationToken;
|
|
36
36
|
constructor(config: RouterConfig);
|
|
37
|
+
isDebugEnabled(): boolean;
|
|
37
38
|
private log;
|
|
38
39
|
navigate: (path: string) => void;
|
|
39
40
|
replace: (path: string, dedupe?: boolean) => void;
|
|
@@ -48,12 +49,22 @@ export declare class Router {
|
|
|
48
49
|
registerSheetDismisser: (key: string, dismisser: () => void) => void;
|
|
49
50
|
unregisterSheetDismisser: (key: string) => void;
|
|
50
51
|
goBack: () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Closes the nearest modal or sheet, regardless of navigation depth inside it.
|
|
54
|
+
* Useful when a NavigationStack is rendered inside a modal and you want to
|
|
55
|
+
* close the entire modal from any screen within it.
|
|
56
|
+
*/
|
|
57
|
+
dismiss: () => void;
|
|
51
58
|
getState: () => RouterState;
|
|
52
59
|
subscribe(listener: Listener): () => void;
|
|
53
60
|
getStackHistory: (stackId?: string) => HistoryItem[];
|
|
61
|
+
/**
|
|
62
|
+
* Returns all history items in navigation order.
|
|
63
|
+
* Useful for rendering all screens including modal stacks.
|
|
64
|
+
*/
|
|
65
|
+
getFullHistory: () => HistoryItem[];
|
|
54
66
|
subscribeStack: (stackId: string, cb: Listener) => (() => void);
|
|
55
67
|
getRootStackId(): string | undefined;
|
|
56
|
-
getGlobalStackId(): string | undefined;
|
|
57
68
|
subscribeRoot(listener: Listener): () => void;
|
|
58
69
|
private emitRootChange;
|
|
59
70
|
getRootTransition(): RootTransition | undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ScreenStackItemPhase } from '../ScreenStackItem/ScreenStackItem.types';
|
|
2
2
|
import type { TransitionStatus } from 'react-transition-state';
|
|
3
|
-
export type PresentationTypeClass = 'push' | 'modal' | 'transparent-modal' | 'contained-modal' | 'contained-transparent-modal' | 'fullscreen-modal' | 'formsheet' | 'pagesheet' | 'sheet';
|
|
4
|
-
export type AnimationType = 'push-enter' | 'push-exit' | 'push-background' | 'pop-enter' | 'pop-exit' | 'pop-background' | 'modal-enter' | 'modal-exit' | 'transparent-modal-enter' | 'transparent-modal-exit' | 'contained-modal-enter' | 'contained-modal-exit' | 'fullscreen-modal-enter' | 'fullscreen-modal-exit' | 'formsheet-enter' | 'formsheet-exit' | 'pagesheet-enter' | 'pagesheet-exit' | 'sheet-enter' | 'sheet-exit' | 'no-animate' | 'none';
|
|
3
|
+
export type PresentationTypeClass = 'push' | 'modal' | 'modal-right' | 'transparent-modal' | 'contained-modal' | 'contained-transparent-modal' | 'fullscreen-modal' | 'formsheet' | 'pagesheet' | 'sheet';
|
|
4
|
+
export type AnimationType = 'push-enter' | 'push-exit' | 'push-background' | 'pop-enter' | 'pop-exit' | 'pop-background' | 'modal-enter' | 'modal-exit' | 'modal-right-enter' | 'modal-right-exit' | 'transparent-modal-enter' | 'transparent-modal-exit' | 'contained-modal-enter' | 'contained-modal-exit' | 'fullscreen-modal-enter' | 'fullscreen-modal-exit' | 'formsheet-enter' | 'formsheet-exit' | 'pagesheet-enter' | 'pagesheet-exit' | 'sheet-enter' | 'sheet-exit' | 'no-animate' | 'none';
|
|
5
5
|
export type ScreenStackItemState = {
|
|
6
6
|
presentationType: PresentationTypeClass;
|
|
7
7
|
animationType: AnimationType;
|
|
@@ -4,5 +4,12 @@ export interface RenderSplitViewProps {
|
|
|
4
4
|
splitView: SplitView;
|
|
5
5
|
appearance?: NavigationAppearance;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* On native (iPhone), SplitView renders primary and secondary screens
|
|
9
|
+
* in a SINGLE ScreenStack to get native push/pop animations.
|
|
10
|
+
*
|
|
11
|
+
* The combined history is: [...primaryHistory, ...secondaryHistory]
|
|
12
|
+
* This way, navigating from primary to secondary is a native push.
|
|
13
|
+
*/
|
|
7
14
|
export declare const RenderSplitView: import("react").NamedExoticComponent<RenderSplitViewProps>;
|
|
8
15
|
//# sourceMappingURL=RenderSplitView.native.d.ts.map
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ColorValue, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
2
2
|
import type { BottomTabsScreenProps, ScreenProps as RNSScreenProps, ScreenStackHeaderConfigProps, TabBarItemLabelVisibilityMode, TabBarMinimizeBehavior } from 'react-native-screens';
|
|
3
|
-
export type StackPresentationTypes = 'push' | 'modal' | 'transparentModal' | 'containedModal' | 'containedTransparentModal' | 'fullScreenModal' | 'formSheet' | 'pageSheet' | 'sheet';
|
|
3
|
+
export type StackPresentationTypes = 'push' | 'modal' | 'modalRight' | 'transparentModal' | 'containedModal' | 'containedTransparentModal' | 'fullScreenModal' | 'formSheet' | 'pageSheet' | 'sheet';
|
|
4
|
+
/**
|
|
5
|
+
* Presentations that behave like modals (overlay on top of content).
|
|
6
|
+
*/
|
|
7
|
+
export declare const MODAL_LIKE_PRESENTATIONS: ReadonlySet<StackPresentationTypes>;
|
|
8
|
+
/**
|
|
9
|
+
* Check if a presentation type is modal-like (renders as overlay).
|
|
10
|
+
*/
|
|
11
|
+
export declare function isModalLikePresentation(presentation: StackPresentationTypes | undefined): boolean;
|
|
4
12
|
export type TabItem = Omit<BottomTabsScreenProps, 'isFocused' | 'children'>;
|
|
5
13
|
export type NavigationState<Route extends TabItem> = {
|
|
6
14
|
index: number;
|
|
@@ -29,6 +37,11 @@ export type ScreenOptions = Partial<Omit<RNSScreenProps, 'stackPresentation'>> &
|
|
|
29
37
|
* Useful for secondary stacks in split-view / overlays.
|
|
30
38
|
*/
|
|
31
39
|
allowRootPop?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Maximum width for modal presentation on web (in pixels).
|
|
42
|
+
* Only applies to modal-like presentations on web platform.
|
|
43
|
+
*/
|
|
44
|
+
maxWidth?: number;
|
|
32
45
|
};
|
|
33
46
|
export type HistoryItem = {
|
|
34
47
|
key: string;
|