@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.
Files changed (155) hide show
  1. package/build/angular/drawer/index.js +6 -4
  2. package/build/angular/stack/index.d.ts +2 -0
  3. package/build/angular/stack/index.js +23 -3
  4. package/build/angular/tabs/index.d.ts +1 -1
  5. package/build/angular/tabs/index.js +20 -7
  6. package/build/core/index.d.ts +2 -1
  7. package/build/core/index.js +5 -1
  8. package/build/core/stack-router-state/index.d.ts +2 -0
  9. package/build/core/stack-router-state/index.js +44 -0
  10. package/build/core/tab-router-state/index.d.ts +1 -0
  11. package/build/core/tab-router-state/index.js +19 -0
  12. package/build/react/drawer/index.js +23 -13
  13. package/build/react/stack/index.js +13 -2
  14. package/build/react/tabs/index.js +23 -9
  15. package/build/svelte/attachments.d.ts +5 -0
  16. package/build/svelte/attachments.js +61 -0
  17. package/build/svelte/descriptor-subtree.d.ts +3 -0
  18. package/build/svelte/descriptor-subtree.js +37 -0
  19. package/build/svelte/drawer/drawer-props.d.ts +14 -0
  20. package/build/svelte/drawer/drawer-props.js +3 -0
  21. package/build/svelte/drawer/index.d.ts +10 -0
  22. package/build/svelte/drawer/index.js +6 -0
  23. package/build/svelte/drawer/index.svelte +421 -0
  24. package/build/svelte/drawer/index.svelte.d.ts +10 -0
  25. package/build/svelte/drawer-screen-props.d.ts +13 -0
  26. package/build/svelte/drawer-screen-props.js +4 -0
  27. package/build/svelte/drawer-screen.svelte +24 -0
  28. package/build/svelte/drawer-screen.svelte.d.ts +4 -0
  29. package/build/svelte/index.d.ts +21 -0
  30. package/build/svelte/index.js +20 -0
  31. package/build/svelte/linking.svelte.d.ts +2 -0
  32. package/build/svelte/linking.svelte.js +55 -0
  33. package/build/svelte/navigation-context.d.ts +14 -0
  34. package/build/svelte/navigation-context.js +44 -0
  35. package/build/svelte/navigation-scope.svelte +24 -0
  36. package/build/svelte/navigation-scope.svelte.d.ts +9 -0
  37. package/build/svelte/registry-host.d.ts +1 -0
  38. package/build/svelte/registry-host.js +26 -0
  39. package/build/svelte/runes/index.d.ts +12 -0
  40. package/build/svelte/runes/index.js +12 -0
  41. package/build/svelte/runes/use-drawer-navigation.svelte.d.ts +6 -0
  42. package/build/svelte/runes/use-drawer-navigation.svelte.js +19 -0
  43. package/build/svelte/runes/use-focus-effect.svelte.d.ts +1 -0
  44. package/build/svelte/runes/use-focus-effect.svelte.js +31 -0
  45. package/build/svelte/runes/use-is-focused.svelte.d.ts +3 -0
  46. package/build/svelte/runes/use-is-focused.svelte.js +34 -0
  47. package/build/svelte/runes/use-navigation-state.svelte.d.ts +4 -0
  48. package/build/svelte/runes/use-navigation-state.svelte.js +39 -0
  49. package/build/svelte/runes/use-navigation.svelte.d.ts +9 -0
  50. package/build/svelte/runes/use-navigation.svelte.js +22 -0
  51. package/build/svelte/runes/use-route.svelte.d.ts +4 -0
  52. package/build/svelte/runes/use-route.svelte.js +12 -0
  53. package/build/svelte/runes/use-stack-navigation.svelte.d.ts +6 -0
  54. package/build/svelte/runes/use-stack-navigation.svelte.js +22 -0
  55. package/build/svelte/runes/use-tab-navigation.svelte.d.ts +6 -0
  56. package/build/svelte/runes/use-tab-navigation.svelte.js +19 -0
  57. package/build/svelte/screen-props.d.ts +21 -0
  58. package/build/svelte/screen-props.js +6 -0
  59. package/build/svelte/screen-registry.d.ts +17 -0
  60. package/build/svelte/screen-registry.js +57 -0
  61. package/build/svelte/screen.svelte +28 -0
  62. package/build/svelte/screen.svelte.d.ts +4 -0
  63. package/build/svelte/stack/index.d.ts +14 -0
  64. package/build/svelte/stack/index.js +7 -0
  65. package/build/svelte/stack/index.svelte +224 -0
  66. package/build/svelte/stack/index.svelte.d.ts +15 -0
  67. package/build/svelte/stack/stack-props.d.ts +20 -0
  68. package/build/svelte/stack/stack-props.js +4 -0
  69. package/build/svelte/stack/stack-screen.svelte +148 -0
  70. package/build/svelte/stack/stack-screen.svelte.d.ts +4 -0
  71. package/build/svelte/tab-screen-props.d.ts +13 -0
  72. package/build/svelte/tab-screen-props.js +4 -0
  73. package/build/svelte/tab-screen.svelte +26 -0
  74. package/build/svelte/tab-screen.svelte.d.ts +4 -0
  75. package/build/svelte/tabs/index.d.ts +8 -0
  76. package/build/svelte/tabs/index.js +6 -0
  77. package/build/svelte/tabs/index.svelte +247 -0
  78. package/build/svelte/tabs/index.svelte.d.ts +8 -0
  79. package/build/svelte/tabs/tab-props.d.ts +7 -0
  80. package/build/svelte/tabs/tab-props.js +3 -0
  81. package/build/vue/drawer/index.js +6 -5
  82. package/build/vue/stack/index.js +26 -12
  83. package/build/vue/tabs/index.js +48 -35
  84. package/build-ngc/angular/drawer/index.js +9 -7
  85. package/build-ngc/angular/drawer/index.js.map +1 -1
  86. package/build-ngc/angular/drawer-screen.directive.js +3 -3
  87. package/build-ngc/angular/navigation-context.service.js +3 -3
  88. package/build-ngc/angular/navigation-scope.directive.js +3 -3
  89. package/build-ngc/angular/screen.directive.js +3 -3
  90. package/build-ngc/angular/search-bar-ref.directive.js +3 -3
  91. package/build-ngc/angular/stack/index.d.ts +2 -0
  92. package/build-ngc/angular/stack/index.js +26 -6
  93. package/build-ngc/angular/stack/index.js.map +1 -1
  94. package/build-ngc/angular/tab-screen.directive.js +3 -3
  95. package/build-ngc/angular/tabs/index.d.ts +1 -1
  96. package/build-ngc/angular/tabs/index.js +23 -10
  97. package/build-ngc/angular/tabs/index.js.map +1 -1
  98. package/build-ngc/core/index.d.ts +2 -1
  99. package/build-ngc/core/index.js +5 -1
  100. package/build-ngc/core/index.js.map +1 -1
  101. package/build-ngc/core/stack-router-state/index.d.ts +2 -0
  102. package/build-ngc/core/stack-router-state/index.js +45 -0
  103. package/build-ngc/core/stack-router-state/index.js.map +1 -0
  104. package/build-ngc/core/tab-router-state/index.d.ts +1 -0
  105. package/build-ngc/core/tab-router-state/index.js +19 -0
  106. package/build-ngc/core/tab-router-state/index.js.map +1 -1
  107. package/package.json +28 -14
  108. package/src/angular/drawer/index.ts +6 -4
  109. package/src/angular/stack/index.ts +23 -2
  110. package/src/angular/tabs/index.ts +19 -5
  111. package/src/core/index.ts +11 -1
  112. package/src/core/stack-router-state/index.ts +49 -0
  113. package/src/core/tab-router-state/index.ts +25 -0
  114. package/src/react/drawer/index.ts +28 -13
  115. package/src/react/stack/index.ts +15 -1
  116. package/src/react/tabs/index.ts +43 -10
  117. package/src/svelte/attachments.ts +68 -0
  118. package/src/svelte/descriptor-subtree.ts +49 -0
  119. package/src/svelte/drawer/drawer-props.ts +28 -0
  120. package/src/svelte/drawer/index.svelte +421 -0
  121. package/src/svelte/drawer/index.ts +10 -0
  122. package/src/svelte/drawer-screen-props.ts +20 -0
  123. package/src/svelte/drawer-screen.svelte +24 -0
  124. package/src/svelte/fabric-tree.test-helper.ts +61 -0
  125. package/src/svelte/index.ts +71 -0
  126. package/src/svelte/linking.svelte.ts +66 -0
  127. package/src/svelte/navigation-context.ts +62 -0
  128. package/src/svelte/navigation-scope.svelte +24 -0
  129. package/src/svelte/registry-host.ts +26 -0
  130. package/src/svelte/runes/index.ts +17 -0
  131. package/src/svelte/runes/use-drawer-navigation.svelte.ts +29 -0
  132. package/src/svelte/runes/use-focus-effect.svelte.ts +35 -0
  133. package/src/svelte/runes/use-is-focused.svelte.ts +38 -0
  134. package/src/svelte/runes/use-navigation-state.svelte.ts +46 -0
  135. package/src/svelte/runes/use-navigation.svelte.ts +39 -0
  136. package/src/svelte/runes/use-route.svelte.ts +16 -0
  137. package/src/svelte/runes/use-stack-navigation.svelte.ts +32 -0
  138. package/src/svelte/runes/use-tab-navigation.svelte.ts +29 -0
  139. package/src/svelte/screen-props.ts +45 -0
  140. package/src/svelte/screen-registry.ts +92 -0
  141. package/src/svelte/screen.svelte +28 -0
  142. package/src/svelte/stack/index.svelte +224 -0
  143. package/src/svelte/stack/index.ts +11 -0
  144. package/src/svelte/stack/stack-props.ts +31 -0
  145. package/src/svelte/stack/stack-screen.svelte +148 -0
  146. package/src/svelte/svelte-ambient.d.ts +6 -0
  147. package/src/svelte/svelte-compile.test-helper.ts +143 -0
  148. package/src/svelte/tab-screen-props.ts +20 -0
  149. package/src/svelte/tab-screen.svelte +26 -0
  150. package/src/svelte/tabs/index.svelte +247 -0
  151. package/src/svelte/tabs/index.ts +10 -0
  152. package/src/svelte/tabs/tab-props.ts +13 -0
  153. package/src/vue/drawer/index.ts +6 -5
  154. package/src/vue/stack/index.ts +28 -11
  155. package/src/vue/tabs/index.ts +68 -65
