@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.
Files changed (68) hide show
  1. package/README.md +2 -2
  2. package/components/AgentStatus/AgentStatus.css +82 -22
  3. package/components/AgentStatus/AgentStatus.d.ts +6 -2
  4. package/components/AgentStatus/AgentStatus.js +38 -6
  5. package/components/AiButton/AiButton.css +163 -0
  6. package/components/AiButton/AiButton.d.ts +36 -0
  7. package/components/AiButton/AiButton.js +58 -0
  8. package/components/ChatHeader/ChatHeader.css +35 -0
  9. package/components/ChatHeader/ChatHeader.d.ts +30 -0
  10. package/components/ChatHeader/ChatHeader.js +17 -0
  11. package/components/ChatMarker/ChatMarker.css +52 -0
  12. package/components/ChatMarker/ChatMarker.d.ts +22 -0
  13. package/components/ChatMarker/ChatMarker.js +18 -0
  14. package/components/ChatMessage/ChatMessage.css +304 -0
  15. package/components/ChatMessage/ChatMessage.d.ts +68 -0
  16. package/components/ChatMessage/ChatMessage.js +69 -0
  17. package/components/ChatThread/ChatThread.css +171 -0
  18. package/components/ChatThread/ChatThread.d.ts +41 -0
  19. package/components/ChatThread/ChatThread.js +174 -0
  20. package/components/Chip/Chip.css +27 -0
  21. package/components/Chip/Chip.d.ts +2 -2
  22. package/components/CircularButton/CircularButton.d.ts +23 -8
  23. package/components/CircularButton/CircularButton.js +54 -52
  24. package/components/CodeBlock/CodeBlock.css +2 -2
  25. package/components/Composer/Composer.css +164 -0
  26. package/components/Composer/Composer.d.ts +67 -0
  27. package/components/Composer/Composer.js +120 -0
  28. package/components/DocumentChip/DocumentChip.css +181 -0
  29. package/components/DocumentChip/DocumentChip.d.ts +41 -0
  30. package/components/DocumentChip/DocumentChip.js +78 -0
  31. package/components/InterruptCard/InterruptCard.css +164 -0
  32. package/components/InterruptCard/InterruptCard.d.ts +55 -0
  33. package/components/InterruptCard/InterruptCard.js +57 -0
  34. package/components/MessageActions/MessageActions.css +76 -0
  35. package/components/MessageActions/MessageActions.d.ts +38 -0
  36. package/components/MessageActions/MessageActions.js +33 -0
  37. package/components/MessageCard/MessageCard.css +112 -0
  38. package/components/MessageCard/MessageCard.d.ts +35 -0
  39. package/components/MessageCard/MessageCard.js +28 -0
  40. package/components/NavList/NavList.css +155 -0
  41. package/components/NavList/NavList.d.ts +56 -0
  42. package/components/NavList/NavList.js +99 -0
  43. package/components/PromptSuggestions/PromptSuggestions.css +87 -0
  44. package/components/PromptSuggestions/PromptSuggestions.d.ts +51 -0
  45. package/components/PromptSuggestions/PromptSuggestions.js +34 -0
  46. package/components/Prose/Prose.css +252 -0
  47. package/components/Prose/Prose.d.ts +21 -0
  48. package/components/Prose/Prose.js +14 -0
  49. package/components/Reasoning/Reasoning.css +149 -25
  50. package/components/Reasoning/Reasoning.d.ts +19 -0
  51. package/components/Reasoning/Reasoning.js +32 -3
  52. package/components/SourceChip/SourceChip.css +102 -0
  53. package/components/SourceChip/SourceChip.d.ts +32 -0
  54. package/components/SourceChip/SourceChip.js +31 -0
  55. package/components/Stat/Stat.css +2 -2
  56. package/components/ToolCall/ToolCall.css +3 -3
  57. package/components/registry.json +113 -1
  58. package/components/registry.json.d.ts +113 -1
  59. package/components/registry.json.js +1 -1
  60. package/index.d.ts +14 -0
  61. package/index.js +28 -0
  62. package/package.json +1 -1
  63. package/tokens/registry.json +12 -0
  64. package/tokens/registry.json.d.ts +12 -0
  65. package/tokens/registry.json.js +1 -1
  66. package/tokens/tokens-dark.css +13 -0
  67. package/tokens/tokens-light.css +42 -0
  68. 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
+ };