@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 +5 -4
- package/lib/module/Navigation.js +1 -1
- package/lib/module/Router.js +24 -2
- package/lib/module/TabBar/RenderTabBar.web.js +55 -34
- package/lib/module/styles.css +135 -17
- package/lib/typescript/src/Router.d.ts +6 -2
- package/package.json +1 -1
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({
|
|
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({
|
|
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(
|
|
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[] }`
|
package/lib/module/Navigation.js
CHANGED
package/lib/module/Router.js
CHANGED
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
107
|
+
"data-tabs-count": tabs.length,
|
|
108
|
+
"data-active-index": index,
|
|
109
|
+
children: /*#__PURE__*/_jsxs("div", {
|
|
101
110
|
className: "tab-bar-inner",
|
|
102
|
-
children:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
})
|
package/lib/module/styles.css
CHANGED
|
@@ -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:
|
|
520
|
-
padding-bottom: max(
|
|
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:
|
|
544
|
-
height:
|
|
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:
|
|
555
|
-
transition:
|
|
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:
|
|
622
|
+
background: transparent;
|
|
560
623
|
}
|
|
561
624
|
|
|
562
625
|
.tab-item:hover:not(.active) {
|
|
563
|
-
background: color-mix(in srgb,
|
|
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
|
-
|
|
573
|
-
|
|
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: #
|
|
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
|
-
|
|
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(
|
|
60
|
+
setRoot(nextRootKey: string, options?: {
|
|
57
61
|
transition?: RootTransition;
|
|
58
62
|
}): void;
|
|
59
63
|
getActiveRoute: () => ActiveRoute;
|