@sigmela/router 0.2.2 → 0.2.3

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.
@@ -367,6 +367,14 @@ export class Router {
367
367
  }
368
368
  return;
369
369
  }
370
+
371
+ // Ensure the root-level container (e.g. TabBar) is activated for the matched route.
372
+ // This is important when the matched route belongs to a nested stack inside a composite node
373
+ // like SplitView: stackActivators won't fire for the tab itself in that case.
374
+ const rootStackId = this.root?.getId();
375
+ if (rootStackId) {
376
+ this.activateContainerForRoute(base.routeId, rootStackId);
377
+ }
370
378
  const activator = base.stackId ? this.stackActivators.get(base.stackId) : undefined;
371
379
  if (activator) {
372
380
  activator();
@@ -5,7 +5,7 @@ import { TabBarContext } from "./TabBarContext.js";
5
5
  import { useRouter } from "../RouterContext.js";
6
6
  import { BottomTabsScreen, BottomTabs, ScreenStackItem } from 'react-native-screens';
7
7
  import { Platform, StyleSheet, View } from 'react-native';
8
- import { useCallback, useSyncExternalStore, memo, useEffect, useState } from 'react';
8
+ import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo } from 'react';
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  const isImageSource = value => {
11
11
  if (value == null) return false;
@@ -146,6 +146,16 @@ const TabStackRenderer = /*#__PURE__*/memo(({
146
146
  });
147
147
  });
148
148
  TabStackRenderer.displayName = 'TabStackRenderer';