@@ -243,12 +243,14 @@ let Drawer = (() => {
243
243
  this.dispatch({ type: 'toggleDrawer' });
244
244
  };
245
245
  jumpTo = (name) => {
246
- // Captured BEFORE dispatch, mirroring vue/drawer/index.ts's jumpTo: a signal (like Vue's ref)
247
- // mutates synchronously inside dispatch, so reading isOpen after it would already see the
248
- // reducer's own isOpen: false and never animate the panel closed.
246
+ // Both sides of the dispatch, mirroring vue/drawer/index.ts's jumpTo: an unregistered name is
247
+ // a documented reducer no-op that hands the SAME state back, so animating off the pre-dispatch
248
+ // snapshot alone would slide the panel shut while the router still says isOpen. The signal is
249
+ // set synchronously inside dispatch, so the second read is already the reducer's own answer.
249
250
  const wasOpen = this.stateSignal()?.isOpen ?? false;
250
251
  this.dispatch({ type: 'jumpTo', name });
251
- if (wasOpen)
252
+ const isOpenNow = this.stateSignal()?.isOpen ?? false;
253
+ if (wasOpen && !isOpenNow)
252
254
  this.animateProgressTo(false);
253
255
  };
254
256
  panResponder = PanResponder.create({
@@ -32,9 +32,11 @@ export declare class Stack implements AfterContentInit, OnDestroy, INavigatorHan
32
32
  ngAfterContentInit(): void;
33
33
  ngOnDestroy(): void;
34
34
  private rebuildRegistry;
35
+ private reconcileWithRegistry;
35
36
  private initializeState;
36
37
  private createRoute;
37
38
  private dispatch;
39
+ private commitState;
38
40
  emitterFor(routeKey: string): INavigationEmitter;
39
41
  private mergedOptionsFor;
40
42
  componentFor(route: IRoute<unknown>): Type<unknown> | null;
@@ -35,7 +35,7 @@
35
35
  // self-registers instead: `adapters/angular/babel-register-composed.cjs` (a Metro babel preset
36
36
  // applied bundle-wide, not scoped to adapters/angular) scans this package's own AOT-compiled
37
37
  // (`ngc`) `ɵɵngDeclareComponent({selector: 'Stack', ...})` output and auto-calls
38
- // `registerComposedComponent('Stack')` at bundle time - same mechanism `.examples/angular`
38
+ // `registerComposedComponent('Stack')` at bundle time - same mechanism `examples/angular`
39
39
  // navigation-demo screens and `@symbiote-native/slider`'s `Slider` rely on for their own composed
40
40
  // components mounted statically or via `NgComponentOutlet`. Unregistered, `createElement('Stack')`
41
41
  // falls through to a real `createNode` call and RN paints its own "Unimplemented component"
@@ -81,7 +81,7 @@ import { ChangeDetectionStrategy, Component, ContentChildren, Input, NO_ERRORS_S
81
81
  import { NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
82
82
  import { SymbioteHostPropsDirective } from '@symbiote-native/angular';
83
83
  import { Platform, dlog } from '@symbiote-native/engine';
84
- import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_MODAL_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, STACK_ON_FINISH_TRANSITIONING, buildSearchBarPassthrough, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, resolveHeaderInModalStackStyle, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
84
+ import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_MODAL_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, STACK_ON_FINISH_TRANSITIONING, buildSearchBarPassthrough, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, reconcileStackRoutes, resolveHeaderInModalStackStyle, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
85
85
  import { NavigationScopeDirective } from '../navigation-scope.directive.js';
86
86
  import { SearchBarRefDirective } from '../search-bar-ref.directive.js';
87
87
  import { ScreenDirective } from '../screen.directive.js';
@@ -246,6 +246,21 @@ let Stack = (() => {
246
246
  for (const screen of this.screenChildren) {
247
247
  this.registry.set(screen.name, screen);
248
248
  }
249
+ this.reconcileWithRegistry();
250
+ }
251
+ // A `<ng-template symbioteScreen>` marker can leave the @ContentChildren query (a marker behind
252
+ // an `@if`, a data-driven screen list) while its route is still in the pushed history, which
253
+ // would leave that entry with nothing for componentFor() to mount (reconcileStackRoutes' header).
254
+ // The signal write lives HERE, in the query-change callback, and never in a computed/template
255
+ // accessor - reconciliation is a reaction to the registry changing, not a derivation of it.
256
+ reconcileWithRegistry() {
257
+ const current = this.stateSignal();
258
+ if (current === undefined)
259
+ return;
260
+ const next = reconcileStackRoutes(current, [...this.registry.keys()]);
261
+ if (next === current)
262
+ return;
263
+ this.commitState(next);
249
264
  }
250
265
  initializeState() {
251
266
  if (this.stateSignal() !== undefined)
@@ -266,7 +281,12 @@ let Stack = (() => {
266
281
  const current = this.stateSignal();
267
282
  if (current === undefined)
268
283
  return;
269
- const next = navigatorReducer(current, action);
284
+ this.commitState(navigatorReducer(current, action));
285
+ }
286
+ // The one write path into stateSignal, shared by a dispatched action and a registry-driven
287
+ // reconciliation: both invalidate the plan cache, broadcast to every still-live route, and prune
288
+ // the emitters of routes that are gone.
289
+ commitState(next) {
270
290
  this.planCache.clear();
271
291
  this.stateSignal.set(next);
272
292
  const liveRouteKeys = new Set(next.routes.map(route => route.key));
@@ -32,7 +32,7 @@ export declare class Tab implements AfterContentInit, OnDestroy, ITabNavigatorHa
32
32
  ngOnDestroy(): void;
33
33
  private rebuildRegistry;
34
34
  private routesFromRegistry;
35
- private initializeState;
35
+ private syncRegistry;
36
36
  private dispatch;
37
37
  private resolveTabOptions;
38
38
  focusedRoute(): IRoute<unknown> | undefined;
@@ -53,7 +53,7 @@ import { ChangeDetectionStrategy, Component, ContentChildren, Input, QueryList,
53
53
  import { NgComponentOutlet } from '@angular/common';
54
54
  import { dlog } from '@symbiote-native/engine';
55
55
  import { DescriptorOutlet, View } from '@symbiote-native/angular';
56
- import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, diffFocusedRoute, isFocusedRoute, renderTabBar, tabRouterReducer, } from '../../core/index.js';
56
+ import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, diffFocusedRoute, isFocusedRoute, reconcileTabRoutes, renderTabBar, tabRouterReducer, } from '../../core/index.js';
57
57
  import { NavigationScopeDirective } from '../navigation-scope.directive.js';
58
58
  import { TabScreenDirective } from '../tab-screen.directive.js';
59
59
  const TAB_ROOT_STYLE = { flex: 1 };
@@ -128,10 +128,9 @@ let Tab = (() => {
128
128
  jumpTo = (name, params) => this.dispatch({ type: 'jumpTo', name, params });
129
129
  setParams = (params, key) => this.dispatch({ type: 'setParams', key, params });
130
130
  ngAfterContentInit() {
131
- this.rebuildRegistry();
132
- this.initializeState();
131
+ this.syncRegistry();
133
132
  this.tabScreenChildrenSubscription = this.tabScreenChildren.changes.subscribe(() => {
134
- this.rebuildRegistry();
133
+ this.syncRegistry();
135
134
  });
136
135
  }
137
136
  ngOnDestroy() {
@@ -152,12 +151,26 @@ let Tab = (() => {
152
151
  params: entry.initialParams,
153
152
  }));
154
153
  }
155
- initializeState() {
156
- if (this.stateSignal() !== undefined)
157
- return;
154
+ // A <ng-template symbioteTabScreen> can appear or disappear after mount (a marker behind an
155
+ // @if, a data-driven screen list), so the route list follows the LIVE query instead of staying
156
+ // frozen at whatever ngAfterContentInit first saw: reconcileTabRoutes (core) keeps each
157
+ // surviving route's key and accumulated params and moves focus only when the focused route is
158
+ // the one that left. Runs from the query's own `changes` subscription - an ordinary imperative
159
+ // callback, deliberately not a computed, since this writes the state signal.
160
+ syncRegistry() {
161
+ this.rebuildRegistry();
158
162
  const routes = this.routesFromRegistry();
159
163
  if (routes.length === 0)
160
164
  dlog('Tab: no <ng-template symbioteTabScreen> children registered');
165
+ const current = this.stateSignal();
166
+ if (current !== undefined) {
167
+ this.stateSignal.set(reconcileTabRoutes(current, routes));
168
+ return;
169
+ }
170
+ // Seeding an empty list would resolve initialRouteName against nothing and lose it once the
171
+ // markers do arrive, so the state stays unseeded until at least one has registered.
172
+ if (routes.length === 0)
173
+ return;
161
174
  this.stateSignal.set(createInitialTabState(routes, this.initialRouteName));
162
175
  }
163
176
  dispatch(action) {
@@ -1,6 +1,7 @@
1
1
  export * from './constants';
2
2
  export { createInitialNavigatorState, navigatorReducer, computeActivityState, isTopRoute, } from './navigator-state';
3
3
  export type { IRoute, INavigatorState, INavigatorAction } from './navigator-state';
4
+ export { reconcileStackRoutes } from './stack-router-state';
4
5
  export type { IStackAnimation, IStackPresentation, IHeaderBackButtonDisplayMode, IHeaderUserInterfaceStyle, IOptionalBooleanNativeProp, ISheetAllowedDetents, ISheetLargestUndimmedDetent, ISheetInitialDetent, IHeaderBarButtonIcon, IHeaderBarButtonItemBadge, IHeaderBarButtonItemAction, IHeaderBarButtonMenuAction, IHeaderBarButtonSubmenu, IHeaderBarButtonItemMenu, IHeaderBarButtonItemSpacing, IHeaderBarButtonItem, ISearchBarAutoCapitalize, ISearchBarPlacement, ISearchBarOptions, IScreenOrientation, IScreenOptions, INavigatorPlatform, IScreenViewProps, IHeaderConfigViewProps, IStackViewProps, ISearchBarViewProps, } from './navigator-props';
5
6
  export { resolveHeaderConfigView, resolveScreenView, resolveSearchBarView } from './screen-options';
6
7
  export { buildSearchBarHandle } from './search-bar-commands';
@@ -11,7 +12,7 @@ export { serializeNavigatorState, deserializeNavigatorState } from './state-pers
11
12
  export { createNavigationEmitter, diffFocusedRoute, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_STATE, NAVIGATION_EVENT_BEFORE_REMOVE, } from './navigation-events';
12
13
  export type { INavigationEmitter, INavigationEventListener, INavigationEventName, IFocusTransition, } from './navigation-events';
13
14
  export { isRecord } from './guards';
14
- export { createInitialTabState, tabRouterReducer, isFocusedRoute } from './tab-router-state';
15
+ export { createInitialTabState, tabRouterReducer, reconcileTabRoutes, isFocusedRoute, } from './tab-router-state';
15
16
  export type { ITabRouterState, ITabRouterAction } from './tab-router-state';
16
17
  export type { ITabBarIcon, ITabOptions } from './tab-options';
17
18
  export { renderTabBarItem, renderTabBar } from './render-tabs';
@@ -7,6 +7,10 @@
7
7
  // the descriptor bridge.
8
8
  export * from './constants.js';
9
9
  export { createInitialNavigatorState, navigatorReducer, computeActivityState, isTopRoute, } from './navigator-state/index.js';
10
+ // Drops history entries whose `<Stack.Screen>` marker has unregistered - see
11
+ // stack-router-state.ts's header for why the Stack's history needs this and the Tab's projection
12
+ // does not.
13
+ export { reconcileStackRoutes } from './stack-router-state/index.js';
10
14
  export { resolveHeaderConfigView, resolveScreenView, resolveSearchBarView } from './screen-options.js';
11
15
  export { buildSearchBarHandle } from './search-bar-commands.js';
12
16
  export { resolveScreenProps, resolveStackProps, renderHeaderConfig, resolveSearchBarProps, resolveScreenContentWrapperStyle, resolveScreenViewName, isHeaderInModal, resolveHeaderInModalStackStyle, resolveHeaderInModalScreenStyle, buildSearchBarPassthrough, resolveScreenRenderPlan, } from './render-stack.js';
@@ -18,7 +22,7 @@ export { createNavigationEmitter, diffFocusedRoute, NAVIGATION_EVENT_FOCUS, NAVI
18
22
  export { isRecord } from './guards.js';
19
23
  // Bottom-tabs navigator: a FOCUSED-INDEX router (not a stack - see tab-router-state.ts) driving
20
24
  // a pure-JS tab bar render, framework-agnostic and shared verbatim by every adapter.
21
- export { createInitialTabState, tabRouterReducer, isFocusedRoute } from './tab-router-state/index.js';
25
+ export { createInitialTabState, tabRouterReducer, reconcileTabRoutes, isFocusedRoute, } from './tab-router-state/index.js';
22
26
  export { renderTabBarItem, renderTabBar } from './render-tabs.js';
23
27
  // Linking config: resolves a URL to a route (and back), the piece @react-navigation's
24
28
  // NavigationContainer `linking` prop provides on top of the framework-agnostic Linking module.
@@ -0,0 +1,2 @@
1
+ import type { INavigatorState } from '../navigator-state';
2
+ export declare function reconcileStackRoutes(state: INavigatorState, registeredNames: readonly string[]): INavigatorState;
@@ -0,0 +1,44 @@
1
+ // StackRouter: the registry-reconciliation half (framework-agnostic, zero render). A Stack's route
2
+ // list is navigation HISTORY, not a projection of the registered `<Stack.Screen>` markers the way a
3
+ // Tab's is (tab-router-state) - it seeds exactly one route and grows only through push/replace. So
4
+ // a marker that unregisters while its route still sits in the history (a marker behind an `{#if}`,
5
+ // a data-driven screen list) leaves behind an entry nothing can render: the route stays, the
6
+ // adapter's componentFor() finds no screen, and the user is parked on a blank RNSScreen with no
7
+ // way to tell what happened. Mirrors @react-navigation/routers' StackRouter.getStateForRouteNames-
8
+ // Change, which filters those routes out of the history and pulls the focused index back into
9
+ // range.
10
+ //
11
+ // Deliberately NOT a navigatorReducer action: this is not something a caller dispatches, it is a
12
+ // reaction to the registry changing underneath an unchanged history, and each adapter observes
13
+ // that change through its own lifecycle. Keeping it a separate pure function lets every adapter
14
+ // wire it its own way without teaching the reducer about a registry it has never seen.
15
+ // The contract, and the two places it deliberately parts from upstream:
16
+ //
17
+ // - SURVIVORS KEEP THEIR IDENTITY. A stack route's key is COUNTER-derived (every adapter's
18
+ // createRoute increments a per-navigator sequence), not name-derived like a tab route's, so
19
+ // re-deriving the list from the registry would hand every survivor a brand-new key and remount
20
+ // live screens. Only the phantom entries go; every other route object passes through by
21
+ // reference, params included.
22
+ //
23
+ // - THE FOCUSED ROUTE IS THE TOP ONE. This state carries no `index` field: the focused route is
24
+ // always the last one (see computeActivityState's comment for why no route can outrank it), so
25
+ // upstream's `index: Math.min(state.index, routes.length - 1)` degenerates here to "the last
26
+ // survivor". Dropping the FOCUSED route therefore lands the user on the nearest still-registered
27
+ // route beneath it - where a pop() would have left them - rather than on nothing.
28
+ //
29
+ // - THE ROUTE LIST IS NEVER EMPTIED. `routes.length - 1` IS the focused index, so an empty list
30
+ // means no focused route at all - the blank screen this function exists to prevent. When nothing
31
+ // in the history survives (every marker gone, or the whole screen set swapped for new names) the
32
+ // state is returned UNCHANGED. Upstream instead pushes a freshly nanoid()-keyed route for
33
+ // initialRouteName; we cannot, because minting a route key belongs to the caller here
34
+ // (navigator-state.ts's header) and a derivation that mints one would allocate a new key on every
35
+ // re-run. Unchanged is also the right answer for the usual cause of a momentarily empty registry
36
+ // - markers unregistering and re-registering across a re-render - since the next run reconciles
37
+ // for real.
38
+ export function reconcileStackRoutes(state, registeredNames) {
39
+ const routes = state.routes.filter(route => registeredNames.includes(route.name));
40
+ // filter preserves order, so an unchanged length means nothing was dropped.
41
+ if (routes.length === state.routes.length || routes.length === 0)
42
+ return state;
43
+ return { routes };
44
+ }
@@ -15,5 +15,6 @@ export type ITabRouterAction = {
15
15
  params: unknown;
16
16
  };
17
17
  export declare function createInitialTabState(routes: readonly IRouteEntry[], initialRouteName?: string): ITabRouterState;
18
+ export declare function reconcileTabRoutes(state: ITabRouterState, nextRoutes: readonly IRouteEntry[]): ITabRouterState;
18
19
  export declare function tabRouterReducer(state: ITabRouterState, action: ITabRouterAction): ITabRouterState;
19
20
  export declare function isFocusedRoute(index: number, focusedIndex: number): boolean;
@@ -14,6 +14,25 @@ export function createInitialTabState(routes, initialRouteName) {
14
14
  const index = routes.findIndex(route => route.name === initialRouteName);
15
15
  return { routes, index: index === -1 ? INITIAL_FOCUSED_INDEX : index };
16
16
  }
17
+ // A screen marker can appear or disappear after mount (a marker behind an `{#if}`, a data-driven
18
+ // screen list), so a navigator's route list has to follow its registry instead of staying frozen
19
+ // at whatever createInitialTabState saw first. Mirrors TabRouter.getStateForRouteNamesChange:
20
+ // membership and order come from `nextRoutes`, but a route that survives keeps its IDENTITY (its
21
+ // key) and everything jumpTo/setParams accumulated onto its params - only genuinely new names
22
+ // take their entry from `nextRoutes`. Focus follows the previously focused route's NAME, so an
23
+ // unrelated screen disappearing never moves the user; when the focused route is the one that
24
+ // disappeared, focus falls back to the first route, the same landing spot every unresolvable
25
+ // focus gets here (createInitialTabState's own fallback).
26
+ export function reconcileTabRoutes(state, nextRoutes) {
27
+ const routes = nextRoutes.map(next => state.routes.find(route => route.name === next.name) ?? next);
28
+ const isUnchanged = routes.length === state.routes.length &&
29
+ routes.every((route, at) => route === state.routes[at]);
30
+ if (isUnchanged)
31
+ return state;
32
+ const focusedName = state.routes[state.index]?.name;
33
+ const index = routes.findIndex(route => route.name === focusedName);
34
+ return { routes, index: index === -1 ? INITIAL_FOCUSED_INDEX : index };
35
+ }
17
36
  // jumpTo moves the focused index to an existing route by name and optionally merges params
18
37
  // (navigation.jumpTo(name, params) - the tab-actions doc's own signature); it never adds,
19
38
  // removes, or reorders routes (mirrors TabRouter: JUMP_TO is a no-op when the target name isn't
@@ -71,8 +71,10 @@ const DrawerImpl = forwardRef((props, forwardedRef) => {
71
71
  optionsRef.current = options;
72
72
  const screenWidthRef = useRef(screenWidth);
73
73
  screenWidthRef.current = screenWidth;
74
- const isOpenRef = useRef(state.isOpen);
75
- isOpenRef.current = state.isOpen;
74
+ // The whole router state, not just its isOpen bit: jumpTo below has to re-run the reducer to
75
+ // learn what the dispatch actually produced, and that needs the routes too.
76
+ const stateRef = useRef(state);
77
+ stateRef.current = state;
76
78
  const animateProgressTo = useCallback((open) => {
77
79
  // Investigation instrumentation (Drawer openDrawer-no-op / toggleDrawer-no-animation bug):
78
80
  // every imperative caller (openDrawer/closeDrawer/toggleDrawer/jumpTo) funnels through here,
@@ -92,40 +94,48 @@ const DrawerImpl = forwardRef((props, forwardedRef) => {
92
94
  }, [progress]);
93
95
  const handle = useMemo(() => ({
94
96
  openDrawer: () => {
95
- dlog(`Drawer: openDrawer() called, isOpen=${isOpenRef.current} at t=${Date.now()}`);
97
+ dlog(`Drawer: openDrawer() called, isOpen=${stateRef.current.isOpen} at t=${Date.now()}`);
96
98
  animateProgressTo(true);
97
99
  dispatch({ type: 'openDrawer' });
98
100
  },
99
101
  closeDrawer: () => {
100
- dlog(`Drawer: closeDrawer() called, isOpen=${isOpenRef.current} at t=${Date.now()}`);
102
+ dlog(`Drawer: closeDrawer() called, isOpen=${stateRef.current.isOpen} at t=${Date.now()}`);
101
103
  animateProgressTo(false);
102
104
  dispatch({ type: 'closeDrawer' });
103
105
  },
104
106
  toggleDrawer: () => {
105
- dlog(`Drawer: toggleDrawer() called, isOpen=${isOpenRef.current} at t=${Date.now()}`);
106
- animateProgressTo(!isOpenRef.current);
107
+ dlog(`Drawer: toggleDrawer() called, isOpen=${stateRef.current.isOpen} at t=${Date.now()}`);
108
+ animateProgressTo(!stateRef.current.isOpen);
107
109
  dispatch({ type: 'toggleDrawer' });
108
110
  },
109
111
  jumpTo: (name) => {
110
- dispatch({ type: 'jumpTo', name });
111
- if (isOpenRef.current)
112
+ // An unregistered name is a documented reducer no-op that hands the SAME state back, so
113
+ // animating off `isOpen` alone would slide the panel shut while the router still says
114
+ // open. useReducer's dispatch is async - stateRef only catches up on the next render - so
115
+ // the reducer (a pure function) is re-run here to learn what the dispatch produces, and
116
+ // the animation runs only on a genuine open -> closed transition.
117
+ const action = { type: 'jumpTo', name };
118
+ const current = stateRef.current;
119
+ const next = drawerRouterReducer(current, action);
120
+ dispatch(action);
121
+ if (current.isOpen && !next.isOpen)
112
122
  animateProgressTo(false);
113
123
  },
114
124
  }), [animateProgressTo]);
115
125
  useImperativeHandle(forwardedRef, () => handle, [handle]);
116
126
  const panResponder = useRef(PanResponder.create({
117
- onStartShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, screenWidthRef.current, isOpenRef.current, optionsRef.current, 'start'),
118
- onMoveShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, screenWidthRef.current, isOpenRef.current, optionsRef.current, 'move'),
127
+ onStartShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, screenWidthRef.current, stateRef.current.isOpen, optionsRef.current, 'start'),
128
+ onMoveShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, screenWidthRef.current, stateRef.current.isOpen, optionsRef.current, 'move'),
119
129
  onPanResponderGrant: () => {
120
130
  dlog('Drawer: gesture grant');
121
- dragStartProgress.current = isOpenRef.current ? 1 : 0;
131
+ dragStartProgress.current = stateRef.current.isOpen ? 1 : 0;
122
132
  },
123
133
  onPanResponderMove: (_event, gestureState) => {
124
134
  const nextProgress = resolveDragProgress(gestureState, dragStartProgress.current, optionsRef.current);
125
135
  progress.setValue(nextProgress);
126
136
  },
127
137
  onPanResponderRelease: (_event, gestureState) => {
128
- const intent = resolveSwipeIntent(gestureState, isOpenRef.current, optionsRef.current);
138
+ const intent = resolveSwipeIntent(gestureState, stateRef.current.isOpen, optionsRef.current);
129
139
  const open = intent === 'open';
130
140
  dlog(`Drawer: gesture release -> ${open ? 'open' : 'close'}`);
131
141
  animateProgressTo(open);
@@ -133,7 +143,7 @@ const DrawerImpl = forwardRef((props, forwardedRef) => {
133
143
  },
134
144
  onPanResponderTerminate: () => {
135
145
  dlog('Drawer: gesture terminated, snapping back');
136
- animateProgressTo(isOpenRef.current);
146
+ animateProgressTo(stateRef.current.isOpen);
137
147
  },
138
148
  })).current;
139
149
  const animated = isDrawerAnimated(options);
@@ -11,7 +11,7 @@
11
11
  import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useReducer, useRef, } from 'react';
12
12
  import { descriptorToReact } from '@symbiote-native/react';
13
13
  import { Platform, debugNodeId, dlog } from '@symbiote-native/engine';
14
- import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, RNS_SCREEN_STACK_VIEW_NAME, RNS_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, SCREEN_ON_WILL_APPEAR, SCREEN_ON_WILL_DISAPPEAR, STACK_ON_FINISH_TRANSITIONING, buildSearchBarHandle, buildSearchBarPassthrough, computeActivityState, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
14
+ import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, RNS_SCREEN_STACK_VIEW_NAME, RNS_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, SCREEN_ON_WILL_APPEAR, SCREEN_ON_WILL_DISAPPEAR, STACK_ON_FINISH_TRANSITIONING, buildSearchBarHandle, buildSearchBarPassthrough, computeActivityState, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, reconcileStackRoutes, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
15
15
  import { collectRegistry } from '../collect-registry.js';
16
16
  import { NavigationContext } from '../navigation-context.js';
17
17
  import { Screen } from '../screen.js';
@@ -53,7 +53,8 @@ const StackImpl = forwardRef((props, forwardedRef) => {
53
53
  routeSequence.current += 1;
54
54
  return { key: `${routeIdPrefix}-${name}-${routeSequence.current}`, name, params };
55
55
  }, [routeIdPrefix]);
56
- const [state, dispatch] = useReducer(navigatorReducer, undefined, () => {
56
+ const registeredNames = useMemo(() => [...registry.keys()], [registry]);
57
+ const [dispatchedState, dispatch] = useReducer(navigatorReducer, undefined, () => {
57
58
  const initialRouteName = props.initialRouteName ?? registry.keys().next().value;
58
59
  if (initialRouteName === undefined) {
59
60
  dlog('Stack: no <Stack.Screen> children registered');
@@ -61,6 +62,16 @@ const StackImpl = forwardRef((props, forwardedRef) => {
61
62
  }
62
63
  return createInitialNavigatorState(createRoute(initialRouteName, registry.get(initialRouteName)?.initialParams));
63
64
  });
65
+ // A <Stack.Screen> marker can unregister while its route is still in the pushed history, which
66
+ // would leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling here
67
+ // rather than in an effect repairs the CURRENT paint - the call is pure and hands back the same
68
+ // reference when nothing changed, so it is safe during render; the effect below only PERSISTS
69
+ // the repair, so the next push builds on the pruned history instead of the phantom one.
70
+ const state = reconcileStackRoutes(dispatchedState, registeredNames);
71
+ useEffect(() => {
72
+ if (state !== dispatchedState)
73
+ dispatch({ type: 'reset', state });
74
+ }, [state, dispatchedState]);
64
75
  const handle = useMemo(() => ({
65
76
  push: (name, params) => dispatch({ type: 'push', route: createRoute(name, params) }),
66
77
  pop: count => dispatch({ type: 'pop', count }),
@@ -1,15 +1,15 @@
1
1
  // Tab, the React lifecycle half. The focused-index router (tab-router-state) and the tab-bar
2
2
  // Descriptor builder (render-tabs) live in @symbiote-native/navigation core, shared verbatim
3
- // with the Vue/Angular adapters; here React supplies the lifecycle - useReducer for the focused
4
- // index, useId for route-key generation, useImperativeHandle for the jumpTo/setParams handle -
5
- // plus the descriptor bridge for the tab-bar leaf, exactly like Stack bridges its header config
6
- // (react/stack.ts). Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary
3
+ // with the Vue/Angular adapters; here React supplies the lifecycle - useState for the dispatched
4
+ // router state, useId for route-key generation, useImperativeHandle for the jumpTo/setParams
5
+ // handle - plus the descriptor bridge for the tab-bar leaf, exactly like Stack bridges its header
6
+ // config (react/stack.ts). Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary
7
7
  // `symbiote-view`/`symbiote-text` primitives via the shared render fn, so there is no
8
8
  // react-native-screens ViewConfig to register here - Tab needs no `../register` import.
9
- import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useReducer, } from 'react';
9
+ import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useState, } from 'react';
10
10
  import { descriptorToReact } from '@symbiote-native/react';
11
11
  import { dlog } from '@symbiote-native/engine';
12
- import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, isFocusedRoute, renderTabBar, tabRouterReducer, } from '../../core/index.js';
12
+ import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, isFocusedRoute, reconcileTabRoutes, renderTabBar, tabRouterReducer, } from '../../core/index.js';
13
13
  import { collectRegistry } from '../collect-registry.js';
14
14
  import { NavigationContext } from '../navigation-context.js';
15
15
  import { TabScreen } from '../tab-screen.js';
@@ -34,11 +34,25 @@ const TabImpl = forwardRef((props, forwardedRef) => {
34
34
  name,
35
35
  params: entry.initialParams,
36
36
  })), [registry, routeIdPrefix]);
37
- const [state, dispatch] = useReducer(tabRouterReducer, undefined, () => createInitialTabState(routes, props.initialRouteName));
37
+ // A <Tab.Screen> can appear or disappear after mount (a marker behind a conditional, a
38
+ // data-driven screen list), so the route list is re-derived from `children` on every render and
39
+ // the STORED state is reconciled against it (reconcileTabRoutes, core - it preserves each
40
+ // surviving route's key and accumulated params and keeps focus on the same route NAME) rather
41
+ // than staying frozen at whatever the first render saw. `null` means no marker had registered
42
+ // yet: seeding an empty list would resolve initialRouteName against nothing and lose it, so the
43
+ // state stays derived until either a marker arrives or something dispatches.
44
+ const [storedState, setStoredState] = useState(() => routes.length === 0 ? null : createInitialTabState(routes, props.initialRouteName));
45
+ const resolveState = useCallback((stored) => stored === null
46
+ ? createInitialTabState(routes, props.initialRouteName)
47
+ : reconcileTabRoutes(stored, routes), [routes, props.initialRouteName]);
48
+ const state = resolveState(storedState);
38
49
  if (routes.length === 0)
39
50
  dlog('Tab: no <Tab.Screen> children registered');
40
- const jumpTo = useCallback((name, params) => dispatch({ type: 'jumpTo', name, params }), []);
41
- const setParams = useCallback((params, key) => dispatch({ type: 'setParams', key, params }), []);
51
+ // The updater re-resolves rather than closing over `state` so that two dispatches batched into
52
+ // one render still compose: the second reduces over the first's result, not a stale snapshot.
53
+ const dispatch = useCallback((action) => setStoredState(current => tabRouterReducer(resolveState(current), action)), [resolveState]);
54
+ const jumpTo = useCallback((name, params) => dispatch({ type: 'jumpTo', name, params }), [dispatch]);
55
+ const setParams = useCallback((params, key) => dispatch({ type: 'setParams', key, params }), [dispatch]);
42
56
  const handle = useMemo(() => ({ jumpTo, setParams }), [jumpTo, setParams]);
43
57
  useImperativeHandle(forwardedRef, () => handle, [handle]);
44
58
  const focusedRoute = state.routes[state.index];
@@ -0,0 +1,5 @@
1
+ import type { ISearchBarCommands } from '../core';
2
+ export declare function hostProps(props: Record<string, unknown>): (node: unknown) => void;
3
+ export declare function searchBarRef(ref: {
4
+ current: ISearchBarCommands | null;
5
+ } | undefined): (node: unknown) => (() => void) | void;
@@ -0,0 +1,61 @@
1
+ // Svelte attachments (`{@attach ...}`) for the react-native-screens native leaves.
2
+ //
3
+ // WHY THESE EXIST AT ALL: every react-native-screens Fabric view name is capitalized and
4
+ // un-hyphenated (RNSScreen, RNSScreenStack, RNSScreenStackHeaderConfig, RNSSearchBar, ...), so a
5
+ // literal `<RNSScreen>` in a Svelte template parses as a COMPONENT reference, not an element -
6
+ // the same wall @symbiote-native/slider's 'RNCSlider' leaf hit. Slider sidestepped it by mounting
7
+ // the whole leaf through the Descriptor bridge, which works because that leaf carries no live
8
+ // framework children. A stack screen DOES (the app's own screen component lives inside
9
+ // RNSScreenContentWrapper), so the bridge is not an option here.
10
+ //
11
+ // The way through is `<svelte:element this={'RNSScreen'}>`: it creates the element via
12
+ // `document.createElement(tag)` with a plain runtime string the compiler never inspects, and it
13
+ // accepts ordinary framework children. Its one catch is documented in svelte-adapter-dom-shim
14
+ // skill §15 - a dynamic tag compiles through Svelte's generic setAttribute/property-diffing
15
+ // codegen, NOT the custom-element `p=` property-SET path, so writing `p={bag}` as an ATTRIBUTE on
16
+ // it silently fails. An attachment bypasses that entirely: it is handed the raw element and
17
+ // assigns the property from plain JS, then re-runs whenever the props it read change. Verified
18
+ // against the real compiler + the real shim before this was built on.
19
+ import { dlog, isSymbioteNode } from '@symbiote-native/engine';
20
+ import { buildSearchBarHandle } from '../core/index.js';
21
+ // `unknown` rather than `Element`: the value Svelte hands an attachment is one of the DOM shim's
22
+ // own element classes, not a real DOM Element, and a function accepting `unknown` is still
23
+ // assignable everywhere Svelte expects `(element: Element) => ...`.
24
+ function isShimElement(value) {
25
+ return typeof value === 'object' && value !== null && 'engineNode' in value;
26
+ }
27
+ // The object-bag prop set, the same entry point every adapter's props go through (routeProp).
28
+ // Re-runs on every change of `props`, and the shim's own `p` setter diffs per key from there.
29
+ export function hostProps(props) {
30
+ return node => {
31
+ if (!isShimElement(node)) {
32
+ dlog('navigation: hostProps attached to a non-shim element, ignored');
33
+ return;
34
+ }
35
+ node.p = props;
36
+ };
37
+ }
38
+ // The Svelte twin of Angular's SearchBarRefDirective / React's and Vue's callback `ref` on the
39
+ // RNSSearchBar element: fills the app-supplied cell once the native node exists and clears it on
40
+ // teardown. buildSearchBarHandle's own getter is LAZY, so handing it the node here is safe even
41
+ // before the first commit.
42
+ export function searchBarRef(ref) {
43
+ return node => {
44
+ if (ref === undefined)
45
+ return;
46
+ if (!isShimElement(node)) {
47
+ ref.current = null;
48
+ return;
49
+ }
50
+ const engineNode = node.engineNode;
51
+ if (!isSymbioteNode(engineNode)) {
52
+ dlog('navigation: search bar attachment ran before the shim went live');
53
+ ref.current = null;
54
+ return;
55
+ }
56
+ ref.current = buildSearchBarHandle(() => engineNode);
57
+ return () => {
58
+ ref.current = null;
59
+ };
60
+ };
61
+ }
@@ -0,0 +1,3 @@
1
+ import type { ShimElement } from '@symbiote-native/svelte/native-view-bridge';
2
+ import type { IDescriptorChild } from '@symbiote-native/components';
3
+ export declare function createDescriptorSubtreeSync(): (host: ShimElement | null, children: IDescriptorChild[]) => void;
@@ -0,0 +1,37 @@
1
+ // A shape-CHANGE-tolerant wrapper over @symbiote-native/svelte's Descriptor bridge.
2
+ //
3
+ // `mountDescriptorChildren` (svelte-adapter-dom-shim skill §19) is deliberately shape-stable: it
4
+ // builds each shim node once and only re-sets props thereafter, throwing if the tree shape ever
5
+ // changes, because every `core/components` render-*.ts produces a constant shape. The navigation
6
+ // package's tab bar is the exception - `renderTabBar` emits one child per registered route, and
7
+ // that count genuinely varies (it is 0 until the `<Tab.Screen>` markers have registered, and
8
+ // changes again if a screen is added or removed). Same situation @symbiote-native/slider's own
9
+ // steps-indicator overlay is in, and the same resolution: keep the fast path for the common
10
+ // "same shape, new prop values" case and rebuild only when the shape actually changed.
11
+ //
12
+ // The shape signature covers types and nesting, not just the top-level count, because a tab bar
13
+ // item's own children vary too (an item gains an icon wrapper only when it has an icon or badge).
14
+ import { mountDescriptorChildren } from '@symbiote-native/svelte/native-view-bridge';
15
+ const TEXT_CHILD_MARK = '#';
16
+ function shapeOf(children) {
17
+ return children
18
+ .map(child => typeof child === 'string' ? TEXT_CHILD_MARK : `${child.type}(${shapeOf(child.children)})`)
19
+ .join(',');
20
+ }
21
+ export function createDescriptorSubtreeSync() {
22
+ let mounted;
23
+ let shape;
24
+ return (host, children) => {
25
+ if (host === null)
26
+ return;
27
+ const nextShape = shapeOf(children);
28
+ if (mounted === undefined || nextShape !== shape) {
29
+ for (const child of host.children.slice())
30
+ host.removeChild(child);
31
+ mounted = mountDescriptorChildren(host, children);
32
+ shape = nextShape;
33
+ return;
34
+ }
35
+ mounted.update(children);
36
+ };
37
+ }
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
3
+ import type { IDrawerDescriptorMap, IDrawerNavigatorHandle, IDrawerOptions, IDrawerRouterState } from '../../core';
4
+ export type IDrawerContentSlotProps = {
5
+ state: IDrawerRouterState;
6
+ descriptors: IDrawerDescriptorMap;
7
+ navigation: IDrawerNavigatorHandle;
8
+ };
9
+ export type IDrawerProps = IDrawerOptions & {
10
+ initialRouteName?: string;
11
+ drawerStyle?: IStyleProp<IViewStyle>;
12
+ children?: Snippet;
13
+ drawerContent?: Snippet<[IDrawerContentSlotProps]>;
14
+ };
@@ -0,0 +1,3 @@
1
+ // Drawer's own public prop surface. Plain `.ts` for the tsc-cannot-see-a-`.svelte`-named-export
2
+ // reason ../screen-props.ts documents.
3
+ export {};
@@ -0,0 +1,10 @@
1
+ export declare const Drawer: import("svelte").Component<import("./drawer-props").IDrawerProps, {
2
+ openDrawer: () => void;
3
+ closeDrawer: () => void;
4
+ toggleDrawer: () => void;
5
+ jumpTo: (name: string) => void;
6
+ }, ""> & {
7
+ Screen: import("svelte").Component<import("..").IDrawerScreenProps, {}, "">;
8
+ };
9
+ export type { IDrawerNavigatorHandle, IDrawerDescriptorMap } from '../../core';
10
+ export type { IDrawerContentSlotProps, IDrawerProps } from './drawer-props';
@@ -0,0 +1,6 @@
1
+ // `Drawer.Screen` alongside the standalone `DrawerScreen` export, matching React's and Vue's
2
+ // barrels - see ../stack/index.ts for why Object.assign is the right mechanism on a compiled
3
+ // Svelte component.
4
+ import DrawerImpl from './index.svelte';
5
+ import DrawerScreen from '../drawer-screen.svelte';
6
+ export const Drawer = Object.assign(DrawerImpl, { Screen: DrawerScreen });