@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.
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 +1508 -501
  5. package/lib/module/RouterContext.js +1 -1
  6. package/lib/module/ScreenStack/ScreenStack.web.js +343 -117
  7. package/lib/module/ScreenStack/ScreenStackContext.js +15 -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 +79 -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 +117 -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 +22 -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 +21 -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
@@ -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 { jsx as _jsx } from "react/jsx-runtime";
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: item.options?.stackPresentation ?? 'push',
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
- return /*#__PURE__*/_jsx("div", {
20
- "data-presentation": value.presentation,
21
- className: "screen-stack-item",
22
- "data-phase": phase,
23
- children: /*#__PURE__*/_jsx(RouteLocalContext.Provider, {
24
- value: value,
25
- children: /*#__PURE__*/_jsx(View, {
26
- style: [styles.flex, appearance?.screen],
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 = () => {
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
 
3
3
  export const ScreenStackSheetItem = () => {
4
- return null; // TODO: Implement
4
+ return null;
5
5
  };
@@ -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,4 @@
1
+ "use strict";
2
+
3
+ import { createContext } from 'react';
4
+ export const SplitViewContext = /*#__PURE__*/createContext(null);
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ export { SplitView } from "./SplitView.js";
4
+ export { RenderSplitView } from './RenderSplitView';
5
+ export { useSplitView } from "./useSplitView.js";
@@ -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 historyForThisStack = useSyncExternalStore(subscribe, get, get);
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 isRNSIcon = value => {
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
- if (isRNSIcon(value)) return value;
31
- return {
32
- templateSource: value
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
- // Map unified tab icon props to RNS BottomTabsScreen platform-specific props
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 || selectedIcon) {
43
- if (Platform.OS === 'android' && isImageSource(icon)) {
44
- return {
45
- iconResource: icon
46
- };
47
- }
48
- return {
49
- selectedIcon: buildIOSIcon(selectedIcon),
50
- icon: buildIOSIcon(icon)
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 undefined;
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
- router.onTabIndexChange(tabIndex);
86
- }, [tabs, router]);
175
+ tabBar.onIndexChange(tabIndex);
176
+ }, [tabs, tabBar]);
87
177
  const onTabPress = useCallback(nextIndex => {
88
- router.onTabIndexChange(nextIndex);
89
- }, [router]);
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(StackRenderer, {
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
- children: stack ? /*#__PURE__*/_jsx(StackRenderer, {
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