@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.
- package/DesktopNavigation.podspec +18 -0
- package/LICENSE +21 -0
- package/NATIVE.md +300 -0
- package/README.md +127 -0
- package/dist/core/NavigationContainer.d.ts +23 -0
- package/dist/core/NavigationContainer.js +68 -0
- package/dist/core/NavigationItem.d.ts +27 -0
- package/dist/core/NavigationItem.js +71 -0
- package/dist/core/Scene.d.ts +19 -0
- package/dist/core/Scene.js +124 -0
- package/dist/core/SelectionNavigator.d.ts +38 -0
- package/dist/core/SelectionNavigator.js +194 -0
- package/dist/core/builder.d.ts +40 -0
- package/dist/core/builder.js +81 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/context.js +36 -0
- package/dist/core/focus.d.ts +14 -0
- package/dist/core/focus.js +39 -0
- package/dist/core/hooks.d.ts +8 -0
- package/dist/core/hooks.js +48 -0
- package/dist/core/navigation.d.ts +6 -0
- package/dist/core/navigation.js +57 -0
- package/dist/core/store.d.ts +39 -0
- package/dist/core/store.js +391 -0
- package/dist/core/types.d.ts +155 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +46 -0
- package/dist/native/host.d.ts +100 -0
- package/dist/native/host.js +190 -0
- package/dist/native/icons.d.ts +56 -0
- package/dist/native/icons.js +55 -0
- package/dist/native/index.d.ts +13 -0
- package/dist/native/index.js +44 -0
- package/dist/native/sidebar.d.ts +39 -0
- package/dist/native/sidebar.js +126 -0
- package/dist/native/specs/DesktopNavigationHostNativeComponent.d.ts +10 -0
- package/dist/native/specs/DesktopNavigationHostNativeComponent.js +4 -0
- package/dist/native/split.d.ts +27 -0
- package/dist/native/split.js +105 -0
- package/dist/native/stack.d.ts +26 -0
- package/dist/native/stack.js +66 -0
- package/dist/platform/index.d.ts +52 -0
- package/dist/platform/index.js +28 -0
- package/dist/platform/macos.d.ts +2 -0
- package/dist/platform/macos.js +18 -0
- package/dist/platform/windows.d.ts +2 -0
- package/dist/platform/windows.js +18 -0
- package/dist/routers/index.d.ts +23 -0
- package/dist/routers/index.js +393 -0
- package/dist/routers/types.d.ts +116 -0
- package/dist/routers/types.js +2 -0
- package/dist/sidebar/index.d.ts +17 -0
- package/dist/sidebar/index.js +18 -0
- package/dist/split/index.d.ts +42 -0
- package/dist/split/index.js +199 -0
- package/dist/stack/index.d.ts +23 -0
- package/dist/stack/index.js +88 -0
- package/dist/tabs/index.d.ts +13 -0
- package/dist/tabs/index.js +15 -0
- package/docs/GUIDE.md +384 -0
- package/macos/DDNNavigationComponentView.h +4 -0
- package/macos/DDNNavigationComponentView.mm +40 -0
- package/macos/DDNNavigationView.swift +398 -0
- package/macos/DDNNavigationViewManager.mm +11 -0
- package/package.json +82 -0
- package/react-native.config.js +19 -0
- package/src/core/NavigationContainer.tsx +137 -0
- package/src/core/NavigationItem.tsx +149 -0
- package/src/core/Scene.tsx +194 -0
- package/src/core/SelectionNavigator.tsx +371 -0
- package/src/core/builder.tsx +147 -0
- package/src/core/context.tsx +43 -0
- package/src/core/focus.tsx +50 -0
- package/src/core/hooks.ts +42 -0
- package/src/core/navigation.ts +77 -0
- package/src/core/store.ts +471 -0
- package/src/core/types.ts +176 -0
- package/src/index.ts +36 -0
- package/src/native/host.tsx +377 -0
- package/src/native/icons.ts +110 -0
- package/src/native/index.ts +56 -0
- package/src/native/sidebar.tsx +230 -0
- package/src/native/specs/DesktopNavigationHostNativeComponent.ts +12 -0
- package/src/native/split.tsx +160 -0
- package/src/native/stack.tsx +157 -0
- package/src/platform/index.tsx +76 -0
- package/src/platform/macos.ts +15 -0
- package/src/platform/windows.ts +15 -0
- package/src/routers/index.ts +442 -0
- package/src/routers/types.ts +117 -0
- package/src/sidebar/index.tsx +42 -0
- package/src/split/index.tsx +350 -0
- package/src/stack/index.tsx +213 -0
- package/src/tabs/index.tsx +40 -0
- package/windows/DesktopNavigation/DesktopNavigation.def +3 -0
- package/windows/DesktopNavigation/DesktopNavigation.vcxproj +125 -0
- package/windows/DesktopNavigation/NavigationHost.cpp +432 -0
- package/windows/DesktopNavigation/ReactPackageProvider.cpp +9 -0
- package/windows/DesktopNavigation/ReactPackageProvider.h +10 -0
- package/windows/DesktopNavigation/ReactPackageProvider.idl +6 -0
- package/windows/DesktopNavigation/pch.cpp +1 -0
- package/windows/DesktopNavigation/pch.h +27 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
3
|
+
Pod::Spec.new do |s|
|
|
4
|
+
s.name = 'DesktopNavigation'
|
|
5
|
+
s.version = package['version']
|
|
6
|
+
s.summary = package['description']
|
|
7
|
+
s.homepage = 'https://github.com/mika-f/react-native-desktop-navigation'
|
|
8
|
+
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
|
9
|
+
s.author = 'Natsuneko Laboratory'
|
|
10
|
+
s.source = { :git => s.homepage + '.git', :tag => s.version.to_s }
|
|
11
|
+
s.platform = :osx, '14.0'
|
|
12
|
+
s.swift_version = '5.0'
|
|
13
|
+
s.source_files = 'macos/**/*.{h,mm,swift}'
|
|
14
|
+
# Keep React's C++ Fabric headers out of the Swift underlying module.
|
|
15
|
+
s.private_header_files = 'macos/DDNNavigationComponentView.h'
|
|
16
|
+
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'SWIFT_OBJC_INTERFACE_HEADER_NAME' => 'DesktopNavigation-Swift.h' }
|
|
17
|
+
install_modules_dependencies(s)
|
|
18
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kanon Mochizuki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NATIVE.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Native desktop navigation (Fabric / experimental)
|
|
2
|
+
|
|
3
|
+
`@natsuneko-laboratory/react-native-desktop-navigation/native` is an entry point that reuses the existing Router / Store / Context and connects navigation rendering to SwiftUI / WinUI. The existing `/` entry point continues to provide the React Native `View`-based Navigators.
|
|
4
|
+
|
|
5
|
+
| Navigator | macOS | Windows |
|
|
6
|
+
| ------------------------ | ---------------------------------------------- | ----------------------------------------------------------------------- |
|
|
7
|
+
| `createStackNavigator` | SwiftUI `NavigationStack` with a native header | A WinUI header and content area; history is managed by the shared Store |
|
|
8
|
+
| `createSidebarNavigator` | `NavigationSplitView` and `List` | `NavigationView` |
|
|
9
|
+
| `createSplitNavigator` | A 2-3 column `NavigationSplitView` | `SplitView`, plus an additional Grid / Thumb for 3 columns |
|
|
10
|
+
|
|
11
|
+
Each screen's React tree stays inside the original Fabric tree. React Scenes are placed into the content area the native side reports, so Context, per-screen local state, nested Navigators, and existing Hooks are all shared. React screens are never remounted onto a separate root or reparented into the native hierarchy.
|
|
12
|
+
|
|
13
|
+
## Requirements and setup
|
|
14
|
+
|
|
15
|
+
- **Fabric only.** This throws an error under Paper.
|
|
16
|
+
- macOS: implementation targets macOS **14 or later**, and React Native macOS **0.81.x or later**.
|
|
17
|
+
- Windows: uses React Native Windows **0.82 or later**'s Fabric / XAML Island APIs and requires `UseExperimentalWinUI3`. Windows on 0.81 is not supported.
|
|
18
|
+
- Metro's Babel preset should be the `@react-native/babel-preset` that matches your host's RN version. Static ViewConfigs are generated from the NativeComponent specs.
|
|
19
|
+
|
|
20
|
+
The package bundles a Podspec, Fabric Codegen specs, a Windows C++ project, and autolinking configuration. A native rebuild of your app is required after installation. There is no automatic fallback to the JS version when native linking is missing.
|
|
21
|
+
|
|
22
|
+
### macOS
|
|
23
|
+
|
|
24
|
+
Enable Fabric in your host app and set the deployment target to 14.0 or later. Run `bundle exec pod install` (or plain `pod install` for hosts not using Bundler) in the host's macOS directory, then rebuild in Xcode. This wires in the `DesktopNavigation` Pod and the Codegen `DesktopNavigationHost` component provider.
|
|
25
|
+
|
|
26
|
+
### Windows
|
|
27
|
+
|
|
28
|
+
Set the following in the `PropertyGroup` of your host's `windows/ExperimentalFeatures.props`, keeping any other existing settings, then run RNW's autolinking and build.
|
|
29
|
+
|
|
30
|
+
```xml
|
|
31
|
+
<UseNewArchitecture>true</UseNewArchitecture>
|
|
32
|
+
<UseExperimentalWinUI3>true</UseExperimentalWinUI3>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx react-native autolink-windows
|
|
37
|
+
npx react-native run-windows
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The library project targets the VS 2022 v143 toolset. Your host needs a matching Desktop C++ / Windows SDK / RNW build environment. For hosts where autolinking isn't available, reference `windows/DesktopNavigation/DesktopNavigation.vcxproj` directly and add `winrt::DesktopNavigation::ReactPackageProvider()` to the host's PackageProviders.
|
|
41
|
+
|
|
42
|
+
### Hosts that only use the JS version
|
|
43
|
+
|
|
44
|
+
The JS API itself doesn't require a native host. If you don't need to pull in the native project at all, add the following to your host's `react-native.config.js` `dependencies` to disable autolinking for the relevant platforms:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
'@natsuneko-laboratory/react-native-desktop-navigation': {
|
|
48
|
+
platforms: { ios: null, macos: null, windows: null },
|
|
49
|
+
},
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import {
|
|
56
|
+
NavigationContainer,
|
|
57
|
+
createStackNavigator,
|
|
58
|
+
type StackScreenProps,
|
|
59
|
+
} from '@natsuneko-laboratory/react-native-desktop-navigation/native';
|
|
60
|
+
import { Button, Text } from 'react-native';
|
|
61
|
+
|
|
62
|
+
type Params = { Home: undefined; Detail: { id: string } };
|
|
63
|
+
const Stack = createStackNavigator<Params>();
|
|
64
|
+
|
|
65
|
+
function Home({ navigation }: StackScreenProps<Params, 'Home'>) {
|
|
66
|
+
return (
|
|
67
|
+
<Button
|
|
68
|
+
title="Open"
|
|
69
|
+
onPress={() => navigation.push('Detail', { id: '42' })}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
function Detail({ route }: StackScreenProps<Params, 'Detail'>) {
|
|
74
|
+
return <Text>{route.params.id}</Text>;
|
|
75
|
+
}
|
|
76
|
+
export default function App() {
|
|
77
|
+
return (
|
|
78
|
+
<NavigationContainer>
|
|
79
|
+
<Stack.Navigator
|
|
80
|
+
appearance={{
|
|
81
|
+
backgroundColor: '#201a2b',
|
|
82
|
+
foregroundColor: '#ffffff',
|
|
83
|
+
accentColor: '#c4a0ff',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<Stack.Screen name="Home" component={Home} />
|
|
87
|
+
<Stack.Screen
|
|
88
|
+
name="Detail"
|
|
89
|
+
component={Detail}
|
|
90
|
+
options={{ title: 'Detail' }}
|
|
91
|
+
/>
|
|
92
|
+
</Stack.Navigator>
|
|
93
|
+
</NavigationContainer>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`NavigationContainer`, the ref, Hooks, `StackNavigation` / `SidebarNavigation`, and the persistence functions are the same implementation as the JS version. You can combine them with the JS version's Tabs or a custom Sidebar inside the same Container. `/native` does not export Tabs; import Tabs from `/` instead.
|
|
99
|
+
|
|
100
|
+
## Styling and API
|
|
101
|
+
|
|
102
|
+
Each Native Navigator's `appearance` accepts the following colors. Any value left unspecified falls back to the Container's `theme`'s `background` / `text` / `accent` / `surface`. Use `#RRGGBB` or `#RRGGBBAA` for native boundary colors.
|
|
103
|
+
|
|
104
|
+
- `backgroundColor`: content background
|
|
105
|
+
- `foregroundColor`: text color for headers and items
|
|
106
|
+
- `accentColor`: macOS tint color; Windows back button and selection indicator
|
|
107
|
+
- `sidebarBackgroundColor`: Sidebar background
|
|
108
|
+
|
|
109
|
+
Passing `null` for a value skips the Container's `theme` and falls back to the OS default appearance. On macOS, `sidebarBackgroundColor: null` gives a translucent sidebar material, `accentColor: null` uses the system accent color, and `foregroundColor: null` uses a label color that follows light / dark mode. On Windows, each control's own default color is used.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<Sidebar.Navigator
|
|
113
|
+
appearance={{ sidebarBackgroundColor: null, foregroundColor: null }}
|
|
114
|
+
/>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
On macOS, the native Sidebar does not apply `foregroundColor` to the label of the selected item (the OS's own selection color is preserved).
|
|
118
|
+
|
|
119
|
+
`style` applies to the whole Navigator, and `sceneStyle` / `contentStyle` apply to a Screen. This is not an API for applying an arbitrary `ViewStyle` directly to OS-drawn parts.
|
|
120
|
+
|
|
121
|
+
### Stack
|
|
122
|
+
|
|
123
|
+
`initialRouteName`, `historyBehavior`, `screenOptions`, Screen `options`, and typed params work the same as in the JS version. `title`, `headerShown`, and `headerBackTitle` are reflected in the native header.
|
|
124
|
+
|
|
125
|
+
For a screen that specifies a custom header via `header`, `headerStyle`, `headerTitleStyle`, `headerTintColor`, `headerLeft` / `headerRight`, or similar, rendering falls back to the existing React Native `StackHeader`. The navigation container itself remains native.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
<Stack.Navigator
|
|
129
|
+
screenOptions={{
|
|
130
|
+
headerStyle: { backgroundColor: '#201a2b', borderBottomWidth: 0 },
|
|
131
|
+
headerTitleStyle: { color: '#ffffff', fontSize: 18 },
|
|
132
|
+
contentStyle: { padding: 16 },
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{/* Stack.Screen */}
|
|
136
|
+
</Stack.Navigator>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
At this stage, only card presentation is supported; `presentation` / `animation` / overlay / dialog options are not provided. SwiftUI's own transition animations are also disabled, so the native container and the React Scene don't animate independently. Use the JS version's Stack for any part of the tree that needs these.
|
|
140
|
+
|
|
141
|
+
### Sidebar
|
|
142
|
+
|
|
143
|
+
`Navigator` accepts `initialRouteName`, `defaultCollapsed`, `width` (a number, 100 or greater), `appearance`, `style`, `screenOptions`, and `renderSidebarFooter`. `Screen` supports `label` / `title`, `icon`, `iconSize`, `badge`, `hidden`, `disabled`, and the common Scene options. Use `Section title="…"` for a heading.
|
|
144
|
+
|
|
145
|
+
Selection and open/close state stay in sync with the existing `select` / `navigate` / `collapseSidebar` / `expandSidebar` / `toggleSidebar`. The macOS width is the width suggested to SwiftUI.
|
|
146
|
+
|
|
147
|
+
`icon` accepts a React element (e.g. Lucide or `react-native-svg` JSX), a native system icon, or an image. macOS renders SF Symbols and Windows renders `FontIcon` / `BitmapIcon` inside the native item.
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<Sidebar.Screen
|
|
151
|
+
name="Home"
|
|
152
|
+
component={Home}
|
|
153
|
+
options={{
|
|
154
|
+
icon: ({ focused, disabled }) => ({
|
|
155
|
+
type: 'system',
|
|
156
|
+
macos: focused ? 'house.fill' : 'house',
|
|
157
|
+
windows: { glyph: '' },
|
|
158
|
+
size: 16,
|
|
159
|
+
color: disabled ? '#888888' : '#c4a0ff',
|
|
160
|
+
}),
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`icon` can be a React element, a `NativeSidebarIcon` object, `null`, or a function receiving `{ focused, disabled }`. As in the JS version, `focused` represents the item's **selected** state. Returning `null` hides the icon. Set a default with `screenOptions.icon` and override it per Screen if needed.
|
|
166
|
+
|
|
167
|
+
- `type: 'system'`: pass an SF Symbols name in `macos`, and a Unicode **string** in `windows.glyph`. If `fontFamily` is omitted on Windows, WinUI's standard OS icon font is used. To use a custom font, specify it like `windows: { glyph: '', fontFamily: 'My Icon Font' }` and make sure the host app bundles that font. Omitting the value for one OS shows text only on that OS.
|
|
168
|
+
- `size`: defaults to 16. Units are logical points / DIPs. Choose a size that fits the standard item layout.
|
|
169
|
+
- `color`: `#RRGGBB` / `#RRGGBBAA`. If omitted, the native item's own style is used.
|
|
170
|
+
|
|
171
|
+
You can also use a custom image.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
options={{
|
|
175
|
+
icon: {
|
|
176
|
+
type: 'image',
|
|
177
|
+
source: require('./icons/library.png'),
|
|
178
|
+
size: 18,
|
|
179
|
+
template: true,
|
|
180
|
+
color: '#c4a0ff',
|
|
181
|
+
},
|
|
182
|
+
}}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`source` is a Metro `require()` or `{ uri: '…' }`. Use a raster image format such as PNG that both OSes can read. `template: true` renders the image as a single-color icon based on its alpha channel; the default `false` preserves the image's original colors. `color` applies to system icons and template images.
|
|
186
|
+
|
|
187
|
+
Image URIs support HTTP(S) and `file://`, and on Windows also `ms-appx:///` / `ms-appdata:///`. Your host app must be able to access that file or URI. If loading fails, the item's title and selection still work. This image descriptor does not support SVG, data URIs, or sources requiring auth headers. Pass SVGs as a React element as described below instead. The icon image itself is treated as decorative; the accessibility label uses the item's title.
|
|
188
|
+
|
|
189
|
+
#### Badges
|
|
190
|
+
|
|
191
|
+
Setting `badge` to a number or string shows a native badge at the end of the item. A number `0` or below, and an empty string, hide it.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<Sidebar.Screen
|
|
195
|
+
name="Notifications"
|
|
196
|
+
component={Notifications}
|
|
197
|
+
options={{ badge: unreadCount }}
|
|
198
|
+
/>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
macOS uses SwiftUI's `badge`, and Windows uses `InfoBadge`. Since Windows' `InfoBadge` can only display integers, a non-numeric string falls back to a dot indicator.
|
|
202
|
+
|
|
203
|
+
#### Lucide / React element icons
|
|
204
|
+
|
|
205
|
+
In your app, install and natively link `lucide-react-native` and its dependency `react-native-svg` (see the [official Lucide guide](https://lucide.dev/guide/react-native/getting-started)). Neither is a runtime dependency of this library.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { House } from 'lucide-react-native';
|
|
209
|
+
import { createSidebarNavigator } from '@natsuneko-laboratory/react-native-desktop-navigation/native';
|
|
210
|
+
|
|
211
|
+
const Sidebar = createSidebarNavigator<{ Home: undefined }>();
|
|
212
|
+
|
|
213
|
+
<Sidebar.Screen
|
|
214
|
+
name="Home"
|
|
215
|
+
component={Home}
|
|
216
|
+
options={{
|
|
217
|
+
iconSize: 20,
|
|
218
|
+
icon: ({ focused, disabled }) => (
|
|
219
|
+
<House
|
|
220
|
+
size={20}
|
|
221
|
+
color={disabled ? '#888888' : focused ? '#c4a0ff' : '#ffffff'}
|
|
222
|
+
strokeWidth={1.75}
|
|
223
|
+
/>
|
|
224
|
+
),
|
|
225
|
+
}}
|
|
226
|
+
/>;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
You can also pass an element directly, e.g. `icon: <House size={20} color="#c4a0ff" />`. `iconSize` is the square area reserved inside the native item for a React icon, defaulting to 24 points / DIPs; it can also be set via `screenOptions.iconSize`. The element is centered, and anything that overflows the area is clipped. Set the icon's own `size` and `color` in the JSX; native selected/disabled colors are not applied to the JSX automatically. The descriptor's own size is still `icon.size`, unchanged.
|
|
230
|
+
|
|
231
|
+
React icons render inside the original Fabric tree and preserve React Context. Only the size of the empty icon area is sent to the native side — React elements and SVGs are never serialized to JSON or rasterized. They follow the position and clip region measured natively, and keep their SVG size while clipped during scrolling. Clicks and scrolling pass through to the native item, and icons are treated as decorative for accessibility. They are hidden while a macOS sidebar is collapsed, and follow the visible area of a compact Windows pane.
|
|
232
|
+
|
|
233
|
+
SVG icon rendering has been verified on macOS 0.81 / Fabric with `react-native-svg` 15.15.5; use 15.15.5 or later. Existing hosts also need to rebuild this library's native code.
|
|
234
|
+
|
|
235
|
+
See [`examples/desktop/NativeLucide.tsx`](examples/desktop/NativeLucide.tsx) for a runnable example.
|
|
236
|
+
|
|
237
|
+
#### Footer
|
|
238
|
+
|
|
239
|
+
`renderSidebarFooter` lets you place a React footer (e.g. an account display or settings button) below the item list.
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
<Sidebar.Navigator
|
|
243
|
+
renderSidebarFooter={({ collapsed, toggleSidebar }) => (
|
|
244
|
+
<Pressable
|
|
245
|
+
accessibilityRole="button"
|
|
246
|
+
accessibilityLabel="Settings"
|
|
247
|
+
focusable
|
|
248
|
+
onPress={openSettings}
|
|
249
|
+
style={{ padding: 12 }}
|
|
250
|
+
>
|
|
251
|
+
<Text>Settings</Text>
|
|
252
|
+
</Pressable>
|
|
253
|
+
)}
|
|
254
|
+
>
|
|
255
|
+
{/* Sidebar.Screen */}
|
|
256
|
+
</Sidebar.Navigator>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The footer renders inside the original Fabric tree, so Context, state, interaction, and accessibility all work as usual. The natural height measured on the React side is sent to the native side, which reserves an empty area of that height below the sidebar column's List on macOS, or in `NavigationView.PaneFooter` on Windows. The footer is positioned to match the area and width measured natively, and stays hidden — with only its height measured — before that first measurement. Because height is driven by the footer's own content, avoid styles like `flex: 1` that depend on a parent's height.
|
|
260
|
+
|
|
261
|
+
`SidebarFooterProps`'s `collapsed` and `toggleSidebar` / `collapseSidebar` / `expandSidebar` are the same as in the JS version. Because the native Sidebar shows its own OS-drawn collapse button, `children` is always `null` here. The footer is hidden while the sidebar is closed (collapsed on macOS, or a closed pane on Windows). Returning `null` from `renderSidebarFooter` removes the area entirely.
|
|
262
|
+
|
|
263
|
+
`renderItemContent`, ReactNode badges, and per-item RN styles are not provided by this Native Sidebar. If you need them, use the JS version's Sidebar inside a Native Split instead.
|
|
264
|
+
|
|
265
|
+
### Split
|
|
266
|
+
|
|
267
|
+
As with the JS version, declare 2 or 3 `Split.Column`s. A Column accepts `id`, `component`, `defaultWidth`, `minWidth`, `maxWidth`, and `style` / `contentStyle`. `layout(size)` selects which columns to show; hidden columns keep their React tree and navigation state.
|
|
268
|
+
|
|
269
|
+
`onColumnResize` fires for any column whose natively measured content width changed — from a drag, or from a window resize. Widths are saved to the Store, and on restore the native suggested width is used. Because SwiftUI decides its own spacing and divider width, pixel widths won't exactly match the JS version.
|
|
270
|
+
|
|
271
|
+
Arbitrary native divider rendering / hiding, and the JS version's `collapsible` (which ties a child Sidebar's collapse state to the column width), are not provided. If you only need a collapsible navigation pane, use the Native Sidebar instead. The Windows divider supports drag via its Thumb, and left/right arrow keys when focused.
|
|
272
|
+
|
|
273
|
+
## Relationship to the window (macOS)
|
|
274
|
+
|
|
275
|
+
The native host behaves as a view embedded in React Native's layout. Yoga determines its size; SwiftUI's content size does not constrain the window's size. Window safe areas from things like `NSWindowStyleMaskFullSizeContentView` (the region under a transparent title bar) are not applied to the host's slot placement — adjust padding on the React side if you place content under the title bar. Note that the toolbar area SwiftUI's `NavigationSplitView` itself reserves is still reflected in the Sidebar's detail column and in Split columns.
|
|
276
|
+
|
|
277
|
+
## State synchronization
|
|
278
|
+
|
|
279
|
+
Native back, selection, and open/close actions are treated as requests to dispatch an action to the shared Store; native history never overwrites JS state directly. Even if `beforeRemove` prevents a back action, the current state is re-notified to the native side. Delayed events that don't match the current settings revision are discarded.
|
|
280
|
+
|
|
281
|
+
Focus restoration happens after the native layout notification. `inactiveBehavior`, persistence, and Back / Forward under `historyBehavior="desktop"` all use the shared implementation.
|
|
282
|
+
|
|
283
|
+
## Verification scope
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
npm run check
|
|
287
|
+
npm run check:native-macos
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`check` runs TypeScript, Router / Store / React UI tests, the build, and Fabric Codegen. The Native UI tests cover `beforeRemove`, discarding stale events, screen retention, history restoration, Sidebar selection and icon switching, image source resolution, Split measurements, and nesting with JS Navigators.
|
|
291
|
+
|
|
292
|
+
`check:native-macos` requires macOS / Xcode. It compiles the SwiftUI implementation and inspects layout notifications for Stack / Sidebar (including SF Symbols and local-image icons) / 3-column Split inside an AppKit window. The window is never shown on screen.
|
|
293
|
+
|
|
294
|
+
This repository does not include an RN native host app. SVG icon rendering on macOS 0.81 / Fabric with `react-native-svg` 15.15.5 has been verified in a user's host app. **Windows C++ builds and on-device behavior are unverified.** As an experimental implementation at this stage, verify the following in your own host app:
|
|
295
|
+
|
|
296
|
+
- Native component registration, placement of nested containers, and window resizing.
|
|
297
|
+
- Native item selection, back navigation, and pane open/close staying in sync with JS state / focus.
|
|
298
|
+
- VoiceOver / Narrator, keyboard interaction, and input delivery / overlap between the Windows XAML Island and the React Scene.
|
|
299
|
+
|
|
300
|
+
Official references used during implementation: [RN macOS native development](https://microsoft.github.io/react-native-macos/docs/guides/native-development), the [RNW XAML Island component sample](https://github.com/microsoft/react-native-windows/blob/0.82-stable/packages/sample-custom-component/windows/SampleCustomComponent/CalendarView.cpp), and [NavigationView](https://learn.microsoft.com/en-us/windows/apps/design/controls/navigationview).
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# React Native Desktop Navigation
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@natsuneko-laboratory/react-native-desktop-navigation)
|
|
4
|
+
[](https://github.com/mika-f/react-native-desktop-navigation/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A desktop-first navigation framework for **React Native macOS / Windows**. Stack, Sidebar, top Tabs, and a resizable 2-3 column Split, composed behind one typed, declarative API.
|
|
8
|
+
|
|
9
|
+
This is not a port of a mobile navigation library — it's built from the ground up around desktop interaction: mouse-draggable Split dividers, OS-style Back/Forward keyboard shortcuts, keyboard/accessibility-driven focus, and per-state (hover/press/focus) styling. iOS, Android, Web, and React Navigation API/state compatibility are explicit non-goals.
|
|
10
|
+
|
|
11
|
+
The only required runtime dependencies are `react` and `react-native`.
|
|
12
|
+
|
|
13
|
+
> **Status:** `0.1.0-alpha` — the JS API is exercised by an automated test suite, but real-device verification is still limited. See [Verification and compatibility](docs/GUIDE.md#verification-and-compatibility) before shipping to production.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Stack** — typed push/pop/replace/popTo history, card/modal/dialog presentation, per-screen animations
|
|
18
|
+
- **Sidebar** — collapsible, sectioned, with badges, custom icons, and full keyboard navigation
|
|
19
|
+
- **Tabs** — top tab bar with Ctrl+Tab cycling
|
|
20
|
+
- **Split** — 2-3 resizable columns with draggable dividers, min/max constraints, and responsive layout
|
|
21
|
+
- **Desktop keyboard model** — Cmd+[ / Cmd+] on macOS, Alt+Left / Alt+Right on Windows, out of the box
|
|
22
|
+
- **Deep styling** — every part accepts `StyleSheet`-compatible styles or state-aware style functions, plus custom renderers for items and dividers
|
|
23
|
+
- **State persistence** — serialize/restore navigation state, `NavigationContainer` ref API
|
|
24
|
+
- **Optional native rendering** — an experimental `/native` entry point renders Stack/Sidebar/Split with SwiftUI (macOS) and WinUI (Windows) via Fabric, sharing the same Router/Store as the JS version
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install @natsuneko-laboratory/react-native-desktop-navigation
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To use a development build of the package instead, run `npm ci && npm pack` in this repository and install the generated `.tgz` into your desktop app. Metro uses the TypeScript source from the `react-native` export; regular JavaScript and type definitions are built into `dist/`.
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import {
|
|
38
|
+
NavigationContainer,
|
|
39
|
+
createStackNavigator,
|
|
40
|
+
type StackScreenProps,
|
|
41
|
+
} from '@natsuneko-laboratory/react-native-desktop-navigation';
|
|
42
|
+
import { Button, Text } from 'react-native';
|
|
43
|
+
|
|
44
|
+
type Params = {
|
|
45
|
+
Home: undefined;
|
|
46
|
+
Profile: { userId: string };
|
|
47
|
+
};
|
|
48
|
+
const Stack = createStackNavigator<Params>();
|
|
49
|
+
|
|
50
|
+
function Home({ navigation }: StackScreenProps<Params, 'Home'>) {
|
|
51
|
+
return (
|
|
52
|
+
<Button
|
|
53
|
+
title="Profile"
|
|
54
|
+
onPress={() => navigation.push('Profile', { userId: '123' })}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function Profile({ route }: StackScreenProps<Params, 'Profile'>) {
|
|
59
|
+
return <Text>{route.params.userId}</Text>;
|
|
60
|
+
}
|
|
61
|
+
export default function App() {
|
|
62
|
+
return (
|
|
63
|
+
<NavigationContainer>
|
|
64
|
+
<Stack.Navigator initialRouteName="Home">
|
|
65
|
+
<Stack.Screen name="Home" component={Home} />
|
|
66
|
+
<Stack.Screen
|
|
67
|
+
name="Profile"
|
|
68
|
+
component={Profile}
|
|
69
|
+
options={({ route }) => ({ title: route.params.userId })}
|
|
70
|
+
/>
|
|
71
|
+
</Stack.Navigator>
|
|
72
|
+
</NavigationContainer>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`navigate('Profile')` with a wrong route name or params is a type error. See the [Guide](docs/GUIDE.md) for Sidebar, Tabs, Split, hooks, keyboard customization, persistence, and styling.
|
|
78
|
+
|
|
79
|
+
## Native rendering (`/native`, Fabric, experimental)
|
|
80
|
+
|
|
81
|
+
`@natsuneko-laboratory/react-native-desktop-navigation/native` swaps in native Stack / Sidebar / Split implementations — SwiftUI's `NavigationStack` / `NavigationSplitView` on macOS, WinUI's `NavigationView` / `SplitView` on Windows — while reusing the same Router, Store, hooks, and `NavigationContainer` as the JS version. React screens keep rendering inside the original Fabric tree, so Context, local state, and nested Navigators all keep working.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import {
|
|
85
|
+
NavigationContainer,
|
|
86
|
+
createStackNavigator,
|
|
87
|
+
} from '@natsuneko-laboratory/react-native-desktop-navigation/native';
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It targets Fabric only, macOS 14+ / RN macOS 0.81.x, and Fabric on RN Windows 0.82+, and needs an additional native rebuild (Pod install / autolinking) after installing. It does not export Tabs — combine it with the JS version's `createTabNavigator` where needed. See **[NATIVE.md](NATIVE.md)** for setup instructions, styling/icon APIs, and the current verification scope, and [examples/desktop/Native.tsx](examples/desktop/Native.tsx) for a runnable example.
|
|
91
|
+
|
|
92
|
+
## Documentation
|
|
93
|
+
|
|
94
|
+
- **[Guide](docs/GUIDE.md)** — full JS API reference: Sidebar/Tabs/Split, hooks & events, focus, keyboard, persistence, styling and customization, verification/compatibility
|
|
95
|
+
- **[NATIVE.md](NATIVE.md)** — native (SwiftUI/WinUI) rendering: setup, styling, icons, verification scope
|
|
96
|
+
- **[examples/desktop](examples/desktop)** — a runnable example app plus a manual acceptance checklist
|
|
97
|
+
|
|
98
|
+
## Project layout
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
src/core Store / Container / hooks / events / focus / scene
|
|
102
|
+
src/routers Platform- and React-independent state transitions
|
|
103
|
+
src/stack Stack / Header
|
|
104
|
+
src/sidebar Sidebar / Section
|
|
105
|
+
src/tabs Desktop Tabs
|
|
106
|
+
src/split Columns / resizing / responsive layout
|
|
107
|
+
src/platform macOS / Windows adapters and the type boundary for native props
|
|
108
|
+
src/native Fabric-backed SwiftUI / WinUI renderer (`/native` entry point)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
Multi-window support, detachable tabs, native title bars, Group, deep linking, and custom transition drivers are out of scope for this version.
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
npm ci
|
|
119
|
+
npm run check # typecheck, tests, build, native codegen check
|
|
120
|
+
npm run format:check
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Issues and pull requests are welcome at [mika-f/react-native-desktop-navigation](https://github.com/mika-f/react-native-desktop-navigation).
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
[MIT](LICENSE) © Kanon Mochizuki
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { NavigationRefHandle, NavigationTheme } from './types';
|
|
4
|
+
import type { ParamListBase, RootNavigationState } from '../routers';
|
|
5
|
+
import { type KeyboardShortcut, type NavigationPlatformAdapter } from '../platform';
|
|
6
|
+
export interface KeyboardShortcutOptions {
|
|
7
|
+
back?: KeyboardShortcut[];
|
|
8
|
+
forward?: KeyboardShortcut[];
|
|
9
|
+
nextTab?: KeyboardShortcut[];
|
|
10
|
+
previousTab?: KeyboardShortcut[];
|
|
11
|
+
}
|
|
12
|
+
export interface NavigationContainerProps {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
initialState?: RootNavigationState;
|
|
15
|
+
onStateChange?: (state: RootNavigationState) => void;
|
|
16
|
+
keyboardShortcuts?: boolean | KeyboardShortcutOptions;
|
|
17
|
+
platformAdapter?: NavigationPlatformAdapter;
|
|
18
|
+
theme?: NavigationTheme;
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
}
|
|
21
|
+
export declare const NavigationContainer: <P extends ParamListBase = ParamListBase>(props: NavigationContainerProps & {
|
|
22
|
+
ref?: React.Ref<NavigationRefHandle<P>>;
|
|
23
|
+
}) => React.ReactElement;
|
|
@@ -0,0 +1,68 @@
|
|
|
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.NavigationContainer = void 0;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = __importDefault(require("react"));
|
|
9
|
+
const store_1 = require("./store");
|
|
10
|
+
const context_1 = require("./context");
|
|
11
|
+
const platform_1 = require("../platform");
|
|
12
|
+
function Container({ children, initialState, onStateChange, keyboardShortcuts = true, platformAdapter = platform_1.defaultPlatformAdapter, theme = context_1.DefaultTheme, style, }, ref) {
|
|
13
|
+
const [store] = react_1.default.useState(() => new store_1.NavigationStore(initialState));
|
|
14
|
+
const callback = react_1.default.useRef(onStateChange);
|
|
15
|
+
callback.current = onStateChange;
|
|
16
|
+
react_1.default.useEffect(() => store.subscribe(() => {
|
|
17
|
+
if (store.isReady())
|
|
18
|
+
callback.current?.(store.getState());
|
|
19
|
+
}), [store]);
|
|
20
|
+
react_1.default.useImperativeHandle(ref, () => ({
|
|
21
|
+
isReady: store.isReady,
|
|
22
|
+
navigate: (name, params) => {
|
|
23
|
+
if (store.isReady())
|
|
24
|
+
store.dispatch({ type: 'navigate', payload: { name, params } });
|
|
25
|
+
},
|
|
26
|
+
dispatch: (action) => {
|
|
27
|
+
if (store.isReady())
|
|
28
|
+
store.dispatch(action);
|
|
29
|
+
},
|
|
30
|
+
goBack: () => {
|
|
31
|
+
if (store.isReady())
|
|
32
|
+
store.dispatch({ type: 'back' });
|
|
33
|
+
},
|
|
34
|
+
goForward: () => {
|
|
35
|
+
if (store.isReady())
|
|
36
|
+
store.dispatch({ type: 'forward' });
|
|
37
|
+
},
|
|
38
|
+
canGoBack: () => store.isReady() && store.canDispatch('back'),
|
|
39
|
+
canGoForward: () => store.isReady() && store.canDispatch('forward'),
|
|
40
|
+
getRootState: store.getState,
|
|
41
|
+
}), [store]);
|
|
42
|
+
const shortcuts = {
|
|
43
|
+
...platformAdapter.keyboardShortcuts,
|
|
44
|
+
...(typeof keyboardShortcuts === 'object' ? keyboardShortcuts : {}),
|
|
45
|
+
};
|
|
46
|
+
return ((0, jsx_runtime_1.jsx)(context_1.StoreContext.Provider, { value: store, children: (0, jsx_runtime_1.jsx)(context_1.ScopeContext.Provider, { value: {}, children: (0, jsx_runtime_1.jsx)(context_1.PlatformContext.Provider, { value: platformAdapter, children: (0, jsx_runtime_1.jsx)(context_1.ThemeContext.Provider, { value: theme, children: (0, jsx_runtime_1.jsx)(platform_1.DesktopView, { style: [
|
|
47
|
+
{ flex: 1, backgroundColor: theme.colors.background },
|
|
48
|
+
style,
|
|
49
|
+
], focusable: true, keyDownEvents: keyboardShortcuts ? Object.values(shortcuts).flat() : [], onKeyDown: (event) => {
|
|
50
|
+
if (!keyboardShortcuts ||
|
|
51
|
+
(0, platform_1.eventHandled)(event) ||
|
|
52
|
+
platformAdapter.isTextInputEvent(event))
|
|
53
|
+
return;
|
|
54
|
+
for (const type of [
|
|
55
|
+
'back',
|
|
56
|
+
'forward',
|
|
57
|
+
'nextTab',
|
|
58
|
+
'previousTab',
|
|
59
|
+
]) {
|
|
60
|
+
if (shortcuts[type].some((key) => (0, platform_1.matchesShortcut)(event, key)) &&
|
|
61
|
+
store.dispatch({ type })) {
|
|
62
|
+
(0, platform_1.consumeKey)(event);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, children: children }) }) }) }) }));
|
|
67
|
+
}
|
|
68
|
+
exports.NavigationContainer = react_1.default.forwardRef(Container);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NavigationRoute } from '../routers';
|
|
3
|
+
import type { FocusTarget } from './focus';
|
|
4
|
+
import type { SidebarScreenOptions } from './types';
|
|
5
|
+
export interface NavigationItemProps {
|
|
6
|
+
route: NavigationRoute;
|
|
7
|
+
options: SidebarScreenOptions;
|
|
8
|
+
selected: boolean;
|
|
9
|
+
focused: boolean;
|
|
10
|
+
collapsed: boolean;
|
|
11
|
+
sidebar: boolean;
|
|
12
|
+
onFocus: () => void;
|
|
13
|
+
onBlur: () => void;
|
|
14
|
+
onPress: () => void;
|
|
15
|
+
itemRef: (target: FocusTarget | null) => void;
|
|
16
|
+
}
|
|
17
|
+
/** The interactive shell remains owned by navigation when its content is customized. */
|
|
18
|
+
export declare function NavigationItem({ route, options, selected, focused, collapsed, sidebar, onFocus, onBlur, onPress, itemRef, }: NavigationItemProps): React.JSX.Element;
|
|
19
|
+
export declare const navigationItemStyle: {
|
|
20
|
+
flexDirection: "row";
|
|
21
|
+
alignItems: "center";
|
|
22
|
+
gap: number;
|
|
23
|
+
padding: number;
|
|
24
|
+
margin: number;
|
|
25
|
+
borderRadius: number;
|
|
26
|
+
borderWidth: number;
|
|
27
|
+
};
|