@robr0/design-system 0.4.0 → 0.5.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 +2 -2
- package/components/AgentStatus/AgentStatus.css +82 -22
- package/components/AgentStatus/AgentStatus.d.ts +6 -2
- package/components/AgentStatus/AgentStatus.js +38 -6
- package/components/AiButton/AiButton.css +163 -0
- package/components/AiButton/AiButton.d.ts +36 -0
- package/components/AiButton/AiButton.js +58 -0
- package/components/ChatHeader/ChatHeader.css +35 -0
- package/components/ChatHeader/ChatHeader.d.ts +30 -0
- package/components/ChatHeader/ChatHeader.js +17 -0
- package/components/ChatMarker/ChatMarker.css +52 -0
- package/components/ChatMarker/ChatMarker.d.ts +22 -0
- package/components/ChatMarker/ChatMarker.js +18 -0
- package/components/ChatMessage/ChatMessage.css +304 -0
- package/components/ChatMessage/ChatMessage.d.ts +68 -0
- package/components/ChatMessage/ChatMessage.js +69 -0
- package/components/ChatThread/ChatThread.css +171 -0
- package/components/ChatThread/ChatThread.d.ts +41 -0
- package/components/ChatThread/ChatThread.js +174 -0
- package/components/Chip/Chip.css +27 -0
- package/components/Chip/Chip.d.ts +2 -2
- package/components/CircularButton/CircularButton.d.ts +23 -8
- package/components/CircularButton/CircularButton.js +54 -52
- package/components/CodeBlock/CodeBlock.css +2 -2
- package/components/Composer/Composer.css +164 -0
- package/components/Composer/Composer.d.ts +67 -0
- package/components/Composer/Composer.js +120 -0
- package/components/DocumentChip/DocumentChip.css +181 -0
- package/components/DocumentChip/DocumentChip.d.ts +41 -0
- package/components/DocumentChip/DocumentChip.js +78 -0
- package/components/InterruptCard/InterruptCard.css +164 -0
- package/components/InterruptCard/InterruptCard.d.ts +55 -0
- package/components/InterruptCard/InterruptCard.js +57 -0
- package/components/MessageActions/MessageActions.css +76 -0
- package/components/MessageActions/MessageActions.d.ts +38 -0
- package/components/MessageActions/MessageActions.js +33 -0
- package/components/MessageCard/MessageCard.css +112 -0
- package/components/MessageCard/MessageCard.d.ts +35 -0
- package/components/MessageCard/MessageCard.js +28 -0
- package/components/NavList/NavList.css +155 -0
- package/components/NavList/NavList.d.ts +56 -0
- package/components/NavList/NavList.js +99 -0
- package/components/PromptSuggestions/PromptSuggestions.css +87 -0
- package/components/PromptSuggestions/PromptSuggestions.d.ts +51 -0
- package/components/PromptSuggestions/PromptSuggestions.js +34 -0
- package/components/Prose/Prose.css +252 -0
- package/components/Prose/Prose.d.ts +21 -0
- package/components/Prose/Prose.js +14 -0
- package/components/Reasoning/Reasoning.css +149 -25
- package/components/Reasoning/Reasoning.d.ts +19 -0
- package/components/Reasoning/Reasoning.js +32 -3
- package/components/SourceChip/SourceChip.css +102 -0
- package/components/SourceChip/SourceChip.d.ts +32 -0
- package/components/SourceChip/SourceChip.js +31 -0
- package/components/Stat/Stat.css +2 -2
- package/components/ToolCall/ToolCall.css +3 -3
- package/components/registry.json +113 -1
- package/components/registry.json.d.ts +113 -1
- package/components/registry.json.js +1 -1
- package/index.d.ts +14 -0
- package/index.js +28 -0
- package/package.json +1 -1
- package/tokens/registry.json +12 -0
- package/tokens/registry.json.d.ts +12 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +13 -0
- package/tokens/tokens-light.css +42 -0
- package/tokens/tokens-typography.css +24 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
MESSAGE ACTIONS COMPONENT
|
|
3
|
+
The icon-button row under a chat message:
|
|
4
|
+
copy, retry, feedback. Ghost buttons — no
|
|
5
|
+
border, no resting background — so the row
|
|
6
|
+
stays quiet until pointed at.
|
|
7
|
+
|
|
8
|
+
Stateless beyond hover: copy feedback is the
|
|
9
|
+
consumer swapping an item's icon, never a
|
|
10
|
+
timer inside the row.
|
|
11
|
+
============================================ */
|
|
12
|
+
|
|
13
|
+
.ds-message-actions {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: var(--gap-xxs);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ============================================
|
|
20
|
+
BUTTON
|
|
21
|
+
Ghost: transparent at rest, the passive
|
|
22
|
+
action ramp on hover and for the active item.
|
|
23
|
+
============================================ */
|
|
24
|
+
|
|
25
|
+
.ds-message-actions__button {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
padding: var(--padding-xxs);
|
|
30
|
+
border: none;
|
|
31
|
+
border-radius: var(--radius-sm);
|
|
32
|
+
background-color: transparent;
|
|
33
|
+
color: var(--color-icon-primary);
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
transition:
|
|
36
|
+
background-color var(--motion-duration-fast) var(--motion-ease-standard),
|
|
37
|
+
color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ds-message-actions__button .material-symbols-rounded {
|
|
41
|
+
--icon-size: var(--icon-size-sm);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ds-message-actions__button:hover:not(:disabled) {
|
|
45
|
+
background-color: var(--color-action-passive-bg-hover);
|
|
46
|
+
color: var(--color-text-primary);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ============================================
|
|
50
|
+
ACTIVE
|
|
51
|
+
The selected item, e.g. the chosen feedback
|
|
52
|
+
thumb — held on the pressed step of the ramp.
|
|
53
|
+
============================================ */
|
|
54
|
+
|
|
55
|
+
.ds-message-actions__button--active {
|
|
56
|
+
background-color: var(--color-action-passive-bg-active);
|
|
57
|
+
color: var(--color-text-primary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ============================================
|
|
61
|
+
FOCUS
|
|
62
|
+
============================================ */
|
|
63
|
+
|
|
64
|
+
.ds-message-actions__button:focus-visible {
|
|
65
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
66
|
+
outline-offset: -2px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ============================================
|
|
70
|
+
DISABLED
|
|
71
|
+
============================================ */
|
|
72
|
+
|
|
73
|
+
.ds-message-actions__button:disabled {
|
|
74
|
+
opacity: 0.4;
|
|
75
|
+
cursor: not-allowed;
|
|
76
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** One action in the row: a stable id, a glyph, and an accessible name. */
|
|
3
|
+
export interface MessageAction {
|
|
4
|
+
/** Stable identifier passed to `onActionClick`. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Material Symbol name for the button glyph. */
|
|
7
|
+
icon: string;
|
|
8
|
+
/** Accessible name, also shown as the tooltip. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Marks the action as selected, e.g. the chosen feedback thumb. */
|
|
11
|
+
active?: boolean;
|
|
12
|
+
/** Disables just this action. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Props owned by MessageActions itself — everything else falls through to the root element. */
|
|
16
|
+
type MessageActionsOwnProps = {
|
|
17
|
+
/** The actions, rendered left to right. */
|
|
18
|
+
items: MessageAction[];
|
|
19
|
+
/** Fires with the pressed action's id. */
|
|
20
|
+
onActionClick?: (id: string) => void;
|
|
21
|
+
/** Wrap each button in a Tooltip showing its label. The label stays as `aria-label` either way. */
|
|
22
|
+
showTooltips?: boolean;
|
|
23
|
+
/** Additional CSS classes */
|
|
24
|
+
className?: string;
|
|
25
|
+
};
|
|
26
|
+
export interface MessageActionsProps extends MessageActionsOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof MessageActionsOwnProps> {
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* MessageActions is the icon-button row for message-level actions: copy,
|
|
30
|
+
* retry, feedback. It slots into ChatMessage's `actions` prop, which reveals
|
|
31
|
+
* it on hover and keyboard focus.
|
|
32
|
+
*
|
|
33
|
+
* The row is stateless beyond hover — copy feedback is the consumer swapping
|
|
34
|
+
* an item's `icon` (content_copy → check) and flipping it back, so the row
|
|
35
|
+
* never owns a timer or a clipboard call.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MessageActions: React.ForwardRefExoticComponent<MessageActionsProps & React.RefAttributes<HTMLDivElement>>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Tooltip } from "../Tooltip/Tooltip.js";
|
|
4
|
+
import "./MessageActions.css";
|
|
5
|
+
import "../../fonts/material-symbols.css";
|
|
6
|
+
const MessageActions = React.forwardRef(
|
|
7
|
+
({ items, onActionClick, showTooltips = true, className = "", ...rest }, ref) => {
|
|
8
|
+
const baseClass = "ds-message-actions";
|
|
9
|
+
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
10
|
+
return /* @__PURE__ */ jsx("div", { ...rest, ref, className: classes, children: items.map((item) => {
|
|
11
|
+
const button = /* @__PURE__ */ jsx(
|
|
12
|
+
"button",
|
|
13
|
+
{
|
|
14
|
+
type: "button",
|
|
15
|
+
className: [
|
|
16
|
+
`${baseClass}__button`,
|
|
17
|
+
item.active ? `${baseClass}__button--active` : ""
|
|
18
|
+
].filter(Boolean).join(" "),
|
|
19
|
+
"aria-label": item.label,
|
|
20
|
+
"aria-pressed": item.active !== void 0 ? item.active : void 0,
|
|
21
|
+
disabled: item.disabled,
|
|
22
|
+
onClick: () => onActionClick?.(item.id),
|
|
23
|
+
children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: item.icon })
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
return showTooltips ? /* @__PURE__ */ jsx(Tooltip, { content: item.label, children: button }, item.id) : /* @__PURE__ */ jsx(React.Fragment, { children: button }, item.id);
|
|
27
|
+
}) });
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
MessageActions.displayName = "MessageActions";
|
|
31
|
+
export {
|
|
32
|
+
MessageActions
|
|
33
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
MESSAGE CARD COMPONENT
|
|
3
|
+
Structured rich content inside a chat turn:
|
|
4
|
+
media on top, a title/meta/description body,
|
|
5
|
+
and an actions footer.
|
|
6
|
+
|
|
7
|
+
Not the navigation Card — this is content
|
|
8
|
+
furniture inside a conversation, --radius-md
|
|
9
|
+
like the other chat primitives, sized by the
|
|
10
|
+
bubble or turn that holds it.
|
|
11
|
+
============================================ */
|
|
12
|
+
|
|
13
|
+
.ds-message-card {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
width: 100%;
|
|
17
|
+
max-width: var(--ds-message-card-max-width, 100%);
|
|
18
|
+
background-color: var(--color-bg-container-primary);
|
|
19
|
+
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
20
|
+
border-radius: var(--radius-md);
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ============================================
|
|
25
|
+
MEDIA
|
|
26
|
+
Full bleed — the card's overflow clip rounds
|
|
27
|
+
the corners, so the slot carries no padding.
|
|
28
|
+
============================================ */
|
|
29
|
+
|
|
30
|
+
.ds-message-card__media {
|
|
31
|
+
display: block;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ds-message-card__media img {
|
|
35
|
+
display: block;
|
|
36
|
+
width: 100%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ============================================
|
|
40
|
+
BODY
|
|
41
|
+
============================================ */
|
|
42
|
+
|
|
43
|
+
.ds-message-card__body {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: var(--gap-xs);
|
|
47
|
+
padding: var(--padding-sm-md) var(--padding-md);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ds-message-card__title-row {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: var(--gap-xs);
|
|
54
|
+
min-width: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ds-message-card__icon {
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
flex: none;
|
|
61
|
+
color: var(--color-icon-primary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.ds-message-card__icon .material-symbols-rounded {
|
|
65
|
+
--icon-size: var(--icon-size-sm);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ds-message-card__title {
|
|
69
|
+
min-width: 0;
|
|
70
|
+
font-family: var(--font-title-body-family);
|
|
71
|
+
font-size: var(--font-title-body-size);
|
|
72
|
+
font-weight: var(--font-title-body-weight);
|
|
73
|
+
line-height: var(--font-title-body-line-height);
|
|
74
|
+
letter-spacing: var(--font-title-body-letter-spacing);
|
|
75
|
+
color: var(--color-text-primary);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ds-message-card__meta {
|
|
79
|
+
font-family: var(--font-paragraph-sm-family);
|
|
80
|
+
font-size: var(--font-paragraph-sm-size);
|
|
81
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
82
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
83
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
84
|
+
color: var(--color-text-tertiary);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ds-message-card__description {
|
|
88
|
+
margin: 0;
|
|
89
|
+
font-family: var(--font-paragraph-sm-family);
|
|
90
|
+
font-size: var(--font-paragraph-sm-size);
|
|
91
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
92
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
93
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
94
|
+
color: var(--color-text-secondary);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ============================================
|
|
98
|
+
ACTIONS
|
|
99
|
+
Mirrors ToolCall's actions footer exactly, so
|
|
100
|
+
approvals on a tool call and link actions on
|
|
101
|
+
a card read as the same row.
|
|
102
|
+
============================================ */
|
|
103
|
+
|
|
104
|
+
.ds-message-card__actions {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: flex-end;
|
|
108
|
+
gap: var(--gap-sm);
|
|
109
|
+
padding: var(--padding-sm) var(--padding-sm-md);
|
|
110
|
+
border-top: var(--border-xs) solid var(--color-divider);
|
|
111
|
+
background-color: var(--color-bg-container-secondary);
|
|
112
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by MessageCard itself — everything else falls through to the root element. */
|
|
3
|
+
type MessageCardOwnProps = {
|
|
4
|
+
/** Card heading. */
|
|
5
|
+
title?: string;
|
|
6
|
+
/** One or two lines under the title. */
|
|
7
|
+
description?: string;
|
|
8
|
+
/** Top media slot — an image or chart, rendered flush to the card edges (the card clips it). */
|
|
9
|
+
media?: React.ReactNode;
|
|
10
|
+
/** Small leading icon beside the title — a Material Symbol name, or any custom element. */
|
|
11
|
+
icon?: string | React.ReactNode;
|
|
12
|
+
/** Free-text metadata line, e.g. a domain or date, so callers keep their own formatting. */
|
|
13
|
+
meta?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Action row rendered in a footer outside the body — small secondary or
|
|
16
|
+
* tertiary Buttons, mirroring ToolCall's actions footer.
|
|
17
|
+
*/
|
|
18
|
+
actions?: React.ReactNode;
|
|
19
|
+
/** Additional CSS classes */
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Body content between the description and the footer — rich content, a Prose block, a DocumentChip row. */
|
|
22
|
+
children?: React.ReactNode;
|
|
23
|
+
};
|
|
24
|
+
export interface MessageCardProps extends MessageCardOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof MessageCardOwnProps> {
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* MessageCard is a structured rich-content card embedded in a chat message:
|
|
28
|
+
* media, title, body, and an actions row, for link previews, search results,
|
|
29
|
+
* and booking-style rich responses inside an assistant turn or bubble.
|
|
30
|
+
*
|
|
31
|
+
* It is not the navigation Card — this is content furniture inside a
|
|
32
|
+
* conversation, sized by the bubble or turn that holds it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MessageCard: React.ForwardRefExoticComponent<MessageCardProps & React.RefAttributes<HTMLDivElement>>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./MessageCard.css";
|
|
4
|
+
import "../../fonts/material-symbols.css";
|
|
5
|
+
const MessageCard = React.forwardRef(
|
|
6
|
+
({ title, description, media, icon, meta, actions, className = "", children, ...rest }, ref) => {
|
|
7
|
+
const baseClass = "ds-message-card";
|
|
8
|
+
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
9
|
+
const hasBody = Boolean(title || icon || meta || description || children);
|
|
10
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
11
|
+
media && /* @__PURE__ */ jsx("div", { className: `${baseClass}__media`, children: media }),
|
|
12
|
+
hasBody && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__body`, children: [
|
|
13
|
+
(title || icon) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__title-row`, children: [
|
|
14
|
+
icon && /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: icon }) : icon }),
|
|
15
|
+
title && /* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title })
|
|
16
|
+
] }),
|
|
17
|
+
meta && /* @__PURE__ */ jsx("span", { className: `${baseClass}__meta`, children: meta }),
|
|
18
|
+
description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, children: description }),
|
|
19
|
+
children
|
|
20
|
+
] }),
|
|
21
|
+
actions && /* @__PURE__ */ jsx("div", { className: `${baseClass}__actions`, children: actions })
|
|
22
|
+
] });
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
MessageCard.displayName = "MessageCard";
|
|
26
|
+
export {
|
|
27
|
+
MessageCard
|
|
28
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
NAV LIST COMPONENT
|
|
3
|
+
Vertical navigation link list for drawers and
|
|
4
|
+
side menus. Rows are tertiary Buttons — their
|
|
5
|
+
shape, sizing, and states live in Button.css;
|
|
6
|
+
this file owns only layout, the chevron
|
|
7
|
+
toggle, and the expand animation.
|
|
8
|
+
============================================ */
|
|
9
|
+
|
|
10
|
+
.ds-nav-list {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: var(--gap-xxs);
|
|
14
|
+
width: 100%;
|
|
15
|
+
margin: 0;
|
|
16
|
+
padding: 0;
|
|
17
|
+
list-style: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Every row reserves the toggle's 40px height, so leaf rows (compact
|
|
21
|
+
Buttons, ~32px) and rows carrying a 40px chevron toggle sit on the same
|
|
22
|
+
vertical rhythm instead of producing uneven gaps down the list. */
|
|
23
|
+
.ds-nav-list__item {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
min-height: 40px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ============================================
|
|
31
|
+
ROWS
|
|
32
|
+
Full-width, left-aligned Buttons; long labels
|
|
33
|
+
truncate with an ellipsis.
|
|
34
|
+
============================================ */
|
|
35
|
+
|
|
36
|
+
.ds-nav-list .ds-button {
|
|
37
|
+
width: 100%;
|
|
38
|
+
max-width: 100%;
|
|
39
|
+
justify-content: flex-start;
|
|
40
|
+
text-align: left;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ds-nav-list .ds-button__text {
|
|
44
|
+
min-width: 0;
|
|
45
|
+
flex: 0 1 auto;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
text-overflow: ellipsis;
|
|
48
|
+
white-space: nowrap;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* A row with a toggle shares its width with the chevron button. */
|
|
52
|
+
.ds-nav-list__row {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: var(--gap-xxs);
|
|
56
|
+
width: 100%;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ds-nav-list__row .ds-button {
|
|
60
|
+
width: auto;
|
|
61
|
+
flex: 1 1 auto;
|
|
62
|
+
min-width: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ============================================
|
|
66
|
+
TOGGLE
|
|
67
|
+
Mirrors CircularButton's 40px tertiary recipe.
|
|
68
|
+
============================================ */
|
|
69
|
+
|
|
70
|
+
.ds-nav-list__toggle {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
flex-shrink: 0;
|
|
75
|
+
width: 40px;
|
|
76
|
+
height: 40px;
|
|
77
|
+
padding: 0;
|
|
78
|
+
border: none;
|
|
79
|
+
border-radius: var(--radius-full);
|
|
80
|
+
background-color: var(--color-action-passive-bg);
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
transition: background-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ds-nav-list__toggle:hover {
|
|
86
|
+
background-color: var(--color-action-passive-bg-hover);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ds-nav-list__toggle:active {
|
|
90
|
+
background-color: var(--color-action-passive-bg-active);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.ds-nav-list__toggle:focus-visible {
|
|
94
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
95
|
+
outline-offset: -2px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.ds-nav-list__chevron {
|
|
99
|
+
--icon-size: var(--icon-size-sm);
|
|
100
|
+
color: var(--color-icon-primary);
|
|
101
|
+
line-height: 1;
|
|
102
|
+
transition: transform var(--motion-duration-slow) var(--motion-ease-standard);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ds-nav-list__toggle[aria-expanded='true'] .ds-nav-list__chevron {
|
|
106
|
+
transform: rotate(180deg);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ============================================
|
|
110
|
+
PANEL — collapsible sublist wrapper
|
|
111
|
+
Grid-rows height animation; reduced motion is
|
|
112
|
+
handled by the motion-token contract.
|
|
113
|
+
============================================ */
|
|
114
|
+
|
|
115
|
+
.ds-nav-list__panel {
|
|
116
|
+
display: grid;
|
|
117
|
+
grid-template-rows: 0fr;
|
|
118
|
+
margin-top: 0;
|
|
119
|
+
transition:
|
|
120
|
+
grid-template-rows var(--motion-duration-slow) var(--motion-ease-emphasized),
|
|
121
|
+
margin-top var(--motion-duration-slow) var(--motion-ease-emphasized);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Open panels take the list's item gap above them, so the first child sits
|
|
125
|
+
the same distance from its parent row as any two siblings do. */
|
|
126
|
+
.ds-nav-list__panel--open {
|
|
127
|
+
grid-template-rows: 1fr;
|
|
128
|
+
margin-top: var(--gap-xxs);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ============================================
|
|
132
|
+
SUBLIST — one indent step per nesting level
|
|
133
|
+
============================================ */
|
|
134
|
+
|
|
135
|
+
.ds-nav-list__sublist {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
gap: var(--gap-xxs);
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
min-height: 0;
|
|
141
|
+
margin: 0;
|
|
142
|
+
padding: 0;
|
|
143
|
+
padding-left: var(--padding-xl);
|
|
144
|
+
list-style: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.ds-nav-list__sublist .ds-nav-list__sublist {
|
|
148
|
+
padding-left: var(--gap-lg);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Permanent (non-collapsible) groups render their sublist directly under the
|
|
152
|
+
row, with no panel — give it the same item gap the panel carries when open. */
|
|
153
|
+
.ds-nav-list__item > .ds-nav-list__sublist {
|
|
154
|
+
margin-top: var(--gap-xxs);
|
|
155
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export interface NavListItem {
|
|
3
|
+
/** Row text. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Link destination — every row is a real link. */
|
|
6
|
+
href: string;
|
|
7
|
+
/** Stable identity for expansion state and aria wiring. Defaults to `href`. */
|
|
8
|
+
id?: string;
|
|
9
|
+
/** Child rows, indented one level. Nesting is supported three levels deep. */
|
|
10
|
+
items?: NavListItem[];
|
|
11
|
+
/**
|
|
12
|
+
* When false, an item with children renders no toggle and its children stay
|
|
13
|
+
* visible — a permanent group rather than a collapsible section.
|
|
14
|
+
*/
|
|
15
|
+
collapsible?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Forces the current-page treatment on this row regardless of the exact
|
|
18
|
+
* match against `currentHref` — for prefix-matched sections that should
|
|
19
|
+
* stay current on their child routes.
|
|
20
|
+
*/
|
|
21
|
+
current?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Props owned by NavList itself — everything else falls through to the list element. */
|
|
24
|
+
type NavListOwnProps = {
|
|
25
|
+
/** Navigation tree to render. */
|
|
26
|
+
items: NavListItem[];
|
|
27
|
+
/** href of the current page — the exactly matching row gets `aria-current="page"` and the active treatment. */
|
|
28
|
+
currentHref?: string;
|
|
29
|
+
/** Controlled list of expanded item ids. */
|
|
30
|
+
expandedIds?: string[];
|
|
31
|
+
/** Initially expanded item ids (uncontrolled). */
|
|
32
|
+
defaultExpandedIds?: string[];
|
|
33
|
+
/** Fires with the full new list of expanded ids whenever a toggle is pressed, in both controlled and uncontrolled modes. */
|
|
34
|
+
onExpandedChange?: (expandedIds: string[]) => void;
|
|
35
|
+
/** Allow several sections expanded at once. Default false — expanding one collapses the rest. */
|
|
36
|
+
multiple?: boolean;
|
|
37
|
+
/** Fires when any link is clicked, alongside native navigation — e.g. to close the drawer that contains the list. */
|
|
38
|
+
onNavigate?: (item: NavListItem, event: React.MouseEvent<HTMLElement>) => void;
|
|
39
|
+
/** Additional CSS classes */
|
|
40
|
+
className?: string;
|
|
41
|
+
};
|
|
42
|
+
export interface NavListProps extends NavListOwnProps, Omit<React.ComponentPropsWithoutRef<'ul'>, keyof NavListOwnProps> {
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Vertical list of navigation links for drawers and side menus.
|
|
46
|
+
*
|
|
47
|
+
* Every row is a real link, rendered as a tertiary Button so shape, sizing,
|
|
48
|
+
* and interaction states stay identical to every other button in the system.
|
|
49
|
+
* An item with children gets a separate chevron toggle — tapping the label
|
|
50
|
+
* navigates, tapping the chevron expands — and collapsed sublists are `inert`,
|
|
51
|
+
* so they leave the tab order and the accessibility tree. Expansion is
|
|
52
|
+
* uncontrolled by default (`defaultExpandedIds`) or controlled via
|
|
53
|
+
* `expandedIds` + `onExpandedChange`.
|
|
54
|
+
*/
|
|
55
|
+
export declare const NavList: React.ForwardRefExoticComponent<NavListProps & React.RefAttributes<HTMLUListElement>>;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useId, useState } from "react";
|
|
3
|
+
import { Button } from "../Button/Button.js";
|
|
4
|
+
import "./NavList.css";
|
|
5
|
+
import "../../fonts/material-symbols.css";
|
|
6
|
+
const MAX_DEPTH = 3;
|
|
7
|
+
const NavList = React.forwardRef(
|
|
8
|
+
({
|
|
9
|
+
items,
|
|
10
|
+
currentHref,
|
|
11
|
+
expandedIds,
|
|
12
|
+
defaultExpandedIds,
|
|
13
|
+
onExpandedChange,
|
|
14
|
+
multiple = false,
|
|
15
|
+
onNavigate,
|
|
16
|
+
className = "",
|
|
17
|
+
...rest
|
|
18
|
+
}, ref) => {
|
|
19
|
+
const baseClass = "ds-nav-list";
|
|
20
|
+
const uid = useId();
|
|
21
|
+
const [internalExpanded, setInternalExpanded] = useState(
|
|
22
|
+
defaultExpandedIds ?? []
|
|
23
|
+
);
|
|
24
|
+
const expanded = expandedIds ?? internalExpanded;
|
|
25
|
+
const itemId = (item) => item.id ?? item.href;
|
|
26
|
+
const toggle = (id) => {
|
|
27
|
+
const next = expanded.includes(id) ? expanded.filter((entry) => entry !== id) : multiple ? [...expanded, id] : [id];
|
|
28
|
+
if (expandedIds === void 0) setInternalExpanded(next);
|
|
29
|
+
onExpandedChange?.(next);
|
|
30
|
+
};
|
|
31
|
+
const renderItems = (level, levelItems) => levelItems.map((item) => {
|
|
32
|
+
const id = itemId(item);
|
|
33
|
+
const isCurrent = item.current ?? (currentHref !== void 0 && item.href === currentHref);
|
|
34
|
+
const children = level < MAX_DEPTH ? item.items : void 0;
|
|
35
|
+
const collapsible = item.collapsible ?? true;
|
|
36
|
+
const isOpen = expanded.includes(id);
|
|
37
|
+
const panelId = `${uid}-${id}`;
|
|
38
|
+
const link = /* @__PURE__ */ jsx(
|
|
39
|
+
Button,
|
|
40
|
+
{
|
|
41
|
+
variant: "tertiary",
|
|
42
|
+
size: level === 1 ? "default" : "compact",
|
|
43
|
+
label: item.label,
|
|
44
|
+
href: item.href,
|
|
45
|
+
ariaCurrent: isCurrent,
|
|
46
|
+
className: isCurrent ? "ds-button--active" : void 0,
|
|
47
|
+
onClick: (event) => onNavigate?.(item, event)
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
if (!children || children.length === 0) {
|
|
51
|
+
return /* @__PURE__ */ jsx("li", { className: `${baseClass}__item`, children: link }, id);
|
|
52
|
+
}
|
|
53
|
+
if (!collapsible) {
|
|
54
|
+
return /* @__PURE__ */ jsxs("li", { className: `${baseClass}__item`, children: [
|
|
55
|
+
link,
|
|
56
|
+
/* @__PURE__ */ jsx("ul", { className: `${baseClass}__sublist`, children: renderItems(level + 1, children) })
|
|
57
|
+
] }, id);
|
|
58
|
+
}
|
|
59
|
+
return /* @__PURE__ */ jsxs("li", { className: `${baseClass}__item`, children: [
|
|
60
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__row`, children: [
|
|
61
|
+
link,
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"button",
|
|
64
|
+
{
|
|
65
|
+
type: "button",
|
|
66
|
+
className: `${baseClass}__toggle`,
|
|
67
|
+
"aria-expanded": isOpen,
|
|
68
|
+
"aria-controls": panelId,
|
|
69
|
+
"aria-label": `${isOpen ? "Collapse" : "Expand"} ${item.label}`,
|
|
70
|
+
onClick: () => toggle(id),
|
|
71
|
+
children: /* @__PURE__ */ jsx(
|
|
72
|
+
"span",
|
|
73
|
+
{
|
|
74
|
+
className: `${baseClass}__chevron material-symbols-rounded`,
|
|
75
|
+
"aria-hidden": "true",
|
|
76
|
+
children: "expand_more"
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
] }),
|
|
82
|
+
/* @__PURE__ */ jsx(
|
|
83
|
+
"div",
|
|
84
|
+
{
|
|
85
|
+
id: panelId,
|
|
86
|
+
className: `${baseClass}__panel ${isOpen ? `${baseClass}__panel--open` : ""}`,
|
|
87
|
+
inert: !isOpen,
|
|
88
|
+
children: /* @__PURE__ */ jsx("ul", { className: `${baseClass}__sublist`, children: renderItems(level + 1, children) })
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
] }, id);
|
|
92
|
+
});
|
|
93
|
+
return /* @__PURE__ */ jsx("ul", { ...rest, ref, className: `${baseClass} ${className}`.trim(), children: renderItems(1, items) });
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
NavList.displayName = "NavList";
|
|
97
|
+
export {
|
|
98
|
+
NavList
|
|
99
|
+
};
|