@robr0/design-system 0.14.0 → 0.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 +11 -8
- package/behaviors/focusable.d.ts +26 -0
- package/behaviors/focusable.js +18 -0
- package/behaviors/useFocusScope.d.ts +38 -0
- package/behaviors/useFocusScope.js +82 -0
- package/behaviors/useLayer.d.ts +43 -0
- package/behaviors/useLayer.js +55 -0
- package/behaviors/useMounted.d.ts +7 -0
- package/behaviors/useMounted.js +14 -0
- package/behaviors/useScrollLock.d.ts +55 -0
- package/behaviors/useScrollLock.js +59 -0
- package/charts.d.ts +1 -0
- package/charts.js +2 -0
- package/components/AgentPlan/AgentPlan.css +12 -3
- package/components/AiButton/AiButton.css +327 -0
- package/components/AiButton/AiButton.d.ts +42 -0
- package/components/AiButton/AiButton.js +140 -16
- package/components/AlertDialog/AlertDialog.js +13 -58
- package/components/AnchorNav/AnchorNav.css +74 -0
- package/components/AnchorNav/AnchorNav.d.ts +7 -0
- package/components/AnchorNav/AnchorNav.js +74 -21
- package/components/Button/Button.css +54 -0
- package/components/Button/Button.d.ts +2 -2
- package/components/Chart/Chart.css +4 -1
- package/components/CircularButton/CircularButton.css +46 -0
- package/components/CircularButton/CircularButton.d.ts +13 -2
- package/components/CircularButton/CircularButton.js +19 -16
- package/components/Combobox/Combobox.js +2 -1
- package/components/CommandPalette/CommandPalette.css +36 -8
- package/components/CommandPalette/CommandPalette.js +9 -54
- package/components/Composer/Composer.css +72 -0
- package/components/Composer/Composer.d.ts +25 -6
- package/components/Composer/Composer.js +8 -0
- package/components/ContributionGraph/ContributionGraph.css +21 -0
- package/components/ContributionGraph/ContributionGraph.js +18 -10
- package/components/Dialog/Dialog.js +9 -62
- package/components/Drawer/Drawer.js +9 -62
- package/components/Dropdown/Dropdown.js +2 -1
- package/components/DropdownMenu/DropdownMenu.js +11 -3
- package/components/Figure/Figure.css +14 -4
- package/components/Figure/Figure.js +11 -20
- package/components/FunnelChart/FunnelChart.css +11 -36
- package/components/FunnelChart/FunnelChart.d.ts +19 -16
- package/components/FunnelChart/FunnelChart.js +67 -39
- package/components/Gauge/Gauge.css +11 -1
- package/components/Meter/Meter.css +10 -1
- package/components/ModelPicker/ModelPicker.js +2 -1
- package/components/Popover/Popover.js +3 -0
- package/components/ProgressBar/ProgressBar.css +1 -1
- package/components/SegmentedControl/SegmentedControl.css +27 -2
- package/components/SegmentedControl/SegmentedControl.d.ts +3 -1
- package/components/SegmentedControl/SegmentedControl.js +3 -1
- package/components/Slider/Slider.css +1 -1
- package/components/Slider/Slider.js +1 -1
- package/components/SourceTrail/SourceTrail.css +209 -0
- package/components/SourceTrail/SourceTrail.d.ts +49 -0
- package/components/SourceTrail/SourceTrail.js +108 -0
- package/components/Sparkline/Sparkline.css +26 -2
- package/components/Sparkline/Sparkline.js +2 -1
- package/components/Spinner/Spinner.css +1 -1
- package/components/SplitButton/SplitButton.d.ts +1 -1
- package/components/SplitButton/SplitButton.js +1 -0
- package/components/TimePicker/TimePicker.js +2 -1
- package/components/ToggleGroup/ToggleGroup.css +17 -0
- package/components/ToggleGroup/ToggleGroup.d.ts +2 -0
- package/components/ToggleGroup/ToggleGroup.js +2 -0
- package/components/ToolCall/ToolCall.css +16 -3
- package/components/Tooltip/Tooltip.css +24 -7
- package/components/Tooltip/Tooltip.d.ts +2 -1
- package/components/Tooltip/Tooltip.js +6 -0
- package/components/UsageCard/UsageCard.css +30 -0
- package/components/UsageCard/UsageCard.d.ts +48 -0
- package/components/UsageCard/UsageCard.js +54 -0
- package/components/registry.d.ts +8 -2
- package/components/registry.json +43 -16
- package/components/registry.json.d.ts +43 -16
- package/components/registry.json.js +1 -1
- package/index.d.ts +2 -1
- package/index.js +4 -2
- package/package.json +5 -1
- package/tokens/motion.d.ts +10 -4
- package/tokens/motion.js +7 -1
- package/tokens/registry.json +10 -0
- package/tokens/registry.json.d.ts +10 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +22 -2
- package/tokens/tokens-light.css +38 -11
- package/tokens/tokens-primitives.css +3 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useRef, useId,
|
|
3
|
+
import { useState, useRef, useId, useCallback, useMemo, useEffect } from "react";
|
|
4
4
|
import ReactDOM from "react-dom";
|
|
5
5
|
import { Kbd } from "../Kbd/Kbd.js";
|
|
6
6
|
import "./CommandPalette.css";
|
|
7
7
|
import "../../fonts/material-symbols.css";
|
|
8
|
-
|
|
9
|
-
};
|
|
10
|
-
|
|
8
|
+
import { useMounted } from "../../behaviors/useMounted.js";
|
|
9
|
+
import { useLayer } from "../../behaviors/useLayer.js";
|
|
10
|
+
import { useFocusScope } from "../../behaviors/useFocusScope.js";
|
|
11
|
+
import { useScrollLock } from "../../behaviors/useScrollLock.js";
|
|
11
12
|
const CommandPalette = ({
|
|
12
13
|
open,
|
|
13
14
|
onOpenChange,
|
|
@@ -28,10 +29,9 @@ const CommandPalette = ({
|
|
|
28
29
|
const inputRef = useRef(null);
|
|
29
30
|
const panelRef = useRef(null);
|
|
30
31
|
const listRef = useRef(null);
|
|
31
|
-
const previousFocusRef = useRef(null);
|
|
32
32
|
const generatedId = useId();
|
|
33
33
|
const baseClass = "ds-command-palette";
|
|
34
|
-
const mounted =
|
|
34
|
+
const mounted = useMounted();
|
|
35
35
|
const matches = useCallback(
|
|
36
36
|
(command) => {
|
|
37
37
|
const q = query.trim().toLowerCase();
|
|
@@ -68,54 +68,9 @@ const CommandPalette = ({
|
|
|
68
68
|
document.addEventListener("keydown", handleHotkey);
|
|
69
69
|
return () => document.removeEventListener("keydown", handleHotkey);
|
|
70
70
|
}, [hotkey, open, onOpenChange]);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const raf = requestAnimationFrame(() => inputRef.current?.focus());
|
|
75
|
-
return () => cancelAnimationFrame(raf);
|
|
76
|
-
}
|
|
77
|
-
if (previousFocusRef.current) {
|
|
78
|
-
previousFocusRef.current.focus();
|
|
79
|
-
previousFocusRef.current = null;
|
|
80
|
-
}
|
|
81
|
-
}, [open]);
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (open) {
|
|
84
|
-
document.body.style.overflow = "hidden";
|
|
85
|
-
} else {
|
|
86
|
-
document.body.style.overflow = "";
|
|
87
|
-
}
|
|
88
|
-
return () => {
|
|
89
|
-
document.body.style.overflow = "";
|
|
90
|
-
};
|
|
91
|
-
}, [open]);
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (!open) return;
|
|
94
|
-
const handleTrapKeyDown = (e) => {
|
|
95
|
-
if (e.key === "Escape") {
|
|
96
|
-
e.preventDefault();
|
|
97
|
-
onOpenChange(false);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (e.key === "Tab") {
|
|
101
|
-
const panel = panelRef.current;
|
|
102
|
-
if (!panel) return;
|
|
103
|
-
const focusable = panel.querySelectorAll(FOCUSABLE);
|
|
104
|
-
if (focusable.length === 0) return;
|
|
105
|
-
const first = focusable[0];
|
|
106
|
-
const last = focusable[focusable.length - 1];
|
|
107
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
last.focus();
|
|
110
|
-
} else if (!e.shiftKey && document.activeElement === last) {
|
|
111
|
-
e.preventDefault();
|
|
112
|
-
first.focus();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
document.addEventListener("keydown", handleTrapKeyDown);
|
|
117
|
-
return () => document.removeEventListener("keydown", handleTrapKeyDown);
|
|
118
|
-
}, [open, onOpenChange]);
|
|
71
|
+
useLayer({ open, onDismiss: () => onOpenChange(false) });
|
|
72
|
+
useFocusScope(panelRef, { active: open, initialFocus: () => inputRef.current });
|
|
73
|
+
useScrollLock(open, { strategy: "cancel-events", allowWithin: listRef });
|
|
119
74
|
const runCommand = (command) => {
|
|
120
75
|
if (command.disabled) return;
|
|
121
76
|
command.onSelect?.();
|
|
@@ -140,6 +140,24 @@
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/* Working: while a response streams, the ring stays lit and keeps turning
|
|
144
|
+
whether or not the field holds focus — the visible half of the `streaming`
|
|
145
|
+
contract for glowing composers. The pairing is defined in design.md's
|
|
146
|
+
Composer section; streaming without aiGlow changes nothing. */
|
|
147
|
+
.ds-composer--ai-glow.ds-composer--streaming {
|
|
148
|
+
border-color: transparent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.ds-composer--ai-glow.ds-composer--streaming::before {
|
|
152
|
+
opacity: 1;
|
|
153
|
+
animation-play-state: running;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.ds-composer--ai-glow.ds-composer--streaming::after {
|
|
157
|
+
opacity: 0.3;
|
|
158
|
+
animation-play-state: running;
|
|
159
|
+
}
|
|
160
|
+
|
|
143
161
|
/* Reduced motion: the ring and glow stay, the turning stops. */
|
|
144
162
|
@media (prefers-reduced-motion: reduce) {
|
|
145
163
|
.ds-composer--ai-glow::before,
|
|
@@ -148,6 +166,60 @@
|
|
|
148
166
|
}
|
|
149
167
|
}
|
|
150
168
|
|
|
169
|
+
/* ============================================
|
|
170
|
+
CONTEXT CHIP
|
|
171
|
+
A full-width, non-interactive note at the very top of the shell — what
|
|
172
|
+
the model is currently looking at. Composer owns the chrome; the caller
|
|
173
|
+
passes the text.
|
|
174
|
+
============================================ */
|
|
175
|
+
|
|
176
|
+
.ds-composer__context {
|
|
177
|
+
box-sizing: border-box;
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
gap: var(--gap-xs);
|
|
181
|
+
/* The action bar's tight --padding-sm ring, open toward the text below */
|
|
182
|
+
margin: var(--padding-sm) var(--padding-sm) 0;
|
|
183
|
+
/* ModelPicker's trigger geometry — the send button's height with the same
|
|
184
|
+
0/padding-md inset — so the chip and the bar's pills read as one family */
|
|
185
|
+
height: var(--ds-composer-send-size);
|
|
186
|
+
padding: 0 var(--padding-md);
|
|
187
|
+
background-color: var(--color-chat-context-bg);
|
|
188
|
+
/* A full pill, matching the suggestion chips around the composer */
|
|
189
|
+
border-radius: var(--radius-full);
|
|
190
|
+
font-family: var(--font-paragraph-sm-family);
|
|
191
|
+
font-size: var(--font-paragraph-sm-size);
|
|
192
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
193
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
194
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
195
|
+
/* Tertiary, like DocumentChip's metadata line — the note is ambient
|
|
196
|
+
context, quieter than anything a person typed. The icon inherits it:
|
|
197
|
+
one tone for the whole note. */
|
|
198
|
+
color: var(--color-text-tertiary);
|
|
199
|
+
/* Not editable, not a control — but a click still falls through the shell
|
|
200
|
+
handler and focuses the textarea */
|
|
201
|
+
cursor: default;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.ds-composer__context-icon {
|
|
205
|
+
display: inline-flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
flex-shrink: 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.ds-composer__context-icon .material-symbols-rounded {
|
|
211
|
+
--icon-size: var(--icon-size-sm);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* The text owns the truncation: one line, ellipsized only when the note
|
|
215
|
+
actually outgrows the shell — an ellipsis is a truncation mark, not
|
|
216
|
+
punctuation. The icon beside it never shrinks. */
|
|
217
|
+
.ds-composer__context-text {
|
|
218
|
+
overflow: hidden;
|
|
219
|
+
text-overflow: ellipsis;
|
|
220
|
+
white-space: nowrap;
|
|
221
|
+
}
|
|
222
|
+
|
|
151
223
|
/* ============================================
|
|
152
224
|
ATTACHMENTS ROW
|
|
153
225
|
============================================ */
|
|
@@ -21,7 +21,8 @@ type ComposerOwnProps = {
|
|
|
21
21
|
onSubmit?: (value: string) => void;
|
|
22
22
|
/**
|
|
23
23
|
* A response is streaming: the send button becomes a stop button, submit
|
|
24
|
-
* is blocked, and Enter is inert.
|
|
24
|
+
* is blocked, and Enter is inert. On a glowing composer (`aiGlow`) the
|
|
25
|
+
* gradient ring also stays lit and keeps turning while this is true.
|
|
25
26
|
*/
|
|
26
27
|
streaming?: boolean;
|
|
27
28
|
/** Fires when the stop button is pressed while `streaming`. */
|
|
@@ -32,9 +33,26 @@ type ComposerOwnProps = {
|
|
|
32
33
|
* While focused, the shell wears AiButton's slowly rotating gradient ring
|
|
33
34
|
* and glow in place of the plain selected border — the system's "a model
|
|
34
35
|
* answers here" signal, for composers whose messages are answered by one.
|
|
35
|
-
*
|
|
36
|
+
* While `streaming`, the ring stays lit and turning whether or not the
|
|
37
|
+
* field holds focus. Off by default.
|
|
36
38
|
*/
|
|
37
39
|
aiGlow?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Contextual note rendered as a full-width, non-interactive chip at the
|
|
42
|
+
* very top of the shell, above any attachments — the "what the model is
|
|
43
|
+
* looking at" line a chat host pins over the message ("Looking at
|
|
44
|
+
* “Page name”"). One line: a note too long for the shell truncates with
|
|
45
|
+
* an ellipsis. Composer owns the chip's chrome; the caller passes the
|
|
46
|
+
* text.
|
|
47
|
+
*/
|
|
48
|
+
context?: React.ReactNode;
|
|
49
|
+
/**
|
|
50
|
+
* Material Symbol name rendered at the left of the context chip
|
|
51
|
+
* (`visibility`, `article`…). Decorative and hidden from assistive
|
|
52
|
+
* technology — the chip's text carries the meaning. None by default,
|
|
53
|
+
* matching the ai set's icon-free-unless-asked convention.
|
|
54
|
+
*/
|
|
55
|
+
contextIcon?: string;
|
|
38
56
|
/**
|
|
39
57
|
* Attachment row rendered above the textarea (DocumentChips). Fully
|
|
40
58
|
* controlled by the caller — Composer never owns the list.
|
|
@@ -57,10 +75,11 @@ type ComposerOwnProps = {
|
|
|
57
75
|
export interface ComposerProps extends ComposerOwnProps, Omit<React.ComponentPropsWithoutRef<'textarea'>, keyof ComposerOwnProps> {
|
|
58
76
|
}
|
|
59
77
|
/**
|
|
60
|
-
* Composer is the chat input shell: an
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
78
|
+
* Composer is the chat input shell: an optional context note ("Looking at
|
|
79
|
+
* “Page name”"), an attachments row, an auto-growing textarea, a leading
|
|
80
|
+
* actions slot, and a trailing send button — the one sanctioned
|
|
81
|
+
* primary-action teal in the chat set, because sending a message is a
|
|
82
|
+
* genuine primary CTA. While `streaming`, send becomes stop and Enter
|
|
64
83
|
* is inert.
|
|
65
84
|
*
|
|
66
85
|
* The textarea grows with its content up to `maxRows`, then scrolls
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
3
|
import React, { useRef, useState, useLayoutEffect } from "react";
|
|
4
4
|
import { CircularButton } from "../CircularButton/CircularButton.js";
|
|
5
|
+
import "../../fonts/material-symbols.css";
|
|
5
6
|
import "./Composer.css";
|
|
6
7
|
const Composer = React.forwardRef(
|
|
7
8
|
({
|
|
@@ -13,6 +14,8 @@ const Composer = React.forwardRef(
|
|
|
13
14
|
onStop,
|
|
14
15
|
maxRows = 8,
|
|
15
16
|
aiGlow = false,
|
|
17
|
+
context,
|
|
18
|
+
contextIcon,
|
|
16
19
|
attachments,
|
|
17
20
|
actions,
|
|
18
21
|
trailingActions,
|
|
@@ -77,6 +80,7 @@ const Composer = React.forwardRef(
|
|
|
77
80
|
const classes = [
|
|
78
81
|
baseClass,
|
|
79
82
|
aiGlow ? `${baseClass}--ai-glow` : "",
|
|
83
|
+
streaming ? `${baseClass}--streaming` : "",
|
|
80
84
|
disabled ? `${baseClass}--disabled` : "",
|
|
81
85
|
className
|
|
82
86
|
].filter(Boolean).join(" ");
|
|
@@ -88,6 +92,10 @@ const Composer = React.forwardRef(
|
|
|
88
92
|
style: { "--ds-composer-max-rows": maxRows },
|
|
89
93
|
onClick: handleShellClick,
|
|
90
94
|
children: [
|
|
95
|
+
context && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__context`, children: [
|
|
96
|
+
contextIcon && /* @__PURE__ */ jsx("span", { className: `${baseClass}__context-icon`, "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: contextIcon }) }),
|
|
97
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__context-text`, children: context })
|
|
98
|
+
] }),
|
|
91
99
|
attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
|
|
92
100
|
/* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, ref: contentRef, children: /* @__PURE__ */ jsx(
|
|
93
101
|
"textarea",
|
|
@@ -79,6 +79,27 @@
|
|
|
79
79
|
width: 100%;
|
|
80
80
|
height: auto;
|
|
81
81
|
aspect-ratio: 1 / 1;
|
|
82
|
+
/* The year sweeps in left to right on mount: each column's delay steps by
|
|
83
|
+
a fraction of the entrance duration (--ds-contribution-week, set inline
|
|
84
|
+
per column). Token-composed, so reduced motion collapses the sweep. */
|
|
85
|
+
animation: ds-contribution-fade var(--motion-duration-slow) var(--motion-ease-standard)
|
|
86
|
+
backwards;
|
|
87
|
+
animation-delay: calc(
|
|
88
|
+
var(--ds-contribution-week, 0) * var(--motion-duration-slower) / 40
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@keyframes ds-contribution-fade {
|
|
93
|
+
from {
|
|
94
|
+
opacity: 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Hover lifts the day under the pointer, so the title tooltip has a visible
|
|
99
|
+
anchor. */
|
|
100
|
+
.ds-contribution-graph__week .ds-contribution-graph__cell:not(.ds-contribution-graph__cell--empty):hover {
|
|
101
|
+
outline: var(--border-xs) solid var(--color-text-tertiary);
|
|
102
|
+
outline-offset: calc(-1 * var(--border-xs));
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
.ds-contribution-graph__cell--empty {
|
|
@@ -112,16 +112,24 @@ const ContributionGraph = ({
|
|
|
112
112
|
style: { gridTemplateColumns: columnTemplate },
|
|
113
113
|
role: "img",
|
|
114
114
|
"aria-label": caption ?? "Contribution activity",
|
|
115
|
-
children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
children: weeks.map((week, weekIndex) => /* @__PURE__ */ jsx(
|
|
116
|
+
"div",
|
|
117
|
+
{
|
|
118
|
+
className: "ds-contribution-graph__week",
|
|
119
|
+
style: { "--ds-contribution-week": weekIndex },
|
|
120
|
+
children: week.map(
|
|
121
|
+
(day, dayIndex) => day ? /* @__PURE__ */ jsx(
|
|
122
|
+
"div",
|
|
123
|
+
{
|
|
124
|
+
className: `ds-contribution-graph__cell ds-contribution-graph__cell--level-${day.level}`,
|
|
125
|
+
title: formatTooltip(day)
|
|
126
|
+
},
|
|
127
|
+
day.date
|
|
128
|
+
) : /* @__PURE__ */ jsx("div", { className: "ds-contribution-graph__cell ds-contribution-graph__cell--empty" }, `pad-${dayIndex}`)
|
|
129
|
+
)
|
|
130
|
+
},
|
|
131
|
+
weekIndex
|
|
132
|
+
))
|
|
125
133
|
}
|
|
126
134
|
)
|
|
127
135
|
] }),
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import React, { useRef, useId
|
|
3
|
+
import React, { useRef, useId } from "react";
|
|
4
4
|
import ReactDOM from "react-dom";
|
|
5
5
|
import "./Dialog.css";
|
|
6
6
|
import "../../fonts/material-symbols.css";
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
|
|
7
|
+
import { useMounted } from "../../behaviors/useMounted.js";
|
|
8
|
+
import { useLayer } from "../../behaviors/useLayer.js";
|
|
9
|
+
import { useFocusScope } from "../../behaviors/useFocusScope.js";
|
|
10
|
+
import { useScrollLock } from "../../behaviors/useScrollLock.js";
|
|
10
11
|
const Dialog = React.forwardRef(
|
|
11
12
|
({
|
|
12
13
|
open,
|
|
@@ -21,7 +22,6 @@ const Dialog = React.forwardRef(
|
|
|
21
22
|
...rest
|
|
22
23
|
}, ref) => {
|
|
23
24
|
const panelRef = useRef(null);
|
|
24
|
-
const previousFocusRef = useRef(null);
|
|
25
25
|
const titleId = useId();
|
|
26
26
|
const descId = useId();
|
|
27
27
|
const baseClass = "ds-dialog";
|
|
@@ -30,63 +30,10 @@ const Dialog = React.forwardRef(
|
|
|
30
30
|
if (typeof ref === "function") ref(node);
|
|
31
31
|
else if (ref) ref.current = node;
|
|
32
32
|
};
|
|
33
|
-
const mounted =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
if (open) {
|
|
40
|
-
previousFocusRef.current = document.activeElement;
|
|
41
|
-
}
|
|
42
|
-
}, [open]);
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (!open) return;
|
|
45
|
-
const panel = panelRef.current;
|
|
46
|
-
if (panel) {
|
|
47
|
-
const first = panel.querySelector(
|
|
48
|
-
"button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])"
|
|
49
|
-
) ?? panel.querySelector(FOCUSABLE);
|
|
50
|
-
(first ?? panel).focus();
|
|
51
|
-
}
|
|
52
|
-
const handleKeyDown = (e) => {
|
|
53
|
-
if (e.key === "Escape" && dismissible) {
|
|
54
|
-
onOpenChange(false);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (e.key === "Tab" && panel) {
|
|
58
|
-
const focusable = panel.querySelectorAll(FOCUSABLE);
|
|
59
|
-
if (focusable.length === 0) return;
|
|
60
|
-
const first = focusable[0];
|
|
61
|
-
const last = focusable[focusable.length - 1];
|
|
62
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
63
|
-
e.preventDefault();
|
|
64
|
-
last.focus();
|
|
65
|
-
} else if (!e.shiftKey && document.activeElement === last) {
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
first.focus();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
72
|
-
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
73
|
-
}, [open, dismissible, onOpenChange]);
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (!open && previousFocusRef.current) {
|
|
76
|
-
previousFocusRef.current.focus();
|
|
77
|
-
previousFocusRef.current = null;
|
|
78
|
-
}
|
|
79
|
-
}, [open]);
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (open) {
|
|
82
|
-
document.body.style.overflow = "hidden";
|
|
83
|
-
} else {
|
|
84
|
-
document.body.style.overflow = "";
|
|
85
|
-
}
|
|
86
|
-
return () => {
|
|
87
|
-
document.body.style.overflow = "";
|
|
88
|
-
};
|
|
89
|
-
}, [open]);
|
|
33
|
+
const mounted = useMounted();
|
|
34
|
+
useLayer({ open, dismissOnEscape: dismissible, onDismiss: () => onOpenChange(false) });
|
|
35
|
+
useFocusScope(panelRef, { active: open });
|
|
36
|
+
useScrollLock(open);
|
|
90
37
|
const handleBackdropClick = () => {
|
|
91
38
|
if (dismissible) onOpenChange(false);
|
|
92
39
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import React, { useRef, useId
|
|
3
|
+
import React, { useRef, useId } from "react";
|
|
4
4
|
import ReactDOM from "react-dom";
|
|
5
5
|
import "./Drawer.css";
|
|
6
6
|
import "../../fonts/material-symbols.css";
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
|
|
7
|
+
import { useMounted } from "../../behaviors/useMounted.js";
|
|
8
|
+
import { useLayer } from "../../behaviors/useLayer.js";
|
|
9
|
+
import { useFocusScope } from "../../behaviors/useFocusScope.js";
|
|
10
|
+
import { useScrollLock } from "../../behaviors/useScrollLock.js";
|
|
10
11
|
const Drawer = React.forwardRef(
|
|
11
12
|
({
|
|
12
13
|
open,
|
|
@@ -22,7 +23,6 @@ const Drawer = React.forwardRef(
|
|
|
22
23
|
...rest
|
|
23
24
|
}, ref) => {
|
|
24
25
|
const panelRef = useRef(null);
|
|
25
|
-
const previousFocusRef = useRef(null);
|
|
26
26
|
const titleId = useId();
|
|
27
27
|
const descId = useId();
|
|
28
28
|
const baseClass = "ds-drawer";
|
|
@@ -31,63 +31,10 @@ const Drawer = React.forwardRef(
|
|
|
31
31
|
if (typeof ref === "function") ref(node);
|
|
32
32
|
else if (ref) ref.current = node;
|
|
33
33
|
};
|
|
34
|
-
const mounted =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (open) {
|
|
41
|
-
previousFocusRef.current = document.activeElement;
|
|
42
|
-
}
|
|
43
|
-
}, [open]);
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (!open) return;
|
|
46
|
-
const panel = panelRef.current;
|
|
47
|
-
if (panel) {
|
|
48
|
-
const first = panel.querySelector(
|
|
49
|
-
"button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])"
|
|
50
|
-
) ?? panel.querySelector(FOCUSABLE);
|
|
51
|
-
(first ?? panel).focus();
|
|
52
|
-
}
|
|
53
|
-
const handleKeyDown = (e) => {
|
|
54
|
-
if (e.key === "Escape" && dismissible) {
|
|
55
|
-
onOpenChange(false);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (e.key === "Tab" && panel) {
|
|
59
|
-
const focusable = panel.querySelectorAll(FOCUSABLE);
|
|
60
|
-
if (focusable.length === 0) return;
|
|
61
|
-
const first = focusable[0];
|
|
62
|
-
const last = focusable[focusable.length - 1];
|
|
63
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
64
|
-
e.preventDefault();
|
|
65
|
-
last.focus();
|
|
66
|
-
} else if (!e.shiftKey && document.activeElement === last) {
|
|
67
|
-
e.preventDefault();
|
|
68
|
-
first.focus();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
document.addEventListener("keydown", handleKeyDown);
|
|
73
|
-
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
74
|
-
}, [open, dismissible, onOpenChange]);
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
if (!open && previousFocusRef.current) {
|
|
77
|
-
previousFocusRef.current.focus();
|
|
78
|
-
previousFocusRef.current = null;
|
|
79
|
-
}
|
|
80
|
-
}, [open]);
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (open) {
|
|
83
|
-
document.body.style.overflow = "hidden";
|
|
84
|
-
} else {
|
|
85
|
-
document.body.style.overflow = "";
|
|
86
|
-
}
|
|
87
|
-
return () => {
|
|
88
|
-
document.body.style.overflow = "";
|
|
89
|
-
};
|
|
90
|
-
}, [open]);
|
|
34
|
+
const mounted = useMounted();
|
|
35
|
+
useLayer({ open, dismissOnEscape: dismissible, onDismiss: () => onOpenChange(false) });
|
|
36
|
+
useFocusScope(panelRef, { active: open });
|
|
37
|
+
useScrollLock(open);
|
|
91
38
|
const handleScrimClick = () => {
|
|
92
39
|
if (dismissible) onOpenChange(false);
|
|
93
40
|
};
|
|
@@ -63,9 +63,10 @@ const Dropdown = React.forwardRef(
|
|
|
63
63
|
}
|
|
64
64
|
}, []);
|
|
65
65
|
useEffect(() => {
|
|
66
|
+
if (!isOpen) return;
|
|
66
67
|
document.addEventListener("mousedown", handleClickOutside);
|
|
67
68
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
68
|
-
}, [handleClickOutside]);
|
|
69
|
+
}, [isOpen, handleClickOutside]);
|
|
69
70
|
const handleKeyDown = (e) => {
|
|
70
71
|
if (disabled) return;
|
|
71
72
|
switch (e.key) {
|
|
@@ -177,6 +177,7 @@ const DropdownMenu = ({
|
|
|
177
177
|
setFocusedIndex(-1);
|
|
178
178
|
}, []);
|
|
179
179
|
useEffect(() => {
|
|
180
|
+
if (!isOpen) return;
|
|
180
181
|
const handler = (e) => {
|
|
181
182
|
if (rootRef.current && !rootRef.current.contains(e.target)) {
|
|
182
183
|
handleClose();
|
|
@@ -184,7 +185,7 @@ const DropdownMenu = ({
|
|
|
184
185
|
};
|
|
185
186
|
document.addEventListener("mousedown", handler);
|
|
186
187
|
return () => document.removeEventListener("mousedown", handler);
|
|
187
|
-
}, [handleClose]);
|
|
188
|
+
}, [isOpen, handleClose]);
|
|
188
189
|
const handleKeyDown = (e) => {
|
|
189
190
|
switch (e.key) {
|
|
190
191
|
case "Enter":
|
|
@@ -323,14 +324,21 @@ const DropdownMenu = ({
|
|
|
323
324
|
size === "compact" ? `${baseClass}__panel--compact` : ""
|
|
324
325
|
].filter(Boolean).join(" ");
|
|
325
326
|
const itemCounter = { count: 0 };
|
|
327
|
+
const triggerProps = React.isValidElement(trigger) ? trigger.props : void 0;
|
|
326
328
|
return /* @__PURE__ */ jsxs("div", { className: classes, ref: rootRef, children: [
|
|
327
329
|
/* @__PURE__ */ jsx("div", { className: `${baseClass}__trigger`, children: React.isValidElement(trigger) ? React.cloneElement(trigger, {
|
|
328
330
|
"aria-haspopup": "menu",
|
|
329
331
|
"aria-expanded": isOpen,
|
|
330
332
|
"aria-controls": isOpen ? menuId : void 0,
|
|
331
333
|
"aria-activedescendant": activeDescendantId,
|
|
332
|
-
onClick:
|
|
333
|
-
|
|
334
|
+
onClick: (e) => {
|
|
335
|
+
triggerProps?.onClick?.(e);
|
|
336
|
+
handleToggle();
|
|
337
|
+
},
|
|
338
|
+
onKeyDown: (e) => {
|
|
339
|
+
triggerProps?.onKeyDown?.(e);
|
|
340
|
+
handleKeyDown(e);
|
|
341
|
+
}
|
|
334
342
|
}) : /* @__PURE__ */ jsx(
|
|
335
343
|
"button",
|
|
336
344
|
{
|
|
@@ -47,15 +47,25 @@
|
|
|
47
47
|
ZOOMABLE (has onClick)
|
|
48
48
|
============================================ */
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
/* The zoom control is a real <button> wrapping the media (ARIA forbids
|
|
51
|
+
role="button" on <figure>), stripped back to a bare block. */
|
|
52
|
+
.ds-figure__zoom {
|
|
53
|
+
display: block;
|
|
54
|
+
width: 100%;
|
|
55
|
+
padding: 0;
|
|
56
|
+
border: 0;
|
|
57
|
+
background: none;
|
|
58
|
+
font: inherit;
|
|
51
59
|
cursor: zoom-in;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
.ds-
|
|
62
|
+
.ds-figure__zoom:hover .ds-figure__media img {
|
|
55
63
|
opacity: 0.88;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
/* Inset ring: the button sits inside the figure's overflow clip, so an
|
|
67
|
+
outset outline would be cropped away. */
|
|
68
|
+
.ds-figure__zoom:focus-visible {
|
|
59
69
|
outline: 2px solid var(--color-action-primary-bg);
|
|
60
|
-
outline-offset: 2px;
|
|
70
|
+
outline-offset: -2px;
|
|
61
71
|
}
|
|
@@ -9,26 +9,17 @@ const Figure = ({
|
|
|
9
9
|
}) => {
|
|
10
10
|
const baseClass = "ds-figure";
|
|
11
11
|
const classes = [baseClass, onClick && `${baseClass}--zoomable`, className].filter(Boolean).join(" ");
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
onKeyDown,
|
|
24
|
-
role: onClick ? "button" : void 0,
|
|
25
|
-
tabIndex: onClick ? 0 : void 0,
|
|
26
|
-
children: [
|
|
27
|
-
/* @__PURE__ */ jsx("div", { className: `${baseClass}__media`, children }),
|
|
28
|
-
caption && /* @__PURE__ */ jsx("figcaption", { className: `${baseClass}__caption`, children: caption })
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
);
|
|
12
|
+
const media = /* @__PURE__ */ jsx("div", { className: `${baseClass}__media`, children });
|
|
13
|
+
return /* @__PURE__ */ jsxs("figure", { className: classes, children: [
|
|
14
|
+
onClick ? (
|
|
15
|
+
// A real button rather than role="button" on the figure: ARIA
|
|
16
|
+
// forbids the button role on <figure>, and the element brings
|
|
17
|
+
// Enter/Space activation itself. Its accessible name is the
|
|
18
|
+
// child image's alt text.
|
|
19
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: `${baseClass}__zoom`, onClick, children: media })
|
|
20
|
+
) : media,
|
|
21
|
+
caption && /* @__PURE__ */ jsx("figcaption", { className: `${baseClass}__caption`, children: caption })
|
|
22
|
+
] });
|
|
32
23
|
};
|
|
33
24
|
export {
|
|
34
25
|
Figure
|