@ankhorage/contracts 10.1.0 → 11.0.0
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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/appManifest/navigator.d.ts +3 -0
- package/dist/appManifest/navigator.d.ts.map +1 -0
- package/dist/appManifest/navigator.js +142 -0
- package/dist/appManifest/navigator.js.map +1 -0
- package/dist/appManifest/navigatorOptions.d.ts +13 -0
- package/dist/appManifest/navigatorOptions.d.ts.map +1 -0
- package/dist/appManifest/navigatorOptions.js +226 -0
- package/dist/appManifest/navigatorOptions.js.map +1 -0
- package/dist/appManifest/screens.d.ts +1 -2
- package/dist/appManifest/screens.d.ts.map +1 -1
- package/dist/appManifest/screens.js +2 -116
- package/dist/appManifest/screens.js.map +1 -1
- package/dist/navigator.d.ts +101 -14
- package/dist/navigator.d.ts.map +1 -1
- package/dist/navigator.js +23 -1
- package/dist/navigator.js.map +1 -1
- package/package.json +1 -1
- package/src/appManifest/navigator.ts +179 -0
- package/src/appManifest/navigatorOptions.ts +292 -0
- package/src/appManifest/screens.ts +3 -170
- package/src/contracts.test.ts +1 -1
- package/src/navigator-validation.test.ts +153 -0
- package/src/navigator.test.ts +153 -10
- package/src/navigator.ts +149 -13
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from 'bun:test';
|
|
5
|
+
|
|
6
|
+
import { isAppNavigatorManifest } from './appManifest/screens';
|
|
7
|
+
import type {
|
|
8
|
+
CustomNavigatorNode,
|
|
9
|
+
CustomTabsConfig,
|
|
10
|
+
NavigatorNode,
|
|
11
|
+
StackImplementationConfig,
|
|
12
|
+
} from './navigator';
|
|
13
|
+
|
|
14
|
+
describe('app navigator manifest type discrimination', () => {
|
|
15
|
+
it('prevents incompatible options and non-serializable configuration at compile time', () => {
|
|
16
|
+
// @ts-expect-error Slot does not own an arbitrary options bag.
|
|
17
|
+
const slot: NavigatorNode = { type: 'slot', options: { arbitrary: true }, routes: [] };
|
|
18
|
+
// @ts-expect-error Experimental Stack has no presentation options.
|
|
19
|
+
const experimental: StackImplementationConfig = {
|
|
20
|
+
implementation: 'experimental',
|
|
21
|
+
options: { presentation: 'modal' },
|
|
22
|
+
};
|
|
23
|
+
// @ts-expect-error Fixed custom tabs do not accept a registered custom presentation id.
|
|
24
|
+
const tabs: CustomTabsConfig = {
|
|
25
|
+
implementation: 'custom',
|
|
26
|
+
presentation: 'sidebar',
|
|
27
|
+
customPresentationId: 'workspace-tabs',
|
|
28
|
+
};
|
|
29
|
+
// @ts-expect-error Custom navigator configuration must be serializable manifest values.
|
|
30
|
+
const custom: CustomNavigatorNode = {
|
|
31
|
+
type: 'custom',
|
|
32
|
+
navigatorId: 'workspace',
|
|
33
|
+
config: { callback: () => undefined },
|
|
34
|
+
routes: [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
expect([slot, experimental, tabs, custom]).toHaveLength(4);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('app navigator manifest stack validation', () => {
|
|
42
|
+
it('rejects the removed untyped options bag and unsupported stack branch fields', () => {
|
|
43
|
+
expect(isAppNavigatorManifest({ type: 'slot', options: { arbitrary: true }, routes: [] })).toBe(
|
|
44
|
+
false,
|
|
45
|
+
);
|
|
46
|
+
expect(
|
|
47
|
+
isAppNavigatorManifest({
|
|
48
|
+
type: 'stack',
|
|
49
|
+
implementation: 'experimental',
|
|
50
|
+
options: { presentation: 'modal' },
|
|
51
|
+
routes: [],
|
|
52
|
+
}),
|
|
53
|
+
).toBe(false);
|
|
54
|
+
expect(
|
|
55
|
+
isAppNavigatorManifest({
|
|
56
|
+
type: 'stack',
|
|
57
|
+
implementation: 'javascript',
|
|
58
|
+
options: { presentation: 'formSheet' },
|
|
59
|
+
routes: [],
|
|
60
|
+
}),
|
|
61
|
+
).toBe(false);
|
|
62
|
+
expect(
|
|
63
|
+
isAppNavigatorManifest({
|
|
64
|
+
type: 'stack',
|
|
65
|
+
implementation: 'native',
|
|
66
|
+
minimizeBehavior: 'never',
|
|
67
|
+
routes: [],
|
|
68
|
+
}),
|
|
69
|
+
).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('app navigator manifest sheet validation', () => {
|
|
74
|
+
it('validates native form-sheet detents and their presentation discriminant', () => {
|
|
75
|
+
for (const sheetAllowedDetents of [[], [0.8, 0.4], [0.4, 0.4], [0], [1.1], [Number.NaN]]) {
|
|
76
|
+
expect(
|
|
77
|
+
isAppNavigatorManifest({
|
|
78
|
+
type: 'stack',
|
|
79
|
+
options: { presentation: 'formSheet', sheetAllowedDetents },
|
|
80
|
+
routes: [],
|
|
81
|
+
}),
|
|
82
|
+
).toBe(false);
|
|
83
|
+
}
|
|
84
|
+
expect(
|
|
85
|
+
isAppNavigatorManifest({
|
|
86
|
+
type: 'stack',
|
|
87
|
+
options: { presentation: 'modal', sheetGrabberVisible: true },
|
|
88
|
+
routes: [],
|
|
89
|
+
}),
|
|
90
|
+
).toBe(false);
|
|
91
|
+
expect(
|
|
92
|
+
isAppNavigatorManifest({
|
|
93
|
+
type: 'stack',
|
|
94
|
+
routes: [
|
|
95
|
+
{
|
|
96
|
+
name: 'compose',
|
|
97
|
+
stackOptions: {
|
|
98
|
+
presentation: 'formSheet',
|
|
99
|
+
sheetAllowedDetents: 'fitToContents',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('app navigator manifest custom validation', () => {
|
|
109
|
+
it('rejects non-JSON and cyclic custom navigator configuration', () => {
|
|
110
|
+
const cyclicConfig: Record<string, unknown> = {};
|
|
111
|
+
cyclicConfig.self = cyclicConfig;
|
|
112
|
+
|
|
113
|
+
for (const invalidConfig of [
|
|
114
|
+
{ callback: () => undefined },
|
|
115
|
+
{ token: Symbol('token') },
|
|
116
|
+
{ count: Number.NaN },
|
|
117
|
+
{ count: Number.POSITIVE_INFINITY },
|
|
118
|
+
cyclicConfig,
|
|
119
|
+
]) {
|
|
120
|
+
expect(
|
|
121
|
+
isAppNavigatorManifest({
|
|
122
|
+
type: 'custom',
|
|
123
|
+
navigatorId: 'workspace',
|
|
124
|
+
config: invalidConfig,
|
|
125
|
+
routes: [],
|
|
126
|
+
}),
|
|
127
|
+
).toBe(false);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('rejects incomplete Split View bindings', () => {
|
|
132
|
+
expect(
|
|
133
|
+
isAppNavigatorManifest({
|
|
134
|
+
type: 'split-view',
|
|
135
|
+
columns: {},
|
|
136
|
+
routes: [],
|
|
137
|
+
}),
|
|
138
|
+
).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('app navigator manifest package boundary', () => {
|
|
143
|
+
it('publishes the focused navigator contract subpath', async () => {
|
|
144
|
+
const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8')) as {
|
|
145
|
+
exports?: Record<string, { default?: string; types?: string }>;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
expect(packageJson.exports?.['./navigator']).toEqual({
|
|
149
|
+
types: './dist/navigator.d.ts',
|
|
150
|
+
default: './dist/navigator.js',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
package/src/navigator.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { describe, expect, it } from 'bun:test';
|
|
5
2
|
|
|
6
3
|
import { isAppNavigatorManifest } from './appManifest/screens';
|
|
7
4
|
import {
|
|
8
5
|
type AppNavigatorManifest,
|
|
9
6
|
NAVIGATOR_PRESETS,
|
|
7
|
+
NAVIGATOR_TYPES,
|
|
8
|
+
type NavigatorNode,
|
|
9
|
+
type StackImplementationConfig,
|
|
10
10
|
type TabsNavigatorConfig,
|
|
11
11
|
} from './navigator';
|
|
12
12
|
|
|
@@ -28,8 +28,66 @@ function createAdaptiveTabs(): AppNavigatorManifest {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const VALID_NAVIGATOR_NODES = [
|
|
32
|
+
{ type: 'slot', routes: [] },
|
|
33
|
+
{
|
|
34
|
+
type: 'stack',
|
|
35
|
+
implementation: 'native',
|
|
36
|
+
options: {
|
|
37
|
+
presentation: 'formSheet',
|
|
38
|
+
sheetAllowedDetents: [0.4, 0.8],
|
|
39
|
+
sheetGrabberVisible: true,
|
|
40
|
+
},
|
|
41
|
+
routes: [],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'stack',
|
|
45
|
+
implementation: 'javascript',
|
|
46
|
+
options: { presentation: 'transparentModal', headerShown: false },
|
|
47
|
+
routes: [],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'stack',
|
|
51
|
+
implementation: 'experimental',
|
|
52
|
+
options: { title: 'Profile', headerTransparent: true },
|
|
53
|
+
routes: [],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'tabs',
|
|
57
|
+
implementation: 'native',
|
|
58
|
+
minimizeBehavior: 'onScrollDown',
|
|
59
|
+
bottomAccessory: { screenId: 'now-playing' },
|
|
60
|
+
routes: [],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'drawer',
|
|
64
|
+
options: { drawerPosition: 'right', drawerType: 'permanent', swipeEnabled: false },
|
|
65
|
+
routes: [],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'split-view',
|
|
69
|
+
columns: {
|
|
70
|
+
primary: { screenId: 'sidebar' },
|
|
71
|
+
supplementary: { screenId: 'inspector-list' },
|
|
72
|
+
},
|
|
73
|
+
inspector: { screenId: 'inspector' },
|
|
74
|
+
topColumnForCollapsing: 'secondary',
|
|
75
|
+
routes: [{ name: 'detail', screenId: 'detail' }],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'custom',
|
|
79
|
+
navigatorId: 'workspace',
|
|
80
|
+
config: { animation: 'spring', thresholds: [0.25, 0.75], enabled: true },
|
|
81
|
+
routes: [],
|
|
82
|
+
},
|
|
83
|
+
] as const satisfies readonly NavigatorNode[];
|
|
84
|
+
|
|
31
85
|
describe('app navigator manifest topology', () => {
|
|
32
86
|
it('keeps canonical topology presets finite and authorable', () => {
|
|
87
|
+
expect(NAVIGATOR_TYPES).toEqual(['slot', 'stack', 'tabs', 'drawer', 'split-view', 'custom']);
|
|
88
|
+
expect(NAVIGATOR_PRESETS).toContain('slot');
|
|
89
|
+
expect(NAVIGATOR_PRESETS).toContain('split-view');
|
|
90
|
+
expect(NAVIGATOR_PRESETS).toContain('custom');
|
|
33
91
|
expect(NAVIGATOR_PRESETS).toContain('root-stack-tabs-stack');
|
|
34
92
|
expect(NAVIGATOR_PRESETS).toContain('root-stack-drawer-tabs-stack');
|
|
35
93
|
});
|
|
@@ -69,6 +127,31 @@ describe('app navigator manifest topology', () => {
|
|
|
69
127
|
});
|
|
70
128
|
});
|
|
71
129
|
|
|
130
|
+
describe('app navigator manifest node variants', () => {
|
|
131
|
+
it('accepts every typed navigator node variant', () => {
|
|
132
|
+
expect(VALID_NAVIGATOR_NODES.every(isAppNavigatorManifest)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('accepts stack defaults and per-platform implementation overrides', () => {
|
|
136
|
+
const experimental = {
|
|
137
|
+
implementation: 'experimental',
|
|
138
|
+
options: { headerShown: false },
|
|
139
|
+
} as const satisfies StackImplementationConfig;
|
|
140
|
+
|
|
141
|
+
expect(
|
|
142
|
+
isAppNavigatorManifest({
|
|
143
|
+
type: 'stack',
|
|
144
|
+
routes: [],
|
|
145
|
+
defaults: { stack: { implementation: 'native' } },
|
|
146
|
+
platforms: {
|
|
147
|
+
ios: { stack: experimental },
|
|
148
|
+
web: { stack: { implementation: 'javascript', options: { presentation: 'card' } } },
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
72
155
|
describe('app navigator manifest custom presentation', () => {
|
|
73
156
|
it('accepts fixed and registered custom Web presentations', () => {
|
|
74
157
|
expect(
|
|
@@ -112,6 +195,50 @@ describe('app navigator manifest custom presentation', () => {
|
|
|
112
195
|
});
|
|
113
196
|
});
|
|
114
197
|
|
|
198
|
+
describe('app navigator manifest tabs branch validation', () => {
|
|
199
|
+
it('rejects fields from incompatible tab branches', () => {
|
|
200
|
+
expect(
|
|
201
|
+
isAppNavigatorManifest({
|
|
202
|
+
type: 'tabs',
|
|
203
|
+
implementation: 'native',
|
|
204
|
+
presentation: 'bottom',
|
|
205
|
+
routes: [],
|
|
206
|
+
}),
|
|
207
|
+
).toBe(false);
|
|
208
|
+
|
|
209
|
+
expect(
|
|
210
|
+
isAppNavigatorManifest({
|
|
211
|
+
type: 'tabs',
|
|
212
|
+
implementation: 'javascript',
|
|
213
|
+
presentation: 'bottom',
|
|
214
|
+
native: { implementation: 'native' },
|
|
215
|
+
routes: [],
|
|
216
|
+
}),
|
|
217
|
+
).toBe(false);
|
|
218
|
+
|
|
219
|
+
expect(
|
|
220
|
+
isAppNavigatorManifest({
|
|
221
|
+
type: 'tabs',
|
|
222
|
+
implementation: 'custom',
|
|
223
|
+
presentation: 'sidebar',
|
|
224
|
+
customPresentationId: 'must-not-apply',
|
|
225
|
+
routes: [],
|
|
226
|
+
}),
|
|
227
|
+
).toBe(false);
|
|
228
|
+
|
|
229
|
+
expect(
|
|
230
|
+
isAppNavigatorManifest({
|
|
231
|
+
type: 'tabs',
|
|
232
|
+
implementation: 'custom',
|
|
233
|
+
presentation: 'custom',
|
|
234
|
+
customPresentationId: 'workspace-tabs',
|
|
235
|
+
responsive: { compact: 'bottom', expanded: 'sidebar' },
|
|
236
|
+
routes: [],
|
|
237
|
+
}),
|
|
238
|
+
).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
115
242
|
describe('app navigator manifest composition', () => {
|
|
116
243
|
it('keeps nested topology separate from app-level flow metadata', () => {
|
|
117
244
|
expect(
|
|
@@ -131,14 +258,30 @@ describe('app navigator manifest composition', () => {
|
|
|
131
258
|
).toBe(true);
|
|
132
259
|
});
|
|
133
260
|
|
|
134
|
-
it('
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
261
|
+
it('preserves authored route names, labels, paths, guards, and visibility without inference', () => {
|
|
262
|
+
const navigator = {
|
|
263
|
+
type: 'stack',
|
|
264
|
+
routes: [
|
|
265
|
+
{
|
|
266
|
+
name: '(app)',
|
|
267
|
+
label: 'Application',
|
|
268
|
+
guards: ['authenticated'],
|
|
269
|
+
showInPrimaryNavigation: false,
|
|
270
|
+
navigator: {
|
|
271
|
+
type: 'tabs',
|
|
272
|
+
routes: [{ name: 'learn', path: '/learn', label: 'Train', screenId: 'learn' }],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
} as const satisfies AppNavigatorManifest;
|
|
138
277
|
|
|
139
|
-
expect(
|
|
140
|
-
|
|
141
|
-
|
|
278
|
+
expect(isAppNavigatorManifest(navigator)).toBe(true);
|
|
279
|
+
expect(JSON.parse(JSON.stringify(navigator))).toEqual(navigator);
|
|
280
|
+
expect(navigator.routes[0].navigator.routes[0]).toEqual({
|
|
281
|
+
name: 'learn',
|
|
282
|
+
path: '/learn',
|
|
283
|
+
label: 'Train',
|
|
284
|
+
screenId: 'learn',
|
|
142
285
|
});
|
|
143
286
|
});
|
|
144
287
|
});
|
package/src/navigator.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { IconSpec } from './types';
|
|
1
|
+
import type { IconSpec, ManifestValue } from './types';
|
|
2
2
|
|
|
3
|
-
export const NAVIGATOR_TYPES = ['stack', 'tabs', 'drawer'] as const;
|
|
3
|
+
export const NAVIGATOR_TYPES = ['slot', 'stack', 'tabs', 'drawer', 'split-view', 'custom'] as const;
|
|
4
4
|
export type NavigatorType = (typeof NAVIGATOR_TYPES)[number];
|
|
5
5
|
|
|
6
6
|
export const NAVIGATOR_PRESETS = [
|
|
7
|
+
'slot',
|
|
7
8
|
'stack',
|
|
8
9
|
'tabs',
|
|
9
10
|
'tabs-stack',
|
|
@@ -17,9 +18,82 @@ export const NAVIGATOR_PRESETS = [
|
|
|
17
18
|
'root-stack-drawer-stack',
|
|
18
19
|
'root-stack-drawer-tabs',
|
|
19
20
|
'root-stack-drawer-tabs-stack',
|
|
21
|
+
'split-view',
|
|
22
|
+
'custom',
|
|
20
23
|
] as const;
|
|
21
24
|
export type NavigatorPreset = (typeof NAVIGATOR_PRESETS)[number];
|
|
22
25
|
|
|
26
|
+
export const STACK_IMPLEMENTATIONS = ['native', 'javascript', 'experimental'] as const;
|
|
27
|
+
export type StackImplementation = (typeof STACK_IMPLEMENTATIONS)[number];
|
|
28
|
+
|
|
29
|
+
export const STACK_PRESENTATIONS = [
|
|
30
|
+
'card',
|
|
31
|
+
'modal',
|
|
32
|
+
'transparentModal',
|
|
33
|
+
'containedModal',
|
|
34
|
+
'containedTransparentModal',
|
|
35
|
+
'fullScreenModal',
|
|
36
|
+
'formSheet',
|
|
37
|
+
] as const;
|
|
38
|
+
export type StackPresentation = (typeof STACK_PRESENTATIONS)[number];
|
|
39
|
+
|
|
40
|
+
export const JAVASCRIPT_STACK_PRESENTATIONS = ['card', 'modal', 'transparentModal'] as const;
|
|
41
|
+
export type JavaScriptStackPresentation = (typeof JAVASCRIPT_STACK_PRESENTATIONS)[number];
|
|
42
|
+
|
|
43
|
+
export interface StackHeaderOptions {
|
|
44
|
+
title?: string;
|
|
45
|
+
headerShown?: boolean;
|
|
46
|
+
headerTransparent?: boolean;
|
|
47
|
+
headerBackVisible?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type StackScreenOptions = StackHeaderOptions &
|
|
51
|
+
(
|
|
52
|
+
| {
|
|
53
|
+
presentation?: Exclude<StackPresentation, 'formSheet'>;
|
|
54
|
+
sheetAllowedDetents?: never;
|
|
55
|
+
sheetGrabberVisible?: never;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
presentation: 'formSheet';
|
|
59
|
+
sheetAllowedDetents?: 'fitToContents' | number[];
|
|
60
|
+
sheetGrabberVisible?: boolean;
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export type JavaScriptStackScreenOptions = StackHeaderOptions & {
|
|
65
|
+
presentation?: JavaScriptStackPresentation;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type StackImplementationConfig =
|
|
69
|
+
| {
|
|
70
|
+
/** Omission selects the stable native stack implementation. */
|
|
71
|
+
implementation?: 'native';
|
|
72
|
+
options?: StackScreenOptions;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
implementation: 'javascript';
|
|
76
|
+
options?: JavaScriptStackScreenOptions;
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
/** Expo Router Experimental Stack is alpha, native-only, and available from SDK 56. */
|
|
80
|
+
implementation: 'experimental';
|
|
81
|
+
options?: StackHeaderOptions;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const DRAWER_POSITIONS = ['left', 'right'] as const;
|
|
85
|
+
export type DrawerPosition = (typeof DRAWER_POSITIONS)[number];
|
|
86
|
+
|
|
87
|
+
export const DRAWER_TYPES = ['front', 'back', 'slide', 'permanent'] as const;
|
|
88
|
+
export type DrawerType = (typeof DRAWER_TYPES)[number];
|
|
89
|
+
|
|
90
|
+
export interface DrawerNavigatorOptions {
|
|
91
|
+
drawerPosition?: DrawerPosition;
|
|
92
|
+
drawerType?: DrawerType;
|
|
93
|
+
swipeEnabled?: boolean;
|
|
94
|
+
headerShown?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
23
97
|
export const FIXED_CUSTOM_TABS_PRESENTATIONS = ['bottom', 'top', 'rail', 'sidebar'] as const;
|
|
24
98
|
export type FixedCustomTabsPresentation = (typeof FIXED_CUSTOM_TABS_PRESENTATIONS)[number];
|
|
25
99
|
|
|
@@ -33,22 +107,54 @@ export type CustomTabsPresentation = (typeof CUSTOM_TABS_PRESENTATIONS)[number];
|
|
|
33
107
|
export const JAVASCRIPT_TABS_PRESENTATIONS = ['bottom', 'top'] as const;
|
|
34
108
|
export type JavaScriptTabsPresentation = (typeof JAVASCRIPT_TABS_PRESENTATIONS)[number];
|
|
35
109
|
|
|
110
|
+
export const NATIVE_TABS_MINIMIZE_BEHAVIORS = [
|
|
111
|
+
'automatic',
|
|
112
|
+
'never',
|
|
113
|
+
'onScrollDown',
|
|
114
|
+
'onScrollUp',
|
|
115
|
+
] as const;
|
|
116
|
+
export type NativeTabsMinimizeBehavior = (typeof NATIVE_TABS_MINIMIZE_BEHAVIORS)[number];
|
|
117
|
+
|
|
36
118
|
export interface ResponsiveTabsPresentation {
|
|
37
119
|
compact: FixedCustomTabsPresentation;
|
|
38
120
|
medium?: FixedCustomTabsPresentation;
|
|
39
121
|
expanded: FixedCustomTabsPresentation;
|
|
40
122
|
}
|
|
41
123
|
|
|
42
|
-
export
|
|
124
|
+
export type CustomTabsPresentationConfig =
|
|
125
|
+
| {
|
|
126
|
+
presentation: FixedCustomTabsPresentation;
|
|
127
|
+
responsive?: never;
|
|
128
|
+
customPresentationId?: never;
|
|
129
|
+
}
|
|
130
|
+
| {
|
|
131
|
+
presentation: 'responsive';
|
|
132
|
+
responsive: ResponsiveTabsPresentation;
|
|
133
|
+
customPresentationId?: never;
|
|
134
|
+
}
|
|
135
|
+
| {
|
|
136
|
+
presentation: 'custom';
|
|
137
|
+
responsive?: never;
|
|
138
|
+
/** Serializable registered presentation id. */
|
|
139
|
+
customPresentationId: string;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type CustomTabsConfig = {
|
|
43
143
|
implementation: 'custom';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
144
|
+
} & CustomTabsPresentationConfig;
|
|
145
|
+
|
|
146
|
+
export type CustomTabsWebConfig = CustomTabsPresentationConfig;
|
|
147
|
+
|
|
148
|
+
export interface NavigatorScreenReference {
|
|
149
|
+
screenId: string;
|
|
48
150
|
}
|
|
49
151
|
|
|
50
152
|
export interface NativeTabsConfig {
|
|
51
153
|
implementation: 'native';
|
|
154
|
+
/** Expo Router Native Tabs is alpha; minimizing is available on iOS 26+ from SDK 55. */
|
|
155
|
+
minimizeBehavior?: NativeTabsMinimizeBehavior;
|
|
156
|
+
/** Registered screen rendered through NativeTabs.BottomAccessory, available from SDK 55. */
|
|
157
|
+
bottomAccessory?: NavigatorScreenReference;
|
|
52
158
|
}
|
|
53
159
|
|
|
54
160
|
export interface JavaScriptTabsConfig {
|
|
@@ -62,7 +168,7 @@ export interface AdaptiveTabsConfig {
|
|
|
62
168
|
/** Android/iOS branch. Expo Router may expose this implementation as unstable. */
|
|
63
169
|
native?: NativeTabsConfig;
|
|
64
170
|
/** Web branch rendered through headless custom tabs. */
|
|
65
|
-
web?:
|
|
171
|
+
web?: CustomTabsWebConfig;
|
|
66
172
|
}
|
|
67
173
|
|
|
68
174
|
export type TabsImplementationConfig =
|
|
@@ -71,16 +177,19 @@ export type TabsImplementationConfig =
|
|
|
71
177
|
interface NavigatorNodeBase {
|
|
72
178
|
initialRouteName?: string;
|
|
73
179
|
routes: RouteDefinition[];
|
|
74
|
-
/** Typed upstream options can be layered by the owning Navigator package. */
|
|
75
|
-
options?: Record<string, unknown>;
|
|
76
180
|
}
|
|
77
181
|
|
|
78
|
-
export interface
|
|
79
|
-
type: '
|
|
182
|
+
export interface SlotNavigatorNode extends NavigatorNodeBase {
|
|
183
|
+
type: 'slot';
|
|
80
184
|
}
|
|
81
185
|
|
|
186
|
+
export type StackNavigatorNode = NavigatorNodeBase & {
|
|
187
|
+
type: 'stack';
|
|
188
|
+
} & StackImplementationConfig;
|
|
189
|
+
|
|
82
190
|
export interface DrawerNavigatorNode extends NavigatorNodeBase {
|
|
83
191
|
type: 'drawer';
|
|
192
|
+
options?: DrawerNavigatorOptions;
|
|
84
193
|
}
|
|
85
194
|
|
|
86
195
|
export type TabsNavigatorConfig = {
|
|
@@ -89,7 +198,30 @@ export type TabsNavigatorConfig = {
|
|
|
89
198
|
|
|
90
199
|
export type TabsNavigatorNode = NavigatorNodeBase & TabsNavigatorConfig;
|
|
91
200
|
|
|
92
|
-
export
|
|
201
|
+
export interface SplitViewNavigatorNode extends NavigatorNodeBase {
|
|
202
|
+
type: 'split-view';
|
|
203
|
+
columns: {
|
|
204
|
+
primary: NavigatorScreenReference;
|
|
205
|
+
supplementary?: NavigatorScreenReference;
|
|
206
|
+
};
|
|
207
|
+
inspector?: NavigatorScreenReference;
|
|
208
|
+
/** Split View is alpha and iOS-only; other platforms fall back to Slot. */
|
|
209
|
+
topColumnForCollapsing?: 'primary' | 'supplementary' | 'secondary';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface CustomNavigatorNode extends NavigatorNodeBase {
|
|
213
|
+
type: 'custom';
|
|
214
|
+
navigatorId: string;
|
|
215
|
+
config?: Readonly<Record<string, ManifestValue>>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type NavigatorNode =
|
|
219
|
+
| CustomNavigatorNode
|
|
220
|
+
| DrawerNavigatorNode
|
|
221
|
+
| SlotNavigatorNode
|
|
222
|
+
| SplitViewNavigatorNode
|
|
223
|
+
| StackNavigatorNode
|
|
224
|
+
| TabsNavigatorNode;
|
|
93
225
|
|
|
94
226
|
export interface RouteDefinition {
|
|
95
227
|
name: string;
|
|
@@ -100,6 +232,8 @@ export interface RouteDefinition {
|
|
|
100
232
|
showInPrimaryNavigation?: boolean;
|
|
101
233
|
guards?: string[];
|
|
102
234
|
screenId?: string;
|
|
235
|
+
/** Presentation applied by the resolved parent stack implementation. */
|
|
236
|
+
stackOptions?: StackScreenOptions;
|
|
103
237
|
navigator?: NavigatorNode;
|
|
104
238
|
}
|
|
105
239
|
|
|
@@ -110,10 +244,12 @@ export interface NavigatorFlows {
|
|
|
110
244
|
|
|
111
245
|
export interface NavigatorDefaults {
|
|
112
246
|
tabs?: TabsImplementationConfig;
|
|
247
|
+
stack?: StackImplementationConfig;
|
|
113
248
|
}
|
|
114
249
|
|
|
115
250
|
export interface NavigatorPlatformConfig {
|
|
116
251
|
tabs?: TabsImplementationConfig;
|
|
252
|
+
stack?: StackImplementationConfig;
|
|
117
253
|
}
|
|
118
254
|
|
|
119
255
|
export interface NavigatorPlatforms {
|