@symbiote-native/navigation 2.1.0 → 2.2.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 (234) hide show
  1. package/README.md +42 -12
  2. package/build/angular/drawer/index.js +23 -9
  3. package/build/angular/index.d.ts +4 -4
  4. package/build/angular/injectors/inject-navigation.js +1 -1
  5. package/build/angular/navigation-scope.directive.js +1 -1
  6. package/build/angular/search-bar-ref.directive.js +1 -1
  7. package/build/angular/stack/index.js +20 -6
  8. package/build/angular/tabs/index.js +21 -7
  9. package/build/core/drawer-options/index.js +19 -5
  10. package/build/core/drawer-router-state/index.js +8 -2
  11. package/build/core/guards.js +3 -1
  12. package/build/core/header-bar-buttons.js +14 -4
  13. package/build/core/index.d.ts +4 -4
  14. package/build/core/index.js +2 -2
  15. package/build/core/navigator-state/index.js +12 -4
  16. package/build/core/render-drawer.js +4 -1
  17. package/build/core/render-stack.js +13 -4
  18. package/build/core/screen-options.js +1 -1
  19. package/build/core/search-bar-commands.js +1 -1
  20. package/build/core/state-persistence/index.js +6 -2
  21. package/build/core/tab-router-state/index.js +8 -2
  22. package/build/react/drawer/index.js +36 -13
  23. package/build/react/index.d.ts +7 -7
  24. package/build/react/index.js +2 -2
  25. package/build/react/stack/index.js +34 -8
  26. package/build/react/tabs/index.js +14 -4
  27. package/build/solid/drawer/index.d.ts +22 -0
  28. package/build/solid/drawer/index.js +394 -0
  29. package/build/solid/drawer-screen-props.d.ts +13 -0
  30. package/build/solid/drawer-screen-props.js +2 -0
  31. package/build/solid/host.d.ts +2 -0
  32. package/build/solid/host.js +23 -0
  33. package/build/solid/index.d.ts +19 -0
  34. package/build/solid/index.js +24 -0
  35. package/build/solid/linking.d.ts +3 -0
  36. package/build/solid/linking.js +55 -0
  37. package/build/solid/navigation-context.d.ts +13 -0
  38. package/build/solid/navigation-context.js +37 -0
  39. package/build/solid/primitives/create-focus-effect.d.ts +1 -0
  40. package/build/solid/primitives/create-focus-effect.js +30 -0
  41. package/build/solid/primitives/create-is-focused.d.ts +2 -0
  42. package/build/solid/primitives/create-is-focused.js +31 -0
  43. package/build/solid/primitives/create-navigation-state.d.ts +3 -0
  44. package/build/solid/primitives/create-navigation-state.js +30 -0
  45. package/build/solid/primitives/index.d.ts +12 -0
  46. package/build/solid/primitives/index.js +8 -0
  47. package/build/solid/primitives/use-drawer-navigation.d.ts +5 -0
  48. package/build/solid/primitives/use-drawer-navigation.js +15 -0
  49. package/build/solid/primitives/use-navigation.d.ts +8 -0
  50. package/build/solid/primitives/use-navigation.js +23 -0
  51. package/build/solid/primitives/use-route.d.ts +3 -0
  52. package/build/solid/primitives/use-route.js +14 -0
  53. package/build/solid/primitives/use-stack-navigation.d.ts +5 -0
  54. package/build/solid/primitives/use-stack-navigation.js +19 -0
  55. package/build/solid/primitives/use-tab-navigation.d.ts +5 -0
  56. package/build/solid/primitives/use-tab-navigation.js +15 -0
  57. package/build/solid/screen-props.d.ts +19 -0
  58. package/build/solid/screen-props.js +9 -0
  59. package/build/solid/screen-registry.d.ts +31 -0
  60. package/build/solid/screen-registry.js +86 -0
  61. package/build/solid/screen.d.ts +6 -0
  62. package/build/solid/screen.js +69 -0
  63. package/build/solid/stack/index.d.ts +15 -0
  64. package/build/solid/stack/index.js +348 -0
  65. package/build/solid/tab-screen-props.d.ts +13 -0
  66. package/build/solid/tab-screen-props.js +3 -0
  67. package/build/solid/tabs/index.d.ts +14 -0
  68. package/build/solid/tabs/index.js +228 -0
  69. package/build/svelte/descriptor-subtree.js +3 -1
  70. package/build/svelte/drawer/index.svelte +163 -25
  71. package/build/svelte/drawer-screen.svelte +2 -1
  72. package/build/svelte/index.d.ts +3 -3
  73. package/build/svelte/navigation-scope.svelte +4 -1
  74. package/build/svelte/runes/use-navigation.svelte.js +5 -1
  75. package/build/svelte/stack/index.svelte +43 -6
  76. package/build/svelte/stack/stack-screen.svelte +66 -9
  77. package/build/svelte/tabs/index.svelte +42 -20
  78. package/build/vue/composables/use-navigation.js +5 -1
  79. package/build/vue/drawer/index.js +30 -8
  80. package/build/vue/index.d.ts +3 -3
  81. package/build/vue/navigation-context.js +1 -1
  82. package/build/vue/stack/index.js +17 -6
  83. package/build/vue/tabs/index.js +18 -5
  84. package/build-ngc/angular/drawer/index.js +31 -11
  85. package/build-ngc/angular/drawer/index.js.map +1 -1
  86. package/build-ngc/angular/index.d.ts +4 -4
  87. package/build-ngc/angular/index.js.map +1 -1
  88. package/build-ngc/angular/injectors/inject-focus-effect.js.map +1 -1
  89. package/build-ngc/angular/injectors/inject-is-focused.js.map +1 -1
  90. package/build-ngc/angular/injectors/inject-navigation-state.js.map +1 -1
  91. package/build-ngc/angular/injectors/inject-navigation.js +1 -1
  92. package/build-ngc/angular/injectors/inject-navigation.js.map +1 -1
  93. package/build-ngc/angular/linking.js.map +1 -1
  94. package/build-ngc/angular/navigation-context.service.js.map +1 -1
  95. package/build-ngc/angular/navigation-scope.directive.js +1 -1
  96. package/build-ngc/angular/navigation-scope.directive.js.map +1 -1
  97. package/build-ngc/angular/screen.directive.js.map +1 -1
  98. package/build-ngc/angular/search-bar-ref.directive.js +1 -1
  99. package/build-ngc/angular/search-bar-ref.directive.js.map +1 -1
  100. package/build-ngc/angular/stack/index.js +26 -8
  101. package/build-ngc/angular/stack/index.js.map +1 -1
  102. package/build-ngc/angular/tab-screen.directive.js.map +1 -1
  103. package/build-ngc/angular/tabs/index.js +21 -7
  104. package/build-ngc/angular/tabs/index.js.map +1 -1
  105. package/build-ngc/core/constants.js.map +1 -1
  106. package/build-ngc/core/drawer-options/index.js +19 -5
  107. package/build-ngc/core/drawer-options/index.js.map +1 -1
  108. package/build-ngc/core/drawer-router-state/index.js +8 -2
  109. package/build-ngc/core/drawer-router-state/index.js.map +1 -1
  110. package/build-ngc/core/guards.js +3 -1
  111. package/build-ngc/core/guards.js.map +1 -1
  112. package/build-ngc/core/header-bar-buttons.js +14 -4
  113. package/build-ngc/core/header-bar-buttons.js.map +1 -1
  114. package/build-ngc/core/index.d.ts +4 -4
  115. package/build-ngc/core/index.js +2 -2
  116. package/build-ngc/core/index.js.map +1 -1
  117. package/build-ngc/core/linking-config/index.js.map +1 -1
  118. package/build-ngc/core/navigation-events/index.js.map +1 -1
  119. package/build-ngc/core/navigator-handles/index.js.map +1 -1
  120. package/build-ngc/core/navigator-state/index.js +12 -4
  121. package/build-ngc/core/navigator-state/index.js.map +1 -1
  122. package/build-ngc/core/render-drawer.js +4 -1
  123. package/build-ngc/core/render-drawer.js.map +1 -1
  124. package/build-ngc/core/render-stack.js +13 -4
  125. package/build-ngc/core/render-stack.js.map +1 -1
  126. package/build-ngc/core/render-tabs.js.map +1 -1
  127. package/build-ngc/core/screen-options.js +1 -1
  128. package/build-ngc/core/screen-options.js.map +1 -1
  129. package/build-ngc/core/search-bar-commands.js +1 -1
  130. package/build-ngc/core/search-bar-commands.js.map +1 -1
  131. package/build-ngc/core/stack-router-state/index.js.map +1 -1
  132. package/build-ngc/core/state-persistence/index.js +6 -2
  133. package/build-ngc/core/state-persistence/index.js.map +1 -1
  134. package/build-ngc/core/tab-router-state/index.js +8 -2
  135. package/build-ngc/core/tab-router-state/index.js.map +1 -1
  136. package/package.json +23 -9
  137. package/src/angular/drawer/index.ts +69 -25
  138. package/src/angular/index.ts +20 -4
  139. package/src/angular/injectors/inject-focus-effect.ts +8 -2
  140. package/src/angular/injectors/inject-is-focused.ts +6 -4
  141. package/src/angular/injectors/inject-navigation-state.ts +7 -4
  142. package/src/angular/injectors/inject-navigation.ts +12 -3
  143. package/src/angular/linking.ts +4 -1
  144. package/src/angular/navigation-context.service.ts +5 -2
  145. package/src/angular/navigation-scope.directive.ts +6 -2
  146. package/src/angular/screen.directive.ts +13 -3
  147. package/src/angular/search-bar-ref.directive.ts +10 -2
  148. package/src/angular/stack/index.ts +68 -22
  149. package/src/angular/tab-screen.directive.ts +3 -1
  150. package/src/angular/tabs/index.ts +36 -12
  151. package/src/core/constants.ts +6 -3
  152. package/src/core/drawer-options/index.ts +40 -13
  153. package/src/core/drawer-router-state/index.ts +11 -3
  154. package/src/core/guards.ts +3 -1
  155. package/src/core/header-bar-buttons.ts +41 -12
  156. package/src/core/index.ts +18 -4
  157. package/src/core/linking-config/index.ts +31 -10
  158. package/src/core/navigation-events/index.ts +8 -2
  159. package/src/core/navigator-handles/index.ts +8 -3
  160. package/src/core/navigator-props.ts +13 -4
  161. package/src/core/navigator-state/index.ts +24 -7
  162. package/src/core/render-drawer.ts +24 -9
  163. package/src/core/render-stack.ts +52 -13
  164. package/src/core/render-tabs.ts +24 -6
  165. package/src/core/screen-options.ts +15 -5
  166. package/src/core/search-bar-commands.ts +12 -3
  167. package/src/core/stack-router-state/index.ts +5 -2
  168. package/src/core/state-persistence/index.ts +13 -3
  169. package/src/core/tab-router-state/index.ts +10 -3
  170. package/src/react/drawer/index.ts +408 -344
  171. package/src/react/drawer-screen.ts +8 -2
  172. package/src/react/hooks/use-focus-effect.ts +8 -2
  173. package/src/react/hooks/use-is-focused.ts +6 -2
  174. package/src/react/hooks/use-navigation.ts +8 -2
  175. package/src/react/index.ts +37 -7
  176. package/src/react/linking.ts +4 -1
  177. package/src/react/navigation-context.ts +6 -2
  178. package/src/react/screen.ts +12 -3
  179. package/src/react/stack/index.ts +294 -231
  180. package/src/react/tab-screen.ts +3 -1
  181. package/src/react/tabs/index.ts +188 -158
  182. package/src/solid/drawer/index.ts +570 -0
  183. package/src/solid/drawer-screen-props.ts +24 -0
  184. package/src/solid/host.ts +29 -0
  185. package/src/solid/index.ts +87 -0
  186. package/src/solid/linking.ts +74 -0
  187. package/src/solid/navigation-context.ts +56 -0
  188. package/src/solid/primitives/create-focus-effect.ts +41 -0
  189. package/src/solid/primitives/create-is-focused.ts +41 -0
  190. package/src/solid/primitives/create-navigation-state.ts +44 -0
  191. package/src/solid/primitives/index.ts +12 -0
  192. package/src/solid/primitives/use-drawer-navigation.ts +25 -0
  193. package/src/solid/primitives/use-navigation.ts +44 -0
  194. package/src/solid/primitives/use-route.ts +18 -0
  195. package/src/solid/primitives/use-stack-navigation.ts +29 -0
  196. package/src/solid/primitives/use-tab-navigation.ts +25 -0
  197. package/src/solid/screen-props.ts +51 -0
  198. package/src/solid/screen-registry.ts +159 -0
  199. package/src/solid/screen.ts +80 -0
  200. package/src/solid/stack/index.ts +528 -0
  201. package/src/solid/tab-screen-props.ts +21 -0
  202. package/src/solid/tabs/index.ts +327 -0
  203. package/src/svelte/attachments.ts +3 -1
  204. package/src/svelte/descriptor-subtree.ts +3 -1
  205. package/src/svelte/drawer/index.svelte +163 -25
  206. package/src/svelte/drawer-screen-props.ts +8 -2
  207. package/src/svelte/drawer-screen.svelte +2 -1
  208. package/src/svelte/fabric-tree.test-helper.ts +37 -3
  209. package/src/svelte/index.ts +15 -3
  210. package/src/svelte/linking.svelte.ts +11 -5
  211. package/src/svelte/navigation-scope.svelte +4 -1
  212. package/src/svelte/runes/use-drawer-navigation.svelte.ts +3 -1
  213. package/src/svelte/runes/use-focus-effect.svelte.ts +8 -2
  214. package/src/svelte/runes/use-navigation-state.svelte.ts +3 -1
  215. package/src/svelte/runes/use-navigation.svelte.ts +13 -3
  216. package/src/svelte/runes/use-stack-navigation.svelte.ts +3 -1
  217. package/src/svelte/screen-props.ts +7 -2
  218. package/src/svelte/screen-registry.ts +9 -3
  219. package/src/svelte/stack/index.svelte +43 -6
  220. package/src/svelte/stack/stack-screen.svelte +66 -9
  221. package/src/svelte/svelte-compile.test-helper.ts +16 -12
  222. package/src/svelte/tab-screen-props.ts +3 -1
  223. package/src/svelte/tabs/index.svelte +42 -20
  224. package/src/vue/composables/use-navigation-state.ts +7 -4
  225. package/src/vue/composables/use-navigation.ts +13 -3
  226. package/src/vue/drawer/index.ts +122 -36
  227. package/src/vue/drawer-screen.ts +12 -5
  228. package/src/vue/index.ts +15 -3
  229. package/src/vue/linking.ts +7 -2
  230. package/src/vue/navigation-context.ts +17 -5
  231. package/src/vue/screen.ts +7 -2
  232. package/src/vue/stack/index.ts +82 -34
  233. package/src/vue/tab-screen.ts +3 -1
  234. package/src/vue/tabs/index.ts +51 -13
