@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,421 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// Drawer, the Svelte lifecycle half. The open/closed + focused-route router
|
|
3
|
+
// (drawer-router-state) and the pure swipe/geometry math (drawer-options) live in
|
|
4
|
+
// @symbiote-native/navigation core, shared verbatim with the React/Vue/Angular entries; here
|
|
5
|
+
// Svelte supplies the lifecycle - `$state.raw` for the router (its twin of useReducer), a
|
|
6
|
+
// PanResponder for the swipe gesture (built ONCE, its callbacks reading options and open-state
|
|
7
|
+
// LIVE off `$props()`/`$state` at gesture time), an AnimatedValue driving the slide/opacity
|
|
8
|
+
// transforms, and `export function`s for the open/close/toggle/jumpTo handle - mirroring
|
|
9
|
+
// tabs/index.svelte's shape (Tab is the closer sibling: both are fixed-route-list,
|
|
10
|
+
// no-react-native-screens navigators; Stack's push/pop + native-screen bridging don't apply).
|
|
11
|
+
//
|
|
12
|
+
// FEASIBILITY NOTE (mirrored from the React/Vue twins): the REAL @react-navigation/drawer is
|
|
13
|
+
// built on react-native-gesture-handler + react-native-reanimated, neither of which this
|
|
14
|
+
// codebase depends on. What's built here reaches the same swipe-to-open/close +
|
|
15
|
+
// front/back/slide/permanent behavior using only PanResponder + Animated (both already in
|
|
16
|
+
// @symbiote-native/engine), which is sufficient for a solid drawer but NOT byte-for-byte parity
|
|
17
|
+
// - see the explicit gap list at the bottom of this file (verbatim from the React twin - the
|
|
18
|
+
// gaps are architectural, not adapter-specific).
|
|
19
|
+
let drawerInstanceCounter = 0;
|
|
20
|
+
|
|
21
|
+
const DRAWER_SNAP_DURATION = 250;
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
import type { Component } from 'svelte';
|
|
26
|
+
import { onDestroy, tick } from 'svelte';
|
|
27
|
+
import { AnimatedValue, Dimensions, PanResponder, dlog, timing } from '@symbiote-native/engine';
|
|
28
|
+
import type { IPanResponderGestureState, ISymbioteEvent } from '@symbiote-native/engine';
|
|
29
|
+
// The ONLY import from the adapter's main barrel: `Animated.View` is a compiled `.svelte`
|
|
30
|
+
// component, so unlike PanResponder/Dimensions/AnimatedValue (pure engine re-exports, taken
|
|
31
|
+
// from the engine directly here) it has no `.svelte`-free home to import from.
|
|
32
|
+
import { Animated } from '@symbiote-native/svelte';
|
|
33
|
+
import {
|
|
34
|
+
DRAWER_DEFAULT_OVERLAY_COLOR,
|
|
35
|
+
NAVIGATION_EVENT_BLUR,
|
|
36
|
+
NAVIGATION_EVENT_FOCUS,
|
|
37
|
+
createInitialDrawerRouterState,
|
|
38
|
+
createNavigationEmitter,
|
|
39
|
+
diffFocusedRoute,
|
|
40
|
+
drawerChildOrder,
|
|
41
|
+
drawerRouterReducer,
|
|
42
|
+
isDrawerAnimated,
|
|
43
|
+
renderDrawer,
|
|
44
|
+
resolveDragProgress,
|
|
45
|
+
resolveDrawerGeometry,
|
|
46
|
+
resolveDrawerSlotInterpolation,
|
|
47
|
+
resolveSwipeIntent,
|
|
48
|
+
shouldClaimDrawerSwipe,
|
|
49
|
+
} from '../../core';
|
|
50
|
+
import type {
|
|
51
|
+
IDescriptor,
|
|
52
|
+
IDrawerDescriptorMap,
|
|
53
|
+
IDrawerNavigatorHandle,
|
|
54
|
+
IDrawerOptions,
|
|
55
|
+
IDrawerRouterAction,
|
|
56
|
+
IDrawerRouterState,
|
|
57
|
+
IDrawerScreenOptions,
|
|
58
|
+
IDrawerSlot,
|
|
59
|
+
INavigationEmitter,
|
|
60
|
+
IRoute,
|
|
61
|
+
} from '../../core';
|
|
62
|
+
import { getNavigationScope } from '../navigation-context';
|
|
63
|
+
import type { INavigationScopeValue } from '../navigation-context';
|
|
64
|
+
import NavigationScope from '../navigation-scope.svelte';
|
|
65
|
+
import { SCREEN_REGISTRY_HOST_PROPS } from '../registry-host';
|
|
66
|
+
import { setScreenCollector, toRegistry, withoutScreen } from '../screen-registry';
|
|
67
|
+
import type { IRegisteredScreen } from '../screen-registry';
|
|
68
|
+
import type { IDrawerScreenProps } from '../drawer-screen-props';
|
|
69
|
+
import type { IDrawerProps } from './drawer-props';
|
|
70
|
+
|
|
71
|
+
let {
|
|
72
|
+
initialRouteName,
|
|
73
|
+
drawerStyle,
|
|
74
|
+
drawerType,
|
|
75
|
+
drawerPosition,
|
|
76
|
+
drawerWidth,
|
|
77
|
+
overlayColor,
|
|
78
|
+
swipeEnabled,
|
|
79
|
+
swipeEdgeWidth,
|
|
80
|
+
swipeMinDistance,
|
|
81
|
+
swipeMinVelocity,
|
|
82
|
+
children,
|
|
83
|
+
drawerContent,
|
|
84
|
+
}: IDrawerProps = $props();
|
|
85
|
+
|
|
86
|
+
// Read BEFORE this Drawer establishes its own per-screen NavigationScope - becomes the `parent`
|
|
87
|
+
// link a nested screen's useNavigation().getParent() walks. undefined at the nesting root.
|
|
88
|
+
const parentScope = getNavigationScope();
|
|
89
|
+
|
|
90
|
+
const routeIdPrefix = `drawer-${(drawerInstanceCounter += 1)}`;
|
|
91
|
+
|
|
92
|
+
// Unlike Vue (which reads everything off untyped `attrs` and needs a per-field guard to get
|
|
93
|
+
// back into the typed core surface), Svelte's `$props()` is already typed by IDrawerProps, so
|
|
94
|
+
// this is a plain re-assembly with no narrowing at all.
|
|
95
|
+
const options = $derived<IDrawerOptions>({
|
|
96
|
+
drawerType,
|
|
97
|
+
drawerPosition,
|
|
98
|
+
drawerWidth,
|
|
99
|
+
overlayColor,
|
|
100
|
+
swipeEnabled,
|
|
101
|
+
swipeEdgeWidth,
|
|
102
|
+
swipeMinDistance,
|
|
103
|
+
swipeMinVelocity,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let screens = $state.raw<IRegisteredScreen<IDrawerScreenProps['options']>[]>([]);
|
|
107
|
+
setScreenCollector<IDrawerScreenProps['options']>({
|
|
108
|
+
kind: 'drawer',
|
|
109
|
+
register: screen => {
|
|
110
|
+
screens = [...screens, screen];
|
|
111
|
+
},
|
|
112
|
+
unregister: screen => {
|
|
113
|
+
screens = withoutScreen(screens, screen);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const registry = $derived(toRegistry(screens));
|
|
118
|
+
|
|
119
|
+
// Same seed-once-then-dispatch shape Stack and Tab use.
|
|
120
|
+
let dispatchedState = $state.raw<IDrawerRouterState | null>(null);
|
|
121
|
+
let seededState: IDrawerRouterState | undefined;
|
|
122
|
+
let routeSequence = 0;
|
|
123
|
+
|
|
124
|
+
function seedState(): IDrawerRouterState {
|
|
125
|
+
if (seededState !== undefined) return seededState;
|
|
126
|
+
const routes: IRoute<unknown>[] = [...registry.entries()].map(([name, entry]) => {
|
|
127
|
+
routeSequence += 1;
|
|
128
|
+
return { key: `${routeIdPrefix}-${name}-${routeSequence}`, name, params: entry.initialParams };
|
|
129
|
+
});
|
|
130
|
+
if (routes.length === 0) {
|
|
131
|
+
dlog('Drawer: no <Drawer.Screen> children registered');
|
|
132
|
+
// Deliberately not memoized - markers may still be registering.
|
|
133
|
+
return createInitialDrawerRouterState(routes, initialRouteName);
|
|
134
|
+
}
|
|
135
|
+
seededState = createInitialDrawerRouterState(routes, initialRouteName);
|
|
136
|
+
return seededState;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const state = $derived(dispatchedState ?? seedState());
|
|
140
|
+
|
|
141
|
+
function dispatch(action: IDrawerRouterAction): void {
|
|
142
|
+
dispatchedState = drawerRouterReducer(state, action);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// progress: 0 closed -> 1 open, the single AnimatedValue every slide/opacity transform below
|
|
146
|
+
// interpolates from. A plain `const`: a Svelte component's script runs once, so this needs no
|
|
147
|
+
// re-creation guard the way React's useRef(new Animated.Value(...)).current does.
|
|
148
|
+
const progress = new AnimatedValue(state.isOpen ? 1 : 0);
|
|
149
|
+
// Where a drag STARTS from, in progress units. Always exactly 0 or 1: a gesture only ever
|
|
150
|
+
// begins at rest, since terminate/release always snap the value back to a resting state before
|
|
151
|
+
// another grant can fire.
|
|
152
|
+
let dragStartProgress = 0;
|
|
153
|
+
|
|
154
|
+
function animateProgressTo(open: boolean): void {
|
|
155
|
+
dlog(`Drawer: animateProgressTo(open=${open}) starting at t=${Date.now()}`);
|
|
156
|
+
timing(progress, {
|
|
157
|
+
toValue: open ? 1 : 0,
|
|
158
|
+
duration: DRAWER_SNAP_DURATION,
|
|
159
|
+
// Native-driver wiring is deferred for v1 - see this file's header feasibility note. The
|
|
160
|
+
// JS timing loop still drives every frame, same as any other non-native-driven timing in
|
|
161
|
+
// this codebase.
|
|
162
|
+
useNativeDriver: false,
|
|
163
|
+
}).start();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function openDrawer(): void {
|
|
167
|
+
dlog(`Drawer: openDrawer() called, isOpen=${state.isOpen} at t=${Date.now()}`);
|
|
168
|
+
animateProgressTo(true);
|
|
169
|
+
dispatch({ type: 'openDrawer' });
|
|
170
|
+
}
|
|
171
|
+
export function closeDrawer(): void {
|
|
172
|
+
dlog(`Drawer: closeDrawer() called, isOpen=${state.isOpen} at t=${Date.now()}`);
|
|
173
|
+
animateProgressTo(false);
|
|
174
|
+
dispatch({ type: 'closeDrawer' });
|
|
175
|
+
}
|
|
176
|
+
export function toggleDrawer(): void {
|
|
177
|
+
dlog(`Drawer: toggleDrawer() called, isOpen=${state.isOpen} at t=${Date.now()}`);
|
|
178
|
+
animateProgressTo(!state.isOpen);
|
|
179
|
+
dispatch({ type: 'toggleDrawer' });
|
|
180
|
+
}
|
|
181
|
+
export function jumpTo(name: string): void {
|
|
182
|
+
// Both sides of the dispatch, because an unregistered name is a documented reducer no-op that
|
|
183
|
+
// hands the SAME state back: animating off the pre-dispatch snapshot alone would slide the
|
|
184
|
+
// panel shut while the router still says isOpen. `state` is a $derived recomputed
|
|
185
|
+
// synchronously off the reassigned `dispatchedState`, so the second read is already the
|
|
186
|
+
// reducer's own answer.
|
|
187
|
+
const wasOpen = state.isOpen;
|
|
188
|
+
dispatch({ type: 'jumpTo', name });
|
|
189
|
+
if (wasOpen && !state.isOpen) animateProgressTo(false);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const handle: IDrawerNavigatorHandle = { openDrawer, closeDrawer, toggleDrawer, jumpTo };
|
|
193
|
+
|
|
194
|
+
// The window width is read fresh AT GESTURE TIME rather than through a reactive
|
|
195
|
+
// window-dimensions rune: it is only ever consumed inside these PanResponder callbacks (never
|
|
196
|
+
// rendered), so a live `Dimensions.get` is both simpler and strictly more current than a
|
|
197
|
+
// subscription that has to round-trip a re-render first.
|
|
198
|
+
const panResponder = PanResponder.create({
|
|
199
|
+
onStartShouldSetPanResponder: (
|
|
200
|
+
event: ISymbioteEvent,
|
|
201
|
+
gestureState: IPanResponderGestureState,
|
|
202
|
+
): boolean =>
|
|
203
|
+
shouldClaimDrawerSwipe(
|
|
204
|
+
event,
|
|
205
|
+
gestureState,
|
|
206
|
+
Dimensions.get('window').width,
|
|
207
|
+
state.isOpen,
|
|
208
|
+
options,
|
|
209
|
+
'start',
|
|
210
|
+
),
|
|
211
|
+
onMoveShouldSetPanResponder: (
|
|
212
|
+
event: ISymbioteEvent,
|
|
213
|
+
gestureState: IPanResponderGestureState,
|
|
214
|
+
): boolean =>
|
|
215
|
+
shouldClaimDrawerSwipe(
|
|
216
|
+
event,
|
|
217
|
+
gestureState,
|
|
218
|
+
Dimensions.get('window').width,
|
|
219
|
+
state.isOpen,
|
|
220
|
+
options,
|
|
221
|
+
'move',
|
|
222
|
+
),
|
|
223
|
+
onPanResponderGrant: (): void => {
|
|
224
|
+
dlog('Drawer: gesture grant');
|
|
225
|
+
dragStartProgress = state.isOpen ? 1 : 0;
|
|
226
|
+
},
|
|
227
|
+
onPanResponderMove: (_event: ISymbioteEvent, gestureState: IPanResponderGestureState): void => {
|
|
228
|
+
progress.setValue(resolveDragProgress(gestureState, dragStartProgress, options));
|
|
229
|
+
},
|
|
230
|
+
onPanResponderRelease: (
|
|
231
|
+
_event: ISymbioteEvent,
|
|
232
|
+
gestureState: IPanResponderGestureState,
|
|
233
|
+
): void => {
|
|
234
|
+
const intent = resolveSwipeIntent(gestureState, state.isOpen, options);
|
|
235
|
+
const open = intent === 'open';
|
|
236
|
+
dlog(`Drawer: gesture release -> ${open ? 'open' : 'close'}`);
|
|
237
|
+
animateProgressTo(open);
|
|
238
|
+
dispatch(open ? { type: 'openDrawer' } : { type: 'closeDrawer' });
|
|
239
|
+
},
|
|
240
|
+
onPanResponderTerminate: (): void => {
|
|
241
|
+
dlog('Drawer: gesture terminated, snapping back');
|
|
242
|
+
animateProgressTo(state.isOpen);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Only the focused route's screen is ever mounted (like Tab, unlike Stack which keeps every
|
|
247
|
+
// pushed route alive). One emitter per route.key, created lazily and cached for the navigator's
|
|
248
|
+
// whole lifetime.
|
|
249
|
+
const emitters = new Map<string, INavigationEmitter>();
|
|
250
|
+
function emitterFor(routeKey: string): INavigationEmitter {
|
|
251
|
+
let emitter = emitters.get(routeKey);
|
|
252
|
+
if (emitter === undefined) {
|
|
253
|
+
emitter = createNavigationEmitter();
|
|
254
|
+
emitters.set(routeKey, emitter);
|
|
255
|
+
}
|
|
256
|
+
return emitter;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Focus/blur synthesis, deferred to `tick()` for the same reason tabs/index.svelte documents:
|
|
260
|
+
// the newly-focused screen's own subscriptions are created during THIS flush, after this
|
|
261
|
+
// effect was.
|
|
262
|
+
let previousFocusedKey: string | undefined;
|
|
263
|
+
$effect(() => {
|
|
264
|
+
const nextKey = state.routes[state.index]?.key;
|
|
265
|
+
const { blurKey, focusKey } = diffFocusedRoute(previousFocusedKey, nextKey);
|
|
266
|
+
if (blurKey === undefined && focusKey === undefined) return;
|
|
267
|
+
previousFocusedKey = nextKey;
|
|
268
|
+
void tick().then(() => {
|
|
269
|
+
if (blurKey !== undefined) {
|
|
270
|
+
dlog(`Drawer: route "${blurKey}" blurred at t=${Date.now()}`);
|
|
271
|
+
emitterFor(blurKey).emit(NAVIGATION_EVENT_BLUR);
|
|
272
|
+
}
|
|
273
|
+
if (focusKey !== undefined) {
|
|
274
|
+
dlog(`Drawer: route "${focusKey}" focused at t=${Date.now()}`);
|
|
275
|
+
emitterFor(focusKey).emit(NAVIGATION_EVENT_FOCUS);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
onDestroy(() => {
|
|
281
|
+
if (previousFocusedKey !== undefined) {
|
|
282
|
+
emitterFor(previousFocusedKey).emit(NAVIGATION_EVENT_BLUR);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const animated = $derived(isDrawerAnimated(options));
|
|
287
|
+
const geometry = $derived(resolveDrawerGeometry(options));
|
|
288
|
+
|
|
289
|
+
const panelTranslateX = $derived(
|
|
290
|
+
animated
|
|
291
|
+
? progress.interpolate(resolveDrawerSlotInterpolation(geometry, 'panel').translateX)
|
|
292
|
+
: undefined,
|
|
293
|
+
);
|
|
294
|
+
const contentTranslateX = $derived(
|
|
295
|
+
animated
|
|
296
|
+
? progress.interpolate(resolveDrawerSlotInterpolation(geometry, 'content').translateX)
|
|
297
|
+
: undefined,
|
|
298
|
+
);
|
|
299
|
+
// The overlay is a full-screen absolutely-positioned sibling BELOW content in paint order (see
|
|
300
|
+
// render-drawer.ts's drawerChildOrder) - for 'front' that's fine since content never moves, but
|
|
301
|
+
// for 'slide' content itself translates away by contentTranslateX, and without following it the
|
|
302
|
+
// overlay stays pinned full-screen. resolveDrawerSlotInterpolation's 'overlay' branch ties its
|
|
303
|
+
// translateX to the SAME range as content's for exactly this reason, so overlayOpacity and
|
|
304
|
+
// overlayTranslateX below share one resolved config.
|
|
305
|
+
const overlayInterpolation = $derived(
|
|
306
|
+
animated ? resolveDrawerSlotInterpolation(geometry, 'overlay') : undefined,
|
|
307
|
+
);
|
|
308
|
+
const overlayStyle = $derived(
|
|
309
|
+
overlayInterpolation === undefined
|
|
310
|
+
? undefined
|
|
311
|
+
: {
|
|
312
|
+
opacity: progress.interpolate(overlayInterpolation.opacity),
|
|
313
|
+
transform: [{ translateX: progress.interpolate(overlayInterpolation.translateX) }],
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
const contentStyle = $derived(
|
|
317
|
+
contentTranslateX === undefined ? undefined : { transform: [{ translateX: contentTranslateX }] },
|
|
318
|
+
);
|
|
319
|
+
const panelStyle = $derived(
|
|
320
|
+
panelTranslateX === undefined ? undefined : { transform: [{ translateX: panelTranslateX }] },
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const focusedRoute = $derived(state.routes[state.index]);
|
|
324
|
+
const focusedScreen = $derived.by<
|
|
325
|
+
{ scope: INavigationScopeValue; component: Component } | undefined
|
|
326
|
+
>(() => {
|
|
327
|
+
if (focusedRoute === undefined) return undefined;
|
|
328
|
+
const entry = registry.get(focusedRoute.name);
|
|
329
|
+
if (entry === undefined) {
|
|
330
|
+
dlog(`Drawer: no screen registered for route name "${focusedRoute.name}"`);
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
component: entry.component,
|
|
335
|
+
scope: {
|
|
336
|
+
route: focusedRoute,
|
|
337
|
+
navigation: handle,
|
|
338
|
+
emitter: emitterFor(focusedRoute.key),
|
|
339
|
+
parent: parentScope?.current,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
function resolveDrawerScreenOptions(
|
|
345
|
+
entry: IRegisteredScreen<IDrawerScreenProps['options']>,
|
|
346
|
+
route: IRoute<unknown>,
|
|
347
|
+
): IDrawerScreenOptions {
|
|
348
|
+
if (typeof entry.options === 'function') return entry.options({ route, navigation: handle });
|
|
349
|
+
return entry.options ?? {};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const descriptors = $derived.by<IDrawerDescriptorMap>(() => {
|
|
353
|
+
const map: IDrawerDescriptorMap = {};
|
|
354
|
+
for (const route of state.routes) {
|
|
355
|
+
const entry = registry.get(route.name);
|
|
356
|
+
if (entry === undefined) continue;
|
|
357
|
+
map[route.key] = { options: resolveDrawerScreenOptions(entry, route), navigation: handle };
|
|
358
|
+
}
|
|
359
|
+
return map;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const root = $derived.by<IDescriptor>(() =>
|
|
363
|
+
renderDrawer(
|
|
364
|
+
{
|
|
365
|
+
overlayColor: overlayColor ?? DRAWER_DEFAULT_OVERLAY_COLOR,
|
|
366
|
+
drawerStyle,
|
|
367
|
+
contentPassthrough: {},
|
|
368
|
+
overlayPassthrough: animated
|
|
369
|
+
? {
|
|
370
|
+
pointerEvents: state.isOpen ? 'auto' : 'none',
|
|
371
|
+
onStartShouldSetResponder: () => true,
|
|
372
|
+
onResponderRelease: () => {
|
|
373
|
+
animateProgressTo(false);
|
|
374
|
+
dispatch({ type: 'closeDrawer' });
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
: {},
|
|
378
|
+
panelPassthrough: {},
|
|
379
|
+
},
|
|
380
|
+
options,
|
|
381
|
+
),
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const order = $derived(drawerChildOrder(options));
|
|
385
|
+
|
|
386
|
+
// renderDrawer emits its slots as root children IN `drawerChildOrder`'s order, so the two zip
|
|
387
|
+
// together - the same index-pairing Vue's own render loop does.
|
|
388
|
+
const slotDescriptors = $derived.by<Partial<Record<IDrawerSlot, IDescriptor>>>(() => {
|
|
389
|
+
const map: Partial<Record<IDrawerSlot, IDescriptor>> = {};
|
|
390
|
+
order.forEach((slot, index) => {
|
|
391
|
+
const descriptor = root.children[index];
|
|
392
|
+
if (typeof descriptor !== 'string' && descriptor !== undefined) map[slot] = descriptor;
|
|
393
|
+
});
|
|
394
|
+
return map;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const rootProps = $derived<Record<string, unknown>>({
|
|
398
|
+
style: root.props.style,
|
|
399
|
+
...panResponder.panHandlers,
|
|
400
|
+
});
|
|
401
|
+
</script>
|
|
402
|
+
|
|
403
|
+
{#snippet screenContent()}{#if focusedScreen !== undefined}{@const FocusedComponent = focusedScreen.component}<NavigationScope value={focusedScreen.scope}><FocusedComponent /></NavigationScope>{/if}{/snippet}{#snippet contentSlot()}{@const descriptor = slotDescriptors.content}{#if descriptor !== undefined}{#if contentStyle !== undefined}<Animated.View {...descriptor.props} style={[descriptor.props.style, contentStyle]}>{@render screenContent()}</Animated.View>{:else}<symbiote-view p={descriptor.props}>{@render screenContent()}</symbiote-view>{/if}{/if}{/snippet}{#snippet overlaySlot()}{@const descriptor = slotDescriptors.overlay}{#if descriptor !== undefined}{#if overlayStyle !== undefined}<Animated.View {...descriptor.props} style={[descriptor.props.style, overlayStyle]} />{:else}<symbiote-view p={descriptor.props} />{/if}{/if}{/snippet}{#snippet panelSlot()}{@const descriptor = slotDescriptors.panel}{#if descriptor !== undefined}{#if panelStyle !== undefined}<Animated.View {...descriptor.props} style={[descriptor.props.style, panelStyle]}>{@render drawerContent?.({ state, descriptors, navigation: handle })}</Animated.View>{:else}<symbiote-view p={descriptor.props}>{@render drawerContent?.({ state, descriptors, navigation: handle })}</symbiote-view>{/if}{/if}{/snippet}<symbiote-view p={rootProps}><symbiote-text p={SCREEN_REGISTRY_HOST_PROPS}>{@render children?.()}</symbiote-text>{#each order as slot (slot)}{#if slot === 'content'}{@render contentSlot()}{:else if slot === 'overlay'}{@render overlaySlot()}{:else}{@render panelSlot()}{/if}{/each}</symbiote-view>
|
|
404
|
+
|
|
405
|
+
<!-- --- Explicit gap list vs the real react-native-gesture-handler + react-native-reanimated
|
|
406
|
+
@react-navigation/drawer (confirmed against its current docs, mirrored verbatim from the
|
|
407
|
+
React twin - the gaps are architectural, not adapter-specific) ---
|
|
408
|
+
1. `configureGestureHandler` - a raw react-native-gesture-handler `Gesture` object escape
|
|
409
|
+
hatch. No PanResponder equivalent exists; not ported.
|
|
410
|
+
2. Simultaneous/failure gesture RELATIONSHIPS (gesture-handler's declarative composition vs
|
|
411
|
+
a nested ScrollView, another PanResponder, etc.) - PanResponder only offers negotiation
|
|
412
|
+
via the should-set boolean gates used here (edge-start + dominant-axis), which is more
|
|
413
|
+
prone to an accidental hijack of a nested horizontal ScrollView/Swiper than
|
|
414
|
+
gesture-handler's system.
|
|
415
|
+
3. `useDrawerProgress` - a Reanimated SharedValue read on the UI thread. `progress` here is a
|
|
416
|
+
JS-thread AnimatedValue; interpolating it for consumer-facing content animation works, but
|
|
417
|
+
without native-driver wiring (gap noted above) it does not carry the same synchronous
|
|
418
|
+
UI-thread guarantee under JS-thread load.
|
|
419
|
+
4. `hideStatusBarOnOpen` / `keyboardDismissMode` / `statusBarAnimation` / `overlayStyle` -
|
|
420
|
+
not wired in this pass; straightforward additions once StatusBar/Keyboard module wiring is
|
|
421
|
+
needed here (not a PanResponder/Animated limitation, just unscoped for v1). -->
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { IDrawerProps } from './drawer-props';
|
|
3
|
+
declare const Index: Component<IDrawerProps, {
|
|
4
|
+
openDrawer: () => void;
|
|
5
|
+
closeDrawer: () => void;
|
|
6
|
+
toggleDrawer: () => void;
|
|
7
|
+
jumpTo: (name: string) => void;
|
|
8
|
+
}, "">;
|
|
9
|
+
type Index = ReturnType<typeof Index>;
|
|
10
|
+
export default Index;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { IDrawerNavigatorHandle, IDrawerScreenOptions, IRoute } from '../core';
|
|
3
|
+
export type IDrawerScreenOptionsArgs = {
|
|
4
|
+
route: IRoute<unknown>;
|
|
5
|
+
navigation: IDrawerNavigatorHandle;
|
|
6
|
+
};
|
|
7
|
+
export type IDrawerScreenOptionsResolver = (args: IDrawerScreenOptionsArgs) => IDrawerScreenOptions;
|
|
8
|
+
export type IDrawerScreenProps = {
|
|
9
|
+
name: string;
|
|
10
|
+
component: Component;
|
|
11
|
+
options?: IDrawerScreenOptions | IDrawerScreenOptionsResolver;
|
|
12
|
+
initialParams?: unknown;
|
|
13
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Drawer.Screen's public prop surface - mirrors tab-screen-props.ts (Tab is the closer sibling:
|
|
2
|
+
// both are fixed-route-list, no-push navigators). Split out of `drawer-screen.svelte` for the
|
|
3
|
+
// same tsc-cannot-see-a-`.svelte`-named-export reason screen-props.ts documents.
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Drawer.Screen: a declarative marker that paints nothing, registered through the same
|
|
3
|
+
// context-based collector Stack.Screen/Tab.Screen use (screen-registry.ts). See CLAUDE.md
|
|
4
|
+
// <third_party_rn_packages_are_react_only> - this and Drawer import nothing from react-native.
|
|
5
|
+
import { collectScreen } from './screen-registry';
|
|
6
|
+
import type { IDrawerScreenProps } from './drawer-screen-props';
|
|
7
|
+
|
|
8
|
+
let { name, component, options, initialParams }: IDrawerScreenProps = $props();
|
|
9
|
+
|
|
10
|
+
collectScreen('drawer', {
|
|
11
|
+
get name(): IDrawerScreenProps['name'] {
|
|
12
|
+
return name;
|
|
13
|
+
},
|
|
14
|
+
get component(): IDrawerScreenProps['component'] {
|
|
15
|
+
return component;
|
|
16
|
+
},
|
|
17
|
+
get options(): IDrawerScreenProps['options'] {
|
|
18
|
+
return options;
|
|
19
|
+
},
|
|
20
|
+
get initialParams(): unknown {
|
|
21
|
+
return initialParams;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import '../register';
|
|
2
|
+
export { Stack } from './stack';
|
|
3
|
+
export type { INavigatorHandle, IStackProps } from './stack';
|
|
4
|
+
export { default as Screen } from './screen.svelte';
|
|
5
|
+
export type { IScreenOptionsArgs, IScreenOptionsResolver, IScreenProps, ISvelteScreenOptions, ISvelteSearchBarOptions, } from './screen-props';
|
|
6
|
+
export type { IScreenOptions, IStackAnimation, IStackPresentation } from '../core';
|
|
7
|
+
export { useNavigation, useStackNavigation, useTabNavigation, useDrawerNavigation, useRoute, useIsFocused, useFocusEffect, useNavigationState, } from './runes';
|
|
8
|
+
export type { INavigationHandle, IStackNavigationHandle, ITabNavigationHandle, IDrawerNavigationHandle, } from './runes';
|
|
9
|
+
export type { INavigationScopeValue, IAnyNavigatorHandle } from './navigation-context';
|
|
10
|
+
export { useLinkingIntegration } from './linking.svelte';
|
|
11
|
+
export type { ILinkingConfig, IScreenLinkingConfig } from '../core';
|
|
12
|
+
export { Tab } from './tabs';
|
|
13
|
+
export type { ITabNavigatorHandle, ITabProps } from './tabs';
|
|
14
|
+
export { default as TabScreen } from './tab-screen.svelte';
|
|
15
|
+
export type { ITabScreenOptionsArgs, ITabScreenOptionsResolver, ITabScreenProps, } from './tab-screen-props';
|
|
16
|
+
export type { ITabOptions, ITabBarIcon } from '../core';
|
|
17
|
+
export { Drawer } from './drawer';
|
|
18
|
+
export type { IDrawerContentSlotProps, IDrawerNavigatorHandle, IDrawerProps, IDrawerDescriptorMap, } from './drawer';
|
|
19
|
+
export { default as DrawerScreen } from './drawer-screen.svelte';
|
|
20
|
+
export type { IDrawerScreenOptionsArgs, IDrawerScreenOptionsResolver, IDrawerScreenProps, } from './drawer-screen-props';
|
|
21
|
+
export type { IDrawerOptions, IDrawerScreenOptions, IDrawerType, IDrawerPosition } from '../core';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @symbiote-native/navigation/svelte: the Svelte native-stack navigator over
|
|
2
|
+
// react-native-screens' native view primitives. Importing this barrel first registers the native
|
|
3
|
+
// views' ViewConfigs (../register, a side-effect import of the codegen specs - never
|
|
4
|
+
// react-native-screens' own React components), then exposes Stack (with Stack.Screen attached)
|
|
5
|
+
// and the navigator handle. Mirrors vue/index.ts's barrel shape.
|
|
6
|
+
import '../register';
|
|
7
|
+
export { Stack } from './stack/index.js';
|
|
8
|
+
export { default as Screen } from './screen.svelte';
|
|
9
|
+
export { useNavigation, useStackNavigation, useTabNavigation, useDrawerNavigation, useRoute, useIsFocused, useFocusEffect, useNavigationState, } from './runes/index.js';
|
|
10
|
+
export { useLinkingIntegration } from './linking.svelte';
|
|
11
|
+
// Tab: the bottom-tabs navigator, a PURE-JS UI (no react-native-screens views involved, so no
|
|
12
|
+
// extra ViewConfig registration is needed beyond the ../register import above).
|
|
13
|
+
export { Tab } from './tabs/index.js';
|
|
14
|
+
export { default as TabScreen } from './tab-screen.svelte';
|
|
15
|
+
// Drawer: the swipeable drawer navigator, a PURE-JS UI (PanResponder + Animated, no
|
|
16
|
+
// react-native-screens views involved, so no extra ViewConfig registration is needed beyond the
|
|
17
|
+
// ../register import above). See drawer/index.svelte's header for the feasibility note re:
|
|
18
|
+
// react-native-gesture-handler / react-native-reanimated parity gaps.
|
|
19
|
+
export { Drawer } from './drawer/index.js';
|
|
20
|
+
export { default as DrawerScreen } from './drawer-screen.svelte';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// useLinkingIntegration: wires the framework-agnostic linking config (../core's
|
|
2
|
+
// resolveRouteFromUrl) onto an existing Stack's INavigatorHandle. Deliberately NOT called from
|
|
3
|
+
// inside stack/index.svelte - app code owns the wiring, the same top-level-prop shape
|
|
4
|
+
// @react-navigation keeps `linking` in on NavigationContainer rather than baking it into the
|
|
5
|
+
// router itself.
|
|
6
|
+
//
|
|
7
|
+
// Svelte-shape difference from React and Vue: React's twin takes the RESOLVED
|
|
8
|
+
// `navigatorHandle: INavigatorHandle` (a React hook re-runs on every render with the caller's
|
|
9
|
+
// current value); Vue's takes the `Ref` object and reads `.value` inside onMounted. A Svelte
|
|
10
|
+
// component's script runs exactly ONCE and its `bind:this` target is only assigned during mount,
|
|
11
|
+
// so this takes a GETTER - the boxed-getter convention this adapter uses everywhere - and reads
|
|
12
|
+
// it INSIDE the effect, i.e. after mount. The read is deliberately TRACKED (no `untrack`, unlike
|
|
13
|
+
// the ./runes files): when the app declares its handle with `$state.raw`, the effect re-runs the
|
|
14
|
+
// moment `bind:this` lands, so the wiring never depends on a mount-ordering guarantee at all.
|
|
15
|
+
// Re-running is safe because the previous run's cleanup tears the subscription down first.
|
|
16
|
+
//
|
|
17
|
+
// `.svelte.ts`: `$effect` is only usable outside a component in a file with this extension.
|
|
18
|
+
import { Linking, dlog } from '@symbiote-native/engine';
|
|
19
|
+
import { resolveRouteFromUrl } from '../core/index.js';
|
|
20
|
+
export function useLinkingIntegration(config, getNavigatorHandle) {
|
|
21
|
+
$effect(() => {
|
|
22
|
+
const handle = getNavigatorHandle();
|
|
23
|
+
if (handle === null) {
|
|
24
|
+
dlog('useLinkingIntegration: navigatorHandle is not available yet');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
const applyRoute = (url, dispatch) => {
|
|
29
|
+
const route = resolveRouteFromUrl(config, url);
|
|
30
|
+
if (route === null) {
|
|
31
|
+
dlog(`useLinkingIntegration: no route resolved for "${url}"`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
dispatch(route.name, route.params);
|
|
35
|
+
};
|
|
36
|
+
Linking.getInitialURL()
|
|
37
|
+
.then(url => {
|
|
38
|
+
if (cancelled || url === null)
|
|
39
|
+
return;
|
|
40
|
+
dlog(`useLinkingIntegration: initial URL "${url}"`);
|
|
41
|
+
applyRoute(url, handle.replace);
|
|
42
|
+
})
|
|
43
|
+
.catch((error) => {
|
|
44
|
+
dlog(`useLinkingIntegration: getInitialURL failed: ${String(error)}`);
|
|
45
|
+
});
|
|
46
|
+
const subscription = Linking.addEventListener('url', (event) => {
|
|
47
|
+
dlog(`useLinkingIntegration: url event "${event.url}"`);
|
|
48
|
+
applyRoute(event.url, handle.push);
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
subscription.remove();
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { INavigationEmitter, IRoute, IAnyNavigatorHandle } from '../core';
|
|
2
|
+
export type { IAnyNavigatorHandle };
|
|
3
|
+
export type INavigationScopeValue = {
|
|
4
|
+
route: IRoute<unknown>;
|
|
5
|
+
navigation: IAnyNavigatorHandle;
|
|
6
|
+
emitter: INavigationEmitter;
|
|
7
|
+
parent?: INavigationScopeValue;
|
|
8
|
+
};
|
|
9
|
+
export type INavigationScope = {
|
|
10
|
+
readonly current: INavigationScopeValue;
|
|
11
|
+
};
|
|
12
|
+
export declare function setNavigationScope(scope: INavigationScope): void;
|
|
13
|
+
export declare function getNavigationScope(): INavigationScope | undefined;
|
|
14
|
+
export declare function requireNavigationScope(runeName: string): INavigationScope;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The lifecycle half of the framework-agnostic emitter (../core/navigation-events). Svelte's twin
|
|
2
|
+
// of React's Context / Vue's provide-inject is setContext/getContext over one shared key, so a
|
|
3
|
+
// screen's subtree can read its own route, navigator handle, and per-route emitter without
|
|
4
|
+
// prop-drilling.
|
|
5
|
+
//
|
|
6
|
+
// Context in Svelte follows the RUNTIME render tree, not the lexical definition site: a component
|
|
7
|
+
// instantiated inside a snippet the app passed down to <Stack> still sees the context <Stack> (or
|
|
8
|
+
// the NavigationScope it renders) set - verified against the real compiler before this was built
|
|
9
|
+
// on. That is what lets a navigator hand each mounted route its OWN scope: Stack/Tab/Drawer mount
|
|
10
|
+
// one NavigationScope per route (keyed by route.key, exactly like React's per-route
|
|
11
|
+
// `<NavigationContext.Provider>`), and each instance's own init calls the setter once.
|
|
12
|
+
//
|
|
13
|
+
// The value is a BOXED GETTER (`{ get current() }`), never a bare value: Svelte 5 reactivity does
|
|
14
|
+
// not survive being handed out of the declaring scope as a plain value (svelte-adapter-dom-shim
|
|
15
|
+
// skill §21), and consumers must keep seeing the CURRENT route (a setParams that produces a new
|
|
16
|
+
// route object for the same key still has to reach them).
|
|
17
|
+
//
|
|
18
|
+
// `parent` threads the ambient value a navigator read on ITS OWN mount (undefined at the root)
|
|
19
|
+
// into the value it provides to its own screens, forming a linked list - this is what lets a
|
|
20
|
+
// screen nested inside e.g. a Stack-screen-renders-a-Tab composition reach the enclosing Stack via
|
|
21
|
+
// useNavigation().getParent().
|
|
22
|
+
import { getContext, setContext } from 'svelte';
|
|
23
|
+
const NAVIGATION_SCOPE_KEY = Symbol('symbiote-navigation-scope');
|
|
24
|
+
// Written only from NavigationScope's own init (navigation-scope.svelte), never by app code.
|
|
25
|
+
export function setNavigationScope(scope) {
|
|
26
|
+
setContext(NAVIGATION_SCOPE_KEY, scope);
|
|
27
|
+
}
|
|
28
|
+
// undefined simply means "no ancestor NavigationScope", which is a legitimate state (the nesting
|
|
29
|
+
// root a navigator itself reads on mount) rather than an error - callers that require one go
|
|
30
|
+
// through requireNavigationScope below.
|
|
31
|
+
export function getNavigationScope() {
|
|
32
|
+
return getContext(NAVIGATION_SCOPE_KEY);
|
|
33
|
+
}
|
|
34
|
+
// The "throw if missing" half every rune in ./runes needs (useNavigation, useRoute, useIsFocused,
|
|
35
|
+
// useFocusEffect, useNavigationState) - they all read the scope and threw the same shaped error,
|
|
36
|
+
// differing only in which rune name the message names. Centralized here so a wording change lands
|
|
37
|
+
// once instead of five times.
|
|
38
|
+
export function requireNavigationScope(runeName) {
|
|
39
|
+
const scope = getNavigationScope();
|
|
40
|
+
if (scope === undefined) {
|
|
41
|
+
throw new Error(`${runeName} must be used within a screen rendered by <Stack>, <Tab>, or <Drawer>`);
|
|
42
|
+
}
|
|
43
|
+
return scope;
|
|
44
|
+
}
|