@sigmela/router 0.0.13 → 0.0.15

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 (37) hide show
  1. package/lib/module/Navigation.js +14 -22
  2. package/lib/module/Router.js +299 -52
  3. package/lib/module/ScreenStack/ScreenStack.native.js +3 -0
  4. package/lib/module/ScreenStack/ScreenStack.web.js +139 -0
  5. package/lib/module/ScreenStack/index.js +3 -0
  6. package/lib/module/{ScreenStackItem.js → ScreenStackItem/ScreenStackItem.js} +7 -13
  7. package/lib/module/ScreenStackItem/ScreenStackItem.types.js +3 -0
  8. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +38 -0
  9. package/lib/module/ScreenStackItem/index.js +3 -0
  10. package/lib/module/StackRenderer.js +8 -8
  11. package/lib/module/TabBar/RenderTabBar.native.js +224 -0
  12. package/lib/module/TabBar/RenderTabBar.web.js +130 -0
  13. package/lib/module/TabBar/TabIcon.web.js +42 -0
  14. package/lib/module/TabBar/index.js +3 -0
  15. package/lib/module/styles.css +139 -0
  16. package/lib/module/web/TransitionStack.js +227 -0
  17. package/lib/typescript/src/Navigation.d.ts +3 -2
  18. package/lib/typescript/src/Router.d.ts +15 -1
  19. package/lib/typescript/src/ScreenStack/ScreenStack.native.d.ts +2 -0
  20. package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +11 -0
  21. package/lib/typescript/src/ScreenStack/index.d.ts +2 -0
  22. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.d.ts +3 -0
  23. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +10 -0
  24. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +3 -0
  25. package/lib/typescript/src/ScreenStackItem/index.d.ts +3 -0
  26. package/lib/typescript/src/StackRenderer.d.ts +2 -2
  27. package/lib/typescript/src/TabBar/RenderTabBar.native.d.ts +8 -0
  28. package/lib/typescript/src/TabBar/{RenderTabBar.d.ts → RenderTabBar.web.d.ts} +1 -1
  29. package/lib/typescript/src/TabBar/TabBar.d.ts +11 -3
  30. package/lib/typescript/src/TabBar/TabIcon.web.d.ts +7 -0
  31. package/lib/typescript/src/TabBar/index.d.ts +2 -0
  32. package/lib/typescript/src/index.d.ts +1 -1
  33. package/lib/typescript/src/types.d.ts +24 -165
  34. package/lib/typescript/src/web/TransitionStack.d.ts +21 -0
  35. package/package.json +3 -2
  36. package/lib/module/TabBar/RenderTabBar.js +0 -123
  37. package/lib/typescript/src/ScreenStackItem.d.ts +0 -12
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
 
3
- import { ScreenStack, ScreenStackItem as RNNScreenStackItem } from 'react-native-screens';
4
- import { memo, useCallback, useEffect, useState, useSyncExternalStore } from 'react';
5
- import { RenderTabBar } from "./TabBar/RenderTabBar.js";
6
- import { ScreenStackItem } from "./ScreenStackItem.js";
3
+ // import { ScreenStackItem as RNNScreenStackItem } from 'react-native-screens';
4
+
5
+ import { RenderTabBar } from './TabBar/RenderTabBar';
6
+ import { ScreenStackItem } from "./ScreenStackItem/index.js";
7
7
  import { RouterContext } from "./RouterContext.js";
8
+ import { ScreenStack } from "./ScreenStack/index.js";
8
9
  import { StyleSheet } from 'react-native';
10
+ import { useSyncExternalStore, memo, useCallback, useEffect, useState } from 'react';
9
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
12
  const EMPTY_HISTORY = [];
