@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
package/docs/GUIDE.md ADDED
@@ -0,0 +1,384 @@
1
+ # Guide
2
+
3
+ This guide covers the full JS API exported from `@natsuneko-laboratory/react-native-desktop-navigation` (the `/` entry point): Stack, Sidebar, Tabs, Split, hooks, keyboard handling, persistence, and styling.
4
+
5
+ For the experimental SwiftUI / WinUI renderer exported from `/native`, see [NATIVE.md](../NATIVE.md). For a full running example, see [examples/desktop](../examples/desktop).
6
+
7
+ ## Stack
8
+
9
+ ```tsx
10
+ import {
11
+ NavigationContainer,
12
+ createStackNavigator,
13
+ type StackScreenProps,
14
+ } from '@natsuneko-laboratory/react-native-desktop-navigation';
15
+ import { Button, Text } from 'react-native';
16
+
17
+ type Params = {
18
+ Home: undefined;
19
+ Profile: { userId: string };
20
+ };
21
+ const Stack = createStackNavigator<Params>();
22
+
23
+ function Home({ navigation }: StackScreenProps<Params, 'Home'>) {
24
+ return (
25
+ <Button
26
+ title="Profile"
27
+ onPress={() => navigation.push('Profile', { userId: '123' })}
28
+ />
29
+ );
30
+ }
31
+ function Profile({ route }: StackScreenProps<Params, 'Profile'>) {
32
+ return <Text>{route.params.userId}</Text>;
33
+ }
34
+ export default function App() {
35
+ return (
36
+ <NavigationContainer>
37
+ <Stack.Navigator
38
+ initialRouteName="Home"
39
+ screenOptions={{ animation: 'fade' }}
40
+ >
41
+ <Stack.Screen name="Home" component={Home} />
42
+ <Stack.Screen
43
+ name="Profile"
44
+ component={Profile}
45
+ options={({ route }) => ({ title: route.params.userId })}
46
+ />
47
+ </Stack.Navigator>
48
+ </NavigationContainer>
49
+ );
50
+ }
51
+ ```
52
+
53
+ Calling `navigate('Profile')` with a wrong route name or params produces a type error. If a screen requires params at launch, provide `initialParams` on that Screen. Keep screen declarations stable while mounted; when adding or removing declarations, or changing a Navigator's `id`, change the React `key` to force a remount.
54
+
55
+ | API | Behavior |
56
+ | -------------------------------- | ------------------------------------------------------------------------------------- |
57
+ | `navigate(name, params?)` | Goes back to the closest route with the same name, or pushes a new one if none exists |
58
+ | `push(name, params?)` | Adds a new instance with a unique key |
59
+ | `replace(name, params?)` | Replaces the active route |
60
+ | `pop(count = 1)` | Removes the given number of routes, keeping the root |
61
+ | `popTo(name, params?)` | Goes back to the given route; warns and no-ops if it doesn't exist |
62
+ | `popToRoot()` | Returns to the first route |
63
+ | `goBack()` / `canGoBack()` | Handles going back from the current Navigator to its parent |
64
+ | `goForward()` / `canGoForward()` | Moves forward to the next state in desktop history |
65
+
66
+ `historyBehavior="stack"` is the default. With `"desktop"`, the route array before each transition is kept as Back / Forward history, and the Forward history is discarded when a new transition happens after going Back. There is no cap on history size.
67
+
68
+ Screen options support `title`, `headerShown`, `header`, `headerLeft`, `headerRight`, `contentStyle`, `animation`, `presentation`, `focusBehavior`, and `inactiveBehavior`. `header` and similar options accept a ReactNode or a function that receives props.
69
+
70
+ - `inactiveBehavior`: `keep` (default) keeps the local state of hidden screens without unmounting them. `unmount` destroys the inactive component. The navigation state of child Navigators is preserved either way.
71
+ - `animation`: `default` / `none` / `fade` / `slide-horizontal` / `slide-vertical`. Enter animations are implemented with React Native's `Animated`.
72
+ - `presentation`: `card` / `modal` / `dialog`. `modal` and `dialog` are overlays built with React Native `View`; they are not OS-native dialogs and do not provide a complete keyboard focus trap.
73
+
74
+ ## Sidebar / Tabs
75
+
76
+ ```tsx
77
+ const Sidebar = createSidebarNavigator<{
78
+ Timeline: undefined;
79
+ Albums: undefined;
80
+ Settings: undefined;
81
+ }>();
82
+
83
+ <Sidebar.Navigator width={240} collapsible>
84
+ <Sidebar.Section title="Workspace">
85
+ <Sidebar.Screen name="Timeline" component={Timeline} />
86
+ </Sidebar.Section>
87
+ <Sidebar.Section title="Library">
88
+ <Sidebar.Screen name="Albums" component={Albums} options={{ badge: 3 }} />
89
+ <Sidebar.Screen
90
+ name="Settings"
91
+ component={Settings}
92
+ options={{ disabled: false }}
93
+ />
94
+ </Sidebar.Section>
95
+ </Sidebar.Navigator>;
96
+ ```
97
+
98
+ Sidebar supports `label`, `icon` (a ReactNode, or a function that receives `{ focused, disabled }`), `badge`, `hidden`, and `disabled`. Use `select(name)` or `navigate(name, params?)` to select an item, and `collapseSidebar()` / `expandSidebar()` / `toggleSidebar()` to open and close it. It also supports `position="right"`, `width` / `minWidth` / `maxWidth`, `collapsedWidth`, and `defaultCollapsed`.
99
+
100
+ Arrow Up / Down, Home, and End move focus among visible, enabled items, and Enter selects the focused item. Avoid configurations where every item is hidden or disabled.
101
+
102
+ `createTabNavigator<Params>()` also returns `Navigator` and `Screen`. Tabs render at the top of the screen and support `select` / `navigate` / `nextTab` / `previousTab`. Switch tabs with Ctrl+Tab / Ctrl+Shift+Tab, and use ← / → / Home / End / Enter within the tab bar.
103
+
104
+ ## Split and nesting
105
+
106
+ ```tsx
107
+ const Split = createSplitNavigator();
108
+
109
+ <Split.Navigator
110
+ layout={({ width }) => (width < 700 ? ['content'] : ['sidebar', 'content'])}
111
+ onColumnResize={(id, width) => console.log(id, width)}
112
+ >
113
+ <Split.Column
114
+ id="sidebar"
115
+ component={SidebarNavigation}
116
+ minWidth={180}
117
+ maxWidth={320}
118
+ />
119
+ <Split.Column id="content" component={ContentNavigation} minWidth={320} />
120
+ </Split.Navigator>;
121
+ ```
122
+
123
+ Split supports 2 or 3 columns. Dragging a divider with the mouse, pressing ← / →, or using accessibility increase/decrease actions updates the widths of the two adjacent columns together, respects the min/max on both sides, and preserves the position of the other divider when there are three columns. The displayed width is computed from the measured width of the Split itself and of each Divider, and the last visible column absorbs the remaining width. When the window shrinks, each column shrinks down to its `minWidth`. If the total falls below the sum of all columns' `minWidth`, hide a column via `layout`. `layout` must return one or more unique, known column IDs. Hidden columns keep their component and navigation state. Whether a column is shown or hidden is determined entirely by the return value of `layout`.
124
+
125
+ ### Making the Sidebar bar follow the Divider too
126
+
127
+ If the Sidebar's bar has a fixed numeric width, changing the Split column's width won't change the bar's own width. When you place the item list in a dedicated Split column, specify `width="fill"`. Dragging the column then makes the items, selection background, and footer all follow the column's width. The bar never draws beyond the column's width.
128
+
129
+ ```tsx
130
+ function SidebarNavigation() {
131
+ return (
132
+ <Sidebar.Navigator width="fill" collapsedWidth={56}>
133
+ {/* Sidebar.Screen for the item list; screen content lives in another Split column */}
134
+ </Sidebar.Navigator>
135
+ );
136
+ }
137
+
138
+ <Split.Navigator onColumnResize={(id, width) => saveWidth(id, width)}>
139
+ <Split.Column
140
+ id="sidebar"
141
+ component={SidebarNavigation}
142
+ defaultWidth={240}
143
+ minWidth={180}
144
+ maxWidth={400}
145
+ />
146
+ <Split.Column id="content" component={ContentNavigation} minWidth={320} />
147
+ </Split.Navigator>;
148
+ ```
149
+
150
+ `width="fill"` makes the Sidebar's bar use the entire width of its parent. If you display a screen next to the bar within the same Sidebar Navigator, keep using a numeric `width` as before. Collapsing a Sidebar placed directly in a `Split.Column` also shrinks the column width and the Divider down to `collapsedWidth` (default 56). Expanding it restores the last drag-adjusted column width, recalculated to fit the current window width and each column's min/max. This also works for a right-side Sidebar, `defaultCollapsed`, programmatic open/close, and restoring from saved state.
151
+
152
+ While collapsed, `collapsedWidth` takes priority over the column's `minWidth` and pins the column at that width. To keep the column width fixed and only open/close the Sidebar's bar, set `collapsible={false}` on the `Split.Column`. A Sidebar nested further inside a Stack or Tabs screen does not affect the outer Split column's width.
153
+
154
+ Column widths and divider positions read the same navigation state. `onColumnResize` is called for each column whose width changed, whether from a drag or a linked Sidebar open/close, so a single drag can fire two notifications (for the columns on either side). Width recalculation from a window resize is reflected via `onStateChange`. Neither resizing nor toggling divider visibility remounts a column's component.
155
+
156
+ A Navigator can be placed inside a Screen's or Column's component. Actions are handled by the current Navigator and, if unhandled, propagate to the parent. Between sibling Navigators, you can target one explicitly with `dispatch({ type, target: nodeId, ... })`. When placing multiple Navigators of the same kind under the same Screen, give each a unique `id`.
157
+
158
+ ## Hooks / Events / Focus
159
+
160
+ ```tsx
161
+ const navigation = useNavigation<StackNavigation<Params>>();
162
+ const route = useRoute<NavigationRoute<'Profile', Params['Profile']>>();
163
+ const activeKey = useNavigationState((state) => state.activeRouteKey);
164
+
165
+ useFocusEffect(
166
+ React.useCallback(() => {
167
+ const unsubscribe = subscribeToUpdates();
168
+ return unsubscribe;
169
+ }, []),
170
+ );
171
+
172
+ React.useEffect(
173
+ () =>
174
+ navigation.addListener('beforeRemove', (event) => {
175
+ if (hasUnsavedChanges) event.preventDefault();
176
+ }),
177
+ [navigation, hasUnsavedChanges],
178
+ );
179
+ ```
180
+
181
+ `focus` / `blur` / `beforeRemove` / `state` events are dispatched, and `addListener` returns an unsubscribe function. When an action such as going back or replacing also removes child screens, their `beforeRemove` listeners are checked too; if any one of them is prevented, the whole action is not applied. On a focus change, `blur` is dispatched from child to parent, and `focus` from parent to child.
182
+
183
+ Because desktop focus APIs differ per platform, register native controls that need reliable focus restoration with `NavigationFocusable`.
184
+
185
+ ```tsx
186
+ <NavigationFocusable id="search">
187
+ <TextInput accessibilityLabel="Search" />
188
+ </NavigationFocusable>
189
+ ```
190
+
191
+ The child should be a control that exposes `ref.focus()` and `onFocus`. `focusBehavior="restore"` (default) restores the most recently registered control, `"first"` restores the first registered control, and `"none"` leaves it to the app. There is no automatic restoration that scans unregistered native controls.
192
+
193
+ ## Keyboard / Platform Adapter
194
+
195
+ The default Back / Forward shortcuts are Cmd+[ / Cmd+] on macOS and Alt+Left / Alt+Right on Windows. Key events that bubble within the Container are handled there. Events already handled by a child, and events during TextInput editing, are left alone. To implement your own shortcut scope, call `stopPropagation()` in the child's handler.
196
+
197
+ You can disable this with `keyboardShortcuts={false}`. Pass shortcut arrays via `{ back, forward, nextTab, previousTab }` to override the defaults. Use `platformAdapter` to replace transitions, default widths, TextInput detection, and key bindings.
198
+
199
+ Native event wiring is based on the official [macOS View events](https://microsoft.github.io/react-native-macos/api/view-events) and [Windows IKeyboardProps](https://microsoft.github.io/react-native-windows/docs/ikeyboardprops-api/) docs. Verify native key event / focus behavior against your app's RN desktop version.
200
+
201
+ ## Persistence and Ref
202
+
203
+ ```tsx
204
+ const navigationRef = createNavigationRef<Params>();
205
+ const initialState = savedJson ? restoreNavigationState(savedJson) : undefined;
206
+
207
+ <NavigationContainer
208
+ ref={navigationRef}
209
+ initialState={initialState}
210
+ onStateChange={(state) => save(serializeNavigationState(state))}
211
+ >
212
+ <AppNavigator />
213
+ </NavigationContainer>;
214
+
215
+ if (navigationRef.isReady()) {
216
+ navigationRef.navigate('Profile', { userId: '123' });
217
+ }
218
+ ```
219
+
220
+ Ref operations before the Container mounts or after it unmounts are no-ops. `initialState` is only read on the first mount. The snapshot is a version-1 node tree; putting non-JSON values such as functions, circular references, or `Date` objects into params will throw. Components, options, React local state, and the last-focused control are not persisted. You choose the storage backend.
221
+
222
+ `theme` accepts a `NavigationTheme`, and `DefaultTheme` / `useNavigationTheme` are exported.
223
+
224
+ ## Customizing component design
225
+
226
+ Overall coloring comes from the Container's `theme`; shape, spacing, and text for each part are set through the following props / options. They accept `StyleSheet.create` values, arrays, and regular React Native `StyleProp`s.
227
+
228
+ | Target | Customization API |
229
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
230
+ | Container / whole Navigator | `style` (Container also has `theme`) |
231
+ | Screen | `sceneStyle` (outer frame) and `contentStyle` (screen content) in `screenOptions` / Screen `options` |
232
+ | Stack Header | `headerStyle`, `headerTitleStyle`, `headerTintColor` |
233
+ | Header left/right, Back | `headerLeftContainerStyle`, `headerRightContainerStyle`, `headerBackButtonStyle`, `headerBackTitleStyle`, `headerBackTitle` |
234
+ | Modal / Dialog | `overlayStyle` (background) and `dialogStyle` (dialog surface) in Stack options |
235
+ | Sidebar / Tabs bar | Navigator's `barStyle`, `barContentStyle` (inside the scroll area), `contentStyle` (outer frame of the screen area) |
236
+ | Sidebar / Tabs items | Screen options' `itemStyle`, `labelStyle`, `iconContainerStyle`, `badgeStyle`, `renderItemContent` |
237
+ | Sidebar Section heading | Navigator's `sectionStyle` / `sectionTitleStyle`, Section's `style` / `titleStyle` / `renderTitle` |
238
+ | Sidebar collapse button | Navigator's `collapseButtonShown`, `collapseButtonStyle`, `collapseLabelStyle`, `renderCollapseButtonContent` |
239
+ | Split columns | Navigator's `columnStyle`, Column's `style` / `contentStyle` |
240
+ | Split dividers | Navigator's / Column's `dividerShown`, `dividerStyle`, `renderDivider` |
241
+
242
+ `screenOptions` sets the default for every Screen. Specifying the same option on a Screen's `options` overrides it for that screen. Your styles are applied after the built-in default styles. However, navigation itself still controls `display: none` on hidden scenes, Split column widths / min-max / visibility, and `opacity` / `transform` during transitions. Set column widths via `defaultWidth` / `minWidth` / `maxWidth` and resize interactions.
243
+
244
+ ### Selected, focused, hovered, and pressed states
245
+
246
+ ```tsx
247
+ <Sidebar.Navigator
248
+ barStyle={{ backgroundColor: '#201a2b', borderRightWidth: 0 }}
249
+ barContentStyle={{ padding: 8 }}
250
+ screenOptions={{
251
+ itemStyle: ({ selected, focused, hovered, pressed, disabled }) => ({
252
+ backgroundColor: pressed
253
+ ? '#68458c'
254
+ : selected
255
+ ? '#493265'
256
+ : hovered
257
+ ? '#30253e'
258
+ : 'transparent',
259
+ borderColor: focused ? '#c4a0ff' : 'transparent',
260
+ opacity: disabled ? 0.4 : 1,
261
+ borderRadius: 10,
262
+ }),
263
+ labelStyle: ({ selected }) => ({
264
+ color: '#fff',
265
+ fontWeight: selected ? '700' : '400',
266
+ }),
267
+ badgeStyle: { color: '#ddc9ff', paddingHorizontal: 6 },
268
+ }}
269
+ >
270
+ {/* Sidebar.Screen / Sidebar.Section */}
271
+ </Sidebar.Navigator>
272
+ ```
273
+
274
+ Each of the four item style options accepts either a static style or a function that receives `NavigationItemState`. The state includes `selected`, `focused`, `hovered`, `pressed`, `disabled`, and `collapsed`. `selected` reflects the currently displayed screen, and `focused` reflects native / keyboard focus on the item. The existing `icon({ focused })` still uses `focused` to mean the selected state, as before. `badgeStyle` styles a string/number badge; for a ReactNode badge, style the node itself.
275
+
276
+ ### Hiding the Divider / replacing the Sidebar footer
277
+
278
+ To fully hide a Split divider, set `dividerShown={false}`. This removes not just the line, but also the divider's hit area, the mouse-drag region, and its keyboard / accessibility actions. The column's component and state are preserved.
279
+
280
+ ```tsx
281
+ <Split.Navigator dividerShown={false}>
282
+ <Split.Column id="sidebar" component={SidebarNavigation} minWidth={180} />
283
+ <Split.Column id="content" component={ContentNavigation} minWidth={320} />
284
+ </Split.Navigator>
285
+ ```
286
+
287
+ Setting `dividerShown` on a Column overrides the Navigator's setting for the divider immediately after that column. The last visible column never shows a divider. `renderDivider={() => null}` is the existing API for hiding only the divider's contents; use `dividerShown` when you want to remove the whole hit area. To keep resize interactions but make only the line invisible, use `dividerStyle={{ backgroundColor: 'transparent' }}`.
288
+
289
+ The Sidebar's own edge border is separate from the Split divider. To remove both, also set `barStyle={{ borderLeftWidth: 0, borderRightWidth: 0 }}` on the Sidebar.
290
+
291
+ The `«` shown at the bottom of the Sidebar is its collapse button. To hide only the button, set `collapseButtonShown={false}`. To replace the entire footer with custom UI, use `renderSidebarFooter`.
292
+
293
+ ```tsx
294
+ <Sidebar.Navigator
295
+ sidebarFooterStyle={{ padding: 8 }}
296
+ renderSidebarFooter={({ collapsed, toggleSidebar }) => (
297
+ <Pressable
298
+ accessibilityRole="button"
299
+ accessibilityLabel={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
300
+ focusable
301
+ onPress={toggleSidebar}
302
+ style={{ padding: 10, borderWidth: 0 }}
303
+ >
304
+ <Text>{collapsed ? 'Expand' : 'Collapse'}</Text>
305
+ </Pressable>
306
+ )}
307
+ >
308
+ {/* Sidebar.Screen */}
309
+ </Sidebar.Navigator>
310
+ ```
311
+
312
+ This renderer entirely replaces the default Pressable. `renderSidebarFooter={() => null}` also removes the footer's outer frame and padding. The existing `renderCollapseButtonContent` remains available as the API for replacing only the contents of the default button.
313
+
314
+ `SidebarFooterProps` provides `collapsed`, `toggleSidebar` / `collapseSidebar` / `expandSidebar`, and `children`, the default button's contents. Rendering `children` lets you include the default button inside your own footer. When `collapseButtonShown={false}` or `collapsible={false}`, `children` is `null`. A fully custom footer can still be shown independent of these settings. Labels, roles, and key handling for a fully replaced UI are your app's responsibility.
315
+
316
+ ### Replacing item and divider contents
317
+
318
+ ```tsx
319
+ <Sidebar.Screen
320
+ name="Albums"
321
+ component={Albums}
322
+ options={{
323
+ renderItemContent: ({ label, selected, collapsed, children }) => (
324
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
325
+ <View style={{ width: 3, height: 20, backgroundColor: selected ? '#c4a0ff' : 'transparent' }} />
326
+ {children}
327
+ {!collapsed && <Text accessibilityElementsHidden>{label.length}</Text>}
328
+ </View>
329
+ ),
330
+ }}
331
+ />
332
+
333
+ <Split.Navigator
334
+ dividerStyle={({ dragging }) => ({ width: 12, backgroundColor: dragging ? '#493265' : '#222' })}
335
+ renderDivider={({ columnId, width, dragging }) => (
336
+ <View style={{ width: 3, height: 40, backgroundColor: dragging ? '#c4a0ff' : '#555' }} />
337
+ )}
338
+ >
339
+ {/* Split.Column */}
340
+ </Split.Navigator>
341
+ ```
342
+
343
+ `renderItemContent`'s `children` is the standard icon / label / badge with your styles already applied. You can also replace the whole thing. The library still manages the outer Pressable, so click handling, keyboard selection, `disabled`, the accessibility label, and selection state all keep working. Return only display nodes; do not nest independent interactive elements such as buttons inside the return value.
344
+
345
+ `renderCollapseButtonContent` also receives the standard `children` and `collapsed`. `renderDivider` receives `columnId`, the column's current `width`, and `dragging`, while the outer resize / keyboard / accessibility handling is preserved. A renderer set on a Column takes priority over the Navigator's renderer, and divider styles are applied Navigator first, then Column.
346
+
347
+ The whole header can also be replaced with the existing `header`, and its left/right contents with `headerLeft` / `headerRight`. When you replace these entirely, implement your own rendering, interaction, and accessibility attributes in the renderer. The default Dialog background color is `theme.colors.surface`.
348
+
349
+ Styles and renderers you configure are not saved into the navigation state JSON. See [Customization.tsx](../examples/desktop/Customization.tsx) for a runnable example.
350
+
351
+ ## Verification and compatibility
352
+
353
+ The peerDependencies are React `>=18` / React Native `>=0.78`. This is the installable range, not a guarantee that every combination has been verified natively.
354
+
355
+ | Library | React / RN types | RN macOS native | RN Windows native |
356
+ | ------- | ----------------------------------------------------- | --------------- | ----------------- |
357
+ | 0.1.0 | Locally automated tests with React 19.1.0 / RN 0.81.0 | Unverified | Unverified |
358
+
359
+ CI runs type checking, Router / Store tests, React UI tests with native primitives replaced, and package builds on Linux / macOS / Windows. As of adding this CI configuration, its results on remote CI runners have not yet been confirmed. Only versions that have completed real-device verification will be added to this table.
360
+
361
+ ```sh
362
+ npm ci
363
+ npm run check
364
+ npm run format:check
365
+ npm pack --dry-run
366
+ ```
367
+
368
+ An [Example App](../examples/desktop/App.tsx) and [run / acceptance instructions](../examples/desktop/README.md) are provided. Automated tests verify screen retention, nesting, focus restoration, per-OS key bindings, Tabs, Split dragging, responsive layout, and serialize / restore.
369
+
370
+ ## Internal structure
371
+
372
+ ```text
373
+ src/core Store / Container / hooks / events / focus / scene
374
+ src/routers Platform- and React-independent state transitions
375
+ src/stack Stack / Header
376
+ src/sidebar Sidebar / Section
377
+ src/tabs Desktop Tabs
378
+ src/split Columns / resizing / responsive layout
379
+ src/platform macOS / Windows adapters and the type boundary for native props
380
+ ```
381
+
382
+ The Router alone is available from `@natsuneko-laboratory/react-native-desktop-navigation/routers`. A Navigator dispatches actions to the Store; it never mutates the Router or State directly.
383
+
384
+ Multi-window support, detachable tabs, native title bars, Group, deep linking, and custom transition drivers are out of scope for this version.
@@ -0,0 +1,4 @@
1
+ #import <React/RCTViewComponentView.h>
2
+
3
+ @interface DDNNavigationComponentView : RCTViewComponentView
4
+ @end
@@ -0,0 +1,40 @@
1
+ #import "DDNNavigationComponentView.h"
2
+ #if __has_include(<DesktopNavigation/DesktopNavigation-Swift.h>)
3
+ #import <DesktopNavigation/DesktopNavigation-Swift.h>
4
+ #else
5
+ #import "DesktopNavigation-Swift.h"
6
+ #endif
7
+ #import <react/renderer/components/DesktopNavigationSpec/ComponentDescriptors.h>
8
+ #import <react/renderer/components/DesktopNavigationSpec/EventEmitters.h>
9
+ #import <react/renderer/components/DesktopNavigationSpec/Props.h>
10
+
11
+ using namespace facebook::react;
12
+
13
+ @implementation DDNNavigationComponentView {
14
+ DDNNavigationView *_navigationView;
15
+ }
16
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
17
+ return concreteComponentDescriptorProvider<DesktopNavigationHostComponentDescriptor>();
18
+ }
19
+ - (instancetype)initWithFrame:(CGRect)frame {
20
+ if (self = [super initWithFrame:frame]) {
21
+ _props = std::make_shared<const DesktopNavigationHostProps>();
22
+ _navigationView = [[DDNNavigationView alloc] initWithFrame:frame];
23
+ __weak DDNNavigationComponentView *weakSelf = self;
24
+ _navigationView.onEvent = ^(NSString *payload) {
25
+ DDNNavigationComponentView *strongSelf = weakSelf;
26
+ if (!strongSelf || !strongSelf->_eventEmitter) return;
27
+ auto emitter = std::static_pointer_cast<const DesktopNavigationHostEventEmitter>(strongSelf->_eventEmitter);
28
+ emitter->onNavigationEvent({std::string(payload.UTF8String)});
29
+ };
30
+ self.contentView = _navigationView;
31
+ }
32
+ return self;
33
+ }
34
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
35
+ const auto &next = *std::static_pointer_cast<const DesktopNavigationHostProps>(props);
36
+ [_navigationView setConfiguration:[NSString stringWithUTF8String:next.configuration.c_str()]];
37
+ [super updateProps:props oldProps:oldProps];
38
+ }
39
+ + (BOOL)shouldBeRecycled { return NO; }
40
+ @end