@natsuneko-laboratory/react-native-desktop-navigation 0.1.0-alpha.12

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 (103) hide show
  1. package/DesktopNavigation.podspec +18 -0
  2. package/LICENSE +21 -0
  3. package/NATIVE.md +300 -0
  4. package/README.md +127 -0
  5. package/dist/core/NavigationContainer.d.ts +23 -0
  6. package/dist/core/NavigationContainer.js +68 -0
  7. package/dist/core/NavigationItem.d.ts +27 -0
  8. package/dist/core/NavigationItem.js +71 -0
  9. package/dist/core/Scene.d.ts +19 -0
  10. package/dist/core/Scene.js +124 -0
  11. package/dist/core/SelectionNavigator.d.ts +38 -0
  12. package/dist/core/SelectionNavigator.js +194 -0
  13. package/dist/core/builder.d.ts +40 -0
  14. package/dist/core/builder.js +81 -0
  15. package/dist/core/context.d.ts +26 -0
  16. package/dist/core/context.js +36 -0
  17. package/dist/core/focus.d.ts +14 -0
  18. package/dist/core/focus.js +39 -0
  19. package/dist/core/hooks.d.ts +8 -0
  20. package/dist/core/hooks.js +48 -0
  21. package/dist/core/navigation.d.ts +6 -0
  22. package/dist/core/navigation.js +57 -0
  23. package/dist/core/store.d.ts +39 -0
  24. package/dist/core/store.js +391 -0
  25. package/dist/core/types.d.ts +155 -0
  26. package/dist/core/types.js +2 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.js +46 -0
  29. package/dist/native/host.d.ts +100 -0
  30. package/dist/native/host.js +190 -0
  31. package/dist/native/icons.d.ts +56 -0
  32. package/dist/native/icons.js +55 -0
  33. package/dist/native/index.d.ts +13 -0
  34. package/dist/native/index.js +44 -0
  35. package/dist/native/sidebar.d.ts +39 -0
  36. package/dist/native/sidebar.js +126 -0
  37. package/dist/native/specs/DesktopNavigationHostNativeComponent.d.ts +10 -0
  38. package/dist/native/specs/DesktopNavigationHostNativeComponent.js +4 -0
  39. package/dist/native/split.d.ts +27 -0
  40. package/dist/native/split.js +105 -0
  41. package/dist/native/stack.d.ts +26 -0
  42. package/dist/native/stack.js +66 -0
  43. package/dist/platform/index.d.ts +52 -0
  44. package/dist/platform/index.js +28 -0
  45. package/dist/platform/macos.d.ts +2 -0
  46. package/dist/platform/macos.js +18 -0
  47. package/dist/platform/windows.d.ts +2 -0
  48. package/dist/platform/windows.js +18 -0
  49. package/dist/routers/index.d.ts +23 -0
  50. package/dist/routers/index.js +393 -0
  51. package/dist/routers/types.d.ts +116 -0
  52. package/dist/routers/types.js +2 -0
  53. package/dist/sidebar/index.d.ts +17 -0
  54. package/dist/sidebar/index.js +18 -0
  55. package/dist/split/index.d.ts +42 -0
  56. package/dist/split/index.js +199 -0
  57. package/dist/stack/index.d.ts +23 -0
  58. package/dist/stack/index.js +88 -0
  59. package/dist/tabs/index.d.ts +13 -0
  60. package/dist/tabs/index.js +15 -0
  61. package/docs/GUIDE.md +384 -0
  62. package/macos/DDNNavigationComponentView.h +4 -0
  63. package/macos/DDNNavigationComponentView.mm +40 -0
  64. package/macos/DDNNavigationView.swift +398 -0
  65. package/macos/DDNNavigationViewManager.mm +11 -0
  66. package/package.json +82 -0
  67. package/react-native.config.js +19 -0
  68. package/src/core/NavigationContainer.tsx +137 -0
  69. package/src/core/NavigationItem.tsx +149 -0
  70. package/src/core/Scene.tsx +194 -0
  71. package/src/core/SelectionNavigator.tsx +371 -0
  72. package/src/core/builder.tsx +147 -0
  73. package/src/core/context.tsx +43 -0
  74. package/src/core/focus.tsx +50 -0
  75. package/src/core/hooks.ts +42 -0
  76. package/src/core/navigation.ts +77 -0
  77. package/src/core/store.ts +471 -0
  78. package/src/core/types.ts +176 -0
  79. package/src/index.ts +36 -0
  80. package/src/native/host.tsx +377 -0
  81. package/src/native/icons.ts +110 -0
  82. package/src/native/index.ts +56 -0
  83. package/src/native/sidebar.tsx +230 -0
  84. package/src/native/specs/DesktopNavigationHostNativeComponent.ts +12 -0
  85. package/src/native/split.tsx +160 -0
  86. package/src/native/stack.tsx +157 -0
  87. package/src/platform/index.tsx +76 -0
  88. package/src/platform/macos.ts +15 -0
  89. package/src/platform/windows.ts +15 -0
  90. package/src/routers/index.ts +442 -0
  91. package/src/routers/types.ts +117 -0
  92. package/src/sidebar/index.tsx +42 -0
  93. package/src/split/index.tsx +350 -0
  94. package/src/stack/index.tsx +213 -0
  95. package/src/tabs/index.tsx +40 -0
  96. package/windows/DesktopNavigation/DesktopNavigation.def +3 -0
  97. package/windows/DesktopNavigation/DesktopNavigation.vcxproj +125 -0
  98. package/windows/DesktopNavigation/NavigationHost.cpp +432 -0
  99. package/windows/DesktopNavigation/ReactPackageProvider.cpp +9 -0
  100. package/windows/DesktopNavigation/ReactPackageProvider.h +10 -0
  101. package/windows/DesktopNavigation/ReactPackageProvider.idl +6 -0
  102. package/windows/DesktopNavigation/pch.cpp +1 -0
  103. package/windows/DesktopNavigation/pch.h +27 -0