11
13
  function useStackHistory(router, stackId) {
@@ -41,29 +43,19 @@ export const Navigation = /*#__PURE__*/memo(({
41
43
  value: router,
42
44
  children: /*#__PURE__*/_jsxs(ScreenStack, {
43
45
  style: styles.flex,
44
- children: [hasTabBar && /*#__PURE__*/_jsx(RNNScreenStackItem, {
45
- screenId: "root-tabbar",
46
- headerConfig: {
47
- hidden: true
48
- },
49
- style: styles.flex,
50
- stackAnimation: rootTransition,
51
- children: /*#__PURE__*/_jsx(RenderTabBar, {
52
- tabBar: router.tabBar,
53
- appearance: appearance
54
- })
46
+ children: [hasTabBar && /*#__PURE__*/_jsx(RenderTabBar, {
47
+ tabBar: router.tabBar,
48
+ appearance: appearance
55
49
  }), rootItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
56
50
  stackId: rootId,
57
51
  item: item,
58
52
  stackAnimation: rootTransition,
59
- screenStyle: appearance?.screenStyle,
60
- headerAppearance: appearance?.header
61
- }, item.key)), globalItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
53
+ appearance: appearance
54
+ }, `root-${item.key}`)), globalItems.map(item => /*#__PURE__*/_jsx(ScreenStackItem, {
55
+ appearance: appearance,
62
56
  stackId: globalId,
63
- item: item,
64
- screenStyle: appearance?.screenStyle,
65
- headerAppearance: appearance?.header
66
- }, item.key))]
57
+ item: item
58
+ }, `global-${item.key}`))]
67
59
  })
68
60
  });
69
61
  });
@@ -48,59 +48,45 @@ export class Router {
48
48
  };
49
49
  }
50
50
  this.buildRegistry();
51
- this.seedInitialHistory();
51
+ if (this.isWebEnv()) {
52
+ this.setupBrowserHistory();
53
+ this.parse(this.getCurrentUrl());
54
+ } else {
55
+ this.seedInitialHistory();
56
+ }
52
57
  this.recomputeVisibleRoute();
53
58
  }
54
59
 
55
60
  // Public API
56
61
  navigate = path => {
62
+ if (this.isWebEnv()) {
63
+ this.pushUrl(path);
64
+ return;
65
+ }
57
66
  this.performNavigation(path, 'push');
58
67
  };
