rift-ds 1.0.1 → 1.2.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 (51) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -2
  3. package/components/AppLayout/AppLayout.css +13 -3
  4. package/components/AppLayout/AppLayout.d.ts +5 -1
  5. package/components/AppLayout/AppLayout.js +4 -0
  6. package/components/AppSidebar/AppSidebar.css +237 -0
  7. package/components/AppSidebar/AppSidebar.d.ts +32 -3
  8. package/components/AppSidebar/AppSidebar.js +187 -16
  9. package/components/BottomNav/BottomNav.css +417 -0
  10. package/components/BottomNav/BottomNav.d.ts +73 -0
  11. package/components/BottomNav/BottomNav.js +100 -0
  12. package/components/Chart/AreaChart.js +67 -56
  13. package/components/Chart/BarChart.js +67 -56
  14. package/components/Chart/ComboChart.js +93 -82
  15. package/components/Chart/LineChart.js +59 -48
  16. package/components/Chart/PieChart.js +56 -45
  17. package/components/Chart/RadarChart.js +60 -49
  18. package/components/Chart/RadialChart.js +70 -59
  19. package/components/Chart/ScatterChart.js +61 -50
  20. package/components/Chart/StackedBarChart.js +64 -53
  21. package/components/Chart/Treemap.js +34 -23
  22. package/components/Inspector/Inspector.css +511 -0
  23. package/components/Inspector/InspectorDropdown.d.ts +39 -0
  24. package/components/Inspector/InspectorDropdown.js +68 -0
  25. package/components/Inspector/InspectorInput.d.ts +32 -0
  26. package/components/Inspector/InspectorInput.js +52 -0
  27. package/components/Inspector/InspectorSection.d.ts +32 -0
  28. package/components/Inspector/InspectorSection.js +70 -0
  29. package/components/Inspector/InspectorSegmentedControl.d.ts +46 -0
  30. package/components/Inspector/InspectorSegmentedControl.js +62 -0
  31. package/components/Inspector/InspectorSlider.d.ts +41 -0
  32. package/components/Inspector/InspectorSlider.js +81 -0
  33. package/components/Inspector/InspectorToggleSwitch.d.ts +29 -0
  34. package/components/Inspector/InspectorToggleSwitch.js +51 -0
  35. package/components/Nav/Nav.d.ts +11 -3
  36. package/components/Nav/Nav.js +3 -2
  37. package/components/SegmentedControl/SegmentedControl.css +7 -5
  38. package/components/SidebarPanel/SidebarPanel.css +291 -0
  39. package/components/SidebarPanel/SidebarPanel.d.ts +45 -0
  40. package/components/SidebarPanel/SidebarPanel.js +166 -0
  41. package/components/Stepper/Stepper.d.ts +3 -1
  42. package/components/Stepper/Stepper.js +1 -1
  43. package/components/TopAppBar/TopAppBar.css +464 -0
  44. package/components/TopAppBar/TopAppBar.d.ts +80 -0
  45. package/components/TopAppBar/TopAppBar.js +138 -0
  46. package/components/registry.json +84 -1
  47. package/components/registry.json.d.ts +84 -1
  48. package/components/registry.json.js +2 -2
  49. package/index.d.ts +9 -0
  50. package/index.js +18 -0
  51. package/package.json +5 -1
