rift-ds 1.15.0 → 1.16.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.
@@ -0,0 +1,27 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import "react";
3
+ import "./ProgressiveBlur.css";
4
+ function ProgressiveBlur({
5
+ side = "top",
6
+ tinted = false,
7
+ className = "",
8
+ ref,
9
+ ...rest
10
+ }) {
11
+ const baseClass = "ds-progressive-blur";
12
+ const classes = [
13
+ baseClass,
14
+ `${baseClass}--${side}`,
15
+ tinted && `${baseClass}--tinted`,
16
+ className
17
+ ].filter(Boolean).join(" ");
18
+ return /* @__PURE__ */ jsxs("div", { "aria-hidden": "true", ...rest, ref, className: classes, children: [
19
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__layer` }),
20
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__layer` }),
21
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__layer` }),
22
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__layer` })
23
+ ] });
24
+ }
25
+ export {
26
+ ProgressiveBlur
27
+ };
@@ -508,7 +508,7 @@
508
508
  .ds-prompt-suggestions--kept .ds-prompt-suggestions__tile,
509
509
  .ds-prompt-suggestions--kept .ds-prompt-suggestions__card {
510
510
  border-style: dashed;
511
- border-color: var(--color-text-tertiary);
511
+ border-color: var(--color-bg-container-border);
512
512
  /* A record carries no fill, like QuestionCard's rows once answered: the
513
513
  outline alone says which way the conversation went. */
514
514
  background-color: transparent;
@@ -113,7 +113,8 @@
113
113
  }
114
114
  }
115
115
 
116
- /* An open row is drawn dashed: an answer on offer, not yet given. A pick
116
+ /* An open row is drawn dashed, in the suggestion chip's hairline colour:
117
+ an answer on offer, not yet given. A pick
117
118
  turns it solid, in the strongest ink, with the control's indicator on at
118
119
  its end, so a chosen row reads as decided at a glance and stays readable
119
120
  after the moment has passed. The row is a control, so it sits on Input's
@@ -128,7 +129,7 @@
128
129
  min-width: 0;
129
130
  margin: 0;
130
131
  padding: var(--padding-300) var(--padding-400);
131
- border: var(--border-025) dashed var(--color-text-tertiary);
132
+ border: var(--border-025) dashed var(--color-bg-container-border);
132
133
  border-radius: var(--radius-300);
133
134
  background-color: var(--color-input-bg-primary);
134
135
  color: var(--color-text-primary);
@@ -92,14 +92,8 @@ const QuestionCard = React.forwardRef(
92
92
  return;
93
93
  }
94
94
  const next = { values: [option.value], other: "", otherOpen: false };
95
- if (!isSet) {
96
- writeDraft(current, next);
97
- submit({ ...drafts, [current.id]: next });
98
- } else if (step < questions.length - 1) {
99
- advance(next);
100
- } else {
101
- writeDraft(current, next);
102
- }
95
+ writeDraft(current, next);
96
+ if (!isSet) submit({ ...drafts, [current.id]: next });
103
97
  };
104
98
  const openOther = () => {
105
99
  if (!live) return;
@@ -26,8 +26,10 @@
26
26
  width: 24px;
27
27
  height: 24px;
28
28
  box-sizing: border-box;
29
- border: var(--border-050) solid var(--color-input-border-primary);
30
- border-radius: var(--radius-pill); /* 999px — perfect circle */
29
+ border: var(--border-050) solid var(--color-input-indicator-border);
30
+ /* Always a circle, whatever the theme's radius: a radio's shape is its
31
+ meaning, so it never follows --radius-pill into a square. */
32
+ border-radius: 50%;
31
33
  background-color: var(--color-input-bg-primary);
32
34
  display: inline-flex;
33
35
  align-items: center;
