@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,471 @@
1
+ import {
2
+ reduceNavigation,
3
+ createRouteKey,
4
+ type NavigationAction,
5
+ type NavigationNode,
6
+ type NavigationState,
7
+ type RootNavigationState,
8
+ type RouterOptions,
9
+ } from '../routers';
10
+ export type NavigationEventType = 'focus' | 'blur' | 'beforeRemove' | 'state';
11
+ export interface NavigationEvent {
12
+ type: NavigationEventType;
13
+ target: string;
14
+ action?: NavigationAction;
15
+ defaultPrevented: boolean;
16
+ preventDefault(): void;
17
+ }
18
+ type Listener = (event: NavigationEvent) => void;
19
+ function assertSerializable(value: unknown, seen = new Set<object>()): void {
20
+ if (
21
+ value === undefined ||
22
+ value === null ||
23
+ typeof value === 'string' ||
24
+ typeof value === 'boolean'
25
+ )
26
+ return;
27
+ if (typeof value === 'number' && Number.isFinite(value)) return;
28
+ if (typeof value !== 'object')
29
+ throw new Error('Navigation params must be JSON serializable.');
30
+ if (seen.has(value))
31
+ throw new Error('Navigation state contains a circular reference.');
32
+ if (
33
+ !Array.isArray(value) &&
34
+ Object.getPrototypeOf(value) !== Object.prototype
35
+ )
36
+ throw new Error(
37
+ 'Navigation params must contain only plain objects and arrays.',
38
+ );
39
+ seen.add(value);
40
+ for (const child of Object.values(value)) assertSerializable(child, seen);
41
+ seen.delete(value);
42
+ }
43
+ export function serializeNavigationState(state: RootNavigationState): string {
44
+ assertSerializable(state);
45
+ return JSON.stringify(state);
46
+ }
47
+ export function restoreNavigationState(
48
+ input: string | RootNavigationState,
49
+ ): RootNavigationState {
50
+ const state: RootNavigationState =
51
+ typeof input === 'string'
52
+ ? JSON.parse(input)
53
+ : JSON.parse(serializeNavigationState(input));
54
+ if (
55
+ !state ||
56
+ state.version !== 1 ||
57
+ typeof state.rootNodeId !== 'string' ||
58
+ !state.nodes ||
59
+ !state.nodes[state.rootNodeId]
60
+ )
61
+ throw new Error('Invalid navigation snapshot (expected version 1).');
62
+ for (const [id, node] of Object.entries(state.nodes)) {
63
+ const s = node.state;
64
+ if (
65
+ id !== node.id ||
66
+ s?.key !== id ||
67
+ node.type !== s.type ||
68
+ !['stack', 'sidebar', 'tabs', 'split'].includes(s.type) ||
69
+ !Array.isArray(s.routes) ||
70
+ !s.routes.length
71
+ )
72
+ throw new Error(`Invalid navigation node: ${id}`);
73
+ const keys = s.routes.map((r) => r.key);
74
+ if (
75
+ new Set(keys).size !== keys.length ||
76
+ !keys.includes(s.activeRouteKey) ||
77
+ s.routes.some(
78
+ (r) => typeof r.key !== 'string' || typeof r.name !== 'string',
79
+ )
80
+ )
81
+ throw new Error(`Invalid routes: ${id}`);
82
+ if (
83
+ s.type === 'stack' &&
84
+ (!Number.isInteger(s.nextRouteId) || s.nextRouteId < 1)
85
+ )
86
+ throw new Error('Invalid stack route counter.');
87
+ if (
88
+ (s.type === 'sidebar' || s.type === 'tabs') &&
89
+ typeof s.collapsed !== 'boolean'
90
+ )
91
+ throw new Error('Invalid selection state.');
92
+ if (
93
+ s.type === 'stack' &&
94
+ s.activeRouteKey !== s.routes[s.routes.length - 1].key
95
+ )
96
+ throw new Error('Stack active route must be the last route.');
97
+ if (s.type === 'stack' && s.history) {
98
+ for (const routes of [...s.history.past, ...s.history.future]) {
99
+ if (
100
+ !Array.isArray(routes) ||
101
+ !routes.length ||
102
+ routes.some(
103
+ (r) => typeof r.key !== 'string' || typeof r.name !== 'string',
104
+ )
105
+ )
106
+ throw new Error('Invalid stack history.');
107
+ }
108
+ }
109
+ if (
110
+ s.type === 'split' &&
111
+ (!s.widths ||
112
+ !Array.isArray(s.visibleColumnIds) ||
113
+ !s.visibleColumnIds.length ||
114
+ new Set(s.visibleColumnIds).size !== s.visibleColumnIds.length ||
115
+ s.visibleColumnIds.some(
116
+ (name) => !s.routes.some((r) => r.name === name),
117
+ ) ||
118
+ s.routes.some(
119
+ (r) => !Number.isFinite(s.widths[r.name]) || s.widths[r.name] < 0,
120
+ ))
121
+ )
122
+ throw new Error('Invalid split state.');
123
+ if (s.type === 'split' && s.collapsedColumns !== undefined) {
124
+ if (
125
+ !s.collapsedColumns ||
126
+ typeof s.collapsedColumns !== 'object' ||
127
+ Array.isArray(s.collapsedColumns) ||
128
+ Object.entries(s.collapsedColumns).some(
129
+ ([name, column]) =>
130
+ !s.routes.some((r) => r.name === name) ||
131
+ !column ||
132
+ !Number.isFinite(column.width) ||
133
+ column.width < 0 ||
134
+ !Number.isFinite(column.expandedWidth) ||
135
+ column.expandedWidth < 0 ||
136
+ s.widths[name] !== column.width,
137
+ )
138
+ )
139
+ throw new Error('Invalid collapsed split state.');
140
+ }
141
+ const visited = new Set([id]);
142
+ let current = node;
143
+ while (current.parentId) {
144
+ const parent = state.nodes[current.parentId];
145
+ if (
146
+ !parent ||
147
+ visited.has(parent.id) ||
148
+ !retainedRoutes(parent.state).some(
149
+ (r) => r.key === current.parentRouteKey,
150
+ )
151
+ )
152
+ throw new Error('Invalid navigation parent relationship.');
153
+ visited.add(parent.id);
154
+ current = parent;
155
+ }
156
+ if (current.id !== state.rootNodeId)
157
+ throw new Error('Navigation snapshot has multiple roots.');
158
+ }
159
+ if (state.activeNodeId && !state.nodes[state.activeNodeId])
160
+ throw new Error('Invalid active navigator.');
161
+ return state;
162
+ }
163
+ function retainedRoutes(state: NavigationState) {
164
+ return state.type === 'stack' && state.history
165
+ ? [
166
+ ...state.routes,
167
+ ...state.history.past.flat(),
168
+ ...state.history.future.flat(),
169
+ ]
170
+ : state.routes;
171
+ }
172
+ export class NavigationStore {
173
+ private snapshot: RootNavigationState;
174
+ private subscribers = new Set<() => void>();
175
+ private events = new Map<string, Map<NavigationEventType, Set<Listener>>>();
176
+ private configs = new Map<string, RouterOptions>();
177
+ private mounted = new Set<string>();
178
+ constructor(initialState?: RootNavigationState) {
179
+ this.snapshot = initialState
180
+ ? restoreNavigationState(initialState)
181
+ : { version: 1, rootNodeId: 'root', nodes: {} };
182
+ }
183
+ getState = () => this.snapshot;
184
+ getNode = (id: string): NavigationNode | undefined => this.snapshot.nodes[id];
185
+ subscribe = (listener: () => void) => {
186
+ this.subscribers.add(listener);
187
+ return () => {
188
+ this.subscribers.delete(listener);
189
+ };
190
+ };
191
+ isReady = () => this.mounted.has(this.snapshot.rootNodeId);
192
+ register(node: NavigationNode, options: RouterOptions) {
193
+ assertSerializable(node.state);
194
+ if (this.mounted.has(node.id))
195
+ throw new Error(
196
+ `Duplicate navigator id: ${node.id}. Supply a unique id for sibling navigators.`,
197
+ );
198
+ const saved = this.snapshot.nodes[node.id];
199
+ if (
200
+ saved &&
201
+ (saved.type !== node.type ||
202
+ retainedRoutes(saved.state).some(
203
+ (r) => !options.routes.some((c) => c.name === r.name),
204
+ ))
205
+ )
206
+ throw new Error(`Saved state does not match navigator ${node.id}.`);
207
+ const previousFocus = this.focusedRouteKeys();
208
+ this.configs.set(node.id, options);
209
+ this.mounted.add(node.id);
210
+ this.commit(
211
+ {
212
+ ...this.snapshot,
213
+ rootNodeId: node.parentId ? this.snapshot.rootNodeId : node.id,
214
+ nodes: { ...this.snapshot.nodes, [node.id]: saved ?? node },
215
+ },
216
+ undefined,
217
+ previousFocus,
218
+ );
219
+ return () => {
220
+ const oldFocus = this.focusedRouteKeys();
221
+ this.mounted.delete(node.id);
222
+ this.configs.delete(node.id);
223
+ this.publishFocus(oldFocus);
224
+ for (const listener of this.subscribers) listener();
225
+ };
226
+ }
227
+ updateOptions(id: string, options: RouterOptions) {
228
+ this.configs.set(id, options);
229
+ }
230
+ addListener(routeKey: string, type: NavigationEventType, listener: Listener) {
231
+ let types = this.events.get(routeKey);
232
+ if (!types) this.events.set(routeKey, (types = new Map()));
233
+ let listeners = types.get(type);
234
+ if (!listeners) types.set(type, (listeners = new Set()));
235
+ listeners.add(listener);
236
+ return () => {
237
+ listeners!.delete(listener);
238
+ if (!listeners!.size) types!.delete(type);
239
+ if (!types!.size) this.events.delete(routeKey);
240
+ };
241
+ }
242
+ private emit(
243
+ target: string,
244
+ type: NavigationEventType,
245
+ action?: NavigationAction,
246
+ ) {
247
+ const event: NavigationEvent = {
248
+ type,
249
+ target,
250
+ action,
251
+ defaultPrevented: false,
252
+ preventDefault() {
253
+ if (type === 'beforeRemove') this.defaultPrevented = true;
254
+ },
255
+ };
256
+ for (const listener of this.events.get(target)?.get(type) ?? [])
257
+ listener(event);
258
+ return event.defaultPrevented;
259
+ }
260
+ isNodeFocused(id: string): boolean {
261
+ const node = this.snapshot.nodes[id];
262
+ if (!node || !this.mounted.has(id)) return false;
263
+ if (!node.parentId) return true;
264
+ const parent = this.snapshot.nodes[node.parentId];
265
+ return (
266
+ !!parent &&
267
+ parent.state.activeRouteKey === node.parentRouteKey &&
268
+ this.isNodeFocused(parent.id)
269
+ );
270
+ }
271
+ isRouteFocused(id: string, routeKey: string) {
272
+ return (
273
+ this.getNode(id)?.state.activeRouteKey === routeKey &&
274
+ this.isNodeFocused(id)
275
+ );
276
+ }
277
+ private focusedRouteKeys() {
278
+ const depth = (node: NavigationNode): number =>
279
+ node.parentId && this.getNode(node.parentId)
280
+ ? 1 + depth(this.getNode(node.parentId)!)
281
+ : 0;
282
+ return Object.values(this.snapshot.nodes)
283
+ .filter((n) => this.isNodeFocused(n.id))
284
+ .sort((a, b) => depth(a) - depth(b))
285
+ .map((n) => n.state.activeRouteKey);
286
+ }
287
+ private publishFocus(previous: string[]) {
288
+ const next = this.focusedRouteKeys();
289
+ for (const key of [...previous].reverse())
290
+ if (!next.includes(key)) this.emit(key, 'blur');
291
+ for (const key of next)
292
+ if (!previous.includes(key)) this.emit(key, 'focus');
293
+ }
294
+ private commit(
295
+ next: RootNavigationState,
296
+ action?: NavigationAction,
297
+ previous = this.focusedRouteKeys(),
298
+ ) {
299
+ this.snapshot = next;
300
+ this.publishFocus(previous);
301
+ for (const listener of this.subscribers) listener();
302
+ for (const node of Object.values(next.nodes))
303
+ for (const route of node.state.routes)
304
+ this.emit(route.key, 'state', action);
305
+ }
306
+ focusRoute(id: string, routeKey: string) {
307
+ const node = this.getNode(id);
308
+ if (
309
+ node?.state.type === 'split' &&
310
+ node.state.activeRouteKey !== routeKey
311
+ ) {
312
+ const route = node.state.routes.find((r) => r.key === routeKey);
313
+ if (!route || !node.state.visibleColumnIds.includes(route.name)) return;
314
+ this.commit({
315
+ ...this.snapshot,
316
+ nodes: {
317
+ ...this.snapshot.nodes,
318
+ [id]: { ...node, state: { ...node.state, activeRouteKey: routeKey } },
319
+ },
320
+ });
321
+ }
322
+ // Native focus bubbles: an ancestor Scene must not replace the focused leaf.
323
+ let active = this.getNode(this.getActiveNodeId());
324
+ while (active) {
325
+ if (active.id === id && this.isNodeFocused(this.getActiveNodeId()))
326
+ return;
327
+ active = active.parentId ? this.getNode(active.parentId) : undefined;
328
+ }
329
+ this.setFocusedNode(id);
330
+ }
331
+ setFocusedNode(id: string) {
332
+ if (!this.mounted.has(id)) return;
333
+ let node = this.getNode(id);
334
+ const nodes = { ...this.snapshot.nodes };
335
+ while (node?.parentId) {
336
+ const parent = nodes[node.parentId];
337
+ if (!parent) return;
338
+ if (parent.state.type === 'split') {
339
+ const route = parent.state.routes.find(
340
+ (r) => r.key === node!.parentRouteKey,
341
+ );
342
+ if (!route || !parent.state.visibleColumnIds.includes(route.name))
343
+ return;
344
+ nodes[parent.id] = {
345
+ ...parent,
346
+ state: { ...parent.state, activeRouteKey: route.key },
347
+ };
348
+ } else if (parent.state.activeRouteKey !== node.parentRouteKey) return;
349
+ node = parent;
350
+ }
351
+ if (
352
+ this.snapshot.activeNodeId === id &&
353
+ Object.keys(nodes).every((key) => nodes[key] === this.snapshot.nodes[key])
354
+ )
355
+ return;
356
+ this.commit({ ...this.snapshot, nodes, activeNodeId: id });
357
+ }
358
+ getActiveNodeId() {
359
+ const preferred = this.snapshot.activeNodeId;
360
+ let id =
361
+ preferred && this.isNodeFocused(preferred)
362
+ ? preferred
363
+ : this.snapshot.rootNodeId;
364
+ while (true) {
365
+ const child = Object.values(this.snapshot.nodes).find(
366
+ (n) => n.parentId === id && this.isNodeFocused(n.id),
367
+ );
368
+ if (!child) return id;
369
+ id = child.id;
370
+ }
371
+ }
372
+ canDispatch(
373
+ type: 'back' | 'forward',
374
+ target = this.getActiveNodeId(),
375
+ ): boolean {
376
+ let node = this.getNode(target);
377
+ while (node) {
378
+ const s = node.state;
379
+ if (
380
+ s.type === 'stack' &&
381
+ (type === 'back'
382
+ ? s.history
383
+ ? s.history.past.length > 0
384
+ : s.routes.length > 1
385
+ : !!s.history?.future.length)
386
+ )
387
+ return true;
388
+ node = node.parentId ? this.getNode(node.parentId) : undefined;
389
+ }
390
+ return false;
391
+ }
392
+ dispatch = (original: NavigationAction): boolean => {
393
+ let action = original;
394
+ if ('payload' in action) assertSerializable(action.payload);
395
+ if (
396
+ action.type === 'navigate' ||
397
+ action.type === 'push' ||
398
+ action.type === 'replace'
399
+ )
400
+ action = {
401
+ ...action,
402
+ payload: {
403
+ ...action.payload,
404
+ key: action.payload.key ?? createRouteKey(action.payload.name),
405
+ },
406
+ };
407
+ let node = this.getNode(action.target ?? this.getActiveNodeId());
408
+ while (node) {
409
+ const options = this.configs.get(node.id);
410
+ const next = options
411
+ ? reduceNavigation(node.state, action, options)
412
+ : null;
413
+ if (next) {
414
+ if (next === node.state) return true;
415
+ const removed = node.state.routes
416
+ .filter((r) => !next.routes.some((n) => n.key === r.key))
417
+ .map((r) => r.key);
418
+ const removedNodes = new Set<string>();
419
+ let changed = true;
420
+ while (changed) {
421
+ changed = false;
422
+ for (const child of Object.values(this.snapshot.nodes))
423
+ if (
424
+ !removedNodes.has(child.id) &&
425
+ ((child.parentId === node.id &&
426
+ removed.includes(child.parentRouteKey!)) ||
427
+ removedNodes.has(child.parentId!))
428
+ ) {
429
+ removedNodes.add(child.id);
430
+ removed.push(...child.state.routes.map((r) => r.key));
431
+ changed = true;
432
+ }
433
+ }
434
+ let blocked = false;
435
+ for (const key of [...removed].reverse())
436
+ blocked = this.emit(key, 'beforeRemove', action) || blocked;
437
+ if (blocked) return true;
438
+ const nodes = {
439
+ ...this.snapshot.nodes,
440
+ [node.id]: { ...node, state: next },
441
+ };
442
+ // Keep history-owned subtrees so Back/Forward can restore nested navigator state.
443
+ let pruned = true;
444
+ while (pruned) {
445
+ pruned = false;
446
+ for (const child of Object.values(nodes))
447
+ if (
448
+ child.parentId &&
449
+ (!nodes[child.parentId] ||
450
+ !retainedRoutes(nodes[child.parentId].state).some(
451
+ (r) => r.key === child.parentRouteKey,
452
+ ))
453
+ ) {
454
+ delete nodes[child.id];
455
+ pruned = true;
456
+ }
457
+ }
458
+ this.commit({ ...this.snapshot, nodes, activeNodeId: node.id }, action);
459
+ return true;
460
+ }
461
+ node = node.parentId ? this.getNode(node.parentId) : undefined;
462
+ }
463
+ if (
464
+ ['navigate', 'push', 'replace', 'popTo', 'select'].includes(action.type)
465
+ )
466
+ console.warn(
467
+ `Unhandled navigation action: ${action.type}${'payload' in action && action.payload && 'name' in action.payload ? ` (${action.payload.name})` : ''}`,
468
+ );
469
+ return false;
470
+ };
471
+ }
@@ -0,0 +1,176 @@
1
+ import type React from 'react';
2
+ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
3
+ import type {
4
+ ParamListBase,
5
+ RouteName,
6
+ RouteArgs,
7
+ NavigationRoute,
8
+ NavigationState,
9
+ NavigationAction,
10
+ RootNavigationState,
11
+ } from '../routers';
12
+ import type { NavigationEvent, NavigationEventType } from './store';
13
+ export interface Navigation<P extends ParamListBase = ParamListBase> {
14
+ navigate(...args: RouteArgs<P>): void;
15
+ goBack(): void;
16
+ goForward(): void;
17
+ canGoBack(): boolean;
18
+ canGoForward(): boolean;
19
+ dispatch(action: NavigationAction): void;
20
+ getState(): NavigationState;
21
+ getParent(): Navigation | undefined;
22
+ isFocused(): boolean;
23
+ addListener(
24
+ type: NavigationEventType,
25
+ listener: (event: NavigationEvent) => void,
26
+ ): () => void;
27
+ }
28
+ export interface StackNavigation<P extends ParamListBase = ParamListBase>
29
+ extends Navigation<P> {
30
+ push(...args: RouteArgs<P>): void;
31
+ replace(...args: RouteArgs<P>): void;
32
+ pop(count?: number): void;
33
+ popTo(...args: RouteArgs<P>): void;
34
+ popToRoot(): void;
35
+ }
36
+ export interface SidebarNavigation<P extends ParamListBase = ParamListBase>
37
+ extends Navigation<P> {
38
+ select(name: RouteName<P>): void;
39
+ collapseSidebar(): void;
40
+ expandSidebar(): void;
41
+ toggleSidebar(): void;
42
+ }
43
+ export interface TabNavigation<P extends ParamListBase = ParamListBase>
44
+ extends Navigation<P> {
45
+ select(name: RouteName<P>): void;
46
+ nextTab(): void;
47
+ previousTab(): void;
48
+ }
49
+ export type StackScreenProps<
50
+ P extends ParamListBase,
51
+ N extends RouteName<P>,
52
+ > = { navigation: StackNavigation<P>; route: NavigationRoute<N, P[N]> };
53
+ export type SidebarScreenProps<
54
+ P extends ParamListBase,
55
+ N extends RouteName<P>,
56
+ > = { navigation: SidebarNavigation<P>; route: NavigationRoute<N, P[N]> };
57
+ export type TabScreenProps<P extends ParamListBase, N extends RouteName<P>> = {
58
+ navigation: TabNavigation<P>;
59
+ route: NavigationRoute<N, P[N]>;
60
+ };
61
+ export type FocusBehavior = 'none' | 'first' | 'restore';
62
+ export interface CommonScreenOptions {
63
+ title?: string;
64
+ focusBehavior?: FocusBehavior;
65
+ inactiveBehavior?: 'keep' | 'unmount';
66
+ contentStyle?: StyleProp<ViewStyle>;
67
+ sceneStyle?: StyleProp<ViewStyle>;
68
+ }
69
+ export interface HeaderActionProps {
70
+ navigation: StackNavigation;
71
+ route: NavigationRoute;
72
+ canGoBack: boolean;
73
+ }
74
+ export interface HeaderProps extends HeaderActionProps {
75
+ options: StackScreenOptions;
76
+ }
77
+ export type StackAnimation =
78
+ | 'default'
79
+ | 'none'
80
+ | 'fade'
81
+ | 'slide-horizontal'
82
+ | 'slide-vertical';
83
+ export interface StackScreenOptions extends CommonScreenOptions {
84
+ headerShown?: boolean;
85
+ headerStyle?: StyleProp<ViewStyle>;
86
+ headerTitleStyle?: StyleProp<TextStyle>;
87
+ headerLeftContainerStyle?: StyleProp<ViewStyle>;
88
+ headerRightContainerStyle?: StyleProp<ViewStyle>;
89
+ headerBackButtonStyle?: StyleProp<ViewStyle>;
90
+ headerBackTitleStyle?: StyleProp<TextStyle>;
91
+ headerBackTitle?: string;
92
+ headerTintColor?: string;
93
+ overlayStyle?: StyleProp<ViewStyle>;
94
+ dialogStyle?: StyleProp<ViewStyle>;
95
+ header?: React.ReactNode | ((props: HeaderProps) => React.ReactNode);
96
+ headerLeft?:
97
+ | React.ReactNode
98
+ | ((props: HeaderActionProps) => React.ReactNode);
99
+ headerRight?:
100
+ | React.ReactNode
101
+ | ((props: HeaderActionProps) => React.ReactNode);
102
+ animation?: StackAnimation;
103
+ presentation?: 'card' | 'modal' | 'dialog';
104
+ }
105
+ export interface NavigationItemState {
106
+ selected: boolean;
107
+ /** Keyboard/native focus, independent of selection. */
108
+ focused: boolean;
109
+ pressed: boolean;
110
+ hovered: boolean;
111
+ disabled: boolean;
112
+ collapsed: boolean;
113
+ }
114
+ export type NavigationItemStyle<T extends ViewStyle | TextStyle> =
115
+ | StyleProp<T>
116
+ | ((state: NavigationItemState) => StyleProp<T>);
117
+ export interface NavigationItemContentProps extends NavigationItemState {
118
+ route: NavigationRoute;
119
+ label: string;
120
+ /** Default icon, label, and badge, with their configured styles. */
121
+ children: React.ReactNode;
122
+ }
123
+ export interface SidebarFooterProps {
124
+ collapsed: boolean;
125
+ collapseSidebar(): void;
126
+ expandSidebar(): void;
127
+ toggleSidebar(): void;
128
+ /** The complete default collapse button, or null when it is hidden. */
129
+ children: React.ReactNode;
130
+ }
131
+ export interface SidebarSectionOptions {
132
+ title?: string;
133
+ style?: StyleProp<ViewStyle>;
134
+ titleStyle?: StyleProp<TextStyle>;
135
+ renderTitle?: (props: { title: string }) => React.ReactNode;
136
+ }
137
+ export interface SidebarScreenOptions extends CommonScreenOptions {
138
+ itemStyle?: NavigationItemStyle<ViewStyle>;
139
+ labelStyle?: NavigationItemStyle<TextStyle>;
140
+ iconContainerStyle?: NavigationItemStyle<ViewStyle>;
141
+ badgeStyle?: NavigationItemStyle<TextStyle>;
142
+ renderItemContent?: (props: NavigationItemContentProps) => React.ReactNode;
143
+ label?: string;
144
+ icon?:
145
+ | React.ReactNode
146
+ | ((props: { focused: boolean; disabled: boolean }) => React.ReactNode);
147
+ badge?: React.ReactNode;
148
+ hidden?: boolean;
149
+ disabled?: boolean;
150
+ }
151
+ export interface TabScreenOptions extends SidebarScreenOptions {}
152
+ export interface NavigationTheme {
153
+ dark: boolean;
154
+ colors: {
155
+ background: string;
156
+ surface: string;
157
+ text: string;
158
+ mutedText: string;
159
+ border: string;
160
+ accent: string;
161
+ selectedBackground: string;
162
+ };
163
+ }
164
+ export interface NavigationRef<P extends ParamListBase> {
165
+ current: NavigationRefHandle<P> | null;
166
+ isReady(): boolean;
167
+ navigate(...args: RouteArgs<P>): void;
168
+ goBack(): void;
169
+ goForward(): void;
170
+ canGoBack(): boolean;
171
+ canGoForward(): boolean;
172
+ dispatch(action: NavigationAction): void;
173
+ getRootState(): RootNavigationState | undefined;
174
+ }
175
+ export interface NavigationRefHandle<P extends ParamListBase>
176
+ extends Omit<NavigationRef<P>, 'current'> {}
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ export {
2
+ NavigationContainer,
3
+ type NavigationContainerProps,
4
+ type KeyboardShortcutOptions,
5
+ } from './core/NavigationContainer';
6
+ export { createNavigationRef } from './core/navigation';
7
+ export {
8
+ useNavigation,
9
+ useRoute,
10
+ useNavigationState,
11
+ useFocusEffect,
12
+ useIsFocused,
13
+ } from './core/hooks';
14
+ export { NavigationFocusable, type FocusTarget } from './core/focus';
15
+ export { DefaultTheme, useNavigationTheme } from './core/context';
16
+ export {
17
+ NavigationStore,
18
+ serializeNavigationState,
19
+ restoreNavigationState,
20
+ type NavigationEvent,
21
+ type NavigationEventType,
22
+ } from './core/store';
23
+ export * from './core/types';
24
+ export * from './routers';
25
+ export * from './stack';
26
+ export * from './sidebar';
27
+ export * from './tabs';
28
+ export * from './split';
29
+ export { macosAdapter } from './platform/macos';
30
+ export { windowsAdapter } from './platform/windows';
31
+ export {
32
+ type NavigationPlatformAdapter,
33
+ type KeyboardShortcut,
34
+ type DesktopKeyEvent,
35
+ type TransitionPreset,
36
+ } from './platform';