@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
|
@@ -61,3 +61,77 @@
|
|
|
61
61
|
font-weight: var(--font-paragraph-em-weight);
|
|
62
62
|
border-left-color: var(--color-text-primary);
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/* ============================================
|
|
66
|
+
FLOATING VARIANT
|
|
67
|
+
Collapsed to a minimap of short lines; the
|
|
68
|
+
full panel expands over it on hover, focus
|
|
69
|
+
or tap. The caller positions the nav itself
|
|
70
|
+
(typically fixed on the right page edge).
|
|
71
|
+
============================================ */
|
|
72
|
+
.ds-anchor-nav--floating {
|
|
73
|
+
position: relative;
|
|
74
|
+
display: inline-block;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ds-anchor-nav__lines {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
align-items: flex-end;
|
|
81
|
+
gap: var(--gap-sm);
|
|
82
|
+
/* A comfortable hover/tap target around the thin lines */
|
|
83
|
+
padding: var(--padding-sm);
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ds-anchor-nav__line {
|
|
88
|
+
/* One line per section: bespoke minimap geometry, specced in design.md */
|
|
89
|
+
width: 16px;
|
|
90
|
+
height: var(--border-md);
|
|
91
|
+
border-radius: var(--radius-full);
|
|
92
|
+
background-color: var(--color-divider);
|
|
93
|
+
transition:
|
|
94
|
+
width var(--motion-duration-fast) var(--motion-ease-standard),
|
|
95
|
+
background-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.ds-anchor-nav__line--active {
|
|
99
|
+
width: 24px;
|
|
100
|
+
background-color: var(--color-text-primary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ds-anchor-nav__panel {
|
|
104
|
+
position: absolute;
|
|
105
|
+
z-index: 10;
|
|
106
|
+
top: 50%;
|
|
107
|
+
right: 0;
|
|
108
|
+
min-width: 200px;
|
|
109
|
+
max-width: 280px;
|
|
110
|
+
/* A long list scrolls inside the panel rather than past the viewport */
|
|
111
|
+
max-height: 70vh;
|
|
112
|
+
overflow-y: auto;
|
|
113
|
+
padding: var(--padding-md);
|
|
114
|
+
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
115
|
+
border-radius: var(--radius-sm);
|
|
116
|
+
/* Page background, not container: the container fill is semi-transparent
|
|
117
|
+
in dark mode, and a floating panel must not show text through itself */
|
|
118
|
+
background-color: var(--color-bg-page-primary);
|
|
119
|
+
box-shadow: var(--shadow-floating);
|
|
120
|
+
|
|
121
|
+
/* Hidden by default — but never visibility: hidden, because the links must
|
|
122
|
+
stay focusable so tabbing into the nav expands it via :focus-within */
|
|
123
|
+
opacity: 0;
|
|
124
|
+
pointer-events: none;
|
|
125
|
+
transform: translate(var(--gap-xxs), -50%);
|
|
126
|
+
transition:
|
|
127
|
+
opacity var(--motion-duration-fast) var(--motion-ease-standard),
|
|
128
|
+
transform var(--motion-duration-fast) var(--motion-ease-standard);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.ds-anchor-nav--floating:hover .ds-anchor-nav__panel,
|
|
132
|
+
.ds-anchor-nav--floating:focus-within .ds-anchor-nav__panel,
|
|
133
|
+
.ds-anchor-nav--open .ds-anchor-nav__panel {
|
|
134
|
+
opacity: 1;
|
|
135
|
+
pointer-events: auto;
|
|
136
|
+
transform: translate(0, -50%);
|
|
137
|
+
}
|
|
@@ -8,6 +8,8 @@ export interface AnchorNavItem {
|
|
|
8
8
|
type AnchorNavOwnProps = {
|
|
9
9
|
/** The on-page sections to list, in document order */
|
|
10
10
|
items: AnchorNavItem[];
|
|
11
|
+
/** Visual form: `list` renders the inline "On this page" rail; `floating` collapses to a stack of short lines that expands into a panel on hover, keyboard focus, or tap, so it can ride a page edge without taking column width */
|
|
12
|
+
variant?: 'list' | 'floating';
|
|
11
13
|
/** Header text above the list; pass an empty string to render no header */
|
|
12
14
|
title?: string;
|
|
13
15
|
/** Material Symbols icon name beside the header; pass an empty string for none */
|
|
@@ -28,6 +30,11 @@ export interface AnchorNavProps extends AnchorNavOwnProps, Omit<React.ComponentP
|
|
|
28
30
|
* has passed `offset` as current. Pass `activeId` to control the highlight
|
|
29
31
|
* instead (no listener is attached). The active link carries
|
|
30
32
|
* `aria-current="location"`.
|
|
33
|
+
*
|
|
34
|
+
* The `floating` variant collapses the list to a minimap of short lines and
|
|
35
|
+
* expands the full panel on hover or keyboard focus (the links stay focusable
|
|
36
|
+
* while collapsed, so tabbing in opens it). A tap opens it where hover
|
|
37
|
+
* doesn't exist; Escape or a click elsewhere closes it again.
|
|
31
38
|
*/
|
|
32
39
|
export declare const AnchorNav: React.ForwardRefExoticComponent<AnchorNavProps & React.RefAttributes<HTMLElement>>;
|
|
33
40
|
export {};
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs,
|
|
2
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useRef, useImperativeHandle, useState, useCallback, useEffect } from "react";
|
|
4
4
|
import "./AnchorNav.css";
|
|
5
5
|
const AnchorNav = React.forwardRef(
|
|
6
6
|
({
|
|
7
7
|
items,
|
|
8
|
+
variant = "list",
|
|
8
9
|
title = "On this page",
|
|
9
10
|
icon = "toc",
|
|
10
11
|
activeId,
|
|
11
12
|
onActiveChange,
|
|
12
13
|
offset = 96,
|
|
13
14
|
className,
|
|
15
|
+
onKeyDown,
|
|
14
16
|
...rest
|
|
15
17
|
}, ref) => {
|
|
16
18
|
const navRef = useRef(null);
|
|
17
19
|
useImperativeHandle(ref, () => navRef.current);
|
|
20
|
+
const floating = variant === "floating";
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
18
22
|
const isControlled = activeId !== void 0;
|
|
19
23
|
const [trackedId, setTrackedId] = useState(void 0);
|
|
20
24
|
const currentId = isControlled ? activeId : trackedId;
|
|
@@ -54,26 +58,75 @@ const AnchorNav = React.forwardRef(
|
|
|
54
58
|
if (frame !== 0) window.cancelAnimationFrame(frame);
|
|
55
59
|
};
|
|
56
60
|
}, [isControlled, items, offset, setActive]);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!open) return;
|
|
63
|
+
const onPointerDown = (event) => {
|
|
64
|
+
if (navRef.current && !navRef.current.contains(event.target)) setOpen(false);
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
67
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
68
|
+
}, [open]);
|
|
69
|
+
const handleKeyDown = (event) => {
|
|
70
|
+
onKeyDown?.(event);
|
|
71
|
+
if (floating && event.key === "Escape") {
|
|
72
|
+
setOpen(false);
|
|
73
|
+
const focused = document.activeElement;
|
|
74
|
+
if (focused instanceof HTMLElement && navRef.current?.contains(focused)) focused.blur();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const classes = [
|
|
78
|
+
"ds-anchor-nav",
|
|
79
|
+
floating && "ds-anchor-nav--floating",
|
|
80
|
+
floating && open && "ds-anchor-nav--open",
|
|
81
|
+
className
|
|
82
|
+
].filter(Boolean).join(" ");
|
|
83
|
+
const header = title ? /* @__PURE__ */ jsxs("p", { className: "ds-anchor-nav__header", children: [
|
|
84
|
+
icon && /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded ds-anchor-nav__icon", "aria-hidden": "true", children: icon }),
|
|
85
|
+
title
|
|
86
|
+
] }) : null;
|
|
87
|
+
const list = /* @__PURE__ */ jsx("ul", { className: "ds-anchor-nav__list", children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
88
|
+
"a",
|
|
89
|
+
{
|
|
90
|
+
href: `#${item.id}`,
|
|
91
|
+
className: item.id === currentId ? "ds-anchor-nav__link ds-anchor-nav__link--active" : "ds-anchor-nav__link",
|
|
92
|
+
"aria-current": item.id === currentId ? "location" : void 0,
|
|
93
|
+
onClick: (event) => {
|
|
94
|
+
if (!isControlled) setActive(item.id);
|
|
95
|
+
else onActiveChangeRef.current?.(item.id);
|
|
96
|
+
if (floating) {
|
|
97
|
+
setOpen(false);
|
|
98
|
+
event.currentTarget.blur();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
children: item.label
|
|
102
|
+
}
|
|
103
|
+
) }, item.id)) });
|
|
104
|
+
return /* @__PURE__ */ jsx(
|
|
105
|
+
"nav",
|
|
106
|
+
{
|
|
107
|
+
ref: navRef,
|
|
108
|
+
"aria-label": title || "On this page",
|
|
109
|
+
className: classes,
|
|
110
|
+
...rest,
|
|
111
|
+
onKeyDown: handleKeyDown,
|
|
112
|
+
children: floating ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
113
|
+
/* @__PURE__ */ jsx("div", { className: "ds-anchor-nav__lines", "aria-hidden": "true", onClick: () => setOpen(true), children: items.map((item) => /* @__PURE__ */ jsx(
|
|
114
|
+
"span",
|
|
115
|
+
{
|
|
116
|
+
className: item.id === currentId ? "ds-anchor-nav__line ds-anchor-nav__line--active" : "ds-anchor-nav__line"
|
|
117
|
+
},
|
|
118
|
+
item.id
|
|
119
|
+
)) }),
|
|
120
|
+
/* @__PURE__ */ jsxs("div", { className: "ds-anchor-nav__panel", children: [
|
|
121
|
+
header,
|
|
122
|
+
list
|
|
123
|
+
] })
|
|
124
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
125
|
+
header,
|
|
126
|
+
list
|
|
127
|
+
] })
|
|
128
|
+
}
|
|
129
|
+
);
|
|
77
130
|
}
|
|
78
131
|
);
|
|
79
132
|
AnchorNav.displayName = "AnchorNav";
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
line-height: var(--font-paragraph-em-line-height);
|
|
24
24
|
letter-spacing: var(--font-paragraph-em-letter-spacing);
|
|
25
25
|
text-align: center;
|
|
26
|
+
/* A pill's label never wraps — a squeezed layout shortens around the
|
|
27
|
+
button, it doesn't fold the silhouette into two lines. */
|
|
28
|
+
white-space: nowrap;
|
|
26
29
|
|
|
27
30
|
/* Transitions */
|
|
28
31
|
transition: background-color var(--motion-duration-base) var(--motion-ease-standard), border-color var(--motion-duration-base) var(--motion-ease-standard), opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
@@ -182,6 +185,35 @@
|
|
|
182
185
|
cursor: not-allowed;
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
/* ============================================
|
|
189
|
+
NEUTRAL BUTTON (SOLID GREY)
|
|
190
|
+
Filled like primary but in the neutral greys —
|
|
191
|
+
unlike tertiary, the background is always shown.
|
|
192
|
+
============================================ */
|
|
193
|
+
|
|
194
|
+
.ds-button--neutral {
|
|
195
|
+
background-color: var(--color-action-neutral-bg);
|
|
196
|
+
border: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.ds-button--neutral .ds-button__text {
|
|
200
|
+
color: var(--color-action-neutral-text);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.ds-button--neutral.ds-button--hover {
|
|
204
|
+
background-color: var(--color-action-neutral-bg-hover);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.ds-button--neutral.ds-button--active {
|
|
208
|
+
background-color: var(--color-action-neutral-bg-active);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.ds-button--neutral.ds-button--disabled {
|
|
212
|
+
background-color: var(--color-action-neutral-bg);
|
|
213
|
+
opacity: 0.4;
|
|
214
|
+
cursor: not-allowed;
|
|
215
|
+
}
|
|
216
|
+
|
|
185
217
|
/* ============================================
|
|
186
218
|
DESTRUCTIVE BUTTON
|
|
187
219
|
For dangerous actions: delete, remove, etc.
|
|
@@ -266,6 +298,10 @@
|
|
|
266
298
|
color: var(--color-icon-primary);
|
|
267
299
|
}
|
|
268
300
|
|
|
301
|
+
.ds-button--neutral .ds-button__icon {
|
|
302
|
+
color: var(--color-action-neutral-text);
|
|
303
|
+
}
|
|
304
|
+
|
|
269
305
|
.ds-button--destructive .ds-button__icon {
|
|
270
306
|
color: var(--color-core-accent-coral);
|
|
271
307
|
}
|
|
@@ -315,6 +351,10 @@
|
|
|
315
351
|
background-color: var(--color-action-passive-bg-hover);
|
|
316
352
|
}
|
|
317
353
|
|
|
354
|
+
.ds-button--neutral:not(.ds-button--disabled):hover {
|
|
355
|
+
background-color: var(--color-action-neutral-bg-hover);
|
|
356
|
+
}
|
|
357
|
+
|
|
318
358
|
.ds-button--destructive:not(.ds-button--disabled):hover {
|
|
319
359
|
background-color: var(--color-core-accent-coral);
|
|
320
360
|
}
|
|
@@ -359,6 +399,10 @@
|
|
|
359
399
|
background-color: var(--color-action-passive-bg-active);
|
|
360
400
|
}
|
|
361
401
|
|
|
402
|
+
.ds-button--neutral:not(.ds-button--disabled):active {
|
|
403
|
+
background-color: var(--color-action-neutral-bg-active);
|
|
404
|
+
}
|
|
405
|
+
|
|
362
406
|
.ds-button--destructive:not(.ds-button--disabled):active {
|
|
363
407
|
background-color: var(--color-core-accent-coral);
|
|
364
408
|
}
|
|
@@ -382,6 +426,16 @@
|
|
|
382
426
|
pointer-events: none;
|
|
383
427
|
}
|
|
384
428
|
|
|
429
|
+
/* The spinner is a box, not a glyph: left inline in the icon slot, it sits
|
|
430
|
+
on the text baseline and the strut's descender pushes it above centre.
|
|
431
|
+
Flex centres it geometrically — the same fix as CircularButton's loading
|
|
432
|
+
icon. */
|
|
433
|
+
.ds-button--loading .ds-button__icon {
|
|
434
|
+
display: inline-flex;
|
|
435
|
+
align-items: center;
|
|
436
|
+
justify-content: center;
|
|
437
|
+
}
|
|
438
|
+
|
|
385
439
|
/* The generic icon-slot svg sizing must not distort the spinner —
|
|
386
440
|
it sizes itself from the Spinner size prop */
|
|
387
441
|
.ds-button .ds-button__icon .ds-spinner__circle {
|
|
@@ -8,7 +8,7 @@ type ButtonOwnProps = {
|
|
|
8
8
|
/** Icon for right side — Material Symbol name (string) or custom element (ReactNode) */
|
|
9
9
|
iconRight?: string | React.ReactNode;
|
|
10
10
|
/** Visual treatment */
|
|
11
|
-
variant?: 'primary' | 'secondary' | 'tertiary' | 'destructive';
|
|
11
|
+
variant?: 'primary' | 'secondary' | 'tertiary' | 'neutral' | 'destructive';
|
|
12
12
|
/** Button size */
|
|
13
13
|
size?: 'default' | 'compact';
|
|
14
14
|
/** Whether the button is disabled */
|
|
@@ -63,7 +63,7 @@ export interface ButtonProps extends ButtonOwnProps, Omit<React.ComponentPropsWi
|
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* Button component from Figma design system.
|
|
66
|
-
* Supports primary, secondary, tertiary and destructive variants.
|
|
66
|
+
* Supports primary, secondary, tertiary, neutral and destructive variants.
|
|
67
67
|
* Icons can be placed on the left and/or right side of the text.
|
|
68
68
|
*
|
|
69
69
|
* Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
|
|
@@ -149,7 +149,10 @@
|
|
|
149
149
|
|
|
150
150
|
.ds-chart__tooltip {
|
|
151
151
|
padding: var(--padding-sm) var(--padding-md);
|
|
152
|
-
background-color: var(--color-bg-
|
|
152
|
+
background-color: var(--color-bg-glass);
|
|
153
|
+
/* The glass token is deliberately translucent; this blur is what keeps
|
|
154
|
+
the chart's marks readable through the overlay. */
|
|
155
|
+
backdrop-filter: blur(24px);
|
|
153
156
|
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
154
157
|
border-radius: var(--radius-sm); /* 8px */
|
|
155
158
|
box-shadow: var(--shadow-floating);
|
|
@@ -196,6 +196,43 @@
|
|
|
196
196
|
background-color: var(--color-action-passive-bg-active);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/* ============================================
|
|
200
|
+
NEUTRAL VARIANT (SOLID GREY)
|
|
201
|
+
Filled like primary but in the neutral greys —
|
|
202
|
+
unlike tertiary, the background is always shown.
|
|
203
|
+
============================================ */
|
|
204
|
+
|
|
205
|
+
.ds-circular-button--neutral {
|
|
206
|
+
background-color: var(--color-action-neutral-bg);
|
|
207
|
+
border: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.ds-circular-button--neutral .ds-circular-button__icon {
|
|
211
|
+
color: var(--color-action-neutral-text);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.ds-circular-button--neutral.ds-circular-button--hover {
|
|
215
|
+
background-color: var(--color-action-neutral-bg-hover);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.ds-circular-button--neutral.ds-circular-button--active {
|
|
219
|
+
background-color: var(--color-action-neutral-bg-active);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ds-circular-button--neutral.ds-circular-button--disabled {
|
|
223
|
+
opacity: 0.4;
|
|
224
|
+
cursor: not-allowed;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* Interactive hover/active */
|
|
228
|
+
.ds-circular-button--neutral:not(.ds-circular-button--disabled):hover {
|
|
229
|
+
background-color: var(--color-action-neutral-bg-hover);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.ds-circular-button--neutral:not(.ds-circular-button--disabled):active {
|
|
233
|
+
background-color: var(--color-action-neutral-bg-active);
|
|
234
|
+
}
|
|
235
|
+
|
|
199
236
|
/* ============================================
|
|
200
237
|
LOADING
|
|
201
238
|
Keeps the variant's full-colour appearance (no disabled dim);
|
|
@@ -207,6 +244,15 @@
|
|
|
207
244
|
pointer-events: none;
|
|
208
245
|
}
|
|
209
246
|
|
|
247
|
+
/* The spinner is a box, not a glyph: left inline, it sits on the text
|
|
248
|
+
baseline and the strut's descender pushes it ~1px above the button's
|
|
249
|
+
centre. Flex centres it geometrically. */
|
|
250
|
+
.ds-circular-button--loading .ds-circular-button__icon {
|
|
251
|
+
display: inline-flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: center;
|
|
254
|
+
}
|
|
255
|
+
|
|
210
256
|
/* ============================================
|
|
211
257
|
FOCUS
|
|
212
258
|
============================================ */
|
|
@@ -4,7 +4,7 @@ type CircularButtonOwnProps = {
|
|
|
4
4
|
/** Material Symbol icon name */
|
|
5
5
|
icon: string;
|
|
6
6
|
/** Visual treatment */
|
|
7
|
-
variant?: 'primary' | 'secondary' | 'tertiary';
|
|
7
|
+
variant?: 'primary' | 'secondary' | 'tertiary' | 'neutral';
|
|
8
8
|
/**
|
|
9
9
|
* Legacy alias for `variant`.
|
|
10
10
|
*
|
|
@@ -34,6 +34,17 @@ type CircularButtonOwnProps = {
|
|
|
34
34
|
loading?: boolean;
|
|
35
35
|
/** Accessible label — required, because the button has no visible text */
|
|
36
36
|
ariaLabel: string;
|
|
37
|
+
/**
|
|
38
|
+
* The hover/focus tooltip. An icon-only control names itself: by default
|
|
39
|
+
* the button wears a Tooltip carrying `ariaLabel`. Pass a string to show
|
|
40
|
+
* different wording, or `false` to opt out — for a host that labels the
|
|
41
|
+
* control another way, or one that owns the button's box directly
|
|
42
|
+
* (SplitButton's trigger stretches to its sibling segment, which the
|
|
43
|
+
* tooltip wrapper would block).
|
|
44
|
+
*/
|
|
45
|
+
tooltip?: string | false;
|
|
46
|
+
/** Which side the tooltip opens on. */
|
|
47
|
+
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
|
|
37
48
|
/** Optional href — renders as <a> instead of <button> */
|
|
38
49
|
href?: string;
|
|
39
50
|
/** Optional target attribute for links */
|
|
@@ -48,7 +59,7 @@ export interface CircularButtonProps extends CircularButtonOwnProps, Omit<React.
|
|
|
48
59
|
/**
|
|
49
60
|
* Circular icon button component.
|
|
50
61
|
* A round button containing a single icon, available in
|
|
51
|
-
* primary, secondary and
|
|
62
|
+
* primary, secondary, tertiary and neutral variants with default and compact sizes.
|
|
52
63
|
*
|
|
53
64
|
* Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
|
|
54
65
|
* whichever element it renders, and spreads unrecognised props onto it.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Spinner } from "../Spinner/Spinner.js";
|
|
4
|
+
import { Tooltip } from "../Tooltip/Tooltip.js";
|
|
4
5
|
import "./CircularButton.css";
|
|
5
6
|
import "../../fonts/material-symbols.css";
|
|
6
7
|
const CircularButton = React.forwardRef(
|
|
@@ -13,6 +14,8 @@ const CircularButton = React.forwardRef(
|
|
|
13
14
|
size = "default",
|
|
14
15
|
loading,
|
|
15
16
|
ariaLabel,
|
|
17
|
+
tooltip,
|
|
18
|
+
tooltipPosition = "top",
|
|
16
19
|
href,
|
|
17
20
|
target,
|
|
18
21
|
rel,
|
|
@@ -33,22 +36,19 @@ const CircularButton = React.forwardRef(
|
|
|
33
36
|
className
|
|
34
37
|
].filter(Boolean).join(" ");
|
|
35
38
|
const children = isLoading ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Spinner, { size: size === "compact" ? "sm" : "md", variant: "inherit" }) }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon });
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
return /* @__PURE__ */ jsx(
|
|
39
|
+
const control = href && !isDisabled && !isLoading ? /* @__PURE__ */ jsx(
|
|
40
|
+
"a",
|
|
41
|
+
{
|
|
42
|
+
...rest,
|
|
43
|
+
ref,
|
|
44
|
+
className: classes,
|
|
45
|
+
href,
|
|
46
|
+
target,
|
|
47
|
+
rel,
|
|
48
|
+
"aria-label": ariaLabel,
|
|
49
|
+
children
|
|
50
|
+
}
|
|
51
|
+
) : /* @__PURE__ */ jsx(
|
|
52
52
|
"button",
|
|
53
53
|
{
|
|
54
54
|
...rest,
|
|
@@ -61,6 +61,9 @@ const CircularButton = React.forwardRef(
|
|
|
61
61
|
children
|
|
62
62
|
}
|
|
63
63
|
);
|
|
64
|
+
const tooltipContent = tooltip === false ? null : tooltip ?? ariaLabel;
|
|
65
|
+
if (!tooltipContent) return control;
|
|
66
|
+
return /* @__PURE__ */ jsx(Tooltip, { content: tooltipContent, position: tooltipPosition, children: control });
|
|
64
67
|
}
|
|
65
68
|
);
|
|
66
69
|
CircularButton.displayName = "CircularButton";
|
|
@@ -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;
|
|
@@ -14,13 +14,20 @@
|
|
|
14
14
|
opacity: 0;
|
|
15
15
|
visibility: hidden; /* also removes the input from the tab order */
|
|
16
16
|
pointer-events: none;
|
|
17
|
-
|
|
17
|
+
/* Visibility waits out the fade only on close (duration token as delay);
|
|
18
|
+
on open it must flip instantly — the open effect focuses the input a
|
|
19
|
+
frame later, and focus() on a still-hidden element is a no-op, which
|
|
20
|
+
left the palette opening without its caret. */
|
|
21
|
+
transition: opacity var(--motion-duration-fast) var(--motion-ease-standard),
|
|
22
|
+
visibility 0s linear var(--motion-duration-fast);
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
.ds-command-palette--open {
|
|
21
26
|
opacity: 1;
|
|
22
27
|
visibility: visible;
|
|
23
28
|
pointer-events: auto;
|
|
29
|
+
transition: opacity var(--motion-duration-fast) var(--motion-ease-standard),
|
|
30
|
+
visibility 0s;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/* ============================================
|
|
@@ -47,9 +54,12 @@
|
|
|
47
54
|
max-height: 60vh;
|
|
48
55
|
margin-top: 10vh;
|
|
49
56
|
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
50
|
-
border-radius: var(--radius-
|
|
51
|
-
background-color: var(--color-bg-
|
|
52
|
-
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);
|
|
53
63
|
overflow: hidden;
|
|
54
64
|
transform: scale(0.98);
|
|
55
65
|
transition: transform var(--motion-duration-fast) var(--motion-ease-standard);
|
|
@@ -67,9 +77,16 @@
|
|
|
67
77
|
display: flex;
|
|
68
78
|
align-items: center;
|
|
69
79
|
gap: var(--gap-sm); /* 8px */
|
|
70
|
-
padding: var(--padding-
|
|
80
|
+
padding: var(--padding-md) var(--padding-lg); /* 16px 20px */
|
|
71
81
|
border-bottom: var(--border-xs) solid var(--color-divider);
|
|
72
82
|
flex-shrink: 0;
|
|
83
|
+
transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* The focused field marks the whole row with the active input border, the
|
|
87
|
+
same signal a focused Input gives its own edge. */
|
|
88
|
+
.ds-command-palette__search:focus-within {
|
|
89
|
+
border-bottom-color: var(--color-input-border-selected);
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
.ds-command-palette__search-icon {
|
|
@@ -134,7 +151,18 @@
|
|
|
134
151
|
flex: 1;
|
|
135
152
|
min-height: 0;
|
|
136
153
|
overflow-y: auto;
|
|
137
|
-
padding: var(--padding-
|
|
154
|
+
padding: var(--padding-sm);
|
|
155
|
+
/* Quiet scrollbar, ChatThread's recipe: thin and token-coloured instead
|
|
156
|
+
of the 15px native chrome rail a classic-scrollbar platform would draw
|
|
157
|
+
inside the panel. The stable gutter matters here because filtering
|
|
158
|
+
toggles overflow — without it every keystroke that crosses the
|
|
159
|
+
threshold reflows the rows sideways. */
|
|
160
|
+
scrollbar-width: thin;
|
|
161
|
+
scrollbar-gutter: stable;
|
|
162
|
+
scrollbar-color: var(--color-divider) transparent;
|
|
163
|
+
/* The scroll lock lets wheel and touch through only inside this list, so
|
|
164
|
+
hitting its ends must not chain the leftover delta to the page. */
|
|
165
|
+
overscroll-behavior: contain;
|
|
138
166
|
}
|
|
139
167
|
|
|
140
168
|
.ds-command-palette__status {
|
|
@@ -177,7 +205,7 @@
|
|
|
177
205
|
display: flex;
|
|
178
206
|
align-items: center;
|
|
179
207
|
gap: var(--gap-sm-md);
|
|
180
|
-
padding: var(--padding-sm
|
|
208
|
+
padding: var(--padding-sm-md); /* 12px — 8px list inset keeps the text edge on the search row's 20px */
|
|
181
209
|
border-radius: var(--radius-sm);
|
|
182
210
|
cursor: pointer;
|
|
183
211
|
transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
@@ -251,7 +279,7 @@
|
|
|
251
279
|
display: flex;
|
|
252
280
|
align-items: center;
|
|
253
281
|
gap: var(--gap-md);
|
|
254
|
-
padding: var(--padding-sm) var(--padding-
|
|
282
|
+
padding: var(--padding-sm-md) var(--padding-lg);
|
|
255
283
|
border-top: var(--border-xs) solid var(--color-divider);
|
|
256
284
|
flex-shrink: 0;
|
|
257
285
|
flex-wrap: wrap;
|