rift-ds 1.15.0 → 1.17.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 (52) hide show
  1. package/README.md +3 -3
  2. package/bin/templates/blocks/components/blocks/AppShell.tsx +1 -1
  3. package/chat/ChatLauncher.d.ts +11 -1
  4. package/chat/ChatLauncher.js +7 -1
  5. package/chat/ChatWidget.css +21 -0
  6. package/components/AppLayout/AppLayout.css +90 -2
  7. package/components/AppLayout/AppLayout.d.ts +10 -2
  8. package/components/AppLayout/AppLayout.js +20 -2
  9. package/components/AppSidebar/AppSidebar.css +26 -0
  10. package/components/AppSidebar/AppSidebar.d.ts +15 -4
  11. package/components/AppSidebar/AppSidebar.js +7 -2
  12. package/components/Checkbox/Checkbox.css +4 -2
  13. package/components/DataTable/DataTable.js +29 -9
  14. package/components/DataTable/FilterEditor.js +1 -2
  15. package/components/DataTable/filterModel.js +4 -3
  16. package/components/DataTable/views.d.ts +9 -0
  17. package/components/DataTable/views.js +10 -1
  18. package/components/Drawer/Drawer.d.ts +6 -0
  19. package/components/Drawer/Drawer.js +11 -1
  20. package/components/MegaMenu/MegaMenu.css +271 -0
  21. package/components/MegaMenu/MegaMenu.d.ts +72 -0
  22. package/components/MegaMenu/MegaMenu.js +71 -0
  23. package/components/Nav/Nav.d.ts +7 -2
  24. package/components/ProgressiveBlur/ProgressiveBlur.css +102 -0
  25. package/components/ProgressiveBlur/ProgressiveBlur.d.ts +30 -0
  26. package/components/ProgressiveBlur/ProgressiveBlur.js +27 -0
  27. package/components/PromptSuggestions/PromptSuggestions.css +1 -1
  28. package/components/QuestionCard/QuestionCard.css +3 -2
  29. package/components/QuestionCard/QuestionCard.js +2 -8
  30. package/components/RadioButton/RadioButton.css +5 -3
  31. package/components/SegmentedControl/SegmentedControl.css +69 -0
  32. package/components/SegmentedControl/SegmentedControl.d.ts +4 -3
  33. package/components/SegmentedControl/SegmentedControl.js +90 -48
  34. package/components/SiteHeader/SiteHeader.css +305 -0
  35. package/components/SiteHeader/SiteHeader.d.ts +117 -0
  36. package/components/SiteHeader/SiteHeader.js +329 -0
  37. package/components/TableToolbar/TableToolbar.css +6 -0
  38. package/components/TableToolbar/TableToolbar.js +52 -16
  39. package/components/ThemeToggle/ThemeToggle.css +2 -72
  40. package/components/ThemeToggle/ThemeToggle.d.ts +1 -1
  41. package/components/ThemeToggle/ThemeToggle.js +4 -40
  42. package/components/registry.json +26 -2
  43. package/components/registry.json.d.ts +26 -2
  44. package/components/registry.json.js +1 -1
  45. package/index.d.ts +3 -0
  46. package/index.js +6 -0
  47. package/package.json +1 -1
  48. package/tokens/registry.json +3 -0
  49. package/tokens/registry.json.d.ts +3 -0
  50. package/tokens/registry.json.js +1 -1
  51. package/tokens/tokens-dark.css +8 -0
  52. package/tokens/tokens-light.css +8 -0
@@ -5,6 +5,7 @@ import { CircularButton } from "../CircularButton/CircularButton.js";
5
5
  import ReactDOM from "react-dom";
6
6
  import "./Drawer.css";
7
7
  import "../../behaviors/direction.css";
8
+ import "../../behaviors/visually-hidden.css";
8
9
  import "../../fonts/material-symbols.css";
9
10
  import { useMounted } from "../../behaviors/useMounted.js";
10
11
  import { useLayer } from "../../behaviors/useLayer.js";
