rift-ds 1.0.1 → 1.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.
- package/LICENSE +1 -1
- package/README.md +8 -2
- package/components/AppLayout/AppLayout.css +13 -3
- package/components/AppLayout/AppLayout.d.ts +5 -1
- package/components/AppLayout/AppLayout.js +4 -0
- package/components/AppSidebar/AppSidebar.css +237 -0
- package/components/AppSidebar/AppSidebar.d.ts +32 -3
- package/components/AppSidebar/AppSidebar.js +187 -16
- package/components/BottomNav/BottomNav.css +417 -0
- package/components/BottomNav/BottomNav.d.ts +73 -0
- package/components/BottomNav/BottomNav.js +100 -0
- package/components/Chart/AreaChart.js +67 -56
- package/components/Chart/BarChart.js +67 -56
- package/components/Chart/ComboChart.js +93 -82
- package/components/Chart/LineChart.js +59 -48
- package/components/Chart/PieChart.js +56 -45
- package/components/Chart/RadarChart.js +60 -49
- package/components/Chart/RadialChart.js +70 -59
- package/components/Chart/ScatterChart.js +61 -50
- package/components/Chart/StackedBarChart.js +64 -53
- package/components/Chart/Treemap.js +34 -23
- package/components/Nav/Nav.d.ts +11 -3
- package/components/Nav/Nav.js +3 -2
- package/components/SidebarPanel/SidebarPanel.css +291 -0
- package/components/SidebarPanel/SidebarPanel.d.ts +45 -0
- package/components/SidebarPanel/SidebarPanel.js +166 -0
- package/components/Stepper/Stepper.d.ts +3 -1
- package/components/Stepper/Stepper.js +1 -1
- package/components/TopAppBar/TopAppBar.css +464 -0
- package/components/TopAppBar/TopAppBar.d.ts +80 -0
- package/components/TopAppBar/TopAppBar.js +138 -0
- package/components/registry.json +25 -1
- package/components/registry.json.d.ts +25 -1
- package/components/registry.json.js +1 -1
- package/index.d.ts +3 -0
- package/index.js +6 -0
- package/package.json +5 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
BOTTOM NAV — the mobile tab bar
|
|
3
|
+
Three platforms share one markup:
|
|
4
|
+
default the Rift bar, fully on-token
|
|
5
|
+
ios the iOS 26 Liquid Glass capsule
|
|
6
|
+
android the Material 3 Expressive bar
|
|
7
|
+
Each slot is icon over label; the selection
|
|
8
|
+
is carried per platform (tint, lens, pill).
|
|
9
|
+
============================================ */
|
|
10
|
+
|
|
11
|
+
/* ============================================
|
|
12
|
+
BASE (shared by every platform)
|
|
13
|
+
============================================ */
|
|
14
|
+
.ds-bottom-nav {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: stretch;
|
|
17
|
+
gap: var(--gap-200);
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
width: 100%;
|
|
20
|
+
/* The home-indicator area on notched phones; zero elsewhere. */
|
|
21
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ds-bottom-nav--fixed {
|
|
25
|
+
position: fixed;
|
|
26
|
+
left: 0;
|
|
27
|
+
right: 0;
|
|
28
|
+
bottom: 0;
|
|
29
|
+
z-index: 80;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.ds-bottom-nav__bar {
|
|
33
|
+
position: relative;
|
|
34
|
+
flex: 1;
|
|
35
|
+
min-width: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ds-bottom-nav__list {
|
|
39
|
+
position: relative;
|
|
40
|
+
display: flex;
|
|
41
|
+
height: 100%;
|
|
42
|
+
margin: 0;
|
|
43
|
+
padding: 0;
|
|
44
|
+
list-style: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ds-bottom-nav__slot {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex: 1 1 0;
|
|
50
|
+
min-width: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ds-bottom-nav__item {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex: 1;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
gap: var(--gap-100);
|
|
60
|
+
min-width: 0;
|
|
61
|
+
padding: 0;
|
|
62
|
+
border: none;
|
|
63
|
+
background: transparent;
|
|
64
|
+
color: var(--color-text-secondary);
|
|
65
|
+
font: inherit;
|
|
66
|
+
text-decoration: none;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
-webkit-tap-highlight-color: transparent;
|
|
69
|
+
transition: color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ds-bottom-nav__item:focus-visible,
|
|
73
|
+
.ds-bottom-nav__search:focus-visible {
|
|
74
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
75
|
+
outline-offset: -2px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ds-bottom-nav__indicator {
|
|
79
|
+
position: relative;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ds-bottom-nav__icon {
|
|
86
|
+
--icon-size: var(--icon-size-600);
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
color: inherit;
|
|
91
|
+
/* The icon font's hooks, never a raw font-variation-settings, so the
|
|
92
|
+
dark theme's grade and the default weight survive (design.md's icon
|
|
93
|
+
style hooks). */
|
|
94
|
+
--material-symbols-fill: 0;
|
|
95
|
+
transition: font-variation-settings var(--motion-duration-fast) var(--motion-ease-standard);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Selected icons draw filled, the convention on all three platforms. */
|
|
99
|
+
.ds-bottom-nav__item--active .ds-bottom-nav__icon {
|
|
100
|
+
--material-symbols-fill: 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ds-bottom-nav__label {
|
|
104
|
+
max-width: 100%;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
text-overflow: ellipsis;
|
|
107
|
+
white-space: nowrap;
|
|
108
|
+
font-family: var(--font-caption-family);
|
|
109
|
+
font-size: var(--font-caption-size);
|
|
110
|
+
font-weight: var(--font-caption-weight);
|
|
111
|
+
line-height: var(--font-caption-line-height);
|
|
112
|
+
letter-spacing: var(--font-caption-letter-spacing);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Count badge on the icon's top-end corner. */
|
|
116
|
+
.ds-bottom-nav__badge {
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: calc(var(--padding-100) * -1);
|
|
119
|
+
left: calc(50% + var(--padding-150));
|
|
120
|
+
box-sizing: border-box;
|
|
121
|
+
min-width: 16px;
|
|
122
|
+
height: 16px;
|
|
123
|
+
padding: 0 var(--padding-100);
|
|
124
|
+
border-radius: var(--radius-pill);
|
|
125
|
+
background-color: var(--color-status-error-icon);
|
|
126
|
+
color: var(--color-bg-page-primary);
|
|
127
|
+
font-family: var(--font-caption-family);
|
|
128
|
+
font-size: var(--font-caption-size);
|
|
129
|
+
font-weight: var(--font-paragraph-sm-emphasis-weight);
|
|
130
|
+
line-height: 16px;
|
|
131
|
+
text-align: center;
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.ds-bottom-nav__badge--dot {
|
|
136
|
+
top: 0;
|
|
137
|
+
left: calc(50% + var(--padding-150));
|
|
138
|
+
min-width: 0;
|
|
139
|
+
width: 6px;
|
|
140
|
+
height: 6px;
|
|
141
|
+
padding: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.ds-bottom-nav__sr {
|
|
145
|
+
position: absolute;
|
|
146
|
+
width: 1px;
|
|
147
|
+
height: 1px;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
clip-path: inset(50%);
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.ds-bottom-nav__search {
|
|
154
|
+
display: none;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Icons take the colour of the text around them, whatever a consumer
|
|
158
|
+
stylesheet sets on the icon font class globally. */
|
|
159
|
+
.ds-bottom-nav .material-symbols-rounded {
|
|
160
|
+
color: inherit;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* ============================================
|
|
164
|
+
DEFAULT — the Rift bar
|
|
165
|
+
A full-width bar on the page ground with a
|
|
166
|
+
hairline above; the selected destination is
|
|
167
|
+
the teal selection of a mutually exclusive set.
|
|
168
|
+
============================================ */
|
|
169
|
+
.ds-bottom-nav--default {
|
|
170
|
+
background-color: var(--color-bg-page-primary);
|
|
171
|
+
border-top: var(--border-025) solid var(--color-bg-container-border);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.ds-bottom-nav--default .ds-bottom-nav__bar {
|
|
175
|
+
height: 64px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.ds-bottom-nav--default .ds-bottom-nav__item:hover {
|
|
179
|
+
color: var(--color-text-primary);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.ds-bottom-nav--default .ds-bottom-nav__item--active,
|
|
183
|
+
.ds-bottom-nav--default .ds-bottom-nav__item--active:hover {
|
|
184
|
+
color: var(--color-action-primary-bg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.ds-bottom-nav--default .ds-bottom-nav__item--active .ds-bottom-nav__label {
|
|
188
|
+
font-weight: var(--font-paragraph-sm-emphasis-weight);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ============================================
|
|
192
|
+
iOS — Liquid Glass (iOS 26)
|
|
193
|
+
ds-allow(platform): iOS 26 tab-bar geometry and material — the 62pt
|
|
194
|
+
capsule floating 21pt in from the screen edges, 10pt medium SF labels
|
|
195
|
+
in the system face, the glass fill, blur and saturation, the white
|
|
196
|
+
specular rim, and the white badge numeral — restated from the platform
|
|
197
|
+
so the mock reads as system chrome. The tint stays the action token.
|
|
198
|
+
============================================ */
|
|
199
|
+
.ds-bottom-nav--ios {
|
|
200
|
+
--ds-bottom-nav-ios-height: 62px;
|
|
201
|
+
--ds-bottom-nav-ios-inset: 21px;
|
|
202
|
+
/* The glass: a thin tint of the container over a strong blur, with the
|
|
203
|
+
colour behind pushed up so the material reads as lit, not grey. */
|
|
204
|
+
--ds-bottom-nav-glass: color-mix(in srgb, var(--color-bg-container-primary) 58%, transparent);
|
|
205
|
+
--ds-bottom-nav-glass-filter: blur(16px) saturate(180%);
|
|
206
|
+
--ds-bottom-nav-glass-rim:
|
|
207
|
+
inset 0 1px 0 0 rgb(255 255 255 / 0.55),
|
|
208
|
+
inset 0 0 0 0.5px rgb(255 255 255 / 0.28),
|
|
209
|
+
0 8px 32px rgb(0 0 0 / 0.14),
|
|
210
|
+
0 1px 3px rgb(0 0 0 / 0.08);
|
|
211
|
+
padding: 0 var(--ds-bottom-nav-ios-inset) calc(env(safe-area-inset-bottom, 0px) + var(--ds-bottom-nav-ios-inset) - 8px);
|
|
212
|
+
font-family: system-ui, -apple-system, 'SF Pro Text', sans-serif;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.ds-bottom-nav--ios .ds-bottom-nav__bar {
|
|
216
|
+
height: var(--ds-bottom-nav-ios-height);
|
|
217
|
+
padding: 4px;
|
|
218
|
+
box-sizing: border-box;
|
|
219
|
+
border-radius: 999px;
|
|
220
|
+
background-color: var(--ds-bottom-nav-glass);
|
|
221
|
+
box-shadow: var(--ds-bottom-nav-glass-rim);
|
|
222
|
+
-webkit-backdrop-filter: var(--ds-bottom-nav-glass-filter);
|
|
223
|
+
backdrop-filter: var(--ds-bottom-nav-glass-filter);
|
|
224
|
+
transition: width var(--motion-duration-slow) var(--motion-ease-spring);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* The lens: a lighter glass pill behind the selected slot, sliding
|
|
228
|
+
between slots on the platform's springy curve. */
|
|
229
|
+
.ds-bottom-nav--ios .ds-bottom-nav__lens {
|
|
230
|
+
position: absolute;
|
|
231
|
+
top: 4px;
|
|
232
|
+
bottom: 4px;
|
|
233
|
+
left: 4px;
|
|
234
|
+
width: calc((100% - 8px) / var(--ds-bottom-nav-count));
|
|
235
|
+
border-radius: 999px;
|
|
236
|
+
background-color: color-mix(in srgb, var(--color-text-primary) 9%, transparent);
|
|
237
|
+
box-shadow: inset 0 1px 0 0 rgb(255 255 255 / 0.35);
|
|
238
|
+
transform: translateX(calc(100% * var(--ds-bottom-nav-index)));
|
|
239
|
+
transition:
|
|
240
|
+
transform var(--motion-duration-slow) var(--motion-ease-spring),
|
|
241
|
+
width var(--motion-duration-slow) var(--motion-ease-spring);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.ds-bottom-nav--ios .ds-bottom-nav__item {
|
|
245
|
+
gap: 1px;
|
|
246
|
+
color: var(--color-text-primary);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.ds-bottom-nav--ios .ds-bottom-nav__item--active {
|
|
250
|
+
color: var(--color-action-primary-bg);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.ds-bottom-nav--ios .ds-bottom-nav__icon {
|
|
254
|
+
--icon-size: var(--icon-size-600);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* A medium stroke, as SF Symbols draw in a tab bar. */
|
|
258
|
+
.ds-bottom-nav--ios .ds-bottom-nav__icon {
|
|
259
|
+
--material-symbols-weight: 500;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.ds-bottom-nav--ios .ds-bottom-nav__label {
|
|
263
|
+
font-family: inherit;
|
|
264
|
+
font-size: 10px;
|
|
265
|
+
font-weight: 500;
|
|
266
|
+
line-height: 12px;
|
|
267
|
+
letter-spacing: 0.01em;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.ds-bottom-nav--ios .ds-bottom-nav__badge {
|
|
271
|
+
/* systemRed, light and dark alike on a badge. */
|
|
272
|
+
background-color: #ff3b30;
|
|
273
|
+
color: #fff;
|
|
274
|
+
font-family: inherit;
|
|
275
|
+
font-size: 11px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* The search destination: its own glass circle, the bar's height. */
|
|
279
|
+
.ds-bottom-nav--ios .ds-bottom-nav__search {
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
flex-shrink: 0;
|
|
284
|
+
width: var(--ds-bottom-nav-ios-height);
|
|
285
|
+
height: var(--ds-bottom-nav-ios-height);
|
|
286
|
+
padding: 0;
|
|
287
|
+
border: none;
|
|
288
|
+
border-radius: 999px;
|
|
289
|
+
background-color: var(--ds-bottom-nav-glass);
|
|
290
|
+
box-shadow: var(--ds-bottom-nav-glass-rim);
|
|
291
|
+
-webkit-backdrop-filter: var(--ds-bottom-nav-glass-filter);
|
|
292
|
+
backdrop-filter: var(--ds-bottom-nav-glass-filter);
|
|
293
|
+
color: var(--color-text-primary);
|
|
294
|
+
text-decoration: none;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* Minimised: the capsule closes to a circle holding only the selection;
|
|
299
|
+
the other slots fold to nothing and the lens fills what is left. */
|
|
300
|
+
.ds-bottom-nav--ios .ds-bottom-nav__slot {
|
|
301
|
+
transition:
|
|
302
|
+
flex-grow var(--motion-duration-slow) var(--motion-ease-spring),
|
|
303
|
+
opacity var(--motion-duration-fast) var(--motion-ease-standard);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.ds-bottom-nav--ios.ds-bottom-nav--minimized .ds-bottom-nav__bar {
|
|
307
|
+
flex: 0 0 var(--ds-bottom-nav-ios-height);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.ds-bottom-nav--ios.ds-bottom-nav--minimized .ds-bottom-nav__slot:not(:has(.ds-bottom-nav__item--active)) {
|
|
311
|
+
flex-grow: 0;
|
|
312
|
+
opacity: 0;
|
|
313
|
+
overflow: hidden;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.ds-bottom-nav--ios.ds-bottom-nav--minimized .ds-bottom-nav__lens {
|
|
317
|
+
width: calc(100% - 8px);
|
|
318
|
+
transform: none;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* The label leaves the picture but stays the button's accessible name. */
|
|
322
|
+
.ds-bottom-nav--ios.ds-bottom-nav--minimized .ds-bottom-nav__label {
|
|
323
|
+
position: absolute;
|
|
324
|
+
width: 1px;
|
|
325
|
+
height: 1px;
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
clip-path: inset(50%);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* ============================================
|
|
331
|
+
ANDROID — Material 3 Expressive
|
|
332
|
+
ds-allow(platform): the M3 Expressive short navigation bar — a 64dp
|
|
333
|
+
bar on the surface-container role, a 56 x 32dp pill indicator behind
|
|
334
|
+
the selected icon, 12/16 medium labels tracked 0.5, the platform face
|
|
335
|
+
(Google Sans Flex, then Roboto), and the white badge numeral — restated
|
|
336
|
+
from the Material token set so the mock reads as system chrome.
|
|
337
|
+
============================================ */
|
|
338
|
+
.ds-bottom-nav--android {
|
|
339
|
+
background-color: var(--color-bg-container-secondary);
|
|
340
|
+
font-family: 'Google Sans Flex', Roboto, system-ui, sans-serif;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.ds-bottom-nav--android .ds-bottom-nav__bar {
|
|
344
|
+
height: 64px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.ds-bottom-nav--android .ds-bottom-nav__item {
|
|
348
|
+
justify-content: flex-start;
|
|
349
|
+
gap: 4px;
|
|
350
|
+
padding-top: 6px;
|
|
351
|
+
color: var(--color-text-secondary);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.ds-bottom-nav--android .ds-bottom-nav__item--active {
|
|
355
|
+
color: var(--color-text-primary);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.ds-bottom-nav--android .ds-bottom-nav__indicator {
|
|
359
|
+
width: 56px;
|
|
360
|
+
height: 32px;
|
|
361
|
+
flex-shrink: 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* The pill grows out from the icon's centre on selection, M3's own
|
|
365
|
+
indicator motion, on the spring curve. */
|
|
366
|
+
.ds-bottom-nav--android .ds-bottom-nav__indicator::before {
|
|
367
|
+
content: '';
|
|
368
|
+
position: absolute;
|
|
369
|
+
inset: 0;
|
|
370
|
+
border-radius: 999px;
|
|
371
|
+
background-color: color-mix(in srgb, var(--color-action-primary-bg) 22%, var(--color-bg-container-secondary));
|
|
372
|
+
opacity: 0;
|
|
373
|
+
transform: scaleX(0.4);
|
|
374
|
+
transition:
|
|
375
|
+
opacity var(--motion-duration-fast) var(--motion-ease-standard),
|
|
376
|
+
transform var(--motion-duration-slow) var(--motion-ease-spring);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.ds-bottom-nav--android .ds-bottom-nav__item--active .ds-bottom-nav__indicator::before {
|
|
380
|
+
opacity: 1;
|
|
381
|
+
transform: none;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* A pressed or hovered slot gets the state layer, the indicator's shape
|
|
385
|
+
at low strength. */
|
|
386
|
+
.ds-bottom-nav--android .ds-bottom-nav__item:hover:not(.ds-bottom-nav__item--active) .ds-bottom-nav__indicator::before {
|
|
387
|
+
opacity: 0.35;
|
|
388
|
+
transform: none;
|
|
389
|
+
background-color: var(--color-bg-container-tertiary);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.ds-bottom-nav--android .ds-bottom-nav__icon {
|
|
393
|
+
position: relative;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.ds-bottom-nav--android .ds-bottom-nav__label {
|
|
397
|
+
font-family: inherit;
|
|
398
|
+
font-size: 12px;
|
|
399
|
+
font-weight: 500;
|
|
400
|
+
line-height: 16px;
|
|
401
|
+
letter-spacing: 0.5px;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.ds-bottom-nav--android .ds-bottom-nav__badge {
|
|
405
|
+
top: 2px;
|
|
406
|
+
left: calc(50% + 4px);
|
|
407
|
+
/* The Material error role's baseline red. */
|
|
408
|
+
background-color: #b3261e;
|
|
409
|
+
color: #fff;
|
|
410
|
+
font-family: inherit;
|
|
411
|
+
font-size: 11px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.ds-bottom-nav--android .ds-bottom-nav__badge--dot {
|
|
415
|
+
top: 4px;
|
|
416
|
+
left: calc(50% + 6px);
|
|
417
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export interface BottomNavItem {
|
|
3
|
+
/** Unique key for this destination */
|
|
4
|
+
key: string;
|
|
5
|
+
/** Display label, always shown under the icon */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Icon — Material Symbol name (string, drawn filled when selected) or custom element (ReactNode) */
|
|
8
|
+
icon: string | React.ReactNode;
|
|
9
|
+
/** Renders the destination as a real link to this URL instead of a button */
|
|
10
|
+
href?: string;
|
|
11
|
+
/** Click handler — also fires on a link destination, so a consumer can route client-side */
|
|
12
|
+
onClick?: () => void;
|
|
13
|
+
/** Count on the icon's corner; `true` shows a dot with no number */
|
|
14
|
+
badge?: number | string | true;
|
|
15
|
+
}
|
|
16
|
+
export interface BottomNavSearch {
|
|
17
|
+
/** The button's accessible name on iOS, and its visible tab label on the other platforms */
|
|
18
|
+
label: string;
|
|
19
|
+
/**
|
|
20
|
+
* Icon — Material Symbol name (string) or custom element (ReactNode).
|
|
21
|
+
* Defaults to `search`; set it when the set-apart slot launches something
|
|
22
|
+
* else, such as an assistant.
|
|
23
|
+
*/
|
|
24
|
+
icon?: string | React.ReactNode;
|
|
25
|
+
/** Click handler */
|
|
26
|
+
onClick?: () => void;
|
|
27
|
+
/** Renders the button as a link to this URL instead */
|
|
28
|
+
href?: string;
|
|
29
|
+
}
|
|
30
|
+
type BottomNavOwnProps = {
|
|
31
|
+
/** Destinations, three to five of them */
|
|
32
|
+
items: BottomNavItem[];
|
|
33
|
+
/** Key of the destination for the current page */
|
|
34
|
+
activeKey?: string;
|
|
35
|
+
/** Called with a destination's key when it is pressed */
|
|
36
|
+
onValueChange?: (key: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Which platform's tab bar to draw. `default` is the Rift bar; `ios` is the
|
|
39
|
+
* iOS 26 Liquid Glass capsule floating over content with a sliding lens
|
|
40
|
+
* behind the selection; `android` is the Material 3 Expressive navigation
|
|
41
|
+
* bar with a pill indicator behind the selected icon.
|
|
42
|
+
*/
|
|
43
|
+
platform?: 'default' | 'ios' | 'android';
|
|
44
|
+
/**
|
|
45
|
+
* A destination set apart from the rest, search unless its `icon` says
|
|
46
|
+
* otherwise. On iOS it is its own glass circle beside the bar; elsewhere it
|
|
47
|
+
* joins the bar as the last destination.
|
|
48
|
+
*/
|
|
49
|
+
search?: BottomNavSearch;
|
|
50
|
+
/**
|
|
51
|
+
* iOS only: shrinks the bar to the selected destination alone, the way the
|
|
52
|
+
* system bar minimises while the page scrolls down. Ignored on other platforms.
|
|
53
|
+
*/
|
|
54
|
+
minimized?: boolean;
|
|
55
|
+
/** Pins the bar to the bottom of the viewport, above the home-indicator safe area */
|
|
56
|
+
fixed?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Accessible name of the navigation landmark. Defaults to "Tabs", distinct
|
|
59
|
+
* from Nav's "Main", so a screen carrying both announces two different navs
|
|
60
|
+
*/
|
|
61
|
+
'aria-label'?: string;
|
|
62
|
+
/** Additional CSS classes */
|
|
63
|
+
className?: string;
|
|
64
|
+
};
|
|
65
|
+
export interface BottomNavProps extends BottomNavOwnProps, Omit<React.ComponentPropsWithoutRef<'nav'>, keyof BottomNavOwnProps> {
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* BottomNav — the tab bar of a mobile app: three to five top-level
|
|
69
|
+
* destinations along the bottom of the screen, in the Rift style or as a
|
|
70
|
+
* close match for iOS or Android.
|
|
71
|
+
*/
|
|
72
|
+
export declare const BottomNav: React.ForwardRefExoticComponent<BottomNavProps & React.RefAttributes<HTMLElement>>;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import "./BottomNav.css";
|
|
5
|
+
import "../../fonts/material-symbols.css";
|
|
6
|
+
const BottomNav = React.forwardRef(
|
|
7
|
+
({
|
|
8
|
+
items,
|
|
9
|
+
activeKey,
|
|
10
|
+
onValueChange,
|
|
11
|
+
platform = "default",
|
|
12
|
+
search,
|
|
13
|
+
minimized = false,
|
|
14
|
+
fixed = false,
|
|
15
|
+
"aria-label": ariaLabel = "Tabs",
|
|
16
|
+
className = "",
|
|
17
|
+
style,
|
|
18
|
+
...rest
|
|
19
|
+
}, ref) => {
|
|
20
|
+
const baseClass = "ds-bottom-nav";
|
|
21
|
+
const isIos = platform === "ios";
|
|
22
|
+
const destinations = search && !isIos ? [...items, { key: "__search", label: search.label, icon: search.icon ?? "search", href: search.href, onClick: search.onClick }] : items;
|
|
23
|
+
const activeIndex = destinations.findIndex((item) => item.key === activeKey);
|
|
24
|
+
const isMinimized = isIos && minimized && activeIndex >= 0;
|
|
25
|
+
const classes = [
|
|
26
|
+
baseClass,
|
|
27
|
+
`${baseClass}--${platform}`,
|
|
28
|
+
fixed ? `${baseClass}--fixed` : "",
|
|
29
|
+
isMinimized ? `${baseClass}--minimized` : "",
|
|
30
|
+
className
|
|
31
|
+
].filter(Boolean).join(" ");
|
|
32
|
+
const lensStyle = {
|
|
33
|
+
...style,
|
|
34
|
+
"--ds-bottom-nav-count": destinations.length,
|
|
35
|
+
"--ds-bottom-nav-index": Math.max(activeIndex, 0)
|
|
36
|
+
};
|
|
37
|
+
const renderIcon = (icon) => typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: icon });
|
|
38
|
+
const renderBadge = (badge) => {
|
|
39
|
+
if (badge === void 0) return null;
|
|
40
|
+
return badge === true ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__badge ${baseClass}__badge--dot`, "aria-hidden": "true" }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__badge`, "aria-hidden": "true", children: badge });
|
|
41
|
+
};
|
|
42
|
+
return /* @__PURE__ */ jsxs("nav", { ...rest, ref, className: classes, style: lensStyle, "aria-label": ariaLabel, children: [
|
|
43
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__bar`, children: [
|
|
44
|
+
isIos && activeIndex >= 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__lens`, "aria-hidden": "true" }),
|
|
45
|
+
/* @__PURE__ */ jsx("ul", { className: `${baseClass}__list`, children: destinations.map((item) => {
|
|
46
|
+
const isActive = item.key === activeKey;
|
|
47
|
+
const itemClasses = [
|
|
48
|
+
`${baseClass}__item`,
|
|
49
|
+
isActive ? `${baseClass}__item--active` : ""
|
|
50
|
+
].filter(Boolean).join(" ");
|
|
51
|
+
const badgeText = item.badge === void 0 ? "" : item.badge === true ? ", new" : `, ${item.badge} new`;
|
|
52
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
53
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__indicator`, children: [
|
|
54
|
+
renderIcon(item.icon),
|
|
55
|
+
renderBadge(item.badge)
|
|
56
|
+
] }),
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: item.label }),
|
|
58
|
+
badgeText && /* @__PURE__ */ jsx("span", { className: `${baseClass}__sr`, children: badgeText })
|
|
59
|
+
] });
|
|
60
|
+
const handleClick = () => {
|
|
61
|
+
item.onClick?.();
|
|
62
|
+
if (item.key !== "__search") onValueChange?.(item.key);
|
|
63
|
+
};
|
|
64
|
+
return /* @__PURE__ */ jsx(
|
|
65
|
+
"li",
|
|
66
|
+
{
|
|
67
|
+
className: `${baseClass}__slot`,
|
|
68
|
+
inert: isMinimized && !isActive,
|
|
69
|
+
children: item.href ? /* @__PURE__ */ jsx(
|
|
70
|
+
"a",
|
|
71
|
+
{
|
|
72
|
+
className: itemClasses,
|
|
73
|
+
href: item.href,
|
|
74
|
+
onClick: handleClick,
|
|
75
|
+
"aria-current": isActive ? "page" : void 0,
|
|
76
|
+
children: content
|
|
77
|
+
}
|
|
78
|
+
) : /* @__PURE__ */ jsx(
|
|
79
|
+
"button",
|
|
80
|
+
{
|
|
81
|
+
type: "button",
|
|
82
|
+
className: itemClasses,
|
|
83
|
+
onClick: handleClick,
|
|
84
|
+
"aria-current": isActive ? "page" : void 0,
|
|
85
|
+
children: content
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
item.key
|
|
90
|
+
);
|
|
91
|
+
}) })
|
|
92
|
+
] }),
|
|
93
|
+
isIos && search && (search.href ? /* @__PURE__ */ jsx("a", { className: `${baseClass}__search`, href: search.href, onClick: search.onClick, "aria-label": search.label, children: renderIcon(search.icon ?? "search") }) : /* @__PURE__ */ jsx("button", { type: "button", className: `${baseClass}__search`, onClick: search.onClick, "aria-label": search.label, children: renderIcon(search.icon ?? "search") }))
|
|
94
|
+
] });
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
BottomNav.displayName = "BottomNav";
|
|
98
|
+
export {
|
|
99
|
+
BottomNav
|
|
100
|
+
};
|