149
+ const TabNodeRenderer = /*#__PURE__*/memo(({
150
+ node,
151
+ appearance
152
+ }) => {
153
+ const Renderer = useMemo(() => node.getRenderer(), [node]);
154
+ return /*#__PURE__*/_jsx(Renderer, {
155
+ appearance: appearance
156
+ });
157
+ });
158
+ TabNodeRenderer.displayName = 'TabNodeRenderer';
149
159
  export const RenderTabBar = /*#__PURE__*/memo(({
150
160
  tabBar,
151
161
  appearance = {}
@@ -245,12 +255,16 @@ export const RenderTabBar = /*#__PURE__*/memo(({
245
255
  children: tabs.filter(t => visited[t.tabKey]).map(tab => {
246
256
  const isActive = tab.tabKey === tabs[index]?.tabKey;
247
257
  const stackForTab = tabBar.stacks[tab.tabKey];
258
+ const nodeForTab = tabBar.nodes[tab.tabKey];
248
259
  const ScreenForTab = tabBar.screens[tab.tabKey];
249
260
  return /*#__PURE__*/_jsx(View, {
250
261
  style: [styles.flex, !isActive && styles.hidden],
251
262
  children: stackForTab ? /*#__PURE__*/_jsx(TabStackRenderer, {
252
263
  appearance: appearance,
253
264
  stack: stackForTab
265
+ }) : nodeForTab ? /*#__PURE__*/_jsx(TabNodeRenderer, {
266
+ appearance: appearance,
267
+ node: nodeForTab
254
268
  }) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
255
269
  }, `tab-content-${tab.tabKey}`);
256
270
  })
@@ -277,6 +291,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
277
291
  children: tabs.map(tab => {
278
292
  const isFocused = tab.tabKey === tabs[index]?.tabKey;
279
293
  const stack = tabBar.stacks[tab.tabKey];
294
+ const node = tabBar.nodes[tab.tabKey];
280
295
  const Screen = tabBar.screens[tab.tabKey];
281
296
  const icon = getTabIcon(tab);
282
297
  return /*#__PURE__*/_jsx(BottomTabsScreen, {
@@ -292,6 +307,9 @@ export const RenderTabBar = /*#__PURE__*/memo(({
292
307
  children: stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
293
308
  appearance: appearance,
294
309
  stack: stack
310
+ }) : node ? /*#__PURE__*/_jsx(TabNodeRenderer, {
311
+ appearance: appearance,
312
+ node: node
295
313
  }) : Screen ? /*#__PURE__*/_jsx(Screen, {}) : null
296
314
  }, tab.tabKey);
297
315
  })
@@ -34,6 +34,29 @@ const TabStackRenderer = /*#__PURE__*/memo(({
34
34
  });
35
35
  });
36
36
  TabStackRenderer.displayName = 'TabStackRenderer';
37
+ const TabNodeRenderer = /*#__PURE__*/memo(({
38
+ node,
39
+ appearance
40
+ }) => {
41
+ const Renderer = useMemo(() => node.getRenderer(), [node]);
42
+ return /*#__PURE__*/_jsx(Renderer, {
43
+ appearance: appearance
44
+ });
45
+ });
46
+ TabNodeRenderer.displayName = 'TabNodeRenderer';
47
+ const joinPrefixAndPath = (prefix, path) => {
48
+ const base = (prefix ?? '').trim();
49
+ const child = (path || '/').trim();
50
+ if (!base) return child || '/';
51
+ const baseNorm = base === '/' ? '' : base.endsWith('/') ? base.slice(0, -1) : base;
52
+ if (!child || child === '/') {
53
+ return baseNorm || '/';
54
+ }
55
+ if (child.startsWith('/')) {
56
+ return `${baseNorm}${child}`;
57
+ }
58
+ return `${baseNorm}/${child}`;
59
+ };
37
60
  export const RenderTabBar = /*#__PURE__*/memo(({
38
61
  tabBar,
39
62
  appearance
@@ -48,11 +71,13 @@ export const RenderTabBar = /*#__PURE__*/memo(({
48
71
  } = snapshot;
49
72
  const focusedTab = tabs[index];
50
73
  const stack = focusedTab ? tabBar.stacks[focusedTab.tabKey] : undefined;
74
+ const node = focusedTab ? tabBar.nodes[focusedTab.tabKey] : undefined;
51
75
  const Screen = focusedTab ? tabBar.screens[focusedTab.tabKey] : undefined;
52
76
  const onTabClick = useCallback(nextIndex => {
53
77
  const targetTab = tabs[nextIndex];
54
78
  if (!targetTab) return;
55
79
  const targetStack = tabBar.stacks[targetTab.tabKey];
80
+ const targetNode = tabBar.nodes[targetTab.tabKey];
56
81
  if (targetStack) {
57
82
  // Keep TabBar UI in sync immediately.
58
83
  if (nextIndex !== index) {
@@ -65,6 +90,17 @@ export const RenderTabBar = /*#__PURE__*/memo(({
65
90
  router.reset(firstRoutePath);
66
91
  return;
67
92
  }
93
+ } else if (targetNode) {
94
+ // Keep TabBar UI in sync immediately.
95
+ if (nextIndex !== index) {
96
+ tabBar.onIndexChange(nextIndex);
97
+ }
98
+ const seedPath = targetNode.seed?.()?.path;
99
+ const fallbackFirstPath = targetNode.getNodeRoutes()?.[0]?.path;
100
+ const path = seedPath ?? fallbackFirstPath ?? '/';
101
+ const fullPath = joinPrefixAndPath(targetTab.tabPrefix, path);
102
+ router.reset(fullPath);
103
+ return;
68
104
  }
69
105
  if (nextIndex !== index) {
70
106
  tabBar.onIndexChange(nextIndex);
@@ -97,6 +133,9 @@ export const RenderTabBar = /*#__PURE__*/memo(({
97
133
  children: [stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
98
134
  appearance: appearance,
99
135
  stack: stack
136
+ }) : node ? /*#__PURE__*/_jsx(TabNodeRenderer, {
137
+ appearance: appearance,
138
+ node: node
100
139
  }) : Screen ? /*#__PURE__*/_jsx(Screen, {}) : null, CustomTabBar ? /*#__PURE__*/_jsx(CustomTabBar, {
101
140
  onTabPress: onTabClick,
102
141
  activeIndex: index,
@@ -8,6 +8,7 @@ import { RenderTabBar } from './RenderTabBar';
8
8
  export class TabBar {
9
9
  screens = {};
10
10
  stacks = {};
11
+ nodes = {};
11
12
  listeners = new Set();
12
13
  constructor(options = {}) {
13
14
  this.tabBarId = `tabbar-${Math.random().toString(36).slice(2)}`;
@@ -23,6 +24,10 @@ export class TabBar {
23
24
  return this.tabBarId;
24
25
  }
25
26
  addTab(tab) {
27
+ const sourcesCount = (tab.stack ? 1 : 0) + (tab.node ? 1 : 0) + (tab.screen ? 1 : 0);
28
+ if (sourcesCount !== 1) {
29
+ throw new Error(`TabBar.addTab: exactly one of { stack, node, screen } must be provided (got ${sourcesCount})`);
30
+ }
26
31
  const {
27
32
  key,
28
33
  ...rest
@@ -31,6 +36,7 @@ export class TabBar {
31
36
  const tabKey = key ?? `tab-${nextIndex}`;
32
37
  const nextTabs = [...this.state.tabs, {
33
38
  tabKey,
39
+ tabPrefix: tab.prefix,
34
40
  ...rest
35
41
  }];
36
42
  this.setState({
@@ -38,6 +44,8 @@ export class TabBar {
38
44
  });
39
45
  if (tab.stack) {
40
46
  this.stacks[tabKey] = tab.stack;
47
+ } else if (tab.node) {
48
+ this.nodes[tabKey] = tab.node;
41
49
  } else if (tab.screen) {
42
50
  this.screens[tabKey] = tab.screen;
43
51
  }
@@ -102,8 +110,8 @@ export class TabBar {
102
110
  getActiveChildId() {
103
111
  const activeTab = this.state.tabs[this.state.index];
104
112
  if (!activeTab) return undefined;
105
- const stack = this.stacks[activeTab.tabKey];
106
- return stack?.getId();
113
+ const node = this.nodes[activeTab.tabKey] ?? this.stacks[activeTab.tabKey];
114
+ return node?.getId();
107
115
  }
108
116
  switchToRoute(routeId) {
109
117
  const idx = this.findTabIndexByRoute(routeId);
@@ -126,11 +134,11 @@ export class TabBar {
126
134
  const children = [];
127
135
  for (let idx = 0; idx < this.state.tabs.length; idx++) {
128
136
  const tab = this.state.tabs[idx];
129
- const stack = tab ? this.stacks[tab.tabKey] : undefined;
130
- if (stack) {
137
+ const node = tab ? this.nodes[tab.tabKey] ?? this.stacks[tab.tabKey] : undefined;
138
+ if (node) {
131
139
  children.push({
132
- prefix: '',
133
- node: stack,
140
+ prefix: tab?.tabPrefix ?? '',
141
+ node,
134
142
  onMatch: () => this.onIndexChange(idx)
135
143
  });
136
144
  }
@@ -150,6 +158,10 @@ export class TabBar {
150
158
  seed() {
151
159
  const activeTab = this.state.tabs[this.state.index];
152
160
  if (!activeTab) return null;
161
+ const node = this.nodes[activeTab.tabKey];
162
+ if (node) {
163
+ return node.seed?.() ?? null;
164
+ }
153
165
  const stack = this.stacks[activeTab.tabKey];
154
166
  if (!stack) return null;
155
167
  const firstRoute = stack.getFirstRoute();
@@ -163,9 +175,9 @@ export class TabBar {
163
175
  findTabIndexByRoute(routeId) {
164
176
  for (let i = 0; i < this.state.tabs.length; i++) {
165
177
  const tab = this.state.tabs[i];
166
- const stack = tab && this.stacks[tab.tabKey];
167
- if (!stack) continue;
168
- const hasRoute = this.nodeHasRoute(stack, routeId);
178
+ const node = tab ? this.nodes[tab.tabKey] ?? this.stacks[tab.tabKey] : undefined;
179
+ if (!node) continue;
180
+ const hasRoute = this.nodeHasRoute(node, routeId);
169
181
  if (hasRoute) {
170
182
  return i;
171
183
  }
@@ -489,7 +489,8 @@
489
489
  position: relative;
490
490
  }
491
491
 
492
- .tab-stacks-container > .screen-stack {
492
+ .tab-stacks-container > .screen-stack,
493
+ .tab-stacks-container > .split-view-container {
493
494
  padding-bottom: calc(73px + env(safe-area-inset-bottom));
494
495
  }
495
496
 
@@ -725,7 +726,8 @@
725
726
  flex-direction: row;
726
727
  }
727
728
 
728
- .tab-stacks-container > .screen-stack {
729
+ .tab-stacks-container > .screen-stack,
730
+ .tab-stacks-container > .split-view-container {
729
731
  padding-bottom: 0;
730
732
  margin-left: 260px;
731
733
  min-width: 0;
@@ -17,6 +17,11 @@ export type InternalTabItem = Omit<TabItem, 'icon' | 'selectedIcon'> & {
17
17
  icon?: ExtendedIcon;
18
18
  selectedIcon?: ExtendedIcon;
19
19
  badgeValue?: string;
20
+ /**
21
+ * Optional base prefix for this tab's navigation subtree, e.g. '/mail'.
22
+ * Used by Router registry building via TabBar.getNodeChildren().
23
+ */
24
+ tabPrefix?: string;
20
25
  };
21
26
  export type TabBarProps = {
22
27
  onTabPress: (index: number) => void;
@@ -27,8 +32,25 @@ export type TabBarDescriptor = {
27
32
  renderer?: ComponentType<TabBarProps>;
28
33
  };
29
34
  type TabBarConfig = Omit<InternalTabItem, 'tabKey' | 'key'> & {
35
+ /**
36
+ * Legacy content type: a stack rendered in the tab.
37
+ */
30
38
  stack?: NavigationStack;
39
+ /**
40
+ * New content type: any NavigationNode (e.g. SplitView).
41
+ */
42
+ node?: NavigationNode;
43
+ /**
44
+ * Screen component rendered in the tab (no routing).
45
+ */
31
46
  screen?: React.ComponentType<any>;
47
+ /**
48
+ * Optional base prefix for node/stack routes, e.g. '/mail'.
49
+ */
50
+ prefix?: string;
51
+ /**
52
+ * Custom tab bar component (UI). Kept for compatibility.
53
+ */
32
54
  component?: ComponentType<TabBarProps>;
33
55
  };
34
56
  type TabBarOptions = {
@@ -40,6 +62,7 @@ export declare class TabBar implements NavigationNode {
40
62
  private readonly tabBarId;
41
63
  screens: Record<string, React.ComponentType<any>>;
42
64
  stacks: Record<string, NavigationStack>;
65
+ nodes: Record<string, NavigationNode>;
43
66
  private listeners;
44
67
  private state;
45
68
  constructor(options?: TabBarOptions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigmela/router",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "React Native Router",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",