@sigmela/router 0.2.8 → 0.3.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 (38) hide show
  1. package/lib/module/Drawer/Drawer.js +250 -0
  2. package/lib/module/Drawer/DrawerContext.js +4 -0
  3. package/lib/module/Drawer/DrawerIcon.web.js +47 -0
  4. package/lib/module/Drawer/RenderDrawer.native.js +241 -0
  5. package/lib/module/Drawer/RenderDrawer.web.js +197 -0
  6. package/lib/module/Drawer/useDrawer.js +11 -0
  7. package/lib/module/Navigation.js +4 -2
  8. package/lib/module/NavigationStack.js +14 -4
  9. package/lib/module/Router.js +214 -60
  10. package/lib/module/RouterContext.js +1 -1
  11. package/lib/module/ScreenStack/ScreenStack.web.js +78 -12
  12. package/lib/module/ScreenStackItem/ScreenStackItem.js +7 -7
  13. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +65 -6
  14. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +4 -5
  15. package/lib/module/SplitView/RenderSplitView.web.js +5 -7
  16. package/lib/module/SplitView/SplitView.js +10 -2
  17. package/lib/module/StackRenderer.js +10 -4
  18. package/lib/module/TabBar/RenderTabBar.native.js +10 -9
  19. package/lib/module/TabBar/RenderTabBar.web.js +25 -2
  20. package/lib/module/TabBar/TabBar.js +8 -1
  21. package/lib/module/TabBar/TabIcon.web.js +12 -7
  22. package/lib/module/index.js +2 -0
  23. package/lib/module/styles.css +246 -91
  24. package/lib/typescript/src/Drawer/Drawer.d.ts +100 -0
  25. package/lib/typescript/src/Drawer/DrawerContext.d.ts +3 -0
  26. package/lib/typescript/src/Drawer/DrawerIcon.web.d.ts +7 -0
  27. package/lib/typescript/src/Drawer/RenderDrawer.native.d.ts +8 -0
  28. package/lib/typescript/src/Drawer/RenderDrawer.web.d.ts +8 -0
  29. package/lib/typescript/src/Drawer/useDrawer.d.ts +2 -0
  30. package/lib/typescript/src/NavigationStack.d.ts +1 -0
  31. package/lib/typescript/src/Router.d.ts +13 -0
  32. package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +1 -1
  33. package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +1 -1
  34. package/lib/typescript/src/SplitView/SplitView.d.ts +2 -0
  35. package/lib/typescript/src/TabBar/TabBar.d.ts +1 -0
  36. package/lib/typescript/src/index.d.ts +5 -0
  37. package/lib/typescript/src/types.d.ts +1 -1
  38. package/package.json +15 -4
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { RenderDrawer } from './RenderDrawer';
5
+ export class Drawer {
6
+ screens = {};
7
+ stacks = {};
8
+ nodes = {};
9
+ listeners = new Set();
10
+ openListeners = new Set();
11
+ constructor(options = {}) {
12
+ this.drawerId = `drawer-${Math.random().toString(36).slice(2)}`;
13
+ this.width = options.width ?? 280;
14
+ this.state = {
15
+ tabs: [],
16
+ index: options.initialIndex ?? 0,
17
+ config: {
18
+ component: options.component
19
+ },
20
+ isOpen: false
21
+ };
22
+ }
23
+ getId() {
24
+ return this.drawerId;
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(`Drawer.addTab: exactly one of { stack, node, screen } must be provided (got ${sourcesCount})`);
30
+ }
31
+ const {
32
+ key,
33
+ ...rest
34
+ } = tab;
35
+ const nextIndex = this.state.tabs.length;
36
+ const tabKey = key ?? `drawer-item-${nextIndex}`;
37
+ const nextTabs = [...this.state.tabs, {
38
+ tabKey,
39
+ tabPrefix: tab.prefix,
40
+ title: rest.title,
41
+ icon: rest.icon,
42
+ selectedIcon: rest.selectedIcon,
43
+ badgeValue: rest.badgeValue
44
+ }];
45
+ this.setState({
46
+ tabs: nextTabs
47
+ });
48
+ if (tab.stack) {
49
+ this.stacks[tabKey] = tab.stack;
50
+ } else if (tab.node) {
51
+ this.nodes[tabKey] = tab.node;
52
+ } else if (tab.screen) {
53
+ this.screens[tabKey] = tab.screen;
54
+ }
55
+ return this;
56
+ }
57
+ setBadge(tabIndex, badge) {
58
+ this.setState({
59
+ tabs: this.state.tabs.map((item, index) => index === tabIndex ? {
60
+ ...item,
61
+ badgeValue: badge ?? undefined
62
+ } : item)
63
+ });
64
+ }
65
+ setTabBarConfig(config) {
66
+ this.setState({
67
+ config: {
68
+ ...this.state.config,
69
+ ...config
70
+ }
71
+ });
72
+ }
73
+ onIndexChange(index) {
74
+ this.setState({
75
+ index
76
+ });
77
+ }
78
+
79
+ // ---- Open/Close state ----
80
+
81
+ open() {
82
+ if (!this.state.isOpen) {
83
+ this.setState({
84
+ isOpen: true
85
+ });
86
+ this.notifyOpenListeners(true);
87
+ }
88
+ }
89
+ close() {
90
+ if (this.state.isOpen) {
91
+ this.setState({
92
+ isOpen: false
93
+ });
94
+ this.notifyOpenListeners(false);
95
+ }
96
+ }
97
+ toggle() {
98
+ if (this.state.isOpen) {
99
+ this.close();
100
+ } else {
101
+ this.open();
102
+ }
103
+ }
104
+ getIsOpen() {
105
+ return this.state.isOpen;
106
+ }
107
+ subscribeOpenState(listener) {
108
+ this.openListeners.add(listener);
109
+ return () => {
110
+ this.openListeners.delete(listener);
111
+ };
112
+ }
113
+ notifyOpenListeners(isOpen) {
114
+ for (const listener of Array.from(this.openListeners)) {
115
+ try {
116
+ listener(isOpen);
117
+ } catch (e) {
118
+ if (__DEV__) {
119
+ console.error('[Drawer] openListener error', e);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ // ---- Standard TabBar-like methods ----
126
+
127
+ getState = () => {
128
+ return this.state;
129
+ };
130
+ setState(state) {
131
+ this.state = {
132
+ ...this.state,
133
+ ...state
134
+ };
135
+ this.notifyListeners();
136
+ }
137
+ notifyListeners() {
138
+ for (const listener of Array.from(this.listeners)) {
139
+ try {
140
+ listener();
141
+ } catch (e) {
142
+ if (__DEV__) {
143
+ console.error('[Drawer] listener error', e);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ subscribe(listener) {
149
+ this.listeners.add(listener);
150
+ return () => {
151
+ this.listeners.delete(listener);
152
+ };
153
+ }
154
+ getTabs() {
155
+ return this.state.tabs.slice();
156
+ }
157
+ getInitialIndex() {
158
+ return this.state.index ?? 0;
159
+ }
160
+ getActiveChildId() {
161
+ const activeTab = this.state.tabs[this.state.index];
162
+ if (!activeTab) return undefined;
163
+ const node = this.nodes[activeTab.tabKey] ?? this.stacks[activeTab.tabKey];
164
+ return node?.getId();
165
+ }
166
+ switchToRoute(routeId) {
167
+ const idx = this.findTabIndexByRoute(routeId);
168
+ if (idx === -1) return;
169
+ if (idx === this.state.index) return;
170
+ this.setState({
171
+ index: idx
172
+ });
173
+ }
174
+ hasRoute(routeId) {
175
+ return this.findTabIndexByRoute(routeId) !== -1;
176
+ }
177
+ setActiveChildByRoute(routeId) {
178
+ this.switchToRoute(routeId);
179
+ }
180
+ getNodeRoutes() {
181
+ return [];
182
+ }
183
+ getNodeChildren() {
184
+ const children = [];
185
+ for (let idx = 0; idx < this.state.tabs.length; idx++) {
186
+ const tab = this.state.tabs[idx];
187
+ const node = tab ? this.nodes[tab.tabKey] ?? this.stacks[tab.tabKey] : undefined;
188
+ if (node) {
189
+ children.push({
190
+ prefix: tab?.tabPrefix ?? '',
191
+ node,
192
+ onMatch: () => this.onIndexChange(idx)
193
+ });
194
+ }
195
+ }
196
+ return children;
197
+ }
198
+ getRenderer() {
199
+ const drawerInstance = this;
200
+ return function DrawerScreen(props) {
201
+ return /*#__PURE__*/React.createElement(RenderDrawer, {
202
+ drawer: drawerInstance,
203
+ appearance: props?.appearance
204
+ });
205
+ };
206
+ }
207
+ seed() {
208
+ const activeTab = this.state.tabs[this.state.index];
209
+ if (!activeTab) return null;
210
+ const node = this.nodes[activeTab.tabKey];
211
+ if (node) {
212
+ return node.seed?.() ?? null;
213
+ }
214
+ const stack = this.stacks[activeTab.tabKey];
215
+ if (!stack) return null;
216
+ const firstRoute = stack.getFirstRoute();
217
+ if (!firstRoute) return null;
218
+ return {
219
+ routeId: firstRoute.routeId,
220
+ path: firstRoute.path,
221
+ stackId: stack.getId()
222
+ };
223
+ }
224
+ findTabIndexByRoute(routeId) {
225
+ for (let i = 0; i < this.state.tabs.length; i++) {
226
+ const tab = this.state.tabs[i];
227
+ const node = tab ? this.nodes[tab.tabKey] ?? this.stacks[tab.tabKey] : undefined;
228
+ if (!node) continue;
229
+ const hasRoute = this.nodeHasRoute(node, routeId);
230
+ if (hasRoute) {
231
+ return i;
232
+ }
233
+ }
234
+ return -1;
235
+ }
236
+ nodeHasRoute(node, routeId) {
237
+ const routes = node.getNodeRoutes();
238
+ for (const r of routes) {
239
+ if (r.routeId === routeId) return true;
240
+ if (r.childNode) {
241
+ if (this.nodeHasRoute(r.childNode, routeId)) return true;
242
+ }
243
+ }
244
+ const children = node.getNodeChildren();
245
+ for (const child of children) {
246
+ if (this.nodeHasRoute(child.node, routeId)) return true;
247
+ }
248
+ return false;
249
+ }
250
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ import { createContext } from 'react';
4
+ export const DrawerContext = /*#__PURE__*/createContext(null);
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ import { memo, useMemo } from 'react';
4
+ import { Image } from 'react-native';
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ const resolveImageUri = source => {
7
+ if (!source) return undefined;
8
+ const resolved = typeof Image.resolveAssetSource === 'function' ? Image.resolveAssetSource(source) : undefined;
9
+ if (resolved?.uri) return resolved.uri;
10
+ if (Array.isArray(source)) {
11
+ const first = source[0];
12
+ if (first && typeof first === 'object' && 'uri' in first) return first.uri;
13
+ }
14
+ if (typeof source === 'object' && source && 'uri' in source) return source.uri;
15
+ return undefined;
16
+ };
17
+ export const DrawerIcon = /*#__PURE__*/memo(({
18
+ source,
19
+ tintColor
20
+ }) => {
21
+ const iconUri = resolveImageUri(source);
22
+ const useMask = Boolean(tintColor && iconUri);
23
+ const maskStyle = useMemo(() => {
24
+ if (!useMask || !iconUri) return undefined;
25
+ return {
26
+ backgroundColor: tintColor,
27
+ WebkitMaskImage: `url(${iconUri})`,
28
+ WebkitMaskSize: 'contain',
29
+ WebkitMaskRepeat: 'no-repeat',
30
+ WebkitMaskPosition: 'center',
31
+ maskImage: `url(${iconUri})`,
32
+ maskSize: 'contain',
33
+ maskRepeat: 'no-repeat',
34
+ maskPosition: 'center',
35
+ width: '100%',
36
+ height: '100%'
37
+ };
38
+ }, [tintColor, iconUri, useMask]);
39
+ if (maskStyle) {
40
+ return /*#__PURE__*/_jsx("div", {
41
+ style: maskStyle
42
+ });
43
+ }
44
+ return /*#__PURE__*/_jsx(Image, {
45
+ source: source
46
+ });
47
+ });
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+
3
+ import { StackRenderer } from "../StackRenderer.js";
4
+ import { DrawerContext } from "./DrawerContext.js";
5
+ import { useRouter } from "../RouterContext.js";
6
+ import { ScreenStackItem } from 'react-native-screens';
7
+ import { Pressable, StyleSheet, View, Text } from 'react-native';
8
+ import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo } from 'react';
9
+ import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ const TIMING_CONFIG = {
12
+ duration: 300,
13
+ easing: Easing.bezier(0.22, 0.61, 0.36, 1)
14
+ };
15
+ const DrawerStackRenderer = /*#__PURE__*/memo(({
16
+ stack,
17
+ appearance
18
+ }) => {
19
+ const router = useRouter();
20
+ const stackId = stack.getId();
21
+ const subscribe = useCallback(cb => router.subscribeStack(stackId, cb), [router, stackId]);
22
+ const get = useCallback(() => router.getStackHistory(stackId), [router, stackId]);
23
+ const history = useSyncExternalStore(subscribe, get, get);
24
+ return /*#__PURE__*/_jsx(StackRenderer, {
25
+ appearance: appearance,
26
+ stackId: stackId,
27
+ history: history
28
+ });
29
+ });
30
+ DrawerStackRenderer.displayName = 'DrawerStackRenderer';
31
+ const DrawerNodeRenderer = /*#__PURE__*/memo(({
32
+ node,
33
+ appearance
34
+ }) => {
35
+ const Renderer = useMemo(() => node.getRenderer(), [node]);
36
+ return /*#__PURE__*/_jsx(Renderer, {
37
+ appearance: appearance
38
+ });
39
+ });
40
+ DrawerNodeRenderer.displayName = 'DrawerNodeRenderer';
41
+ export const RenderDrawer = /*#__PURE__*/memo(({
42
+ drawer,
43
+ appearance = {}
44
+ }) => {
45
+ const router = useRouter();
46
+ const drawerWidth = drawer.width;
47
+ const subscribe = useCallback(cb => drawer.subscribe(cb), [drawer]);
48
+ const snapshot = useSyncExternalStore(subscribe, drawer.getState, drawer.getState);
49
+ const {
50
+ tabs,
51
+ index,
52
+ config,
53
+ isOpen
54
+ } = snapshot;
55
+
56
+ // Reanimated shared value for drawer position
57
+ const translateX = useSharedValue(0);
58
+ const overlayOpacity = useSharedValue(0);
59
+ useEffect(() => {
60
+ translateX.value = withTiming(isOpen ? drawerWidth : 0, TIMING_CONFIG);
61
+ overlayOpacity.value = withTiming(isOpen ? 0.3 : 0, TIMING_CONFIG);
62
+ }, [isOpen, drawerWidth, translateX, overlayOpacity]);
63
+ const drawerStyle = useAnimatedStyle(() => ({
64
+ transform: [{
65
+ translateX: translateX.value - drawerWidth
66
+ }]
67
+ }));
68
+ const mainStyle = useAnimatedStyle(() => ({
69
+ transform: [{
70
+ translateX: translateX.value
71
+ }]
72
+ }));
73
+ const overlayStyle = useAnimatedStyle(() => ({
74
+ opacity: overlayOpacity.value,
75
+ pointerEvents: isOpen ? 'auto' : 'none'
76
+ }));
77
+ const onItemPress = useCallback(nextIndex => {
78
+ const targetTab = tabs[nextIndex];
79
+ if (!targetTab) return;
80
+ const targetStack = drawer.stacks[targetTab.tabKey];
81
+ const targetNode = drawer.nodes[targetTab.tabKey];
82
+ if (nextIndex !== index) {
83
+ drawer.onIndexChange(nextIndex);
84
+ }
85
+ if (targetStack) {
86
+ const stackId = targetStack.getId();
87
+ const stackHistory = router.getStackHistory(stackId);
88
+ if (stackHistory.length === 0) {
89
+ const firstRoute = targetStack.getFirstRoute();
90
+ if (firstRoute?.path) {
91
+ router.navigate(firstRoute.path);
92
+ }
93
+ }
94
+ } else if (targetNode) {
95
+ const nodeId = targetNode.getId?.();
96
+ if (nodeId) {
97
+ const nodeHistory = router.getStackHistory(nodeId);
98
+ if (nodeHistory.length === 0) {
99
+ const seed = targetNode.seed?.();
100
+ if (seed?.path) {
101
+ const prefix = targetTab.tabPrefix ?? '';
102
+ const fullPath = prefix && !seed.path.startsWith(prefix) ? `${prefix}${seed.path.startsWith('/') ? '' : '/'}${seed.path}` : seed.path;
103
+ router.navigate(fullPath);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ drawer.close();
109
+ }, [tabs, drawer, index, router]);
110
+ const handleOverlayPress = useCallback(() => {
111
+ drawer.close();
112
+ }, [drawer]);
113
+ const [visited, setVisited] = useState({});
114
+ useEffect(() => {
115
+ const key = tabs[index]?.tabKey;
116
+ if (key) {
117
+ setVisited(prev => prev[key] ? prev : {
118
+ ...prev,
119
+ [key]: true
120
+ });
121
+ }
122
+ }, [tabs, index]);
123
+ const CustomDrawer = config.component;
124
+ return /*#__PURE__*/_jsx(ScreenStackItem, {
125
+ screenId: "root-drawer",
126
+ headerConfig: {
127
+ hidden: true
128
+ },
129
+ style: StyleSheet.absoluteFill,
130
+ stackAnimation: "slide_from_right",
131
+ children: /*#__PURE__*/_jsx(DrawerContext.Provider, {
132
+ value: drawer,
133
+ children: /*#__PURE__*/_jsxs(View, {
134
+ style: styles.container,
135
+ children: [/*#__PURE__*/_jsx(Animated.View, {
136
+ style: [styles.sidebar, {
137
+ width: drawerWidth
138
+ }, drawerStyle],
139
+ children: CustomDrawer ? /*#__PURE__*/_jsx(CustomDrawer, {
140
+ onItemPress: onItemPress,
141
+ activeIndex: index,
142
+ items: tabs,
143
+ isOpen: isOpen,
144
+ onClose: handleOverlayPress
145
+ }) : /*#__PURE__*/_jsx(View, {
146
+ style: styles.sidebarContent,
147
+ children: tabs.map((tab, i) => {
148
+ const isActive = i === index;
149
+ return /*#__PURE__*/_jsx(Pressable, {
150
+ onPress: () => onItemPress(i),
151
+ style: [styles.item, isActive && styles.itemActive],
152
+ children: /*#__PURE__*/_jsx(Text, {
153
+ style: [styles.itemText, isActive && styles.itemTextActive],
154
+ children: tab.title
155
+ })
156
+ }, tab.tabKey);
157
+ })
158
+ })
159
+ }), /*#__PURE__*/_jsx(Animated.View, {
160
+ style: [styles.overlay, overlayStyle],
161
+ children: /*#__PURE__*/_jsx(Pressable, {
162
+ style: StyleSheet.absoluteFill,
163
+ onPress: handleOverlayPress
164
+ })
165
+ }), /*#__PURE__*/_jsx(Animated.View, {
166
+ style: [styles.main, mainStyle],
167
+ children: tabs.filter(t => visited[t.tabKey]).map(tab => {
168
+ const isActive = tab.tabKey === tabs[index]?.tabKey;
169
+ const stackForTab = drawer.stacks[tab.tabKey];
170
+ const nodeForTab = drawer.nodes[tab.tabKey];
171
+ const ScreenForTab = drawer.screens[tab.tabKey];
172
+ return /*#__PURE__*/_jsx(View, {
173
+ style: [styles.flex, !isActive && styles.hidden],
174
+ children: stackForTab ? /*#__PURE__*/_jsx(DrawerStackRenderer, {
175
+ appearance: appearance,
176
+ stack: stackForTab
177
+ }) : nodeForTab ? /*#__PURE__*/_jsx(DrawerNodeRenderer, {
178
+ appearance: appearance,
179
+ node: nodeForTab
180
+ }) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
181
+ }, `drawer-content-${tab.tabKey}`);
182
+ })
183
+ })]
184
+ })
185
+ })
186
+ });
187
+ });
188
+ const styles = StyleSheet.create({
189
+ container: {
190
+ flex: 1,
191
+ flexDirection: 'row',
192
+ overflow: 'hidden'
193
+ },
194
+ sidebar: {
195
+ position: 'absolute',
196
+ top: 0,
197
+ bottom: 0,
198
+ left: 0,
199
+ zIndex: 10,
200
+ backgroundColor: '#ffffff'
201
+ },
202
+ sidebarContent: {
203
+ flex: 1,
204
+ paddingTop: 12
205
+ },
206
+ main: {
207
+ flex: 1,
208
+ width: '100%'
209
+ },
210
+ overlay: {
211
+ ...StyleSheet.absoluteFillObject,
212
+ zIndex: 5,
213
+ backgroundColor: '#000000'
214
+ },
215
+ item: {
216
+ height: 44,
217
+ flexDirection: 'row',
218
+ alignItems: 'center',
219
+ paddingHorizontal: 12,
220
+ marginHorizontal: 12,
221
+ marginBottom: 4,
222
+ borderRadius: 10
223
+ },
224
+ itemActive: {
225
+ backgroundColor: 'rgba(201, 204, 209, 0.32)'
226
+ },
227
+ itemText: {
228
+ flex: 1,
229
+ fontSize: 14,
230
+ fontWeight: '600'
231
+ },
232
+ itemTextActive: {
233
+ fontWeight: '700'
234
+ },
235
+ flex: {
236
+ flex: 1
237
+ },
238
+ hidden: {
239
+ display: 'none'
240
+ }
241
+ });