@@ -0,0 +1,570 @@
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
+
28
+ import {
29
+ createEffect,
30
+ createMemo,
31
+ createSignal,
32
+ mergeProps,
33
+ onCleanup,
34
+ untrack,
35
+ } from 'solid-js';
36
+ import type { Accessor } from 'solid-js';
37
+ import { Animated, createWindowDimensions } from '@symbiote-native/solid';
38
+ import type { IAnimatedComponentProps } from '@symbiote-native/solid';
39
+ import type { JSX } from '@symbiote-native/solid/jsx-runtime';
40
+ // createComponent from solid-js, insert from the renderer - see stack/index.ts's note.
41
+ import { createComponent } from 'solid-js';
42
+ import { insert } from '@symbiote-native/solid/renderer';
43
+ import { PanResponder, dlog } from '@symbiote-native/engine';
44
+ import type {
45
+ IPanResponderGestureState,
46
+ IStyleProp,
47
+ ISymbioteEvent,
48
+ ISymbioteNode,
49
+ IViewStyle,
50
+ } from '@symbiote-native/engine';
51
+ import type { IDescriptor } from '@symbiote-native/components';
52
+ import {
53
+ DRAWER_DEFAULT_OVERLAY_COLOR,
54
+ NAVIGATION_EVENT_BLUR,
55
+ NAVIGATION_EVENT_FOCUS,
56
+ createInitialDrawerRouterState,
57
+ createNavigationEmitter,
58
+ diffFocusedRoute,
59
+ drawerChildOrder,
60
+ drawerRouterReducer,
61
+ isDrawerAnimated,
62
+ renderDrawer,
63
+ resolveDragProgress,
64
+ resolveDrawerGeometry,
65
+ resolveDrawerPosition,
66
+ resolveDrawerSlotInterpolation,
67
+ resolveDrawerType,
68
+ resolveSwipeIntent,
69
+ shouldClaimDrawerSwipe,
70
+ } from '../../core';
71
+ import type {
72
+ IDrawerDescriptorMap,
73
+ IDrawerNavigatorHandle,
74
+ IDrawerOptions,
75
+ IDrawerRouterState,
76
+ IDrawerScreenOptions,
77
+ IDrawerSlot,
78
+ INavigationEmitter,
79
+ IRoute,
80
+ } from '../../core';
81
+ import { hostElement } from '../host';
82
+ import {
83
+ NavigationScopeProvider,
84
+ useNavigationScope,
85
+ } from '../navigation-context';
86
+ import type { INavigationScope } from '../navigation-context';
87
+ import {
88
+ ScreenCollectorProvider,
89
+ createScreenSignal,
90
+ toRegistry,
91
+ } from '../screen-registry';
92
+ import type { IRegisteredScreen } from '../screen-registry';
93
+ import { DrawerScreen } from '../screen';
94
+ import type {
95
+ IDrawerScreenOptionsArgs,
96
+ IDrawerScreenProps,
97
+ } from '../drawer-screen-props';
98
+
99
+ export type { IDrawerNavigatorHandle, IDrawerDescriptorMap } from '../../core';
100
+
101
+ // React's `renderDrawerContent` render-PROP and Vue's `drawerContent` scoped slot become a render
102
+ // prop over an ACCESSOR here - the shape Solid core itself uses for `<For>{(item) => …}`.
103
+ export type IDrawerContentSlotProps = {
104
+ state: IDrawerRouterState;
105
+ descriptors: IDrawerDescriptorMap;
106
+ navigation: IDrawerNavigatorHandle;
107
+ };
108
+
109
+ export type IDrawerProps = IDrawerOptions & {
110
+ initialRouteName?: string;
111
+ drawerStyle?: IStyleProp<IViewStyle>;
112
+ drawerContent?: (props: Accessor<IDrawerContentSlotProps>) => JSX.Element;
113
+ ref?: (handle: IDrawerNavigatorHandle) => void;
114
+ children?: JSX.Element;
115
+ };
116
+
117
+ const DRAWER_SNAP_DURATION = 250;
118
+
119
+ let navigatorSequence = 0;
120
+
121
+ type IDrawerScreenEntry = IRegisteredScreen<IDrawerScreenProps['options']>;
122
+
123
+ function resolveDrawerScreenOptions(
124
+ entry: IDrawerScreenEntry,
125
+ args: IDrawerScreenOptionsArgs,
126
+ ): IDrawerScreenOptions {
127
+ if (typeof entry.options === 'function') return entry.options(args);
128
+ return entry.options ?? {};
129
+ }
130
+
131
+ // What makes renderDrawer's Descriptor a DIFFERENT tree rather than the same tree with new props.
132
+ type IDrawerShape = { type: string; position: string; animated: boolean };
133
+
134
+ function sameDrawerShape(a: IDrawerShape, b: IDrawerShape): boolean {
135
+ return (
136
+ a.type === b.type && a.position === b.position && a.animated === b.animated
137
+ );
138
+ }
139
+
140
+ function DrawerImpl(props: IDrawerProps): JSX.Element {
141
+ const parentScope = useNavigationScope();
142
+
143
+ const { screens, collector } = createScreenSignal<
144
+ 'drawer',
145
+ IDrawerScreenProps['options']
146
+ >('drawer');
147
+ const registry = createMemo(() => toRegistry(screens()));
148
+
149
+ navigatorSequence += 1;
150
+ const routeIdPrefix = `drawer-${navigatorSequence}`;
151
+
152
+ // Every field read live off `props`, so a gesture callback (built once, below) always negotiates
153
+ // against the CURRENT options - Solid props are getters, which is what removes React's
154
+ // useRef(value) mirroring here.
155
+ function currentOptions(): IDrawerOptions {
156
+ return {
157
+ drawerType: props.drawerType,
158
+ drawerPosition: props.drawerPosition,
159
+ drawerWidth: props.drawerWidth,
160
+ overlayColor: props.overlayColor,
161
+ swipeEnabled: props.swipeEnabled,
162
+ swipeEdgeWidth: props.swipeEdgeWidth,
163
+ swipeMinDistance: props.swipeMinDistance,
164
+ swipeMinVelocity: props.swipeMinVelocity,
165
+ };
166
+ }
167
+
168
+ // Route keys are derived from the screen NAME, so the derivation is pure and re-runnable.
169
+ const routes = createMemo<readonly IRoute<unknown>[]>(() =>
170
+ [...registry().entries()].map(([name, entry]) => ({
171
+ key: `${routeIdPrefix}-${name}`,
172
+ name,
173
+ params: entry.initialParams,
174
+ })),
175
+ );
176
+
177
+ // `null` means nothing has dispatched yet - and until then the initial state is RE-DERIVED from
178
+ // the registry on every read rather than cached. That is not an optimisation detail, it is what
179
+ // makes the drawer correct: each marker registers with its OWN signal write, and Solid can
180
+ // recompute a downstream memo between two of them, so a one-shot seed reliably captured only the
181
+ // FIRST screen and every later jumpTo was a no-op against a one-route router. Once the app
182
+ // dispatches, the route list freezes - matching React's and Vue's, which build it once in their
183
+ // setup pass and never reconcile (a marker added after that is ignored on every adapter).
184
+ const [dispatched, setDispatched] = createSignal<IDrawerRouterState | null>(
185
+ null,
186
+ );
187
+
188
+ const state = createMemo<IDrawerRouterState>(() => {
189
+ const current = dispatched();
190
+ if (current !== null) return current;
191
+ const initial = routes();
192
+ if (initial.length === 0) {
193
+ dlog('Drawer: no <Drawer.Screen> children registered');
194
+ }
195
+ return createInitialDrawerRouterState(initial, props.initialRouteName);
196
+ });
197
+
198
+ function dispatch(action: Parameters<typeof drawerRouterReducer>[1]): void {
199
+ setDispatched(drawerRouterReducer(state(), action));
200
+ }
201
+
202
+ const windowDimensions = createWindowDimensions();
203
+
204
+ // progress: 0 closed -> 1 open, the single Animated.Value every slide/opacity transform below
205
+ // interpolates from. A plain const: a Solid body runs once, so this needs no re-creation guard
206
+ // the way React's useRef(new Animated.Value(...)).current does.
207
+ const progress = new Animated.Value(state().isOpen ? 1 : 0);
208
+ // Where a drag STARTS from, in progress units. Always exactly 0 or 1: a gesture only begins at
209
+ // rest, since terminate/release always snap back before another grant can fire.
210
+ let dragStartProgress = 0;
211
+
212
+ function animateProgressTo(open: boolean): void {
213
+ dlog(`Drawer: animateProgressTo(open=${open}) at t=${Date.now()}`);
214
+ Animated.timing(progress, {
215
+ toValue: open ? 1 : 0,
216
+ duration: DRAWER_SNAP_DURATION,
217
+ // Native-driver wiring is deferred for v1 - see the header feasibility note.
218
+ useNativeDriver: false,
219
+ }).start();
220
+ }
221
+
222
+ const handle: IDrawerNavigatorHandle = {
223
+ openDrawer: () => {
224
+ animateProgressTo(true);
225
+ dispatch({ type: 'openDrawer' });
226
+ },
227
+ closeDrawer: () => {
228
+ animateProgressTo(false);
229
+ dispatch({ type: 'closeDrawer' });
230
+ },
231
+ toggleDrawer: () => {
232
+ animateProgressTo(!state().isOpen);
233
+ dispatch({ type: 'toggleDrawer' });
234
+ },
235
+ jumpTo: (name: string) => {
236
+ // Both sides of the dispatch, because an unregistered name is a documented reducer no-op that
237
+ // hands the SAME state back: animating off the pre-dispatch snapshot alone would slide the
238
+ // panel shut while the router still says isOpen. The signal is written synchronously inside
239
+ // dispatch, so the second read is already the reducer's own answer.
240
+ const wasOpen = state().isOpen;
241
+ dispatch({ type: 'jumpTo', name });
242
+ if (wasOpen && !state().isOpen) animateProgressTo(false);
243
+ },
244
+ };
245
+ props.ref?.(handle);
246
+
247
+ const panResponder = PanResponder.create({
248
+ onStartShouldSetPanResponder: (
249
+ event: ISymbioteEvent,
250
+ gestureState: IPanResponderGestureState,
251
+ ): boolean =>
252
+ shouldClaimDrawerSwipe(
253
+ event,
254
+ gestureState,
255
+ windowDimensions().width,
256
+ state().isOpen,
257
+ currentOptions(),
258
+ 'start',
259
+ ),
260
+ onMoveShouldSetPanResponder: (
261
+ event: ISymbioteEvent,
262
+ gestureState: IPanResponderGestureState,
263
+ ): boolean =>
264
+ shouldClaimDrawerSwipe(
265
+ event,
266
+ gestureState,
267
+ windowDimensions().width,
268
+ state().isOpen,
269
+ currentOptions(),
270
+ 'move',
271
+ ),
272
+ onPanResponderGrant: (): void => {
273
+ dragStartProgress = state().isOpen ? 1 : 0;
274
+ },
275
+ onPanResponderMove: (
276
+ _event: ISymbioteEvent,
277
+ gestureState: IPanResponderGestureState,
278
+ ): void => {
279
+ progress.setValue(
280
+ resolveDragProgress(gestureState, dragStartProgress, currentOptions()),
281
+ );
282
+ },
283
+ onPanResponderRelease: (
284
+ _event: ISymbioteEvent,
285
+ gestureState: IPanResponderGestureState,
286
+ ): void => {
287
+ const intent = resolveSwipeIntent(
288
+ gestureState,
289
+ state().isOpen,
290
+ currentOptions(),
291
+ );
292
+ const open = intent === 'open';
293
+ dlog(`Drawer: gesture release -> ${open ? 'open' : 'close'}`);
294
+ animateProgressTo(open);
295
+ dispatch(open ? { type: 'openDrawer' } : { type: 'closeDrawer' });
296
+ },
297
+ onPanResponderTerminate: (): void => {
298
+ animateProgressTo(state().isOpen);
299
+ },
300
+ });
301
+
302
+ const emitters = new Map<string, INavigationEmitter>();
303
+ function emitterFor(routeKey: string): INavigationEmitter {
304
+ let emitter = emitters.get(routeKey);
305
+ if (emitter === undefined) {
306
+ emitter = createNavigationEmitter();
307
+ emitters.set(routeKey, emitter);
308
+ }
309
+ return emitter;
310
+ }
311
+
312
+ const focusedKey = createMemo(() => {
313
+ const current = state();
314
+ return current.routes[current.index]?.key;
315
+ });
316
+
317
+ // Deferred by a microtask for the same reason Tab's is: the newly focused screen's subtree, and
318
+ // the subscriptions inside it, are built by the effect flush this callback would otherwise run
319
+ // inside.
320
+ let lastFocusedKey: string | undefined;
321
+ createEffect(() => {
322
+ const nextKey = focusedKey();
323
+ const { blurKey, focusKey } = diffFocusedRoute(lastFocusedKey, nextKey);
324
+ if (blurKey === undefined && focusKey === undefined) return;
325
+ lastFocusedKey = nextKey;
326
+ queueMicrotask(() => {
327
+ if (blurKey !== undefined) {
328
+ dlog(`Drawer: route "${blurKey}" blurred at t=${Date.now()}`);
329
+ emitterFor(blurKey).emit(NAVIGATION_EVENT_BLUR);
330
+ }
331
+ if (focusKey !== undefined) {
332
+ dlog(`Drawer: route "${focusKey}" focused at t=${Date.now()}`);
333
+ emitterFor(focusKey).emit(NAVIGATION_EVENT_FOCUS);
334
+ }
335
+ });
336
+ });
337
+
338
+ onCleanup(() => {
339
+ if (lastFocusedKey !== undefined) {
340
+ emitterFor(lastFocusedKey).emit(NAVIGATION_EVENT_BLUR);
341
+ }
342
+ });
343
+
344
+ function routeFor(routeKey: string): IRoute<unknown> {
345
+ const found = state().routes.find(route => route.key === routeKey);
346
+ return found ?? { key: routeKey, name: '', params: undefined };
347
+ }
348
+
349
+ const focusedComponent = createMemo(() => {
350
+ const current = state();
351
+ const route = current.routes[current.index];
352
+ return route === undefined
353
+ ? undefined
354
+ : registry().get(route.name)?.component;
355
+ });
356
+
357
+ const content = createMemo(() => {
358
+ const routeKey = focusedKey();
359
+ const Component = focusedComponent();
360
+ return untrack(() => {
361
+ if (routeKey === undefined || Component === undefined) return undefined;
362
+ const scope: INavigationScope = () => ({
363
+ route: routeFor(routeKey),
364
+ navigation: handle,
365
+ emitter: emitterFor(routeKey),
366
+ parent: parentScope?.(),
367
+ });
368
+ return createComponent(NavigationScopeProvider, {
369
+ value: scope,
370
+ get children(): JSX.Element {
371
+ return createComponent(Component, {});
372
+ },
373
+ });
374
+ });
375
+ });
376
+
377
+ const descriptors = createMemo<IDrawerDescriptorMap>(() => {
378
+ const map: IDrawerDescriptorMap = {};
379
+ const entries = registry();
380
+ for (const route of state().routes) {
381
+ const entry = entries.get(route.name);
382
+ if (entry === undefined) continue;
383
+ map[route.key] = {
384
+ options: resolveDrawerScreenOptions(entry, {
385
+ route,
386
+ navigation: handle,
387
+ }),
388
+ navigation: handle,
389
+ };
390
+ }
391
+ return map;
392
+ });
393
+
394
+ // Called ONCE, untracked, with an accessor - trap §4. Calling it per change (or handing it a
395
+ // value) would rebuild the whole panel subtree on every drawer state change, which mid-gesture is
396
+ // exactly how a responder grant gets lost.
397
+ //
398
+ // LAZY, though, and inside a memo of its OWN - two separate reasons, both load-bearing.
399
+ // (a) Not called from this body directly: the markers have not registered yet at that point, so a
400
+ // render prop reading `state`/`descriptors` at its own top level (which trap §4 documents as
401
+ // a frozen read) would snapshot an EMPTY navigator.
402
+ // (b) Not a plain `let` cache either. Whatever computation first CALLS the render prop OWNS every
403
+ // memo the returned subtree created, and disposes them when it re-runs - so a cached node
404
+ // handed back to a re-running slot effect is a node whose own reactivity has been torn down,
405
+ // silently frozen at its first paint. A dependency-free memo has a stable owner (this
406
+ // component) and runs exactly once, which gives both properties at the same time.
407
+ const drawerContent = createMemo(() =>
408
+ untrack(() =>
409
+ props.drawerContent?.(() => ({
410
+ state: state(),
411
+ descriptors: descriptors(),
412
+ navigation: handle,
413
+ })),
414
+ ),
415
+ );
416
+
417
+ const shape = createMemo<IDrawerShape>(
418
+ () => {
419
+ const options = currentOptions();
420
+ return {
421
+ type: resolveDrawerType(options),
422
+ position: resolveDrawerPosition(options),
423
+ animated: isDrawerAnimated(options),
424
+ };
425
+ },
426
+ { type: '', position: '', animated: false },
427
+ { equals: sameDrawerShape },
428
+ );
429
+
430
+ function buildDrawer(current: IDrawerShape): ISymbioteNode {
431
+ const order = drawerChildOrder(currentOptions());
432
+
433
+ const rootDescriptor = createMemo<IDescriptor>(() => {
434
+ const options = currentOptions();
435
+ return renderDrawer(
436
+ {
437
+ overlayColor: props.overlayColor ?? DRAWER_DEFAULT_OVERLAY_COLOR,
438
+ drawerStyle: props.drawerStyle,
439
+ contentPassthrough: {},
440
+ overlayPassthrough: current.animated
441
+ ? {
442
+ pointerEvents: state().isOpen ? 'auto' : 'none',
443
+ onStartShouldSetResponder: () => true,
444
+ onResponderRelease: () => {
445
+ animateProgressTo(false);
446
+ dispatch({ type: 'closeDrawer' });
447
+ },
448
+ }
449
+ : {},
450
+ panelPassthrough: {},
451
+ },
452
+ options,
453
+ );
454
+ });
455
+
456
+ function slotDescriptor(slot: IDrawerSlot): IDescriptor {
457
+ const child = rootDescriptor().children[order.indexOf(slot)];
458
+ if (child === undefined || typeof child === 'string') {
459
+ throw new Error(`Drawer: renderDrawer produced no "${slot}" slot`);
460
+ }
461
+ return child;
462
+ }
463
+
464
+ // Each animated style holds an AnimatedInterpolation node, not a plain number/colour - it feeds
465
+ // only Animated.View's deliberately permissive `style?: unknown`, never the plain-IViewStyle
466
+ // branch. Recomputed reactively (geometry follows drawerWidth), which reaches the live node as
467
+ // an ordinary prop update rather than a rebuild.
468
+ function animatedStyle(slot: IDrawerSlot): unknown {
469
+ if (!current.animated) return undefined;
470
+ const geometry = resolveDrawerGeometry(currentOptions());
471
+ // Three branches, not one call on a `slot` union: resolveDrawerSlotInterpolation is OVERLOADED
472
+ // per slot (only the overlay's result carries `opacity`), and a union argument matches no
473
+ // overload. The branch is also what makes the opacity read type-safe without a cast.
474
+ if (slot === 'overlay') {
475
+ // The overlay is a full-screen absolutely-positioned sibling BELOW content in paint order;
476
+ // for 'slide' the content itself translates away, so the overlay's translateX shares
477
+ // content's range rather than staying pinned full-screen.
478
+ const overlay = resolveDrawerSlotInterpolation(geometry, 'overlay');
479
+ return {
480
+ opacity: progress.interpolate(overlay.opacity),
481
+ transform: [{ translateX: progress.interpolate(overlay.translateX) }],
482
+ };
483
+ }
484
+ const interpolation =
485
+ slot === 'content'
486
+ ? resolveDrawerSlotInterpolation(geometry, 'content')
487
+ : resolveDrawerSlotInterpolation(geometry, 'panel');
488
+ return {
489
+ transform: [
490
+ { translateX: progress.interpolate(interpolation.translateX) },
491
+ ],
492
+ };
493
+ }
494
+
495
+ function slotChildren(slot: IDrawerSlot): JSX.Element {
496
+ if (slot === 'content') return content();
497
+ if (slot === 'panel') return drawerContent();
498
+ return undefined;
499
+ }
500
+
501
+ function buildSlot(slot: IDrawerSlot): JSX.Element {
502
+ const style = animatedStyle(slot);
503
+ if (style === undefined) {
504
+ const node = hostElement(
505
+ slotDescriptor(slot).type,
506
+ () => slotDescriptor(slot).props,
507
+ );
508
+ const children = slotChildren(slot);
509
+ if (children !== undefined) insert(node, children);
510
+ return node;
511
+ }
512
+ // ONE source, never a spread followed by an explicit prop: mergeProps takes the first
513
+ // NON-undefined value scanning back-to-front, so a later `undefined` silently loses to an
514
+ // earlier value (.claude/rules/solid-descriptor-bridge.md §6). Collapsing to a single bag
515
+ // removes the question. mergeProps still earns its place - it wraps the function source in a
516
+ // memo and hands Animated.View a reactive proxy, which is what keeps the slot's props live
517
+ // without rebuilding the node.
518
+ return createComponent(
519
+ Animated.View,
520
+ mergeProps((): IAnimatedComponentProps => ({
521
+ ...slotDescriptor(slot).props,
522
+ style: [slotDescriptor(slot).props.style, animatedStyle(slot)],
523
+ get children(): JSX.Element {
524
+ return slotChildren(slot);
525
+ },
526
+ })),
527
+ );
528
+ }
529
+
530
+ const root = hostElement('symbiote-view', () => ({
531
+ ...rootDescriptor().props,
532
+ ...panResponder.panHandlers,
533
+ }));
534
+ insert(root, order.map(buildSlot));
535
+ return root;
536
+ }
537
+
538
+ const drawer = createMemo(() => {
539
+ const current = shape();
540
+ return untrack(() => buildDrawer(current));
541
+ });
542
+
543
+ const host = hostElement('symbiote-view', () => ({ style: { flex: 1 } }));
544
+ insert(host, drawer);
545
+
546
+ return createComponent(ScreenCollectorProvider, {
547
+ value: collector,
548
+ get children(): JSX.Element {
549
+ return [props.children, host];
550
+ },
551
+ });
552
+ }
553
+
554
+ export const Drawer = Object.assign(DrawerImpl, { Screen: DrawerScreen });
555
+
556
+ // --- Explicit gap list vs the real react-native-gesture-handler + react-native-reanimated
557
+ // @react-navigation/drawer (mirrored verbatim from the React twin - the gaps are architectural,
558
+ // not adapter-specific) ---
559
+ // 1. `configureGestureHandler` - a raw react-native-gesture-handler `Gesture` object escape hatch.
560
+ // No PanResponder equivalent exists; not ported.
561
+ // 2. Simultaneous/failure gesture RELATIONSHIPS (gesture-handler's declarative composition vs a
562
+ // nested ScrollView, another PanResponder, etc.) - PanResponder only offers negotiation via the
563
+ // should-set boolean gates used here (edge-start + dominant-axis), which is more prone to an
564
+ // accidental hijack of a nested horizontal ScrollView/Swiper.
565
+ // 3. `useDrawerProgress` - a Reanimated SharedValue read on the UI thread. `progress` here is a
566
+ // JS-thread AnimatedValue; interpolating it works, but without native-driver wiring it does not
567
+ // carry the same synchronous UI-thread guarantee under JS-thread load.
568
+ // 4. `hideStatusBarOnOpen` / `keyboardDismissMode` / `statusBarAnimation` / `overlayStyle` - not
569
+ // wired in this pass; straightforward additions once StatusBar/Keyboard module wiring is needed
570
+ // here (not a PanResponder/Animated limitation, just unscoped for v1).
@@ -0,0 +1,24 @@
1
+ // Drawer.Screen's public prop types. Same split rationale as screen-props.ts.
2
+
3
+ import type { Component } from 'solid-js';
4
+ import type {
5
+ IDrawerNavigatorHandle,
6
+ IDrawerScreenOptions,
7
+ IRoute,
8
+ } from '../core';
9
+
10
+ export type IDrawerScreenOptionsArgs = {
11
+ route: IRoute<unknown>;
12
+ navigation: IDrawerNavigatorHandle;
13
+ };
14
+
15
+ export type IDrawerScreenOptionsResolver = (
16
+ args: IDrawerScreenOptionsArgs,
17
+ ) => IDrawerScreenOptions;
18
+
19
+ export type IDrawerScreenProps = {
20
+ name: string;
21
+ component: Component;
22
+ options?: IDrawerScreenOptions | IDrawerScreenOptionsResolver;
23
+ initialParams?: unknown;
24
+ };
@@ -0,0 +1,29 @@
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
+
21
+ import { descriptorToSolid } from '@symbiote-native/solid';
22
+ import type { ISymbioteNode } from '@symbiote-native/engine';
23
+
24
+ export function hostElement(
25
+ type: string,
26
+ props: () => Record<string, unknown>,
27
+ ): ISymbioteNode {
28
+ return descriptorToSolid(() => ({ type, props: props(), children: [] }));
29
+ }