@@ -0,0 +1,464 @@
1
+ /* ============================================
2
+ TOP APP BAR — the header of a mobile screen
3
+ Three platforms share one markup:
4
+ default the Rift bar, fully on-token
5
+ ios the iOS 26 navigation bar
6
+ android the Material 3 Expressive top app bar
7
+ A row (leading control, title, trailing
8
+ actions) and, for medium and large sizes, an
9
+ expanded title that collapses on scroll.
10
+ ============================================ */
11
+
12
+ /* ============================================
13
+ BASE (shared by every platform)
14
+ ============================================ */
15
+ .ds-top-app-bar {
16
+ position: relative;
17
+ box-sizing: border-box;
18
+ width: 100%;
19
+ /* The status-bar area on notched phones; zero elsewhere. */
20
+ padding-top: env(safe-area-inset-top, 0px);
21
+ color: var(--color-text-primary);
22
+ transition: background-color var(--motion-duration-base) var(--motion-ease-standard);
23
+ }
24
+
25
+ .ds-top-app-bar--sticky {
26
+ position: sticky;
27
+ top: 0;
28
+ z-index: 10;
29
+ }
30
+
31
+ .ds-top-app-bar__row {
32
+ position: relative;
33
+ display: grid;
34
+ grid-template-columns: auto minmax(0, 1fr) auto;
35
+ align-items: center;
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ /* Centred titles hold the middle column however wide either side is. */
40
+ .ds-top-app-bar--align-center .ds-top-app-bar__row {
41
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
42
+ }
43
+
44
+ .ds-top-app-bar__leading,
45
+ .ds-top-app-bar__trailing {
46
+ display: flex;
47
+ align-items: center;
48
+ min-width: 0;
49
+ }
50
+
51
+ .ds-top-app-bar__trailing {
52
+ justify-content: flex-end;
53
+ }
54
+
55
+ .ds-top-app-bar__group {
56
+ display: flex;
57
+ align-items: center;
58
+ }
59
+
60
+ /* The bar's title, and the expanded title under it. */
61
+ .ds-top-app-bar__title,
62
+ .ds-top-app-bar__large-title {
63
+ display: flex;
64
+ flex-direction: column;
65
+ min-width: 0;
66
+ margin: 0;
67
+ font: inherit;
68
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
69
+ }
70
+
71
+ .ds-top-app-bar--align-center .ds-top-app-bar__title {
72
+ align-items: center;
73
+ text-align: center;
74
+ }
75
+
76
+ .ds-top-app-bar__title--hidden {
77
+ opacity: 0;
78
+ }
79
+
80
+ .ds-top-app-bar__title-text,
81
+ .ds-top-app-bar__subtitle {
82
+ max-width: 100%;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ .ds-top-app-bar__subtitle {
89
+ color: var(--color-text-secondary);
90
+ }
91
+
92
+ /* Expanded title: the grid-row trick collapses it to nothing without a
93
+ height guess, and the fade leads the fold. */
94
+ .ds-top-app-bar__expanded {
95
+ display: grid;
96
+ grid-template-rows: 1fr;
97
+ transition:
98
+ grid-template-rows var(--motion-duration-slow) var(--motion-ease-emphasized),
99
+ opacity var(--motion-duration-base) var(--motion-ease-standard);
100
+ }
101
+
102
+ .ds-top-app-bar--scrolled .ds-top-app-bar__expanded {
103
+ grid-template-rows: 0fr;
104
+ opacity: 0;
105
+ }
106
+
107
+ .ds-top-app-bar__expanded-inner {
108
+ min-height: 0;
109
+ overflow: hidden;
110
+ }
111
+
112
+ /* Icon buttons. */
113
+ .ds-top-app-bar__button {
114
+ position: relative;
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ flex-shrink: 0;
119
+ padding: 0;
120
+ border: none;
121
+ background: transparent;
122
+ color: inherit;
123
+ text-decoration: none;
124
+ cursor: pointer;
125
+ -webkit-tap-highlight-color: transparent;
126
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
127
+ }
128
+
129
+ .ds-top-app-bar__button:focus-visible {
130
+ outline: 2px solid var(--color-action-primary-bg);
131
+ outline-offset: -2px;
132
+ }
133
+
134
+ .ds-top-app-bar__icon {
135
+ --icon-size: var(--icon-size-600);
136
+ display: flex;
137
+ color: inherit;
138
+ }
139
+
140
+ .ds-top-app-bar__badge {
141
+ position: absolute;
142
+ top: 25%;
143
+ right: 25%;
144
+ width: 6px;
145
+ height: 6px;
146
+ border-radius: var(--radius-pill);
147
+ background-color: var(--color-status-error-icon);
148
+ }
149
+
150
+ /* The overflow menu's own wrapper takes no room of its own. */
151
+ .ds-top-app-bar__overflow {
152
+ display: flex;
153
+ }
154
+
155
+ .ds-top-app-bar .material-symbols-rounded {
156
+ color: inherit;
157
+ }
158
+
159
+ /* ============================================
160
+ DEFAULT — the Rift bar
161
+ ============================================ */
162
+ .ds-top-app-bar--default {
163
+ background-color: var(--color-bg-page-primary);
164
+ /* The hairline is present from the start, transparent until content
165
+ slides under the bar, so the scrolled state moves no geometry. */
166
+ border-bottom: var(--border-025) solid transparent;
167
+ transition:
168
+ background-color var(--motion-duration-base) var(--motion-ease-standard),
169
+ border-color var(--motion-duration-base) var(--motion-ease-standard);
170
+ }
171
+
172
+ .ds-top-app-bar--default.ds-top-app-bar--scrolled {
173
+ border-bottom-color: var(--color-bg-container-border);
174
+ }
175
+
176
+ .ds-top-app-bar--default .ds-top-app-bar__row {
177
+ min-height: 56px;
178
+ gap: var(--gap-100);
179
+ padding: 0 var(--padding-200);
180
+ }
181
+
182
+ .ds-top-app-bar--default .ds-top-app-bar__leading:empty {
183
+ width: var(--padding-200);
184
+ }
185
+
186
+ .ds-top-app-bar--default .ds-top-app-bar__button {
187
+ width: 40px;
188
+ height: 40px;
189
+ border-radius: var(--radius-pill);
190
+ }
191
+
192
+ .ds-top-app-bar--default .ds-top-app-bar__button:hover {
193
+ background-color: var(--color-action-passive-bg-hover);
194
+ }
195
+
196
+ .ds-top-app-bar--default .ds-top-app-bar__title {
197
+ padding: 0 var(--padding-100);
198
+ }
199
+
200
+ .ds-top-app-bar--default .ds-top-app-bar__title-text {
201
+ font-family: var(--font-title-body-family);
202
+ font-size: var(--font-title-body-size);
203
+ font-weight: var(--font-title-body-weight);
204
+ line-height: var(--font-title-body-line-height);
205
+ letter-spacing: var(--font-title-body-letter-spacing);
206
+ }
207
+
208
+ .ds-top-app-bar--default .ds-top-app-bar__subtitle {
209
+ font-family: var(--font-paragraph-sm-family);
210
+ font-size: var(--font-paragraph-sm-size);
211
+ font-weight: var(--font-paragraph-sm-weight);
212
+ line-height: var(--font-paragraph-sm-line-height);
213
+ }
214
+
215
+ .ds-top-app-bar--default .ds-top-app-bar__large-title {
216
+ gap: var(--gap-100);
217
+ padding: var(--padding-100) var(--padding-400) var(--padding-400);
218
+ }
219
+
220
+ .ds-top-app-bar--default.ds-top-app-bar--medium .ds-top-app-bar__large-title .ds-top-app-bar__title-text {
221
+ font-family: var(--font-heading-2-family);
222
+ font-size: var(--font-heading-2-size);
223
+ font-weight: var(--font-heading-2-weight);
224
+ line-height: var(--font-heading-2-line-height);
225
+ letter-spacing: var(--font-heading-2-letter-spacing);
226
+ }
227
+
228
+ .ds-top-app-bar--default.ds-top-app-bar--large .ds-top-app-bar__large-title .ds-top-app-bar__title-text {
229
+ font-family: var(--font-heading-1-family);
230
+ font-size: var(--font-heading-1-size);
231
+ font-weight: var(--font-heading-1-weight);
232
+ line-height: var(--font-heading-1-line-height);
233
+ letter-spacing: var(--font-heading-1-letter-spacing);
234
+ }
235
+
236
+ /* ============================================
237
+ iOS — the iOS 26 navigation bar
238
+ ds-allow(platform): iOS 26 navigation-bar geometry and type — the 44pt
239
+ row with 16pt margins, 44pt glass circle buttons and the grouped glass
240
+ capsule, the 17pt semibold inline title and 34pt bold large title in
241
+ the system face, the glass fill, blur, saturation and white rim, and
242
+ the blurred scroll edge — restated from the platform so the mock reads
243
+ as system chrome.
244
+ ============================================ */
245
+ .ds-top-app-bar--ios {
246
+ --ds-top-app-bar-glass: color-mix(in srgb, var(--color-bg-container-primary) 58%, transparent);
247
+ --ds-top-app-bar-glass-filter: blur(16px) saturate(180%);
248
+ --ds-top-app-bar-glass-rim:
249
+ inset 0 1px 0 0 rgb(255 255 255 / 0.55),
250
+ inset 0 0 0 0.5px rgb(255 255 255 / 0.28),
251
+ 0 6px 24px rgb(0 0 0 / 0.12),
252
+ 0 1px 3px rgb(0 0 0 / 0.08);
253
+ background: transparent;
254
+ font-family: system-ui, -apple-system, 'SF Pro Text', sans-serif;
255
+ }
256
+
257
+ /* The scroll edge: no bar fill and no hairline, just the content under
258
+ the bar blurring and fading out toward the top edge. */
259
+ .ds-top-app-bar--ios::before {
260
+ content: '';
261
+ position: absolute;
262
+ inset: 0 0 -16px;
263
+ z-index: -1;
264
+ background: linear-gradient(
265
+ to bottom,
266
+ color-mix(in srgb, var(--color-bg-page-primary) 80%, transparent),
267
+ transparent
268
+ );
269
+ -webkit-backdrop-filter: blur(6px);
270
+ backdrop-filter: blur(6px);
271
+ -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent);
272
+ mask-image: linear-gradient(to bottom, #000 55%, transparent);
273
+ opacity: 0;
274
+ pointer-events: none;
275
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
276
+ }
277
+
278
+ .ds-top-app-bar--ios.ds-top-app-bar--scrolled::before {
279
+ opacity: 1;
280
+ }
281
+
282
+ .ds-top-app-bar--ios .ds-top-app-bar__row {
283
+ min-height: 44px;
284
+ gap: 8px;
285
+ padding: 6px 16px;
286
+ }
287
+
288
+ .ds-top-app-bar--ios .ds-top-app-bar__button {
289
+ width: 44px;
290
+ height: 44px;
291
+ border-radius: 999px;
292
+ }
293
+
294
+ /* A lone leading button and a lone trailing button are their own glass
295
+ circles; two or more trailing buttons share one capsule. */
296
+ .ds-top-app-bar--ios .ds-top-app-bar__button--nav,
297
+ .ds-top-app-bar--ios .ds-top-app-bar__group {
298
+ border-radius: 999px;
299
+ background-color: var(--ds-top-app-bar-glass);
300
+ box-shadow: var(--ds-top-app-bar-glass-rim);
301
+ -webkit-backdrop-filter: var(--ds-top-app-bar-glass-filter);
302
+ backdrop-filter: var(--ds-top-app-bar-glass-filter);
303
+ }
304
+
305
+ .ds-top-app-bar--ios .ds-top-app-bar__group {
306
+ padding: 0 2px;
307
+ }
308
+
309
+ .ds-top-app-bar--ios .ds-top-app-bar__group .ds-top-app-bar__button {
310
+ width: 40px;
311
+ }
312
+
313
+ .ds-top-app-bar--ios .ds-top-app-bar__group:has(> :only-child) {
314
+ padding: 0;
315
+ }
316
+
317
+ .ds-top-app-bar--ios .ds-top-app-bar__group:has(> :only-child) .ds-top-app-bar__button {
318
+ width: 44px;
319
+ }
320
+
321
+ /* SF Symbols in a bar draw at a medium weight; the icon font's default
322
+ 400 read spindly inside the glass circles. */
323
+ .ds-top-app-bar--ios .ds-top-app-bar__icon {
324
+ font-variation-settings: 'wght' 500;
325
+ }
326
+
327
+ .ds-top-app-bar--ios .ds-top-app-bar__button--nav .ds-top-app-bar__icon {
328
+ /* The chevron's glyph sits right of centre in its box; a nudge
329
+ centres it optically in the circle. */
330
+ transform: translateX(-1px);
331
+ }
332
+
333
+ .ds-top-app-bar--ios .ds-top-app-bar__title-text {
334
+ font-size: 17px;
335
+ font-weight: 600;
336
+ line-height: 22px;
337
+ letter-spacing: -0.02em;
338
+ }
339
+
340
+ .ds-top-app-bar--ios .ds-top-app-bar__subtitle {
341
+ font-size: 12px;
342
+ font-weight: 400;
343
+ line-height: 16px;
344
+ }
345
+
346
+ .ds-top-app-bar--ios .ds-top-app-bar__large-title {
347
+ padding: 2px 16px 8px;
348
+ }
349
+
350
+ .ds-top-app-bar--ios .ds-top-app-bar__large-title .ds-top-app-bar__title-text {
351
+ font-size: 34px;
352
+ font-weight: 700;
353
+ line-height: 41px;
354
+ letter-spacing: 0.01em;
355
+ }
356
+
357
+ .ds-top-app-bar--ios .ds-top-app-bar__large-title .ds-top-app-bar__subtitle {
358
+ font-size: 15px;
359
+ line-height: 20px;
360
+ }
361
+
362
+ /* ============================================
363
+ ANDROID — Material 3 Expressive top app bar
364
+ ds-allow(platform): the M3 Expressive app bar — a 64dp row with 4dp
365
+ edge padding, 48dp touch targets around 40dp icon-button containers,
366
+ the 22/28 title and 12/16 subtitle, the 28/36 medium and 36/44 large
367
+ flexible titles, and the platform face (Google Sans Flex, then Roboto)
368
+ — restated from the Material token set so the mock reads as system
369
+ chrome. The surface roles map onto the page and container tokens.
370
+ ============================================ */
371
+ .ds-top-app-bar--android {
372
+ background-color: var(--color-bg-page-primary);
373
+ font-family: 'Google Sans Flex', Roboto, system-ui, sans-serif;
374
+ }
375
+
376
+ /* On scroll the surface lifts to the container role. */
377
+ .ds-top-app-bar--android.ds-top-app-bar--scrolled {
378
+ background-color: var(--color-bg-container-secondary);
379
+ }
380
+
381
+ .ds-top-app-bar--android .ds-top-app-bar__row {
382
+ min-height: 64px;
383
+ padding: 0 4px;
384
+ }
385
+
386
+ .ds-top-app-bar--android .ds-top-app-bar__leading:empty {
387
+ width: 12px;
388
+ }
389
+
390
+ .ds-top-app-bar--android .ds-top-app-bar__button {
391
+ width: 48px;
392
+ height: 48px;
393
+ color: var(--color-text-secondary);
394
+ }
395
+
396
+ .ds-top-app-bar--android .ds-top-app-bar__button--nav {
397
+ color: var(--color-text-primary);
398
+ }
399
+
400
+ /* The 40dp state layer inside the 48dp target. */
401
+ .ds-top-app-bar--android .ds-top-app-bar__button::before {
402
+ content: '';
403
+ position: absolute;
404
+ inset: 4px;
405
+ border-radius: 999px;
406
+ background-color: var(--color-text-primary);
407
+ opacity: 0;
408
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
409
+ }
410
+
411
+ .ds-top-app-bar--android .ds-top-app-bar__button:hover::before {
412
+ opacity: 0.08;
413
+ }
414
+
415
+ .ds-top-app-bar--android .ds-top-app-bar__button:active::before {
416
+ opacity: 0.12;
417
+ }
418
+
419
+ .ds-top-app-bar--android .ds-top-app-bar__title {
420
+ padding: 0 4px;
421
+ }
422
+
423
+ .ds-top-app-bar--android .ds-top-app-bar__title-text {
424
+ font-size: 22px;
425
+ font-weight: 400;
426
+ line-height: 28px;
427
+ }
428
+
429
+ .ds-top-app-bar--android .ds-top-app-bar__subtitle {
430
+ font-size: 12px;
431
+ font-weight: 500;
432
+ line-height: 16px;
433
+ letter-spacing: 0.5px;
434
+ }
435
+
436
+ .ds-top-app-bar--android .ds-top-app-bar__large-title {
437
+ padding: 0 16px 12px;
438
+ }
439
+
440
+ .ds-top-app-bar--android.ds-top-app-bar--medium .ds-top-app-bar__large-title .ds-top-app-bar__title-text {
441
+ font-size: 28px;
442
+ line-height: 36px;
443
+ }
444
+
445
+ .ds-top-app-bar--android.ds-top-app-bar--medium .ds-top-app-bar__large-title .ds-top-app-bar__subtitle {
446
+ font-size: 14px;
447
+ line-height: 20px;
448
+ letter-spacing: 0.1px;
449
+ }
450
+
451
+ .ds-top-app-bar--android.ds-top-app-bar--large .ds-top-app-bar__large-title {
452
+ padding-top: 4px;
453
+ }
454
+
455
+ .ds-top-app-bar--android.ds-top-app-bar--large .ds-top-app-bar__large-title .ds-top-app-bar__title-text {
456
+ font-size: 36px;
457
+ line-height: 44px;
458
+ }
459
+
460
+ .ds-top-app-bar--android.ds-top-app-bar--large .ds-top-app-bar__large-title .ds-top-app-bar__subtitle {
461
+ font-size: 16px;
462
+ line-height: 24px;
463
+ letter-spacing: 0.15px;
464
+ }
@@ -0,0 +1,80 @@
1
+ import { default as React } from 'react';
2
+ export interface TopAppBarAction {
3
+ /** Unique key for this action */
4
+ key: string;
5
+ /** Icon — Material Symbol name (string) or custom element (ReactNode) */
6
+ icon: string | React.ReactNode;
7
+ /** Accessible label; also the overflow menu's row text */
8
+ label: string;
9
+ /** Click handler */
10
+ onClick?: () => void;
11
+ /** Renders the action as a link to this URL instead of a button */
12
+ href?: string;
13
+ /** Shows a dot on the icon's corner, e.g. unread notifications */
14
+ badge?: boolean;
15
+ }
16
+ /**
17
+ * An action in the overflow menu. The menu's rows are buttons and draw no
18
+ * icon badge, so `href` and `badge` are left out rather than silently
19
+ * dropped: route from `onClick` instead.
20
+ */
21
+ export type TopAppBarOverflowAction = Omit<TopAppBarAction, 'href' | 'badge'>;
22
+ type TopAppBarOwnProps = {
23
+ /** The screen's title */
24
+ title: string;
25
+ /** A second line under the title, e.g. a count or a date range */
26
+ subtitle?: string;
27
+ /**
28
+ * Which platform's bar to draw. `default` is the Rift bar; `ios` is the iOS
29
+ * 26 navigation bar, with glass circle buttons over content and a soft
30
+ * blurred edge; `android` is the Material 3 Expressive top app bar.
31
+ */
32
+ platform?: 'default' | 'ios' | 'android';
33
+ /**
34
+ * `small` puts the title in the bar. `medium` and `large` add an expanded
35
+ * title under it that collapses into the bar as the page scrolls; on iOS
36
+ * both are the large title.
37
+ */
38
+ size?: 'small' | 'medium' | 'large';
39
+ /** Title alignment in the bar. Defaults to centred on iOS and to the start edge elsewhere */
40
+ align?: 'start' | 'center';
41
+ /** The leading control: a menu button, a back button, or a close button */
42
+ navigation?: 'menu' | 'back' | 'close';
43
+ /** Called when the leading control is pressed */
44
+ onNavigate?: () => void;
45
+ /** Accessible label of the leading control; defaults to "Open menu", "Back" or "Close" */
46
+ navigationLabel?: string;
47
+ /**
48
+ * Trailing actions. The first three show as icon buttons; any more join the
49
+ * overflow menu, ahead of `overflowActions`, where they render as plain rows
50
+ * (a menu row draws no badge and routes from `onClick`, not `href`)
51
+ */
52
+ actions?: TopAppBarAction[];
53
+ /** Actions listed in a menu behind a trailing "More" button */
54
+ overflowActions?: TopAppBarOverflowAction[];
55
+ /** Accessible label of the button that opens the overflow menu */
56
+ overflowLabel?: string;
57
+ /**
58
+ * Whether content has scrolled under the bar, which collapses an expanded
59
+ * title and switches the bar to its scrolled surface. Leave unset to have
60
+ * the bar watch `scrollTarget` itself.
61
+ */
62
+ scrolled?: boolean;
63
+ /** The element whose scroll the bar watches when `scrolled` is unset. Defaults to the window */
64
+ scrollTarget?: HTMLElement | null;
65
+ /** Level of the title's heading element */
66
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
67
+ /** Keeps the bar stuck to the top of its scroll container */
68
+ sticky?: boolean;
69
+ /** Additional CSS classes */
70
+ className?: string;
71
+ };
72
+ export interface TopAppBarProps extends TopAppBarOwnProps, Omit<React.ComponentPropsWithoutRef<'header'>, keyof TopAppBarOwnProps> {
73
+ }
74
+ /**
75
+ * TopAppBar — the header of a mobile screen: a leading menu or back button,
76
+ * the screen title, and trailing actions. Comes in the Rift style or as a
77
+ * close match for iOS or Android, with titles that collapse on scroll.
78
+ */
79
+ export declare const TopAppBar: React.ForwardRefExoticComponent<TopAppBarProps & React.RefAttributes<HTMLElement>>;
80
+ export {};
@@ -0,0 +1,138 @@
1
+ "use client";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
+ import React, { useState, useEffect } from "react";
4
+ import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
5
+ import "./TopAppBar.css";
6
+ import "../../fonts/material-symbols.css";
7
+ const NAV_ICONS = {
8
+ default: { menu: "menu", back: "arrow_back", close: "close" },
9
+ ios: { menu: "menu", back: "chevron_left", close: "close" },
10
+ android: { menu: "menu", back: "arrow_back", close: "close" }
11
+ };
12
+ const NAV_LABELS = { menu: "Open menu", back: "Back", close: "Close" };
13
+ const TopAppBar = React.forwardRef(
14
+ ({
15
+ title,
16
+ subtitle,
17
+ platform = "default",
18
+ size = "small",
19
+ align,
20
+ navigation,
21
+ onNavigate,
22
+ navigationLabel,
23
+ actions = [],
24
+ overflowActions = [],
25
+ overflowLabel = "More",
26
+ scrolled: controlledScrolled,
27
+ scrollTarget,
28
+ headingLevel = 1,
29
+ sticky = true,
30
+ className = "",
31
+ ...rest
32
+ }, ref) => {
33
+ const baseClass = "ds-top-app-bar";
34
+ const isIos = platform === "ios";
35
+ const resolvedAlign = align ?? (isIos ? "center" : "start");
36
+ const expandable = size !== "small";
37
+ const [watchedScrolled, setWatchedScrolled] = useState(false);
38
+ useEffect(() => {
39
+ if (controlledScrolled !== void 0) return;
40
+ const target = scrollTarget ?? window;
41
+ const read = () => {
42
+ const top = target instanceof Window ? target.scrollY : target.scrollTop;
43
+ setWatchedScrolled(top > 4);
44
+ };
45
+ read();
46
+ target.addEventListener("scroll", read, { passive: true });
47
+ return () => target.removeEventListener("scroll", read);
48
+ }, [controlledScrolled, scrollTarget]);
49
+ const isScrolled = controlledScrolled ?? watchedScrolled;
50
+ const inlineTitleShown = !expandable || isScrolled;
51
+ const classes = [
52
+ baseClass,
53
+ `${baseClass}--${platform}`,
54
+ `${baseClass}--${size}`,
55
+ `${baseClass}--align-${resolvedAlign}`,
56
+ isScrolled ? `${baseClass}--scrolled` : "",
57
+ sticky ? `${baseClass}--sticky` : "",
58
+ className
59
+ ].filter(Boolean).join(" ");
60
+ const Heading = `h${headingLevel}`;
61
+ 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 });
62
+ const renderAction = (action) => {
63
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
64
+ renderIcon(action.icon),
65
+ action.badge && /* @__PURE__ */ jsx("span", { className: `${baseClass}__badge`, "aria-hidden": "true" })
66
+ ] });
67
+ const label = action.badge ? `${action.label}, new` : action.label;
68
+ return action.href ? /* @__PURE__ */ jsx(
69
+ "a",
70
+ {
71
+ className: `${baseClass}__button`,
72
+ href: action.href,
73
+ onClick: action.onClick,
74
+ "aria-label": label,
75
+ children: content
76
+ },
77
+ action.key
78
+ ) : /* @__PURE__ */ jsx(
79
+ "button",
80
+ {
81
+ type: "button",
82
+ className: `${baseClass}__button`,
83
+ onClick: action.onClick,
84
+ "aria-label": label,
85
+ children: content
86
+ },
87
+ action.key
88
+ );
89
+ };
90
+ const titleBlock = /* @__PURE__ */ jsxs(Fragment, { children: [
91
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__title-text`, children: title }),
92
+ subtitle && /* @__PURE__ */ jsx("span", { className: `${baseClass}__subtitle`, children: subtitle })
93
+ ] });
94
+ const trailing = actions.slice(0, 3);
95
+ const overflowItems = [
96
+ ...actions.slice(3).map(({ key, icon, label, onClick }) => ({ key, icon, label, onClick })),
97
+ ...overflowActions
98
+ ];
99
+ const hasTrailing = trailing.length > 0 || overflowItems.length > 0;
100
+ return /* @__PURE__ */ jsxs("header", { ...rest, ref, className: classes, children: [
101
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__row`, children: [
102
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__leading`, children: navigation && /* @__PURE__ */ jsx(
103
+ "button",
104
+ {
105
+ type: "button",
106
+ className: `${baseClass}__button ${baseClass}__button--nav`,
107
+ onClick: onNavigate,
108
+ "aria-label": navigationLabel ?? NAV_LABELS[navigation],
109
+ children: renderIcon(NAV_ICONS[platform][navigation])
110
+ }
111
+ ) }),
112
+ inlineTitleShown ? /* @__PURE__ */ jsx(Heading, { className: `${baseClass}__title`, children: titleBlock }) : /* @__PURE__ */ jsx("div", { className: `${baseClass}__title ${baseClass}__title--hidden`, "aria-hidden": "true", children: titleBlock }),
113
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__trailing`, children: hasTrailing && /* iOS groups the trailing buttons into one glass capsule. */
114
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__group`, children: [
115
+ trailing.map(renderAction),
116
+ overflowItems.length > 0 && /* @__PURE__ */ jsx(
117
+ DropdownMenu,
118
+ {
119
+ align: "end",
120
+ className: `${baseClass}__overflow`,
121
+ trigger: /* @__PURE__ */ jsx("button", { type: "button", className: `${baseClass}__button`, "aria-label": overflowLabel, children: renderIcon(isIos ? "more_horiz" : "more_vert") }),
122
+ items: overflowItems.map((action) => ({
123
+ label: action.label,
124
+ icon: action.icon,
125
+ onClick: action.onClick
126
+ }))
127
+ }
128
+ )
129
+ ] }) })
130
+ ] }),
131
+ expandable && /* @__PURE__ */ jsx("div", { className: `${baseClass}__expanded`, inert: isScrolled, children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__expanded-inner`, children: inlineTitleShown ? /* @__PURE__ */ jsx("div", { className: `${baseClass}__large-title`, "aria-hidden": "true", children: titleBlock }) : /* @__PURE__ */ jsx(Heading, { className: `${baseClass}__large-title`, children: titleBlock }) }) })
132
+ ] });
133
+ }
134
+ );
135
+ TopAppBar.displayName = "TopAppBar";
136
+ export {
137
+ TopAppBar
138
+ };