@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,393 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.routers = exports.SplitRouter = exports.TabRouter = exports.SidebarRouter = exports.StackRouter = exports.createRouteKey = void 0;
|
|
18
|
+
exports.validateRoutes = validateRoutes;
|
|
19
|
+
exports.clampColumnWidth = clampColumnWidth;
|
|
20
|
+
exports.fitSplitWidths = fitSplitWidths;
|
|
21
|
+
exports.splitColumnConfig = splitColumnConfig;
|
|
22
|
+
exports.reduceNavigation = reduceNavigation;
|
|
23
|
+
__exportStar(require("./types"), exports);
|
|
24
|
+
let sequence = 0;
|
|
25
|
+
const createRouteKey = (name) => `${name}-${Date.now().toString(36)}-${(++sequence).toString(36)}`;
|
|
26
|
+
exports.createRouteKey = createRouteKey;
|
|
27
|
+
function validateRoutes(options) {
|
|
28
|
+
const names = options.routes.map((r) => r.name);
|
|
29
|
+
if (!names.length)
|
|
30
|
+
throw new Error('A navigator requires at least one screen.');
|
|
31
|
+
if (new Set(names).size !== names.length)
|
|
32
|
+
throw new Error('Duplicate Screen name or Split.Column id.');
|
|
33
|
+
if (options.initialRouteName && !names.includes(options.initialRouteName))
|
|
34
|
+
throw new Error(`Unknown initialRouteName: ${options.initialRouteName}`);
|
|
35
|
+
}
|
|
36
|
+
const routeFor = (options, name, key = `${options.key}/${name}/initial`, params) => ({
|
|
37
|
+
key,
|
|
38
|
+
name,
|
|
39
|
+
params: params === undefined
|
|
40
|
+
? options.routes.find((r) => r.name === name)?.initialParams
|
|
41
|
+
: params,
|
|
42
|
+
});
|
|
43
|
+
const top = (routes) => routes[routes.length - 1].key;
|
|
44
|
+
const eligible = (options, name) => options.routes.some((r) => r.name === name && !r.hidden && !r.disabled);
|
|
45
|
+
function withRoutes(state, routes, record = true) {
|
|
46
|
+
if (routes === state.routes)
|
|
47
|
+
return state;
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
routes,
|
|
51
|
+
activeRouteKey: top(routes),
|
|
52
|
+
history: state.history &&
|
|
53
|
+
(record
|
|
54
|
+
? { past: [...state.history.past, state.routes], future: [] }
|
|
55
|
+
: state.history),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
exports.StackRouter = {
|
|
59
|
+
getInitialState(options) {
|
|
60
|
+
validateRoutes(options);
|
|
61
|
+
const route = routeFor(options, options.initialRouteName ?? options.routes[0].name);
|
|
62
|
+
return {
|
|
63
|
+
type: 'stack',
|
|
64
|
+
key: options.key,
|
|
65
|
+
routes: [route],
|
|
66
|
+
activeRouteKey: route.key,
|
|
67
|
+
nextRouteId: 1,
|
|
68
|
+
...(options.historyBehavior === 'desktop'
|
|
69
|
+
? { history: { past: [], future: [] } }
|
|
70
|
+
: {}),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
reduce(state, action, options) {
|
|
74
|
+
const routes = state.routes;
|
|
75
|
+
switch (action.type) {
|
|
76
|
+
case 'navigate':
|
|
77
|
+
case 'push':
|
|
78
|
+
case 'replace':
|
|
79
|
+
case 'popTo': {
|
|
80
|
+
const { name, params } = action.payload;
|
|
81
|
+
const key = action.payload.key ?? `${state.key}/${name}/${state.nextRouteId}`;
|
|
82
|
+
const fresh = { ...state, nextRouteId: state.nextRouteId + 1 };
|
|
83
|
+
if (!options.routes.some((r) => r.name === name))
|
|
84
|
+
return null;
|
|
85
|
+
if (action.type === 'push')
|
|
86
|
+
return withRoutes(fresh, [
|
|
87
|
+
...routes,
|
|
88
|
+
routeFor(options, name, key, params),
|
|
89
|
+
]);
|
|
90
|
+
if (action.type === 'replace')
|
|
91
|
+
return withRoutes(fresh, [
|
|
92
|
+
...routes.slice(0, -1),
|
|
93
|
+
routeFor(options, name, key, params),
|
|
94
|
+
]);
|
|
95
|
+
let index = routes.length - 1;
|
|
96
|
+
while (index >= 0 && routes[index].name !== name)
|
|
97
|
+
index--;
|
|
98
|
+
if (index < 0) {
|
|
99
|
+
if (action.type === 'popTo') {
|
|
100
|
+
console.warn(`popTo: route ${name} is not in the stack.`);
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
return withRoutes(fresh, [
|
|
104
|
+
...routes,
|
|
105
|
+
routeFor(options, name, key, params),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
if (index === routes.length - 1 && params === undefined)
|
|
109
|
+
return state;
|
|
110
|
+
return withRoutes(state, [
|
|
111
|
+
...routes.slice(0, index),
|
|
112
|
+
{ ...routes[index], ...(params === undefined ? {} : { params }) },
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
case 'back':
|
|
116
|
+
if (state.history?.past.length) {
|
|
117
|
+
const past = state.history.past;
|
|
118
|
+
const previous = past[past.length - 1];
|
|
119
|
+
return {
|
|
120
|
+
...state,
|
|
121
|
+
routes: previous,
|
|
122
|
+
activeRouteKey: top(previous),
|
|
123
|
+
history: {
|
|
124
|
+
past: past.slice(0, -1),
|
|
125
|
+
future: [routes, ...state.history.future],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (state.history)
|
|
130
|
+
return null;
|
|
131
|
+
return routes.length > 1
|
|
132
|
+
? withRoutes(state, routes.slice(0, -1))
|
|
133
|
+
: null;
|
|
134
|
+
case 'forward': {
|
|
135
|
+
if (!state.history?.future.length)
|
|
136
|
+
return null;
|
|
137
|
+
const [next, ...future] = state.history.future;
|
|
138
|
+
return {
|
|
139
|
+
...state,
|
|
140
|
+
routes: next,
|
|
141
|
+
activeRouteKey: top(next),
|
|
142
|
+
history: { past: [...state.history.past, routes], future },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
case 'pop': {
|
|
146
|
+
const count = action.payload?.count ?? 1;
|
|
147
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
148
|
+
console.warn('pop count must be a positive integer.');
|
|
149
|
+
return state;
|
|
150
|
+
}
|
|
151
|
+
if (count >= routes.length)
|
|
152
|
+
console.warn('pop exceeds the stack root; keeping the root screen.');
|
|
153
|
+
return routes.length > 1
|
|
154
|
+
? withRoutes(state, routes.slice(0, Math.max(1, routes.length - count)))
|
|
155
|
+
: state;
|
|
156
|
+
}
|
|
157
|
+
case 'popToRoot':
|
|
158
|
+
return routes.length > 1
|
|
159
|
+
? withRoutes(state, routes.slice(0, 1))
|
|
160
|
+
: state;
|
|
161
|
+
default:
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
function selectionRouter(type) {
|
|
167
|
+
return {
|
|
168
|
+
getInitialState(options) {
|
|
169
|
+
validateRoutes(options);
|
|
170
|
+
const routes = options.routes.map((r) => routeFor(options, r.name));
|
|
171
|
+
const name = options.initialRouteName ??
|
|
172
|
+
options.routes.find((r) => !r.hidden && !r.disabled)?.name;
|
|
173
|
+
if (!name || !eligible(options, name))
|
|
174
|
+
throw new Error('Initial selection must be a visible, enabled screen.');
|
|
175
|
+
return {
|
|
176
|
+
type,
|
|
177
|
+
key: options.key,
|
|
178
|
+
routes,
|
|
179
|
+
activeRouteKey: routes.find((r) => r.name === name).key,
|
|
180
|
+
collapsed: options.defaultCollapsed ?? false,
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
reduce(state, action, options) {
|
|
184
|
+
if (action.type === 'navigate' || action.type === 'select') {
|
|
185
|
+
const name = action.payload.name;
|
|
186
|
+
const route = state.routes.find((r) => r.name === name);
|
|
187
|
+
if (!route)
|
|
188
|
+
return null;
|
|
189
|
+
if (!eligible(options, name))
|
|
190
|
+
return state;
|
|
191
|
+
const params = action.type === 'navigate' ? action.payload.params : undefined;
|
|
192
|
+
if (route.key === state.activeRouteKey && params === undefined)
|
|
193
|
+
return state;
|
|
194
|
+
return {
|
|
195
|
+
...state,
|
|
196
|
+
activeRouteKey: route.key,
|
|
197
|
+
routes: params === undefined
|
|
198
|
+
? state.routes
|
|
199
|
+
: state.routes.map((r) => r.key === route.key ? { ...r, params } : r),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (type === 'tabs' &&
|
|
203
|
+
(action.type === 'nextTab' || action.type === 'previousTab')) {
|
|
204
|
+
const routes = state.routes.filter((r) => eligible(options, r.name));
|
|
205
|
+
if (!routes.length)
|
|
206
|
+
return state;
|
|
207
|
+
const index = routes.findIndex((r) => r.key === state.activeRouteKey);
|
|
208
|
+
const next = (index + (action.type === 'nextTab' ? 1 : -1) + routes.length) %
|
|
209
|
+
routes.length;
|
|
210
|
+
return { ...state, activeRouteKey: routes[next].key };
|
|
211
|
+
}
|
|
212
|
+
if (type === 'sidebar' &&
|
|
213
|
+
['collapseSidebar', 'expandSidebar', 'toggleSidebar'].includes(action.type)) {
|
|
214
|
+
const collapsed = action.type === 'toggleSidebar'
|
|
215
|
+
? !state.collapsed
|
|
216
|
+
: action.type === 'collapseSidebar';
|
|
217
|
+
return collapsed === state.collapsed ? state : { ...state, collapsed };
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
exports.SidebarRouter = selectionRouter('sidebar');
|
|
224
|
+
exports.TabRouter = selectionRouter('tabs');
|
|
225
|
+
function clampColumnWidth(width, config) {
|
|
226
|
+
const min = config.minWidth ?? 100, max = config.maxWidth ?? Number.MAX_SAFE_INTEGER;
|
|
227
|
+
if (!Number.isFinite(min) || min < 0 || !Number.isFinite(max) || max < min)
|
|
228
|
+
throw new Error('Invalid column width constraints.');
|
|
229
|
+
return Math.min(max, Math.max(min, Number.isFinite(width) ? width : min));
|
|
230
|
+
}
|
|
231
|
+
/** Fit visible columns to the available content width (excluding divider widths). */
|
|
232
|
+
function fitSplitWidths(columns, widths, availableWidth) {
|
|
233
|
+
const fitted = Object.fromEntries(columns.map((c) => [
|
|
234
|
+
c.name,
|
|
235
|
+
clampColumnWidth(widths[c.name] ?? c.defaultWidth ?? c.minWidth ?? 240, c),
|
|
236
|
+
]));
|
|
237
|
+
if (!columns.length || !Number.isFinite(availableWidth))
|
|
238
|
+
return fitted;
|
|
239
|
+
const last = columns[columns.length - 1];
|
|
240
|
+
const fixedWidth = columns
|
|
241
|
+
.slice(0, -1)
|
|
242
|
+
.reduce((sum, c) => sum + fitted[c.name], 0);
|
|
243
|
+
fitted[last.name] = clampColumnWidth(Math.max(0, availableWidth - fixedWidth), last);
|
|
244
|
+
let remaining = availableWidth -
|
|
245
|
+
Object.values(fitted).reduce((sum, width) => sum + width, 0);
|
|
246
|
+
// Respect all min/max constraints when the viewport grows or shrinks.
|
|
247
|
+
for (const column of [...columns].reverse()) {
|
|
248
|
+
const previous = fitted[column.name];
|
|
249
|
+
fitted[column.name] = clampColumnWidth(previous + remaining, column);
|
|
250
|
+
remaining -= fitted[column.name] - previous;
|
|
251
|
+
}
|
|
252
|
+
return fitted;
|
|
253
|
+
}
|
|
254
|
+
/** Collapsed columns stay at their Sidebar width, even below the expanded minimum. */
|
|
255
|
+
function splitColumnConfig(state, config) {
|
|
256
|
+
const collapsed = state.collapsedColumns?.[config.name];
|
|
257
|
+
return collapsed
|
|
258
|
+
? { ...config, minWidth: collapsed.width, maxWidth: collapsed.width }
|
|
259
|
+
: config;
|
|
260
|
+
}
|
|
261
|
+
exports.SplitRouter = {
|
|
262
|
+
getInitialState(options) {
|
|
263
|
+
validateRoutes(options);
|
|
264
|
+
if (options.routes.length < 2 || options.routes.length > 3)
|
|
265
|
+
throw new Error('Split requires 2–3 columns.');
|
|
266
|
+
const routes = options.routes.map((r) => routeFor(options, r.name));
|
|
267
|
+
return {
|
|
268
|
+
type: 'split',
|
|
269
|
+
key: options.key,
|
|
270
|
+
routes,
|
|
271
|
+
activeRouteKey: routes[0].key,
|
|
272
|
+
widths: Object.fromEntries(options.routes.map((r) => [
|
|
273
|
+
r.name,
|
|
274
|
+
clampColumnWidth(r.defaultWidth ?? r.minWidth ?? 240, r),
|
|
275
|
+
])),
|
|
276
|
+
visibleColumnIds: routes.map((r) => r.name),
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
reduce(state, action, options) {
|
|
280
|
+
const configs = options.routes.map((config) => splitColumnConfig(state, config));
|
|
281
|
+
if (action.type === 'resizeBoundary') {
|
|
282
|
+
const index = state.visibleColumnIds.indexOf(action.payload.id);
|
|
283
|
+
const rightId = state.visibleColumnIds[index + 1];
|
|
284
|
+
if (index < 0 || !rightId)
|
|
285
|
+
return null;
|
|
286
|
+
const left = configs.find((c) => c.name === action.payload.id);
|
|
287
|
+
const right = configs.find((c) => c.name === rightId);
|
|
288
|
+
const total = state.widths[left.name] + state.widths[right.name];
|
|
289
|
+
const min = Math.max(left.minWidth ?? 100, total - (right.maxWidth ?? Number.MAX_SAFE_INTEGER));
|
|
290
|
+
const max = Math.min(left.maxWidth ?? Number.MAX_SAFE_INTEGER, total - (right.minWidth ?? 100));
|
|
291
|
+
if (min > max || !Number.isFinite(action.payload.width))
|
|
292
|
+
return state;
|
|
293
|
+
const width = Math.max(min, Math.min(max, action.payload.width));
|
|
294
|
+
if (width === state.widths[left.name])
|
|
295
|
+
return state;
|
|
296
|
+
return {
|
|
297
|
+
...state,
|
|
298
|
+
widths: {
|
|
299
|
+
...state.widths,
|
|
300
|
+
[left.name]: width,
|
|
301
|
+
[right.name]: total - width,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (action.type === 'resize') {
|
|
306
|
+
const config = configs.find((r) => r.name === action.payload.id);
|
|
307
|
+
if (!config)
|
|
308
|
+
return null;
|
|
309
|
+
const width = clampColumnWidth(action.payload.width, config);
|
|
310
|
+
return state.widths[config.name] === width
|
|
311
|
+
? state
|
|
312
|
+
: { ...state, widths: { ...state.widths, [config.name]: width } };
|
|
313
|
+
}
|
|
314
|
+
if (action.type === 'layout') {
|
|
315
|
+
const ids = action.payload.ids;
|
|
316
|
+
if (!ids.length ||
|
|
317
|
+
new Set(ids).size !== ids.length ||
|
|
318
|
+
ids.some((id) => !state.routes.some((r) => r.name === id)))
|
|
319
|
+
throw new Error('Split layout must return unique, known column ids.');
|
|
320
|
+
let widths = { ...state.widths };
|
|
321
|
+
const collapsedColumns = { ...state.collapsedColumns };
|
|
322
|
+
const expanded = new Set();
|
|
323
|
+
let collapseChanged = false;
|
|
324
|
+
for (const [columnId, width] of Object.entries(action.payload.collapsedWidths ?? {})) {
|
|
325
|
+
const config = options.routes.find((c) => c.name === columnId);
|
|
326
|
+
if (!config)
|
|
327
|
+
continue;
|
|
328
|
+
const previous = collapsedColumns[columnId];
|
|
329
|
+
if (width === null) {
|
|
330
|
+
if (!previous)
|
|
331
|
+
continue;
|
|
332
|
+
widths[columnId] = clampColumnWidth(previous.expandedWidth, config);
|
|
333
|
+
delete collapsedColumns[columnId];
|
|
334
|
+
expanded.add(columnId);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
if (!Number.isFinite(width) || width < 0)
|
|
338
|
+
throw new Error('Invalid collapsed column width.');
|
|
339
|
+
if (previous?.width === width)
|
|
340
|
+
continue;
|
|
341
|
+
collapsedColumns[columnId] = {
|
|
342
|
+
width,
|
|
343
|
+
expandedWidth: previous?.expandedWidth ?? widths[columnId],
|
|
344
|
+
};
|
|
345
|
+
widths[columnId] = width;
|
|
346
|
+
}
|
|
347
|
+
collapseChanged = true;
|
|
348
|
+
}
|
|
349
|
+
const nextConfigs = options.routes.map((config) => splitColumnConfig({ ...state, collapsedColumns }, config));
|
|
350
|
+
for (const config of nextConfigs) {
|
|
351
|
+
const width = action.payload.widths?.[config.name];
|
|
352
|
+
if (width !== undefined && ids.includes(config.name))
|
|
353
|
+
widths[config.name] = clampColumnWidth(width, config);
|
|
354
|
+
}
|
|
355
|
+
if (action.payload.availableWidth !== undefined) {
|
|
356
|
+
const visibleConfigs = ids.map((id) => nextConfigs.find((c) => c.name === id));
|
|
357
|
+
// Restore an expanded trailing column before giving leftover space to its neighbors.
|
|
358
|
+
const fitOrder = [
|
|
359
|
+
...visibleConfigs.filter((c) => expanded.has(c.name)),
|
|
360
|
+
...visibleConfigs.filter((c) => !expanded.has(c.name)),
|
|
361
|
+
];
|
|
362
|
+
widths = {
|
|
363
|
+
...widths,
|
|
364
|
+
...fitSplitWidths(fitOrder, widths, action.payload.availableWidth),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (!collapseChanged &&
|
|
368
|
+
ids.join('\0') === state.visibleColumnIds.join('\0') &&
|
|
369
|
+
Object.keys(widths).every((id) => widths[id] === state.widths[id]))
|
|
370
|
+
return state;
|
|
371
|
+
const active = state.routes.find((r) => r.key === state.activeRouteKey);
|
|
372
|
+
return {
|
|
373
|
+
...state,
|
|
374
|
+
visibleColumnIds: [...ids],
|
|
375
|
+
widths,
|
|
376
|
+
...(collapseChanged && { collapsedColumns }),
|
|
377
|
+
activeRouteKey: ids.includes(active.name)
|
|
378
|
+
? active.key
|
|
379
|
+
: state.routes.find((r) => r.name === ids[0]).key,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
exports.routers = {
|
|
386
|
+
stack: exports.StackRouter,
|
|
387
|
+
sidebar: exports.SidebarRouter,
|
|
388
|
+
tabs: exports.TabRouter,
|
|
389
|
+
split: exports.SplitRouter,
|
|
390
|
+
};
|
|
391
|
+
function reduceNavigation(state, action, options) {
|
|
392
|
+
return exports.routers[state.type].reduce(state, action, options);
|
|
393
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export type ParamListBase = Record<string, unknown>;
|
|
2
|
+
export type RouteName<P extends ParamListBase> = Extract<keyof P, string>;
|
|
3
|
+
export type RouteArgs<P extends ParamListBase> = {
|
|
4
|
+
[N in RouteName<P>]: undefined extends P[N] ? [name: N, params?: P[N]] : [name: N, params: P[N]];
|
|
5
|
+
}[RouteName<P>];
|
|
6
|
+
export interface NavigationRoute<Name extends string = string, Params = unknown> {
|
|
7
|
+
key: string;
|
|
8
|
+
name: Name;
|
|
9
|
+
params: Params;
|
|
10
|
+
}
|
|
11
|
+
export type HistoryBehavior = 'stack' | 'desktop';
|
|
12
|
+
export interface StackHistoryState {
|
|
13
|
+
past: NavigationRoute[][];
|
|
14
|
+
future: NavigationRoute[][];
|
|
15
|
+
}
|
|
16
|
+
export interface StackNavigationState {
|
|
17
|
+
type: 'stack';
|
|
18
|
+
key: string;
|
|
19
|
+
routes: NavigationRoute[];
|
|
20
|
+
activeRouteKey: string;
|
|
21
|
+
history?: StackHistoryState;
|
|
22
|
+
/** Monotonic route instance counter; independent of back/forward history. */
|
|
23
|
+
nextRouteId: number;
|
|
24
|
+
}
|
|
25
|
+
export interface SelectionNavigationState {
|
|
26
|
+
type: 'sidebar' | 'tabs';
|
|
27
|
+
key: string;
|
|
28
|
+
routes: NavigationRoute[];
|
|
29
|
+
activeRouteKey: string;
|
|
30
|
+
collapsed: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface SplitNavigationState {
|
|
33
|
+
type: 'split';
|
|
34
|
+
key: string;
|
|
35
|
+
routes: NavigationRoute[];
|
|
36
|
+
activeRouteKey: string;
|
|
37
|
+
widths: Record<string, number>;
|
|
38
|
+
visibleColumnIds: string[];
|
|
39
|
+
/** Saved expanded widths for columns currently following a collapsed Sidebar. */
|
|
40
|
+
collapsedColumns?: Record<string, {
|
|
41
|
+
width: number;
|
|
42
|
+
expandedWidth: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
export type NavigationState = StackNavigationState | SelectionNavigationState | SplitNavigationState;
|
|
46
|
+
export interface NavigationNode {
|
|
47
|
+
id: string;
|
|
48
|
+
parentId?: string;
|
|
49
|
+
parentRouteKey?: string;
|
|
50
|
+
type: NavigationState['type'];
|
|
51
|
+
state: NavigationState;
|
|
52
|
+
}
|
|
53
|
+
export interface RootNavigationState {
|
|
54
|
+
version: 1;
|
|
55
|
+
rootNodeId: string;
|
|
56
|
+
activeNodeId?: string;
|
|
57
|
+
nodes: Record<string, NavigationNode>;
|
|
58
|
+
}
|
|
59
|
+
export type NavigationAction = {
|
|
60
|
+
target?: string;
|
|
61
|
+
} & ({
|
|
62
|
+
type: 'navigate' | 'push' | 'replace' | 'popTo';
|
|
63
|
+
payload: {
|
|
64
|
+
name: string;
|
|
65
|
+
params?: unknown;
|
|
66
|
+
key?: string;
|
|
67
|
+
};
|
|
68
|
+
} | {
|
|
69
|
+
type: 'select';
|
|
70
|
+
payload: {
|
|
71
|
+
name: string;
|
|
72
|
+
};
|
|
73
|
+
} | {
|
|
74
|
+
type: 'pop';
|
|
75
|
+
payload?: {
|
|
76
|
+
count?: number;
|
|
77
|
+
};
|
|
78
|
+
} | {
|
|
79
|
+
type: 'back' | 'forward' | 'popToRoot' | 'nextTab' | 'previousTab' | 'collapseSidebar' | 'expandSidebar' | 'toggleSidebar';
|
|
80
|
+
} | {
|
|
81
|
+
type: 'resize' | 'resizeBoundary';
|
|
82
|
+
payload: {
|
|
83
|
+
id: string;
|
|
84
|
+
width: number;
|
|
85
|
+
};
|
|
86
|
+
} | {
|
|
87
|
+
type: 'layout';
|
|
88
|
+
payload: {
|
|
89
|
+
ids: string[];
|
|
90
|
+
widths?: Record<string, number>;
|
|
91
|
+
availableWidth?: number;
|
|
92
|
+
/** null restores the expanded width; omitted ids keep their current state. */
|
|
93
|
+
collapsedWidths?: Record<string, number | null>;
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
export interface RouteConfig {
|
|
97
|
+
name: string;
|
|
98
|
+
initialParams?: unknown;
|
|
99
|
+
hidden?: boolean;
|
|
100
|
+
disabled?: boolean;
|
|
101
|
+
minWidth?: number;
|
|
102
|
+
maxWidth?: number;
|
|
103
|
+
defaultWidth?: number;
|
|
104
|
+
}
|
|
105
|
+
export interface RouterOptions {
|
|
106
|
+
key: string;
|
|
107
|
+
routes: RouteConfig[];
|
|
108
|
+
initialRouteName?: string;
|
|
109
|
+
historyBehavior?: HistoryBehavior;
|
|
110
|
+
defaultCollapsed?: boolean;
|
|
111
|
+
}
|
|
112
|
+
export interface Router<S extends NavigationState = NavigationState> {
|
|
113
|
+
getInitialState(options: RouterOptions): S;
|
|
114
|
+
/** null means unhandled and allows the store to bubble to the parent. */
|
|
115
|
+
reduce(state: S, action: NavigationAction, options: RouterOptions): S | null;
|
|
116
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ParamListBase, RouteName } from '../routers';
|
|
3
|
+
import { type ScreenConfig } from '../core/builder';
|
|
4
|
+
import { type SelectionNavigatorProps } from '../core/SelectionNavigator';
|
|
5
|
+
import type { SidebarNavigation, SidebarScreenOptions, SidebarSectionOptions } from '../core/types';
|
|
6
|
+
export interface SidebarNavigatorOptions extends SelectionNavigatorProps {
|
|
7
|
+
}
|
|
8
|
+
export declare function createSidebarNavigator<P extends ParamListBase = ParamListBase>(): {
|
|
9
|
+
Navigator: (props: Omit<SidebarNavigatorOptions, "initialRouteName"> & {
|
|
10
|
+
initialRouteName?: RouteName<P>;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}) => React.JSX.Element;
|
|
13
|
+
Screen: <N extends RouteName<P>>(_props: ScreenConfig<P, N, SidebarScreenOptions, SidebarNavigation<P>>) => null;
|
|
14
|
+
Section: (_props: SidebarSectionOptions & {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) => null;
|
|
17
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSidebarNavigator = createSidebarNavigator;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const builder_1 = require("../core/builder");
|
|
6
|
+
const SelectionNavigator_1 = require("../core/SelectionNavigator");
|
|
7
|
+
function createSidebarNavigator() {
|
|
8
|
+
function Screen(_props) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
function Section(_props) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function Navigator(props) {
|
|
15
|
+
return ((0, jsx_runtime_1.jsx)(SelectionNavigator_1.SelectionNavigator, { ...props, type: "sidebar", definitions: (0, builder_1.readScreens)(props.children, Screen, Section) }));
|
|
16
|
+
}
|
|
17
|
+
return { Navigator, Screen, Section };
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
export interface SplitColumnProps {
|
|
4
|
+
id: string;
|
|
5
|
+
component: React.ComponentType;
|
|
6
|
+
minWidth?: number;
|
|
7
|
+
maxWidth?: number;
|
|
8
|
+
defaultWidth?: number;
|
|
9
|
+
/** Follow a directly nested Sidebar when it collapses (default: true). */
|
|
10
|
+
collapsible?: boolean;
|
|
11
|
+
style?: StyleProp<ViewStyle>;
|
|
12
|
+
contentStyle?: StyleProp<ViewStyle>;
|
|
13
|
+
/** Hide the entire resize handle, including its space and keyboard target. */
|
|
14
|
+
dividerShown?: boolean;
|
|
15
|
+
dividerStyle?: SplitDividerStyle;
|
|
16
|
+
renderDivider?: (props: SplitDividerRenderProps) => React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export interface SplitDividerRenderProps {
|
|
19
|
+
columnId: string;
|
|
20
|
+
width: number;
|
|
21
|
+
dragging: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type SplitDividerStyle = StyleProp<ViewStyle> | ((props: SplitDividerRenderProps) => StyleProp<ViewStyle>);
|
|
24
|
+
export interface SplitNavigatorProps {
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
id?: string;
|
|
27
|
+
style?: StyleProp<ViewStyle>;
|
|
28
|
+
columnStyle?: StyleProp<ViewStyle>;
|
|
29
|
+
/** Hide the entire resize handle, including its space and keyboard target. */
|
|
30
|
+
dividerShown?: boolean;
|
|
31
|
+
dividerStyle?: SplitDividerStyle;
|
|
32
|
+
renderDivider?: (props: SplitDividerRenderProps) => React.ReactNode;
|
|
33
|
+
layout?: (size: {
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
}) => string[];
|
|
37
|
+
onColumnResize?: (id: string, width: number) => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createSplitNavigator(): {
|
|
40
|
+
Navigator: (props: SplitNavigatorProps) => React.JSX.Element;
|
|
41
|
+
Column: (_props: SplitColumnProps) => null;
|
|
42
|
+
};
|