@robr0/design-system 0.6.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/LICENSE +53 -2
- package/README.md +36 -5
- package/components/AgentPlan/AgentPlan.css +231 -0
- package/components/AgentPlan/AgentPlan.d.ts +41 -0
- package/components/AgentPlan/AgentPlan.js +99 -0
- package/components/AgentStatus/AgentStatus.css +26 -11
- package/components/Card/Card.css +9 -0
- package/components/Card/Card.d.ts +6 -0
- package/components/Card/Card.js +2 -1
- package/components/Composer/Composer.css +18 -0
- package/components/Composer/Composer.d.ts +3 -0
- package/components/Composer/Composer.js +12 -1
- 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/Prose/Prose.css +18 -1
- package/components/Reasoning/Reasoning.css +19 -4
- package/components/ShaderField/ShaderField.css +24 -0
- package/components/ShaderField/ShaderField.d.ts +68 -0
- package/components/ShaderField/ShaderField.js +54 -0
- package/components/ShaderField/field.glsl.d.ts +39 -0
- package/components/ShaderField/field.glsl.js +178 -0
- package/components/ShaderField/useShaderField.d.ts +103 -0
- package/components/ShaderField/useShaderField.js +409 -0
- 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 +48 -0
- package/components/registry.json.d.ts +48 -0
- package/components/registry.json.js +1 -1
- package/index.d.ts +6 -0
- package/index.js +19 -0
- package/package.json +5 -1
- package/tokens/registry.json +1 -0
- package/tokens/registry.json.d.ts +1 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +1 -1
- package/tokens/tokens-light.css +2 -2
- package/tokens/tokens-motion.css +2 -0
|
@@ -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
|
+
};
|
|
@@ -192,8 +192,17 @@
|
|
|
192
192
|
TABLES
|
|
193
193
|
============================================ */
|
|
194
194
|
|
|
195
|
+
/* The table is its own scroll container. Prose styles markup it does not
|
|
196
|
+
render, so it cannot wrap the table in a scrolling div — `display: block`
|
|
197
|
+
makes the table element itself the scroller. The cost is deliberate: the
|
|
198
|
+
table sizes to its content instead of filling the container, because the
|
|
199
|
+
inner anonymous table box a block table generates cannot be stretched.
|
|
200
|
+
A clipped or shattered table is the worse failure. */
|
|
195
201
|
.ds-prose table {
|
|
196
|
-
|
|
202
|
+
display: block;
|
|
203
|
+
width: max-content;
|
|
204
|
+
max-width: 100%;
|
|
205
|
+
overflow-x: auto;
|
|
197
206
|
border-collapse: collapse;
|
|
198
207
|
}
|
|
199
208
|
|
|
@@ -207,10 +216,18 @@
|
|
|
207
216
|
text-align: start;
|
|
208
217
|
}
|
|
209
218
|
|
|
219
|
+
/* Cells never break words, whatever the surrounding context does. An
|
|
220
|
+
ancestor that sets `overflow-wrap: anywhere` (ChatMessage does, so a long
|
|
221
|
+
URL cannot widen a bubble) otherwise inherits in and collapses every
|
|
222
|
+
column's minimum width to one character, which lets auto table layout
|
|
223
|
+
squeeze a heading into "Co / m / pa / ny" rather than let the table reach
|
|
224
|
+
its natural width. Holding the floor is what gives the scroll above
|
|
225
|
+
something to scroll. */
|
|
210
226
|
.ds-prose th,
|
|
211
227
|
.ds-prose td {
|
|
212
228
|
padding: var(--padding-xs) var(--padding-sm-md);
|
|
213
229
|
border-block-end: var(--border-xs) solid var(--color-divider);
|
|
230
|
+
overflow-wrap: normal;
|
|
214
231
|
}
|
|
215
232
|
|
|
216
233
|
/* ============================================
|
|
@@ -128,12 +128,16 @@
|
|
|
128
128
|
align-items: center;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/* The band travels dim → bright → dim, and stays narrow, matching
|
|
132
|
+
AgentStatus's sweep stop for stop: the two sit in the same conversation,
|
|
133
|
+
so a shimmer that read differently here would look like a second effect
|
|
134
|
+
rather than the same one. */
|
|
131
135
|
.ds-reasoning--streaming .ds-reasoning__summary:not(.ds-reasoning__summary--node) {
|
|
132
136
|
background-image: linear-gradient(
|
|
133
137
|
100deg,
|
|
134
|
-
var(--color-text-
|
|
135
|
-
var(--color-text-
|
|
136
|
-
var(--color-text-
|
|
138
|
+
var(--color-text-tertiary) 44%,
|
|
139
|
+
var(--color-text-primary) 50%,
|
|
140
|
+
var(--color-text-tertiary) 56%
|
|
137
141
|
);
|
|
138
142
|
background-size: 250% 100%;
|
|
139
143
|
background-clip: text;
|
|
@@ -183,7 +187,18 @@
|
|
|
183
187
|
the thought-for line never shifts the trace. */
|
|
184
188
|
margin-left: calc(var(--icon-size-md) / 2 - var(--border-md) / 2);
|
|
185
189
|
padding-left: calc(var(--icon-size-md) / 2 + var(--gap-sm-md) - var(--border-md) / 2);
|
|
186
|
-
|
|
190
|
+
/* The rail dissolves at its foot — the Timeline's end-of-line treatment.
|
|
191
|
+
A border can't carry a gradient, so the border only reserves the
|
|
192
|
+
geometry (transparent, same width) and the stripe is painted under it:
|
|
193
|
+
a border-box background sized to the border's own column. */
|
|
194
|
+
border-left: var(--border-md) solid transparent;
|
|
195
|
+
background:
|
|
196
|
+
linear-gradient(
|
|
197
|
+
to bottom,
|
|
198
|
+
var(--color-divider) calc(100% - var(--gap-lg) * 2),
|
|
199
|
+
var(--color-bg-container-primary-transparent)
|
|
200
|
+
)
|
|
201
|
+
left top / var(--border-md) 100% no-repeat border-box;
|
|
187
202
|
color: var(--color-text-secondary);
|
|
188
203
|
font-family: var(--font-paragraph-family);
|
|
189
204
|
font-size: var(--font-paragraph-size);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* ShaderField — the canvas only.
|
|
2
|
+
Placement, clipping and any fade-to-page mask belong to the positioned
|
|
3
|
+
ancestor the caller provides: where the field sits is a layout decision,
|
|
4
|
+
and baking one in would make the component unusable anywhere else. */
|
|
5
|
+
|
|
6
|
+
.ds-shader-field {
|
|
7
|
+
position: absolute;
|
|
8
|
+
inset: 0;
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
display: block;
|
|
12
|
+
/* Scenery, never a target — a full-bleed canvas that swallowed clicks would
|
|
13
|
+
make whatever it covers unreachable. */
|
|
14
|
+
pointer-events: none;
|
|
15
|
+
/* Reveals only once a context is live and the first frame is drawn, so a
|
|
16
|
+
machine that cannot run the shader never shows an empty canvas — it stays
|
|
17
|
+
on the caller's fallback instead. */
|
|
18
|
+
opacity: 0;
|
|
19
|
+
transition: opacity var(--motion-duration-slower) var(--motion-ease-entrance);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.ds-shader-field[data-status='active'] {
|
|
23
|
+
opacity: 1;
|
|
24
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { ShaderBlob, ShaderParams } from './useShaderField';
|
|
3
|
+
export type { ShaderBlob, ShaderParams, ShaderFieldParams, ShaderFieldState, } from './useShaderField';
|
|
4
|
+
export { useShaderField, DEFAULT_SHADER_BLOBS, DEFAULT_SHADER_PARAMS } from './useShaderField';
|
|
5
|
+
export { BLOB_COUNT } from './field.glsl';
|
|
6
|
+
/**
|
|
7
|
+
* How the renderer has resolved.
|
|
8
|
+
*
|
|
9
|
+
* `pending` is the one that matters to a caller: the context is still coming
|
|
10
|
+
* up, so neither the field nor a fallback should be painted yet — showing a
|
|
11
|
+
* fallback here and swapping it a frame later reads as two backgrounds loading
|
|
12
|
+
* in sequence.
|
|
13
|
+
*/
|
|
14
|
+
export type ShaderFieldStatus = 'pending' | 'active' | 'unavailable';
|
|
15
|
+
/** Props owned by ShaderField itself — everything else falls through to the canvas. */
|
|
16
|
+
type ShaderFieldOwnProps = {
|
|
17
|
+
/**
|
|
18
|
+
* Field parameters, merged over the shipped defaults, so passing one
|
|
19
|
+
* property changes one thing. Changing them never rebuilds the GL state.
|
|
20
|
+
*/
|
|
21
|
+
params?: Partial<ShaderParams>;
|
|
22
|
+
/**
|
|
23
|
+
* The light sources. Defaults to an eight-source composition on the core
|
|
24
|
+
* accent tokens. Fewer than `BLOB_COUNT` entries is fine — unused slots park
|
|
25
|
+
* off-field; more are ignored, because the shader's uniform arrays are
|
|
26
|
+
* fixed-size.
|
|
27
|
+
*/
|
|
28
|
+
blobs?: readonly ShaderBlob[];
|
|
29
|
+
/**
|
|
30
|
+
* False keeps WebGL entirely unmounted and reports `unavailable`, so the
|
|
31
|
+
* caller's fallback is the whole background. This is the kill switch: a
|
|
32
|
+
* config lever, or an A/B against the fallback.
|
|
33
|
+
*/
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Called whenever the renderer's status changes. Drive the fallback layer's
|
|
37
|
+
* visibility from this — the component deliberately does not own one, since
|
|
38
|
+
* what to paint instead is a design decision, not a rendering one.
|
|
39
|
+
*/
|
|
40
|
+
onStatusChange?: (status: ShaderFieldStatus) => void;
|
|
41
|
+
/** Additional CSS classes */
|
|
42
|
+
className?: string;
|
|
43
|
+
};
|
|
44
|
+
export interface ShaderFieldProps extends ShaderFieldOwnProps, Omit<React.ComponentPropsWithoutRef<'canvas'>, keyof ShaderFieldOwnProps> {
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* ShaderField paints an ambient WebGL2 field of soft Gaussian light sources,
|
|
48
|
+
* each sampling a semantic colour token at runtime — so the whole thing
|
|
49
|
+
* re-themes with `data-theme`, and with any scoped custom-property override,
|
|
50
|
+
* without the caller wiring anything up.
|
|
51
|
+
*
|
|
52
|
+
* It is scenery, not interface: nothing about it is a signal, so its motion
|
|
53
|
+
* cannot be mistaken for feedback. Two consequences follow, and both are
|
|
54
|
+
* handled here rather than left to the caller.
|
|
55
|
+
*
|
|
56
|
+
* It honours reduced motion in JavaScript. The system's motion tokens collapse
|
|
57
|
+
* under `prefers-reduced-motion`, but that guard is CSS-only and cannot see a
|
|
58
|
+
* `requestAnimationFrame` loop; a GPU-driven surface has to check the query
|
|
59
|
+
* itself. This one renders a single static frame and never starts its loop.
|
|
60
|
+
*
|
|
61
|
+
* And it always reports how it resolved. No WebGL2, a blocked or lost GPU
|
|
62
|
+
* context, a renderer that stalls before its first frame, or `enabled={false}`
|
|
63
|
+
* all arrive at `unavailable` through `onStatusChange`, so the caller can keep
|
|
64
|
+
* a plain CSS fallback painted underneath. The canvas fills its positioned
|
|
65
|
+
* ancestor and is inert to pointer events; give that ancestor the placement,
|
|
66
|
+
* the clipping and any mask.
|
|
67
|
+
*/
|
|
68
|
+
export declare const ShaderField: React.ForwardRefExoticComponent<ShaderFieldProps & React.RefAttributes<HTMLCanvasElement>>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useRef, useMemo, useState, useEffect } from "react";
|
|
3
|
+
import { DEFAULT_SHADER_BLOBS, DEFAULT_SHADER_PARAMS, useShaderField } from "./useShaderField.js";
|
|
4
|
+
import "./ShaderField.css";
|
|
5
|
+
const WATCHDOG_MS = 1500;
|
|
6
|
+
const ShaderField = React.forwardRef(
|
|
7
|
+
({
|
|
8
|
+
params,
|
|
9
|
+
blobs = DEFAULT_SHADER_BLOBS,
|
|
10
|
+
enabled = true,
|
|
11
|
+
onStatusChange,
|
|
12
|
+
className = "",
|
|
13
|
+
...rest
|
|
14
|
+
}, ref) => {
|
|
15
|
+
const canvasRef = useRef(null);
|
|
16
|
+
React.useImperativeHandle(ref, () => canvasRef.current);
|
|
17
|
+
const fieldParams = useMemo(
|
|
18
|
+
() => ({ ...DEFAULT_SHADER_PARAMS, ...params, blobs }),
|
|
19
|
+
[params, blobs]
|
|
20
|
+
);
|
|
21
|
+
const { supported, active } = useShaderField(canvasRef, fieldParams, enabled);
|
|
22
|
+
const [gaveUp, setGaveUp] = useState(false);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!enabled || active || !supported) return;
|
|
25
|
+
const timer = window.setTimeout(() => setGaveUp(true), WATCHDOG_MS);
|
|
26
|
+
return () => window.clearTimeout(timer);
|
|
27
|
+
}, [enabled, active, supported]);
|
|
28
|
+
const status = !enabled || !supported ? "unavailable" : active ? "active" : gaveUp ? "unavailable" : "pending";
|
|
29
|
+
const notify = useRef(onStatusChange);
|
|
30
|
+
notify.current = onStatusChange;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
notify.current?.(status);
|
|
33
|
+
}, [status]);
|
|
34
|
+
if (!enabled) return null;
|
|
35
|
+
const classes = ["ds-shader-field", className].filter(Boolean).join(" ");
|
|
36
|
+
return /* @__PURE__ */ jsx(
|
|
37
|
+
"canvas",
|
|
38
|
+
{
|
|
39
|
+
...rest,
|
|
40
|
+
ref: canvasRef,
|
|
41
|
+
className: classes,
|
|
42
|
+
"data-status": status,
|
|
43
|
+
"aria-hidden": "true"
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
ShaderField.displayName = "ShaderField";
|
|
49
|
+
export {
|
|
50
|
+
DEFAULT_SHADER_BLOBS,
|
|
51
|
+
DEFAULT_SHADER_PARAMS,
|
|
52
|
+
ShaderField,
|
|
53
|
+
useShaderField
|
|
54
|
+
};
|