@robr0/design-system 0.7.0 → 0.8.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.
@@ -0,0 +1,217 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import React, { useId, useRef, useState, useCallback, useEffect } from "react";
3
+ import "./ModelPicker.css";
4
+ import "../../fonts/material-symbols.css";
5
+ const DEFAULT_EFFORT_OPTIONS = [
6
+ { label: "Low", value: "low" },
7
+ { label: "Medium", value: "medium" },
8
+ { label: "High", value: "high" }
9
+ ];
10
+ const ModelPicker = React.forwardRef(
11
+ ({
12
+ models,
13
+ value,
14
+ defaultValue,
15
+ onValueChange,
16
+ effort,
17
+ defaultEffort,
18
+ onEffortChange,
19
+ effortOptions = DEFAULT_EFFORT_OPTIONS,
20
+ placement = "bottom",
21
+ disabled = false,
22
+ className = "",
23
+ ...rest
24
+ }, ref) => {
25
+ const baseClass = "ds-model-picker";
26
+ const id = useId();
27
+ const listboxId = `${id}-listbox`;
28
+ const rootRef = useRef(null);
29
+ const listRef = useRef(null);
30
+ const [isOpen, setIsOpen] = useState(false);
31
+ const [focusedIndex, setFocusedIndex] = useState(-1);
32
+ const isValueControlled = value !== void 0;
33
+ const [uncontrolledValue, setUncontrolledValue] = useState(
34
+ defaultValue ?? models[0]?.value
35
+ );
36
+ const currentValue = isValueControlled ? value : uncontrolledValue;
37
+ const isEffortControlled = effort !== void 0;
38
+ const [uncontrolledEffort, setUncontrolledEffort] = useState(defaultEffort);
39
+ const currentEffort = isEffortControlled ? effort : uncontrolledEffort;
40
+ const hasEffort = currentEffort !== void 0;
41
+ const setRootRef = (node) => {
42
+ rootRef.current = node;
43
+ if (typeof ref === "function") ref(node);
44
+ else if (ref) ref.current = node;
45
+ };
46
+ const selectedModel = models.find((m) => m.value === currentValue);
47
+ const selectModel = (modelValue) => {
48
+ if (!isValueControlled) setUncontrolledValue(modelValue);
49
+ onValueChange?.(modelValue);
50
+ setIsOpen(false);
51
+ };
52
+ const selectEffort = (effortValue) => {
53
+ if (!isEffortControlled) setUncontrolledEffort(effortValue);
54
+ onEffortChange?.(effortValue);
55
+ };
56
+ const handleClickOutside = useCallback((e) => {
57
+ if (rootRef.current && !rootRef.current.contains(e.target)) {
58
+ setIsOpen(false);
59
+ }
60
+ }, []);
61
+ useEffect(() => {
62
+ document.addEventListener("mousedown", handleClickOutside);
63
+ return () => document.removeEventListener("mousedown", handleClickOutside);
64
+ }, [handleClickOutside]);
65
+ const handleTriggerKeyDown = (e) => {
66
+ if (disabled) return;
67
+ switch (e.key) {
68
+ case "Enter":
69
+ case " ":
70
+ e.preventDefault();
71
+ if (isOpen && focusedIndex >= 0 && !models[focusedIndex]?.disabled) {
72
+ selectModel(models[focusedIndex].value);
73
+ } else {
74
+ setIsOpen((prev) => !prev);
75
+ setFocusedIndex(-1);
76
+ }
77
+ break;
78
+ case "ArrowDown":
79
+ e.preventDefault();
80
+ if (!isOpen) {
81
+ setIsOpen(true);
82
+ } else {
83
+ setFocusedIndex((prev) => {
84
+ let next = prev + 1;
85
+ while (next < models.length && models[next].disabled) next++;
86
+ return next < models.length ? next : prev;
87
+ });
88
+ }
89
+ break;
90
+ case "ArrowUp":
91
+ e.preventDefault();
92
+ if (isOpen) {
93
+ setFocusedIndex((prev) => {
94
+ let next = prev - 1;
95
+ while (next >= 0 && models[next].disabled) next--;
96
+ return next >= 0 ? next : prev;
97
+ });
98
+ }
99
+ break;
100
+ case "Escape":
101
+ setIsOpen(false);
102
+ break;
103
+ }
104
+ };
105
+ useEffect(() => {
106
+ if (isOpen && focusedIndex >= 0 && listRef.current) {
107
+ const focusedEl = listRef.current.children[focusedIndex];
108
+ focusedEl?.scrollIntoView({ block: "nearest" });
109
+ }
110
+ }, [focusedIndex, isOpen]);
111
+ const classes = [
112
+ baseClass,
113
+ `${baseClass}--${placement}`,
114
+ isOpen ? `${baseClass}--open` : "",
115
+ disabled ? `${baseClass}--disabled` : "",
116
+ className
117
+ ].filter(Boolean).join(" ");
118
+ return /* @__PURE__ */ jsxs("div", { ...rest, ref: setRootRef, className: classes, children: [
119
+ /* @__PURE__ */ jsxs(
120
+ "button",
121
+ {
122
+ type: "button",
123
+ className: `${baseClass}__trigger`,
124
+ disabled,
125
+ "aria-expanded": isOpen,
126
+ "aria-haspopup": "listbox",
127
+ "aria-controls": isOpen ? listboxId : void 0,
128
+ "aria-label": rest["aria-label"] ?? "Choose a model",
129
+ onClick: () => {
130
+ setIsOpen((prev) => !prev);
131
+ setFocusedIndex(-1);
132
+ },
133
+ onKeyDown: handleTriggerKeyDown,
134
+ children: [
135
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__trigger-label`, children: selectedModel ? selectedModel.label : "Choose a model" }),
136
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__chevron material-symbols-rounded`, "aria-hidden": "true", children: "expand_more" })
137
+ ]
138
+ }
139
+ ),
140
+ isOpen && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__panel`, children: [
141
+ /* @__PURE__ */ jsx(
142
+ "ul",
143
+ {
144
+ className: `${baseClass}__list`,
145
+ role: "listbox",
146
+ id: listboxId,
147
+ ref: listRef,
148
+ "aria-label": "Model",
149
+ children: models.map((model, index) => /* @__PURE__ */ jsxs(
150
+ "li",
151
+ {
152
+ className: [
153
+ `${baseClass}__option`,
154
+ model.value === currentValue ? `${baseClass}__option--selected` : "",
155
+ model.disabled ? `${baseClass}__option--disabled` : "",
156
+ index === focusedIndex ? `${baseClass}__option--focused` : ""
157
+ ].filter(Boolean).join(" "),
158
+ role: "option",
159
+ "aria-selected": model.value === currentValue,
160
+ "aria-disabled": model.disabled,
161
+ onClick: () => !model.disabled && selectModel(model.value),
162
+ children: [
163
+ /* @__PURE__ */ jsxs("span", { className: `${baseClass}__option-main`, children: [
164
+ /* @__PURE__ */ jsxs("span", { className: `${baseClass}__option-label`, children: [
165
+ model.label,
166
+ model.badge && /* @__PURE__ */ jsx("span", { className: `${baseClass}__option-badge`, children: model.badge })
167
+ ] }),
168
+ model.description && /* @__PURE__ */ jsx("span", { className: `${baseClass}__option-description`, children: model.description })
169
+ ] }),
170
+ model.value === currentValue && /* @__PURE__ */ jsx(
171
+ "span",
172
+ {
173
+ className: `${baseClass}__check material-symbols-rounded`,
174
+ "aria-hidden": "true",
175
+ children: "check"
176
+ }
177
+ )
178
+ ]
179
+ },
180
+ model.value
181
+ ))
182
+ }
183
+ ),
184
+ hasEffort && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__effort`, children: [
185
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__effort-label`, id: `${id}-effort-label`, children: "Effort" }),
186
+ /* @__PURE__ */ jsx(
187
+ "div",
188
+ {
189
+ className: `${baseClass}__effort-options`,
190
+ role: "radiogroup",
191
+ "aria-labelledby": `${id}-effort-label`,
192
+ children: effortOptions.map((option) => /* @__PURE__ */ jsx(
193
+ "button",
194
+ {
195
+ type: "button",
196
+ className: [
197
+ `${baseClass}__effort-option`,
198
+ option.value === currentEffort ? `${baseClass}__effort-option--selected` : ""
199
+ ].filter(Boolean).join(" "),
200
+ role: "radio",
201
+ "aria-checked": option.value === currentEffort,
202
+ onClick: () => selectEffort(option.value),
203
+ children: option.label
204
+ },
205
+ option.value
206
+ ))
207
+ }
208
+ )
209
+ ] })
210
+ ] })
211
+ ] });
212
+ }
213
+ );
214
+ ModelPicker.displayName = "ModelPicker";
215
+ export {
216
+ ModelPicker
217
+ };
@@ -0,0 +1,262 @@
1
+ /* ============================================
2
+ NOTIFICATION CENTER COMPONENT
3
+ The persistent inbox: header with unread
4
+ count and mark-all-read, filter tabs with
5
+ counts, and a scrollable list of items.
6
+
7
+ Toast interrupts; this accumulates. The
8
+ container is a floating panel, so it works
9
+ dropped from a bell icon or inline on a page.
10
+ ============================================ */
11
+
12
+ .ds-notification-center {
13
+ display: flex;
14
+ flex-direction: column;
15
+ width: 100%;
16
+ background-color: var(--color-bg-container-primary);
17
+ border: var(--border-xs) solid var(--color-bg-container-border);
18
+ border-radius: var(--radius-md);
19
+ overflow: hidden;
20
+ }
21
+
22
+ /* ============================================
23
+ HEADER
24
+ ============================================ */
25
+
26
+ .ds-notification-center__header {
27
+ display: flex;
28
+ align-items: baseline;
29
+ justify-content: space-between;
30
+ gap: var(--gap-sm);
31
+ padding: var(--padding-sm-md) var(--padding-sm-md) var(--padding-xs);
32
+ }
33
+
34
+ .ds-notification-center__heading {
35
+ display: flex;
36
+ align-items: baseline;
37
+ gap: var(--gap-xs);
38
+ min-width: 0;
39
+ }
40
+
41
+ .ds-notification-center__title {
42
+ color: var(--color-text-primary);
43
+ font-family: var(--font-title-body-family);
44
+ font-size: var(--font-title-body-size);
45
+ font-weight: var(--font-title-body-weight);
46
+ line-height: var(--font-title-body-line-height);
47
+ letter-spacing: var(--font-title-body-letter-spacing);
48
+ white-space: nowrap;
49
+ }
50
+
51
+ .ds-notification-center__unread {
52
+ color: var(--color-text-tertiary);
53
+ font-family: var(--font-paragraph-sm-family);
54
+ font-size: var(--font-paragraph-sm-size);
55
+ font-weight: var(--font-paragraph-sm-weight);
56
+ line-height: var(--font-paragraph-sm-line-height);
57
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
58
+ white-space: nowrap;
59
+ }
60
+
61
+ .ds-notification-center__mark-all {
62
+ flex: none;
63
+ padding: 0;
64
+ background: none;
65
+ border: none;
66
+ cursor: pointer;
67
+ color: var(--color-action-primary-text-tertiary);
68
+ font-family: var(--font-paragraph-sm-em-family);
69
+ font-size: var(--font-paragraph-sm-em-size);
70
+ font-weight: var(--font-paragraph-sm-em-weight);
71
+ line-height: var(--font-paragraph-sm-em-line-height);
72
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
73
+ transition: color var(--motion-duration-fast) var(--motion-ease-standard);
74
+ }
75
+
76
+ .ds-notification-center__mark-all:hover {
77
+ color: var(--color-action-primary-text-secondary);
78
+ }
79
+
80
+ .ds-notification-center__mark-all:focus-visible {
81
+ outline: 2px solid var(--color-action-primary-bg);
82
+ outline-offset: 2px;
83
+ border-radius: var(--radius-xxs);
84
+ }
85
+
86
+ /* ============================================
87
+ FILTER TABS
88
+ ============================================ */
89
+
90
+ .ds-notification-center__tabs {
91
+ display: flex;
92
+ gap: var(--gap-xxs);
93
+ padding: 0 var(--padding-sm-md) var(--padding-xs);
94
+ border-bottom: var(--border-xs) solid var(--color-divider);
95
+ }
96
+
97
+ .ds-notification-center__tab {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ gap: var(--gap-xxs);
101
+ padding: var(--padding-xxs) var(--padding-sm);
102
+ background: none;
103
+ border: none;
104
+ border-radius: var(--radius-full);
105
+ cursor: pointer;
106
+ color: var(--color-text-secondary);
107
+ font-family: var(--font-paragraph-sm-em-family);
108
+ font-size: var(--font-paragraph-sm-em-size);
109
+ font-weight: var(--font-paragraph-sm-em-weight);
110
+ line-height: var(--font-paragraph-sm-em-line-height);
111
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
112
+ transition:
113
+ background-color var(--motion-duration-fast) var(--motion-ease-standard),
114
+ color var(--motion-duration-fast) var(--motion-ease-standard);
115
+ }
116
+
117
+ .ds-notification-center__tab:hover {
118
+ background-color: var(--color-action-passive-bg-hover);
119
+ color: var(--color-text-primary);
120
+ }
121
+
122
+ .ds-notification-center__tab:focus-visible {
123
+ outline: 2px solid var(--color-action-primary-bg);
124
+ outline-offset: -2px;
125
+ }
126
+
127
+ .ds-notification-center__tab--active {
128
+ background-color: var(--color-bg-container-tertiary);
129
+ color: var(--color-text-primary);
130
+ }
131
+
132
+ .ds-notification-center__tab-count {
133
+ color: var(--color-text-tertiary);
134
+ font-variant-numeric: tabular-nums;
135
+ }
136
+
137
+ /* ============================================
138
+ LIST
139
+ ============================================ */
140
+
141
+ .ds-notification-center__list {
142
+ display: flex;
143
+ flex-direction: column;
144
+ overflow-y: auto;
145
+ padding: var(--padding-xxs) 0;
146
+ }
147
+
148
+ .ds-notification-center__list > .ds-notification-item:not(:last-child) {
149
+ border-bottom: var(--border-xs) solid var(--color-divider);
150
+ }
151
+
152
+ /* Give the built-in empty state breathing room */
153
+ .ds-notification-center__list > .ds-empty-state {
154
+ padding: var(--padding-lg) var(--padding-sm-md);
155
+ }
156
+
157
+ /* ============================================
158
+ NOTIFICATION ITEM
159
+ ============================================ */
160
+
161
+ .ds-notification-item {
162
+ display: flex;
163
+ align-items: flex-start;
164
+ gap: var(--gap-sm);
165
+ padding: var(--padding-sm) var(--padding-sm-md);
166
+ }
167
+
168
+ .ds-notification-item__media {
169
+ display: inline-flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ flex: none;
173
+ color: var(--color-icon-primary);
174
+ }
175
+
176
+ .ds-notification-item__media .material-symbols-rounded {
177
+ --icon-size: var(--icon-size-md);
178
+ }
179
+
180
+ .ds-notification-item__body {
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: var(--gap-xxs);
184
+ min-width: 0;
185
+ flex: 1 1 auto;
186
+ }
187
+
188
+ .ds-notification-item__top {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: var(--gap-xs);
192
+ }
193
+
194
+ .ds-notification-item__title {
195
+ flex: 1 1 auto;
196
+ min-width: 0;
197
+ color: var(--color-text-primary);
198
+ font-family: var(--font-paragraph-sm-family);
199
+ font-size: var(--font-paragraph-sm-size);
200
+ font-weight: var(--font-paragraph-sm-weight);
201
+ line-height: var(--font-paragraph-sm-line-height);
202
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
203
+ }
204
+
205
+ .ds-notification-item--unread .ds-notification-item__title {
206
+ font-family: var(--font-paragraph-sm-em-family);
207
+ font-weight: var(--font-paragraph-sm-em-weight);
208
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
209
+ }
210
+
211
+ .ds-notification-item__time {
212
+ flex: none;
213
+ color: var(--color-text-tertiary);
214
+ font-family: var(--font-paragraph-sm-family);
215
+ font-size: var(--font-paragraph-sm-size);
216
+ font-weight: var(--font-paragraph-sm-weight);
217
+ line-height: var(--font-paragraph-sm-line-height);
218
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
219
+ white-space: nowrap;
220
+ }
221
+
222
+ /* Unread dot — informational, so it takes the info status colour rather
223
+ than the action teal */
224
+ .ds-notification-item__dot {
225
+ flex: none;
226
+ width: var(--gap-xs);
227
+ height: var(--gap-xs);
228
+ border-radius: var(--radius-full);
229
+ background-color: var(--color-status-info-border);
230
+ }
231
+
232
+ .ds-notification-item__text {
233
+ color: var(--color-text-secondary);
234
+ font-family: var(--font-paragraph-sm-family);
235
+ font-size: var(--font-paragraph-sm-size);
236
+ font-weight: var(--font-paragraph-sm-weight);
237
+ line-height: var(--font-paragraph-sm-line-height);
238
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
239
+ }
240
+
241
+ .ds-notification-item__actions {
242
+ display: flex;
243
+ align-items: center;
244
+ gap: var(--gap-sm);
245
+ padding-top: var(--padding-xxxs);
246
+ }
247
+
248
+ /* ============================================
249
+ SCREEN READER TEXT
250
+ ============================================ */
251
+
252
+ .ds-notification-item__sr {
253
+ position: absolute;
254
+ width: 1px;
255
+ height: 1px;
256
+ padding: 0;
257
+ margin: -1px;
258
+ overflow: hidden;
259
+ clip: rect(0, 0, 0, 0);
260
+ white-space: nowrap;
261
+ border: 0;
262
+ }
@@ -0,0 +1,72 @@
1
+ import { default as React } from 'react';
2
+ export interface NotificationCenterTab {
3
+ /** Display label */
4
+ label: string;
5
+ /** Unique tab identifier */
6
+ value: string;
7
+ /** Count shown beside the label, e.g. the number of notifications in this filter. */
8
+ count?: number;
9
+ }
10
+ /** Props owned by NotificationItem itself — everything else falls through to the root element. */
11
+ type NotificationItemOwnProps = {
12
+ /** One-line headline, e.g. who did what. */
13
+ title: string;
14
+ /** When it happened, as already-formatted text like "2m" or "Mon". */
15
+ time?: string;
16
+ /** Marks the notification as not yet read: a dot beside the time and emphasised title. */
17
+ unread?: boolean;
18
+ /** Leading visual — a Material Symbol name (string) or a custom element (e.g. an Avatar). */
19
+ media?: string | React.ReactNode;
20
+ /** Action row under the body — typically compact Buttons like "Reply" or "Retry". */
21
+ actions?: React.ReactNode;
22
+ /** Additional CSS classes */
23
+ className?: string;
24
+ /** Supporting copy under the title. */
25
+ children?: React.ReactNode;
26
+ };
27
+ export interface NotificationItemProps extends NotificationItemOwnProps, Omit<React.ComponentPropsWithoutRef<'article'>, keyof NotificationItemOwnProps> {
28
+ }
29
+ /**
30
+ * NotificationItem is one row of a NotificationCenter: a leading visual, a
31
+ * headline with its time, supporting copy, and an optional row of actions.
32
+ * An unread item takes a dot and an emphasised title.
33
+ */
34
+ export declare const NotificationItem: React.ForwardRefExoticComponent<NotificationItemProps & React.RefAttributes<HTMLElement>>;
35
+ /** Props owned by NotificationCenter itself — everything else falls through to the root element. */
36
+ type NotificationCenterOwnProps = {
37
+ /** Heading shown in the header. */
38
+ title?: string;
39
+ /** How many notifications are unread, shown under the heading. Omit to hide the line. */
40
+ unreadCount?: number;
41
+ /** Fires when "Mark all read" is pressed; the control only renders when this is set. */
42
+ onMarkAllRead?: () => void;
43
+ /** Text of the mark-all-read control. */
44
+ markAllLabel?: string;
45
+ /** Filter tabs under the header, each with an optional count. */
46
+ tabs?: NotificationCenterTab[];
47
+ /** Active tab value for controlled use. Pair with `onTabChange`. */
48
+ activeTab?: string;
49
+ /** Initially active tab value for uncontrolled use. Defaults to the first tab. */
50
+ defaultTab?: string;
51
+ /** Fires with the newly selected tab's value. */
52
+ onTabChange?: (value: string) => void;
53
+ /** What to render when there are no children — defaults to a built-in empty state. */
54
+ emptyState?: React.ReactNode;
55
+ /** Additional CSS classes */
56
+ className?: string;
57
+ /** The notifications, newest first — typically NotificationItems. */
58
+ children?: React.ReactNode;
59
+ };
60
+ export interface NotificationCenterProps extends NotificationCenterOwnProps, Omit<React.ComponentPropsWithoutRef<'section'>, keyof NotificationCenterOwnProps> {
61
+ }
62
+ /**
63
+ * NotificationCenter is the persistent inbox for everything that happened
64
+ * while the user was away: a header with the unread count and a mark-all-read
65
+ * control, optional filter tabs with counts, and a scrollable list of
66
+ * NotificationItems. Filtering is the consumer's job — swap the children when
67
+ * the tab changes.
68
+ *
69
+ * Toast interrupts; this accumulates.
70
+ */
71
+ export declare const NotificationCenter: React.ForwardRefExoticComponent<NotificationCenterProps & React.RefAttributes<HTMLElement>>;
72
+ export {};
@@ -0,0 +1,128 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import React, { useState } from "react";
3
+ import { EmptyState } from "../EmptyState/EmptyState.js";
4
+ import "./NotificationCenter.css";
5
+ import "../../fonts/material-symbols.css";
6
+ const NotificationItem = React.forwardRef(
7
+ ({ title, time, unread = false, media, actions, className = "", children, ...rest }, ref) => {
8
+ const baseClass = "ds-notification-item";
9
+ const classes = [baseClass, unread ? `${baseClass}--unread` : "", className].filter(Boolean).join(" ");
10
+ return /* @__PURE__ */ jsxs("article", { ...rest, ref, className: classes, children: [
11
+ media && /* @__PURE__ */ jsx("span", { className: `${baseClass}__media`, "aria-hidden": "true", children: typeof media === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: media }) : media }),
12
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__body`, children: [
13
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__top`, children: [
14
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title }),
15
+ time && /* @__PURE__ */ jsx("span", { className: `${baseClass}__time`, children: time }),
16
+ unread && /* @__PURE__ */ jsx("span", { className: `${baseClass}__dot`, children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__sr`, children: "Unread" }) })
17
+ ] }),
18
+ children && /* @__PURE__ */ jsx("div", { className: `${baseClass}__text`, children }),
19
+ actions && /* @__PURE__ */ jsx("div", { className: `${baseClass}__actions`, children: actions })
20
+ ] })
21
+ ] });
22
+ }
23
+ );
24
+ NotificationItem.displayName = "NotificationItem";
25
+ const NotificationCenter = React.forwardRef(
26
+ ({
27
+ title = "Notifications",
28
+ unreadCount,
29
+ onMarkAllRead,
30
+ markAllLabel = "Mark all read",
31
+ tabs,
32
+ activeTab,
33
+ defaultTab,
34
+ onTabChange,
35
+ emptyState,
36
+ className = "",
37
+ children,
38
+ ...rest
39
+ }, ref) => {
40
+ const baseClass = "ds-notification-center";
41
+ const isTabControlled = activeTab !== void 0;
42
+ const [uncontrolledTab, setUncontrolledTab] = useState(defaultTab ?? tabs?.[0]?.value);
43
+ const currentTab = isTabControlled ? activeTab : uncontrolledTab;
44
+ const selectTab = (value) => {
45
+ if (!isTabControlled) setUncontrolledTab(value);
46
+ onTabChange?.(value);
47
+ };
48
+ const handleTabKeyDown = (e, index) => {
49
+ if (!tabs) return;
50
+ let target;
51
+ switch (e.key) {
52
+ case "ArrowRight":
53
+ e.preventDefault();
54
+ target = tabs[(index + 1) % tabs.length];
55
+ break;
56
+ case "ArrowLeft":
57
+ e.preventDefault();
58
+ target = tabs[(index - 1 + tabs.length) % tabs.length];
59
+ break;
60
+ case "Home":
61
+ e.preventDefault();
62
+ target = tabs[0];
63
+ break;
64
+ case "End":
65
+ e.preventDefault();
66
+ target = tabs[tabs.length - 1];
67
+ break;
68
+ }
69
+ if (target) {
70
+ selectTab(target.value);
71
+ const tabList = e.target.parentElement;
72
+ const buttons = tabList?.querySelectorAll(`.${baseClass}__tab`);
73
+ buttons?.[tabs.indexOf(target)]?.focus();
74
+ }
75
+ };
76
+ const hasItems = React.Children.count(children) > 0;
77
+ const classes = [baseClass, className].filter(Boolean).join(" ");
78
+ return /* @__PURE__ */ jsxs("section", { ...rest, ref, className: classes, children: [
79
+ /* @__PURE__ */ jsxs("header", { className: `${baseClass}__header`, children: [
80
+ /* @__PURE__ */ jsxs("div", { className: `${baseClass}__heading`, children: [
81
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title }),
82
+ unreadCount !== void 0 && unreadCount > 0 && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__unread`, children: [
83
+ unreadCount,
84
+ " unread"
85
+ ] })
86
+ ] }),
87
+ onMarkAllRead && /* @__PURE__ */ jsx("button", { type: "button", className: `${baseClass}__mark-all`, onClick: onMarkAllRead, children: markAllLabel })
88
+ ] }),
89
+ tabs && tabs.length > 0 && /* @__PURE__ */ jsx("div", { className: `${baseClass}__tabs`, role: "tablist", "aria-label": `${title} filters`, children: tabs.map((tab, index) => {
90
+ const isActive = tab.value === currentTab;
91
+ return /* @__PURE__ */ jsxs(
92
+ "button",
93
+ {
94
+ type: "button",
95
+ role: "tab",
96
+ "aria-selected": isActive,
97
+ tabIndex: isActive ? 0 : -1,
98
+ className: [
99
+ `${baseClass}__tab`,
100
+ isActive ? `${baseClass}__tab--active` : ""
101
+ ].filter(Boolean).join(" "),
102
+ onClick: () => selectTab(tab.value),
103
+ onKeyDown: (e) => handleTabKeyDown(e, index),
104
+ children: [
105
+ tab.label,
106
+ tab.count !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__tab-count`, children: tab.count })
107
+ ]
108
+ },
109
+ tab.value
110
+ );
111
+ }) }),
112
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__list`, children: hasItems ? children : emptyState ?? /* @__PURE__ */ jsx(
113
+ EmptyState,
114
+ {
115
+ icon: "notifications",
116
+ title: "Nothing new",
117
+ description: "Notifications will collect here as they arrive.",
118
+ size: "compact"
119
+ }
120
+ ) })
121
+ ] });
122
+ }
123
+ );
124
+ NotificationCenter.displayName = "NotificationCenter";
125
+ export {
126
+ NotificationCenter,
127
+ NotificationItem
128
+ };