59
- replace = path => {
60
- this.performNavigation(path, 'replace');
68
+ replace = (path, dedupe) => {
69
+ if (this.isWebEnv()) {
70
+ this.pendingReplaceDedupe = !!dedupe;
71
+ this.replaceUrl(path);
72
+ return;
73
+ }
74
+ this.performNavigation(path, 'replace', {
75
+ dedupe: !!dedupe
76
+ });
61
77
  };
62
78
  goBack = () => {
63
- // Global layer wins
64
- if (this.global) {
65
- const gid = this.global.getId();
66
- const gslice = this.getStackHistory(gid);
67
- const gtop = gslice.length ? gslice[gslice.length - 1] : undefined;
68
- if (gtop) {
69
- this.applyHistoryChange('pop', gtop);
70
- return;
71
- }
72
- }
73
-
74
- // Tab layer
75
- if (this.tabBar) {
76
- const idx = this.getActiveTabIndex();
77
- const state = this.tabBar.getState();
78
- const route = state.tabs[idx];
79
- if (!route) return;
80
- const stack = this.tabBar.stacks[route.tabKey];
81
- if (!stack) return;
82
- const sid = stack.getId();
83
- const slice = this.getStackHistory(sid);
84
- if (slice.length > 1) {
85
- const top = slice[slice.length - 1];
86
- if (top) {
87
- this.applyHistoryChange('pop', top);
88
- }
79
+ if (this.isWebEnv()) {
80
+ // Web: only go back within the current active stack.
81
+ const didPop = this.tryPopActiveStack();
82
+ if (didPop) {
83
+ // Keep URL in sync with the new visible route without adding history entries
84
+ const path = this.getVisibleRoute()?.path;
85
+ if (path) this.replaceUrl(path);
89
86
  }
90
87
  return;
91
88
  }
92
-
93
- // Root layer
94
- if (isNavigationStackLike(this.root)) {
95
- const sid = this.root.getId();
96
- const slice = this.getStackHistory(sid);
97
- if (slice.length > 1) {
98
- const top = slice[slice.length - 1];
99
- if (top) {
100
- this.applyHistoryChange('pop', top);
101
- }
102
- }
103
- }
89
+ this.popOnce();
104
90
  };
105
91
  onTabIndexChange = index => {
106
92
  if (this.tabBar) {
@@ -242,7 +228,8 @@ export class Router {
242
228
  ...meta,
243
229
  routeId: gtop.routeId,
244
230
  params: gtop.params,
245
- query: gtop.query
231
+ query: gtop.query,
232
+ path: gtop.path
246
233
  } : {
247
234
  routeId: gtop.routeId,
248
235
  stackId: gtop.stackId,
@@ -271,7 +258,8 @@ export class Router {
271
258
  ...meta,
272
259
  routeId: top.routeId,
273
260
  params: top.params,
274
- query: top.query
261
+ query: top.query,
262
+ path: top.path
275
263
  } : {
276
264
  routeId: top.routeId,
277
265
  stackId: sid,
@@ -304,7 +292,8 @@ export class Router {
304
292
  ...meta,
305
293
  routeId: top.routeId,
306
294
  params: top.params,
307
- query: top.query
295
+ query: top.query,
296
+ path: top.path
308
297
  } : {
309
298
  routeId: top.routeId,
310
299
  stackId: sid,
@@ -319,7 +308,7 @@ export class Router {
319
308
  }
320
309
 
321
310
  // Internal navigation logic
322
- performNavigation(path, action) {
311
+ performNavigation(path, action, opts) {
323
312
  const {
324
313
  pathname,
325
314
  query
@@ -346,6 +335,19 @@ export class Router {
346
335
  }
347
336
  }
348
337
 
338
+ // Optional dedupe for replace: no-op when nothing changes at the top
339
+ if (action === 'replace' && opts?.dedupe) {
340
+ const top = this.getTopForTarget(matched.stackId);
341
+ if (top && top.routeId === matched.routeId) {
342
+ const sameParams = JSON.stringify(top.params ?? {}) === JSON.stringify(params ?? {});
343
+ const sameQuery = JSON.stringify(top.query ?? {}) === JSON.stringify(query ?? {});
344
+ const samePath = (top.path ?? '') === pathname;
345
+ if (sameParams && sameQuery && samePath) {
346
+ return;
347
+ }
348
+ }
349
+ }
350
+
349
351
  // If there's a controller, execute it first
350
352
  if (matched.controller) {
351
353
  const controllerInput = {
@@ -501,15 +503,27 @@ export class Router {
501
503
  history: [...this.state.history.slice(0, -1), item]
502
504
  });
503
505
  if (prevSid && prevSid !== sid) {
504
- const prevSlice = this.stackSlices.get(prevSid) ?? EMPTY_ARRAY;
505
- const trimmed = prevSlice.slice(0, -1);
506
- this.stackSlices.set(prevSid, trimmed);
507
- this.emit(this.stackListeners.get(prevSid));
508
- const newSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
509
- const appended = [...newSlice, item];
510
- this.stackSlices.set(sid, appended);
506
+ // Cross-stack replace: do not modify the source stack.
507
+ // Update target stack without growing it unnecessarily.
508
+ const targetSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
509
+ if (targetSlice.length === 0) {
510
+ this.stackSlices.set(sid, [item]);
511
+ } else {
512
+ const targetTop = targetSlice[targetSlice.length - 1];
513
+ const sameRoute = targetTop?.routeId === item.routeId;
514
+ const sameParams = JSON.stringify(targetTop?.params ?? {}) === JSON.stringify(item.params ?? {});
515
+ if (sameRoute && sameParams) {
516
+ // No change needed for target stack
517
+ this.stackSlices.set(sid, targetSlice);
518
+ } else {
519
+ // Replace top of target stack
520
+ const replaced = [...targetSlice.slice(0, -1), item];
521
+ this.stackSlices.set(sid, replaced);
522
+ }
523
+ }
511
524
  this.emit(this.stackListeners.get(sid));
512
525
  } else {
526
+ // Same-stack replace: replace top element
513
527
  const prevSlice = this.stackSlices.get(sid) ?? EMPTY_ARRAY;
514
528
  const nextSlice = prevSlice.length ? [...prevSlice.slice(0, -1), item] : [item];
515
529
  this.stackSlices.set(sid, nextSlice);
@@ -568,4 +582,237 @@ export class Router {
568
582
  findStackById(stackId) {
569
583
  return this.stackById.get(stackId);
570
584
  }
585
+
586
+ // ==== Web integration (History API) ====
587
+ isWebEnv() {
588
+ const g = globalThis;
589
+ return !!(g.addEventListener && g.history && g.location);
590
+ }
591
+ getCurrentUrl() {
592
+ const g = globalThis;
593
+ return g.location ? `${g.location.pathname}${g.location.search}` : '/';
594
+ }
595
+ readIndex(state) {
596
+ if (state && typeof state === 'object' && '__srIndex' in state) {
597
+ const idx = state.__srIndex;
598
+ if (typeof idx === 'number') return idx;
599
+ }
600
+ return 0;
601
+ }
602
+ getHistoryIndex() {
603
+ const g = globalThis;
604
+ return this.readIndex(g.history?.state);
605
+ }
606
+ ensureHistoryIndex() {
607
+ const g = globalThis;
608
+ const st = g.history?.state ?? {};
609
+ if (this.readIndex(st) === 0 && !(st && typeof st === 'object' && '__srIndex' in st)) {
610
+ const next = {
611
+ ...st,
612
+ __srIndex: 0
613
+ };
614
+ try {
615
+ g.history?.replaceState(next, '', g.location?.href);
616
+ } catch {
617
+ // ignore
618
+ }
619
+ }
620
+ }
621
+ pushUrl(to) {
622
+ const g = globalThis;
623
+ const st = g.history?.state ?? {};
624
+ const prev = this.readIndex(st);
625
+ const next = {
626
+ ...st,
627
+ __srIndex: prev + 1
628
+ };
629
+ g.history?.pushState(next, '', to);
630
+ if (g.Event && g.dispatchEvent) {
631
+ g.dispatchEvent(new g.Event('pushState'));
632
+ }
633
+ }
634
+ replaceUrl(to) {
635
+ const g = globalThis;
636
+ const st = g.history?.state ?? {};
637
+ g.history?.replaceState(st, '', to);
638
+ if (g.Event && g.dispatchEvent) {
639
+ g.dispatchEvent(new g.Event('replaceState'));
640
+ }
641
+ }
642
+ patchHistoryOnce() {
643
+ const g = globalThis;
644
+ const key = Symbol.for('sigmela_router_history_patch');
645
+ if (g[key]) return;
646
+ const hist = g.history;
647
+ if (hist && hist.pushState && hist.replaceState) {
648
+ const originalPush = hist.pushState.bind(hist);
649
+ const originalReplace = hist.replaceState.bind(hist);
650
+ hist.pushState = (data, unused, url) => {
651
+ const res = originalPush(data, unused, url);
652
+ if (g.Event && g.dispatchEvent) {
653
+ g.dispatchEvent(new g.Event('pushState'));
654
+ }
655
+ return res;
656
+ };
657
+ hist.replaceState = (data, unused, url) => {
658
+ const res = originalReplace(data, unused, url);
659
+ if (g.Event && g.dispatchEvent) {
660
+ g.dispatchEvent(new g.Event('replaceState'));
661
+ }
662
+ return res;
663
+ };
664
+ }
665
+ g[key] = true;
666
+ }
667
+ lastBrowserIndex = 0;
668
+ pendingReplaceDedupe = false;
669
+ setupBrowserHistory() {
670
+ const g = globalThis;
671
+ this.patchHistoryOnce();
672
+ this.ensureHistoryIndex();
673
+ this.lastBrowserIndex = this.getHistoryIndex();
674
+ const onHistory = ev => {
675
+ const url = this.getCurrentUrl();
676
+ if (ev.type === 'pushState') {
677
+ this.lastBrowserIndex = this.getHistoryIndex();
678
+ this.performNavigation(url, 'push');
679
+ return;
680
+ }
681
+ if (ev.type === 'replaceState') {
682
+ const dedupe = this.pendingReplaceDedupe === true;
683
+ this.performNavigation(url, 'replace', {
684
+ dedupe
685
+ });
686
+ this.lastBrowserIndex = this.getHistoryIndex();
687
+ this.pendingReplaceDedupe = false;
688
+ return;
689
+ }
690
+ if (ev.type === 'popstate') {
691
+ const idx = this.getHistoryIndex();
692
+ const delta = idx - this.lastBrowserIndex;
693
+ if (delta < 0) {
694
+ let steps = -delta;
695
+ while (steps-- > 0) this.popOnce();
696
+ const target = this.parsePath(url).pathname;
697
+ const current = this.getVisibleRoute()?.path;
698
+ if (current !== target) this.performNavigation(url, 'replace');
699
+ } else if (delta > 0) {
700
+ this.performNavigation(url, 'push');
701
+ } else {
702
+ this.performNavigation(url, 'replace');
703
+ }
704
+ this.lastBrowserIndex = idx;
705
+ }
706
+ };
707
+ g.addEventListener?.('pushState', onHistory);
708
+ g.addEventListener?.('replaceState', onHistory);
709
+ g.addEventListener?.('popstate', onHistory);
710
+ }
711
+ popOnce() {
712
+ if (this.tryPopActiveStack()) return;
713
+ }
714
+
715
+ // Attempts to pop exactly one screen within the active stack only.
716
+ // Returns true if a pop occurred; false otherwise.
717
+ tryPopActiveStack() {
718
+ if (this.global) {
719
+ const gid = this.global.getId();
720
+ const gslice = this.getStackHistory(gid);
721
+ const gtop = gslice.length ? gslice[gslice.length - 1] : undefined;
722
+ if (gtop) {
723
+ this.applyHistoryChange('pop', gtop);
724
+ return true;
725
+ }
726
+ }
727
+ if (this.tabBar) {
728
+ const idx = this.getActiveTabIndex();
729
+ const state = this.tabBar.getState();
730
+ const route = state.tabs[idx];
731
+ if (!route) return false;
732
+ const stack = this.tabBar.stacks[route.tabKey];
733
+ if (!stack) return false;
734
+ const sid = stack.getId();
735
+ const slice = this.getStackHistory(sid);
736
+ if (slice.length > 1) {
737
+ const top = slice[slice.length - 1];
738
+ if (top) {
739
+ this.applyHistoryChange('pop', top);
740
+ return true;
741
+ }
742
+ }
743
+ return false;
744
+ }
745
+ if (isNavigationStackLike(this.root)) {
746
+ const sid = this.root.getId();
747
+ const slice = this.getStackHistory(sid);
748
+ if (slice.length > 1) {
749
+ const top = slice[slice.length - 1];
750
+ if (top) {
751
+ this.applyHistoryChange('pop', top);
752
+ return true;
753
+ }
754
+ }
755
+ }
756
+ return false;
757
+ }
758
+
759
+ // Expand deep URL into a stack chain on initial load
760
+ parse(url) {
761
+ const {
762
+ pathname,
763
+ query
764
+ } = this.parsePath(url);
765
+ const deepest = this.matchRoute(pathname);
766
+ if (!deepest) {
767
+ this.seedInitialHistory();
768
+ return;
769
+ }
770
+ if (deepest.scope === 'tab' && this.tabBar && deepest.tabIndex !== undefined) {
771
+ this.onTabIndexChange(deepest.tabIndex);
772
+ }
773
+ const parts = pathname.split('/').filter(Boolean);
774
+ const prefixes = new Array(parts.length + 1);
775
+ let acc = '';
776
+ prefixes[0] = '/';
777
+ for (let i = 0; i < parts.length; i++) {
778
+ acc += `/${parts[i]}`;
779
+ prefixes[i + 1] = acc;
780
+ }
781
+ const candidates = [];
782
+ for (const route of this.registry) {
783
+ if (route.stackId !== deepest.stackId || route.scope !== deepest.scope) {
784
+ continue;
785
+ }
786
+ const segmentsCount = route.path.split('/').filter(Boolean).length;
787
+ const candidateUrl = prefixes[segmentsCount];
788
+ if (!candidateUrl) {
789
+ continue;
790
+ }
791
+ const matchResult = route.match(candidateUrl);
792
+ if (!matchResult) {
793
+ continue;
794
+ }
795
+ candidates.push({
796
+ params: matchResult.params,
797
+ segmentCount: segmentsCount,
798
+ candidateUrl,
799
+ route
800
+ });
801
+ }
802
+ if (!candidates.length) {
803
+ this.seedInitialHistory();
804
+ return;
805
+ }
806
+ candidates.sort((a, b) => a.segmentCount - b.segmentCount);
807
+ const items = candidates.map((c, i) => this.createHistoryItem(c.route, c.params, i === candidates.length - 1 ? query : {}, c.candidateUrl));
808
+ const first = items[0];
809
+ const sid = first?.stackId ?? deepest.stackId;
810
+ this.setState({
811
+ history: items
812
+ });
813
+ if (sid) {
814
+ this.stackSlices.set(sid, items);
815
+ this.emit(this.stackListeners.get(sid));
816
+ }
817
+ }
571
818
  }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ export { ScreenStack } from 'react-native-screens';
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+
3
+ import { Fragment, memo, useRef } from 'react';
4
+ import TransitionStack from "../web/TransitionStack.js";
5
+ import { Children, cloneElement, useLayoutEffect, useMemo, useState } from 'react';
6
+ import { jsx as _jsx } from "react/jsx-runtime";
7
+ export const ScreenStack = /*#__PURE__*/memo(props => {
8
+ const {
9
+ children,
10
+ transitionTime = 250,
11
+ type,
12
+ animated = true
13
+ } = props;
14
+ const [records, setRecords] = useState([]);
15
+ const containerRef = useRef(null);
16
+ const transitionRef = useRef(null);
17
+ const prevSelectedRef = useRef(-1);
18
+ const lastExitingIndexRef = useRef(null);
19
+ const prevSignatureRef = useRef(null);
20
+ const childArray = useMemo(() => Children.toArray(children).filter(Boolean), [children]);
21
+ const nextKeys = useMemo(() => childArray.map(c => c.key), [childArray]);
22
+ useLayoutEffect(() => {
23
+ setRecords(prev => {
24
+ const prevKeys = prev.map(r => r.key);
25
+ if (nextKeys.length === prevKeys.length + 1 && prevKeys.every((k, i) => k === nextKeys[i])) {
26
+ const child = childArray[childArray.length - 1];
27
+ if (!child) return prev;
28
+ const key = child.key;
29
+ const element = /*#__PURE__*/cloneElement(child, {
30
+ phase: 'active'
31
+ });
32
+ return [...prev, {
33
+ key,
34
+ phase: 'active',
35
+ element
36
+ }];
37
+ }
38
+ if (prevKeys.length === nextKeys.length + 1 && nextKeys.every((k, i) => k === prevKeys[i])) {
39
+ const last = prev[prev.length - 1];
40
+ if (!last) return prev;
41
+ if (last.phase === 'exiting') return prev;
42
+ return [...prev.slice(0, -1), {
43
+ key: last.key,
44
+ phase: 'exiting',
45
+ element: /*#__PURE__*/cloneElement(last.element, {
46
+ phase: 'exiting'
47
+ })
48
+ }];
49
+ }
50
+ const nextByKey = new Map();
51
+ for (const ch of childArray) nextByKey.set(ch.key, ch);
52
+ const prevByKey = new Map();
53
+ for (const r of prev) prevByKey.set(r.key, r);
54
+ const result = [];
55
+ for (const ch of childArray) {
56
+ const key = ch.key;
57
+ const existed = prevByKey.get(key);
58
+ const nextEl = /*#__PURE__*/cloneElement(ch, {
59
+ phase: 'active'
60
+ });
61
+ if (existed) {
62
+ const sameActive = existed.phase === 'active';
63
+ result.push(sameActive ? {
64
+ key,
65
+ phase: 'active',
66
+ element: nextEl
67
+ } : {
68
+ key,
69
+ phase: 'active',
70
+ element: nextEl
71
+ });
72
+ } else {
73
+ result.push({
74
+ key,
75
+ phase: 'active',
76
+ element: nextEl
77
+ });
78
+ }
79
+ }
80
+ for (const r of prev) {
81
+ if (!nextByKey.has(r.key)) {
82
+ const exitingEl = /*#__PURE__*/cloneElement(r.element, {
83
+ phase: 'exiting'
84
+ });
85
+ result.push({
86
+ key: r.key,
87
+ phase: 'exiting',
88
+ element: exitingEl
89
+ });
90
+ }
91
+ }
92
+ return result;
93
+ });
94
+ }, [childArray, nextKeys]);
95
+ useLayoutEffect(() => {
96
+ if (!containerRef.current || transitionRef.current) return;
97
+ transitionRef.current = TransitionStack({
98
+ content: containerRef.current,
99
+ type: type,
100
+ transitionTime,
101
+ withAnimationListener: animated,
102
+ onTransitionEnd: () => {
103
+ setRecords(prev => {
104
+ const idx = lastExitingIndexRef.current ?? -1;
105
+ if (idx < 0 || idx >= prev.length) return prev;
106
+ const candidate = prev[idx];
107
+ if (candidate?.phase !== 'exiting') return prev;
108
+ return [...prev.slice(0, idx), ...prev.slice(idx + 1)];
109
+ });
110
+ }
111
+ });
112
+ }, [type, transitionTime, animated]);
113
+ useLayoutEffect(() => {
114
+ if (!transitionRef.current) return;
115
+ const targetIndex = (() => {
116
+ for (let i = records.length - 1; i >= 0; i--) {
117
+ const rec = records[i];
118
+ if (rec && rec.phase === 'active') return i;
119
+ }
120
+ return -1;
121
+ })();
122
+ if (targetIndex === -1) return;
123
+ const outgoingIndex = records.findIndex(it => it.phase === 'exiting');
124
+ lastExitingIndexRef.current = outgoingIndex !== -1 ? outgoingIndex : null;
125
+ const signature = records.map(r => `${String(r.key)}:${r.phase}`).join('|');
126
+ const stackChanged = prevSignatureRef.current !== null && prevSignatureRef.current !== signature;
127
+ const animate = animated && stackChanged;
128
+ transitionRef.current(targetIndex, animate);
129
+ prevSignatureRef.current = signature;
130
+ prevSelectedRef.current = targetIndex;
131
+ }, [records, animated]);
132
+ return /*#__PURE__*/_jsx("div", {
133
+ ref: containerRef,
134
+ className: `screen-stack${type ? ` ${type}` : ''}`,
135
+ children: records.map(r => /*#__PURE__*/_jsx(Fragment, {
136
+ children: r.element
137
+ }, r.key))
138
+ });
139
+ });
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ export { ScreenStack } from './ScreenStack';
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import { ScreenStackItem as RNSScreenStackItem } from 'react-native-screens';
4
- import { RouteLocalContext, useRouter } from "./RouterContext.js";
4
+ import { RouteLocalContext, useRouter } from "../RouterContext.js";
5
5
  import { StyleSheet } from 'react-native';
6
6
  import { memo } from 'react';
7
7
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -9,7 +9,7 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
9
9
  item,
10
10
  stackId,
11
11
  stackAnimation,
12
- screenStyle
12
+ appearance
13
13
  }) => {
14
14
  const router = useRouter();
15
15
  const onDismissed = () => {
@@ -32,22 +32,16 @@ export const ScreenStackItem = /*#__PURE__*/memo(({
32
32
  header,
33
33
  ...screenProps
34
34
  } = item.options || {};
35
- // Merge global header appearance with per-screen header options
36
- // const mergedHeader = {
37
- // ...(headerAppearance ?? {}),
38
- // ...(header ?? {}),
39
- // } as any;
40
- // Hide header by default if not specified
41
- const headerConfig = header ?? {
42
- hidden: true
35
+ const headerConfig = {
36
+ ...header,
37
+ hidden: !header?.title || header?.hidden,
38
+ backgroundColor: appearance?.header?.backgroundColor ?? 'transparent'
43
39
  };
44
-
45
- // console.log('headerConfig', headerAppearance, item.key);
46
40
  return /*#__PURE__*/_jsx(RNSScreenStackItem, {
47
41
  screenId: item.key,
48
42
  onDismissed: onDismissed,
49
43
  style: StyleSheet.absoluteFill,
50
- contentStyle: screenStyle,
44
+ contentStyle: appearance?.screen,
51
45
  headerConfig: headerConfig,
52
46
  ...screenProps,
53
47
  stackAnimation: stackAnimation ?? item.options?.stackAnimation,
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ export {};