@symbiote-native/navigation 2.0.3 → 2.1.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/build/angular/drawer/index.js +6 -4
- package/build/angular/stack/index.d.ts +2 -0
- package/build/angular/stack/index.js +23 -3
- package/build/angular/tabs/index.d.ts +1 -1
- package/build/angular/tabs/index.js +20 -7
- package/build/core/index.d.ts +2 -1
- package/build/core/index.js +5 -1
- package/build/core/stack-router-state/index.d.ts +2 -0
- package/build/core/stack-router-state/index.js +44 -0
- package/build/core/tab-router-state/index.d.ts +1 -0
- package/build/core/tab-router-state/index.js +19 -0
- package/build/react/drawer/index.js +23 -13
- package/build/react/stack/index.js +13 -2
- package/build/react/tabs/index.js +23 -9
- package/build/svelte/attachments.d.ts +5 -0
- package/build/svelte/attachments.js +61 -0
- package/build/svelte/descriptor-subtree.d.ts +3 -0
- package/build/svelte/descriptor-subtree.js +37 -0
- package/build/svelte/drawer/drawer-props.d.ts +14 -0
- package/build/svelte/drawer/drawer-props.js +3 -0
- package/build/svelte/drawer/index.d.ts +10 -0
- package/build/svelte/drawer/index.js +6 -0
- package/build/svelte/drawer/index.svelte +421 -0
- package/build/svelte/drawer/index.svelte.d.ts +10 -0
- package/build/svelte/drawer-screen-props.d.ts +13 -0
- package/build/svelte/drawer-screen-props.js +4 -0
- package/build/svelte/drawer-screen.svelte +24 -0
- package/build/svelte/drawer-screen.svelte.d.ts +4 -0
- package/build/svelte/index.d.ts +21 -0
- package/build/svelte/index.js +20 -0
- package/build/svelte/linking.svelte.d.ts +2 -0
- package/build/svelte/linking.svelte.js +55 -0
- package/build/svelte/navigation-context.d.ts +14 -0
- package/build/svelte/navigation-context.js +44 -0
- package/build/svelte/navigation-scope.svelte +24 -0
- package/build/svelte/navigation-scope.svelte.d.ts +9 -0
- package/build/svelte/registry-host.d.ts +1 -0
- package/build/svelte/registry-host.js +26 -0
- package/build/svelte/runes/index.d.ts +12 -0
- package/build/svelte/runes/index.js +12 -0
- package/build/svelte/runes/use-drawer-navigation.svelte.d.ts +6 -0
- package/build/svelte/runes/use-drawer-navigation.svelte.js +19 -0
- package/build/svelte/runes/use-focus-effect.svelte.d.ts +1 -0
- package/build/svelte/runes/use-focus-effect.svelte.js +31 -0
- package/build/svelte/runes/use-is-focused.svelte.d.ts +3 -0
- package/build/svelte/runes/use-is-focused.svelte.js +34 -0
- package/build/svelte/runes/use-navigation-state.svelte.d.ts +4 -0
- package/build/svelte/runes/use-navigation-state.svelte.js +39 -0
- package/build/svelte/runes/use-navigation.svelte.d.ts +9 -0
- package/build/svelte/runes/use-navigation.svelte.js +22 -0
- package/build/svelte/runes/use-route.svelte.d.ts +4 -0
- package/build/svelte/runes/use-route.svelte.js +12 -0
- package/build/svelte/runes/use-stack-navigation.svelte.d.ts +6 -0
- package/build/svelte/runes/use-stack-navigation.svelte.js +22 -0
- package/build/svelte/runes/use-tab-navigation.svelte.d.ts +6 -0
- package/build/svelte/runes/use-tab-navigation.svelte.js +19 -0
- package/build/svelte/screen-props.d.ts +21 -0
- package/build/svelte/screen-props.js +6 -0
- package/build/svelte/screen-registry.d.ts +17 -0
- package/build/svelte/screen-registry.js +57 -0
- package/build/svelte/screen.svelte +28 -0
- package/build/svelte/screen.svelte.d.ts +4 -0
- package/build/svelte/stack/index.d.ts +14 -0
- package/build/svelte/stack/index.js +7 -0
- package/build/svelte/stack/index.svelte +224 -0
- package/build/svelte/stack/index.svelte.d.ts +15 -0
- package/build/svelte/stack/stack-props.d.ts +20 -0
- package/build/svelte/stack/stack-props.js +4 -0
- package/build/svelte/stack/stack-screen.svelte +148 -0
- package/build/svelte/stack/stack-screen.svelte.d.ts +4 -0
- package/build/svelte/tab-screen-props.d.ts +13 -0
- package/build/svelte/tab-screen-props.js +4 -0
- package/build/svelte/tab-screen.svelte +26 -0
- package/build/svelte/tab-screen.svelte.d.ts +4 -0
- package/build/svelte/tabs/index.d.ts +8 -0
- package/build/svelte/tabs/index.js +6 -0
- package/build/svelte/tabs/index.svelte +247 -0
- package/build/svelte/tabs/index.svelte.d.ts +8 -0
- package/build/svelte/tabs/tab-props.d.ts +7 -0
- package/build/svelte/tabs/tab-props.js +3 -0
- package/build/vue/drawer/index.js +6 -5
- package/build/vue/stack/index.js +26 -12
- package/build/vue/tabs/index.js +48 -35
- package/build-ngc/angular/drawer/index.js +9 -7
- package/build-ngc/angular/drawer/index.js.map +1 -1
- package/build-ngc/angular/drawer-screen.directive.js +3 -3
- package/build-ngc/angular/navigation-context.service.js +3 -3
- package/build-ngc/angular/navigation-scope.directive.js +3 -3
- package/build-ngc/angular/screen.directive.js +3 -3
- package/build-ngc/angular/search-bar-ref.directive.js +3 -3
- package/build-ngc/angular/stack/index.d.ts +2 -0
- package/build-ngc/angular/stack/index.js +26 -6
- package/build-ngc/angular/stack/index.js.map +1 -1
- package/build-ngc/angular/tab-screen.directive.js +3 -3
- package/build-ngc/angular/tabs/index.d.ts +1 -1
- package/build-ngc/angular/tabs/index.js +23 -10
- package/build-ngc/angular/tabs/index.js.map +1 -1
- package/build-ngc/core/index.d.ts +2 -1
- package/build-ngc/core/index.js +5 -1
- package/build-ngc/core/index.js.map +1 -1
- package/build-ngc/core/stack-router-state/index.d.ts +2 -0
- package/build-ngc/core/stack-router-state/index.js +45 -0
- package/build-ngc/core/stack-router-state/index.js.map +1 -0
- package/build-ngc/core/tab-router-state/index.d.ts +1 -0
- package/build-ngc/core/tab-router-state/index.js +19 -0
- package/build-ngc/core/tab-router-state/index.js.map +1 -1
- package/package.json +28 -14
- package/src/angular/drawer/index.ts +6 -4
- package/src/angular/stack/index.ts +23 -2
- package/src/angular/tabs/index.ts +19 -5
- package/src/core/index.ts +11 -1
- package/src/core/stack-router-state/index.ts +49 -0
- package/src/core/tab-router-state/index.ts +25 -0
- package/src/react/drawer/index.ts +28 -13
- package/src/react/stack/index.ts +15 -1
- package/src/react/tabs/index.ts +43 -10
- package/src/svelte/attachments.ts +68 -0
- package/src/svelte/descriptor-subtree.ts +49 -0
- package/src/svelte/drawer/drawer-props.ts +28 -0
- package/src/svelte/drawer/index.svelte +421 -0
- package/src/svelte/drawer/index.ts +10 -0
- package/src/svelte/drawer-screen-props.ts +20 -0
- package/src/svelte/drawer-screen.svelte +24 -0
- package/src/svelte/fabric-tree.test-helper.ts +61 -0
- package/src/svelte/index.ts +71 -0
- package/src/svelte/linking.svelte.ts +66 -0
- package/src/svelte/navigation-context.ts +62 -0
- package/src/svelte/navigation-scope.svelte +24 -0
- package/src/svelte/registry-host.ts +26 -0
- package/src/svelte/runes/index.ts +17 -0
- package/src/svelte/runes/use-drawer-navigation.svelte.ts +29 -0
- package/src/svelte/runes/use-focus-effect.svelte.ts +35 -0
- package/src/svelte/runes/use-is-focused.svelte.ts +38 -0
- package/src/svelte/runes/use-navigation-state.svelte.ts +46 -0
- package/src/svelte/runes/use-navigation.svelte.ts +39 -0
- package/src/svelte/runes/use-route.svelte.ts +16 -0
- package/src/svelte/runes/use-stack-navigation.svelte.ts +32 -0
- package/src/svelte/runes/use-tab-navigation.svelte.ts +29 -0
- package/src/svelte/screen-props.ts +45 -0
- package/src/svelte/screen-registry.ts +92 -0
- package/src/svelte/screen.svelte +28 -0
- package/src/svelte/stack/index.svelte +224 -0
- package/src/svelte/stack/index.ts +11 -0
- package/src/svelte/stack/stack-props.ts +31 -0
- package/src/svelte/stack/stack-screen.svelte +148 -0
- package/src/svelte/svelte-ambient.d.ts +6 -0
- package/src/svelte/svelte-compile.test-helper.ts +143 -0
- package/src/svelte/tab-screen-props.ts +20 -0
- package/src/svelte/tab-screen.svelte +26 -0
- package/src/svelte/tabs/index.svelte +247 -0
- package/src/svelte/tabs/index.ts +10 -0
- package/src/svelte/tabs/tab-props.ts +13 -0
- package/src/vue/drawer/index.ts +6 -5
- package/src/vue/stack/index.ts +28 -11
- package/src/vue/tabs/index.ts +68 -65
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// Stack, the Svelte lifecycle half. The route-stack transitions (navigator-state) and the
|
|
3
|
+
// options/props folds (screen-options, render-stack) live in @symbiote-native/navigation core,
|
|
4
|
+
// shared verbatim with the React/Vue/Angular entries; here Svelte supplies the lifecycle -
|
|
5
|
+
// `$state.raw` for the pushed-route stack (its twin of useReducer: reassign from the same pure
|
|
6
|
+
// reducer), a module counter for route-key generation, `export function`s for the push/pop/
|
|
7
|
+
// replace navigator handle (Svelte's twin of Vue's expose(), reachable via `bind:this`) - plus
|
|
8
|
+
// the react-native-screens chrome per route (stack-screen.svelte). Pushing/popping a route is
|
|
9
|
+
// an ordinary child mount/unmount: RNSScreenStack diffs its RNSScreen children natively, so no
|
|
10
|
+
// imperative native command is needed here at all. Neither this nor the Screen marker imports
|
|
11
|
+
// react-native-screens' own React components (ScreenStack.tsx et al - hooks, crashes a
|
|
12
|
+
// non-React adapter); the native views are driven directly through the ViewConfig ../../register
|
|
13
|
+
// registers. See CLAUDE.md <third_party_rn_packages_are_react_only>.
|
|
14
|
+
//
|
|
15
|
+
// HOW SCREENS ARE DISCOVERED: not by reading `children`, which Svelte hands over as an opaque
|
|
16
|
+
// Snippet - the markers register themselves through the context collector (../screen-registry.ts)
|
|
17
|
+
// while the snippet renders, and the route list derives from that. The snippet is rendered
|
|
18
|
+
// inside a collapsed `symbiote-text` (../registry-host.ts) so whitespace between two markers
|
|
19
|
+
// can never become an illegal raw-text child of a native view.
|
|
20
|
+
|
|
21
|
+
// Route keys must be unique per Stack INSTANCE (Svelte has no useId); a module counter is the
|
|
22
|
+
// same mechanism the Angular entry uses.
|
|
23
|
+
let stackInstanceCounter = 0;
|
|
24
|
+
|
|
25
|
+
const STACK_ROOT_PROPS: Record<string, unknown> = { style: { flex: 1 } };
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script lang="ts">
|
|
29
|
+
import type { Component } from 'svelte';
|
|
30
|
+
import { dlog } from '@symbiote-native/engine';
|
|
31
|
+
import {
|
|
32
|
+
NAVIGATION_EVENT_STATE,
|
|
33
|
+
RNS_SCREEN_STACK_VIEW_NAME,
|
|
34
|
+
STACK_ON_FINISH_TRANSITIONING,
|
|
35
|
+
createInitialNavigatorState,
|
|
36
|
+
createNavigationEmitter,
|
|
37
|
+
navigatorReducer,
|
|
38
|
+
reconcileStackRoutes,
|
|
39
|
+
resolveStackProps,
|
|
40
|
+
} from '../../core';
|
|
41
|
+
import type {
|
|
42
|
+
INavigationEmitter,
|
|
43
|
+
INavigatorAction,
|
|
44
|
+
INavigatorHandle,
|
|
45
|
+
INavigatorState,
|
|
46
|
+
IRoute,
|
|
47
|
+
} from '../../core';
|
|
48
|
+
import { hostProps } from '../attachments';
|
|
49
|
+
import { getNavigationScope } from '../navigation-context';
|
|
50
|
+
import { SCREEN_REGISTRY_HOST_PROPS } from '../registry-host';
|
|
51
|
+
import { setScreenCollector, toRegistry, withoutScreen } from '../screen-registry';
|
|
52
|
+
import type { IRegisteredScreen } from '../screen-registry';
|
|
53
|
+
import type { IScreenProps, ISvelteScreenOptions } from '../screen-props';
|
|
54
|
+
import StackScreen from './stack-screen.svelte';
|
|
55
|
+
import type { IStackProps } from './stack-props';
|
|
56
|
+
|
|
57
|
+
let { initialRouteName, screenOptions, children }: IStackProps = $props();
|
|
58
|
+
|
|
59
|
+
// Read BEFORE this Stack establishes its own per-screen NavigationScope - becomes the `parent`
|
|
60
|
+
// link a nested screen's useNavigation().getParent() walks (e.g. this Stack rendered as a Tab
|
|
61
|
+
// screen's content reaches that Tab via this value). undefined when this Stack is the nesting
|
|
62
|
+
// root. Kept as the BOX, not its unwrapped value, so every read below sees the current one.
|
|
63
|
+
const parentScope = getNavigationScope();
|
|
64
|
+
|
|
65
|
+
const routeIdPrefix = `stack-${(stackInstanceCounter += 1)}`;
|
|
66
|
+
|
|
67
|
+
// `$state.raw` + reassignment, never a mutated array: an entry holds a component reference (a
|
|
68
|
+
// plain function) behind live getters, and a deep reactive proxy would wrap both.
|
|
69
|
+
let screens = $state.raw<IRegisteredScreen<IScreenProps['options']>[]>([]);
|
|
70
|
+
setScreenCollector<IScreenProps['options']>({
|
|
71
|
+
kind: 'stack',
|
|
72
|
+
register: screen => {
|
|
73
|
+
screens = [...screens, screen];
|
|
74
|
+
},
|
|
75
|
+
unregister: screen => {
|
|
76
|
+
screens = withoutScreen(screens, screen);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const registry = $derived(toRegistry(screens));
|
|
81
|
+
|
|
82
|
+
let routeSequence = 0;
|
|
83
|
+
function createRoute(name: string, params: unknown): IRoute<unknown> {
|
|
84
|
+
routeSequence += 1;
|
|
85
|
+
return { key: `${routeIdPrefix}-${name}-${routeSequence}`, name, params };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The pushed stack only exists once something has dispatched; until then the state derives from
|
|
89
|
+
// whatever the markers registered. `seededState` is a PLAIN local (not `$state`), so memoizing
|
|
90
|
+
// it inside the derivation below is not a state write during a derivation - it just makes the
|
|
91
|
+
// one route-key-allocating call happen exactly once, no matter how often the derivation re-runs
|
|
92
|
+
// as markers register.
|
|
93
|
+
let pushedState = $state.raw<INavigatorState | null>(null);
|
|
94
|
+
let seededState: INavigatorState | undefined;
|
|
95
|
+
|
|
96
|
+
function seedState(): INavigatorState {
|
|
97
|
+
if (seededState !== undefined) return seededState;
|
|
98
|
+
const startRouteName = initialRouteName ?? registry.keys().next().value;
|
|
99
|
+
if (startRouteName === undefined) {
|
|
100
|
+
dlog('Stack: no <Stack.Screen> children registered');
|
|
101
|
+
// Deliberately NOT memoized: markers may still be registering, and the next derivation run
|
|
102
|
+
// should get a real initial route rather than being stuck on this placeholder.
|
|
103
|
+
return createInitialNavigatorState({ key: routeIdPrefix, name: '', params: undefined });
|
|
104
|
+
}
|
|
105
|
+
seededState = createInitialNavigatorState(
|
|
106
|
+
createRoute(startRouteName, registry.get(startRouteName)?.initialParams),
|
|
107
|
+
);
|
|
108
|
+
return seededState;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A `<Stack.Screen>` marker can unregister while its route is still in the pushed history (a
|
|
112
|
+
// marker behind an `{#if}`, a data-driven screen list), which would leave that entry with nothing
|
|
113
|
+
// for componentFor() to mount (reconcileStackRoutes' header). The reconciliation belongs in the
|
|
114
|
+
// derivation because it is PURE - it hands back the same state reference when nothing changed, so
|
|
115
|
+
// it writes no state and cannot re-trigger itself; `dispatch` below then persists the pruned
|
|
116
|
+
// history, since it reduces over this value rather than over `pushedState`.
|
|
117
|
+
const state = $derived(reconcileStackRoutes(pushedState ?? seedState(), [...registry.keys()]));
|
|
118
|
+
|
|
119
|
+
function dispatch(action: INavigatorAction): void {
|
|
120
|
+
pushedState = navigatorReducer(state, action);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// One emitter per route.key, created lazily the first time a route is rendered and pruned once
|
|
124
|
+
// it is popped off the stack (the broadcast effect below).
|
|
125
|
+
const emitters = new Map<string, INavigationEmitter>();
|
|
126
|
+
function emitterFor(routeKey: string): INavigationEmitter {
|
|
127
|
+
let emitter = emitters.get(routeKey);
|
|
128
|
+
if (emitter === undefined) {
|
|
129
|
+
emitter = createNavigationEmitter();
|
|
130
|
+
emitters.set(routeKey, emitter);
|
|
131
|
+
}
|
|
132
|
+
return emitter;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function push(name: string, params?: unknown): void {
|
|
136
|
+
dispatch({ type: 'push', route: createRoute(name, params) });
|
|
137
|
+
}
|
|
138
|
+
export function pop(count?: number): void {
|
|
139
|
+
dispatch({ type: 'pop', count });
|
|
140
|
+
}
|
|
141
|
+
export function popToTop(): void {
|
|
142
|
+
dispatch({ type: 'popToTop' });
|
|
143
|
+
}
|
|
144
|
+
export function popTo(key: string): void {
|
|
145
|
+
dispatch({ type: 'popTo', key });
|
|
146
|
+
}
|
|
147
|
+
export function replace(name: string, params?: unknown): void {
|
|
148
|
+
dispatch({ type: 'replace', route: createRoute(name, params) });
|
|
149
|
+
}
|
|
150
|
+
export function setParams(params: unknown, key?: string): void {
|
|
151
|
+
dispatch({ type: 'setParams', key, params });
|
|
152
|
+
}
|
|
153
|
+
export function reset(nextState: INavigatorState): void {
|
|
154
|
+
dispatch({ type: 'reset', state: nextState });
|
|
155
|
+
}
|
|
156
|
+
export function canGoBack(): boolean {
|
|
157
|
+
return state.routes.length > 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The same handle, as a value, for everything that needs it INSIDE this component (the options
|
|
161
|
+
// fold's `navigation` argument, every route's NavigationScope). `bind:this` exposes the
|
|
162
|
+
// `export function`s above; this object is the internal twin of that surface.
|
|
163
|
+
const handle: INavigatorHandle = {
|
|
164
|
+
push,
|
|
165
|
+
pop,
|
|
166
|
+
popToTop,
|
|
167
|
+
popTo,
|
|
168
|
+
replace,
|
|
169
|
+
setParams,
|
|
170
|
+
reset,
|
|
171
|
+
canGoBack,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function popOne(): void {
|
|
175
|
+
dispatch({ type: 'pop', count: 1 });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Broadcasts the router state to every still-live route's emitter (useNavigationState's source)
|
|
179
|
+
// after each commit, and prunes emitters for routes popped off the stack. Runs as an $effect
|
|
180
|
+
// rather than inline in the render path (Vue's shape), so the first broadcast lands AFTER the
|
|
181
|
+
// initial route's own screen has mounted and subscribed.
|
|
182
|
+
$effect(() => {
|
|
183
|
+
const current = state;
|
|
184
|
+
for (const route of current.routes) {
|
|
185
|
+
emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, current);
|
|
186
|
+
}
|
|
187
|
+
for (const routeKey of [...emitters.keys()]) {
|
|
188
|
+
if (!current.routes.some(route => route.key === routeKey)) emitters.delete(routeKey);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Investigation instrumentation (flicker-on-focus bug): STACK_ON_FINISH_TRANSITIONING is the
|
|
193
|
+
// native signal that the WHOLE push/pop animation has finished (as opposed to onAppear/
|
|
194
|
+
// onDisappear, which are per-screen) - logging it lets the per-screen appear/disappear
|
|
195
|
+
// timestamps be checked against the actual transition-complete moment. Kept behind DEBUG,
|
|
196
|
+
// never removed.
|
|
197
|
+
const stackProps = resolveStackProps({
|
|
198
|
+
passthrough: {
|
|
199
|
+
[STACK_ON_FINISH_TRANSITIONING]: () => dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
function optionsFor(route: IRoute<unknown>): ISvelteScreenOptions {
|
|
204
|
+
const entry = registry.get(route.name);
|
|
205
|
+
const own =
|
|
206
|
+
entry === undefined
|
|
207
|
+
? undefined
|
|
208
|
+
: typeof entry.options === 'function'
|
|
209
|
+
? entry.options({ route, navigation: handle })
|
|
210
|
+
: entry.options;
|
|
211
|
+
return { ...screenOptions, ...own };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function componentFor(route: IRoute<unknown>): Component | undefined {
|
|
215
|
+
const entry = registry.get(route.name);
|
|
216
|
+
if (entry === undefined) {
|
|
217
|
+
dlog(`Stack: no screen registered for route name "${route.name}"`);
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return entry.component;
|
|
221
|
+
}
|
|
222
|
+
</script>
|
|
223
|
+
|
|
224
|
+
<symbiote-view p={STACK_ROOT_PROPS}><symbiote-text p={SCREEN_REGISTRY_HOST_PROPS}>{@render children?.()}</symbiote-text><svelte:element this={RNS_SCREEN_STACK_VIEW_NAME} {@attach hostProps(stackProps)}>{#each state.routes as route, index (route.key)}{@const screenComponent = componentFor(route)}{#if screenComponent !== undefined}<StackScreen {route} {index} routeCount={state.routes.length} options={optionsFor(route)} navigation={handle} emitter={emitterFor(route.key)} parentScope={parentScope?.current} component={screenComponent} onPopRequested={popOne} />{/if}{/each}</svelte:element></symbiote-view>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `Stack.Screen` alongside the standalone `Screen` export, matching React's and Vue's barrels.
|
|
2
|
+
// A compiled Svelte component is an ordinary function object, so attaching the marker to it is
|
|
3
|
+
// the same Object.assign the other entries do - and `<Stack.Screen />` resolves through Svelte's
|
|
4
|
+
// member-expression component tags (verified against the real compiler).
|
|
5
|
+
import StackImpl from './index.svelte';
|
|
6
|
+
import Screen from '../screen.svelte';
|
|
7
|
+
|
|
8
|
+
export const Stack = Object.assign(StackImpl, { Screen });
|
|
9
|
+
|
|
10
|
+
export type { INavigatorHandle } from '../../core';
|
|
11
|
+
export type { IStackProps } from './stack-props';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Stack's own public prop surface, and the internal per-route prop surface stack-screen.svelte
|
|
2
|
+
// takes. Both live in a plain `.ts` file for the tsc-cannot-see-a-`.svelte`-named-export reason
|
|
3
|
+
// ../screen-props.ts documents.
|
|
4
|
+
|
|
5
|
+
import type { Component, Snippet } from 'svelte';
|
|
6
|
+
import type { INavigationEmitter, INavigatorHandle, IRoute } from '../../core';
|
|
7
|
+
import type { INavigationScopeValue } from '../navigation-context';
|
|
8
|
+
import type { ISvelteScreenOptions } from '../screen-props';
|
|
9
|
+
|
|
10
|
+
// React's `children?: ReactNode` / Vue's default slot become a Svelte Snippet holding the
|
|
11
|
+
// `<Stack.Screen>` markers.
|
|
12
|
+
export type IStackProps = {
|
|
13
|
+
initialRouteName?: string;
|
|
14
|
+
screenOptions?: ISvelteScreenOptions;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type IStackScreenProps = {
|
|
19
|
+
route: IRoute<unknown>;
|
|
20
|
+
index: number;
|
|
21
|
+
routeCount: number;
|
|
22
|
+
options: ISvelteScreenOptions;
|
|
23
|
+
navigation: INavigatorHandle;
|
|
24
|
+
emitter: INavigationEmitter;
|
|
25
|
+
// The ambient scope the Stack itself read on mount (undefined at the nesting root) - becomes
|
|
26
|
+
// the `parent` link a nested screen's useNavigation().getParent() walks.
|
|
27
|
+
parentScope: INavigationScopeValue | undefined;
|
|
28
|
+
component: Component;
|
|
29
|
+
// Fired by RNSScreen's own onDismissed / header back button; the Stack pops one route.
|
|
30
|
+
onPopRequested: () => void;
|
|
31
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { INavigatorPlatform } from '../../core';
|
|
3
|
+
|
|
4
|
+
// backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
|
|
5
|
+
// (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope, so a single
|
|
6
|
+
// constant stands in for the per-platform injection point ISliderPlatform-style adapters use
|
|
7
|
+
// elsewhere.
|
|
8
|
+
const NAVIGATOR_PLATFORM: INavigatorPlatform = { defaultHeaderBackTitleVisible: true };
|
|
9
|
+
|
|
10
|
+
// react-native-screens' RNSScreenStackHeaderConfig.mm requires every header child to be an
|
|
11
|
+
// RNSScreenStackHeaderSubview; `type: 'searchBar'` is how it knows which slot this one fills.
|
|
12
|
+
const HEADER_SUBVIEW_PROPS: Record<string, unknown> = { type: 'searchBar' };
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
// One mounted route's native chrome. Split out of index.svelte so each route owns its own plan
|
|
17
|
+
// derivation and its own attachments, torn down by an ordinary component unmount when the route
|
|
18
|
+
// is popped - the Svelte equivalent of Vue's per-route render-loop closure.
|
|
19
|
+
//
|
|
20
|
+
// Every react-native-screens view here goes through `<svelte:element this={'RNSScreen'}>`
|
|
21
|
+
// rather than a literal tag: their Fabric names are capitalized and un-hyphenated, so a literal
|
|
22
|
+
// tag would parse as a COMPONENT reference in a Svelte template. Their props ride an
|
|
23
|
+
// `{@attach hostProps(...)}` attachment rather than an attribute, because a dynamic tag
|
|
24
|
+
// compiles through Svelte's generic setAttribute path and never the custom-element property-SET
|
|
25
|
+
// path the object bag depends on - see ../attachments.ts for the full reasoning.
|
|
26
|
+
//
|
|
27
|
+
// The whole per-route tree is packed edge-to-edge with zero whitespace between sibling tags:
|
|
28
|
+
// svelte-adapter-dom-shim skill §16, where a stray space would become a real RCTRawText child
|
|
29
|
+
// of a react-native-screens view.
|
|
30
|
+
import { Platform, dlog } from '@symbiote-native/engine';
|
|
31
|
+
import {
|
|
32
|
+
NAVIGATION_EVENT_BLUR,
|
|
33
|
+
NAVIGATION_EVENT_FOCUS,
|
|
34
|
+
RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
|
|
35
|
+
RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME,
|
|
36
|
+
RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME,
|
|
37
|
+
RNS_SCREEN_STACK_VIEW_NAME,
|
|
38
|
+
RNS_SCREEN_VIEW_NAME,
|
|
39
|
+
RNS_SEARCH_BAR_VIEW_NAME,
|
|
40
|
+
SCREEN_ON_APPEAR,
|
|
41
|
+
SCREEN_ON_DISAPPEAR,
|
|
42
|
+
SCREEN_ON_DISMISSED,
|
|
43
|
+
SCREEN_ON_HEADER_BACK_BUTTON_CLICKED,
|
|
44
|
+
SCREEN_ON_WILL_APPEAR,
|
|
45
|
+
SCREEN_ON_WILL_DISAPPEAR,
|
|
46
|
+
buildSearchBarPassthrough,
|
|
47
|
+
resolveScreenRenderPlan,
|
|
48
|
+
} from '../../core';
|
|
49
|
+
import type { IScreenRenderPlan } from '../../core';
|
|
50
|
+
import { hostProps, searchBarRef } from '../attachments';
|
|
51
|
+
import NavigationScope from '../navigation-scope.svelte';
|
|
52
|
+
import type { INavigationScopeValue } from '../navigation-context';
|
|
53
|
+
import type { IStackScreenProps } from './stack-props';
|
|
54
|
+
|
|
55
|
+
let {
|
|
56
|
+
route,
|
|
57
|
+
index,
|
|
58
|
+
routeCount,
|
|
59
|
+
options,
|
|
60
|
+
navigation,
|
|
61
|
+
emitter,
|
|
62
|
+
parentScope,
|
|
63
|
+
// Destructured under a capitalized name because a Svelte template resolves a component tag
|
|
64
|
+
// only from a capitalized identifier.
|
|
65
|
+
component: ScreenComponent,
|
|
66
|
+
onPopRequested,
|
|
67
|
+
}: IStackScreenProps = $props();
|
|
68
|
+
|
|
69
|
+
const searchBarOptions = $derived(options.headerSearchBarOptions);
|
|
70
|
+
|
|
71
|
+
const plan = $derived.by<IScreenRenderPlan>(() =>
|
|
72
|
+
resolveScreenRenderPlan({
|
|
73
|
+
screenId: route.key,
|
|
74
|
+
index,
|
|
75
|
+
routeCount,
|
|
76
|
+
options,
|
|
77
|
+
platform: NAVIGATOR_PLATFORM,
|
|
78
|
+
isAndroid: Platform.OS === 'android',
|
|
79
|
+
screenPassthrough: {
|
|
80
|
+
[SCREEN_ON_DISMISSED]: onPopRequested,
|
|
81
|
+
[SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: onPopRequested,
|
|
82
|
+
// onAppear/onDisappear are the definitive visibility boundary (post-transition-
|
|
83
|
+
// animation), so 'focus'/'blur' fire exactly once per transition; onWillAppear/
|
|
84
|
+
// onWillDisappear fire BEFORE the animation runs, so wiring them to emit() too would
|
|
85
|
+
// double-invoke useFocusEffect per transition - they only get a debug log here.
|
|
86
|
+
[SCREEN_ON_WILL_APPEAR]: () =>
|
|
87
|
+
dlog(`Stack: route "${route.name}" will appear at t=${Date.now()}`),
|
|
88
|
+
[SCREEN_ON_APPEAR]: () => {
|
|
89
|
+
dlog(`Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`);
|
|
90
|
+
emitter.emit(NAVIGATION_EVENT_FOCUS);
|
|
91
|
+
},
|
|
92
|
+
[SCREEN_ON_WILL_DISAPPEAR]: () =>
|
|
93
|
+
dlog(`Stack: route "${route.name}" will disappear at t=${Date.now()}`),
|
|
94
|
+
[SCREEN_ON_DISAPPEAR]: () => {
|
|
95
|
+
dlog(`Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`);
|
|
96
|
+
emitter.emit(NAVIGATION_EVENT_BLUR);
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
// The imperative SearchBarCommands ref rides its OWN attachment on the RNSSearchBar leaf
|
|
100
|
+
// below (../attachments.ts), never this passthrough map - same split Angular uses, so no
|
|
101
|
+
// `ref` key can leak through to Fabric as a real prop.
|
|
102
|
+
searchBarPassthrough: searchBarOptions
|
|
103
|
+
? buildSearchBarPassthrough(searchBarOptions, message =>
|
|
104
|
+
dlog(`Stack: route "${route.name}" ${message}`),
|
|
105
|
+
)
|
|
106
|
+
: undefined,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Investigation instrumentation (flicker-on-focus bug): the actual timing/z-order-relevant
|
|
111
|
+
// values resolved onto the native RNSScreen, once per mounted route - rules a stackAnimation/
|
|
112
|
+
// transitionDuration mismatch against react-native-screens' own native default in or out. Kept
|
|
113
|
+
// behind DEBUG, never removed.
|
|
114
|
+
let hasLoggedScreenProps = false;
|
|
115
|
+
$effect(() => {
|
|
116
|
+
const screenProps = plan.screenProps;
|
|
117
|
+
if (hasLoggedScreenProps) return;
|
|
118
|
+
hasLoggedScreenProps = true;
|
|
119
|
+
dlog(
|
|
120
|
+
`Stack: route "${route.name}" resolved screen props ` +
|
|
121
|
+
`stackAnimation=${String(screenProps.stackAnimation)} ` +
|
|
122
|
+
`stackPresentation=${String(screenProps.stackPresentation)} ` +
|
|
123
|
+
`transitionDuration=${String(screenProps.transitionDuration)} ` +
|
|
124
|
+
`gestureEnabled=${String(screenProps.gestureEnabled)} at t=${Date.now()}`,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
|
|
129
|
+
// RNSScreenStack/RNSScreen purely to host the native header bar (see isHeaderInModal's comment
|
|
130
|
+
// in core/render-stack.ts). Skipping this leaves RNSScreenStackHeaderConfig with no navigation
|
|
131
|
+
// controller to attach to, so the header silently never renders. activityState mirrors the
|
|
132
|
+
// outer screen's own value - RNSScreen.mm treats an unset/inactive nested screen as not yet
|
|
133
|
+
// pushed, leaving it parked at its pre-push transition position.
|
|
134
|
+
const innerStackProps = $derived<Record<string, unknown>>({ style: plan.innerStackStyle });
|
|
135
|
+
const innerScreenProps = $derived<Record<string, unknown>>({
|
|
136
|
+
style: plan.innerScreenStyle,
|
|
137
|
+
activityState: plan.activityState,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const scopeValue = $derived<INavigationScopeValue>({
|
|
141
|
+
route,
|
|
142
|
+
navigation,
|
|
143
|
+
emitter,
|
|
144
|
+
parent: parentScope,
|
|
145
|
+
});
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
{#snippet chrome()}<svelte:element this={RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME} {@attach hostProps(plan.headerConfig.props)}>{#if plan.searchBarProps !== undefined}<svelte:element this={RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME} {@attach hostProps(HEADER_SUBVIEW_PROPS)}><svelte:element this={RNS_SEARCH_BAR_VIEW_NAME} {@attach hostProps(plan.searchBarProps)} {@attach searchBarRef(searchBarOptions?.ref)}></svelte:element></svelte:element>{/if}</svelte:element><svelte:element this={RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME} {@attach hostProps(plan.contentWrapperProps)}><NavigationScope value={scopeValue}><ScreenComponent /></NavigationScope></svelte:element>{/snippet}<svelte:element this={plan.screenViewName} {@attach hostProps(plan.screenProps)}>{#if plan.inModal}<svelte:element this={RNS_SCREEN_STACK_VIEW_NAME} {@attach hostProps(innerStackProps)}><svelte:element this={RNS_SCREEN_VIEW_NAME} {@attach hostProps(innerScreenProps)}>{@render chrome()}</svelte:element></svelte:element>{:else}{@render chrome()}{/if}</svelte:element>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Pulls in svelte's ambient global declarations ($state/$effect/...) for this package's tsc
|
|
2
|
+
// program. adapters/svelte gets these for free because render.ts does a real value import from
|
|
3
|
+
// 'svelte' (any file in a program importing a module loads its ambient globals for the whole
|
|
4
|
+
// program); this package has no such import, so a triple-slash reference does the same job
|
|
5
|
+
// explicitly rather than relying on an incidental side-effect.
|
|
6
|
+
/// <reference types="svelte" />
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// The compile-and-load harness every `*.smoke.test.ts` in this folder uses.
|
|
2
|
+
//
|
|
3
|
+
// There is no `.svelte`-aware bundler wired into this repo's vitest (svelte-adapter-dom-shim
|
|
4
|
+
// skill §15, a deliberate choice), so a test that needs to RUN a component compiles the real
|
|
5
|
+
// source with `svelte/compiler` itself and dynamic-`import()`s the output. Three rules fall out
|
|
6
|
+
// of that, all of them learned the hard way in the adapter's own smokes:
|
|
7
|
+
//
|
|
8
|
+
// 1. The compiled file must sit NEXT TO the real source, because its own relative imports
|
|
9
|
+
// (`./stack-screen.svelte`, `../navigation-context`) resolve from wherever the compiled FILE
|
|
10
|
+
// lives, not from where the source did.
|
|
11
|
+
// 2. Every `.svelte` specifier inside the compiled output has to be rewritten to the compiled
|
|
12
|
+
// twin, recursively - a navigator pulls in stack-screen.svelte, which pulls in
|
|
13
|
+
// navigation-scope.svelte.
|
|
14
|
+
// 3. Node's `import()` caches by path, so re-writing new content to a path a previous test
|
|
15
|
+
// already imported hands back the STALE module. Every harness instance therefore stamps its
|
|
16
|
+
// own id into the filenames.
|
|
17
|
+
//
|
|
18
|
+
// `aliases` covers the one case relative rewriting cannot: `@symbiote-native/svelte`'s main
|
|
19
|
+
// barrel re-exports real `.svelte` sources, which Vite's plain (svelte-plugin-free) transform
|
|
20
|
+
// cannot parse at all - so a test that needs something from it points the specifier at a module
|
|
21
|
+
// it compiled itself instead.
|
|
22
|
+
|
|
23
|
+
import { compile } from 'svelte/compiler';
|
|
24
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
25
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { pathToFileURL } from 'node:url';
|
|
27
|
+
// The real Metro pipeline's own `.svelte.ts` compile step (TS-strip, then `compileModule` to
|
|
28
|
+
// desugar the runes). Reused rather than reimplemented, so the runes in ./runes are exercised
|
|
29
|
+
// through the ACTUAL shipped compile path - a bare, uncompiled `$state`/`$effect` call throws
|
|
30
|
+
// `rune_outside_svelte` at runtime. Default-imported because it is a `.cjs` module.
|
|
31
|
+
import metroSvelteTransformer from '@symbiote-native/svelte/metro-svelte-transformer';
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
compileSvelteModuleFile,
|
|
35
|
+
}: {
|
|
36
|
+
compileSvelteModuleFile: (source: string, filename: string) => string;
|
|
37
|
+
} = metroSvelteTransformer;
|
|
38
|
+
|
|
39
|
+
const COMPILE_OPTIONS = { generate: 'client', fragments: 'tree', css: 'external' } as const;
|
|
40
|
+
// The §16 tell: a single-space text entry inside a compiled `from_tree([...])` array, i.e. real
|
|
41
|
+
// whitespace between two sibling nodes that became a real RCTRawText engine node.
|
|
42
|
+
const STRAY_WHITESPACE_PATTERN = /,\s*'\s+'\s*,/g;
|
|
43
|
+
const SVELTE_SPECIFIER_PATTERN = /(['"])(\.{1,2}\/[^'"]*\.svelte)\1/g;
|
|
44
|
+
|
|
45
|
+
export type ISvelteHarness = {
|
|
46
|
+
// Compile a real `.svelte` file plus everything it imports; returns the compiled entry path.
|
|
47
|
+
compileFile(sveltePath: string): string;
|
|
48
|
+
// Compile an inline fixture written into `dir`, so its own relative imports resolve from there.
|
|
49
|
+
compileSource(dir: string, name: string, source: string): string;
|
|
50
|
+
// The number of stray single-space text entries across everything compiled so far - must be 0.
|
|
51
|
+
strayWhitespaceCount(): number;
|
|
52
|
+
cleanup(): void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function createSvelteHarness(
|
|
56
|
+
id: string,
|
|
57
|
+
aliases: Readonly<Record<string, string>> = {},
|
|
58
|
+
): ISvelteHarness {
|
|
59
|
+
const written: string[] = [];
|
|
60
|
+
const compiledBySource = new Map<string, string>();
|
|
61
|
+
let strayWhitespace = 0;
|
|
62
|
+
|
|
63
|
+
function outPathFor(sveltePath: string): string {
|
|
64
|
+
const stem = basename(sveltePath).replace(/\./g, '-');
|
|
65
|
+
return join(dirname(sveltePath), `.smoke-compiled-${id}-${stem}.mjs`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// `./` is prepended unless the path already walks up: every compiled file's basename STARTS
|
|
69
|
+
// with a dot (`.smoke-compiled-...`), so a bare "starts with '.'" check would leave it looking
|
|
70
|
+
// like a bare package specifier to the resolver.
|
|
71
|
+
function relativeSpecifier(fromFile: string, target: string): string {
|
|
72
|
+
const specifier = relative(dirname(fromFile), target);
|
|
73
|
+
return specifier.startsWith('..') ? specifier : `./${specifier}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function rewrite(code: string, outPath: string): string {
|
|
77
|
+
let rewritten = code.replace(
|
|
78
|
+
SVELTE_SPECIFIER_PATTERN,
|
|
79
|
+
(_match, quote: string, spec: string) => {
|
|
80
|
+
const childSource = resolve(dirname(outPath), spec);
|
|
81
|
+
const childOut = compileFile(childSource);
|
|
82
|
+
return `${quote}${relativeSpecifier(outPath, childOut)}${quote}`;
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
for (const [specifier, target] of Object.entries(aliases)) {
|
|
86
|
+
rewritten = rewritten.split(`'${specifier}'`).join(`'${relativeSpecifier(outPath, target)}'`);
|
|
87
|
+
rewritten = rewritten.split(`"${specifier}"`).join(`"${relativeSpecifier(outPath, target)}"`);
|
|
88
|
+
}
|
|
89
|
+
return rewritten;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function emit(sveltePath: string, source: string, isRuneModule: boolean): string {
|
|
93
|
+
const outPath = outPathFor(sveltePath);
|
|
94
|
+
// Registered BEFORE compiling so a cycle between two components terminates.
|
|
95
|
+
compiledBySource.set(sveltePath, outPath);
|
|
96
|
+
written.push(outPath);
|
|
97
|
+
const filename = basename(sveltePath);
|
|
98
|
+
const code = isRuneModule
|
|
99
|
+
? compileSvelteModuleFile(source, `${filename}.ts`)
|
|
100
|
+
: compile(source, { ...COMPILE_OPTIONS, filename }).js.code;
|
|
101
|
+
strayWhitespace += (code.match(STRAY_WHITESPACE_PATTERN) ?? []).length;
|
|
102
|
+
writeFileSync(outPath, rewrite(code, outPath));
|
|
103
|
+
return outPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A `./x.svelte` specifier is a COMPONENT when `x.svelte` exists on disk and a rune MODULE
|
|
107
|
+
// when `x.svelte.ts` does - the same two-way split the Metro transformer makes on the real
|
|
108
|
+
// filename.
|
|
109
|
+
function compileFile(sveltePath: string): string {
|
|
110
|
+
const cached = compiledBySource.get(sveltePath);
|
|
111
|
+
if (cached !== undefined) return cached;
|
|
112
|
+
const runeModulePath = `${sveltePath}.ts`;
|
|
113
|
+
if (!existsSync(sveltePath) && existsSync(runeModulePath)) {
|
|
114
|
+
return emit(sveltePath, readFileSync(runeModulePath, 'utf8'), true);
|
|
115
|
+
}
|
|
116
|
+
return emit(sveltePath, readFileSync(sveltePath, 'utf8'), false);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
compileFile,
|
|
121
|
+
compileSource(dir, name, source) {
|
|
122
|
+
return emit(join(dir, `${name}.svelte`), source, false);
|
|
123
|
+
},
|
|
124
|
+
strayWhitespaceCount: () => strayWhitespace,
|
|
125
|
+
cleanup() {
|
|
126
|
+
for (const file of written) rmSync(file, { force: true });
|
|
127
|
+
written.length = 0;
|
|
128
|
+
compiledBySource.clear();
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The compiled module's default export, narrowed without a cast so a test never silently mounts
|
|
134
|
+
// `undefined`.
|
|
135
|
+
export async function loadComponent(compiledPath: string): Promise<unknown> {
|
|
136
|
+
// pathToFileURL rather than a `file://${...}` template: the literal form makes Vite's import
|
|
137
|
+
// analyzer log an "Invalid file URL" warning for the un-substituted placeholder.
|
|
138
|
+
const mod: unknown = await import(pathToFileURL(compiledPath).href);
|
|
139
|
+
if (mod === null || typeof mod !== 'object' || !('default' in mod)) {
|
|
140
|
+
throw new Error(`${compiledPath} produced no default export`);
|
|
141
|
+
}
|
|
142
|
+
return mod.default;
|
|
143
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Tab.Screen's public prop surface - mirrors screen-props.ts minus the stack-only concepts (no
|
|
2
|
+
// push/pop lifecycle, no header search bar). Split out of `tab-screen.svelte` for the same
|
|
3
|
+
// tsc-cannot-see-a-`.svelte`-named-export reason screen-props.ts documents.
|
|
4
|
+
|
|
5
|
+
import type { Component } from 'svelte';
|
|
6
|
+
import type { IRoute, ITabNavigatorHandle, ITabOptions } from '../core';
|
|
7
|
+
|
|
8
|
+
export type ITabScreenOptionsArgs = {
|
|
9
|
+
route: IRoute<unknown>;
|
|
10
|
+
navigation: ITabNavigatorHandle;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ITabScreenOptionsResolver = (args: ITabScreenOptionsArgs) => ITabOptions;
|
|
14
|
+
|
|
15
|
+
export type ITabScreenProps = {
|
|
16
|
+
name: string;
|
|
17
|
+
component: Component;
|
|
18
|
+
options?: ITabOptions | ITabScreenOptionsResolver;
|
|
19
|
+
initialParams?: unknown;
|
|
20
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Tab.Screen: a declarative marker that paints nothing, registered through the same
|
|
3
|
+
// context-based collector Stack.Screen uses (screen-registry.ts). The `'tab'` kind tag is what
|
|
4
|
+
// stops it from silently registering with an ancestor Stack/Drawer when it is written in the
|
|
5
|
+
// wrong place. See CLAUDE.md <third_party_rn_packages_are_react_only> - this and Tab import
|
|
6
|
+
// nothing from react-native.
|
|
7
|
+
import { collectScreen } from './screen-registry';
|
|
8
|
+
import type { ITabScreenProps } from './tab-screen-props';
|
|
9
|
+
|
|
10
|
+
let { name, component, options, initialParams }: ITabScreenProps = $props();
|
|
11
|
+
|
|
12
|
+
collectScreen('tab', {
|
|
13
|
+
get name(): ITabScreenProps['name'] {
|
|
14
|
+
return name;
|
|
15
|
+
},
|
|
16
|
+
get component(): ITabScreenProps['component'] {
|
|
17
|
+
return component;
|
|
18
|
+
},
|
|
19
|
+
get options(): ITabScreenProps['options'] {
|
|
20
|
+
return options;
|
|
21
|
+
},
|
|
22
|
+
get initialParams(): unknown {
|
|
23
|
+
return initialParams;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
</script>
|