@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.
- package/README.md +3 -3
- package/components/AgentPlan/AgentPlan.css +231 -0
- package/components/AgentPlan/AgentPlan.d.ts +41 -0
- package/components/AgentPlan/AgentPlan.js +99 -0
- package/components/DataTable/DataTable.css +105 -0
- package/components/DataTable/DataTable.d.ts +81 -0
- package/components/DataTable/DataTable.js +235 -0
- package/components/EventCalendar/EventCalendar.css +328 -0
- package/components/EventCalendar/EventCalendar.d.ts +51 -0
- package/components/EventCalendar/EventCalendar.js +213 -0
- package/components/ModelPicker/ModelPicker.css +238 -0
- package/components/ModelPicker/ModelPicker.d.ts +60 -0
- package/components/ModelPicker/ModelPicker.js +217 -0
- package/components/NotificationCenter/NotificationCenter.css +262 -0
- package/components/NotificationCenter/NotificationCenter.d.ts +72 -0
- package/components/NotificationCenter/NotificationCenter.js +128 -0
- package/components/ShaderField/field.glsl.d.ts +11 -5
- package/components/ShaderField/field.glsl.js +4 -3
- package/components/ShaderField/useShaderField.d.ts +19 -2
- package/components/ShaderField/useShaderField.js +17 -3
- package/components/Timeline/Timeline.css +17 -0
- package/components/Timeline/Timeline.d.ts +2 -0
- package/components/Timeline/Timeline.js +1 -0
- package/components/registry.json +40 -0
- package/components/registry.json.d.ts +40 -0
- package/components/registry.json.js +1 -1
- package/index.d.ts +5 -0
- package/index.js +11 -0
- package/package.json +1 -1
|
@@ -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
|
+
};
|