@robr0/design-system 0.9.0 → 0.11.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 +16 -10
- package/components/AgentPlan/AgentPlan.css +14 -3
- package/components/AiButton/AiButton.css +1 -1
- package/components/Alert/Alert.css +9 -0
- package/components/AlertDialog/AlertDialog.css +3 -1
- package/components/AlertDialog/AlertDialog.js +2 -3
- package/components/AppSidebar/AppSidebar.css +22 -3
- package/components/AppSidebar/AppSidebar.d.ts +6 -2
- package/components/AppSidebar/AppSidebar.js +67 -36
- package/components/Card/Card.css +7 -2
- package/components/CardStack/CardStack.css +106 -0
- package/components/CardStack/CardStack.d.ts +44 -0
- package/components/CardStack/CardStack.js +140 -0
- package/components/Carousel/Carousel.css +9 -0
- package/components/Carousel/Carousel.js +37 -4
- package/components/Chart/AreaChart.js +6 -5
- package/components/Chart/BarChart.js +1 -1
- package/components/Chart/LineChart.js +4 -3
- package/components/Chart/PieChart.js +2 -15
- package/components/Chart/RadarChart.js +5 -4
- package/components/Chart/RadialChart.js +2 -15
- package/components/Chart/ScatterChart.js +3 -13
- package/components/Chart/StackedBarChart.js +2 -15
- package/components/Chart/Treemap.js +3 -17
- package/components/Chart/palette.d.ts +1 -0
- package/components/Chart/palette.js +19 -0
- package/components/ChatMarker/ChatMarker.css +9 -0
- package/components/ChatMessage/ChatMessage.css +16 -0
- package/components/ChatThread/ChatThread.js +2 -2
- package/components/Chip/Chip.css +9 -0
- package/components/CodeBlock/CodeBlock.css +10 -1
- package/components/CodeBlock/CodeBlock.js +2 -1
- package/components/ColorPicker/ColorPicker.js +10 -0
- package/components/Combobox/Combobox.css +9 -0
- package/components/CommandPalette/CommandPalette.css +9 -0
- package/components/CommandPalette/CommandPalette.js +31 -5
- package/components/Composer/Composer.css +118 -0
- package/components/Composer/Composer.d.ts +7 -0
- package/components/Composer/Composer.js +7 -1
- package/components/ContactCard/ContactCard.css +9 -0
- package/components/ContextMenu/ContextMenu.js +39 -3
- package/components/DatePicker/DatePicker.js +16 -0
- package/components/Dialog/Dialog.css +3 -1
- package/components/Dialog/Dialog.js +5 -4
- package/components/DocumentChip/DocumentChip.css +9 -0
- package/components/Dropdown/Dropdown.js +22 -3
- package/components/DropdownMenu/DropdownMenu.js +43 -6
- package/components/EmptyState/EmptyState.css +9 -0
- package/components/FileInput/FileInput.css +9 -0
- package/components/Globe/Globe.css +155 -0
- package/components/Globe/Globe.d.ts +96 -0
- package/components/Globe/Globe.js +415 -0
- package/components/InterruptCard/InterruptCard.css +27 -17
- package/components/InterruptCard/InterruptCard.d.ts +1 -1
- package/components/InterruptCard/InterruptCard.js +1 -1
- package/components/MapCallout/MapCallout.css +46 -0
- package/components/MapCallout/MapCallout.d.ts +29 -0
- package/components/MapCallout/MapCallout.js +17 -0
- package/components/MapLegend/MapLegend.css +88 -0
- package/components/MapLegend/MapLegend.d.ts +39 -0
- package/components/MapLegend/MapLegend.js +68 -0
- package/components/MessageActions/MessageActions.css +9 -0
- package/components/MessageCard/MessageCard.css +68 -16
- package/components/MessageCard/MessageCard.d.ts +1 -1
- package/components/MessageCard/MessageCard.js +6 -4
- package/components/ModelPicker/ModelPicker.css +4 -1
- package/components/ModelPicker/ModelPicker.js +43 -1
- package/components/NotificationCenter/NotificationCenter.css +9 -0
- package/components/NotificationCenter/NotificationCenter.js +26 -10
- package/components/NumberInput/NumberInput.css +9 -0
- package/components/Popover/Popover.css +3 -1
- package/components/Popover/Popover.js +2 -1
- package/components/SectionTitle/SectionTitle.css +8 -0
- package/components/SectionTitle/SectionTitle.d.ts +3 -1
- package/components/SectionTitle/SectionTitle.js +6 -1
- package/components/SourceChip/SourceChip.css +9 -0
- package/components/Stat/Stat.css +9 -0
- package/components/Stepper/Stepper.css +9 -0
- package/components/TagInput/TagInput.css +9 -0
- package/components/TimePicker/TimePicker.js +4 -1
- package/components/Toast/Toast.css +32 -11
- package/components/Toast/Toast.js +5 -4
- package/components/ToggleGroup/ToggleGroup.css +9 -0
- package/components/ToolCall/ToolCall.css +18 -6
- package/components/Tooltip/Tooltip.js +20 -16
- package/components/registry.json +37 -0
- package/components/registry.json.d.ts +37 -0
- package/components/registry.json.js +2 -2
- package/index.d.ts +4 -0
- package/index.js +8 -0
- package/package.json +5 -1
- package/tokens/motion.d.ts +23 -0
- package/tokens/motion.js +14 -0
- package/tokens/registry.json +17 -0
- package/tokens/registry.json.d.ts +17 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +28 -13
- package/tokens/tokens-light.css +26 -12
- package/tokens/tokens-typography.css +20 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
MAP LEGEND COMPONENT
|
|
3
|
+
The corner block of a map: name, one line on
|
|
4
|
+
what it shows, and the key to its markers.
|
|
5
|
+
============================================ */
|
|
6
|
+
|
|
7
|
+
/* Base */
|
|
8
|
+
|
|
9
|
+
.ds-map-legend {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: var(--gap-xs);
|
|
13
|
+
max-width: 32ch;
|
|
14
|
+
font-family: var(--font-family-code);
|
|
15
|
+
color: var(--color-text-primary);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Parts */
|
|
19
|
+
|
|
20
|
+
.ds-map-legend__title {
|
|
21
|
+
font-family: var(--font-family-primary);
|
|
22
|
+
font-size: var(--font-title-body-size);
|
|
23
|
+
font-weight: var(--font-title-body-weight);
|
|
24
|
+
line-height: var(--font-title-body-line-height);
|
|
25
|
+
letter-spacing: var(--font-title-body-letter-spacing);
|
|
26
|
+
color: var(--color-text-primary);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ds-map-legend__description {
|
|
30
|
+
font-size: var(--font-caption-size);
|
|
31
|
+
font-weight: var(--font-caption-weight);
|
|
32
|
+
line-height: var(--font-caption-line-height);
|
|
33
|
+
color: var(--color-text-secondary);
|
|
34
|
+
margin: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Key */
|
|
38
|
+
|
|
39
|
+
.ds-map-legend__key {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: var(--gap-xxs);
|
|
43
|
+
margin: 0;
|
|
44
|
+
margin-top: var(--gap-xs);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ds-map-legend__row {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--gap-sm);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ds-map-legend__glyph {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
color: var(--color-text-secondary);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ds-map-legend__glyph svg {
|
|
62
|
+
display: block;
|
|
63
|
+
overflow: visible;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ds-map-legend__glyph path,
|
|
67
|
+
.ds-map-legend__glyph rect {
|
|
68
|
+
fill: none;
|
|
69
|
+
stroke: currentColor;
|
|
70
|
+
stroke-width: 1.5;
|
|
71
|
+
stroke-linecap: round;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ds-map-legend__arc-stop-start {
|
|
75
|
+
stop-color: var(--color-chart-series-7);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ds-map-legend__arc-stop-end {
|
|
79
|
+
stop-color: var(--color-chart-series-5);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ds-map-legend__label {
|
|
83
|
+
font-size: var(--font-caption-size);
|
|
84
|
+
font-weight: var(--font-caption-weight);
|
|
85
|
+
line-height: var(--font-caption-line-height);
|
|
86
|
+
color: var(--color-text-secondary);
|
|
87
|
+
margin: 0;
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** One row of the key. */
|
|
3
|
+
export interface MapLegendItem {
|
|
4
|
+
/**
|
|
5
|
+
* The marker the row explains. `point` and `anchor` draw Globe's cross and
|
|
6
|
+
* square; `arc` draws a short stroke of the arc gradient; a `line` is a flat
|
|
7
|
+
* rule in `color`; any React node renders as given.
|
|
8
|
+
*/
|
|
9
|
+
glyph: 'point' | 'anchor' | 'arc' | 'line' | React.ReactNode;
|
|
10
|
+
/** What the marker means. */
|
|
11
|
+
label: React.ReactNode;
|
|
12
|
+
/** Colour for the `line` glyph (and the `arc` glyph, replacing its gradient). Any CSS colour. */
|
|
13
|
+
color?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Props owned by MapLegend itself — everything else falls through to the root `<div>`. */
|
|
16
|
+
type MapLegendOwnProps = {
|
|
17
|
+
/**
|
|
18
|
+
* The map's name, set above the key. Intentionally shadows the native
|
|
19
|
+
* `title` tooltip attribute, which MapLegend does not expose.
|
|
20
|
+
*/
|
|
21
|
+
title?: string;
|
|
22
|
+
/** One or two lines saying what the map shows. */
|
|
23
|
+
description?: React.ReactNode;
|
|
24
|
+
/** The key itself: one row per marker kind. */
|
|
25
|
+
items?: MapLegendItem[];
|
|
26
|
+
/** Additional CSS classes */
|
|
27
|
+
className?: string;
|
|
28
|
+
};
|
|
29
|
+
export interface MapLegendProps extends MapLegendOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof MapLegendOwnProps | 'children'> {
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* MapLegend — the block in the corner of a map: its name, a line on what it
|
|
33
|
+
* shows, and the key to its markers. The built-in glyphs are drawn on the
|
|
34
|
+
* same geometry Globe draws its markers with, so the key never shows a shape
|
|
35
|
+
* the map does not. Pure markup, no hooks, so it renders from a Server
|
|
36
|
+
* Component; the key is a `<dl>`, glyph as term and meaning as description.
|
|
37
|
+
*/
|
|
38
|
+
export declare const MapLegend: React.ForwardRefExoticComponent<MapLegendProps & React.RefAttributes<HTMLDivElement>>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./MapLegend.css";
|
|
4
|
+
const GLYPH_SIZE = 12;
|
|
5
|
+
const half = GLYPH_SIZE / 2;
|
|
6
|
+
const mark = 3.5;
|
|
7
|
+
const builtInGlyph = (item, gradientId) => {
|
|
8
|
+
switch (item.glyph) {
|
|
9
|
+
case "point":
|
|
10
|
+
return /* @__PURE__ */ jsx("path", { d: `M${half - mark} ${half}H${half + mark}M${half} ${half - mark}V${half + mark}` });
|
|
11
|
+
case "anchor":
|
|
12
|
+
return /* @__PURE__ */ jsx("rect", { x: half - mark, y: half - mark, width: mark * 2, height: mark * 2 });
|
|
13
|
+
case "arc":
|
|
14
|
+
return /* @__PURE__ */ jsx(
|
|
15
|
+
"path",
|
|
16
|
+
{
|
|
17
|
+
d: `M1 ${GLYPH_SIZE - 2}Q${half} -${half} ${GLYPH_SIZE - 1} ${GLYPH_SIZE - 2}`,
|
|
18
|
+
style: { stroke: item.color ?? `url(#${gradientId})` }
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
case "line":
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
"path",
|
|
24
|
+
{
|
|
25
|
+
d: `M1 ${half}H${GLYPH_SIZE - 1}`,
|
|
26
|
+
style: item.color ? { stroke: item.color } : void 0
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
default:
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const isBuiltIn = (glyph) => glyph === "point" || glyph === "anchor" || glyph === "arc" || glyph === "line";
|
|
34
|
+
const MapLegend = React.forwardRef(
|
|
35
|
+
({ title, description, items = [], className = "", ...rest }, ref) => {
|
|
36
|
+
const baseClass = "ds-map-legend";
|
|
37
|
+
const gradientId = React.useId();
|
|
38
|
+
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
39
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
40
|
+
title && /* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title }),
|
|
41
|
+
description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, children: description }),
|
|
42
|
+
items.length > 0 && /* @__PURE__ */ jsx("dl", { className: `${baseClass}__key`, children: items.map((item, i) => /* @__PURE__ */ jsxs("div", { className: `${baseClass}__row`, children: [
|
|
43
|
+
/* @__PURE__ */ jsx("dt", { className: `${baseClass}__glyph`, "aria-hidden": "true", children: isBuiltIn(item.glyph) ? /* @__PURE__ */ jsxs(
|
|
44
|
+
"svg",
|
|
45
|
+
{
|
|
46
|
+
viewBox: `0 0 ${GLYPH_SIZE} ${GLYPH_SIZE}`,
|
|
47
|
+
width: GLYPH_SIZE,
|
|
48
|
+
height: GLYPH_SIZE,
|
|
49
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
50
|
+
focusable: "false",
|
|
51
|
+
children: [
|
|
52
|
+
item.glyph === "arc" && !item.color && /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: `${gradientId}-${i}`, x1: "0", y1: "0", x2: "1", y2: "0", children: [
|
|
53
|
+
/* @__PURE__ */ jsx("stop", { offset: "0", className: `${baseClass}__arc-stop-start` }),
|
|
54
|
+
/* @__PURE__ */ jsx("stop", { offset: "1", className: `${baseClass}__arc-stop-end` })
|
|
55
|
+
] }) }),
|
|
56
|
+
builtInGlyph(item, `${gradientId}-${i}`)
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
) : item.glyph }),
|
|
60
|
+
/* @__PURE__ */ jsx("dd", { className: `${baseClass}__label`, children: item.label })
|
|
61
|
+
] }, i)) })
|
|
62
|
+
] });
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
MapLegend.displayName = "MapLegend";
|
|
66
|
+
export {
|
|
67
|
+
MapLegend
|
|
68
|
+
};
|
|
@@ -74,3 +74,12 @@
|
|
|
74
74
|
opacity: 0.4;
|
|
75
75
|
cursor: not-allowed;
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
79
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
80
|
+
.material-symbols-rounded itself (a common global default) would
|
|
81
|
+
otherwise override inheritance and detach the icon from the state
|
|
82
|
+
colours this component sets on its parents. */
|
|
83
|
+
.ds-message-actions .material-symbols-rounded {
|
|
84
|
+
color: inherit;
|
|
85
|
+
}
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
and an actions footer.
|
|
6
6
|
|
|
7
7
|
Not the navigation Card — this is content
|
|
8
|
-
furniture inside a conversation,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
furniture inside a conversation, sized by the
|
|
9
|
+
bubble or turn that holds it. It shares Card's
|
|
10
|
+
cover geometry and the chat bubbles' 24px
|
|
11
|
+
shell, so cards read the same across surfaces.
|
|
11
12
|
============================================ */
|
|
12
13
|
|
|
13
14
|
.ds-message-card {
|
|
@@ -17,18 +18,29 @@
|
|
|
17
18
|
max-width: var(--ds-message-card-max-width, 100%);
|
|
18
19
|
background-color: var(--color-bg-container-primary);
|
|
19
20
|
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
20
|
-
border-radius: var(--radius-
|
|
21
|
+
border-radius: var(--radius-xl);
|
|
21
22
|
overflow: hidden;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/* ============================================
|
|
25
26
|
MEDIA
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
Inset, not full bleed: 12px of card around a
|
|
28
|
+
12px-radius slot keeps the corners concentric
|
|
29
|
+
with the card's 24px shell — the exact
|
|
30
|
+
geometry of Card's case-study cover, and the
|
|
31
|
+
same rule the Composer follows.
|
|
28
32
|
============================================ */
|
|
29
33
|
|
|
30
34
|
.ds-message-card__media {
|
|
31
35
|
display: block;
|
|
36
|
+
margin: var(--padding-sm-md) var(--padding-sm-md) 0;
|
|
37
|
+
border-radius: var(--radius-md);
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
/* The slot carries its own surface, so drawn media — a chart, an SVG —
|
|
40
|
+
reads as the same image-shaped panel a photo would. The page tone, not
|
|
41
|
+
the container ramp: lightest in light mode, darkest in dark, so the
|
|
42
|
+
slot recedes behind the drawing instead of tinting it. */
|
|
43
|
+
background-color: var(--color-bg-page-primary);
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
.ds-message-card__media img {
|
|
@@ -36,15 +48,46 @@
|
|
|
36
48
|
width: 100%;
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
/* A Chart in the slot sheds its own panel chrome — the card is already the
|
|
52
|
+
panel, and the slot's page tone is the brightest surface in light mode and
|
|
53
|
+
the darkest in dark, so the drawing keeps maximum contrast against its
|
|
54
|
+
ground instead of sitting on a second container tone. */
|
|
55
|
+
.ds-message-card__media .ds-chart {
|
|
56
|
+
background-color: transparent;
|
|
57
|
+
border: none;
|
|
58
|
+
border-radius: 0;
|
|
59
|
+
padding: var(--padding-sm) var(--padding-sm) 0 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
/* ============================================
|
|
40
63
|
BODY
|
|
64
|
+
The vertical rhythm is deliberately even: the
|
|
65
|
+
media-to-body distance (--padding-md) equals
|
|
66
|
+
the body-to-actions distance (--padding-sm on
|
|
67
|
+
each side of the seam), so the copy sits
|
|
68
|
+
centred between the image and the CTA row.
|
|
41
69
|
============================================ */
|
|
42
70
|
|
|
43
71
|
.ds-message-card__body {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: var(--gap-sm);
|
|
75
|
+
padding: var(--padding-md) var(--padding-lg) var(--padding-sm);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Without an actions footer the body closes the card, so it takes the full
|
|
79
|
+
bottom padding back. */
|
|
80
|
+
.ds-message-card__body:last-child {
|
|
81
|
+
padding-bottom: var(--padding-md);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Title and meta form one unit, tucked closer together than the body's
|
|
85
|
+
stack gap so the meta reads as the title's subline. */
|
|
86
|
+
.ds-message-card__header {
|
|
44
87
|
display: flex;
|
|
45
88
|
flex-direction: column;
|
|
46
89
|
gap: var(--gap-xs);
|
|
47
|
-
|
|
90
|
+
min-width: 0;
|
|
48
91
|
}
|
|
49
92
|
|
|
50
93
|
.ds-message-card__title-row {
|
|
@@ -67,11 +110,11 @@
|
|
|
67
110
|
|
|
68
111
|
.ds-message-card__title {
|
|
69
112
|
min-width: 0;
|
|
70
|
-
font-family: var(--font-
|
|
71
|
-
font-size: var(--font-
|
|
72
|
-
font-weight: var(--font-
|
|
73
|
-
line-height: var(--font-
|
|
74
|
-
letter-spacing: var(--font-
|
|
113
|
+
font-family: var(--font-heading-3-family);
|
|
114
|
+
font-size: var(--font-heading-3-size);
|
|
115
|
+
font-weight: var(--font-heading-3-weight);
|
|
116
|
+
line-height: var(--font-heading-3-line-height);
|
|
117
|
+
letter-spacing: var(--font-heading-3-letter-spacing);
|
|
75
118
|
color: var(--color-text-primary);
|
|
76
119
|
}
|
|
77
120
|
|
|
@@ -98,14 +141,23 @@
|
|
|
98
141
|
ACTIONS
|
|
99
142
|
Mirrors ToolCall's actions footer exactly, so
|
|
100
143
|
approvals on a tool call and link actions on
|
|
101
|
-
a card read as the same row.
|
|
144
|
+
a card read as the same row. Whitespace alone
|
|
145
|
+
separates it from the body — no rule.
|
|
102
146
|
============================================ */
|
|
103
147
|
|
|
104
148
|
.ds-message-card__actions {
|
|
105
149
|
display: flex;
|
|
106
150
|
align-items: center;
|
|
107
|
-
justify-content: flex-
|
|
151
|
+
justify-content: flex-start;
|
|
108
152
|
gap: var(--gap-sm);
|
|
109
|
-
padding: var(--padding-sm-
|
|
110
|
-
|
|
153
|
+
padding: var(--padding-sm) var(--padding-lg) var(--padding-md);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
157
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
158
|
+
.material-symbols-rounded itself (a common global default) would
|
|
159
|
+
otherwise override inheritance and detach the icon from the state
|
|
160
|
+
colours this component sets on its parents. */
|
|
161
|
+
.ds-message-card .material-symbols-rounded {
|
|
162
|
+
color: inherit;
|
|
111
163
|
}
|
|
@@ -5,7 +5,7 @@ type MessageCardOwnProps = {
|
|
|
5
5
|
title?: string;
|
|
6
6
|
/** One or two lines under the title. */
|
|
7
7
|
description?: string;
|
|
8
|
-
/** Top media slot — an image or chart,
|
|
8
|
+
/** Top media slot — an image or chart, inset from the card edges with its own rounded corners. */
|
|
9
9
|
media?: React.ReactNode;
|
|
10
10
|
/** Small leading icon beside the title — a Material Symbol name, or any custom element. */
|
|
11
11
|
icon?: string | React.ReactNode;
|
|
@@ -10,11 +10,13 @@ const MessageCard = React.forwardRef(
|
|
|
10
10
|
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
11
11
|
media && /* @__PURE__ */ jsx("div", { className: `${baseClass}__media`, children: media }),
|
|
12
12
|
hasBody && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__body`, children: [
|
|
13
|
-
(title || icon) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}
|
|
14
|
-
icon && /* @__PURE__ */
|
|
15
|
-
|
|
13
|
+
(title || icon || meta) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
|
|
14
|
+
(title || icon) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__title-row`, children: [
|
|
15
|
+
icon && /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: icon }) : icon }),
|
|
16
|
+
title && /* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title })
|
|
17
|
+
] }),
|
|
18
|
+
meta && /* @__PURE__ */ jsx("span", { className: `${baseClass}__meta`, children: meta })
|
|
16
19
|
] }),
|
|
17
|
-
meta && /* @__PURE__ */ jsx("span", { className: `${baseClass}__meta`, children: meta }),
|
|
18
20
|
description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, children: description }),
|
|
19
21
|
children
|
|
20
22
|
] }),
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
display: inline-flex;
|
|
23
23
|
align-items: center;
|
|
24
24
|
gap: var(--gap-xxs);
|
|
25
|
-
|
|
25
|
+
/* CircularButton's 40px, so the pill and the composer's round icon
|
|
26
|
+
buttons read as one row of equals — same height, same resting shape. */
|
|
27
|
+
height: 40px;
|
|
28
|
+
padding: 0 var(--padding-md);
|
|
26
29
|
background-color: var(--color-action-passive-bg);
|
|
27
30
|
border: none;
|
|
28
31
|
border-radius: var(--radius-full);
|
|
@@ -98,17 +98,54 @@ const ModelPicker = React.forwardRef(
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
break;
|
|
101
|
+
case "Home":
|
|
102
|
+
if (isOpen) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
const first = models.findIndex((m) => !m.disabled);
|
|
105
|
+
if (first >= 0) setFocusedIndex(first);
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case "End":
|
|
109
|
+
if (isOpen) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
let last = models.length - 1;
|
|
112
|
+
while (last >= 0 && models[last].disabled) last--;
|
|
113
|
+
if (last >= 0) setFocusedIndex(last);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
101
116
|
case "Escape":
|
|
102
117
|
setIsOpen(false);
|
|
103
118
|
break;
|
|
104
119
|
}
|
|
105
120
|
};
|
|
121
|
+
const handleEffortKeyDown = (e, index) => {
|
|
122
|
+
let targetIndex = -1;
|
|
123
|
+
switch (e.key) {
|
|
124
|
+
case "ArrowRight":
|
|
125
|
+
case "ArrowDown":
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
targetIndex = (index + 1) % effortOptions.length;
|
|
128
|
+
break;
|
|
129
|
+
case "ArrowLeft":
|
|
130
|
+
case "ArrowUp":
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
targetIndex = (index - 1 + effortOptions.length) % effortOptions.length;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (targetIndex >= 0) {
|
|
136
|
+
selectEffort(effortOptions[targetIndex].value);
|
|
137
|
+
document.getElementById(`${id}-effort-option-${targetIndex}`)?.focus();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
106
140
|
useEffect(() => {
|
|
107
141
|
if (isOpen && focusedIndex >= 0 && listRef.current) {
|
|
108
142
|
const focusedEl = listRef.current.children[focusedIndex];
|
|
109
143
|
focusedEl?.scrollIntoView({ block: "nearest" });
|
|
110
144
|
}
|
|
111
145
|
}, [focusedIndex, isOpen]);
|
|
146
|
+
const activeOptionId = isOpen && focusedIndex >= 0 && models[focusedIndex] ? `${id}-option-${focusedIndex}` : void 0;
|
|
147
|
+
const checkedEffortIndex = effortOptions.findIndex((o) => o.value === currentEffort);
|
|
148
|
+
const effortTabStop = checkedEffortIndex >= 0 ? checkedEffortIndex : 0;
|
|
112
149
|
const classes = [
|
|
113
150
|
baseClass,
|
|
114
151
|
`${baseClass}--${placement}`,
|
|
@@ -126,6 +163,7 @@ const ModelPicker = React.forwardRef(
|
|
|
126
163
|
"aria-expanded": isOpen,
|
|
127
164
|
"aria-haspopup": "listbox",
|
|
128
165
|
"aria-controls": isOpen ? listboxId : void 0,
|
|
166
|
+
"aria-activedescendant": activeOptionId,
|
|
129
167
|
"aria-label": rest["aria-label"] ?? "Choose a model",
|
|
130
168
|
onClick: () => {
|
|
131
169
|
setIsOpen((prev) => !prev);
|
|
@@ -150,6 +188,7 @@ const ModelPicker = React.forwardRef(
|
|
|
150
188
|
children: models.map((model, index) => /* @__PURE__ */ jsxs(
|
|
151
189
|
"li",
|
|
152
190
|
{
|
|
191
|
+
id: `${id}-option-${index}`,
|
|
153
192
|
className: [
|
|
154
193
|
`${baseClass}__option`,
|
|
155
194
|
model.value === currentValue ? `${baseClass}__option--selected` : "",
|
|
@@ -190,17 +229,20 @@ const ModelPicker = React.forwardRef(
|
|
|
190
229
|
className: `${baseClass}__effort-options`,
|
|
191
230
|
role: "radiogroup",
|
|
192
231
|
"aria-labelledby": `${id}-effort-label`,
|
|
193
|
-
children: effortOptions.map((option) => /* @__PURE__ */ jsx(
|
|
232
|
+
children: effortOptions.map((option, index) => /* @__PURE__ */ jsx(
|
|
194
233
|
"button",
|
|
195
234
|
{
|
|
196
235
|
type: "button",
|
|
236
|
+
id: `${id}-effort-option-${index}`,
|
|
197
237
|
className: [
|
|
198
238
|
`${baseClass}__effort-option`,
|
|
199
239
|
option.value === currentEffort ? `${baseClass}__effort-option--selected` : ""
|
|
200
240
|
].filter(Boolean).join(" "),
|
|
201
241
|
role: "radio",
|
|
202
242
|
"aria-checked": option.value === currentEffort,
|
|
243
|
+
tabIndex: index === effortTabStop ? 0 : -1,
|
|
203
244
|
onClick: () => selectEffort(option.value),
|
|
245
|
+
onKeyDown: (e) => handleEffortKeyDown(e, index),
|
|
204
246
|
children: option.label
|
|
205
247
|
},
|
|
206
248
|
option.value
|
|
@@ -260,3 +260,12 @@
|
|
|
260
260
|
white-space: nowrap;
|
|
261
261
|
border: 0;
|
|
262
262
|
}
|
|
263
|
+
|
|
264
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
265
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
266
|
+
.material-symbols-rounded itself (a common global default) would
|
|
267
|
+
otherwise override inheritance and detach the icon from the state
|
|
268
|
+
colours this component sets on its parents. */
|
|
269
|
+
.ds-notification-center .material-symbols-rounded {
|
|
270
|
+
color: inherit;
|
|
271
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import React, { useState } from "react";
|
|
3
|
+
import React, { useState, useId } from "react";
|
|
4
4
|
import { EmptyState } from "../EmptyState/EmptyState.js";
|
|
5
5
|
import "./NotificationCenter.css";
|
|
6
6
|
import "../../fonts/material-symbols.css";
|
|
@@ -42,6 +42,11 @@ const NotificationCenter = React.forwardRef(
|
|
|
42
42
|
const isTabControlled = activeTab !== void 0;
|
|
43
43
|
const [uncontrolledTab, setUncontrolledTab] = useState(defaultTab ?? tabs?.[0]?.value);
|
|
44
44
|
const currentTab = isTabControlled ? activeTab : uncontrolledTab;
|
|
45
|
+
const idBase = useId();
|
|
46
|
+
const panelId = `${idBase}-panel`;
|
|
47
|
+
const tabIdFor = (index) => `${idBase}-tab-${index}`;
|
|
48
|
+
const activeTabIndex = tabs?.findIndex((tab) => tab.value === currentTab) ?? -1;
|
|
49
|
+
const hasTabs = !!tabs && tabs.length > 0;
|
|
45
50
|
const selectTab = (value) => {
|
|
46
51
|
if (!isTabControlled) setUncontrolledTab(value);
|
|
47
52
|
onTabChange?.(value);
|
|
@@ -80,21 +85,23 @@ const NotificationCenter = React.forwardRef(
|
|
|
80
85
|
/* @__PURE__ */ jsxs("header", { className: `${baseClass}__header`, children: [
|
|
81
86
|
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__heading`, children: [
|
|
82
87
|
/* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title }),
|
|
83
|
-
unreadCount !== void 0 && unreadCount > 0 && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__unread`, children: [
|
|
88
|
+
unreadCount !== void 0 && unreadCount > 0 && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__unread`, "aria-live": "polite", children: [
|
|
84
89
|
unreadCount,
|
|
85
90
|
" unread"
|
|
86
91
|
] })
|
|
87
92
|
] }),
|
|
88
93
|
onMarkAllRead && /* @__PURE__ */ jsx("button", { type: "button", className: `${baseClass}__mark-all`, onClick: onMarkAllRead, children: markAllLabel })
|
|
89
94
|
] }),
|
|
90
|
-
|
|
95
|
+
hasTabs && /* @__PURE__ */ jsx("div", { className: `${baseClass}__tabs`, role: "tablist", "aria-label": `${title} filters`, children: tabs.map((tab, index) => {
|
|
91
96
|
const isActive = tab.value === currentTab;
|
|
92
97
|
return /* @__PURE__ */ jsxs(
|
|
93
98
|
"button",
|
|
94
99
|
{
|
|
95
100
|
type: "button",
|
|
101
|
+
id: tabIdFor(index),
|
|
96
102
|
role: "tab",
|
|
97
103
|
"aria-selected": isActive,
|
|
104
|
+
"aria-controls": panelId,
|
|
98
105
|
tabIndex: isActive ? 0 : -1,
|
|
99
106
|
className: [
|
|
100
107
|
`${baseClass}__tab`,
|
|
@@ -110,15 +117,24 @@ const NotificationCenter = React.forwardRef(
|
|
|
110
117
|
tab.value
|
|
111
118
|
);
|
|
112
119
|
}) }),
|
|
113
|
-
/* @__PURE__ */ jsx(
|
|
114
|
-
|
|
120
|
+
/* @__PURE__ */ jsx(
|
|
121
|
+
"div",
|
|
115
122
|
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
className: `${baseClass}__list`,
|
|
124
|
+
role: hasTabs ? "tabpanel" : void 0,
|
|
125
|
+
id: hasTabs ? panelId : void 0,
|
|
126
|
+
"aria-labelledby": hasTabs && activeTabIndex >= 0 ? tabIdFor(activeTabIndex) : void 0,
|
|
127
|
+
children: hasItems ? children : emptyState ?? /* @__PURE__ */ jsx(
|
|
128
|
+
EmptyState,
|
|
129
|
+
{
|
|
130
|
+
icon: "notifications",
|
|
131
|
+
title: "Nothing new",
|
|
132
|
+
description: "Notifications will collect here as they arrive.",
|
|
133
|
+
size: "compact"
|
|
134
|
+
}
|
|
135
|
+
)
|
|
120
136
|
}
|
|
121
|
-
)
|
|
137
|
+
)
|
|
122
138
|
] });
|
|
123
139
|
}
|
|
124
140
|
);
|
|
@@ -152,3 +152,12 @@
|
|
|
152
152
|
.ds-numberinput--compact .ds-numberinput__stepper {
|
|
153
153
|
padding: 0 var(--padding-sm); /* 8px */
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
/* Icons take the colour of the text around them. Explicit, not just
|
|
157
|
+
inherited by default: a consumer stylesheet that sets a colour on
|
|
158
|
+
.material-symbols-rounded itself (a common global default) would
|
|
159
|
+
otherwise override inheritance and detach the icon from the state
|
|
160
|
+
colours this component sets on its parents. */
|
|
161
|
+
.ds-numberinput .material-symbols-rounded {
|
|
162
|
+
color: inherit;
|
|
163
|
+
}
|
|
@@ -35,13 +35,15 @@
|
|
|
35
35
|
|
|
36
36
|
/* Hidden by default */
|
|
37
37
|
opacity: 0;
|
|
38
|
+
visibility: hidden; /* keeps panel content out of the tab order when closed */
|
|
38
39
|
pointer-events: none;
|
|
39
40
|
transform: scale(0.96);
|
|
40
|
-
transition: opacity var(--motion-duration-base) var(--motion-ease-standard), transform var(--motion-duration-base) var(--motion-ease-standard);
|
|
41
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard), transform var(--motion-duration-base) var(--motion-ease-standard), visibility var(--motion-duration-base) var(--motion-ease-standard);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
.ds-popover__panel--open {
|
|
44
45
|
opacity: 1;
|
|
46
|
+
visibility: visible;
|
|
45
47
|
pointer-events: auto;
|
|
46
48
|
transform: scale(1);
|
|
47
49
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useRef, useCallback, useEffect } from "react";
|
|
4
|
+
import { MOTION_HOVER_HIDE_DELAY_MS } from "../../tokens/motion.js";
|
|
4
5
|
import "./Popover.css";
|
|
5
6
|
const INTERACTIVE_TAGS = /* @__PURE__ */ new Set(["button", "a", "input", "select", "textarea", "summary"]);
|
|
6
7
|
const canCarryPopoverSemantics = (el) => typeof el.type === "string" ? INTERACTIVE_TAGS.has(el.type) : true;
|
|
@@ -59,7 +60,7 @@ const Popover = ({
|
|
|
59
60
|
};
|
|
60
61
|
const handleMouseLeave = () => {
|
|
61
62
|
if (trigger === "hover") {
|
|
62
|
-
hoverTimeoutRef.current = setTimeout(() => setOpen(false),
|
|
63
|
+
hoverTimeoutRef.current = setTimeout(() => setOpen(false), MOTION_HOVER_HIDE_DELAY_MS);
|
|
63
64
|
}
|
|
64
65
|
};
|
|
65
66
|
const baseClass = "ds-popover";
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
padding-bottom: var(--padding-xl);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/* Divider-less variant: for headings above content that draws its own
|
|
15
|
+
lines, where a second rule would double up. The generous clearance
|
|
16
|
+
the divider needed drops to a small breathing gap. */
|
|
17
|
+
.ds-section-title--no-divider {
|
|
18
|
+
border-bottom: none;
|
|
19
|
+
padding-bottom: var(--padding-md);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
/* ============================================
|
|
15
23
|
HEADING
|
|
16
24
|
============================================ */
|
|
@@ -4,7 +4,9 @@ export interface SectionTitleProps {
|
|
|
4
4
|
title: string;
|
|
5
5
|
/** Optional trailing content (count, badge, metadata) */
|
|
6
6
|
trailing?: React.ReactNode;
|
|
7
|
+
/** Whether to draw the bottom divider line. Set false above content that draws its own lines (bordered tables, calendars), so the section separates by whitespace alone. */
|
|
8
|
+
divider?: boolean;
|
|
7
9
|
/** Additional CSS classes */
|
|
8
10
|
className?: string;
|
|
9
11
|
}
|
|
10
|
-
export declare function SectionTitle({ title, trailing, className, }: SectionTitleProps): React.JSX.Element;
|
|
12
|
+
export declare function SectionTitle({ title, trailing, divider, className, }: SectionTitleProps): React.JSX.Element;
|