@symbiote-native/navigation 2.1.0 → 3.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.
Files changed (237) hide show
  1. package/README.md +89 -14
  2. package/build/angular/drawer/index.js +23 -9
  3. package/build/angular/index.d.ts +4 -4
  4. package/build/angular/index.js +1 -1
  5. package/build/angular/injectors/inject-navigation.js +1 -1
  6. package/build/angular/navigation-scope.directive.js +1 -1
  7. package/build/angular/search-bar-ref.directive.js +1 -1
  8. package/build/angular/stack/index.js +20 -6
  9. package/build/angular/tabs/index.js +21 -7
  10. package/build/core/drawer-options/index.js +19 -5
  11. package/build/core/drawer-router-state/index.js +8 -2
  12. package/build/core/guards.js +3 -1
  13. package/build/core/header-bar-buttons.js +14 -4
  14. package/build/core/index.d.ts +4 -4
  15. package/build/core/index.js +2 -2
  16. package/build/core/navigator-state/index.js +12 -4
  17. package/build/core/render-drawer.js +4 -1
  18. package/build/core/render-stack.js +13 -4
  19. package/build/core/screen-options.js +1 -1
  20. package/build/core/search-bar-commands.js +1 -1
  21. package/build/core/state-persistence/index.js +6 -2
  22. package/build/core/tab-router-state/index.js +8 -2
  23. package/build/react/drawer/index.js +36 -13
  24. package/build/react/index.d.ts +7 -7
  25. package/build/react/index.js +3 -3
  26. package/build/react/stack/index.js +34 -8
  27. package/build/react/tabs/index.js +14 -4
  28. package/build/solid/drawer/index.d.ts +22 -0
  29. package/build/solid/drawer/index.js +394 -0
  30. package/build/solid/drawer-screen-props.d.ts +13 -0
  31. package/build/solid/drawer-screen-props.js +2 -0
  32. package/build/solid/host.d.ts +2 -0
  33. package/build/solid/host.js +23 -0
  34. package/build/solid/index.d.ts +19 -0
  35. package/build/solid/index.js +24 -0
  36. package/build/solid/linking.d.ts +3 -0
  37. package/build/solid/linking.js +55 -0
  38. package/build/solid/navigation-context.d.ts +13 -0
  39. package/build/solid/navigation-context.js +37 -0
  40. package/build/solid/primitives/create-focus-effect.d.ts +1 -0
  41. package/build/solid/primitives/create-focus-effect.js +30 -0
  42. package/build/solid/primitives/create-is-focused.d.ts +2 -0
  43. package/build/solid/primitives/create-is-focused.js +31 -0
  44. package/build/solid/primitives/create-navigation-state.d.ts +3 -0
  45. package/build/solid/primitives/create-navigation-state.js +30 -0
  46. package/build/solid/primitives/index.d.ts +12 -0
  47. package/build/solid/primitives/index.js +8 -0
  48. package/build/solid/primitives/use-drawer-navigation.d.ts +5 -0
  49. package/build/solid/primitives/use-drawer-navigation.js +15 -0
  50. package/build/solid/primitives/use-navigation.d.ts +8 -0
  51. package/build/solid/primitives/use-navigation.js +23 -0
  52. package/build/solid/primitives/use-route.d.ts +3 -0
  53. package/build/solid/primitives/use-route.js +14 -0
  54. package/build/solid/primitives/use-stack-navigation.d.ts +5 -0
  55. package/build/solid/primitives/use-stack-navigation.js +19 -0
  56. package/build/solid/primitives/use-tab-navigation.d.ts +5 -0
  57. package/build/solid/primitives/use-tab-navigation.js +15 -0
  58. package/build/solid/screen-props.d.ts +19 -0
  59. package/build/solid/screen-props.js +9 -0
  60. package/build/solid/screen-registry.d.ts +31 -0
  61. package/build/solid/screen-registry.js +86 -0
  62. package/build/solid/screen.d.ts +6 -0
  63. package/build/solid/screen.js +69 -0
  64. package/build/solid/stack/index.d.ts +15 -0
  65. package/build/solid/stack/index.js +348 -0
  66. package/build/solid/tab-screen-props.d.ts +13 -0
  67. package/build/solid/tab-screen-props.js +3 -0
  68. package/build/solid/tabs/index.d.ts +14 -0
  69. package/build/solid/tabs/index.js +228 -0
  70. package/build/svelte/descriptor-subtree.js +3 -1
  71. package/build/svelte/drawer/index.svelte +163 -25
  72. package/build/svelte/drawer-screen.svelte +2 -1
  73. package/build/svelte/index.d.ts +3 -3
  74. package/build/svelte/index.js +1 -1
  75. package/build/svelte/navigation-scope.svelte +4 -1
  76. package/build/svelte/runes/use-navigation.svelte.js +5 -1
  77. package/build/svelte/stack/index.svelte +43 -6
  78. package/build/svelte/stack/stack-screen.svelte +66 -9
  79. package/build/svelte/tabs/index.svelte +42 -20
  80. package/build/vue/composables/use-navigation.js +5 -1
  81. package/build/vue/drawer/index.js +30 -8
  82. package/build/vue/index.d.ts +3 -3
  83. package/build/vue/index.js +1 -1
  84. package/build/vue/navigation-context.js +1 -1
  85. package/build/vue/stack/index.js +17 -6
  86. package/build/vue/tabs/index.js +18 -5
  87. package/build-ngc/angular/drawer/index.js +31 -11
  88. package/build-ngc/angular/drawer/index.js.map +1 -1
  89. package/build-ngc/angular/index.d.ts +4 -4
  90. package/build-ngc/angular/index.js.map +1 -1
  91. package/build-ngc/angular/injectors/inject-focus-effect.js.map +1 -1
  92. package/build-ngc/angular/injectors/inject-is-focused.js.map +1 -1
  93. package/build-ngc/angular/injectors/inject-navigation-state.js.map +1 -1
  94. package/build-ngc/angular/injectors/inject-navigation.js +1 -1
  95. package/build-ngc/angular/injectors/inject-navigation.js.map +1 -1
  96. package/build-ngc/angular/linking.js.map +1 -1
  97. package/build-ngc/angular/navigation-context.service.js.map +1 -1
  98. package/build-ngc/angular/navigation-scope.directive.js +1 -1
  99. package/build-ngc/angular/navigation-scope.directive.js.map +1 -1
  100. package/build-ngc/angular/screen.directive.js.map +1 -1
  101. package/build-ngc/angular/search-bar-ref.directive.js +1 -1
  102. package/build-ngc/angular/search-bar-ref.directive.js.map +1 -1
  103. package/build-ngc/angular/stack/index.js +26 -8
  104. package/build-ngc/angular/stack/index.js.map +1 -1
  105. package/build-ngc/angular/tab-screen.directive.js.map +1 -1
  106. package/build-ngc/angular/tabs/index.js +21 -7
  107. package/build-ngc/angular/tabs/index.js.map +1 -1
  108. package/build-ngc/core/constants.js.map +1 -1
  109. package/build-ngc/core/drawer-options/index.js +19 -5
  110. package/build-ngc/core/drawer-options/index.js.map +1 -1
  111. package/build-ngc/core/drawer-router-state/index.js +8 -2
  112. package/build-ngc/core/drawer-router-state/index.js.map +1 -1
  113. package/build-ngc/core/guards.js +3 -1
  114. package/build-ngc/core/guards.js.map +1 -1
  115. package/build-ngc/core/header-bar-buttons.js +14 -4
  116. package/build-ngc/core/header-bar-buttons.js.map +1 -1
  117. package/build-ngc/core/index.d.ts +4 -4
  118. package/build-ngc/core/index.js +2 -2
  119. package/build-ngc/core/index.js.map +1 -1
  120. package/build-ngc/core/linking-config/index.js.map +1 -1
  121. package/build-ngc/core/navigation-events/index.js.map +1 -1
  122. package/build-ngc/core/navigator-handles/index.js.map +1 -1
  123. package/build-ngc/core/navigator-state/index.js +12 -4
  124. package/build-ngc/core/navigator-state/index.js.map +1 -1
  125. package/build-ngc/core/render-drawer.js +4 -1
  126. package/build-ngc/core/render-drawer.js.map +1 -1
  127. package/build-ngc/core/render-stack.js +13 -4
  128. package/build-ngc/core/render-stack.js.map +1 -1
  129. package/build-ngc/core/render-tabs.js.map +1 -1
  130. package/build-ngc/core/screen-options.js +1 -1
  131. package/build-ngc/core/screen-options.js.map +1 -1
  132. package/build-ngc/core/search-bar-commands.js +1 -1
  133. package/build-ngc/core/search-bar-commands.js.map +1 -1
  134. package/build-ngc/core/stack-router-state/index.js.map +1 -1
  135. package/build-ngc/core/state-persistence/index.js +6 -2
  136. package/build-ngc/core/state-persistence/index.js.map +1 -1
  137. package/build-ngc/core/tab-router-state/index.js +8 -2
  138. package/build-ngc/core/tab-router-state/index.js.map +1 -1
  139. package/package.json +40 -15
  140. package/src/angular/drawer/index.ts +69 -25
  141. package/src/angular/index.ts +20 -4
  142. package/src/angular/injectors/inject-focus-effect.ts +8 -2
  143. package/src/angular/injectors/inject-is-focused.ts +6 -4
  144. package/src/angular/injectors/inject-navigation-state.ts +7 -4
  145. package/src/angular/injectors/inject-navigation.ts +12 -3
  146. package/src/angular/linking.ts +4 -1
  147. package/src/angular/navigation-context.service.ts +5 -2
  148. package/src/angular/navigation-scope.directive.ts +6 -2
  149. package/src/angular/screen.directive.ts +13 -3
  150. package/src/angular/search-bar-ref.directive.ts +10 -2
  151. package/src/angular/stack/index.ts +68 -22
  152. package/src/angular/tab-screen.directive.ts +3 -1
  153. package/src/angular/tabs/index.ts +36 -12
  154. package/src/core/constants.ts +6 -3
  155. package/src/core/drawer-options/index.ts +40 -13
  156. package/src/core/drawer-router-state/index.ts +11 -3
  157. package/src/core/guards.ts +3 -1
  158. package/src/core/header-bar-buttons.ts +41 -12
  159. package/src/core/index.ts +18 -4
  160. package/src/core/linking-config/index.ts +31 -10
  161. package/src/core/navigation-events/index.ts +8 -2
  162. package/src/core/navigator-handles/index.ts +8 -3
  163. package/src/core/navigator-props.ts +13 -4
  164. package/src/core/navigator-state/index.ts +24 -7
  165. package/src/core/render-drawer.ts +24 -9
  166. package/src/core/render-stack.ts +52 -13
  167. package/src/core/render-tabs.ts +24 -6
  168. package/src/core/screen-options.ts +15 -5
  169. package/src/core/search-bar-commands.ts +12 -3
  170. package/src/core/stack-router-state/index.ts +5 -2
  171. package/src/core/state-persistence/index.ts +13 -3
  172. package/src/core/tab-router-state/index.ts +10 -3
  173. package/src/react/drawer/index.ts +408 -344
  174. package/src/react/drawer-screen.ts +8 -2
  175. package/src/react/hooks/use-focus-effect.ts +8 -2
  176. package/src/react/hooks/use-is-focused.ts +6 -2
  177. package/src/react/hooks/use-navigation.ts +8 -2
  178. package/src/react/index.ts +37 -7
  179. package/src/react/linking.ts +4 -1
  180. package/src/react/navigation-context.ts +6 -2
  181. package/src/react/screen.ts +12 -3
  182. package/src/react/stack/index.ts +294 -231
  183. package/src/react/tab-screen.ts +3 -1
  184. package/src/react/tabs/index.ts +188 -158
  185. package/src/solid/drawer/index.ts +570 -0
  186. package/src/solid/drawer-screen-props.ts +24 -0
  187. package/src/solid/host.ts +29 -0
  188. package/src/solid/index.ts +87 -0
  189. package/src/solid/linking.ts +74 -0
  190. package/src/solid/navigation-context.ts +56 -0
  191. package/src/solid/primitives/create-focus-effect.ts +41 -0
  192. package/src/solid/primitives/create-is-focused.ts +41 -0
  193. package/src/solid/primitives/create-navigation-state.ts +44 -0
  194. package/src/solid/primitives/index.ts +12 -0
  195. package/src/solid/primitives/use-drawer-navigation.ts +25 -0
  196. package/src/solid/primitives/use-navigation.ts +44 -0
  197. package/src/solid/primitives/use-route.ts +18 -0
  198. package/src/solid/primitives/use-stack-navigation.ts +29 -0
  199. package/src/solid/primitives/use-tab-navigation.ts +25 -0
  200. package/src/solid/screen-props.ts +51 -0
  201. package/src/solid/screen-registry.ts +159 -0
  202. package/src/solid/screen.ts +80 -0
  203. package/src/solid/stack/index.ts +528 -0
  204. package/src/solid/tab-screen-props.ts +21 -0
  205. package/src/solid/tabs/index.ts +327 -0
  206. package/src/svelte/attachments.ts +3 -1
  207. package/src/svelte/descriptor-subtree.ts +3 -1
  208. package/src/svelte/drawer/index.svelte +163 -25
  209. package/src/svelte/drawer-screen-props.ts +8 -2
  210. package/src/svelte/drawer-screen.svelte +2 -1
  211. package/src/svelte/fabric-tree.test-helper.ts +37 -3
  212. package/src/svelte/index.ts +15 -3
  213. package/src/svelte/linking.svelte.ts +11 -5
  214. package/src/svelte/navigation-scope.svelte +4 -1
  215. package/src/svelte/runes/use-drawer-navigation.svelte.ts +3 -1
  216. package/src/svelte/runes/use-focus-effect.svelte.ts +8 -2
  217. package/src/svelte/runes/use-navigation-state.svelte.ts +3 -1
  218. package/src/svelte/runes/use-navigation.svelte.ts +13 -3
  219. package/src/svelte/runes/use-stack-navigation.svelte.ts +3 -1
  220. package/src/svelte/screen-props.ts +7 -2
  221. package/src/svelte/screen-registry.ts +9 -3
  222. package/src/svelte/stack/index.svelte +43 -6
  223. package/src/svelte/stack/stack-screen.svelte +66 -9
  224. package/src/svelte/svelte-compile.test-helper.ts +16 -12
  225. package/src/svelte/tab-screen-props.ts +3 -1
  226. package/src/svelte/tabs/index.svelte +42 -20
  227. package/src/vue/composables/use-navigation-state.ts +7 -4
  228. package/src/vue/composables/use-navigation.ts +13 -3
  229. package/src/vue/drawer/index.ts +122 -36
  230. package/src/vue/drawer-screen.ts +12 -5
  231. package/src/vue/index.ts +15 -3
  232. package/src/vue/linking.ts +7 -2
  233. package/src/vue/navigation-context.ts +17 -5
  234. package/src/vue/screen.ts +7 -2
  235. package/src/vue/stack/index.ts +82 -34
  236. package/src/vue/tab-screen.ts +3 -1
  237. package/src/vue/tabs/index.ts +51 -13
