@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,71 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.navigationItemStyle = void 0;
7
+ exports.NavigationItem = NavigationItem;
8
+ const jsx_runtime_1 = require("react/jsx-runtime");
9
+ const react_1 = __importDefault(require("react"));
10
+ const react_native_1 = require("react-native");
11
+ const context_1 = require("./context");
12
+ function resolveStyle(style, state) {
13
+ return typeof style === 'function' ? style(state) : style;
14
+ }
15
+ /** The interactive shell remains owned by navigation when its content is customized. */
16
+ function NavigationItem({ route, options, selected, focused, collapsed, sidebar, onFocus, onBlur, onPress, itemRef, }) {
17
+ const { colors } = (0, context_1.useNavigationTheme)();
18
+ const [pressed, setPressed] = react_1.default.useState(false);
19
+ const [hovered, setHovered] = react_1.default.useState(false);
20
+ const state = {
21
+ selected,
22
+ focused,
23
+ collapsed,
24
+ hovered,
25
+ pressed: pressed && !options.disabled,
26
+ disabled: !!options.disabled,
27
+ };
28
+ const label = options.label ?? options.title ?? route.name;
29
+ const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [options.icon != null && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: resolveStyle(options.iconContainerStyle, state), children: typeof options.icon === 'function'
30
+ ? options.icon({ focused: selected, disabled: state.disabled })
31
+ : options.icon })), (!collapsed || !options.icon) && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
32
+ { color: colors.text, flexShrink: 1 },
33
+ resolveStyle(options.labelStyle, state),
34
+ ], children: collapsed ? label.slice(0, 1) : label })), !collapsed &&
35
+ options.badge != null &&
36
+ (typeof options.badge === 'string' ||
37
+ typeof options.badge === 'number' ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
38
+ { color: colors.mutedText },
39
+ resolveStyle(options.badgeStyle, state),
40
+ ], children: options.badge })) : (options.badge))] }));
41
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { ref: (target) => itemRef(target), accessible: true, focusable: !options.disabled, disabled: options.disabled, accessibilityRole: sidebar ? 'menuitem' : 'tab', accessibilityLabel: label, accessibilityState: { selected, disabled: state.disabled }, onFocus: onFocus, onBlur: () => {
42
+ setPressed(false);
43
+ onBlur();
44
+ }, onPress: onPress, onHoverIn: () => setHovered(true), onHoverOut: () => setHovered(false), onPressIn: () => setPressed(true), onPressOut: () => setPressed(false), style: [
45
+ exports.navigationItemStyle,
46
+ {
47
+ opacity: state.disabled ? 0.45 : 1,
48
+ backgroundColor: selected ? colors.selectedBackground : 'transparent',
49
+ borderColor: focused ? colors.accent : 'transparent',
50
+ },
51
+ resolveStyle(options.itemStyle, state),
52
+ ], children: options.renderItemContent
53
+ ? options.renderItemContent({
54
+ ...state,
55
+ route,
56
+ label,
57
+ children: content,
58
+ })
59
+ : content }));
60
+ }
61
+ exports.navigationItemStyle = react_native_1.StyleSheet.create({
62
+ item: {
63
+ flexDirection: 'row',
64
+ alignItems: 'center',
65
+ gap: 8,
66
+ padding: 10,
67
+ margin: 2,
68
+ borderRadius: 4,
69
+ borderWidth: 1,
70
+ },
71
+ }).item;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { type StyleProp, type ViewStyle } from 'react-native';
3
+ import type { NavigationRoute } from '../routers';
4
+ import type { CommonScreenOptions, StackAnimation } from './types';
5
+ export interface SceneProps {
6
+ nodeId: string;
7
+ route: NavigationRoute;
8
+ component: React.ComponentType<any>;
9
+ visible: boolean;
10
+ /** Native content slots must be laid out before restoring keyboard focus. */
11
+ focusReady?: boolean;
12
+ options: CommonScreenOptions;
13
+ animation?: StackAnimation;
14
+ overlay?: 'modal' | 'dialog';
15
+ overlayStyle?: StyleProp<ViewStyle>;
16
+ dialogStyle?: StyleProp<ViewStyle>;
17
+ children?: React.ReactNode;
18
+ }
19
+ export declare function Scene({ nodeId, route, component: Component, visible, focusReady, options, animation, overlay, overlayStyle, dialogStyle, children, }: SceneProps): React.JSX.Element | null;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Scene = Scene;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = __importDefault(require("react"));
9
+ const react_native_1 = require("react-native");
10
+ const platform_1 = require("../platform");
11
+ const context_1 = require("./context");
12
+ const navigation_1 = require("./navigation");
13
+ const focus_1 = require("./focus");
14
+ function Scene({ nodeId, route, component: Component, visible, focusReady = true, options, animation = 'none', overlay, overlayStyle, dialogStyle, children, }) {
15
+ const store = (0, context_1.useStore)();
16
+ const { colors } = (0, context_1.useNavigationTheme)();
17
+ const platform = react_1.default.useContext(context_1.PlatformContext);
18
+ const snapshot = react_1.default.useCallback(() => store.isRouteFocused(nodeId, route.key), [store, nodeId, route.key]);
19
+ const focused = react_1.default.useSyncExternalStore(store.subscribe, snapshot, snapshot);
20
+ const navigation = react_1.default.useMemo(() => (0, navigation_1.createNavigation)(store, nodeId, route.key), [store, nodeId, route.key]);
21
+ const targets = react_1.default.useRef(new Map());
22
+ const last = react_1.default.useRef(undefined);
23
+ const registry = react_1.default.useMemo(() => ({
24
+ register(id, target) {
25
+ targets.current.set(id, target);
26
+ return () => {
27
+ targets.current.delete(id);
28
+ };
29
+ },
30
+ record(id) {
31
+ last.current = id;
32
+ store.setFocusedNode(nodeId);
33
+ },
34
+ }), [store, nodeId]);
35
+ const mounted = visible || options.inactiveBehavior !== 'unmount';
36
+ react_1.default.useEffect(() => {
37
+ if (!focused || !visible || !focusReady || options.focusBehavior === 'none')
38
+ return;
39
+ const previous = options.focusBehavior !== 'first' && last.current
40
+ ? targets.current.get(last.current)
41
+ : undefined;
42
+ (previous ?? targets.current.values().next().value)?.focus();
43
+ }, [focused, visible, focusReady, options.focusBehavior, mounted]);
44
+ const [progress] = react_1.default.useState(() => new react_native_1.Animated.Value(1));
45
+ react_1.default.useEffect(() => {
46
+ if (!visible || animation === 'none') {
47
+ progress.setValue(1);
48
+ return;
49
+ }
50
+ progress.setValue(0);
51
+ const transition = react_native_1.Animated.timing(progress, {
52
+ toValue: 1,
53
+ duration: platform.defaultTransitions.push.duration,
54
+ // react-native-macos's Fabric/bridgeless host does not reliably
55
+ // connect native-driven Animated values, leaving views stuck at
56
+ // their initial (invisible) frame. Drive this on the JS thread
57
+ // instead so the entrance transition actually completes.
58
+ useNativeDriver: false,
59
+ });
60
+ transition.start();
61
+ return () => transition.stop();
62
+ }, [visible, animation, progress, platform]);
63
+ if (!mounted)
64
+ return null;
65
+ const offset = platform.defaultTransitions.push.offset;
66
+ const animatedStyle = animation === 'none'
67
+ ? undefined
68
+ : {
69
+ opacity: progress,
70
+ transform: animation === 'fade'
71
+ ? []
72
+ : [
73
+ {
74
+ [animation === 'slide-vertical'
75
+ ? 'translateY'
76
+ : 'translateX']: progress.interpolate({
77
+ inputRange: [0, 1],
78
+ outputRange: [offset, 0],
79
+ }),
80
+ },
81
+ ],
82
+ };
83
+ return ((0, jsx_runtime_1.jsx)(context_1.ScopeContext.Provider, { value: { nodeId, routeKey: route.key }, children: (0, jsx_runtime_1.jsx)(context_1.ScreenContext.Provider, { value: { navigation, route, focused }, children: (0, jsx_runtime_1.jsx)(focus_1.FocusContext.Provider, { value: registry, children: (0, jsx_runtime_1.jsx)(platform_1.DesktopView, { testID: `scene-${route.name}`, onFocus: () => store.focusRoute(nodeId, route.key), pointerEvents: visible ? 'auto' : 'none', accessibilityElementsHidden: !visible, importantForAccessibility: visible ? 'auto' : 'no-hide-descendants', accessibilityViewIsModal: !!overlay && visible, style: [
84
+ styles.scene,
85
+ options.sceneStyle,
86
+ overlay && styles.overlay,
87
+ overlay === 'dialog' && styles.dialogBackdrop,
88
+ overlay && overlayStyle,
89
+ !visible && styles.hidden,
90
+ ], children: (0, jsx_runtime_1.jsxs)(react_native_1.Animated.View, { style: [
91
+ styles.content,
92
+ overlay === 'dialog' && [
93
+ styles.dialog,
94
+ { backgroundColor: colors.surface },
95
+ ],
96
+ options.contentStyle,
97
+ overlay === 'dialog' && dialogStyle,
98
+ animatedStyle,
99
+ ], children: [children, (0, jsx_runtime_1.jsx)(Component, { navigation: navigation, route: route })] }) }) }) }) }));
100
+ }
101
+ const styles = react_native_1.StyleSheet.create({
102
+ scene: { flex: 1 },
103
+ content: { flex: 1 },
104
+ hidden: { display: 'none' },
105
+ overlay: {
106
+ ...react_native_1.StyleSheet.absoluteFillObject,
107
+ backgroundColor: 'rgba(0,0,0,0.25)',
108
+ zIndex: 1,
109
+ },
110
+ dialogBackdrop: {
111
+ alignItems: 'center',
112
+ justifyContent: 'center',
113
+ padding: 32,
114
+ },
115
+ dialog: {
116
+ flex: 0,
117
+ width: '80%',
118
+ maxWidth: 640,
119
+ maxHeight: '90%',
120
+ minHeight: 160,
121
+ borderRadius: 8,
122
+ overflow: 'hidden',
123
+ },
124
+ });
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { type StyleProp, type ViewStyle, type TextStyle } from 'react-native';
3
+ import { type Definition } from './builder';
4
+ import type { SidebarScreenOptions, SidebarFooterProps } from './types';
5
+ export interface SelectionNavigatorProps {
6
+ id?: string;
7
+ initialRouteName?: string;
8
+ screenOptions?: SidebarScreenOptions;
9
+ style?: StyleProp<ViewStyle>;
10
+ barStyle?: StyleProp<ViewStyle>;
11
+ barContentStyle?: StyleProp<ViewStyle>;
12
+ contentStyle?: StyleProp<ViewStyle>;
13
+ sectionStyle?: StyleProp<ViewStyle>;
14
+ sectionTitleStyle?: StyleProp<TextStyle>;
15
+ /** Hide the default button independently of programmatic collapse actions. */
16
+ collapseButtonShown?: boolean;
17
+ sidebarFooterStyle?: StyleProp<ViewStyle>;
18
+ /** Replace the entire footer. Return null to remove it without reserving space. */
19
+ renderSidebarFooter?: (props: SidebarFooterProps) => React.ReactNode;
20
+ collapseButtonStyle?: StyleProp<ViewStyle>;
21
+ collapseLabelStyle?: StyleProp<TextStyle>;
22
+ renderCollapseButtonContent?: (props: {
23
+ collapsed: boolean;
24
+ children: React.ReactNode;
25
+ }) => React.ReactNode;
26
+ position?: 'left' | 'right';
27
+ /** Use 'fill' to follow the width of a containing Split.Column. */
28
+ width?: number | 'fill';
29
+ minWidth?: number;
30
+ maxWidth?: number;
31
+ collapsible?: boolean;
32
+ collapsedWidth?: number;
33
+ defaultCollapsed?: boolean;
34
+ }
35
+ export declare function SelectionNavigator({ type, definitions, ...props }: SelectionNavigatorProps & {
36
+ type: 'sidebar' | 'tabs';
37
+ definitions: Definition<SidebarScreenOptions>[];
38
+ }): React.JSX.Element;
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SelectionNavigator = SelectionNavigator;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = __importDefault(require("react"));
9
+ const react_native_1 = require("react-native");
10
+ const builder_1 = require("./builder");
11
+ const NavigationItem_1 = require("./NavigationItem");
12
+ const Scene_1 = require("./Scene");
13
+ const navigation_1 = require("./navigation");
14
+ const context_1 = require("./context");
15
+ const platform_1 = require("../platform");
16
+ function SelectionNavigator({ type, definitions, ...props }) {
17
+ const platform = react_1.default.useContext(context_1.PlatformContext);
18
+ const { colors } = (0, context_1.useNavigationTheme)();
19
+ const configs = definitions.map((d) => ({
20
+ ...d,
21
+ ...props.screenOptions,
22
+ ...(typeof d.options === 'object' ? d.options : {}),
23
+ }));
24
+ const { id, state, store, navigation } = (0, builder_1.useNavigator)(type, {
25
+ routes: configs,
26
+ initialRouteName: props.initialRouteName,
27
+ defaultCollapsed: props.defaultCollapsed,
28
+ }, props.id);
29
+ const items = state.routes.map((route) => {
30
+ const definition = definitions.find((d) => d.name === route.name);
31
+ if (!definition)
32
+ throw new Error(`Missing Screen: ${route.name}. Remount to change the screen list.`);
33
+ return {
34
+ route,
35
+ definition,
36
+ options: (0, builder_1.resolveOptions)(definition, route, (0, navigation_1.createNavigation)(store, id, route.key), props.screenOptions),
37
+ };
38
+ });
39
+ react_1.default.useLayoutEffect(() => {
40
+ store.updateOptions(id, {
41
+ key: id,
42
+ routes: items.map((i) => ({ name: i.route.name, ...i.options })),
43
+ });
44
+ const current = items.find((i) => i.route.key === state.activeRouteKey);
45
+ if (current?.options.hidden || current?.options.disabled) {
46
+ const next = items.find((i) => !i.options.hidden && !i.options.disabled);
47
+ if (next)
48
+ navigation.select(next.route.name);
49
+ }
50
+ });
51
+ const visibleItems = items.filter((i) => !i.options.hidden);
52
+ const enabled = visibleItems.filter((i) => !i.options.disabled);
53
+ const [focusedKey, setFocusedKey] = react_1.default.useState('');
54
+ const refs = react_1.default.useRef(new Map());
55
+ const sidebar = type === 'sidebar';
56
+ const collapsed = sidebar && state.collapsed;
57
+ const splitColumn = react_1.default.useContext(context_1.SplitColumnContext);
58
+ const scope = react_1.default.useContext(context_1.ScopeContext);
59
+ const syncColumn = sidebar && splitColumn && scope.nodeId === splitColumn.nodeId
60
+ ? splitColumn.onSidebarChange
61
+ : undefined;
62
+ const columnId = splitColumn?.columnId;
63
+ const collapsedWidth = props.collapsedWidth ?? 56;
64
+ react_1.default.useLayoutEffect(() => {
65
+ if (syncColumn && columnId)
66
+ syncColumn(columnId, collapsed ? collapsedWidth : null);
67
+ }, [syncColumn, columnId, collapsed, collapsedWidth]);
68
+ react_1.default.useLayoutEffect(() => () => {
69
+ if (syncColumn && columnId)
70
+ syncColumn(columnId, null);
71
+ }, [syncColumn, columnId]);
72
+ const focusItem = (index) => {
73
+ const item = enabled[(index + enabled.length) % enabled.length];
74
+ if (!item)
75
+ return;
76
+ setFocusedKey(item.route.key);
77
+ refs.current.get(item.route.key)?.focus();
78
+ };
79
+ const bindings = sidebar
80
+ ? ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter'].map((key) => ({ key }))
81
+ : [
82
+ ...['ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter'].map((key) => ({
83
+ key,
84
+ })),
85
+ ...platform.keyboardShortcuts.nextTab,
86
+ ...platform.keyboardShortcuts.previousTab,
87
+ ];
88
+ const collapseContent = ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [{ color: colors.text }, props.collapseLabelStyle], children: collapsed ? '»' : '«' }));
89
+ const collapseButton = sidebar &&
90
+ props.collapsible !== false &&
91
+ props.collapseButtonShown !== false ? ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessible: true, focusable: true, accessibilityRole: "button", accessibilityLabel: collapsed ? 'Expand sidebar' : 'Collapse sidebar', onPress: () => navigation.toggleSidebar(), style: [NavigationItem_1.navigationItemStyle, props.collapseButtonStyle], children: props.renderCollapseButtonContent
92
+ ? props.renderCollapseButtonContent({
93
+ collapsed,
94
+ children: collapseContent,
95
+ })
96
+ : collapseContent })) : null;
97
+ const footer = sidebar && props.renderSidebarFooter
98
+ ? props.renderSidebarFooter({
99
+ collapsed,
100
+ collapseSidebar: navigation.collapseSidebar,
101
+ expandSidebar: navigation.expandSidebar,
102
+ toggleSidebar: navigation.toggleSidebar,
103
+ children: collapseButton,
104
+ })
105
+ : collapseButton;
106
+ const rail = ((0, jsx_runtime_1.jsxs)(platform_1.DesktopView, { accessibilityRole: sidebar ? 'menu' : 'tablist', keyDownEvents: bindings, focusable: true, onKeyDown: (event) => {
107
+ if ((0, platform_1.eventHandled)(event) ||
108
+ platform.isTextInputEvent(event) ||
109
+ !bindings.some((key) => (0, platform_1.matchesShortcut)(event, key)))
110
+ return;
111
+ store.setFocusedNode(id);
112
+ const index = enabled.findIndex((i) => i.route.key === (focusedKey || state.activeRouteKey));
113
+ const key = event.nativeEvent.key;
114
+ if (!sidebar &&
115
+ platform.keyboardShortcuts.nextTab.some((k) => (0, platform_1.matchesShortcut)(event, k)))
116
+ navigation.nextTab();
117
+ else if (!sidebar &&
118
+ platform.keyboardShortcuts.previousTab.some((k) => (0, platform_1.matchesShortcut)(event, k)))
119
+ navigation.previousTab();
120
+ else if (key === 'ArrowUp' || key === 'ArrowLeft')
121
+ focusItem(index - 1);
122
+ else if (key === 'ArrowDown' || key === 'ArrowRight')
123
+ focusItem(index + 1);
124
+ else if (key === 'Home')
125
+ focusItem(0);
126
+ else if (key === 'End')
127
+ focusItem(enabled.length - 1);
128
+ else if (key === 'Enter') {
129
+ const item = enabled.find((i) => i.route.key === (focusedKey || state.activeRouteKey));
130
+ if (item)
131
+ navigation.select(item.route.name);
132
+ }
133
+ (0, platform_1.consumeKey)(event);
134
+ }, style: [
135
+ { backgroundColor: colors.surface, borderColor: colors.border },
136
+ sidebar
137
+ ? {
138
+ width: collapsed
139
+ ? (props.collapsedWidth ?? 56)
140
+ : props.width === 'fill'
141
+ ? '100%'
142
+ : Math.min(props.maxWidth ?? Infinity, Math.max(props.minWidth ?? 120, props.width ?? platform.sidebar.defaultWidth)),
143
+ maxWidth: '100%',
144
+ ...(props.position === 'right'
145
+ ? { borderLeftWidth: react_native_1.StyleSheet.hairlineWidth }
146
+ : { borderRightWidth: react_native_1.StyleSheet.hairlineWidth }),
147
+ }
148
+ : { borderBottomWidth: react_native_1.StyleSheet.hairlineWidth },
149
+ props.barStyle,
150
+ ], children: [(0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: !sidebar, contentContainerStyle: [
151
+ !sidebar && styles.tabItems,
152
+ props.barContentStyle,
153
+ ], children: visibleItems.map((item, index) => {
154
+ const { route, options, definition } = item;
155
+ const selected = route.key === state.activeRouteKey;
156
+ const section = definition.section;
157
+ return ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [sidebar &&
158
+ !collapsed &&
159
+ section?.title &&
160
+ visibleItems[index - 1]?.definition.section?.key !==
161
+ section.key && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [props.sectionStyle, section.style], children: section.renderTitle ? (section.renderTitle({ title: section.title })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { accessibilityRole: "header", style: [
162
+ styles.section,
163
+ { color: colors.mutedText },
164
+ props.sectionTitleStyle,
165
+ section.titleStyle,
166
+ ], children: section.title })) })), (0, jsx_runtime_1.jsx)(NavigationItem_1.NavigationItem, { route: route, options: options, selected: selected, focused: focusedKey === route.key, collapsed: collapsed, sidebar: sidebar, itemRef: (value) => {
167
+ if (value)
168
+ refs.current.set(route.key, value);
169
+ else
170
+ refs.current.delete(route.key);
171
+ }, onFocus: () => {
172
+ setFocusedKey(route.key);
173
+ store.setFocusedNode(id);
174
+ }, onBlur: () => setFocusedKey((current) => current === route.key ? '' : current), onPress: () => {
175
+ store.setFocusedNode(id);
176
+ navigation.select(route.name);
177
+ } })] }, route.key));
178
+ }) }), sidebar && footer != null && footer !== false && ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "sidebar-footer", style: props.sidebarFooterStyle, children: footer }))] }));
179
+ return ((0, jsx_runtime_1.jsx)(builder_1.NavigatorStateContext.Provider, { value: state, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
180
+ styles.container,
181
+ sidebar && {
182
+ flexDirection: props.position === 'right' ? 'row-reverse' : 'row',
183
+ },
184
+ props.style,
185
+ ], children: [rail, (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.content, props.contentStyle], children: items.map(({ route, definition, options }) => ((0, jsx_runtime_1.jsx)(Scene_1.Scene, { nodeId: id, route: route, component: definition.component, visible: route.key === state.activeRouteKey &&
186
+ !options.hidden &&
187
+ !options.disabled, options: options }, route.key))) })] }) }));
188
+ }
189
+ const styles = react_native_1.StyleSheet.create({
190
+ container: { flex: 1 },
191
+ content: { flex: 1, minWidth: 0 },
192
+ tabItems: { flexDirection: 'row' },
193
+ section: { padding: 10, fontSize: 12, fontWeight: '600' },
194
+ });
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { type NavigationState, type RouterOptions, type ParamListBase, type RouteName, type NavigationRoute } from '../routers';
3
+ import type { SidebarSectionOptions } from './types';
4
+ import { type AnyNavigation } from './navigation';
5
+ export declare const NavigatorStateContext: React.Context<NavigationState | null>;
6
+ export interface ScreenConfig<P extends ParamListBase, N extends RouteName<P>, O, Nav = AnyNavigation<P>> {
7
+ name: N;
8
+ component: React.ComponentType<{
9
+ navigation: Nav;
10
+ route: NavigationRoute<N, P[N]>;
11
+ }>;
12
+ initialParams?: P[N];
13
+ options?: O | ((context: {
14
+ navigation: Nav;
15
+ route: NavigationRoute<N, P[N]>;
16
+ }) => O);
17
+ }
18
+ export interface Definition<O> {
19
+ name: string;
20
+ component: React.ComponentType<any>;
21
+ initialParams?: unknown;
22
+ options?: O | ((context: {
23
+ navigation: AnyNavigation;
24
+ route: NavigationRoute;
25
+ }) => O);
26
+ section?: SidebarSectionOptions & {
27
+ key: string;
28
+ };
29
+ }
30
+ export declare function readScreens<O>(children: React.ReactNode, Screen: React.ElementType, Section?: React.ElementType): Definition<O>[];
31
+ export declare function useNavigator<S extends NavigationState>(type: S['type'], options: Omit<RouterOptions, 'key'>, explicitId?: string): {
32
+ id: string;
33
+ state: S;
34
+ store: import("./store").NavigationStore;
35
+ navigation: AnyNavigation<ParamListBase>;
36
+ };
37
+ export declare function resolveOptions<O>(definition: Definition<O>, route: NavigationRoute, navigation: AnyNavigation, defaults?: O | ((context: {
38
+ route: NavigationRoute;
39
+ navigation: AnyNavigation;
40
+ }) => O)): O;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NavigatorStateContext = void 0;
7
+ exports.readScreens = readScreens;
8
+ exports.useNavigator = useNavigator;
9
+ exports.resolveOptions = resolveOptions;
10
+ const react_1 = __importDefault(require("react"));
11
+ const routers_1 = require("../routers");
12
+ const context_1 = require("./context");
13
+ const navigation_1 = require("./navigation");
14
+ exports.NavigatorStateContext = react_1.default.createContext(null);
15
+ function readScreens(children, Screen, Section) {
16
+ const result = [];
17
+ function visit(items, section) {
18
+ react_1.default.Children.forEach(items, (child) => {
19
+ if (!react_1.default.isValidElement(child)) {
20
+ if (child != null && typeof child !== 'boolean')
21
+ throw new Error('Navigator children must be Screen elements.');
22
+ return;
23
+ }
24
+ const props = child.props;
25
+ if (child.type === react_1.default.Fragment)
26
+ visit(props.children, section);
27
+ else if (Section && child.type === Section)
28
+ visit(props.children, {
29
+ key: String(child.key ?? result.length),
30
+ title: props.title,
31
+ style: props.style,
32
+ titleStyle: props.titleStyle,
33
+ renderTitle: props.renderTitle,
34
+ });
35
+ else if (child.type === Screen)
36
+ result.push({ ...child.props, section });
37
+ else
38
+ throw new Error('Navigator children must be Screen, Section, or Fragment elements.');
39
+ });
40
+ }
41
+ visit(children);
42
+ if (!result.length ||
43
+ new Set(result.map((r) => r.name)).size !== result.length)
44
+ throw new Error('Navigator requires unique Screen names.');
45
+ return result;
46
+ }
47
+ function useNavigator(type, options, explicitId) {
48
+ const store = (0, context_1.useStore)();
49
+ const parent = react_1.default.useContext(context_1.ScopeContext);
50
+ const id = parent.nodeId
51
+ ? `${parent.nodeId}/${parent.routeKey}/${explicitId ?? type}`
52
+ : (explicitId ?? 'root');
53
+ const config = { ...options, key: id };
54
+ const [initial] = react_1.default.useState(() => store.getNode(id)?.state ??
55
+ routers_1.routers[type].getInitialState(config));
56
+ const getSnapshot = react_1.default.useCallback(() => store.getNode(id)?.state ?? initial, [store, id, initial]);
57
+ const state = react_1.default.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
58
+ react_1.default.useLayoutEffect(() => store.register({
59
+ id,
60
+ parentId: parent.nodeId,
61
+ parentRouteKey: parent.routeKey,
62
+ type,
63
+ state: initial,
64
+ }, config), [store, id]);
65
+ react_1.default.useLayoutEffect(() => {
66
+ store.updateOptions(id, config);
67
+ });
68
+ const navigation = react_1.default.useMemo(() => (0, navigation_1.createNavigation)(store, id), [store, id]);
69
+ return { id, state, store, navigation };
70
+ }
71
+ function resolveOptions(definition, route, navigation, defaults) {
72
+ const context = { route, navigation };
73
+ return {
74
+ ...(typeof defaults === 'function'
75
+ ? defaults(context)
76
+ : defaults),
77
+ ...(typeof definition.options === 'function'
78
+ ? definition.options(context)
79
+ : definition.options),
80
+ };
81
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import type { NavigationStore } from './store';
3
+ import type { NavigationRoute } from '../routers';
4
+ import type { AnyNavigation } from './navigation';
5
+ import type { NavigationTheme } from './types';
6
+ export declare const StoreContext: React.Context<NavigationStore | null>;
7
+ export declare const ScopeContext: React.Context<{
8
+ nodeId?: string;
9
+ routeKey?: string;
10
+ }>;
11
+ /** Only a Sidebar directly owned by this Split column may resize it. */
12
+ export declare const SplitColumnContext: React.Context<{
13
+ nodeId: string;
14
+ columnId: string;
15
+ onSidebarChange: (columnId: string, width: number | null) => void;
16
+ } | null>;
17
+ export declare const ScreenContext: React.Context<{
18
+ navigation: AnyNavigation;
19
+ route: NavigationRoute;
20
+ focused: boolean;
21
+ } | null>;
22
+ export declare const PlatformContext: React.Context<import("../platform").NavigationPlatformAdapter>;
23
+ export declare const DefaultTheme: NavigationTheme;
24
+ export declare const ThemeContext: React.Context<NavigationTheme>;
25
+ export declare const useNavigationTheme: () => NavigationTheme;
26
+ export declare function useStore(): NavigationStore;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.useNavigationTheme = exports.ThemeContext = exports.DefaultTheme = exports.PlatformContext = exports.ScreenContext = exports.SplitColumnContext = exports.ScopeContext = exports.StoreContext = void 0;
7
+ exports.useStore = useStore;
8
+ const react_1 = __importDefault(require("react"));
9
+ const platform_1 = require("../platform");
10
+ exports.StoreContext = react_1.default.createContext(null);
11
+ exports.ScopeContext = react_1.default.createContext({});
12
+ /** Only a Sidebar directly owned by this Split column may resize it. */
13
+ exports.SplitColumnContext = react_1.default.createContext(null);
14
+ exports.ScreenContext = react_1.default.createContext(null);
15
+ exports.PlatformContext = react_1.default.createContext(platform_1.defaultPlatformAdapter);
16
+ exports.DefaultTheme = {
17
+ dark: false,
18
+ colors: {
19
+ background: '#ffffff',
20
+ surface: '#f5f5f7',
21
+ text: '#202124',
22
+ mutedText: '#64656b',
23
+ border: '#d8d8dc',
24
+ accent: '#0067c0',
25
+ selectedBackground: '#dcecff',
26
+ },
27
+ };
28
+ exports.ThemeContext = react_1.default.createContext(exports.DefaultTheme);
29
+ const useNavigationTheme = () => react_1.default.useContext(exports.ThemeContext);
30
+ exports.useNavigationTheme = useNavigationTheme;
31
+ function useStore() {
32
+ const store = react_1.default.useContext(exports.StoreContext);
33
+ if (!store)
34
+ throw new Error('Navigation must be rendered inside NavigationContainer.');
35
+ return store;
36
+ }
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ export interface FocusTarget {
3
+ focus(): void;
4
+ }
5
+ export interface FocusRegistry {
6
+ register(id: string, target: FocusTarget): () => void;
7
+ record(id: string): void;
8
+ }
9
+ export declare const FocusContext: React.Context<FocusRegistry | null>;
10
+ /** Wrap a ref-forwarding native control (Pressable, TextInput, or a custom control). */
11
+ export declare function NavigationFocusable({ id, children, }: {
12
+ id: string;
13
+ children: React.ReactElement<any>;
14
+ }): React.ReactElement<any, string | React.JSXElementConstructor<any>>;