@robr0/design-system 0.15.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 +3 -3
- 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/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/Button/Button.css +3 -0
- package/components/Combobox/Combobox.js +2 -1
- package/components/CommandPalette/CommandPalette.css +10 -7
- package/components/CommandPalette/CommandPalette.js +9 -58
- package/components/Composer/Composer.css +54 -0
- package/components/Composer/Composer.d.ts +21 -4
- package/components/Composer/Composer.js +7 -0
- 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/ModelPicker/ModelPicker.js +2 -1
- package/components/Popover/Popover.js +3 -0
- package/components/SegmentedControl/SegmentedControl.css +4 -2
- package/components/SourceTrail/SourceTrail.css +2 -2
- package/components/TimePicker/TimePicker.js +2 -1
- package/components/ToolCall/ToolCall.css +9 -1
- package/components/Tooltip/Tooltip.js +6 -0
- package/components/registry.json +2 -2
- package/components/registry.json.d.ts +2 -2
- package/components/registry.json.js +1 -1
- package/package.json +5 -1
- package/tokens/motion.d.ts +10 -4
- package/tokens/motion.js +7 -1
- package/tokens/registry.json +6 -0
- package/tokens/registry.json.d.ts +6 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +11 -0
- package/tokens/tokens-light.css +13 -0
|
@@ -125,9 +125,10 @@ const Combobox = React.forwardRef(({
|
|
|
125
125
|
}
|
|
126
126
|
}, []);
|
|
127
127
|
useEffect(() => {
|
|
128
|
+
if (!isOpen) return;
|
|
128
129
|
document.addEventListener("mousedown", handleClickOutside);
|
|
129
130
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
130
|
-
}, [handleClickOutside]);
|
|
131
|
+
}, [isOpen, handleClickOutside]);
|
|
131
132
|
const moveFocus = (direction) => {
|
|
132
133
|
setFocusedIndex((prev) => {
|
|
133
134
|
let next = prev + direction;
|
|
@@ -54,9 +54,12 @@
|
|
|
54
54
|
max-height: 60vh;
|
|
55
55
|
margin-top: 10vh;
|
|
56
56
|
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
57
|
-
border-radius: var(--radius-
|
|
58
|
-
background-color: var(--color-bg-
|
|
59
|
-
box-shadow: var(--shadow-
|
|
57
|
+
border-radius: var(--radius-xl); /* 24px — the floating-surface shell */
|
|
58
|
+
background-color: var(--color-bg-glass);
|
|
59
|
+
box-shadow: var(--shadow-floating);
|
|
60
|
+
/* The glass token is deliberately translucent; this blur is what keeps
|
|
61
|
+
the page readable through the panel. */
|
|
62
|
+
backdrop-filter: blur(24px);
|
|
60
63
|
overflow: hidden;
|
|
61
64
|
transform: scale(0.98);
|
|
62
65
|
transition: transform var(--motion-duration-fast) var(--motion-ease-standard);
|
|
@@ -74,7 +77,7 @@
|
|
|
74
77
|
display: flex;
|
|
75
78
|
align-items: center;
|
|
76
79
|
gap: var(--gap-sm); /* 8px */
|
|
77
|
-
padding: var(--padding-
|
|
80
|
+
padding: var(--padding-md) var(--padding-lg); /* 16px 20px */
|
|
78
81
|
border-bottom: var(--border-xs) solid var(--color-divider);
|
|
79
82
|
flex-shrink: 0;
|
|
80
83
|
transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
@@ -148,7 +151,7 @@
|
|
|
148
151
|
flex: 1;
|
|
149
152
|
min-height: 0;
|
|
150
153
|
overflow-y: auto;
|
|
151
|
-
padding: var(--padding-
|
|
154
|
+
padding: var(--padding-sm);
|
|
152
155
|
/* Quiet scrollbar, ChatThread's recipe: thin and token-coloured instead
|
|
153
156
|
of the 15px native chrome rail a classic-scrollbar platform would draw
|
|
154
157
|
inside the panel. The stable gutter matters here because filtering
|
|
@@ -202,7 +205,7 @@
|
|
|
202
205
|
display: flex;
|
|
203
206
|
align-items: center;
|
|
204
207
|
gap: var(--gap-sm-md);
|
|
205
|
-
padding: var(--padding-sm
|
|
208
|
+
padding: var(--padding-sm-md); /* 12px — 8px list inset keeps the text edge on the search row's 20px */
|
|
206
209
|
border-radius: var(--radius-sm);
|
|
207
210
|
cursor: pointer;
|
|
208
211
|
transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
@@ -276,7 +279,7 @@
|
|
|
276
279
|
display: flex;
|
|
277
280
|
align-items: center;
|
|
278
281
|
gap: var(--gap-md);
|
|
279
|
-
padding: var(--padding-sm) var(--padding-
|
|
282
|
+
padding: var(--padding-sm-md) var(--padding-lg);
|
|
280
283
|
border-top: var(--border-xs) solid var(--color-divider);
|
|
281
284
|
flex-shrink: 0;
|
|
282
285
|
flex-wrap: wrap;
|
|
@@ -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,58 +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) return;
|
|
84
|
-
const block = (event) => {
|
|
85
|
-
const target = event.target instanceof Element ? event.target : null;
|
|
86
|
-
if (target?.closest(`.${baseClass}__list`)) return;
|
|
87
|
-
event.preventDefault();
|
|
88
|
-
};
|
|
89
|
-
document.addEventListener("wheel", block, { passive: false });
|
|
90
|
-
document.addEventListener("touchmove", block, { passive: false });
|
|
91
|
-
return () => {
|
|
92
|
-
document.removeEventListener("wheel", block);
|
|
93
|
-
document.removeEventListener("touchmove", block);
|
|
94
|
-
};
|
|
95
|
-
}, [open, baseClass]);
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (!open) return;
|
|
98
|
-
const handleTrapKeyDown = (e) => {
|
|
99
|
-
if (e.key === "Escape") {
|
|
100
|
-
e.preventDefault();
|
|
101
|
-
onOpenChange(false);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (e.key === "Tab") {
|
|
105
|
-
const panel = panelRef.current;
|
|
106
|
-
if (!panel) return;
|
|
107
|
-
const focusable = panel.querySelectorAll(FOCUSABLE);
|
|
108
|
-
if (focusable.length === 0) return;
|
|
109
|
-
const first = focusable[0];
|
|
110
|
-
const last = focusable[focusable.length - 1];
|
|
111
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
112
|
-
e.preventDefault();
|
|
113
|
-
last.focus();
|
|
114
|
-
} else if (!e.shiftKey && document.activeElement === last) {
|
|
115
|
-
e.preventDefault();
|
|
116
|
-
first.focus();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
document.addEventListener("keydown", handleTrapKeyDown);
|
|
121
|
-
return () => document.removeEventListener("keydown", handleTrapKeyDown);
|
|
122
|
-
}, [open, onOpenChange]);
|
|
71
|
+
useLayer({ open, onDismiss: () => onOpenChange(false) });
|
|
72
|
+
useFocusScope(panelRef, { active: open, initialFocus: () => inputRef.current });
|
|
73
|
+
useScrollLock(open, { strategy: "cancel-events", allowWithin: listRef });
|
|
123
74
|
const runCommand = (command) => {
|
|
124
75
|
if (command.disabled) return;
|
|
125
76
|
command.onSelect?.();
|
|
@@ -166,6 +166,60 @@
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
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
|
+
|
|
169
223
|
/* ============================================
|
|
170
224
|
ATTACHMENTS ROW
|
|
171
225
|
============================================ */
|
|
@@ -37,6 +37,22 @@ type ComposerOwnProps = {
|
|
|
37
37
|
* field holds focus. Off by default.
|
|
38
38
|
*/
|
|
39
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;
|
|
40
56
|
/**
|
|
41
57
|
* Attachment row rendered above the textarea (DocumentChips). Fully
|
|
42
58
|
* controlled by the caller — Composer never owns the list.
|
|
@@ -59,10 +75,11 @@ type ComposerOwnProps = {
|
|
|
59
75
|
export interface ComposerProps extends ComposerOwnProps, Omit<React.ComponentPropsWithoutRef<'textarea'>, keyof ComposerOwnProps> {
|
|
60
76
|
}
|
|
61
77
|
/**
|
|
62
|
-
* Composer is the chat input shell: an
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
|
66
83
|
* is inert.
|
|
67
84
|
*
|
|
68
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,
|
|
@@ -89,6 +92,10 @@ const Composer = React.forwardRef(
|
|
|
89
92
|
style: { "--ds-composer-max-rows": maxRows },
|
|
90
93
|
onClick: handleShellClick,
|
|
91
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
|
+
] }),
|
|
92
99
|
attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
|
|
93
100
|
/* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, ref: contentRef, children: /* @__PURE__ */ jsx(
|
|
94
101
|
"textarea",
|
|
@@ -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
|
{
|
|
@@ -60,9 +60,10 @@ const ModelPicker = React.forwardRef(
|
|
|
60
60
|
}
|
|
61
61
|
}, []);
|
|
62
62
|
useEffect(() => {
|
|
63
|
+
if (!isOpen) return;
|
|
63
64
|
document.addEventListener("mousedown", handleClickOutside);
|
|
64
65
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
65
|
-
}, [handleClickOutside]);
|
|
66
|
+
}, [isOpen, handleClickOutside]);
|
|
66
67
|
const handleTriggerKeyDown = (e) => {
|
|
67
68
|
if (disabled) return;
|
|
68
69
|
switch (e.key) {
|
|
@@ -27,6 +27,9 @@ const Popover = ({
|
|
|
27
27
|
}
|
|
28
28
|
onOpenChange?.(next);
|
|
29
29
|
}, [isControlled, onOpenChange]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => clearTimeout(hoverTimeoutRef.current);
|
|
32
|
+
}, []);
|
|
30
33
|
useEffect(() => {
|
|
31
34
|
if (!isOpen || trigger !== "click") return;
|
|
32
35
|
const handler = (e) => {
|
|
@@ -55,12 +55,14 @@
|
|
|
55
55
|
transition: color var(--motion-duration-base) var(--motion-ease-standard);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/* Icon
|
|
58
|
+
/* Icon — same idle tier as the label, so an icon never reads dimmer than
|
|
59
|
+
the text beside it, and an icon-only segment (the site's theme toggle)
|
|
60
|
+
holds label-strength contrast in both modes. */
|
|
59
61
|
.ds-segmented-control__icon {
|
|
60
62
|
flex-shrink: 0;
|
|
61
63
|
--icon-size: var(--icon-size-md);
|
|
62
64
|
line-height: 1;
|
|
63
|
-
color: var(--color-
|
|
65
|
+
color: var(--color-text-secondary);
|
|
64
66
|
transition: color var(--motion-duration-base) var(--motion-ease-standard);
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -161,11 +161,11 @@
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
.ds-source-trail__row--active .ds-source-trail__row-indicator {
|
|
164
|
-
color: var(--color-status-info-
|
|
164
|
+
color: var(--color-status-info-icon);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
.ds-source-trail__row--done .ds-source-trail__row-indicator {
|
|
168
|
-
color: var(--color-status-positive-
|
|
168
|
+
color: var(--color-status-positive-icon);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/* A pending source has not been opened yet, so its chip sits quieter */
|
|
@@ -101,9 +101,10 @@ const TimePicker = React.forwardRef(
|
|
|
101
101
|
}
|
|
102
102
|
}, []);
|
|
103
103
|
useEffect(() => {
|
|
104
|
+
if (!isOpen) return;
|
|
104
105
|
document.addEventListener("mousedown", handleClickOutside);
|
|
105
106
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
106
|
-
}, [handleClickOutside]);
|
|
107
|
+
}, [isOpen, handleClickOutside]);
|
|
107
108
|
const handleKeyDown = (e) => {
|
|
108
109
|
if (disabled) return;
|
|
109
110
|
switch (e.key) {
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
============================================ */
|
|
14
14
|
|
|
15
15
|
.ds-tool-call {
|
|
16
|
+
/* Two colour channels per status: -text for the status word (tuned for
|
|
17
|
+
text contrast) and -icon for the indicator glyph, which needs a hue
|
|
18
|
+
that survives a plain surface — see the status token comment. */
|
|
16
19
|
--ds-tool-call-color: var(--color-text-secondary);
|
|
20
|
+
--ds-tool-call-icon-color: var(--color-icon-secondary);
|
|
17
21
|
|
|
18
22
|
display: flex;
|
|
19
23
|
flex-direction: column;
|
|
@@ -71,7 +75,7 @@
|
|
|
71
75
|
align-items: center;
|
|
72
76
|
justify-content: center;
|
|
73
77
|
flex: none;
|
|
74
|
-
color: var(--ds-tool-call-color);
|
|
78
|
+
color: var(--ds-tool-call-icon-color);
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
.ds-tool-call__indicator .material-symbols-rounded {
|
|
@@ -150,18 +154,22 @@
|
|
|
150
154
|
|
|
151
155
|
.ds-tool-call--pending {
|
|
152
156
|
--ds-tool-call-color: var(--color-status-warning-text);
|
|
157
|
+
--ds-tool-call-icon-color: var(--color-status-warning-icon);
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
.ds-tool-call--running {
|
|
156
161
|
--ds-tool-call-color: var(--color-status-info-text);
|
|
162
|
+
--ds-tool-call-icon-color: var(--color-status-info-icon);
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
.ds-tool-call--success {
|
|
160
166
|
--ds-tool-call-color: var(--color-status-positive-text);
|
|
167
|
+
--ds-tool-call-icon-color: var(--color-status-positive-icon);
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
.ds-tool-call--error {
|
|
164
171
|
--ds-tool-call-color: var(--color-status-error-text);
|
|
172
|
+
--ds-tool-call-icon-color: var(--color-status-error-icon);
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
/* ============================================
|
|
@@ -24,6 +24,12 @@ const Tooltip = ({
|
|
|
24
24
|
clearTimeout(showTimeoutRef.current);
|
|
25
25
|
hideTimeoutRef.current = setTimeout(() => setVisible(false), hideDelay);
|
|
26
26
|
};
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
return () => {
|
|
29
|
+
clearTimeout(showTimeoutRef.current);
|
|
30
|
+
clearTimeout(hideTimeoutRef.current);
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
27
33
|
useEffect(() => {
|
|
28
34
|
if (!visible) return;
|
|
29
35
|
const onKeyDown = (e) => {
|