@sigmela/router 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +177 -833
  2. package/lib/module/Navigation.js +1 -10
  3. package/lib/module/NavigationStack.js +168 -19
  4. package/lib/module/Router.js +1523 -501
  5. package/lib/module/RouterContext.js +1 -1
  6. package/lib/module/ScreenStack/ScreenStack.web.js +388 -117
  7. package/lib/module/ScreenStack/ScreenStackContext.js +21 -0
  8. package/lib/module/ScreenStack/animationHelpers.js +72 -0
  9. package/lib/module/ScreenStackItem/ScreenStackItem.js +2 -1
  10. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +76 -16
  11. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +2 -1
  12. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +1 -1
  13. package/lib/module/SplitView/RenderSplitView.native.js +85 -0
  14. package/lib/module/SplitView/RenderSplitView.web.js +109 -0
  15. package/lib/module/SplitView/SplitView.js +89 -0
  16. package/lib/module/SplitView/SplitViewContext.js +4 -0
  17. package/lib/module/SplitView/index.js +5 -0
  18. package/lib/module/SplitView/useSplitView.js +11 -0
  19. package/lib/module/StackRenderer.js +4 -2
  20. package/lib/module/TabBar/RenderTabBar.native.js +118 -33
  21. package/lib/module/TabBar/RenderTabBar.web.js +52 -47
  22. package/lib/module/TabBar/TabBar.js +116 -3
  23. package/lib/module/TabBar/index.js +4 -1
  24. package/lib/module/TabBar/useTabBarHeight.js +22 -0
  25. package/lib/module/index.js +3 -4
  26. package/lib/module/navigationNode.js +3 -0
  27. package/lib/module/styles.css +693 -28
  28. package/lib/typescript/src/NavigationStack.d.ts +25 -13
  29. package/lib/typescript/src/Router.d.ts +147 -34
  30. package/lib/typescript/src/RouterContext.d.ts +1 -1
  31. package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -2
  32. package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +31 -0
  33. package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +6 -0
  34. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +5 -1
  35. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +1 -1
  36. package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +8 -0
  37. package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +8 -0
  38. package/lib/typescript/src/SplitView/SplitView.d.ts +31 -0
  39. package/lib/typescript/src/SplitView/SplitViewContext.d.ts +3 -0
  40. package/lib/typescript/src/SplitView/index.d.ts +5 -0
  41. package/lib/typescript/src/SplitView/useSplitView.d.ts +2 -0
  42. package/lib/typescript/src/StackRenderer.d.ts +2 -1
  43. package/lib/typescript/src/TabBar/TabBar.d.ts +27 -3
  44. package/lib/typescript/src/TabBar/index.d.ts +3 -0
  45. package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +18 -0
  46. package/lib/typescript/src/createController.d.ts +1 -0
  47. package/lib/typescript/src/index.d.ts +4 -3
  48. package/lib/typescript/src/navigationNode.d.ts +41 -0
  49. package/lib/typescript/src/types.d.ts +29 -32
  50. package/package.json +6 -5
  51. package/lib/module/web/TransitionStack.js +0 -227
  52. package/lib/typescript/src/web/TransitionStack.d.ts +0 -21
@@ -4,7 +4,7 @@ import { StackRenderer } from "../StackRenderer.js";
4
4
  import { TabBarContext } from "./TabBarContext.js";
5
5
  import { useRouter } from "../RouterContext.js";
6
6
  import { TabIcon } from './TabIcon';
