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.
- package/README.md +3 -3
- package/components/Checkbox/Checkbox.css +4 -2
- package/components/Drawer/Drawer.d.ts +6 -0
- package/components/Drawer/Drawer.js +11 -1
- package/components/MegaMenu/MegaMenu.css +271 -0
- package/components/MegaMenu/MegaMenu.d.ts +72 -0
- package/components/MegaMenu/MegaMenu.js +71 -0
- package/components/Nav/Nav.d.ts +7 -2
- package/components/ProgressiveBlur/ProgressiveBlur.css +102 -0
- package/components/ProgressiveBlur/ProgressiveBlur.d.ts +30 -0
- package/components/ProgressiveBlur/ProgressiveBlur.js +27 -0
- package/components/PromptSuggestions/PromptSuggestions.css +1 -1
- package/components/QuestionCard/QuestionCard.css +3 -2
- package/components/QuestionCard/QuestionCard.js +2 -8
- package/components/RadioButton/RadioButton.css +5 -3
- package/components/SiteHeader/SiteHeader.css +305 -0
- package/components/SiteHeader/SiteHeader.d.ts +117 -0
- package/components/SiteHeader/SiteHeader.js +329 -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 +1 -1
- package/tokens/registry.json +1 -0
- package/tokens/registry.json.d.ts +1 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +1 -0
- package/tokens/tokens-light.css +1 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ An open source React design system built for AI products and coding agents: comp
|
|
|
13
13
|
|
|
14
14
|
## Why Rift DS
|
|
15
15
|
|
|
16
|
-
- **<!-- component-count -->
|
|
16
|
+
- **<!-- component-count -->162<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. Every one forwards a ref and your own attributes to its element, state you can own works controlled or uncontrolled (a modal opens when your app says so, by design), and every word a component writes has a prop you can localise. The build checks the first two on every component, and the words wherever they sit in markup. React and React DOM are the only required peer dependencies.
|
|
17
17
|
- **AI patterns, the whole chat and templates, ready to use.** Composer, streaming chat thread, tool calls, reasoning and thread panel are in the box, the complete AI chat ships assembled as one widget that one command scaffolds, and the docs site has complete template screens built from them.
|
|
18
18
|
- **Fully themeable.** The shipped themes are complete looks to start from, each in light and dark. One `data-brand` attribute applies a theme, and a few CSS overrides make your own.
|
|
19
19
|
- **Ready for coding agents.** One command installs the agent skills, and a public MCP endpoint serves every component's props, so your agent builds with the library as it actually is. No key, no account.
|
|
@@ -107,13 +107,13 @@ import 'rift-ds/tokens/presets/presets.css';
|
|
|
107
107
|
|
|
108
108
|
## Components
|
|
109
109
|
|
|
110
|
-
<!-- component-count -->
|
|
110
|
+
<!-- component-count -->162<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
|
|
111
111
|
|
|
112
112
|
<details>
|
|
113
113
|
<summary>The full list</summary>
|
|
114
114
|
|
|
115
115
|
<!-- component-list:start -->
|
|
116
|
-
Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Artifact card · Artifact panel · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bento tile · Bento tile art · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Command palette trigger · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Question card · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Table toolbar · Tabs · Tag input · Textarea · Theme picker · Theme toggle · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
|
|
116
|
+
Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Artifact card · Artifact panel · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bento tile · Bento tile art · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Command palette trigger · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mega menu · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Progressive blur · Prompt suggestions · Prose · Question card · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Site header · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Table toolbar · Tabs · Tag input · Textarea · Theme picker · Theme toggle · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
|
|
117
117
|
<!-- component-list:end -->
|
|
118
118
|
|
|
119
119
|
</details>
|
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
width: 24px;
|
|
29
29
|
height: 24px;
|
|
30
30
|
box-sizing: border-box;
|
|
31
|
-
border: var(--border-050) solid var(--color-input-border
|
|
32
|
-
|
|
31
|
+
border: var(--border-050) solid var(--color-input-indicator-border);
|
|
32
|
+
/* Always a rounded square, whatever the theme's radius: never sharp at a
|
|
33
|
+
zero radius, never rounding towards a radio's circle at a large one. */
|
|
34
|
+
border-radius: clamp(4px, var(--radius-200), 8px); /* ds-allow(spacing): the checkbox's shape is fixed, so its corner is bounded */
|
|
33
35
|
background-color: var(--color-input-bg-primary);
|
|
34
36
|
display: inline-flex;
|
|
35
37
|
align-items: center;
|
|
@@ -12,6 +12,12 @@ type DrawerOwnProps = {
|
|
|
12
12
|
title: string;
|
|
13
13
|
/** Optional subtitle under the title */
|
|
14
14
|
description?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Content shown in place of the title text, such as a site's logo. The
|
|
17
|
+
* `title` still names the dialog, read by assistive tech from a visually
|
|
18
|
+
* hidden heading.
|
|
19
|
+
*/
|
|
20
|
+
heading?: React.ReactNode;
|
|
15
21
|
/** Drawer body content */
|
|
16
22
|
children?: React.ReactNode;
|
|
17
23
|
/** Optional footer slot — typically a row of Buttons */
|
|
@@ -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(
|
|
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
|
+
};
|
package/components/Nav/Nav.d.ts
CHANGED
|
@@ -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
|
|
20
|
-
*
|
|
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 {};
|