@@ -0,0 +1,394 @@
1
+ // Drawer, the Solid lifecycle half. The open/closed + focused-route router (drawer-router-state)
2
+ // and the pure swipe/geometry math (drawer-options) live in @symbiote-native/navigation core,
3
+ // shared verbatim with every other adapter; here Solid supplies the lifecycle - a signal for the
4
+ // router, a PanResponder built ONCE whose callbacks read options/window-width/isOpen LIVE (Solid
5
+ // props are getters, so a gesture callback always sees the current value with no ref-mirroring
6
+ // dance), an Animated.Value driving the slide/opacity transforms, and a callback `ref` for the
7
+ // open/close/toggle/jumpTo handle. Tab is the closer sibling (both are fixed-route-list,
8
+ // no-react-native-screens navigators); Stack's push/pop and native-screen bridging don't apply.
9
+ //
10
+ // FEASIBILITY NOTE (mirrored verbatim from the React/Vue twins - the gaps are architectural, not
11
+ // adapter-specific): the REAL @react-navigation/drawer is built on react-native-gesture-handler +
12
+ // react-native-reanimated, neither of which this codebase depends on. What's built here reaches the
13
+ // same swipe-to-open/close + front/back/slide/permanent behavior using only PanResponder +
14
+ // Animated (both already in @symbiote-native/engine). See the explicit gap list at the bottom.
15
+ //
16
+ // THE SOLID-ONLY PART: renderDrawer's Descriptor SHAPE follows `drawerType` - the slot ORDER
17
+ // changes, and 'permanent' drops the animation entirely. descriptorToSolid builds once and updates
18
+ // props in place, so a type/position change is a genuine rebuild boundary and is asked for by name
19
+ // (a createMemo over those discriminators alone, build untracked -
20
+ // .claude/rules/solid-descriptor-bridge.md §5). Everything else - width, overlay colour, the live
21
+ // interpolations, the focused screen's params - flows through prop accessors on the existing nodes.
22
+ //
23
+ // `drawerContent` is a render prop taking an ACCESSOR, called once and untracked, not a plain
24
+ // value: §4 of the same rules file. A value snapshot would freeze the drawer's own content at the
25
+ // state it was built with, and re-invoking it from inside a tracked scope would replace the whole
26
+ // panel subtree mid-gesture.
27
+ import { createEffect, createMemo, createSignal, mergeProps, onCleanup, untrack, } from 'solid-js';
28
+ import { Animated, createWindowDimensions } from '@symbiote-native/solid';
29
+ // createComponent from solid-js, insert from the renderer - see stack/index.ts's note.
30
+ import { createComponent } from 'solid-js';
31
+ import { insert } from '@symbiote-native/solid/renderer';
32
+ import { PanResponder, dlog } from '@symbiote-native/engine';
33
+ import { DRAWER_DEFAULT_OVERLAY_COLOR, NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialDrawerRouterState, createNavigationEmitter, diffFocusedRoute, drawerChildOrder, drawerRouterReducer, isDrawerAnimated, renderDrawer, resolveDragProgress, resolveDrawerGeometry, resolveDrawerPosition, resolveDrawerSlotInterpolation, resolveDrawerType, resolveSwipeIntent, shouldClaimDrawerSwipe, } from '../../core/index.js';
34
+ import { hostElement } from '../host.js';
35
+ import { NavigationScopeProvider, useNavigationScope, } from '../navigation-context.js';
36
+ import { ScreenCollectorProvider, createScreenSignal, toRegistry, } from '../screen-registry.js';
37
+ import { DrawerScreen } from '../screen.js';
38
+ const DRAWER_SNAP_DURATION = 250;
39
+ let navigatorSequence = 0;
40
+ function resolveDrawerScreenOptions(entry, args) {
41
+ if (typeof entry.options === 'function')
42
+ return entry.options(args);
43
+ return entry.options ?? {};
44
+ }
45
+ function sameDrawerShape(a, b) {
46
+ return (a.type === b.type && a.position === b.position && a.animated === b.animated);
47
+ }
48
+ function DrawerImpl(props) {
49
+ const parentScope = useNavigationScope();
50
+ const { screens, collector } = createScreenSignal('drawer');
51
+ const registry = createMemo(() => toRegistry(screens()));
52
+ navigatorSequence += 1;
53
+ const routeIdPrefix = `drawer-${navigatorSequence}`;
54
+ // Every field read live off `props`, so a gesture callback (built once, below) always negotiates
55
+ // against the CURRENT options - Solid props are getters, which is what removes React's
56
+ // useRef(value) mirroring here.
57
+ function currentOptions() {
58
+ return {
59
+ drawerType: props.drawerType,
60
+ drawerPosition: props.drawerPosition,
61
+ drawerWidth: props.drawerWidth,
62
+ overlayColor: props.overlayColor,
63
+ swipeEnabled: props.swipeEnabled,
64
+ swipeEdgeWidth: props.swipeEdgeWidth,
65
+ swipeMinDistance: props.swipeMinDistance,
66
+ swipeMinVelocity: props.swipeMinVelocity,
67
+ };
68
+ }
69
+ // Route keys are derived from the screen NAME, so the derivation is pure and re-runnable.
70
+ const routes = createMemo(() => [...registry().entries()].map(([name, entry]) => ({
71
+ key: `${routeIdPrefix}-${name}`,
72
+ name,
73
+ params: entry.initialParams,
74
+ })));
75
+ // `null` means nothing has dispatched yet - and until then the initial state is RE-DERIVED from
76
+ // the registry on every read rather than cached. That is not an optimisation detail, it is what
77
+ // makes the drawer correct: each marker registers with its OWN signal write, and Solid can
78
+ // recompute a downstream memo between two of them, so a one-shot seed reliably captured only the
79
+ // FIRST screen and every later jumpTo was a no-op against a one-route router. Once the app
80
+ // dispatches, the route list freezes - matching React's and Vue's, which build it once in their
81
+ // setup pass and never reconcile (a marker added after that is ignored on every adapter).
82
+ const [dispatched, setDispatched] = createSignal(null);
83
+ const state = createMemo(() => {
84
+ const current = dispatched();
85
+ if (current !== null)
86
+ return current;
87
+ const initial = routes();
88
+ if (initial.length === 0) {
89
+ dlog('Drawer: no <Drawer.Screen> children registered');
90
+ }
91
+ return createInitialDrawerRouterState(initial, props.initialRouteName);
92
+ });
93
+ function dispatch(action) {
94
+ setDispatched(drawerRouterReducer(state(), action));
95
+ }
96
+ const windowDimensions = createWindowDimensions();
97
+ // progress: 0 closed -> 1 open, the single Animated.Value every slide/opacity transform below
98
+ // interpolates from. A plain const: a Solid body runs once, so this needs no re-creation guard
99
+ // the way React's useRef(new Animated.Value(...)).current does.
100
+ const progress = new Animated.Value(state().isOpen ? 1 : 0);
101
+ // Where a drag STARTS from, in progress units. Always exactly 0 or 1: a gesture only begins at
102
+ // rest, since terminate/release always snap back before another grant can fire.
103
+ let dragStartProgress = 0;
104
+ function animateProgressTo(open) {
105
+ dlog(`Drawer: animateProgressTo(open=${open}) at t=${Date.now()}`);
106
+ Animated.timing(progress, {
107
+ toValue: open ? 1 : 0,
108
+ duration: DRAWER_SNAP_DURATION,
109
+ // Native-driver wiring is deferred for v1 - see the header feasibility note.
110
+ useNativeDriver: false,
111
+ }).start();
112
+ }
113
+ const handle = {
114
+ openDrawer: () => {
115
+ animateProgressTo(true);
116
+ dispatch({ type: 'openDrawer' });
117
+ },
118
+ closeDrawer: () => {
119
+ animateProgressTo(false);
120
+ dispatch({ type: 'closeDrawer' });
121
+ },
122
+ toggleDrawer: () => {
123
+ animateProgressTo(!state().isOpen);
124
+ dispatch({ type: 'toggleDrawer' });
125
+ },
126
+ jumpTo: (name) => {
127
+ // Both sides of the dispatch, because an unregistered name is a documented reducer no-op that
128
+ // hands the SAME state back: animating off the pre-dispatch snapshot alone would slide the
129
+ // panel shut while the router still says isOpen. The signal is written synchronously inside
130
+ // dispatch, so the second read is already the reducer's own answer.
131
+ const wasOpen = state().isOpen;
132
+ dispatch({ type: 'jumpTo', name });
133
+ if (wasOpen && !state().isOpen)
134
+ animateProgressTo(false);
135
+ },
136
+ };
137
+ props.ref?.(handle);
138
+ const panResponder = PanResponder.create({
139
+ onStartShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, windowDimensions().width, state().isOpen, currentOptions(), 'start'),
140
+ onMoveShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, windowDimensions().width, state().isOpen, currentOptions(), 'move'),
141
+ onPanResponderGrant: () => {
142
+ dragStartProgress = state().isOpen ? 1 : 0;
143
+ },
144
+ onPanResponderMove: (_event, gestureState) => {
145
+ progress.setValue(resolveDragProgress(gestureState, dragStartProgress, currentOptions()));
146
+ },
147
+ onPanResponderRelease: (_event, gestureState) => {
148
+ const intent = resolveSwipeIntent(gestureState, state().isOpen, currentOptions());
149
+ const open = intent === 'open';
150
+ dlog(`Drawer: gesture release -> ${open ? 'open' : 'close'}`);
151
+ animateProgressTo(open);
152
+ dispatch(open ? { type: 'openDrawer' } : { type: 'closeDrawer' });
153
+ },
154
+ onPanResponderTerminate: () => {
155
+ animateProgressTo(state().isOpen);
156
+ },
157
+ });
158
+ const emitters = new Map();
159
+ function emitterFor(routeKey) {
160
+ let emitter = emitters.get(routeKey);
161
+ if (emitter === undefined) {
162
+ emitter = createNavigationEmitter();
163
+ emitters.set(routeKey, emitter);
164
+ }
165
+ return emitter;
166
+ }
167
+ const focusedKey = createMemo(() => {
168
+ const current = state();
169
+ return current.routes[current.index]?.key;
170
+ });
171
+ // Deferred by a microtask for the same reason Tab's is: the newly focused screen's subtree, and
172
+ // the subscriptions inside it, are built by the effect flush this callback would otherwise run
173
+ // inside.
174
+ let lastFocusedKey;
175
+ createEffect(() => {
176
+ const nextKey = focusedKey();
177
+ const { blurKey, focusKey } = diffFocusedRoute(lastFocusedKey, nextKey);
178
+ if (blurKey === undefined && focusKey === undefined)
179
+ return;
180
+ lastFocusedKey = nextKey;
181
+ queueMicrotask(() => {
182
+ if (blurKey !== undefined) {
183
+ dlog(`Drawer: route "${blurKey}" blurred at t=${Date.now()}`);
184
+ emitterFor(blurKey).emit(NAVIGATION_EVENT_BLUR);
185
+ }
186
+ if (focusKey !== undefined) {
187
+ dlog(`Drawer: route "${focusKey}" focused at t=${Date.now()}`);
188
+ emitterFor(focusKey).emit(NAVIGATION_EVENT_FOCUS);
189
+ }
190
+ });
191
+ });
192
+ onCleanup(() => {
193
+ if (lastFocusedKey !== undefined) {
194
+ emitterFor(lastFocusedKey).emit(NAVIGATION_EVENT_BLUR);
195
+ }
196
+ });
197
+ function routeFor(routeKey) {
198
+ const found = state().routes.find(route => route.key === routeKey);
199
+ return found ?? { key: routeKey, name: '', params: undefined };
200
+ }
201
+ const focusedComponent = createMemo(() => {
202
+ const current = state();
203
+ const route = current.routes[current.index];
204
+ return route === undefined
205
+ ? undefined
206
+ : registry().get(route.name)?.component;
207
+ });
208
+ const content = createMemo(() => {
209
+ const routeKey = focusedKey();
210
+ const Component = focusedComponent();
211
+ return untrack(() => {
212
+ if (routeKey === undefined || Component === undefined)
213
+ return undefined;
214
+ const scope = () => ({
215
+ route: routeFor(routeKey),
216
+ navigation: handle,
217
+ emitter: emitterFor(routeKey),
218
+ parent: parentScope?.(),
219
+ });
220
+ return createComponent(NavigationScopeProvider, {
221
+ value: scope,
222
+ get children() {
223
+ return createComponent(Component, {});
224
+ },
225
+ });
226
+ });
227
+ });
228
+ const descriptors = createMemo(() => {
229
+ const map = {};
230
+ const entries = registry();
231
+ for (const route of state().routes) {
232
+ const entry = entries.get(route.name);
233
+ if (entry === undefined)
234
+ continue;
235
+ map[route.key] = {
236
+ options: resolveDrawerScreenOptions(entry, {
237
+ route,
238
+ navigation: handle,
239
+ }),
240
+ navigation: handle,
241
+ };
242
+ }
243
+ return map;
244
+ });
245
+ // Called ONCE, untracked, with an accessor - trap §4. Calling it per change (or handing it a
246
+ // value) would rebuild the whole panel subtree on every drawer state change, which mid-gesture is
247
+ // exactly how a responder grant gets lost.
248
+ //
249
+ // LAZY, though, and inside a memo of its OWN - two separate reasons, both load-bearing.
250
+ // (a) Not called from this body directly: the markers have not registered yet at that point, so a
251
+ // render prop reading `state`/`descriptors` at its own top level (which trap §4 documents as
252
+ // a frozen read) would snapshot an EMPTY navigator.
253
+ // (b) Not a plain `let` cache either. Whatever computation first CALLS the render prop OWNS every
254
+ // memo the returned subtree created, and disposes them when it re-runs - so a cached node
255
+ // handed back to a re-running slot effect is a node whose own reactivity has been torn down,
256
+ // silently frozen at its first paint. A dependency-free memo has a stable owner (this
257
+ // component) and runs exactly once, which gives both properties at the same time.
258
+ const drawerContent = createMemo(() => untrack(() => props.drawerContent?.(() => ({
259
+ state: state(),
260
+ descriptors: descriptors(),
261
+ navigation: handle,
262
+ }))));
263
+ const shape = createMemo(() => {
264
+ const options = currentOptions();
265
+ return {
266
+ type: resolveDrawerType(options),
267
+ position: resolveDrawerPosition(options),
268
+ animated: isDrawerAnimated(options),
269
+ };
270
+ }, { type: '', position: '', animated: false }, { equals: sameDrawerShape });
271
+ function buildDrawer(current) {
272
+ const order = drawerChildOrder(currentOptions());
273
+ const rootDescriptor = createMemo(() => {
274
+ const options = currentOptions();
275
+ return renderDrawer({
276
+ overlayColor: props.overlayColor ?? DRAWER_DEFAULT_OVERLAY_COLOR,
277
+ drawerStyle: props.drawerStyle,
278
+ contentPassthrough: {},
279
+ overlayPassthrough: current.animated
280
+ ? {
281
+ pointerEvents: state().isOpen ? 'auto' : 'none',
282
+ onStartShouldSetResponder: () => true,
283
+ onResponderRelease: () => {
284
+ animateProgressTo(false);
285
+ dispatch({ type: 'closeDrawer' });
286
+ },
287
+ }
288
+ : {},
289
+ panelPassthrough: {},
290
+ }, options);
291
+ });
292
+ function slotDescriptor(slot) {
293
+ const child = rootDescriptor().children[order.indexOf(slot)];
294
+ if (child === undefined || typeof child === 'string') {
295
+ throw new Error(`Drawer: renderDrawer produced no "${slot}" slot`);
296
+ }
297
+ return child;
298
+ }
299
+ // Each animated style holds an AnimatedInterpolation node, not a plain number/colour - it feeds
300
+ // only Animated.View's deliberately permissive `style?: unknown`, never the plain-IViewStyle
301
+ // branch. Recomputed reactively (geometry follows drawerWidth), which reaches the live node as
302
+ // an ordinary prop update rather than a rebuild.
303
+ function animatedStyle(slot) {
304
+ if (!current.animated)
305
+ return undefined;
306
+ const geometry = resolveDrawerGeometry(currentOptions());
307
+ // Three branches, not one call on a `slot` union: resolveDrawerSlotInterpolation is OVERLOADED
308
+ // per slot (only the overlay's result carries `opacity`), and a union argument matches no
309
+ // overload. The branch is also what makes the opacity read type-safe without a cast.
310
+ if (slot === 'overlay') {
311
+ // The overlay is a full-screen absolutely-positioned sibling BELOW content in paint order;
312
+ // for 'slide' the content itself translates away, so the overlay's translateX shares
313
+ // content's range rather than staying pinned full-screen.
314
+ const overlay = resolveDrawerSlotInterpolation(geometry, 'overlay');
315
+ return {
316
+ opacity: progress.interpolate(overlay.opacity),
317
+ transform: [{ translateX: progress.interpolate(overlay.translateX) }],
318
+ };
319
+ }
320
+ const interpolation = slot === 'content'
321
+ ? resolveDrawerSlotInterpolation(geometry, 'content')
322
+ : resolveDrawerSlotInterpolation(geometry, 'panel');
323
+ return {
324
+ transform: [
325
+ { translateX: progress.interpolate(interpolation.translateX) },
326
+ ],
327
+ };
328
+ }
329
+ function slotChildren(slot) {
330
+ if (slot === 'content')
331
+ return content();
332
+ if (slot === 'panel')
333
+ return drawerContent();
334
+ return undefined;
335
+ }
336
+ function buildSlot(slot) {
337
+ const style = animatedStyle(slot);
338
+ if (style === undefined) {
339
+ const node = hostElement(slotDescriptor(slot).type, () => slotDescriptor(slot).props);
340
+ const children = slotChildren(slot);
341
+ if (children !== undefined)
342
+ insert(node, children);
343
+ return node;
344
+ }
345
+ // ONE source, never a spread followed by an explicit prop: mergeProps takes the first
346
+ // NON-undefined value scanning back-to-front, so a later `undefined` silently loses to an
347
+ // earlier value (.claude/rules/solid-descriptor-bridge.md §6). Collapsing to a single bag
348
+ // removes the question. mergeProps still earns its place - it wraps the function source in a
349
+ // memo and hands Animated.View a reactive proxy, which is what keeps the slot's props live
350
+ // without rebuilding the node.
351
+ return createComponent(Animated.View, mergeProps(() => ({
352
+ ...slotDescriptor(slot).props,
353
+ style: [slotDescriptor(slot).props.style, animatedStyle(slot)],
354
+ get children() {
355
+ return slotChildren(slot);
356
+ },
357
+ })));
358
+ }
359
+ const root = hostElement('symbiote-view', () => ({
360
+ ...rootDescriptor().props,
361
+ ...panResponder.panHandlers,
362
+ }));
363
+ insert(root, order.map(buildSlot));
364
+ return root;
365
+ }
366
+ const drawer = createMemo(() => {
367
+ const current = shape();
368
+ return untrack(() => buildDrawer(current));
369
+ });
370
+ const host = hostElement('symbiote-view', () => ({ style: { flex: 1 } }));
371
+ insert(host, drawer);
372
+ return createComponent(ScreenCollectorProvider, {
373
+ value: collector,
374
+ get children() {
375
+ return [props.children, host];
376
+ },
377
+ });
378
+ }
379
+ export const Drawer = Object.assign(DrawerImpl, { Screen: DrawerScreen });
380
+ // --- Explicit gap list vs the real react-native-gesture-handler + react-native-reanimated
381
+ // @react-navigation/drawer (mirrored verbatim from the React twin - the gaps are architectural,
382
+ // not adapter-specific) ---
383
+ // 1. `configureGestureHandler` - a raw react-native-gesture-handler `Gesture` object escape hatch.
384
+ // No PanResponder equivalent exists; not ported.
385
+ // 2. Simultaneous/failure gesture RELATIONSHIPS (gesture-handler's declarative composition vs a
386
+ // nested ScrollView, another PanResponder, etc.) - PanResponder only offers negotiation via the
387
+ // should-set boolean gates used here (edge-start + dominant-axis), which is more prone to an
388
+ // accidental hijack of a nested horizontal ScrollView/Swiper.
389
+ // 3. `useDrawerProgress` - a Reanimated SharedValue read on the UI thread. `progress` here is a
390
+ // JS-thread AnimatedValue; interpolating it works, but without native-driver wiring it does not
391
+ // carry the same synchronous UI-thread guarantee under JS-thread load.
392
+ // 4. `hideStatusBarOnOpen` / `keyboardDismissMode` / `statusBarAnimation` / `overlayStyle` - not
393
+ // wired in this pass; straightforward additions once StatusBar/Keyboard module wiring is needed
394
+ // here (not a PanResponder/Animated limitation, just unscoped for v1).
@@ -0,0 +1,13 @@
1
+ import type { Component } from 'solid-js';
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,2 @@
1
+ // Drawer.Screen's public prop types. Same split rationale as screen-props.ts.
2
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ISymbioteNode } from '@symbiote-native/engine';
2
+ export declare function hostElement(type: string, props: () => Record<string, unknown>): ISymbioteNode;
@@ -0,0 +1,23 @@
1
+ // Building a RAW Fabric host element (RNSScreenStack, RNSScreen, RNSScreenContentWrapper) from
2
+ // Solid, without JSX.
3
+ //
4
+ // The whole `src/solid/**` tree is plain `.ts`, matching what every other adapter's navigation
5
+ // entry does (React calls createElement, Vue calls h, Svelte writes .svelte, Angular writes
6
+ // templates) - and here it is also what keeps ONE tsconfig per package honest: this package's `jsx`
7
+ // setting belongs to its React entry, and a `.tsx` file compiled under it would be type-checked
8
+ // against React's JSX namespace, not Solid's (.claude/rules/solid-jsx-namespace.md).
9
+ //
10
+ // It routes through descriptorToSolid rather than calling createElement/spread directly ON PURPOSE:
11
+ // that bridge already wraps the prop accessor in `withStableKeys`, which is the fix for Solid's
12
+ // `spread` having no removal pass - a key that VANISHES between runs would otherwise keep its last
13
+ // value on the native view forever (.claude/rules/solid-descriptor-bridge.md §1). A screen's
14
+ // resolved prop bag genuinely changes key set between options folds, so this is live, not
15
+ // theoretical. Re-implementing the widening here would be a second copy of a helper the adapter
16
+ // keeps private for exactly that reason.
17
+ //
18
+ // `children: []` because every caller mounts real Solid children with `insert` afterwards, not
19
+ // Descriptor children. The empty list keeps the shape guard's child count stable across recomputes.
20
+ import { descriptorToSolid } from '@symbiote-native/solid';
21
+ export function hostElement(type, props) {
22
+ return descriptorToSolid(() => ({ type, props: props(), children: [] }));
23
+ }
@@ -0,0 +1,19 @@
1
+ import '../register';
2
+ export { Stack } from './stack';
3
+ export type { INavigatorHandle, IStackProps } from './stack';
4
+ export { Screen, TabScreen, DrawerScreen } from './screen';
5
+ export type { IScreenOptionsArgs, IScreenOptionsResolver, IScreenProps, ISolidScreenOptions, ISolidSearchBarOptions, } from './screen-props';
6
+ export type { IScreenOptions, IStackAnimation, IStackPresentation, } from '../core';
7
+ export { useNavigation, useStackNavigation, useTabNavigation, useDrawerNavigation, useRoute, createIsFocused, createFocusEffect, createNavigationState, } from './primitives';
8
+ export type { INavigationHandle, IStackNavigationHandle, ITabNavigationHandle, IDrawerNavigationHandle, } from './primitives';
9
+ export type { INavigationScopeValue, IAnyNavigatorHandle, } from './navigation-context';
10
+ export { createLinkingIntegration } from './linking';
11
+ export type { ILinkingConfig, IScreenLinkingConfig } from '../core';
12
+ export { Tab } from './tabs';
13
+ export type { ITabNavigatorHandle, ITabProps } from './tabs';
14
+ export type { ITabScreenOptionsArgs, ITabScreenOptionsResolver, ITabScreenProps, } from './tab-screen-props';
15
+ export type { ITabOptions, ITabBarIcon } from '../core';
16
+ export { Drawer } from './drawer';
17
+ export type { IDrawerContentSlotProps, IDrawerNavigatorHandle, IDrawerProps, IDrawerDescriptorMap, } from './drawer';
18
+ export type { IDrawerScreenOptionsArgs, IDrawerScreenOptionsResolver, IDrawerScreenProps, } from './drawer-screen-props';
19
+ export type { IDrawerOptions, IDrawerScreenOptions, IDrawerType, IDrawerPosition, } from '../core';
@@ -0,0 +1,24 @@
1
+ // @symbiote-native/navigation/solid: the Solid native-stack navigator over react-native-screens'
2
+ // native view primitives. Importing this barrel first registers the native views' ViewConfigs
3
+ // (../register, a side-effect import of the codegen specs - never react-native-screens' own React
4
+ // components), then exposes Stack (with Stack.Screen attached) and the navigator handle. Mirrors
5
+ // vue/index.ts's and svelte/index.ts's barrel shape.
6
+ //
7
+ // The lifecycle helpers split by what they DO, not by a blanket prefix: `use*` for the ones that
8
+ // only consume the navigation scope already on the owner chain (useNavigation / useRoute and the
9
+ // three narrowing variants - Solid's own `useContext` sense), `create*` for the ones that own a
10
+ // signal, an effect or a subscription (createIsFocused / createFocusEffect / createNavigationState
11
+ // / createLinkingIntegration). Same rule the adapter's `createColorScheme` follows; a blanket `use`
12
+ // would read as a foreign ecosystem's convention (<adapter_src_follows_framework_idioms>).
13
+ import '../register.js';
14
+ export { Stack } from './stack/index.js';
15
+ export { Screen, TabScreen, DrawerScreen } from './screen.js';
16
+ export { useNavigation, useStackNavigation, useTabNavigation, useDrawerNavigation, useRoute, createIsFocused, createFocusEffect, createNavigationState, } from './primitives/index.js';
17
+ export { createLinkingIntegration } from './linking.js';
18
+ // Tab: the bottom-tabs navigator, a PURE-JS UI (no react-native-screens views involved, so no extra
19
+ // ViewConfig registration is needed beyond the ../register import above).
20
+ export { Tab } from './tabs/index.js';
21
+ // Drawer: the swipeable drawer navigator, a PURE-JS UI (PanResponder + Animated). See
22
+ // drawer/index.ts's header for the feasibility note re: react-native-gesture-handler /
23
+ // react-native-reanimated parity gaps.
24
+ export { Drawer } from './drawer/index.js';
@@ -0,0 +1,3 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import type { ILinkingConfig, INavigatorHandle } from '../core';
3
+ export declare function createLinkingIntegration(config: ILinkingConfig, navigatorHandle: Accessor<INavigatorHandle | null>): void;
@@ -0,0 +1,55 @@
1
+ // createLinkingIntegration: wires the framework-agnostic linking config (../core's
2
+ // resolveRouteFromUrl) onto an existing Stack's INavigatorHandle. Deliberately NOT called from
3
+ // inside ./stack - app code owns the wiring, the same top-level-prop shape @react-navigation keeps
4
+ // `linking` in on NavigationContainer rather than baking it into the router itself.
5
+ //
6
+ // `create*`, not `use*`: it owns a Linking subscription and its teardown. (Vue and Svelte spell it
7
+ // `useLinkingIntegration` because their ecosystems put every lifecycle helper behind `use`; Solid
8
+ // reserves that prefix for consuming something that already exists -
9
+ // <adapter_src_follows_framework_idioms>.)
10
+ //
11
+ // Takes an ACCESSOR of the handle, not the handle: `<Stack ref={setHandle}>` resolves after the
12
+ // navigator has been created, so an eagerly-passed value would be `null` at the call site. Reading
13
+ // it lazily inside onMount gives the same "already populated" guarantee React's layout-effect
14
+ // ordering does - a Solid `ref` on a component is called during that component's own creation,
15
+ // which is strictly before any effect runs.
16
+ import { onCleanup, onMount } from 'solid-js';
17
+ import { Linking, dlog } from '@symbiote-native/engine';
18
+ import { resolveRouteFromUrl } from '../core/index.js';
19
+ export function createLinkingIntegration(config, navigatorHandle) {
20
+ let cancelled = false;
21
+ let subscription;
22
+ function applyRoute(url, dispatch) {
23
+ const route = resolveRouteFromUrl(config, url);
24
+ if (route === null) {
25
+ dlog(`createLinkingIntegration: no route resolved for "${url}"`);
26
+ return;
27
+ }
28
+ dispatch(route.name, route.params);
29
+ }
30
+ onMount(() => {
31
+ const handle = navigatorHandle();
32
+ if (handle === null) {
33
+ dlog('createLinkingIntegration: navigatorHandle is not yet available at mount time');
34
+ return;
35
+ }
36
+ Linking.getInitialURL()
37
+ .then(url => {
38
+ if (cancelled || url === null)
39
+ return;
40
+ dlog(`createLinkingIntegration: initial URL "${url}"`);
41
+ applyRoute(url, handle.replace);
42
+ })
43
+ .catch((error) => {
44
+ dlog(`createLinkingIntegration: getInitialURL failed: ${String(error)}`);
45
+ });
46
+ subscription = Linking.addEventListener('url', (event) => {
47
+ dlog(`createLinkingIntegration: url event "${event.url}"`);
48
+ applyRoute(event.url, handle.push);
49
+ });
50
+ });
51
+ onCleanup(() => {
52
+ cancelled = true;
53
+ subscription?.remove();
54
+ });
55
+ }
@@ -0,0 +1,13 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import type { IAnyNavigatorHandle, INavigationEmitter, IRoute } from '../core';
3
+ export type { IAnyNavigatorHandle };
4
+ export type INavigationScopeValue = {
5
+ route: IRoute<unknown>;
6
+ navigation: IAnyNavigatorHandle;
7
+ emitter: INavigationEmitter;
8
+ parent?: INavigationScopeValue;
9
+ };
10
+ export type INavigationScope = Accessor<INavigationScopeValue>;
11
+ export declare const NavigationScopeProvider: import("solid-js").ContextProviderComponent<INavigationScope | undefined>;
12
+ export declare function useNavigationScope(): INavigationScope | undefined;
13
+ export declare function requireNavigationScope(functionName: string): INavigationScope;
@@ -0,0 +1,37 @@
1
+ // The lifecycle half of the framework-agnostic emitter (../core/navigation-events). Solid's twin of
2
+ // React's Context / Vue's provide-inject / Svelte's setContext is `createContext` + a Provider over
3
+ // one shared key, resolved along the OWNER chain - so a screen's subtree reads its own route,
4
+ // navigator handle and per-route emitter without prop-drilling.
5
+ //
6
+ // THE VALUE IS AN ACCESSOR, never a route object. A Solid component body runs once and a context
7
+ // value is captured once, so handing over `{ route, ... }` directly would freeze every consumer at
8
+ // the route as it looked when the screen mounted - `setParams` produces a NEW route object under
9
+ // the same key and would never reach `useRoute()`. This is the same accessors-not-values rule
10
+ // descriptorToSolid is built on, one layer up (.claude/rules/solid-descriptor-bridge.md).
11
+ //
12
+ // Each navigator mounts ONE Provider per live route (keyed by route.key, exactly like React's
13
+ // per-route `<NavigationContext.Provider>`), so sibling screens never see each other's scope -
14
+ // an inherent property of any hierarchical dependency-injection scheme, not a React artifact.
15
+ //
16
+ // `parent` threads the ambient value a navigator read on ITS OWN mount (undefined at the root) into
17
+ // the value it provides to its own screens, forming a linked list - this is what lets a screen
18
+ // nested inside e.g. a Stack-screen-renders-a-Tab composition reach the enclosing Stack via
19
+ // useNavigation().getParent().
20
+ import { createContext, useContext } from 'solid-js';
21
+ const NavigationScopeContext = createContext();
22
+ export const NavigationScopeProvider = NavigationScopeContext.Provider;
23
+ // undefined simply means "no ancestor scope", which is a legitimate state (the nesting root a
24
+ // navigator itself reads on its own mount) rather than an error - callers that require one go
25
+ // through requireNavigationScope below.
26
+ export function useNavigationScope() {
27
+ return useContext(NavigationScopeContext);
28
+ }
29
+ // The "throw if missing" half every primitive in ./primitives needs - they all read the scope and
30
+ // threw the same shaped error, differing only in which function name the message names.
31
+ export function requireNavigationScope(functionName) {
32
+ const scope = useNavigationScope();
33
+ if (scope === undefined) {
34
+ throw new Error(`${functionName} must be used within a screen rendered by <Stack>, <Tab>, or <Drawer>`);
35
+ }
36
+ return scope;
37
+ }
@@ -0,0 +1 @@
1
+ export declare function createFocusEffect(effect: () => (() => void) | void): void;
@@ -0,0 +1,30 @@
1
+ // Mirrors @react-navigation's useFocusEffect: run `effect` when the screen gains focus, run its
2
+ // returned cleanup when it loses focus (and once more on teardown).
3
+ //
4
+ // `create*`: it owns two subscriptions and the effect's cleanup slot.
5
+ //
6
+ // React's version requires memoizing `effect` (useCallback), since a new identity re-subscribes it
7
+ // like any other useEffect dependency. That does not apply here: a Solid body runs once, so
8
+ // `effect` is read once and closed over directly - there is no dependency array to go stale.
9
+ import { onCleanup } from 'solid-js';
10
+ import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS } from '../../core/index.js';
11
+ import { requireNavigationScope } from '../navigation-context.js';
12
+ export function createFocusEffect(effect) {
13
+ const scope = requireNavigationScope('createFocusEffect');
14
+ let cleanup;
15
+ const runEffect = () => {
16
+ cleanup = effect();
17
+ };
18
+ const runCleanup = () => {
19
+ cleanup?.();
20
+ cleanup = undefined;
21
+ };
22
+ const { emitter } = scope();
23
+ const unsubscribeFocus = emitter.addListener(NAVIGATION_EVENT_FOCUS, runEffect);
24
+ const unsubscribeBlur = emitter.addListener(NAVIGATION_EVENT_BLUR, runCleanup);
25
+ onCleanup(() => {
26
+ unsubscribeFocus();
27
+ unsubscribeBlur();
28
+ runCleanup();
29
+ });
30
+ }
@@ -0,0 +1,2 @@
1
+ import type { Accessor } from 'solid-js';
2
+ export declare function createIsFocused(): Accessor<boolean>;