7
- import { useCallback, useSyncExternalStore, memo, useEffect, useMemo } from 'react';
7
+ import { useCallback, useSyncExternalStore, memo, useMemo } from 'react';
8
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  const isImageSource = value => {
10
10
  if (value == null) return false;
@@ -18,9 +18,22 @@ const isImageSource = value => {
18
18
  return false;
19
19
  };
20
20
  const toColorString = c => typeof c === 'string' ? c : undefined;
21
-
22
- //
23
-
21
+ const TabStackRenderer = /*#__PURE__*/memo(({
22
+ stack,
23
+ appearance
24
+ }) => {
25
+ const router = useRouter();
26
+ const stackId = stack.getId();
27
+ const subscribe = useCallback(cb => router.subscribeStack(stackId, cb), [router, stackId]);
28
+ const get = useCallback(() => router.getStackHistory(stackId), [router, stackId]);
29
+ const history = useSyncExternalStore(subscribe, get, get);
30
+ return /*#__PURE__*/_jsx(StackRenderer, {
31
+ appearance: appearance,
32
+ stack: stack,
33
+ history: history
34
+ });
35
+ });
36
+ TabStackRenderer.displayName = 'TabStackRenderer';
24
37
  export const RenderTabBar = /*#__PURE__*/memo(({
25
38
  tabBar,
26
39
  appearance
@@ -33,9 +46,6 @@ export const RenderTabBar = /*#__PURE__*/memo(({
33
46
  index,
34
47
  config
35
48
  } = snapshot;
36
- useEffect(() => {
37
- router.ensureTabSeed(index);
38
- }, [index, router]);
39
49
  const focusedTab = tabs[index];
40
50
  const stack = focusedTab ? tabBar.stacks[focusedTab.tabKey] : undefined;
41
51
  const Screen = focusedTab ? tabBar.screens[focusedTab.tabKey] : undefined;
@@ -44,22 +54,21 @@ export const RenderTabBar = /*#__PURE__*/memo(({
44
54
  if (!targetTab) return;
45
55
  const targetStack = tabBar.stacks[targetTab.tabKey];
46
56
  if (targetStack) {
47
- // Prefer last visited route in the target stack; otherwise first route
48
- const stackId = targetStack.getId();
49
- const history = router.getStackHistory(stackId);
50
- const last = history.length ? history[history.length - 1] : undefined;
51
- const toPath = last?.path ?? targetStack.getFirstRoute()?.path;
52
- if (toPath) {
53
- const currentPath = router.getVisibleRoute()?.path;
54
- if (nextIndex === index && toPath === currentPath) return;
55
- // Use replace to avoid duplicating history entries when switching tabs
56
- router.replace(toPath, true);
57
+ // Keep TabBar UI in sync immediately.
58
+ if (nextIndex !== index) {
59
+ tabBar.onIndexChange(nextIndex);
60
+ }
61
+ const firstRoutePath = targetStack.getFirstRoute()?.path;
62
+ if (firstRoutePath) {
63
+ // Web behavior: reset all preserved stacks when switching tabs.
64
+ // This keeps browser URL and Router state always consistent.
65
+ router.reset(firstRoutePath);
57
66
  return;
58
67
  }
59
68
  }
60
-
61
- // Fallback: just switch tab index (no history push if there is no path)
62
- router.onTabIndexChange(nextIndex);
69
+ if (nextIndex !== index) {
70
+ tabBar.onIndexChange(nextIndex);
71
+ }
63
72
  }, [router, tabBar, tabs, index]);
64
73
  const tabBarStyle = useMemo(() => {
65
74
  const tabBarBg = toColorString(appearance?.tabBar?.backgroundColor);
@@ -73,27 +82,23 @@ export const RenderTabBar = /*#__PURE__*/memo(({
73
82
  fontWeight: appearance?.tabBar?.title?.fontWeight,
74
83
  fontStyle: appearance?.tabBar?.title?.fontStyle
75
84
  }), [appearance?.tabBar?.title?.fontFamily, appearance?.tabBar?.title?.fontSize, appearance?.tabBar?.title?.fontWeight, appearance?.tabBar?.title?.fontStyle]);
76
-
77
- // If a custom component is provided, render it instead of the default web tab bar
78
85
  const CustomTabBar = config.component;
79
- return /*#__PURE__*/_jsx("div", {
80
- className: "screen-stack-item",
81
- "data-presentation": "push",
82
- "data-phase": "active",
83
- children: /*#__PURE__*/_jsx(TabBarContext.Provider, {
84
- value: tabBar,
85
- children: /*#__PURE__*/_jsxs("div", {
86
- className: "tab-stacks-container",
87
- children: [stack ? /*#__PURE__*/_jsx(StackRenderer, {
88
- appearance: appearance,
89
- stack: stack
90
- }) : Screen ? /*#__PURE__*/_jsx(Screen, {}) : null, CustomTabBar ? /*#__PURE__*/_jsx(CustomTabBar, {
91
- onTabPress: onTabClick,
92
- activeIndex: index,
93
- tabs: tabs
94
- }) : /*#__PURE__*/_jsx("div", {
95
- className: "tab-bar",
96
- style: tabBarStyle,
86
+ return /*#__PURE__*/_jsx(TabBarContext.Provider, {
87
+ value: tabBar,
88
+ children: /*#__PURE__*/_jsxs("div", {
89
+ className: "tab-stacks-container",
90
+ children: [stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
91
+ appearance: appearance,
92
+ stack: stack
93
+ }) : Screen ? /*#__PURE__*/_jsx(Screen, {}) : null, CustomTabBar ? /*#__PURE__*/_jsx(CustomTabBar, {
94
+ onTabPress: onTabClick,
95
+ activeIndex: index,
96
+ tabs: tabs
97
+ }) : /*#__PURE__*/_jsx("div", {
98
+ className: "tab-bar",
99
+ style: tabBarStyle,
100
+ children: /*#__PURE__*/_jsx("div", {
101
+ className: "tab-bar-inner",
97
102
  children: tabs.map((tab, i) => {
98
103
  const isActive = i === index;
99
104
  const iconTint = toColorString(isActive ? appearance?.tabBar?.iconColorActive : appearance?.tabBar?.iconColor);
@@ -113,18 +118,18 @@ export const RenderTabBar = /*#__PURE__*/memo(({
113
118
  source: tab.icon,
114
119
  tintColor: iconTint
115
120
  }) : null
116
- }), tab.badgeValue ? /*#__PURE__*/_jsx("span", {
117
- className: "tab-item-label-badge",
118
- children: tab.badgeValue
119
- }) : null, /*#__PURE__*/_jsx("div", {
121
+ }), /*#__PURE__*/_jsx("div", {
120
122
  className: "tab-item-label",
121
123
  style: labelStyle,
122
124
  children: tab.title
123
- })]
125
+ }), tab.badgeValue ? /*#__PURE__*/_jsx("span", {
126
+ className: "tab-item-label-badge",
127
+ children: tab.badgeValue
128
+ }) : null]
124
129
  }, tab.tabKey);
125
130
  })
126
- })]
127
- })
131
+ })
132
+ })]
128
133
  })
129
134
  });
130
135
  });
@@ -1,12 +1,16 @@
1
1
  "use strict";
2
2
 
3
- // Internal representation used by TabBar to support unified icon source while keeping original android props
3
+ import React from 'react';
4
+ import { RenderTabBar } from './RenderTabBar';
5
+
6
+ // Legacy icon format for backward compatibility
4
7
 
5
8
  export class TabBar {
6
9
  screens = {};
7
10
  stacks = {};
8
11
  listeners = new Set();
9
12
  constructor(options = {}) {
13
+ this.tabBarId = `tabbar-${Math.random().toString(36).slice(2)}`;
10
14
  this.state = {
11
15
  tabs: [],
12
16
  index: options.initialIndex ?? 0,
@@ -15,6 +19,9 @@ export class TabBar {
15
19
  }
16
20
  };
17
21
  }
22
+ getId() {
23
+ return this.tabBarId;
24
+ }
18
25
  addTab(tab) {
19
26
  const {
20
27
  key,
@@ -22,9 +29,12 @@ export class TabBar {
22
29
  } = tab;
23
30
  const nextIndex = this.state.tabs.length;
24
31
  const tabKey = key ?? `tab-${nextIndex}`;
25
- this.state.tabs.push({
32
+ const nextTabs = [...this.state.tabs, {
26
33
  tabKey,
27
34
  ...rest
35
+ }];
36
+ this.setState({
37
+ tabs: nextTabs
28
38
  });
29
39
  if (tab.stack) {
30
40
  this.stacks[tabKey] = tab.stack;
@@ -65,7 +75,17 @@ export class TabBar {
65
75
  this.notifyListeners();
66
76
  }
67
77
  notifyListeners() {
68
- this.listeners.forEach(listener => listener());
78
+ // Do not allow one listener to break all others.
79
+ for (const listener of Array.from(this.listeners)) {
80
+ try {
81
+ listener();
82
+ } catch (e) {
83
+ // TabBar has no debug flag; keep behavior quiet in production.
84
+ if (__DEV__) {
85
+ console.error('[TabBar] listener error', e);
86
+ }
87
+ }
88
+ }
69
89
  }
70
90
  subscribe(listener) {
71
91
  this.listeners.add(listener);
@@ -73,4 +93,97 @@ export class TabBar {
73
93
  this.listeners.delete(listener);
74
94
  };
75
95
  }
96
+ getTabs() {
97
+ return this.state.tabs.slice();
98
+ }
99
+ getInitialIndex() {
100
+ return this.state.index ?? 0;
101
+ }
102
+ getActiveChildId() {
103
+ const activeTab = this.state.tabs[this.state.index];
104
+ if (!activeTab) return undefined;
105
+ const stack = this.stacks[activeTab.tabKey];
106
+ return stack?.getId();
107
+ }
108
+ switchToRoute(routeId) {
109
+ const idx = this.findTabIndexByRoute(routeId);
110
+ if (idx === -1) return;
111
+ if (idx === this.state.index) return;
112
+ this.setState({
113
+ index: idx
114
+ });
115
+ }
116
+ hasRoute(routeId) {
117
+ return this.findTabIndexByRoute(routeId) !== -1;
118
+ }
119
+ setActiveChildByRoute(routeId) {
120
+ this.switchToRoute(routeId);
121
+ }
122
+ getNodeRoutes() {
123
+ return [];
124
+ }
125
+ getNodeChildren() {
126
+ const children = [];
127
+ for (let idx = 0; idx < this.state.tabs.length; idx++) {
128
+ const tab = this.state.tabs[idx];
129
+ const stack = tab ? this.stacks[tab.tabKey] : undefined;
130
+ if (stack) {
131
+ children.push({
132
+ prefix: '',
133
+ node: stack,
134
+ onMatch: () => this.onIndexChange(idx)
135
+ });
136
+ }
137
+ }
138
+ return children;
139
+ }
140
+ getRenderer() {
141
+ // eslint-disable-next-line consistent-this
142
+ const tabBarInstance = this;
143
+ return function TabBarScreen(props) {
144
+ return /*#__PURE__*/React.createElement(RenderTabBar, {
145
+ tabBar: tabBarInstance,
146
+ appearance: props?.appearance
147
+ });
148
+ };
149
+ }
150
+ seed() {
151
+ const activeTab = this.state.tabs[this.state.index];
152
+ if (!activeTab) return null;
153
+ const stack = this.stacks[activeTab.tabKey];
154
+ if (!stack) return null;
155
+ const firstRoute = stack.getFirstRoute();
156
+ if (!firstRoute) return null;
157
+ return {
158
+ routeId: firstRoute.routeId,
159
+ path: firstRoute.path,
160
+ stackId: stack.getId()
161
+ };
162
+ }
163
+ findTabIndexByRoute(routeId) {
164
+ for (let i = 0; i < this.state.tabs.length; i++) {
165
+ 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);
169
+ if (hasRoute) {
170
+ return i;
171
+ }
172
+ }
173
+ return -1;
174
+ }
175
+ nodeHasRoute(node, routeId) {
176
+ const routes = node.getNodeRoutes();
177
+ for (const r of routes) {
178
+ if (r.routeId === routeId) return true;
179
+ if (r.childNode) {
180
+ if (this.nodeHasRoute(r.childNode, routeId)) return true;
181
+ }
182
+ }
183
+ const children = node.getNodeChildren();
184
+ for (const child of children) {
185
+ if (this.nodeHasRoute(child.node, routeId)) return true;
186
+ }
187
+ return false;
188
+ }
76
189
  }
@@ -1,3 +1,6 @@
1
1
  "use strict";
2
2
 
3
- export { RenderTabBar } from './RenderTabBar';
3
+ export { RenderTabBar } from './RenderTabBar';
4
+ export { TabBar } from "./TabBar.js";
5
+ export { useTabBar } from "./useTabBar.js";
6
+ export { useTabBarHeight, TAB_BAR_HEIGHT } from "./useTabBarHeight.js";
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The height of the TabBar in pixels
5
+ */
6
+ export const TAB_BAR_HEIGHT = 57;
7
+
8
+ /**
9
+ * Hook that returns the height of the TabBar
10
+ * @returns {number} The height of the TabBar in pixels
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const TabBarExample = () => {
15
+ * const tabBarHeight = useTabBarHeight();
16
+ * return <div style={{ paddingBottom: tabBarHeight }}>Content</div>;
17
+ * };
18
+ * ```
19
+ */
20
+ export const useTabBarHeight = () => {
21
+ return TAB_BAR_HEIGHT;
22
+ };
@@ -1,11 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  export { useTabBar } from "./TabBar/useTabBar.js";
4
+ export { useTabBarHeight, TAB_BAR_HEIGHT } from "./TabBar/useTabBarHeight.js";
4
5
  export { TabBar } from "./TabBar/TabBar.js";
5
- /**
6
- * Navigation System
7
- */
8
-
6
+ export { SplitView } from "./SplitView/SplitView.js";
7
+ export { useSplitView } from "./SplitView/useSplitView.js";
9
8
  export { Router } from "./Router.js";
10
9
  export { Navigation } from "./Navigation.js";
11
10
  export { createController } from "./createController.js";
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ export {};