@@ -43,7 +45,7 @@
43
45
  .ds-radio__dot {
44
46
  width: 12px;
45
47
  height: 12px;
46
- border-radius: var(--radius-pill);
48
+ border-radius: 50%;
47
49
  background-color: var(--color-action-primary-bg);
48
50
  transform: scale(0);
49
51
  transition: transform var(--motion-duration-base) var(--motion-ease-standard);
@@ -0,0 +1,305 @@
1
+ /* ============================================
2
+ SITE HEADER
3
+ The site's top bar: brand, section pills with
4
+ mega menus, actions, and a drawer once folded
5
+ ============================================ */
6
+
7
+ /* Base */
8
+ .ds-site-header {
9
+ /* The bar's height. An override hook: the blur's reach and a page's own
10
+ anchor offsets read it. */
11
+ --ds-site-header-height: calc(var(--control-height-default) + var(--padding-1000));
12
+ /* How far the blur dissolves below the bar. */
13
+ --ds-site-header-blur-reach: var(--gap-1500);
14
+ position: relative;
15
+ /* Lifts the open panel over the page's flow when the header is not sticky. */
16
+ z-index: 1;
17
+ width: 100%;
18
+ }
19
+
20
+ /* Sticky: rides up with whatever sits above it, then holds at the top. */
21
+ .ds-site-header--sticky {
22
+ position: sticky;
23
+ top: 0;
24
+ z-index: var(--layer-sticky);
25
+ }
26
+
27
+ /* The blur stack, viewport-fixed (a sticky box is not a containing block
28
+ for it) so it spans the full width whatever the host's gutters. It sits
29
+ inside the header's stacking context, behind the bar's controls. Never a
30
+ filter on the header itself: that would make the header a backdrop root
31
+ and the mega menu's own glass would sample only the header's subtree. */
32
+ .ds-site-header__blur.ds-progressive-blur {
33
+ position: fixed;
34
+ inset: 0 0 auto;
35
+ height: calc(var(--ds-site-header-height) + var(--ds-site-header-blur-reach));
36
+ z-index: -1;
37
+ }
38
+
39
+ /* The bar: brand, pills, then the end cluster pushed to the far side. */
40
+ .ds-site-header__bar {
41
+ position: relative;
42
+ display: flex;
43
+ align-items: center;
44
+ gap: var(--gap-400);
45
+ height: var(--ds-site-header-height);
46
+ box-sizing: border-box;
47
+ /* The folded bar keeps its pills out of flow at full width so they stay
48
+ measurable; clipping the inline axis keeps them from widening the page
49
+ into a sideways scroll. The margin leaves room for a focus ring at the
50
+ bar's edge, and the block axis stays open for the menu panel. */
51
+ overflow-x: clip;
52
+ overflow-clip-margin: var(--gap-200);
53
+ }
54
+
55
+ .ds-site-header__brand {
56
+ display: flex;
57
+ align-items: center;
58
+ flex-shrink: 0;
59
+ }
60
+
61
+ /* The pills sit beside the brand rather than centred on the page:
62
+ --gap-1000 from the brand (the bar's own gap plus this margin), and the
63
+ first pill's inline padding sets the rest of the visual gap. */
64
+ .ds-site-header__nav {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: var(--gap-100);
68
+ margin-inline-start: calc(var(--gap-1000) - var(--gap-400));
69
+ flex-shrink: 0;
70
+ }
71
+
72
+ .ds-site-header__end {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: var(--gap-400);
76
+ margin-inline-start: auto;
77
+ flex-shrink: 0;
78
+ }
79
+
80
+ .ds-site-header__actions,
81
+ .ds-site-header__wide-actions {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: var(--gap-400);
85
+ flex-shrink: 0;
86
+ }
87
+
88
+ .ds-site-header__section {
89
+ position: relative;
90
+ }
91
+
92
+ /* Pills: a pill-radius link at the compact control height, so a caret's
93
+ icon box never makes one pill taller than its neighbours. */
94
+ .ds-site-header__pill {
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: var(--gap-050);
98
+ height: var(--control-height-compact);
99
+ box-sizing: border-box;
100
+ padding-inline: var(--padding-400);
101
+ border-radius: var(--radius-pill);
102
+ background-color: transparent;
103
+ color: var(--color-text-primary);
104
+ font-family: var(--font-paragraph-sm-emphasis-family);
105
+ font-size: var(--font-paragraph-sm-emphasis-size);
106
+ font-weight: var(--font-paragraph-sm-emphasis-weight);
107
+ letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
108
+ line-height: 1;
109
+ white-space: nowrap;
110
+ text-decoration: none;
111
+ transition:
112
+ background-color var(--motion-duration-fast) var(--motion-ease-standard),
113
+ color var(--motion-duration-fast) var(--motion-ease-standard);
114
+ }
115
+
116
+ /* Hover is the tertiary family's wash, as on a tertiary Button, a NavList
117
+ row and the MegaMenu's rows: a container fill can match the page floor
118
+ in a light theme and vanish. */
119
+ .ds-site-header__pill:hover {
120
+ background-color: var(--color-action-passive-bg-hover);
121
+ }
122
+
123
+ .ds-site-header__pill:focus-visible {
124
+ outline: 2px solid var(--color-action-primary-bg);
125
+ outline-offset: 2px;
126
+ }
127
+
128
+ /* An open menu holds the hover fill and nothing more: opening a menu is
129
+ not a selection. Declared before --current so the current section keeps
130
+ its colour while its menu is open. */
131
+ .ds-site-header__pill--open {
132
+ background-color: var(--color-action-passive-bg-hover);
133
+ }
134
+
135
+ /* The current section is the selected item of an exclusive set, so it
136
+ takes the action selection pair, SegmentedControl's active segment; a
137
+ neutral fill would read as the hover state over the glassy blur. */
138
+ .ds-site-header__pill--current {
139
+ background-color: var(--color-action-primary-bg);
140
+ color: var(--color-action-primary-text);
141
+ }
142
+
143
+ .ds-site-header__pill--current:hover {
144
+ background-color: var(--color-action-primary-bg-hover);
145
+ }
146
+
147
+ .ds-site-header__caret {
148
+ --icon-size: var(--icon-size-500);
149
+ line-height: 1;
150
+ color: var(--color-text-secondary);
151
+ transition: rotate var(--motion-duration-base) var(--motion-ease-standard);
152
+ }
153
+
154
+ .ds-site-header__pill--current .ds-site-header__caret {
155
+ color: inherit;
156
+ }
157
+
158
+ .ds-site-header__pill--open .ds-site-header__caret {
159
+ rotate: 180deg;
160
+ }
161
+
162
+ /* The menu button only exists once the bar folds. */
163
+ .ds-site-header__menu-button {
164
+ display: none;
165
+ }
166
+
167
+ /* While the drawer is open its close button stands in this exact spot, on
168
+ the top layer; the button underneath would only show through the scrim. */
169
+ .ds-site-header__menu-button--hidden {
170
+ visibility: hidden;
171
+ }
172
+
173
+ /* The mega menu hangs under the bar across its full width. It is not
174
+ anchored to a control (useAnchoredPosition's job): it belongs to the bar
175
+ and spans it, so it rides the bar's own box.
176
+ The wrapper keeps the height of the last section shown after the menu
177
+ closes, so it never takes a press itself: a transparent box that size
178
+ would sit over the page and swallow its clicks. Only the menu does, and a
179
+ closed menu is hidden, which takes it out of hit testing too. */
180
+ .ds-site-header__panel {
181
+ position: absolute;
182
+ inset-block-start: calc(100% + var(--gap-200));
183
+ inset-inline: 0;
184
+ pointer-events: none;
185
+ }
186
+
187
+ .ds-site-header__panel > .ds-mega-menu {
188
+ pointer-events: auto;
189
+ }
190
+
191
+ /* Folded: the pills and the wide actions leave the row but stay measurable
192
+ (hidden, out of flow, inert), and the menu button takes their place. */
193
+ .ds-site-header--folded .ds-site-header__nav,
194
+ .ds-site-header--folded .ds-site-header__wide-actions {
195
+ position: absolute;
196
+ visibility: hidden;
197
+ pointer-events: none;
198
+ }
199
+
200
+ .ds-site-header--folded .ds-site-header__menu-button {
201
+ display: inline-flex;
202
+ }
203
+
204
+ .ds-site-header--folded .ds-site-header__panel {
205
+ display: none;
206
+ }
207
+
208
+ /* Before the bar has measured itself (the first frame, which a
209
+ server-rendered page shows before its script runs), it folds by CSS below
210
+ the `initialFold` step, so a phone never sees the pills spill past the
211
+ actions. The first measurement replaces this in the same commit as
212
+ hydration. */
213
+ @media (max-width: 959px) {
214
+ .ds-site-header--prefold-tablet .ds-site-header__nav,
215
+ .ds-site-header--prefold-tablet .ds-site-header__wide-actions {
216
+ position: absolute;
217
+ visibility: hidden;
218
+ pointer-events: none;
219
+ }
220
+
221
+ .ds-site-header--prefold-tablet .ds-site-header__menu-button {
222
+ display: inline-flex;
223
+ }
224
+ }
225
+
226
+ @media (max-width: 1279px) {
227
+ .ds-site-header--prefold-mid .ds-site-header__nav,
228
+ .ds-site-header--prefold-mid .ds-site-header__wide-actions {
229
+ position: absolute;
230
+ visibility: hidden;
231
+ pointer-events: none;
232
+ }
233
+
234
+ .ds-site-header--prefold-mid .ds-site-header__menu-button {
235
+ display: inline-flex;
236
+ }
237
+ }
238
+
239
+ /* The drawer: its brand row lands on the bar's own brand. SiteHeader
240
+ measures the bar as the drawer opens (its top, which is 0 once stuck and
241
+ the host's gutter at rest, and its inline start) and the panel takes
242
+ those as its padding, with the header row the bar's height, so the
243
+ panel slides in under a brand that does not move. */
244
+ .ds-site-header__drawer .ds-drawer__panel {
245
+ width: min(320px, 80vw);
246
+ padding-block-start: var(--ds-site-header-drawer-top, var(--padding-500));
247
+ padding-inline-start: var(--ds-site-header-drawer-start, var(--padding-500));
248
+ /* The floating glass the command palette wears, so the page reaches
249
+ through the menu frosted rather than behind an opaque sheet. The
250
+ panel lets its close button out (below); its body still scrolls. */
251
+ background-color: var(--color-bg-glass);
252
+ backdrop-filter: blur(24px);
253
+ overflow: visible;
254
+ }
255
+
256
+ /* The drawer's close button swaps with the menu button: it sits on the
257
+ scrim exactly where the menu button was (measured as the drawer opens),
258
+ and fades in once the panel has slid home, so the X never travels with
259
+ it. It stays the drawer's own button, inside its focus trap. */
260
+ .ds-site-header__drawer .ds-drawer__close {
261
+ position: absolute;
262
+ inset-block-start: var(--ds-site-header-close-top, var(--padding-500));
263
+ inset-inline-start: var(--ds-site-header-close-start, auto);
264
+ width: var(--control-height-default);
265
+ height: var(--control-height-default);
266
+ /* ds-allow(color): on-scrim furniture, as Lightbox's: the scrim is dark in both themes, so the glyph is white in both. */
267
+ color: #ffffff;
268
+ opacity: 0;
269
+ transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
270
+ }
271
+
272
+ /* The glyph at the menu button's own size, so the swap reads as one icon
273
+ turning into another. */
274
+ .ds-site-header__drawer .ds-drawer__close .material-symbols-rounded {
275
+ --icon-size: var(--icon-size-600);
276
+ }
277
+
278
+ .ds-site-header__drawer .ds-drawer__panel[data-state="open"] .ds-drawer__close {
279
+ opacity: 1;
280
+ transition-delay: var(--motion-duration-slow);
281
+ }
282
+
283
+ .ds-site-header__drawer .ds-drawer__close:hover {
284
+ /* ds-allow(color): the hover wash on the always-dark scrim, as Lightbox's controls. */
285
+ background-color: rgba(255, 255, 255, 0.15);
286
+ }
287
+
288
+ /* The footer row starts under the brand, not at the panel's far edge. */
289
+ .ds-site-header__drawer .ds-drawer__footer {
290
+ justify-content: flex-start;
291
+ }
292
+
293
+ .ds-site-header__drawer .ds-drawer__header {
294
+ align-items: center;
295
+ height: var(--ds-site-header-height, auto);
296
+ }
297
+
298
+ .ds-site-header__drawer-brand {
299
+ display: flex;
300
+ align-items: center;
301
+ }
302
+
303
+ .ds-site-header__drawer .ds-nav-list {
304
+ width: 100%;
305
+ }
@@ -0,0 +1,117 @@
1
+ import { default as React } from 'react';
2
+ import { LinkComponent } from '../../behaviors/LinkProvider';
3
+ import { MegaMenuItem, MegaMenuShowcase } from '../MegaMenu/MegaMenu';
4
+ /** One top-level section of the site: a pill in the bar, a row in the drawer. */
5
+ export interface SiteHeaderSection {
6
+ /** A stable key for the section. */
7
+ id: string;
8
+ /** The pill's text. */
9
+ label: string;
10
+ /** The section's landing page: where a click on the pill goes. */
11
+ href: string;
12
+ /**
13
+ * A mega menu for the section. Hovering or focusing the pill opens it; its
14
+ * items are also the section's sub-list in the drawer.
15
+ */
16
+ mega?: {
17
+ /** The panel's link rows. */
18
+ items: MegaMenuItem[];
19
+ /** A featured card beside the rows. */
20
+ showcase?: MegaMenuShowcase;
21
+ };
22
+ /**
23
+ * The section's sub-list in the drawer, when it should differ from its
24
+ * menu's items: a page that needs a wide screen, say, left out of the
25
+ * drawer that only narrow screens see. Defaults to the menu's items.
26
+ */
27
+ drawerItems?: {
28
+ href: string;
29
+ label: string;
30
+ }[];
31
+ /**
32
+ * Marks the section as the one being viewed. Left unset, the header works
33
+ * it out from `currentHref`: the section's own page, a page under it, or
34
+ * any page its menu lists.
35
+ */
36
+ current?: boolean;
37
+ }
38
+ /** Props owned by SiteHeader itself — everything else falls through to the `<header>`. */
39
+ type SiteHeaderOwnProps = {
40
+ /**
41
+ * The brand at the bar's start, usually a logo linking home. Rendered as
42
+ * given, so it carries its own link.
43
+ */
44
+ brand?: React.ReactNode;
45
+ /** The site's top-level sections, in order. */
46
+ sections: SiteHeaderSection[];
47
+ /** The path of the page being viewed, used to mark the current section, row and drawer item. */
48
+ currentHref?: string;
49
+ /** Controls at the bar's end that stay at every width, such as a search button. */
50
+ actions?: React.ReactNode;
51
+ /**
52
+ * Controls at the bar's end that show only while the pills fit, such as a
53
+ * theme toggle. Once the bar folds to its menu button they leave it; put
54
+ * their drawer twin in `drawerFooter`.
55
+ */
56
+ wideActions?: React.ReactNode;
57
+ /** Content for the drawer's footer, such as the controls `wideActions` holds in the bar. */
58
+ drawerFooter?: React.ReactNode;
59
+ /** Whether the folded bar's drawer is open, for a host that owns that state. Pair with `onDrawerOpenChange`. */
60
+ drawerOpen?: boolean;
61
+ /** Whether the drawer starts open, when the header owns its state. */
62
+ defaultDrawerOpen?: boolean;
63
+ /** Called when the drawer opens or closes, controlled or not. */
64
+ onDrawerOpenChange?: (open: boolean) => void;
65
+ /**
66
+ * Where the bar folds before it has measured itself: on the first frame,
67
+ * which a server-rendered page shows before its script runs. Below this
68
+ * breakpoint (`tablet` or the wider `mid`) the bar starts folded by CSS
69
+ * alone, so a phone never sees the pills overflow; the measurement takes
70
+ * over once the script runs. Pick the step nearest where the pills stop
71
+ * fitting, or `never` for a bar that fits at every width.
72
+ */
73
+ initialFold?: 'tablet' | 'mid' | 'never';
74
+ /**
75
+ * Whether the header sticks to the top of the viewport over a progressive
76
+ * blur. Off, it scrolls away with the page and draws no blur.
77
+ */
78
+ sticky?: boolean;
79
+ /**
80
+ * How far, in pixels, the page scrolls before the page-surface tint fades
81
+ * in over the sticky header's blur.
82
+ */
83
+ tintAfter?: number;
84
+ /**
85
+ * The component every link renders through, so a router's link (Next.js
86
+ * `Link`) navigates without a reload. Falls back to the nearest
87
+ * `LinkProvider`, then `<a>`.
88
+ */
89
+ linkComponent?: LinkComponent;
90
+ /** The accessible name of the section links' `<nav>` landmark. */
91
+ navLabel?: string;
92
+ /** The accessible name of the menu button that opens the drawer. */
93
+ drawerOpenLabel?: string;
94
+ /**
95
+ * The drawer's title, naming the dialog. Shown at its top when there is no
96
+ * `brand`; with one, the brand takes its place and the title is read only
97
+ * by assistive tech.
98
+ */
99
+ drawerTitle?: string;
100
+ /** The accessible name of the drawer's close button. */
101
+ drawerCloseLabel?: string;
102
+ /** Additional CSS classes on the `<header>`. */
103
+ className?: string;
104
+ };
105
+ export interface SiteHeaderProps extends SiteHeaderOwnProps, Omit<React.ComponentPropsWithRef<'header'>, keyof SiteHeaderOwnProps> {
106
+ }
107
+ /**
108
+ * A site's top bar: the brand, a pill per section with a mega menu on any
109
+ * section that has one, and actions at the end, sticking to the viewport
110
+ * over a progressive blur whose tint arrives once the page scrolls. When
111
+ * the pills no longer fit, measured on the bar's own width rather than the
112
+ * viewport's, the bar folds to a menu button that opens a drawer of the
113
+ * same sections. The forwarded ref and any unrecognised props target the
114
+ * `<header>`.
115
+ */
116
+ export declare function SiteHeader({ brand, sections, currentHref, actions, wideActions, drawerFooter, drawerOpen: drawerOpenProp, defaultDrawerOpen, onDrawerOpenChange, initialFold, sticky, tintAfter, linkComponent, navLabel, drawerOpenLabel, drawerTitle, drawerCloseLabel, className, ref, ...rest }: SiteHeaderProps): React.JSX.Element;
117
+ export {};