@sigmela/router 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,7 +72,7 @@ const rootStack = new NavigationStack()
72
72
  .addScreen('/', HomeScreen, { header: { title: 'Home' } })
73
73
  .addScreen('/details/:id', DetailsScreen, { header: { title: 'Details' } });
74
74
 
75
- const router = new Router({ root: rootStack });
75
+ const router = new Router({ roots: { app: rootStack }, root: 'app' });
76
76
 
77
77
  export default function App() {
78
78
  return <Navigation router={router} />;
@@ -94,7 +94,7 @@ const tabBar = new TabBar({ initialIndex: 0 })
94
94
  .addTab({ key: 'home', stack: homeStack, title: 'Home' })
95
95
  .addTab({ key: 'catalog', stack: catalogStack, title: 'Catalog' });
96
96
 
97
- const router = new Router({ root: tabBar });
97
+ const router = new Router({ roots: { app: tabBar }, root: 'app' });
98
98
 
99
99
  export default function App() {
100
100
  return <Navigation router={router} />;
@@ -158,7 +158,8 @@ The `Router` holds navigation state and performs path matching.
158
158
 
159
159
  ```ts
160
160
  const router = new Router({
161
- root, // NavigationNode (NavigationStack, TabBar, SplitView, ...)
161
+ roots: { app: root }, // NavigationNode (NavigationStack, TabBar, SplitView, ...)
162
+ root: 'app',
162
163
  screenOptions, // optional defaults
163
164
  debug, // optional
164
165
  });
@@ -169,7 +170,7 @@ Navigation:
169
170
  - `router.replace(path, dedupe?)` — replace top of the active stack
170
171
  - `router.goBack()` — pop top of the active stack
171
172
  - `router.reset(path)` — **web-only**: rebuild Router state as if app loaded at `path`
172
- - `router.setRoot(nextRoot, { transition? })` — swap root at runtime
173
+ - `router.setRoot(rootKey, { transition? })` — swap root at runtime (`rootKey` from `config.roots`)
173
174
 
174
175
  State/subscriptions:
175
176
  - `router.getState()` → `{ history: HistoryItem[] }`
@@ -47,7 +47,7 @@ export const Navigation = /*#__PURE__*/memo(({
47
47
  stackId: globalId,
48
48
  item: item
49
49
  }, `global-${item.key}`))]
50
- })
50
+ }, rootId ?? 'root')
51
51
  });
52
52
  });
53
53
  const styles = StyleSheet.create({
@@ -27,6 +27,9 @@ export class Router {
27
27
  activeRoute = null;
28
28
  rootListeners = new Set();
29
29
  rootTransition = undefined;
30
+ // Root swaps should behave like a fresh initial mount (no enter animation).
31
+ // We keep the API option for compatibility, but suppress transition application.
32
+ suppressRootTransitionOnNextRead = false;
30
33
  lastBrowserIndex = 0;
31
34
  suppressHistorySyncCount = 0;
32
35
 
@@ -36,7 +39,13 @@ export class Router {
36
39
  this.debugEnabled = config.debug ?? false;
37
40
  this.routerScreenOptions = config.screenOptions;
38
41
  this.log('ctor');
39
- this.root = config.root;
42
+ this.roots = config.roots;
43
+ this.activeRootKey = config.root;
44
+ const initialRoot = this.roots[this.activeRootKey];
45
+ if (!initialRoot) {
46
+ throw new Error(`Router: root "${String(this.activeRootKey)}" not found in config.roots`);
47
+ }
48
+ this.root = initialRoot;
40
49
  this.buildRegistry();
41
50
  if (this.isWebEnv()) {
42
51
  this.setupBrowserHistory();
@@ -174,11 +183,24 @@ export class Router {
174
183
  this.rootListeners.forEach(l => l());
175
184
  }
176
185
  getRootTransition() {
186
+ if (this.suppressRootTransitionOnNextRead) {
187
+ this.suppressRootTransitionOnNextRead = false;
188
+ // Ensure we don't accidentally apply it on subsequent renders.
189
+ this.rootTransition = undefined;
190
+ return undefined;
191
+ }
177
192
  return this.rootTransition;
178
193
  }
179
- setRoot(nextRoot, options) {
194
+ setRoot(nextRootKey, options) {
195
+ const nextRoot = this.roots[nextRootKey];
196
+ if (!nextRoot) {
197
+ throw new Error(`Router: root "${String(nextRootKey)}" not found in config.roots`);
198
+ }
199
+ this.activeRootKey = nextRootKey;
180
200
  this.root = nextRoot;
181
201
  this.rootTransition = options?.transition ?? undefined;
202
+ // Make the incoming root behave like initial: suppress enter animation.
203
+ this.suppressRootTransitionOnNextRead = true;
182
204
  this.registry.length = 0;
183
205
  this.stackById.clear();
184
206
  this.routeById.clear();
@@ -72,10 +72,17 @@ export const RenderTabBar = /*#__PURE__*/memo(({
72
72
  }, [router, tabBar, tabs, index]);
73
73
  const tabBarStyle = useMemo(() => {
74
74
  const tabBarBg = toColorString(appearance?.tabBar?.backgroundColor);
75
- return tabBarBg ? {
76
- backgroundColor: tabBarBg
77
- } : undefined;
78
- }, [appearance?.tabBar?.backgroundColor]);
75
+ const style = {
76
+ ...(tabBarBg ? {
77
+ ['--tabbar-bg']: tabBarBg
78
+ } : null),
79
+ ...(tabs.length ? {
80
+ ['--tabbar-tabs-count']: String(tabs.length),
81
+ ['--tabbar-active-index']: String(index)
82
+ } : null)
83
+ };
84
+ return Object.keys(style).length ? style : undefined;
85
+ }, [appearance?.tabBar?.backgroundColor, tabs.length, index]);
79
86
  const titleBaseStyle = useMemo(() => ({
80
87
  fontFamily: appearance?.tabBar?.title?.fontFamily,
81
88
  fontSize: appearance?.tabBar?.title?.fontSize,
@@ -97,37 +104,51 @@ export const RenderTabBar = /*#__PURE__*/memo(({
97
104
  }) : /*#__PURE__*/_jsx("div", {
98
105
  className: "tab-bar",
99
106
  style: tabBarStyle,
100
- children: /*#__PURE__*/_jsx("div", {
107
+ "data-tabs-count": tabs.length,
108
+ "data-active-index": index,
109
+ children: /*#__PURE__*/_jsxs("div", {
101
110
  className: "tab-bar-inner",
102
- children: tabs.map((tab, i) => {
103
- const isActive = i === index;
104
- const iconTint = toColorString(isActive ? appearance?.tabBar?.iconColorActive : appearance?.tabBar?.iconColor);
105
- const title = appearance?.tabBar?.title;
106
- const labelColor = isActive ? toColorString(title?.activeColor) ?? toColorString(title?.color) : toColorString(title?.color);
107
- const labelStyle = {
108
- ...titleBaseStyle,
109
- color: labelColor
110
- };
111
- return /*#__PURE__*/_jsxs("button", {
112
- "data-index": i,
113
- className: `tab-item${isActive ? ' active' : ''}`,
114
- onClick: () => onTabClick(i),
115
- children: [/*#__PURE__*/_jsx("div", {
116
- className: "tab-item-icon",
117
- children: isImageSource(tab.icon) ? /*#__PURE__*/_jsx(TabIcon, {
118
- source: tab.icon,
119
- tintColor: iconTint
120
- }) : null
121
- }), /*#__PURE__*/_jsx("div", {
122
- className: "tab-item-label",
123
- style: labelStyle,
124
- children: tab.title
125
- }), tab.badgeValue ? /*#__PURE__*/_jsx("span", {
126
- className: "tab-item-label-badge",
127
- children: tab.badgeValue
128
- }) : null]
129
- }, tab.tabKey);
130
- })
111
+ children: [/*#__PURE__*/_jsx("div", {
112
+ className: "tab-bar-glass",
113
+ "aria-hidden": "true"
114
+ }), /*#__PURE__*/_jsxs("div", {
115
+ className: "tab-bar-content",
116
+ children: [/*#__PURE__*/_jsx("div", {
117
+ className: "tab-bar-active-indicator",
118
+ "aria-hidden": "true"
119
+ }), tabs.map((tab, i) => {
120
+ const isActive = i === index;
121
+ const iconTint = toColorString(isActive ? appearance?.tabBar?.iconColorActive : appearance?.tabBar?.iconColor);
122
+ const title = appearance?.tabBar?.title;
123
+ const labelColor = isActive ? toColorString(title?.activeColor) ?? toColorString(title?.color) : toColorString(title?.color);
124
+ const labelStyle = {
125
+ ...titleBaseStyle,
126
+ color: labelColor
127
+ };
128
+ return /*#__PURE__*/_jsxs("button", {
129
+ type: "button",
130
+ "data-index": i,
131
+ "data-active": isActive ? 'true' : 'false',
132
+ "aria-current": isActive ? 'page' : undefined,
133
+ className: `tab-item${isActive ? ' active' : ''}`,
134
+ onClick: () => onTabClick(i),
135
+ children: [/*#__PURE__*/_jsx("div", {
136
+ className: "tab-item-icon",
137
+ children: isImageSource(tab.icon) ? /*#__PURE__*/_jsx(TabIcon, {
138
+ source: tab.icon,
139
+ tintColor: iconTint
140
+ }) : null
141
+ }), /*#__PURE__*/_jsx("div", {
142
+ className: "tab-item-label",
143
+ style: labelStyle,
144
+ children: tab.title
145
+ }), tab.badgeValue ? /*#__PURE__*/_jsx("span", {
146
+ className: "tab-item-label-badge",
147
+ children: tab.badgeValue
148
+ }) : null]
149
+ }, tab.tabKey);
150
+ })]
151
+ })]
131
152
  })
132
153
  })]
133
154
  })
@@ -493,6 +493,20 @@
493
493
  padding-bottom: calc(73px + env(safe-area-inset-bottom));
494
494
  }
495
495
 
496
+ .tab-bar {
497
+ /* CSS variables for theming / behavior */
498
+ --tabbar-bg: #ffffff;
499
+ --tabbar-gap: 4px;
500
+ --tabbar-radius: 9999px;
501
+ --tabbar-padding: 6px;
502
+ --tabbar-item-radius: 9999px;
503
+ --tabbar-height: 56px;
504
+
505
+ /* Used by active indicator (set inline in RenderTabBar.web.tsx) */
506
+ --tabbar-tabs-count: 1;
507
+ --tabbar-active-index: 0;
508
+ }
509
+
496
510
  .tab-bar-blur-overlay {
497
511
  position: fixed;
498
512
  bottom: 0;
@@ -516,23 +530,69 @@
516
530
  bottom: 0;
517
531
  left: 0;
518
532
  right: 0;
519
- padding: 8px 16px;
520
- padding-bottom: max(16px, env(safe-area-inset-bottom));
533
+ padding: 10px 12px;
534
+ padding-bottom: max(12px, env(safe-area-inset-bottom));
521
535
  background: transparent;
522
536
  z-index: 100;
523
537
  }
524
538
 
525
539
  .tab-bar-inner {
540
+ position: relative;
541
+ display: flex;
542
+ width: 100%;
543
+ max-width: 100%;
544
+ border-radius: var(--tabbar-radius);
545
+ overflow: hidden;
546
+ isolation: isolate;
547
+ width: 100%;
548
+ max-width: 100%;
549
+ }
550
+
551
+ .tab-bar-glass {
552
+ position: absolute;
553
+ inset: 0;
554
+ z-index: 0;
555
+ pointer-events: none;
556
+ }
557
+
558
+ .tab-bar-content {
559
+ position: relative;
560
+ z-index: 1;
526
561
  display: flex;
527
562
  flex-direction: row;
528
563
  align-items: center;
529
- background: #FFFFFF;
530
- box-shadow: 0 2px 8px #00000014;
531
- border-radius: 100px;
532
- padding: 4px;
533
- gap: 4px;
534
564
  width: 100%;
535
565
  max-width: 100%;
566
+ gap: var(--tabbar-gap);
567
+ padding: var(--tabbar-padding);
568
+ }
569
+
570
+ .tab-bar-active-indicator {
571
+ position: absolute;
572
+ top: var(--tabbar-padding);
573
+ bottom: var(--tabbar-padding);
574
+ left: var(--tabbar-padding);
575
+ border-radius: var(--tabbar-item-radius);
576
+ z-index: 0;
577
+ pointer-events: none;
578
+ opacity: 0;
579
+ transition:
580
+ transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1),
581
+ opacity 180ms cubic-bezier(0.22, 0.61, 0.36, 1);
582
+ }
583
+
584
+ .tab-bar[data-tabs-count]:not([data-tabs-count="0"]) .tab-bar-active-indicator {
585
+ opacity: 1;
586
+ width: calc(
587
+ (
588
+ 100% -
589
+ (var(--tabbar-padding) * 2) -
590
+ (var(--tabbar-gap) * (var(--tabbar-tabs-count) - 1))
591
+ ) / var(--tabbar-tabs-count)
592
+ );
593
+ transform: translateX(
594
+ calc((var(--tabbar-active-index) * (100% + var(--tabbar-gap))))
595
+ );
536
596
  }
537
597
 
538
598
  .tab-item {
@@ -540,37 +600,44 @@
540
600
  background: transparent;
541
601
  border: 0;
542
602
  margin: 0;
543
- padding: 8px 16px;
544
- height: 49px;
603
+ padding: 6px 10px;
604
+ height: var(--tabbar-height);
545
605
  color: inherit;
546
606
  display: flex;
547
- flex: 1;
607
+ flex: 1 1 0;
548
608
  flex-direction: column;
549
609
  align-items: center;
550
610
  justify-content: center;
551
611
  gap: 4px;
552
612
  position: relative;
553
613
  cursor: pointer;
554
- border-radius: 100px;
555
- transition: all 200ms cubic-bezier(0.22, 0.61, 0.36, 1);
614
+ border-radius: var(--tabbar-item-radius);
615
+ transition:
616
+ transform 120ms cubic-bezier(0.22, 0.61, 0.36, 1),
617
+ background-color 200ms cubic-bezier(0.22, 0.61, 0.36, 1),
618
+ color 200ms cubic-bezier(0.22, 0.61, 0.36, 1);
556
619
  }
557
620
 
558
621
  .tab-item.active {
559
- background: #EDEDED;
622
+ background: transparent;
560
623
  }
561
624
 
562
625
  .tab-item:hover:not(.active) {
563
- background: color-mix(in srgb, currentColor 8%, transparent);
626
+ background: color-mix(in srgb, #0a0a0a 6%, transparent);
564
627
  }
565
628
 
629
+ .tab-item:active {
630
+ transform: scale(0.98);
631
+ }
566
632
 
567
633
  .tab-item:focus {
568
634
  outline: none;
569
635
  }
570
636
 
571
637
  .tab-item:focus-visible {
572
- outline: 2px solid currentColor;
573
- outline-offset: 2px;
638
+ box-shadow:
639
+ 0 0 0 2px color-mix(in srgb, currentColor 40%, transparent),
640
+ 0 10px 28px rgba(0, 0, 0, 0.12);
574
641
  }
575
642
 
576
643
  .tab-item-icon {
@@ -617,6 +684,41 @@
617
684
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
618
685
  }
619
686
 
687
+ /* ==================== MOBILE TAB BAR (<= 640px) — iOS-like floating glass pill ==================== */
688
+ @media (max-width: 640px) {
689
+ .tab-bar-inner {
690
+ max-width: 560px;
691
+ margin: 0 auto;
692
+ }
693
+
694
+ .tab-bar-glass {
695
+ /* Fallbacks first (in case color-mix isn't supported) */
696
+ background-color: rgba(255, 255, 255, 0.72);
697
+ background: rgba(255, 255, 255, 0.72);
698
+ /* Preferred (overrides when supported) */
699
+ background: color-mix(in srgb, var(--tabbar-bg, #ffffff) 76%, transparent);
700
+ backdrop-filter: blur(28px) saturate(180%);
701
+ -webkit-backdrop-filter: blur(28px) saturate(180%);
702
+ border: 1px solid rgba(255, 255, 255, 0.35);
703
+ border: 0.5px solid color-mix(in srgb, #ffffff 55%, transparent);
704
+ box-shadow:
705
+ 0 8px 26px rgba(0, 0, 0, 0.18),
706
+ 0 1px 0 rgba(255, 255, 255, 0.4) inset;
707
+ }
708
+
709
+ .tab-bar-active-indicator {
710
+ background-color: rgba(255, 255, 255, 0.60);
711
+ background: color-mix(in srgb, #ffffff 62%, transparent);
712
+ box-shadow:
713
+ 0 1px 0 rgba(255, 255, 255, 0.5) inset,
714
+ 0 8px 20px rgba(0, 0, 0, 0.10);
715
+ }
716
+
717
+ .tab-item:hover:not(.active) {
718
+ background: color-mix(in srgb, #ffffff 22%, transparent);
719
+ }
720
+ }
721
+
620
722
  /* ==================== DESKTOP TAB BAR (>= 641px) ==================== */
621
723
  @media (min-width: 641px) {
622
724
  .tab-stacks-container {
@@ -660,10 +762,26 @@
660
762
  border-radius: 0;
661
763
  padding: 12px 0 0 0;
662
764
  gap: 0;
663
- background: #FFFFFF;
765
+ background: var(--tabbar-bg, #ffffff);
664
766
  box-shadow: none;
665
767
  }
666
768
 
769
+ .tab-bar-glass {
770
+ display: none;
771
+ }
772
+
773
+ .tab-bar-content {
774
+ flex-direction: column;
775
+ align-items: stretch;
776
+ justify-content: flex-start;
777
+ padding: 0;
778
+ gap: 0;
779
+ }
780
+
781
+ .tab-bar-active-indicator {
782
+ display: none;
783
+ }
784
+
667
785
  .tab-item {
668
786
  margin: 0 12px;
669
787
  margin-bottom: 4px;
@@ -2,7 +2,8 @@ import type { NavigationNode } from './navigationNode';
2
2
  import type { HistoryItem, ScreenOptions, ActiveRoute, QueryPattern } from './types';
3
3
  type Listener = () => void;
4
4
  export interface RouterConfig {
5
- root: NavigationNode;
5
+ roots: Record<string, NavigationNode>;
6
+ root: string;
6
7
  screenOptions?: ScreenOptions;
7
8
  debug?: boolean;
8
9
  }
@@ -12,6 +13,8 @@ type RouterState = {
12
13
  };
13
14
  export declare class Router {
14
15
  root: NavigationNode | null;
16
+ private readonly roots;
17
+ private activeRootKey;
15
18
  private readonly listeners;
16
19
  private readonly registry;
17
20
  private state;
@@ -26,6 +29,7 @@ export declare class Router {
26
29
  private activeRoute;
27
30
  private rootListeners;
28
31
  private rootTransition?;
32
+ private suppressRootTransitionOnNextRead;
29
33
  private lastBrowserIndex;
30
34
  private suppressHistorySyncCount;
31
35
  private navigationToken;
@@ -53,7 +57,7 @@ export declare class Router {
53
57
  subscribeRoot(listener: Listener): () => void;
54
58
  private emitRootChange;
55
59
  getRootTransition(): RootTransition | undefined;
56
- setRoot(nextRoot: NavigationNode, options?: {
60
+ setRoot(nextRootKey: string, options?: {
57
61
  transition?: RootTransition;
58
62
  }): void;
59
63
  getActiveRoute: () => ActiveRoute;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigmela/router",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "React Native Router",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",