@sigmela/router 0.1.2 → 0.2.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.
- package/README.md +177 -833
- package/lib/module/Navigation.js +1 -10
- package/lib/module/NavigationStack.js +168 -19
- package/lib/module/Router.js +1508 -501
- package/lib/module/RouterContext.js +1 -1
- package/lib/module/ScreenStack/ScreenStack.web.js +343 -117
- package/lib/module/ScreenStack/ScreenStackContext.js +15 -0
- package/lib/module/ScreenStack/animationHelpers.js +72 -0
- package/lib/module/ScreenStackItem/ScreenStackItem.js +2 -1
- package/lib/module/ScreenStackItem/ScreenStackItem.web.js +76 -16
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +2 -1
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +1 -1
- package/lib/module/SplitView/RenderSplitView.native.js +85 -0
- package/lib/module/SplitView/RenderSplitView.web.js +79 -0
- package/lib/module/SplitView/SplitView.js +89 -0
- package/lib/module/SplitView/SplitViewContext.js +4 -0
- package/lib/module/SplitView/index.js +5 -0
- package/lib/module/SplitView/useSplitView.js +11 -0
- package/lib/module/StackRenderer.js +4 -2
- package/lib/module/TabBar/RenderTabBar.native.js +118 -33
- package/lib/module/TabBar/RenderTabBar.web.js +52 -47
- package/lib/module/TabBar/TabBar.js +117 -3
- package/lib/module/TabBar/index.js +4 -1
- package/lib/module/TabBar/useTabBarHeight.js +22 -0
- package/lib/module/index.js +3 -4
- package/lib/module/navigationNode.js +3 -0
- package/lib/module/styles.css +693 -28
- package/lib/typescript/src/NavigationStack.d.ts +25 -13
- package/lib/typescript/src/Router.d.ts +147 -34
- package/lib/typescript/src/RouterContext.d.ts +1 -1
- package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -2
- package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +22 -0
- package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +6 -0
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +5 -1
- package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +1 -1
- package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +8 -0
- package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +8 -0
- package/lib/typescript/src/SplitView/SplitView.d.ts +31 -0
- package/lib/typescript/src/SplitView/SplitViewContext.d.ts +3 -0
- package/lib/typescript/src/SplitView/index.d.ts +5 -0
- package/lib/typescript/src/SplitView/useSplitView.d.ts +2 -0
- package/lib/typescript/src/StackRenderer.d.ts +2 -1
- package/lib/typescript/src/TabBar/TabBar.d.ts +27 -3
- package/lib/typescript/src/TabBar/index.d.ts +3 -0
- package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +18 -0
- package/lib/typescript/src/createController.d.ts +1 -0
- package/lib/typescript/src/index.d.ts +4 -3
- package/lib/typescript/src/navigationNode.d.ts +41 -0
- package/lib/typescript/src/types.d.ts +21 -32
- package/package.json +6 -5
- package/lib/module/web/TransitionStack.js +0 -227
- package/lib/typescript/src/web/TransitionStack.d.ts +0 -21
|
@@ -1,35 +1,95 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { RouteLocalContext } from "../RouterContext.js";
|
|
4
|
-
import { memo } from 'react';
|
|
3
|
+
import { RouteLocalContext, useRouter } from "../RouterContext.js";
|
|
4
|
+
import { memo, useMemo } from 'react';
|
|
5
5
|
import { StyleSheet, View } from 'react-native';
|
|
6
|
-
import {
|
|
6
|
+
import { useScreenStackItemsContext } from "../ScreenStack/ScreenStackContext.js";
|
|
7
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
const devLog = (_, __) => {};
|
|
7
9
|
export const ScreenStackItem = /*#__PURE__*/memo(({
|
|
8
|
-
phase = 'active',
|
|
9
10
|
item,
|
|
10
|
-
appearance
|
|
11
|
+
appearance,
|
|
12
|
+
style
|
|
11
13
|
}) => {
|
|
14
|
+
const itemsContext = useScreenStackItemsContext();
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const key = item.key;
|
|
17
|
+
const itemState = itemsContext.items[key];
|
|
18
|
+
const presentationType = itemState?.presentationType;
|
|
19
|
+
const animationType = itemState?.animationType;
|
|
20
|
+
const phase = itemState?.phase;
|
|
21
|
+
const transitionStatus = itemState?.transitionStatus;
|
|
22
|
+
const zIndex = itemState?.zIndex ?? 0;
|
|
23
|
+
const presentation = item.options?.stackPresentation ?? 'push';
|
|
24
|
+
const isModalLike = ['modal', 'transparentModal', 'containedModal', 'containedTransparentModal', 'fullScreenModal', 'formSheet', 'pageSheet', 'sheet'].includes(presentation);
|
|
25
|
+
const className = useMemo(() => {
|
|
26
|
+
const classes = ['screen-stack-item'];
|
|
27
|
+
if (presentationType) {
|
|
28
|
+
classes.push(presentationType);
|
|
29
|
+
}
|
|
30
|
+
if (animationType) {
|
|
31
|
+
classes.push(animationType);
|
|
32
|
+
}
|
|
33
|
+
if (transitionStatus) {
|
|
34
|
+
classes.push(`transition-${transitionStatus}`);
|
|
35
|
+
}
|
|
36
|
+
if (phase) {
|
|
37
|
+
classes.push(`phase-${phase}`);
|
|
38
|
+
}
|
|
39
|
+
devLog('[ScreenStackItem] className', {
|
|
40
|
+
key: item.key,
|
|
41
|
+
path: item.path,
|
|
42
|
+
presentationType,
|
|
43
|
+
animationType,
|
|
44
|
+
phase,
|
|
45
|
+
transitionStatus,
|
|
46
|
+
className: classes.join(' ')
|
|
47
|
+
});
|
|
48
|
+
return classes.join(' ');
|
|
49
|
+
}, [presentationType, animationType, transitionStatus, phase, item.key, item.path]);
|
|
50
|
+
const mergedStyle = useMemo(() => ({
|
|
51
|
+
flex: 1,
|
|
52
|
+
...style,
|
|
53
|
+
zIndex
|
|
54
|
+
}), [style, zIndex]);
|
|
12
55
|
const value = {
|
|
13
|
-
presentation
|
|
56
|
+
presentation,
|
|
14
57
|
params: item.params,
|
|
15
58
|
query: item.query,
|
|
16
59
|
pattern: item.pattern,
|
|
17
60
|
path: item.path
|
|
18
61
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
62
|
+
if (!itemState) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return /*#__PURE__*/_jsxs("div", {
|
|
66
|
+
style: mergedStyle,
|
|
67
|
+
className: className,
|
|
68
|
+
children: [isModalLike && /*#__PURE__*/_jsx("div", {
|
|
69
|
+
className: "stack-modal-overlay",
|
|
70
|
+
onClick: () => router.goBack()
|
|
71
|
+
}), /*#__PURE__*/_jsx("div", {
|
|
72
|
+
className: isModalLike ? 'stack-modal-container' : 'stack-screen-container',
|
|
73
|
+
children: appearance?.screen ? /*#__PURE__*/_jsx(View, {
|
|
74
|
+
style: [appearance?.screen, styles.flex],
|
|
75
|
+
children: /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
|
|
76
|
+
value: value,
|
|
77
|
+
children: /*#__PURE__*/_jsx(item.component, {
|
|
78
|
+
...(item.passProps || {}),
|
|
79
|
+
appearance: appearance
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}) : /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
|
|
83
|
+
value: value,
|
|
27
84
|
children: /*#__PURE__*/_jsx(item.component, {
|
|
28
|
-
...(item.passProps || {})
|
|
85
|
+
...(item.passProps || {}),
|
|
86
|
+
appearance: appearance
|
|
29
87
|
})
|
|
30
88
|
})
|
|
31
|
-
})
|
|
89
|
+
})]
|
|
32
90
|
});
|
|
91
|
+
}, (prevProps, nextProps) => {
|
|
92
|
+
return prevProps.item.key === nextProps.item.key && prevProps.item === nextProps.item && prevProps.appearance === nextProps.appearance && prevProps.style === nextProps.style;
|
|
33
93
|
});
|
|
34
94
|
const styles = StyleSheet.create({
|
|
35
95
|
flex: {
|
|
@@ -28,8 +28,9 @@ export const ScreenStackSheetItem = /*#__PURE__*/memo(props => {
|
|
|
28
28
|
Commands.dismiss(ref.current);
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
|
+
return () => router.unregisterSheetDismisser(item.key);
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
return undefined;
|
|
33
34
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
35
|
}, []);
|
|
35
36
|
const handleSheetDismissed = () => {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { StackRenderer } from "../StackRenderer.js";
|
|
4
|
+
import { SplitViewContext } from "./SplitViewContext.js";
|
|
5
|
+
import { useRouter } from "../RouterContext.js";
|
|
6
|
+
import { memo, useCallback, useSyncExternalStore } from 'react';
|
|
7
|
+
import { StyleSheet, View } from 'react-native';
|
|
8
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
+
const StackSliceRenderer = /*#__PURE__*/memo(({
|
|
10
|
+
stack,
|
|
11
|
+
appearance,
|
|
12
|
+
fallbackToFirstRoute
|
|
13
|
+
}) => {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const stackId = stack.getId();
|
|
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';
|
|
42
|
+
export const RenderSplitView = /*#__PURE__*/memo(({
|
|
43
|
+
splitView,
|
|
44
|
+
appearance
|
|
45
|
+
}) => {
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
const secondaryId = splitView.secondary.getId();
|
|
48
|
+
const subscribe = useCallback(cb => router.subscribeStack(secondaryId, cb), [router, secondaryId]);
|
|
49
|
+
const get = useCallback(() => router.getStackHistory(secondaryId), [router, secondaryId]);
|
|
50
|
+
const secondaryHistory = useSyncExternalStore(subscribe, get, get);
|
|
51
|
+
const hasSecondary = secondaryHistory.length > 0;
|
|
52
|
+
return /*#__PURE__*/_jsx(SplitViewContext.Provider, {
|
|
53
|
+
value: splitView,
|
|
54
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
55
|
+
style: styles.container,
|
|
56
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
57
|
+
style: styles.primary,
|
|
58
|
+
pointerEvents: hasSecondary ? 'none' : 'auto',
|
|
59
|
+
children: /*#__PURE__*/_jsx(StackSliceRenderer, {
|
|
60
|
+
appearance: appearance,
|
|
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]
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
const styles = StyleSheet.create({
|
|
75
|
+
container: {
|
|
76
|
+
flex: 1
|
|
77
|
+
},
|
|
78
|
+
primary: {
|
|
79
|
+
flex: 1
|
|
80
|
+
},
|
|
81
|
+
secondary: {
|
|
82
|
+
...StyleSheet.absoluteFillObject,
|
|
83
|
+
zIndex: 2
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { StackRenderer } from "../StackRenderer.js";
|
|
4
|
+
import { SplitViewContext } from "./SplitViewContext.js";
|
|
5
|
+
import { useRouter } from "../RouterContext.js";
|
|
6
|
+
import { memo, useCallback, useMemo, useSyncExternalStore } from 'react';
|
|
7
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
const StackSliceRenderer = /*#__PURE__*/memo(({
|
|
9
|
+
stack,
|
|
10
|
+
appearance,
|
|
11
|
+
fallbackToFirstRoute
|
|
12
|
+
}) => {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const stackId = stack.getId();
|
|
15
|
+
const subscribe = useCallback(cb => router.subscribeStack(stackId, cb), [router, stackId]);
|
|
16
|
+
const get = useCallback(() => router.getStackHistory(stackId), [router, stackId]);
|
|
17
|
+
const history = useSyncExternalStore(subscribe, get, get);
|
|
18
|
+
let historyToRender = history;
|
|
19
|
+
|
|
20
|
+
// When a container route (like /mail -> SplitView) is active, the child stack can be empty,
|
|
21
|
+
// yet we still want to render its root screen. We keep Router history intact and provide a
|
|
22
|
+
// renderer-only fallback item.
|
|
23
|
+
if (fallbackToFirstRoute && historyToRender.length === 0) {
|
|
24
|
+
const first = stack.getFirstRoute();
|
|
25
|
+
if (first) {
|
|
26
|
+
const activePath = router.getActiveRoute()?.path;
|
|
27
|
+
historyToRender = [{
|
|
28
|
+
key: `splitview-seed-${stackId}`,
|
|
29
|
+
routeId: first.routeId,
|
|
30
|
+
component: first.component,
|
|
31
|
+
options: first.options,
|
|
32
|
+
stackId,
|
|
33
|
+
pattern: first.path,
|
|
34
|
+
path: activePath ?? first.path
|
|
35
|
+
}];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return /*#__PURE__*/_jsx(StackRenderer, {
|
|
39
|
+
appearance: appearance,
|
|
40
|
+
stack: stack,
|
|
41
|
+
history: historyToRender
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
StackSliceRenderer.displayName = 'SplitViewStackSliceRenderer';
|
|
45
|
+
export const RenderSplitView = /*#__PURE__*/memo(({
|
|
46
|
+
splitView,
|
|
47
|
+
appearance
|
|
48
|
+
}) => {
|
|
49
|
+
const instanceClass = useMemo(() => `split-view-instance-${splitView.getId()}`, [splitView]);
|
|
50
|
+
const containerStyle = useMemo(() => {
|
|
51
|
+
return {
|
|
52
|
+
'--split-view-primary-max-width': `${splitView.primaryMaxWidth}px`
|
|
53
|
+
};
|
|
54
|
+
}, [splitView.primaryMaxWidth]);
|
|
55
|
+
return /*#__PURE__*/_jsx(SplitViewContext.Provider, {
|
|
56
|
+
value: splitView,
|
|
57
|
+
children: /*#__PURE__*/_jsx("div", {
|
|
58
|
+
className: instanceClass,
|
|
59
|
+
children: /*#__PURE__*/_jsxs("div", {
|
|
60
|
+
className: "split-view-container",
|
|
61
|
+
style: containerStyle,
|
|
62
|
+
children: [/*#__PURE__*/_jsx("div", {
|
|
63
|
+
className: "split-view-primary",
|
|
64
|
+
children: /*#__PURE__*/_jsx(StackSliceRenderer, {
|
|
65
|
+
appearance: appearance,
|
|
66
|
+
stack: splitView.primary,
|
|
67
|
+
fallbackToFirstRoute: true
|
|
68
|
+
})
|
|
69
|
+
}), /*#__PURE__*/_jsx("div", {
|
|
70
|
+
className: "split-view-secondary",
|
|
71
|
+
children: /*#__PURE__*/_jsx(StackSliceRenderer, {
|
|
72
|
+
appearance: appearance,
|
|
73
|
+
stack: splitView.secondary
|
|
74
|
+
})
|
|
75
|
+
})]
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { RenderSplitView } from './RenderSplitView';
|
|
5
|
+
class SecondaryStackWrapper {
|
|
6
|
+
constructor(stack) {
|
|
7
|
+
this.stack = stack;
|
|
8
|
+
}
|
|
9
|
+
getId() {
|
|
10
|
+
return this.stack.getId();
|
|
11
|
+
}
|
|
12
|
+
getNodeRoutes() {
|
|
13
|
+
// IMPORTANT:
|
|
14
|
+
// - do not mutate routes/options from the original NavigationStack
|
|
15
|
+
// - allowRootPop is used by Router.goBack so that a single-screen secondary can be dismissed
|
|
16
|
+
return this.stack.getNodeRoutes().map(r => ({
|
|
17
|
+
...r,
|
|
18
|
+
options: {
|
|
19
|
+
...(r.options ?? {}),
|
|
20
|
+
allowRootPop: true
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
getNodeChildren() {
|
|
25
|
+
return this.stack.getNodeChildren();
|
|
26
|
+
}
|
|
27
|
+
getRenderer() {
|
|
28
|
+
return this.stack.getRenderer();
|
|
29
|
+
}
|
|
30
|
+
seed() {
|
|
31
|
+
return this.stack.seed?.() ?? null;
|
|
32
|
+
}
|
|
33
|
+
getDefaultOptions() {
|
|
34
|
+
return this.stack.getDefaultOptions?.();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class SplitView {
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.splitViewId = `splitview-${Math.random().toString(36).slice(2)}`;
|
|
40
|
+
this.primary = options.primary;
|
|
41
|
+
this.secondary = options.secondary;
|
|
42
|
+
this.minWidth = options.minWidth;
|
|
43
|
+
this.primaryMaxWidth = options.primaryMaxWidth ?? 390;
|
|
44
|
+
}
|
|
45
|
+
getId() {
|
|
46
|
+
return this.splitViewId;
|
|
47
|
+
}
|
|
48
|
+
getNodeRoutes() {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
getNodeChildren() {
|
|
52
|
+
return [{
|
|
53
|
+
prefix: '',
|
|
54
|
+
node: this.primary
|
|
55
|
+
}, {
|
|
56
|
+
prefix: '',
|
|
57
|
+
node: new SecondaryStackWrapper(this.secondary)
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
getRenderer() {
|
|
61
|
+
// eslint-disable-next-line consistent-this
|
|
62
|
+
const instance = this;
|
|
63
|
+
return function SplitViewScreen(props) {
|
|
64
|
+
return /*#__PURE__*/React.createElement(RenderSplitView, {
|
|
65
|
+
splitView: instance,
|
|
66
|
+
appearance: props?.appearance
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
hasRoute(routeId) {
|
|
71
|
+
return this.primary.getRoutes().some(r => r.routeId === routeId) || this.secondary.getRoutes().some(r => r.routeId === routeId);
|
|
72
|
+
}
|
|
73
|
+
switchToRoute(_routeId) {
|
|
74
|
+
// SplitView does not have a single "active" child like TabBar.
|
|
75
|
+
// It renders both stacks (or overlays secondary on narrow via CSS).
|
|
76
|
+
}
|
|
77
|
+
setActiveChildByRoute(routeId) {
|
|
78
|
+
this.switchToRoute(routeId);
|
|
79
|
+
}
|
|
80
|
+
seed() {
|
|
81
|
+
const firstRoute = this.primary.getFirstRoute();
|
|
82
|
+
if (!firstRoute) return null;
|
|
83
|
+
return {
|
|
84
|
+
routeId: firstRoute.routeId,
|
|
85
|
+
path: firstRoute.path,
|
|
86
|
+
stackId: this.primary.getId()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useContext } from 'react';
|
|
4
|
+
import { SplitViewContext } from "./SplitViewContext.js";
|
|
5
|
+
export const useSplitView = () => {
|
|
6
|
+
const splitView = useContext(SplitViewContext);
|
|
7
|
+
if (!splitView) {
|
|
8
|
+
throw new Error('useSplitView must be used within a SplitViewProvider');
|
|
9
|
+
}
|
|
10
|
+
return splitView;
|
|
11
|
+
};
|
|
@@ -8,13 +8,15 @@ import { StyleSheet } from 'react-native';
|
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
export const StackRenderer = /*#__PURE__*/memo(({
|
|
10
10
|
stack,
|
|
11
|
-
appearance
|
|
11
|
+
appearance,
|
|
12
|
+
history
|
|
12
13
|
}) => {
|
|
13
14
|
const router = useRouter();
|
|
14
15
|
const stackId = stack.getId();
|
|
15
16
|
const subscribe = useCallback(cb => router.subscribeStack(stackId, cb), [router, stackId]);
|
|
16
17
|
const get = useCallback(() => router.getStackHistory(stackId), [router, stackId]);
|
|
17
|
-
const
|
|
18
|
+
const historyFromStore = useSyncExternalStore(subscribe, get, get);
|
|
19
|
+
const historyForThisStack = history ?? historyFromStore;
|
|
18
20
|
return /*#__PURE__*/_jsx(ScreenStack, {
|
|
19
21
|
style: [styles.flex, appearance?.screen],
|
|
20
22
|
children: historyForThisStack.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
@@ -7,7 +7,6 @@ import { BottomTabsScreen, BottomTabs, ScreenStackItem } from 'react-native-scre
|
|
|
7
7
|
import { Platform, StyleSheet, View } from 'react-native';
|
|
8
8
|
import { useCallback, useSyncExternalStore, memo, useEffect, useState } from 'react';
|
|
9
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
-
// Helpers outside render to avoid re-creation
|
|
11
10
|
const isImageSource = value => {
|
|
12
11
|
if (value == null) return false;
|
|
13
12
|
const valueType = typeof value;
|
|
@@ -16,47 +15,141 @@ const isImageSource = value => {
|
|
|
16
15
|
if (valueType === 'object') {
|
|
17
16
|
const v = value;
|
|
18
17
|
if ('uri' in v || 'width' in v || 'height' in v) return true;
|
|
18
|
+
// Check for new PlatformIcon format
|
|
19
|
+
if ('ios' in v || 'android' in v || 'shared' in v) return false;
|
|
20
|
+
// Check for legacy format
|
|
19
21
|
if ('sfSymbolName' in v || 'imageSource' in v || 'templateSource' in v) return false;
|
|
22
|
+
// Check for new type-based format
|
|
23
|
+
if ('type' in v) return false;
|
|
20
24
|
}
|
|
21
25
|
return false;
|
|
22
26
|
};
|
|
23
|
-
const
|
|
27
|
+
const isPlatformIcon = value => {
|
|
28
|
+
if (value == null || typeof value !== 'object') return false;
|
|
29
|
+
const v = value;
|
|
30
|
+
return 'ios' in v || 'android' in v || 'shared' in v;
|
|
31
|
+
};
|
|
32
|
+
const isLegacyIOSIcon = value => {
|
|
24
33
|
if (value == null || typeof value !== 'object') return false;
|
|
25
34
|
const v = value;
|
|
26
35
|
return 'sfSymbolName' in v || 'imageSource' in v || 'templateSource' in v;
|
|
27
36
|
};
|
|
37
|
+
const convertLegacyIOSIconToPlatformIconIOS = value => {
|
|
38
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
39
|
+
const v = value;
|
|
40
|
+
if ('sfSymbolName' in v) {
|
|
41
|
+
return {
|
|
42
|
+
type: 'sfSymbol',
|
|
43
|
+
name: v.sfSymbolName
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if ('templateSource' in v) {
|
|
47
|
+
return {
|
|
48
|
+
type: 'templateSource',
|
|
49
|
+
templateSource: v.templateSource
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if ('imageSource' in v) {
|
|
53
|
+
return {
|
|
54
|
+
type: 'imageSource',
|
|
55
|
+
imageSource: v.imageSource
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
};
|
|
28
60
|
const buildIOSIcon = value => {
|
|
29
61
|
if (!value) return undefined;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
|
|
63
|
+
// If it's already a PlatformIcon, extract ios
|
|
64
|
+
if (isPlatformIcon(value)) {
|
|
65
|
+
return value.ios;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If it's a legacy format, convert it
|
|
69
|
+
if (isLegacyIOSIcon(value)) {
|
|
70
|
+
return convertLegacyIOSIconToPlatformIconIOS(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If it's an ImageSourcePropType, convert to templateSource
|
|
74
|
+
if (isImageSource(value)) {
|
|
75
|
+
return {
|
|
76
|
+
type: 'templateSource',
|
|
77
|
+
templateSource: value
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
34
81
|
};
|
|
82
|
+
const buildPlatformIcon = (icon, selectedIcon) => {
|
|
83
|
+
if (!icon && !selectedIcon) return undefined;
|
|
84
|
+
const iosIcon = buildIOSIcon(icon);
|
|
85
|
+
const iosSelectedIcon = buildIOSIcon(selectedIcon);
|
|
35
86
|
|
|
36
|
-
//
|
|
87
|
+
// If it's already a PlatformIcon, use it directly
|
|
88
|
+
if (isPlatformIcon(icon)) {
|
|
89
|
+
return {
|
|
90
|
+
...icon,
|
|
91
|
+
ios: iosSelectedIcon || icon.ios
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build new PlatformIcon
|
|
96
|
+
const result = {};
|
|
97
|
+
if (iosIcon || iosSelectedIcon) {
|
|
98
|
+
result.ios = iosSelectedIcon || iosIcon;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// For shared imageSource (works on both platforms)
|
|
102
|
+
if (isImageSource(icon) && !iosIcon) {
|
|
103
|
+
result.shared = {
|
|
104
|
+
type: 'imageSource',
|
|
105
|
+
imageSource: icon
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
109
|
+
};
|
|
37
110
|
const getTabIcon = tab => {
|
|
38
111
|
const {
|
|
39
112
|
icon,
|
|
40
113
|
selectedIcon
|
|
41
114
|
} = tab;
|
|
42
|
-
if (icon
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
115
|
+
if (!icon && !selectedIcon) return undefined;
|
|
116
|
+
|
|
117
|
+
// Build PlatformIcon for new API
|
|
118
|
+
const platformIcon = buildPlatformIcon(icon, selectedIcon);
|
|
119
|
+
if (!platformIcon) return undefined;
|
|
120
|
+
|
|
121
|
+
// For Android, if icon is a direct ImageSourcePropType, use shared
|
|
122
|
+
if (Platform.OS === 'android' && isImageSource(icon) && !platformIcon.shared) {
|
|
123
|
+
platformIcon.shared = {
|
|
124
|
+
type: 'imageSource',
|
|
125
|
+
imageSource: icon
|
|
51
126
|
};
|
|
52
127
|
}
|
|
53
|
-
return
|
|
128
|
+
return {
|
|
129
|
+
icon: platformIcon,
|
|
130
|
+
selectedIcon: platformIcon.ios
|
|
131
|
+
};
|
|
54
132
|
};
|
|
133
|
+
const TabStackRenderer = /*#__PURE__*/memo(({
|
|
134
|
+
stack,
|
|
135
|
+
appearance
|
|
136
|
+
}) => {
|
|
137
|
+
const router = useRouter();
|
|
138
|
+
const stackId = stack.getId();
|
|
139
|
+
const subscribe = useCallback(cb => router.subscribeStack(stackId, cb), [router, stackId]);
|
|
140
|
+
const get = useCallback(() => router.getStackHistory(stackId), [router, stackId]);
|
|
141
|
+
const history = useSyncExternalStore(subscribe, get, get);
|
|
142
|
+
return /*#__PURE__*/_jsx(StackRenderer, {
|
|
143
|
+
appearance: appearance,
|
|
144
|
+
stack: stack,
|
|
145
|
+
history: history
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
TabStackRenderer.displayName = 'TabStackRenderer';
|
|
55
149
|
export const RenderTabBar = /*#__PURE__*/memo(({
|
|
56
150
|
tabBar,
|
|
57
151
|
appearance = {}
|
|
58
152
|
}) => {
|
|
59
|
-
const router = useRouter();
|
|
60
153
|
const subscribe = useCallback(cb => tabBar.subscribe(cb), [tabBar]);
|
|
61
154
|
const snapshot = useSyncExternalStore(subscribe, tabBar.getState, tabBar.getState);
|
|
62
155
|
const {
|
|
@@ -76,17 +169,14 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
76
169
|
badgeBackgroundColor,
|
|
77
170
|
iOSShadowColor
|
|
78
171
|
} = appearance?.tabBar ?? {};
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
router.ensureTabSeed(index);
|
|
81
|
-
}, [index, router]);
|
|
82
172
|
const onNativeFocusChange = useCallback(event => {
|
|
83
173
|
const tabKey = event.nativeEvent.tabKey;
|
|
84
174
|
const tabIndex = tabs.findIndex(route => route.tabKey === tabKey);
|
|
85
|
-
|
|
86
|
-
}, [tabs,
|
|
175
|
+
tabBar.onIndexChange(tabIndex);
|
|
176
|
+
}, [tabs, tabBar]);
|
|
87
177
|
const onTabPress = useCallback(nextIndex => {
|
|
88
|
-
|
|
89
|
-
}, [
|
|
178
|
+
tabBar.onIndexChange(nextIndex);
|
|
179
|
+
}, [tabBar]);
|
|
90
180
|
const containerProps = {
|
|
91
181
|
tabBarBackgroundColor: backgroundColor,
|
|
92
182
|
tabBarItemTitleFontFamily: title?.fontFamily,
|
|
@@ -129,11 +219,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
129
219
|
}
|
|
130
220
|
}
|
|
131
221
|
});
|
|
132
|
-
|
|
133
|
-
// If a custom component is provided, render it instead of default native BottomTabs
|
|
134
222
|
const CustomTabBar = config.component;
|
|
135
|
-
|
|
136
|
-
// Track visited tabs to lazily mount on first visit and keep mounted afterwards
|
|
137
223
|
const [visited, setVisited] = useState({});
|
|
138
224
|
useEffect(() => {
|
|
139
225
|
const key = tabs[index]?.tabKey;
|
|
@@ -162,7 +248,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
162
248
|
const ScreenForTab = tabBar.screens[tab.tabKey];
|
|
163
249
|
return /*#__PURE__*/_jsx(View, {
|
|
164
250
|
style: [styles.flex, !isActive && styles.hidden],
|
|
165
|
-
children: stackForTab ? /*#__PURE__*/_jsx(
|
|
251
|
+
children: stackForTab ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
166
252
|
appearance: appearance,
|
|
167
253
|
stack: stackForTab
|
|
168
254
|
}) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
|
|
@@ -201,10 +287,9 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
201
287
|
title: tab.title,
|
|
202
288
|
badgeValue: tab.badgeValue,
|
|
203
289
|
specialEffects: tab.specialEffects,
|
|
204
|
-
selectedIcon: icon?.selectedIcon,
|
|
205
|
-
iconResource: icon?.iconResource,
|
|
206
290
|
icon: icon?.icon,
|
|
207
|
-
|
|
291
|
+
selectedIcon: icon?.selectedIcon,
|
|
292
|
+
children: stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
208
293
|
appearance: appearance,
|
|
209
294
|
stack: stack
|
|
210
295
|
}) : Screen ? /*#__PURE__*/_jsx(Screen, {}) : null
|