@@ -0,0 +1,350 @@
1
+ import React from 'react';
2
+ import {
3
+ PanResponder,
4
+ View,
5
+ StyleSheet,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import { splitColumnConfig, type SplitNavigationState } from '../routers';
10
+ import { NavigatorStateContext, useNavigator } from '../core/builder';
11
+ import { Scene } from '../core/Scene';
12
+ import { SplitColumnContext, useNavigationTheme } from '../core/context';
13
+ import { consumeKey, DesktopView, eventHandled } from '../platform';
14
+ export interface SplitColumnProps {
15
+ id: string;
16
+ component: React.ComponentType;
17
+ minWidth?: number;
18
+ maxWidth?: number;
19
+ defaultWidth?: number;
20
+ /** Follow a directly nested Sidebar when it collapses (default: true). */
21
+ collapsible?: boolean;
22
+ style?: StyleProp<ViewStyle>;
23
+ contentStyle?: StyleProp<ViewStyle>;
24
+ /** Hide the entire resize handle, including its space and keyboard target. */
25
+ dividerShown?: boolean;
26
+ dividerStyle?: SplitDividerStyle;
27
+ renderDivider?: (props: SplitDividerRenderProps) => React.ReactNode;
28
+ }
29
+ export interface SplitDividerRenderProps {
30
+ columnId: string;
31
+ width: number;
32
+ dragging: boolean;
33
+ }
34
+ export type SplitDividerStyle =
35
+ | StyleProp<ViewStyle>
36
+ | ((props: SplitDividerRenderProps) => StyleProp<ViewStyle>);
37
+ export interface SplitNavigatorProps {
38
+ children: React.ReactNode;
39
+ id?: string;
40
+ style?: StyleProp<ViewStyle>;
41
+ columnStyle?: StyleProp<ViewStyle>;
42
+ /** Hide the entire resize handle, including its space and keyboard target. */
43
+ dividerShown?: boolean;
44
+ dividerStyle?: SplitDividerStyle;
45
+ renderDivider?: (props: SplitDividerRenderProps) => React.ReactNode;
46
+ layout?: (size: { width: number; height: number }) => string[];
47
+ onColumnResize?: (id: string, width: number) => void;
48
+ }
49
+ function Divider({
50
+ column,
51
+ width,
52
+ resize,
53
+ style,
54
+ renderContent,
55
+ onWidthChange,
56
+ }: {
57
+ column: SplitColumnProps;
58
+ width: number;
59
+ resize: (width: number) => void;
60
+ style?: SplitDividerStyle;
61
+ renderContent?: (props: SplitDividerRenderProps) => React.ReactNode;
62
+ onWidthChange: (width: number) => void;
63
+ }) {
64
+ const [dragging, setDragging] = React.useState(false);
65
+ const start = React.useRef({ width, pageX: 0 });
66
+ const latest = React.useRef({ width, resize });
67
+ latest.current = { width, resize };
68
+ const responder = React.useMemo(
69
+ () =>
70
+ PanResponder.create({
71
+ onStartShouldSetPanResponder: () => true,
72
+ onMoveShouldSetPanResponder: () => true,
73
+ onPanResponderGrant: (event) => {
74
+ start.current = {
75
+ width: latest.current.width,
76
+ pageX: event.nativeEvent.pageX,
77
+ };
78
+ setDragging(true);
79
+ },
80
+ // Use root coordinates: the handle itself moves during resizing.
81
+ onPanResponderMove: (event) =>
82
+ latest.current.resize(
83
+ start.current.width + event.nativeEvent.pageX - start.current.pageX,
84
+ ),
85
+ onPanResponderRelease: () => setDragging(false),
86
+ onPanResponderTerminate: () => setDragging(false),
87
+ onPanResponderTerminationRequest: () => false,
88
+ }),
89
+ [],
90
+ );
91
+ const { colors } = useNavigationTheme();
92
+ const context = { columnId: column.id, width, dragging };
93
+ return (
94
+ <DesktopView
95
+ {...responder.panHandlers}
96
+ testID={`split-divider-${column.id}`}
97
+ style={styles.dividerHitArea}
98
+ pointerEvents="box-only"
99
+ accessible
100
+ focusable
101
+ accessibilityRole="adjustable"
102
+ accessibilityLabel={`Resize ${column.id}`}
103
+ accessibilityValue={{
104
+ min: column.minWidth ?? 100,
105
+ max: column.maxWidth,
106
+ now: width,
107
+ }}
108
+ accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
109
+ onAccessibilityAction={(event) =>
110
+ resize(
111
+ width + (event.nativeEvent.actionName === 'increment' ? 10 : -10),
112
+ )
113
+ }
114
+ keyDownEvents={[{ key: 'ArrowLeft' }, { key: 'ArrowRight' }]}
115
+ onKeyDown={(event) => {
116
+ if (
117
+ !eventHandled(event) &&
118
+ ['ArrowLeft', 'ArrowRight'].includes(event.nativeEvent.key)
119
+ ) {
120
+ resize(width + (event.nativeEvent.key === 'ArrowRight' ? 10 : -10));
121
+ consumeKey(event);
122
+ }
123
+ }}
124
+ >
125
+ <View
126
+ testID={`split-divider-visual-${column.id}`}
127
+ onLayout={(event) => onWidthChange(event.nativeEvent.layout.width)}
128
+ accessible={false}
129
+ style={[
130
+ { flex: 1, width: 6, backgroundColor: colors.border },
131
+ typeof style === 'function' ? style(context) : style,
132
+ typeof column.dividerStyle === 'function'
133
+ ? column.dividerStyle(context)
134
+ : column.dividerStyle,
135
+ ]}
136
+ >
137
+ {(column.renderDivider ?? renderContent)?.(context)}
138
+ </View>
139
+ </DesktopView>
140
+ );
141
+ }
142
+ export function createSplitNavigator() {
143
+ function Column(_props: SplitColumnProps) {
144
+ return null;
145
+ }
146
+ function Navigator(props: SplitNavigatorProps) {
147
+ const columns: SplitColumnProps[] = [];
148
+ React.Children.forEach(props.children, (child) => {
149
+ if (child == null || typeof child === 'boolean') return;
150
+ if (!React.isValidElement(child) || child.type !== Column)
151
+ throw new Error(
152
+ 'Split.Navigator children must be Split.Column elements.',
153
+ );
154
+ columns.push(child.props as SplitColumnProps);
155
+ });
156
+ const { id, state, store } = useNavigator<SplitNavigationState>(
157
+ 'split',
158
+ { routes: columns.map((c) => ({ ...c, name: c.id })) },
159
+ props.id,
160
+ );
161
+ const [size, setSize] = React.useState<{
162
+ width: number;
163
+ height: number;
164
+ } | null>(null);
165
+ const [dividerWidths, setDividerWidths] = React.useState<
166
+ Record<string, number>
167
+ >({});
168
+ const [collapsedWidths, setCollapsedWidths] = React.useState<
169
+ Record<string, number | null>
170
+ >({});
171
+ const onSidebarChange = React.useCallback(
172
+ (columnId: string, width: number | null) => {
173
+ setCollapsedWidths((previous) => {
174
+ if (previous[columnId] === width) return previous;
175
+ return { ...previous, [columnId]: width };
176
+ });
177
+ },
178
+ [],
179
+ );
180
+ const layout = props.layout;
181
+ const dividerSpace = (ids: string[]) =>
182
+ ids.slice(0, -1).reduce((total, columnId) => {
183
+ const column = columns.find((c) => c.id === columnId);
184
+ return (
185
+ total +
186
+ ((column?.dividerShown ?? props.dividerShown ?? true)
187
+ ? (dividerWidths[columnId] ?? 6)
188
+ : 0)
189
+ );
190
+ }, 0);
191
+ React.useLayoutEffect(() => {
192
+ const ids = size
193
+ ? layout
194
+ ? layout(size)
195
+ : columns.map((c) => c.id)
196
+ : state.visibleColumnIds;
197
+ const before = store.getNode(id)!.state as SplitNavigationState;
198
+ store.dispatch({
199
+ target: id,
200
+ type: 'layout',
201
+ payload: {
202
+ ids,
203
+ availableWidth: size ? size.width - dividerSpace(ids) : undefined,
204
+ collapsedWidths: Object.fromEntries(
205
+ columns
206
+ .filter((c) => c.collapsible === false || c.id in collapsedWidths)
207
+ .map((c) => [
208
+ c.id,
209
+ c.collapsible === false ? null : collapsedWidths[c.id],
210
+ ]),
211
+ ),
212
+ },
213
+ });
214
+ const after = store.getNode(id)!.state as SplitNavigationState;
215
+ if (before.collapsedColumns !== after.collapsedColumns) {
216
+ for (const columnId of Object.keys(after.widths)) {
217
+ if (before.widths[columnId] !== after.widths[columnId])
218
+ props.onColumnResize?.(columnId, after.widths[columnId]);
219
+ }
220
+ }
221
+ });
222
+ const ordered = [
223
+ ...state.visibleColumnIds,
224
+ ...columns
225
+ .filter((c) => !state.visibleColumnIds.includes(c.id))
226
+ .map((c) => c.id),
227
+ ];
228
+ return (
229
+ <NavigatorStateContext.Provider value={state}>
230
+ <View style={[styles.split, props.style]}>
231
+ <View
232
+ testID="split-layout"
233
+ style={styles.row}
234
+ onLayout={(event) => {
235
+ const { width, height } = event.nativeEvent.layout;
236
+ setSize((previous) =>
237
+ previous?.width === width && previous.height === height
238
+ ? previous
239
+ : { width, height },
240
+ );
241
+ }}
242
+ >
243
+ {ordered.map((columnId) => {
244
+ const column = columns.find((c) => c.id === columnId)!;
245
+ const constraints = splitColumnConfig(state, {
246
+ ...column,
247
+ name: columnId,
248
+ });
249
+ const route = state.routes.find((r) => r.name === columnId)!;
250
+ const visible = state.visibleColumnIds.includes(columnId);
251
+ const last =
252
+ columnId ===
253
+ state.visibleColumnIds[state.visibleColumnIds.length - 1];
254
+ const resize = (width: number) => {
255
+ const before = store.getNode(id)!.state as SplitNavigationState;
256
+ store.dispatch({
257
+ target: id,
258
+ // Native layout precedes input. The fallback supports pre-layout imperative use.
259
+ type: size ? 'resizeBoundary' : 'resize',
260
+ payload: { id: columnId, width },
261
+ });
262
+ const after = store.getNode(id)!.state as SplitNavigationState;
263
+ for (const changedId of after.visibleColumnIds) {
264
+ if (before.widths[changedId] !== after.widths[changedId])
265
+ props.onColumnResize?.(changedId, after.widths[changedId]);
266
+ }
267
+ };
268
+ return (
269
+ <React.Fragment key={route.key}>
270
+ <View
271
+ testID={`split-column-${columnId}`}
272
+ style={[
273
+ props.columnStyle,
274
+ column.style,
275
+ {
276
+ overflow: 'hidden',
277
+ width: state.widths[columnId],
278
+ minWidth: constraints.minWidth ?? 100,
279
+ maxWidth: constraints.maxWidth,
280
+ flexBasis: 'auto',
281
+ flexGrow:
282
+ !size && last && !state.collapsedColumns?.[columnId]
283
+ ? 1
284
+ : 0,
285
+ flexShrink: 0,
286
+ },
287
+ !visible && { display: 'none' },
288
+ ]}
289
+ >
290
+ <SplitColumnContext.Provider
291
+ value={{ nodeId: id, columnId, onSidebarChange }}
292
+ >
293
+ <Scene
294
+ nodeId={id}
295
+ route={route}
296
+ component={column.component}
297
+ visible={visible}
298
+ options={{
299
+ inactiveBehavior: 'keep',
300
+ focusBehavior: 'none',
301
+ contentStyle: column.contentStyle,
302
+ }}
303
+ />
304
+ </SplitColumnContext.Provider>
305
+ </View>
306
+ {visible &&
307
+ !last &&
308
+ (column.dividerShown ?? props.dividerShown ?? true) && (
309
+ <Divider
310
+ column={{
311
+ ...column,
312
+ minWidth: constraints.minWidth,
313
+ maxWidth: constraints.maxWidth,
314
+ }}
315
+ width={state.widths[columnId]}
316
+ resize={resize}
317
+ style={props.dividerStyle}
318
+ renderContent={props.renderDivider}
319
+ onWidthChange={(width) => {
320
+ if (!Number.isFinite(width) || width < 0) return;
321
+ setDividerWidths((previous) =>
322
+ previous[columnId] === width
323
+ ? previous
324
+ : { ...previous, [columnId]: width },
325
+ );
326
+ }}
327
+ />
328
+ )}
329
+ </React.Fragment>
330
+ );
331
+ })}
332
+ </View>
333
+ </View>
334
+ </NavigatorStateContext.Provider>
335
+ );
336
+ }
337
+ return { Navigator, Column };
338
+ }
339
+ const styles = StyleSheet.create({
340
+ split: { flex: 1, flexDirection: 'row', overflow: 'hidden' },
341
+ row: { flex: 1, minWidth: 0, flexDirection: 'row', overflow: 'hidden' },
342
+ dividerHitArea: {
343
+ // Enlarge the native view's bounds; negative margins keep the columns
344
+ // and visible divider at their original positions. Measure only the visual.
345
+ paddingHorizontal: 8,
346
+ marginHorizontal: -8,
347
+ flexShrink: 0,
348
+ zIndex: 1,
349
+ },
350
+ });
@@ -0,0 +1,213 @@
1
+ import React from 'react';
2
+ import {
3
+ Pressable,
4
+ Text,
5
+ View,
6
+ StyleSheet,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+ import type {
11
+ HistoryBehavior,
12
+ ParamListBase,
13
+ RouteName,
14
+ StackNavigationState,
15
+ } from '../routers';
16
+ import {
17
+ useNavigator,
18
+ readScreens,
19
+ resolveOptions,
20
+ NavigatorStateContext,
21
+ type ScreenConfig,
22
+ } from '../core/builder';
23
+ import { Scene } from '../core/Scene';
24
+ import { createNavigation } from '../core/navigation';
25
+ import { useNavigationTheme } from '../core/context';
26
+ import type {
27
+ StackNavigation,
28
+ StackScreenOptions,
29
+ HeaderProps,
30
+ } from '../core/types';
31
+ export interface StackNavigatorProps {
32
+ children: React.ReactNode;
33
+ id?: string;
34
+ initialRouteName?: string;
35
+ historyBehavior?: HistoryBehavior;
36
+ screenOptions?:
37
+ | StackScreenOptions
38
+ | ((context: {
39
+ navigation: StackNavigation;
40
+ route: import('../routers').NavigationRoute;
41
+ }) => StackScreenOptions);
42
+ style?: StyleProp<ViewStyle>;
43
+ }
44
+ function renderAction(
45
+ action: StackScreenOptions['headerLeft'],
46
+ props: HeaderProps,
47
+ ) {
48
+ return typeof action === 'function' ? action(props) : action;
49
+ }
50
+ export function StackHeader(props: HeaderProps) {
51
+ const { options, route, navigation, canGoBack } = props;
52
+ const { colors } = useNavigationTheme();
53
+ if (options.headerShown === false) return null;
54
+ if (options.header !== undefined)
55
+ return (
56
+ <>
57
+ {typeof options.header === 'function'
58
+ ? options.header(props)
59
+ : options.header}
60
+ </>
61
+ );
62
+ return (
63
+ <View
64
+ style={[
65
+ styles.header,
66
+ { backgroundColor: colors.surface, borderColor: colors.border },
67
+ options.headerStyle,
68
+ ]}
69
+ >
70
+ <View style={[styles.action, options.headerLeftContainerStyle]}>
71
+ {options.headerLeft !== undefined
72
+ ? renderAction(options.headerLeft, props)
73
+ : canGoBack && (
74
+ <Pressable
75
+ accessibilityRole="button"
76
+ accessibilityLabel={options.headerBackTitle ?? 'Back'}
77
+ style={options.headerBackButtonStyle}
78
+ focusable
79
+ onPress={() => navigation.goBack()}
80
+ >
81
+ <Text
82
+ style={[
83
+ { color: options.headerTintColor ?? colors.accent },
84
+ options.headerBackTitleStyle,
85
+ ]}
86
+ >
87
+ ‹ {options.headerBackTitle ?? 'Back'}
88
+ </Text>
89
+ </Pressable>
90
+ )}
91
+ </View>
92
+ <Text
93
+ accessibilityRole="header"
94
+ numberOfLines={1}
95
+ style={[
96
+ styles.title,
97
+ { color: options.headerTintColor ?? colors.text },
98
+ options.headerTitleStyle,
99
+ ]}
100
+ >
101
+ {options.title ?? route.name}
102
+ </Text>
103
+ <View style={[styles.action, options.headerRightContainerStyle]}>
104
+ {renderAction(options.headerRight, props)}
105
+ </View>
106
+ </View>
107
+ );
108
+ }
109
+ export function createStackNavigator<
110
+ P extends ParamListBase = ParamListBase,
111
+ >() {
112
+ function Screen<N extends RouteName<P>>(
113
+ _props: ScreenConfig<P, N, StackScreenOptions, StackNavigation<P>>,
114
+ ) {
115
+ return null;
116
+ }
117
+ function Navigator(
118
+ props: Omit<StackNavigatorProps, 'initialRouteName'> & {
119
+ initialRouteName?: RouteName<P>;
120
+ },
121
+ ) {
122
+ const definitions = readScreens<StackScreenOptions>(props.children, Screen);
123
+ const { id, state, store } = useNavigator<StackNavigationState>(
124
+ 'stack',
125
+ {
126
+ routes: definitions,
127
+ initialRouteName: props.initialRouteName,
128
+ historyBehavior: props.historyBehavior,
129
+ },
130
+ props.id,
131
+ );
132
+ const getCanGoBack = React.useCallback(
133
+ () => store.canDispatch('back', id),
134
+ [store, id],
135
+ );
136
+ const canGoBack = React.useSyncExternalStore(
137
+ store.subscribe,
138
+ getCanGoBack,
139
+ getCanGoBack,
140
+ );
141
+ const scenes = state.routes.map((route) => {
142
+ const definition = definitions.find((d) => d.name === route.name)!;
143
+ if (!definition)
144
+ throw new Error(
145
+ `Screen ${route.name} was removed. Remount the navigator to change its screen list.`,
146
+ );
147
+ const navigation = createNavigation(store, id, route.key);
148
+ return {
149
+ route,
150
+ definition,
151
+ navigation,
152
+ options: resolveOptions(
153
+ definition,
154
+ route,
155
+ navigation,
156
+ props.screenOptions,
157
+ ),
158
+ };
159
+ });
160
+ let visibleStart = scenes.length - 1;
161
+ while (
162
+ visibleStart > 0 &&
163
+ ['modal', 'dialog'].includes(
164
+ scenes[visibleStart].options.presentation ?? 'card',
165
+ )
166
+ )
167
+ visibleStart--;
168
+ return (
169
+ <NavigatorStateContext.Provider value={state}>
170
+ <View style={[styles.stack, props.style]}>
171
+ {scenes.map(({ route, definition, navigation, options }, index) => (
172
+ <Scene
173
+ key={route.key}
174
+ nodeId={id}
175
+ route={route}
176
+ component={definition.component}
177
+ visible={index >= visibleStart}
178
+ options={options}
179
+ animation={options.animation ?? 'default'}
180
+ overlayStyle={options.overlayStyle}
181
+ dialogStyle={options.dialogStyle}
182
+ overlay={
183
+ options.presentation === 'card'
184
+ ? undefined
185
+ : options.presentation
186
+ }
187
+ >
188
+ <StackHeader
189
+ route={route}
190
+ navigation={navigation}
191
+ options={options}
192
+ canGoBack={canGoBack}
193
+ />
194
+ </Scene>
195
+ ))}
196
+ </View>
197
+ </NavigatorStateContext.Provider>
198
+ );
199
+ }
200
+ return { Navigator, Screen };
201
+ }
202
+ const styles = StyleSheet.create({
203
+ stack: { flex: 1, overflow: 'hidden' },
204
+ header: {
205
+ minHeight: 44,
206
+ paddingHorizontal: 12,
207
+ flexDirection: 'row',
208
+ alignItems: 'center',
209
+ borderBottomWidth: StyleSheet.hairlineWidth,
210
+ },
211
+ title: { flex: 1, textAlign: 'center', fontWeight: '600' },
212
+ action: { minWidth: 60 },
213
+ });
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import type { ParamListBase, RouteName } from '../routers';
3
+ import { readScreens, type ScreenConfig } from '../core/builder';
4
+ import {
5
+ SelectionNavigator,
6
+ type SelectionNavigatorProps,
7
+ } from '../core/SelectionNavigator';
8
+ import type { TabNavigation, TabScreenOptions } from '../core/types';
9
+ export type TabNavigatorOptions = Pick<
10
+ SelectionNavigatorProps,
11
+ | 'id'
12
+ | 'initialRouteName'
13
+ | 'screenOptions'
14
+ | 'style'
15
+ | 'barStyle'
16
+ | 'barContentStyle'
17
+ | 'contentStyle'
18
+ >;
19
+ export function createTabNavigator<P extends ParamListBase = ParamListBase>() {
20
+ function Screen<N extends RouteName<P>>(
21
+ _props: ScreenConfig<P, N, TabScreenOptions, TabNavigation<P>>,
22
+ ) {
23
+ return null;
24
+ }
25
+ function Navigator(
26
+ props: Omit<TabNavigatorOptions, 'initialRouteName'> & {
27
+ initialRouteName?: RouteName<P>;
28
+ children: React.ReactNode;
29
+ },
30
+ ) {
31
+ return (
32
+ <SelectionNavigator
33
+ {...props}
34
+ type="tabs"
35
+ definitions={readScreens(props.children, Screen)}
36
+ />
37
+ );
38
+ }
39
+ return { Navigator, Screen };
40
+ }
@@ -0,0 +1,3 @@
1
+ EXPORTS
2
+ DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
3
+ DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE