@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.
Files changed (48) hide show
  1. package/LICENSE +53 -2
  2. package/README.md +36 -5
  3. package/components/AgentPlan/AgentPlan.css +231 -0
  4. package/components/AgentPlan/AgentPlan.d.ts +41 -0
  5. package/components/AgentPlan/AgentPlan.js +99 -0
  6. package/components/AgentStatus/AgentStatus.css +26 -11
  7. package/components/Card/Card.css +9 -0
  8. package/components/Card/Card.d.ts +6 -0
  9. package/components/Card/Card.js +2 -1
  10. package/components/Composer/Composer.css +18 -0
  11. package/components/Composer/Composer.d.ts +3 -0
  12. package/components/Composer/Composer.js +12 -1
  13. package/components/DataTable/DataTable.css +105 -0
  14. package/components/DataTable/DataTable.d.ts +81 -0
  15. package/components/DataTable/DataTable.js +235 -0
  16. package/components/EventCalendar/EventCalendar.css +328 -0
  17. package/components/EventCalendar/EventCalendar.d.ts +51 -0
  18. package/components/EventCalendar/EventCalendar.js +213 -0
  19. package/components/ModelPicker/ModelPicker.css +238 -0
  20. package/components/ModelPicker/ModelPicker.d.ts +60 -0
  21. package/components/ModelPicker/ModelPicker.js +217 -0
  22. package/components/NotificationCenter/NotificationCenter.css +262 -0
  23. package/components/NotificationCenter/NotificationCenter.d.ts +72 -0
  24. package/components/NotificationCenter/NotificationCenter.js +128 -0
  25. package/components/Prose/Prose.css +18 -1
  26. package/components/Reasoning/Reasoning.css +19 -4
  27. package/components/ShaderField/ShaderField.css +24 -0
  28. package/components/ShaderField/ShaderField.d.ts +68 -0
  29. package/components/ShaderField/ShaderField.js +54 -0
  30. package/components/ShaderField/field.glsl.d.ts +39 -0
  31. package/components/ShaderField/field.glsl.js +178 -0
  32. package/components/ShaderField/useShaderField.d.ts +103 -0
  33. package/components/ShaderField/useShaderField.js +409 -0
  34. package/components/Timeline/Timeline.css +17 -0
  35. package/components/Timeline/Timeline.d.ts +2 -0
  36. package/components/Timeline/Timeline.js +1 -0
  37. package/components/registry.json +48 -0
  38. package/components/registry.json.d.ts +48 -0
  39. package/components/registry.json.js +1 -1
  40. package/index.d.ts +6 -0
  41. package/index.js +19 -0
  42. package/package.json +5 -1
  43. package/tokens/registry.json +1 -0
  44. package/tokens/registry.json.d.ts +1 -0
  45. package/tokens/registry.json.js +1 -1
  46. package/tokens/tokens-dark.css +1 -1
  47. package/tokens/tokens-light.css +2 -2
  48. 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
- width: 100%;
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-primary) 35%,
135
- var(--color-text-tertiary) 50%,
136
- var(--color-text-primary) 65%
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
- border-left: var(--border-md) solid var(--color-divider);
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
+ };