@@ -16,6 +17,7 @@ const Drawer = React.forwardRef(
16
17
  onOpenChange,
17
18
  title,
18
19
  description,
20
+ heading,
19
21
  children,
20
22
  footer,
21
23
  side = "right",
@@ -65,7 +67,15 @@ const Drawer = React.forwardRef(
65
67
  children: [
66
68
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
67
69
  /* @__PURE__ */ jsxs("div", { className: `${baseClass}__heading`, children: [
68
- /* @__PURE__ */ jsx("h2", { className: `${baseClass}__title`, id: titleId, children: title }),
70
+ /* @__PURE__ */ jsx(
71
+ "h2",
72
+ {
73
+ className: heading ? "ds-visually-hidden" : `${baseClass}__title`,
74
+ id: titleId,
75
+ children: title
76
+ }
77
+ ),
78
+ heading,
69
79
  description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, id: descId, children: description })
70
80
  ] }),
71
81
  dismissible && /* @__PURE__ */ jsx(
@@ -0,0 +1,271 @@
1
+ /* ============================================
2
+ MEGA MENU
3
+ A wide navigation panel: icon rows plus an
4
+ optional featured card
5
+ ============================================ */
6
+
7
+ /* Base: the floating glass shell, CommandPalette's surface. The panel is a
8
+ shallow strip that lands on whatever the page opens with, display type
9
+ most of it, so its blur runs harder than the shell's 24px: at that
10
+ strength a heading that size still reads through the row labels. */
11
+ .ds-mega-menu {
12
+ box-sizing: border-box;
13
+ padding: var(--padding-1000);
14
+ background-color: var(--color-bg-glass);
15
+ backdrop-filter: blur(32px);
16
+ border: var(--border-025) solid var(--color-bg-container-border);
17
+ border-radius: var(--radius-400);
18
+ box-shadow: var(--shadow-floating);
19
+ /* The showcase card's height floor is sized against this box. */
20
+ container: ds-mega-menu / inline-size;
21
+
22
+ /* Closed: faded out, then hidden once the fade has run, so a reopen
23
+ mid-fade picks up where it was. */
24
+ opacity: 0;
25
+ visibility: hidden;
26
+ transition:
27
+ opacity var(--motion-duration-base) var(--motion-ease-standard),
28
+ visibility 0s linear var(--motion-duration-base);
29
+ }
30
+
31
+ /* States */
32
+ .ds-mega-menu--open {
33
+ opacity: 1;
34
+ visibility: visible;
35
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
36
+ }
37
+
38
+ /* Layout: the link grid alone, or the grid in two thirds beside the
39
+ showcase in one. */
40
+ .ds-mega-menu__layout {
41
+ display: grid;
42
+ grid-template-columns: minmax(0, 1fr);
43
+ gap: var(--gap-1000);
44
+ }
45
+
46
+ .ds-mega-menu__layout--showcase {
47
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
48
+ }
49
+
50
+ /* The link grid: four rows to a column, wrapping into further columns, so
51
+ a long menu grows sideways and never into a scroll. */
52
+ .ds-mega-menu__grid {
53
+ display: grid;
54
+ grid-template-rows: repeat(4, auto);
55
+ grid-auto-flow: column;
56
+ grid-auto-columns: minmax(0, 1fr);
57
+ gap: var(--gap-200) var(--gap-1000);
58
+ align-content: start;
59
+ }
60
+
61
+ /* Rows: the overlay row family. No fill at rest, the passive wash on hover
62
+ and focus at --radius-200, and the navigation current item one step past
63
+ it, so current and hovered never read the same. */
64
+ .ds-mega-menu__item {
65
+ display: flex;
66
+ align-items: flex-start;
67
+ gap: var(--gap-300);
68
+ min-width: 0;
69
+ padding: var(--padding-300) var(--padding-400);
70
+ border-radius: var(--radius-200);
71
+ color: inherit;
72
+ text-decoration: none;
73
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
74
+ }
75
+
76
+ .ds-mega-menu__item:hover {
77
+ background-color: var(--color-action-passive-bg-hover);
78
+ }
79
+
80
+ .ds-mega-menu__item:focus-visible {
81
+ outline: 2px solid var(--color-action-primary-bg);
82
+ outline-offset: 2px;
83
+ background-color: var(--color-action-passive-bg-hover);
84
+ }
85
+
86
+ .ds-mega-menu__item--current {
87
+ background-color: var(--color-action-passive-bg-active);
88
+ }
89
+
90
+ /* The icon tile: a step of container fill around the glyph, holding that
91
+ step over the row's hover wash. Its corner is the row's own radius, since
92
+ the tile sits at the row's start with no padding between them on that
93
+ axis that would ask for a smaller one. */
94
+ .ds-mega-menu__icon {
95
+ flex-shrink: 0;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ width: calc(var(--icon-size-600) + var(--padding-200) * 2);
100
+ height: calc(var(--icon-size-600) + var(--padding-200) * 2);
101
+ border-radius: var(--radius-200);
102
+ background-color: var(--color-bg-container-secondary);
103
+ color: var(--color-text-primary);
104
+ }
105
+
106
+ /* The tile keeps a step of contrast when the row behind it fills in. The
107
+ hover wash already differs from the tile's resting fill; the current
108
+ row's wash is that same fill, so there the tile takes the other step. */
109
+ .ds-mega-menu__item--current .ds-mega-menu__icon {
110
+ background-color: var(--color-bg-container-tertiary);
111
+ }
112
+
113
+ .ds-mega-menu__icon .material-symbols-rounded {
114
+ --icon-size: var(--icon-size-600);
115
+ line-height: 1;
116
+ }
117
+
118
+ .ds-mega-menu__logo {
119
+ display: block;
120
+ width: var(--icon-size-600);
121
+ height: var(--icon-size-600);
122
+ object-fit: contain;
123
+ }
124
+
125
+ .ds-mega-menu__text {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: var(--gap-100);
129
+ min-width: 0;
130
+ }
131
+
132
+ .ds-mega-menu__label {
133
+ font-family: var(--font-title-body-family);
134
+ font-size: var(--font-title-body-size);
135
+ font-weight: var(--font-title-body-weight);
136
+ line-height: var(--font-title-body-line-height);
137
+ letter-spacing: var(--font-title-body-letter-spacing);
138
+ color: var(--color-text-primary);
139
+ }
140
+
141
+ /* Rows stay even: a description is at most two lines. The full sentence
142
+ lives on the page the row links to. */
143
+ .ds-mega-menu__description {
144
+ display: -webkit-box;
145
+ -webkit-line-clamp: 2;
146
+ -webkit-box-orient: vertical;
147
+ overflow: hidden;
148
+ font-family: var(--font-paragraph-sm-family);
149
+ font-size: var(--font-paragraph-sm-size);
150
+ font-weight: var(--font-paragraph-sm-weight);
151
+ line-height: var(--font-paragraph-sm-line-height);
152
+ color: var(--color-text-tertiary);
153
+ }
154
+
155
+ /* Showcase: the column carries the divider, the card fills it. */
156
+ .ds-mega-menu__showcase-column {
157
+ display: flex;
158
+ padding-inline-start: var(--padding-1000);
159
+ border-inline-start: var(--border-025) solid var(--color-bg-container-border);
160
+ }
161
+
162
+ /* One link that is the whole card. The cover fills it and absorbs whatever
163
+ height the link grid runs to, so the columns always bottom-align; the
164
+ floor keeps a short grid's card at about 4:3 so the drawing has room
165
+ above the copy. */
166
+ .ds-mega-menu__showcase {
167
+ position: relative;
168
+ flex: 1;
169
+ display: flex;
170
+ flex-direction: column;
171
+ justify-content: flex-end;
172
+ min-width: 0;
173
+ min-height: 25cqw;
174
+ overflow: hidden;
175
+ border: var(--border-025) solid var(--color-bg-container-border);
176
+ border-radius: var(--radius-300);
177
+ color: inherit;
178
+ text-decoration: none;
179
+ transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
180
+ }
181
+
182
+ .ds-mega-menu__showcase:hover {
183
+ border-color: var(--color-bg-container-tertiary);
184
+ }
185
+
186
+ .ds-mega-menu__showcase:focus-visible {
187
+ outline: 2px solid var(--color-action-primary-bg);
188
+ outline-offset: 4px;
189
+ }
190
+
191
+ /* The cover's gentle zoom on hover is the card acknowledging the pointer,
192
+ clipped by the frame. */
193
+ .ds-mega-menu__cover {
194
+ position: absolute;
195
+ inset: 0;
196
+ transition: scale var(--motion-duration-fast) var(--motion-ease-standard);
197
+ }
198
+
199
+ .ds-mega-menu__cover > * {
200
+ width: 100%;
201
+ height: 100%;
202
+ }
203
+
204
+ .ds-mega-menu__showcase:hover .ds-mega-menu__cover {
205
+ scale: 1.02;
206
+ }
207
+
208
+ /* The copy, kept legible on the drawing by a rise of page surface from the
209
+ card's foot. The tall top padding is the fade zone; the text never
210
+ reaches it. */
211
+ .ds-mega-menu__showcase-text {
212
+ position: relative;
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: var(--gap-100);
216
+ padding: var(--padding-1500) var(--padding-500) var(--padding-500);
217
+ /* ds-allow(color): the transparent end of the fade, not a palette colour */
218
+ background: linear-gradient(to top, var(--color-bg-page-primary) 55%, transparent);
219
+ }
220
+
221
+ .ds-mega-menu__overline {
222
+ font-family: var(--font-overline-family);
223
+ font-size: var(--font-overline-size);
224
+ font-weight: var(--font-overline-weight);
225
+ line-height: var(--font-overline-line-height);
226
+ letter-spacing: var(--font-overline-letter-spacing);
227
+ text-transform: uppercase;
228
+ color: var(--color-text-tertiary);
229
+ }
230
+
231
+ .ds-mega-menu__showcase-title {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: var(--gap-100);
235
+ }
236
+
237
+ .ds-mega-menu__arrow {
238
+ --icon-size: var(--icon-size-500);
239
+ line-height: 1;
240
+ color: var(--color-action-passive-text);
241
+ transition: translate var(--motion-duration-fast) var(--motion-ease-standard);
242
+ }
243
+
244
+ .ds-mega-menu__showcase:hover .ds-mega-menu__arrow {
245
+ translate: calc(var(--gap-050) * var(--ds-inline-sign, 1)) 0;
246
+ }
247
+
248
+ /* Narrow: a panel without the room for two column groups stacks the
249
+ showcase under the grid and lets the grid run one column deep.
250
+ ds-allow(breakpoint): content-driven: two row columns and the showcase need about 720px of panel. */
251
+ @container ds-mega-menu (max-width: 720px) {
252
+ .ds-mega-menu__layout--showcase {
253
+ grid-template-columns: minmax(0, 1fr);
254
+ }
255
+
256
+ .ds-mega-menu__grid {
257
+ grid-template-rows: none;
258
+ grid-auto-flow: row;
259
+ }
260
+
261
+ .ds-mega-menu__showcase-column {
262
+ padding-inline-start: 0;
263
+ border-inline-start: none;
264
+ }
265
+
266
+ /* Full width now, so the floor that kept a third-width card at 4:3
267
+ becomes a wide band with the same room above the copy. */
268
+ .ds-mega-menu__showcase {
269
+ min-height: 50cqw;
270
+ }
271
+ }
@@ -0,0 +1,72 @@
1
+ import { default as React } from 'react';
2
+ import { LinkComponent } from '../../behaviors/LinkProvider';
3
+ /** One link row in a mega menu: a page and the line that says what is on it. */
4
+ export interface MegaMenuItem {
5
+ /** Where the row links. */
6
+ href: string;
7
+ /** The page's name. */
8
+ label: string;
9
+ /** One line on what the page holds, clamped to two lines in the panel. */
10
+ description?: string;
11
+ /** A Material Symbols name drawn in the row's icon tile. */
12
+ icon?: string;
13
+ /** An image path drawn in the icon tile instead of `icon`, for a brand or product mark. */
14
+ logo?: string;
15
+ }
16
+ /** The featured card beside a mega menu's link grid: one link that is the whole card. */
17
+ export interface MegaMenuShowcase {
18
+ /** Where the card links. */
19
+ href: string;
20
+ /** A short kicker above the title, set in the overline face. */
21
+ overline?: string;
22
+ /** The card's title, followed by a forward arrow. */
23
+ label: string;
24
+ /** One or two lines under the title. */
25
+ description?: string;
26
+ /**
27
+ * A drawing that fills the card behind the copy. Decorative: the title and
28
+ * description carry the meaning, so mark it `aria-hidden`.
29
+ */
30
+ cover?: React.ReactNode;
31
+ }
32
+ /** Props owned by MegaMenu itself — everything else falls through to the panel `<div>`. */
33
+ type MegaMenuOwnProps = {
34
+ /**
35
+ * Whether the panel shows. Controlled by design: the host owns the
36
+ * triggers that open it, so it owns this state too. A closed panel fades
37
+ * out and is `inert`, so its links leave the tab order.
38
+ */
39
+ open: boolean;
40
+ /** The link rows, laid out four to a column and wrapping into further columns. */
41
+ items: MegaMenuItem[];
42
+ /** A featured card in its own column beside the link grid. */
43
+ showcase?: MegaMenuShowcase;
44
+ /**
45
+ * The path of the page being viewed. A row whose `href` is this path, or a
46
+ * parent of it, is marked as the current page.
47
+ */
48
+ currentHref?: string;
49
+ /** Called when any link in the panel is followed, so a host can close it. */
50
+ onNavigate?: () => void;
51
+ /**
52
+ * The component every link renders through, so a router's link (Next.js
53
+ * `Link`) navigates without a reload. The panel is server-renderable, so it
54
+ * cannot read a `LinkProvider`; a client host resolves one and passes it
55
+ * here. Defaults to `<a>`.
56
+ */
57
+ linkComponent?: LinkComponent;
58
+ /** Additional CSS classes on the panel. */
59
+ className?: string;
60
+ };
61
+ export interface MegaMenuProps extends MegaMenuOwnProps, Omit<React.ComponentPropsWithRef<'div'>, keyof MegaMenuOwnProps> {
62
+ }
63
+ /**
64
+ * A wide navigation panel: a grid of icon rows, each a page with a line of
65
+ * description, and an optional featured card in a column of its own. It is
66
+ * the panel only. The host places it (under a header bar, usually) and owns
67
+ * the triggers, the hover delay and the dismissal; SiteHeader is the
68
+ * reference host. The forwarded ref and any unrecognised props target the
69
+ * panel `<div>`.
70
+ */
71
+ export declare function MegaMenu({ open, items, showcase, currentHref, onNavigate, linkComponent, className, ref, ...rest }: MegaMenuProps): React.JSX.Element;
72
+ export {};
@@ -0,0 +1,71 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import "react";
3
+ import { mirrorClass } from "../../behaviors/direction.js";
4
+ import "./MegaMenu.css";
5
+ import "../../fonts/material-symbols.css";
6
+ import "../../behaviors/direction.css";
7
+ const isCurrent = (href, currentHref) => currentHref != null && (currentHref === href || currentHref.startsWith(`${href}/`));
8
+ function MegaMenu({
9
+ open,
10
+ items,
11
+ showcase,
12
+ currentHref,
13
+ onNavigate,
14
+ linkComponent,
15
+ className = "",
16
+ ref,
17
+ ...rest
18
+ }) {
19
+ const Link = linkComponent ?? "a";
20
+ const baseClass = "ds-mega-menu";
21
+ const classes = [baseClass, open && `${baseClass}--open`, className].filter(Boolean).join(" ");
22
+ return /* @__PURE__ */ jsx("div", { ...rest, ref, className: classes, inert: !open, children: /* @__PURE__ */ jsxs(
23
+ "div",
24
+ {
25
+ className: [`${baseClass}__layout`, showcase && `${baseClass}__layout--showcase`].filter(Boolean).join(" "),
26
+ children: [
27
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__grid`, children: items.map((item) => {
28
+ const current = isCurrent(item.href, currentHref);
29
+ return /* @__PURE__ */ jsxs(
30
+ Link,
31
+ {
32
+ href: item.href,
33
+ className: [`${baseClass}__item`, current && `${baseClass}__item--current`].filter(Boolean).join(" "),
34
+ "aria-current": current ? "page" : void 0,
35
+ onClick: onNavigate,
36
+ children: [
37
+ (item.logo || item.icon) && /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: item.logo ? /* @__PURE__ */ jsx("img", { className: `${baseClass}__logo`, src: item.logo, alt: "" }) : /* @__PURE__ */ jsx("span", { className: `material-symbols-rounded ${mirrorClass(item.icon)}`, children: item.icon }) }),
38
+ /* @__PURE__ */ jsxs("span", { className: `${baseClass}__text`, children: [
39
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: item.label }),
40
+ item.description && /* @__PURE__ */ jsx("span", { className: `${baseClass}__description`, children: item.description })
41
+ ] })
42
+ ]
43
+ },
44
+ item.href
45
+ );
46
+ }) }),
47
+ showcase && /* @__PURE__ */ jsx("div", { className: `${baseClass}__showcase-column`, children: /* @__PURE__ */ jsxs(Link, { href: showcase.href, className: `${baseClass}__showcase`, onClick: onNavigate, children: [
48
+ showcase.cover && /* @__PURE__ */ jsx("span", { className: `${baseClass}__cover`, children: showcase.cover }),
49
+ /* @__PURE__ */ jsxs("span", { className: `${baseClass}__showcase-text`, children: [
50
+ showcase.overline && /* @__PURE__ */ jsx("span", { className: `${baseClass}__overline`, children: showcase.overline }),
51
+ /* @__PURE__ */ jsxs("span", { className: `${baseClass}__label ${baseClass}__showcase-title`, children: [
52
+ showcase.label,
53
+ /* @__PURE__ */ jsx(
54
+ "span",
55
+ {
56
+ className: `material-symbols-rounded ds-mirror-rtl ${baseClass}__arrow`,
57
+ "aria-hidden": "true",
58
+ children: "arrow_forward"
59
+ }
60
+ )
61
+ ] }),
62
+ showcase.description && /* @__PURE__ */ jsx("span", { className: `${baseClass}__description`, children: showcase.description })
63
+ ] })
64
+ ] }) })
65
+ ]
66
+ }
67
+ ) });
68
+ }
69
+ export {
70
+ MegaMenu
71
+ };
@@ -16,12 +16,17 @@ type NavOwnProps = {
16
16
  export interface NavProps extends NavOwnProps, Omit<React.ComponentPropsWithoutRef<'nav'>, keyof NavOwnProps> {
17
17
  }
18
18
  /**
19
- * Nav component from Figma design system
20
- * Renders a horizontal navigation bar using ButtonGroup.
19
+ * Nav is the earlier plain top bar: a brand slot and a horizontal
20
+ * ButtonGroup of page links, with no menus and no mobile behaviour of its
21
+ * own. It keeps working for existing consumers, and new work uses
22
+ * SiteHeader, which adds section mega menus, the sticky progressive blur and
23
+ * the fold to a drawer.
21
24
  *
22
25
  * The landmark is labelled "Main" by default so assistive tech can tell it
23
26
  * apart from any other navigation on the page; pass `aria-label` to name it
24
27
  * differently. The forwarded ref and any unrecognised props target the `<nav>`.
28
+ *
29
+ * @deprecated Use `SiteHeader` instead.
25
30
  */
26
31
  export declare const Nav: React.ForwardRefExoticComponent<NavProps & React.RefAttributes<HTMLElement>>;
27
32
  export {};
@@ -0,0 +1,102 @@
1
+ /* ============================================
2
+ PROGRESSIVE BLUR
3
+ A backdrop blur that dissolves along one edge
4
+ ============================================ */
5
+
6
+ /* Why a stack, and why masks: a single backdrop-filter is uniform across its
7
+ whole box, so a tint that fades to transparent still ends in a hard line
8
+ where the blur stops. A mask-image multiplies both the layer's paint and
9
+ its filtered backdrop by its alpha, so the blur itself dissolves. Four
10
+ layers with rising blur strength and a shrinking mask band give a
11
+ progressive blur: frosted at the anchored edge, clear at the far one.
12
+
13
+ The blur sits on child layers, never on the root: a backdrop-filter on an
14
+ ancestor would make it a backdrop root, and any glass inside the host
15
+ (a menu panel, say) would then sample only that subtree instead of the
16
+ page. So the host keeps its own children above this element, and the
17
+ blur reads the page behind both.
18
+
19
+ No hand-written -webkit- twins for backdrop-filter: the build's CSS
20
+ pipeline adds the prefix, and a manual one beside it has made the
21
+ pipeline drop the property entirely. */
22
+
23
+ /* Base */
24
+ .ds-progressive-blur {
25
+ /* The tint's colour. An override hook: a host over a different surface
26
+ points it at that surface. */
27
+ --ds-progressive-blur-tint: var(--color-bg-page-primary);
28
+ position: absolute;
29
+ inset: 0;
30
+ /* A child's backdrop-filter is not reliably clipped by a rounded
31
+ overflow: hidden parent, so the frost would show square at the host's
32
+ corners. The root and every layer take the host's corner instead. */
33
+ border-radius: inherit;
34
+ pointer-events: none;
35
+ }
36
+
37
+ .ds-progressive-blur--top {
38
+ --ds-progressive-blur-toward: to bottom;
39
+ }
40
+
41
+ .ds-progressive-blur--bottom {
42
+ --ds-progressive-blur-toward: to top;
43
+ }
44
+
45
+ .ds-progressive-blur__layer {
46
+ position: absolute;
47
+ inset: 0;
48
+ display: block;
49
+ border-radius: inherit;
50
+ }
51
+
52
+ /* Layers: blur strength rises as the masked band shrinks, so the edge is
53
+ fully frosted and the far side is sharp. The radii are the effect's own
54
+ tuning, not a scale: a blur radius is not spacing. */
55
+
56
+ /* ds-allow(color): mask alpha keywords (black/transparent), not palette colours */
57
+
58
+ .ds-progressive-blur__layer:nth-child(1) {
59
+ backdrop-filter: blur(2px);
60
+ mask-image: linear-gradient(var(--ds-progressive-blur-toward), black 0%, black 55%, transparent 100%);
61
+ }
62
+
63
+ .ds-progressive-blur__layer:nth-child(2) {
64
+ backdrop-filter: blur(6px);
65
+ mask-image: linear-gradient(var(--ds-progressive-blur-toward), black 0%, black 40%, transparent 80%);
66
+ }
67
+
68
+ .ds-progressive-blur__layer:nth-child(3) {
69
+ backdrop-filter: blur(12px);
70
+ mask-image: linear-gradient(var(--ds-progressive-blur-toward), black 0%, black 25%, transparent 60%);
71
+ }
72
+
73
+ .ds-progressive-blur__layer:nth-child(4) {
74
+ backdrop-filter: blur(24px);
75
+ mask-image: linear-gradient(var(--ds-progressive-blur-toward), black 0%, black 10%, transparent 40%);
76
+ }
77
+
78
+ /* Tint: the page surface at the edge stepping to transparent, on its own
79
+ layer above the blur. Only this layer ever fades: transitioning opacity
80
+ on a backdrop-filter makes the browser re-rasterise the blur, which then
81
+ snaps in at the end of the fade, while a plain gradient fades smoothly. */
82
+ .ds-progressive-blur::after {
83
+ content: "";
84
+ position: absolute;
85
+ inset: 0;
86
+ border-radius: inherit;
87
+ background: linear-gradient(
88
+ var(--ds-progressive-blur-toward),
89
+ var(--ds-progressive-blur-tint) 0%,
90
+ color-mix(in srgb, var(--ds-progressive-blur-tint) 90%, transparent) 30%,
91
+ color-mix(in srgb, var(--ds-progressive-blur-tint) 40%, transparent) 65%,
92
+ transparent 100%
93
+ );
94
+ mask-image: linear-gradient(var(--ds-progressive-blur-toward), black 0%, black 55%, transparent 100%);
95
+ opacity: 0;
96
+ transition: opacity var(--motion-duration-slow) var(--motion-ease-standard);
97
+ }
98
+
99
+ /* States */
100
+ .ds-progressive-blur--tinted::after {
101
+ opacity: 1;
102
+ }
@@ -0,0 +1,30 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by ProgressiveBlur itself — everything else falls through to the root `<div>`. */
3
+ type ProgressiveBlurOwnProps = {
4
+ /**
5
+ * The side the blur is anchored to, as Drawer names its edge: fully
6
+ * frosted there, dissolving to clear toward the opposite side. `top` sits
7
+ * behind a header, `bottom` behind a toolbar or a footer bar.
8
+ */
9
+ side?: 'top' | 'bottom';
10
+ /**
11
+ * Whether the page-surface tint shows over the blur. It fades on change,
12
+ * so a header can bring it in once the page has scrolled; the blur
13
+ * itself never fades.
14
+ */
15
+ tinted?: boolean;
16
+ /** Additional CSS classes on the root. */
17
+ className?: string;
18
+ };
19
+ export interface ProgressiveBlurProps extends ProgressiveBlurOwnProps, Omit<React.ComponentPropsWithRef<'div'>, keyof ProgressiveBlurOwnProps> {
20
+ }
21
+ /**
22
+ * A backdrop blur that dissolves along one edge instead of ending in a hard
23
+ * line: four masked blur layers of rising strength, with an optional tint of
24
+ * the page surface on its own layer above them. It fills its positioned
25
+ * parent and is decorative, so it is hidden from assistive tech and lets the
26
+ * pointer through. The forwarded ref and any unrecognised props target the
27
+ * root `<div>`.
28
+ */
29
+ export declare function ProgressiveBlur({ side, tinted, className, ref, ...rest }: ProgressiveBlurProps): React.JSX.Element;
30
+ export {};
@@ -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;