rift-ds 1.5.0 → 1.7.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 +8 -10
- package/bin/rift-ds.mjs +111 -37
- package/components/AgentRail/AgentRail.css +2 -2
- package/components/AppSidebar/AppSidebar.css +6 -0
- package/components/AppSidebar/AppSidebar.d.ts +12 -2
- package/components/AppSidebar/AppSidebar.js +11 -1
- package/components/Attachment/Attachment.css +14 -0
- package/components/Attachment/AttachmentGroup.d.ts +2 -2
- package/components/Attachment/AttachmentGroup.js +7 -2
- package/components/Composer/Composer.css +88 -4
- package/components/Composer/Composer.d.ts +21 -0
- package/components/Composer/Composer.js +199 -14
- package/components/Mention/Mention.css +195 -0
- package/components/Mention/Mention.d.ts +68 -0
- package/components/Mention/Mention.js +145 -0
- package/components/Mention/mentions.d.ts +81 -0
- package/components/Mention/mentions.js +83 -0
- package/components/PixelAvatar/PixelAvatar.css +79 -0
- package/components/PixelAvatar/PixelAvatar.d.ts +47 -0
- package/components/PixelAvatar/PixelAvatar.js +90 -0
- package/components/PixelAvatar/pixelInks.d.ts +27 -0
- package/components/PixelAvatar/pixelInks.js +45 -0
- package/components/PromptSuggestions/PromptSuggestions.css +218 -0
- package/components/PromptSuggestions/PromptSuggestions.d.ts +10 -3
- package/components/PromptSuggestions/PromptSuggestions.js +26 -3
- package/components/ShaderField/ShaderField.d.ts +3 -3
- package/components/SourceChip/SourceChip.css +211 -11
- package/components/SourceChip/SourceChip.d.ts +33 -10
- package/components/SourceChip/SourceChip.js +174 -22
- package/components/StatusDot/StatusDot.css +36 -13
- package/components/StatusDot/StatusDot.d.ts +16 -4
- package/components/StatusDot/StatusDot.js +25 -5
- package/components/ThreadPanel/ThreadPanel.css +26 -11
- package/components/ThreadPanel/ThreadPanel.d.ts +18 -4
- package/components/ThreadPanel/ThreadPanel.js +52 -10
- package/components/ThreadTabs/ThreadTabs.css +18 -8
- package/components/ThreadTabs/ThreadTabs.d.ts +8 -2
- package/components/ThreadTabs/ThreadTabs.js +24 -5
- package/components/Widget/Widget.css +207 -0
- package/components/Widget/Widget.d.ts +83 -0
- package/components/Widget/Widget.js +112 -0
- package/components/registry.json +26 -2
- package/components/registry.json.d.ts +26 -2
- package/components/registry.json.js +1 -1
- package/index.d.ts +3 -0
- package/index.js +21 -0
- package/package.json +5 -1
- package/tokens/registry.json +6 -0
- package/tokens/registry.json.d.ts +6 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +9 -0
- package/tokens/tokens-light.css +14 -0
|
@@ -1,30 +1,182 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
|
+
import React, { useState, useRef, useId, useEffect, useLayoutEffect } from "react";
|
|
3
4
|
import "./SourceChip.css";
|
|
4
5
|
import "../../fonts/material-symbols.css";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
import { MOTION_HOVER_SHOW_DELAY_MS, MOTION_HOVER_HIDE_DELAY_MS } from "../../tokens/motion.js";
|
|
7
|
+
const PREVIEW_EDGE_MARGIN = 8;
|
|
8
|
+
let closeOpenPreview = null;
|
|
9
|
+
function clipBounds(el) {
|
|
10
|
+
const bounds = { top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight };
|
|
11
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
12
|
+
const { overflowX, overflowY } = getComputedStyle(node);
|
|
13
|
+
const rect = node.getBoundingClientRect();
|
|
14
|
+
if (overflowX !== "visible") {
|
|
15
|
+
bounds.left = Math.max(bounds.left, rect.left);
|
|
16
|
+
bounds.right = Math.min(bounds.right, rect.right);
|
|
17
|
+
}
|
|
18
|
+
if (overflowY !== "visible") {
|
|
19
|
+
bounds.top = Math.max(bounds.top, rect.top);
|
|
20
|
+
bounds.bottom = Math.min(bounds.bottom, rect.bottom);
|
|
16
21
|
}
|
|
17
|
-
return /* @__PURE__ */ jsx(
|
|
18
|
-
"span",
|
|
19
|
-
{
|
|
20
|
-
...rest,
|
|
21
|
-
ref,
|
|
22
|
-
className: classes,
|
|
23
|
-
children: content
|
|
24
|
-
}
|
|
25
|
-
);
|
|
26
22
|
}
|
|
27
|
-
|
|
23
|
+
return bounds;
|
|
24
|
+
}
|
|
25
|
+
const SourceChip = React.forwardRef(({ title, index, icon = "language", href, excerpt, source, logo, meta, className = "", ...rest }, ref) => {
|
|
26
|
+
const baseClass = "ds-source-chip";
|
|
27
|
+
const hasPreview = Boolean(excerpt);
|
|
28
|
+
const [visible, setVisible] = useState(false);
|
|
29
|
+
const showTimeoutRef = useRef(void 0);
|
|
30
|
+
const hideTimeoutRef = useRef(void 0);
|
|
31
|
+
const hostRef = useRef(null);
|
|
32
|
+
const panelRef = useRef(null);
|
|
33
|
+
const panelId = useId();
|
|
34
|
+
const clearTimers = () => {
|
|
35
|
+
clearTimeout(showTimeoutRef.current);
|
|
36
|
+
clearTimeout(hideTimeoutRef.current);
|
|
37
|
+
};
|
|
38
|
+
const show = () => {
|
|
39
|
+
clearTimeout(hideTimeoutRef.current);
|
|
40
|
+
showTimeoutRef.current = setTimeout(() => setVisible(true), MOTION_HOVER_SHOW_DELAY_MS);
|
|
41
|
+
};
|
|
42
|
+
const hide = () => {
|
|
43
|
+
clearTimeout(showTimeoutRef.current);
|
|
44
|
+
hideTimeoutRef.current = setTimeout(() => setVisible(false), MOTION_HOVER_HIDE_DELAY_MS);
|
|
45
|
+
};
|
|
46
|
+
const showNow = () => {
|
|
47
|
+
clearTimers();
|
|
48
|
+
setVisible(true);
|
|
49
|
+
};
|
|
50
|
+
useEffect(() => clearTimers, []);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!visible) return;
|
|
53
|
+
const close = () => {
|
|
54
|
+
clearTimers();
|
|
55
|
+
setVisible(false);
|
|
56
|
+
};
|
|
57
|
+
closeOpenPreview?.();
|
|
58
|
+
closeOpenPreview = close;
|
|
59
|
+
const onKeyDown = (e) => {
|
|
60
|
+
if (e.key === "Escape") close();
|
|
61
|
+
};
|
|
62
|
+
const onPointerDown = (e) => {
|
|
63
|
+
if (!hostRef.current?.contains(e.target)) close();
|
|
64
|
+
};
|
|
65
|
+
document.addEventListener("keydown", onKeyDown);
|
|
66
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
67
|
+
return () => {
|
|
68
|
+
if (closeOpenPreview === close) closeOpenPreview = null;
|
|
69
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
70
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
71
|
+
};
|
|
72
|
+
}, [visible]);
|
|
73
|
+
useLayoutEffect(() => {
|
|
74
|
+
const host = hostRef.current;
|
|
75
|
+
const panel = panelRef.current;
|
|
76
|
+
if (!visible || !host || !panel) return;
|
|
77
|
+
const bounds = clipBounds(host);
|
|
78
|
+
const anchor = host.getBoundingClientRect();
|
|
79
|
+
const width = panel.offsetWidth;
|
|
80
|
+
const height = panel.offsetHeight;
|
|
81
|
+
const roomAbove = anchor.top - bounds.top - PREVIEW_EDGE_MARGIN;
|
|
82
|
+
const roomBelow = bounds.bottom - anchor.bottom - PREVIEW_EDGE_MARGIN;
|
|
83
|
+
const side = roomAbove >= height || roomAbove >= roomBelow ? "top" : "bottom";
|
|
84
|
+
const minLeft = bounds.left + PREVIEW_EDGE_MARGIN;
|
|
85
|
+
const maxLeft = bounds.right - PREVIEW_EDGE_MARGIN - width;
|
|
86
|
+
const left = Math.max(minLeft, Math.min(anchor.left, maxLeft));
|
|
87
|
+
panel.dataset.side = side;
|
|
88
|
+
panel.style.setProperty("--ds-source-chip-shift", `${Math.round(left - anchor.left)}px`);
|
|
89
|
+
}, [visible]);
|
|
90
|
+
const element = href ? "a" : hasPreview ? "button" : "span";
|
|
91
|
+
const classes = [
|
|
92
|
+
baseClass,
|
|
93
|
+
href ? `${baseClass}--link` : "",
|
|
94
|
+
element !== "span" ? `${baseClass}--interactive` : "",
|
|
95
|
+
visible ? `${baseClass}--open` : "",
|
|
96
|
+
className
|
|
97
|
+
].filter(Boolean).join(" ");
|
|
98
|
+
const logoMark = logo ? typeof logo === "string" ? /* @__PURE__ */ jsx("img", { src: logo, alt: "" }) : logo : null;
|
|
99
|
+
const leading = index !== void 0 ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__index`, children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__numeral`, children: index }) }) : logoMark ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__logo`, "aria-hidden": "true", children: logoMark }) : icon ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: icon }) : icon }) : null;
|
|
100
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
101
|
+
leading,
|
|
102
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title })
|
|
103
|
+
] });
|
|
104
|
+
const describedBy = hasPreview ? panelId : void 0;
|
|
105
|
+
const chip = element === "a" ? /* @__PURE__ */ jsx(
|
|
106
|
+
"a",
|
|
107
|
+
{
|
|
108
|
+
"aria-describedby": describedBy,
|
|
109
|
+
...rest,
|
|
110
|
+
ref,
|
|
111
|
+
className: classes,
|
|
112
|
+
href,
|
|
113
|
+
children: content
|
|
114
|
+
}
|
|
115
|
+
) : element === "button" ? /* @__PURE__ */ jsx(
|
|
116
|
+
"button",
|
|
117
|
+
{
|
|
118
|
+
type: "button",
|
|
119
|
+
"aria-describedby": describedBy,
|
|
120
|
+
...rest,
|
|
121
|
+
ref,
|
|
122
|
+
className: classes,
|
|
123
|
+
children: content
|
|
124
|
+
}
|
|
125
|
+
) : /* @__PURE__ */ jsx(
|
|
126
|
+
"span",
|
|
127
|
+
{
|
|
128
|
+
...rest,
|
|
129
|
+
ref,
|
|
130
|
+
className: classes,
|
|
131
|
+
children: content
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
if (!hasPreview) return chip;
|
|
135
|
+
const handleBlur = (event) => {
|
|
136
|
+
if (!event.currentTarget.contains(event.relatedTarget)) hide();
|
|
137
|
+
};
|
|
138
|
+
return /* @__PURE__ */ jsxs(
|
|
139
|
+
"span",
|
|
140
|
+
{
|
|
141
|
+
ref: hostRef,
|
|
142
|
+
className: `${baseClass}-host`,
|
|
143
|
+
onMouseEnter: show,
|
|
144
|
+
onMouseLeave: hide,
|
|
145
|
+
onFocus: show,
|
|
146
|
+
onBlur: handleBlur,
|
|
147
|
+
onClick: showNow,
|
|
148
|
+
children: [
|
|
149
|
+
chip,
|
|
150
|
+
/* @__PURE__ */ jsxs(
|
|
151
|
+
"span",
|
|
152
|
+
{
|
|
153
|
+
ref: panelRef,
|
|
154
|
+
id: panelId,
|
|
155
|
+
role: "tooltip",
|
|
156
|
+
className: `${baseClass}__preview${visible ? ` ${baseClass}__preview--visible` : ""}`,
|
|
157
|
+
children: [
|
|
158
|
+
(logo || source || meta || href) && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__preview-top`, children: [
|
|
159
|
+
logoMark && /* @__PURE__ */ jsx("span", { className: `${baseClass}__logo`, "aria-hidden": "true", children: logoMark }),
|
|
160
|
+
source && /* @__PURE__ */ jsx("span", { className: `${baseClass}__preview-source`, children: source }),
|
|
161
|
+
meta && /* @__PURE__ */ jsx("span", { className: `${baseClass}__preview-meta`, children: meta }),
|
|
162
|
+
href && /* @__PURE__ */ jsx(
|
|
163
|
+
"span",
|
|
164
|
+
{
|
|
165
|
+
className: `${baseClass}__preview-open material-symbols-rounded`,
|
|
166
|
+
"aria-hidden": "true",
|
|
167
|
+
children: "arrow_outward"
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
] }),
|
|
171
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__preview-title`, children: title }),
|
|
172
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__preview-excerpt`, children: excerpt })
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
});
|
|
28
180
|
SourceChip.displayName = "SourceChip";
|
|
29
181
|
export {
|
|
30
182
|
SourceChip
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* ============================================
|
|
2
2
|
STATUS DOT COMPONENT
|
|
3
3
|
Bare presence/status indicator with optional
|
|
4
|
-
label and live pulse
|
|
4
|
+
label, hollow outline and live pulse
|
|
5
5
|
============================================ */
|
|
6
6
|
|
|
7
7
|
/* Base */
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
position: relative;
|
|
17
17
|
flex-shrink: 0;
|
|
18
18
|
border-radius: var(--radius-pill);
|
|
19
|
+
background-color: var(--ds-status-dot-color);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
.ds-status-dot__label {
|
|
@@ -29,7 +30,14 @@
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/* Sizes — the dot rides the icon scale at half-diameter, so it can never
|
|
32
|
-
drift from the glyphs it sits beside.
|
|
33
|
+
drift from the glyphs it sits beside. `xs` is the one step off it: the
|
|
34
|
+
list-row mark sits on the gap scale, because no icon token is that small
|
|
35
|
+
and 8px matches a title's cap height. */
|
|
36
|
+
|
|
37
|
+
.ds-status-dot--xs .ds-status-dot__indicator {
|
|
38
|
+
width: var(--gap-200); /* 8px */
|
|
39
|
+
height: var(--gap-200);
|
|
40
|
+
}
|
|
33
41
|
|
|
34
42
|
.ds-status-dot--sm .ds-status-dot__indicator {
|
|
35
43
|
width: calc(var(--icon-size-500) / 2); /* 10px */
|
|
@@ -49,24 +57,39 @@
|
|
|
49
57
|
/* Variants — the plain-surface icon steps, tuned to hold on bare
|
|
50
58
|
containers where the status text tints wash out. */
|
|
51
59
|
|
|
52
|
-
.ds-status-dot--info
|
|
53
|
-
|
|
60
|
+
.ds-status-dot--info {
|
|
61
|
+
--ds-status-dot-color: var(--color-status-info-icon);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.ds-status-dot--positive {
|
|
65
|
+
--ds-status-dot-color: var(--color-status-positive-icon);
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
.ds-status-dot--
|
|
57
|
-
|
|
68
|
+
.ds-status-dot--warning {
|
|
69
|
+
--ds-status-dot-color: var(--color-status-warning-icon);
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
.ds-status-dot--
|
|
61
|
-
|
|
72
|
+
.ds-status-dot--error {
|
|
73
|
+
--ds-status-dot-color: var(--color-status-error-icon);
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
.ds-status-dot--
|
|
65
|
-
|
|
76
|
+
.ds-status-dot--neutral {
|
|
77
|
+
--ds-status-dot-color: var(--color-status-neutral-icon);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Outline — the hollow dot: the role's colour drawn as an inset ring, so
|
|
81
|
+
the mark keeps its box and its seat while reading as "nothing here yet".
|
|
82
|
+
The two small sizes take the hairline; the larger two the heavier step,
|
|
83
|
+
so the ring keeps its proportion to the diameter. */
|
|
84
|
+
|
|
85
|
+
.ds-status-dot--outline .ds-status-dot__indicator {
|
|
86
|
+
background-color: transparent;
|
|
87
|
+
box-shadow: inset 0 0 0 var(--border-025) var(--ds-status-dot-color);
|
|
66
88
|
}
|
|
67
89
|
|
|
68
|
-
.ds-status-dot--
|
|
69
|
-
|
|
90
|
+
.ds-status-dot--outline.ds-status-dot--md .ds-status-dot__indicator,
|
|
91
|
+
.ds-status-dot--outline.ds-status-dot--lg .ds-status-dot__indicator {
|
|
92
|
+
box-shadow: inset 0 0 0 var(--border-050) var(--ds-status-dot-color);
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
/* Pulse — a ring of the dot's own colour swelling out and fading, on the
|
|
@@ -77,7 +100,7 @@
|
|
|
77
100
|
position: absolute;
|
|
78
101
|
inset: 0;
|
|
79
102
|
border-radius: var(--radius-pill);
|
|
80
|
-
background-color:
|
|
103
|
+
background-color: var(--ds-status-dot-color);
|
|
81
104
|
animation: ds-status-dot-pulse var(--motion-duration-loop-shimmer) var(--motion-ease-standard)
|
|
82
105
|
infinite;
|
|
83
106
|
}
|
|
@@ -3,8 +3,14 @@ import { default as React } from 'react';
|
|
|
3
3
|
type StatusDotOwnProps = {
|
|
4
4
|
/** Status role the dot carries — coloured through the plain-surface `--color-status-*-icon` steps. */
|
|
5
5
|
variant?: 'info' | 'positive' | 'warning' | 'error' | 'neutral';
|
|
6
|
-
/** Dot diameter, derived from the icon scale (half of `--icon-size-500/
|
|
7
|
-
size?: 'sm' | 'md' | 'lg';
|
|
6
|
+
/** Dot diameter, derived from the icon scale (half of `--icon-size-500/600/800`); `xs` is the 8px list-row mark, sized on the gap scale. */
|
|
7
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
8
|
+
/**
|
|
9
|
+
* Draws the dot hollow: a ring in the role's colour around an empty
|
|
10
|
+
* centre, for a settled or empty state that still holds its seat beside
|
|
11
|
+
* filled dots.
|
|
12
|
+
*/
|
|
13
|
+
outline?: boolean;
|
|
8
14
|
/**
|
|
9
15
|
* Radiates a repeating ring from the dot for a live state — recording,
|
|
10
16
|
* online now, deploy in flight. The ring stills under reduced motion.
|
|
@@ -16,6 +22,12 @@ type StatusDotOwnProps = {
|
|
|
16
22
|
* a colour alone announces nothing.
|
|
17
23
|
*/
|
|
18
24
|
label?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Renders the dot as decoration: no `role="status"`, hidden from assistive
|
|
27
|
+
* technology. For a dot repeated down a list, where the host announces the
|
|
28
|
+
* state in the row's own text and a live region per row would be noise.
|
|
29
|
+
*/
|
|
30
|
+
decorative?: boolean;
|
|
19
31
|
/** Additional CSS classes */
|
|
20
32
|
className?: string;
|
|
21
33
|
};
|
|
@@ -23,8 +35,8 @@ export interface StatusDotProps extends StatusDotOwnProps, Omit<React.ComponentP
|
|
|
23
35
|
}
|
|
24
36
|
/**
|
|
25
37
|
* StatusDot — the bare presence/status indicator: a small dot in one of the
|
|
26
|
-
* five status roles, with an optional visible label and
|
|
27
|
-
* pulse. Where Badge carries a text label on a tinted fill, StatusDot is the
|
|
38
|
+
* five status roles, filled or hollow, with an optional visible label and
|
|
39
|
+
* an optional live pulse. Where Badge carries a text label on a tinted fill, StatusDot is the
|
|
28
40
|
* mark alone — for table rows, avatars, nav items, and anywhere a full badge
|
|
29
41
|
* is too loud.
|
|
30
42
|
*
|
|
@@ -2,19 +2,39 @@ import { jsxs, jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import "./StatusDot.css";
|
|
4
4
|
const StatusDot = React.forwardRef(
|
|
5
|
-
({
|
|
5
|
+
({
|
|
6
|
+
variant = "neutral",
|
|
7
|
+
size = "md",
|
|
8
|
+
outline = false,
|
|
9
|
+
pulse = false,
|
|
10
|
+
label,
|
|
11
|
+
decorative = false,
|
|
12
|
+
className = "",
|
|
13
|
+
...rest
|
|
14
|
+
}, ref) => {
|
|
6
15
|
const baseClass = "ds-status-dot";
|
|
7
16
|
const classes = [
|
|
8
17
|
baseClass,
|
|
9
18
|
`${baseClass}--${variant}`,
|
|
10
19
|
`${baseClass}--${size}`,
|
|
20
|
+
outline && `${baseClass}--outline`,
|
|
11
21
|
pulse && `${baseClass}--pulse`,
|
|
12
22
|
className
|
|
13
23
|
].filter(Boolean).join(" ");
|
|
14
|
-
return /* @__PURE__ */ jsxs(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
return /* @__PURE__ */ jsxs(
|
|
25
|
+
"span",
|
|
26
|
+
{
|
|
27
|
+
...rest,
|
|
28
|
+
ref,
|
|
29
|
+
className: classes,
|
|
30
|
+
role: decorative ? void 0 : "status",
|
|
31
|
+
"aria-hidden": decorative ? true : rest["aria-hidden"],
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__indicator`, "aria-hidden": "true" }),
|
|
34
|
+
label && /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: label })
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
);
|
|
18
38
|
}
|
|
19
39
|
);
|
|
20
40
|
StatusDot.displayName = "StatusDot";
|
|
@@ -396,6 +396,12 @@
|
|
|
396
396
|
flex-shrink: 0;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
/* A generated avatar in the icon seat: a block-level sprite, so the seat
|
|
400
|
+
lays it out as a box rather than on the text baseline. */
|
|
401
|
+
.ds-thread-panel__project-icon--avatar {
|
|
402
|
+
display: inline-flex;
|
|
403
|
+
}
|
|
404
|
+
|
|
399
405
|
.ds-thread-panel__project-label {
|
|
400
406
|
flex: 1;
|
|
401
407
|
min-width: 0;
|
|
@@ -525,15 +531,24 @@
|
|
|
525
531
|
row-gap: 0;
|
|
526
532
|
}
|
|
527
533
|
|
|
528
|
-
/* The
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
.ds-thread-panel__unread {
|
|
534
|
+
/* The status dot: a StatusDot at its 8px list-row size leading the title.
|
|
535
|
+
The panel seats one on every row or on none, so titles share an edge. */
|
|
536
|
+
.ds-thread-panel__status {
|
|
532
537
|
flex: none;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* The status read aloud: the dot is decorative, so its state rides in the
|
|
541
|
+
row's text for assistive technology only. */
|
|
542
|
+
.ds-thread-panel__status-label {
|
|
543
|
+
position: absolute;
|
|
544
|
+
width: 1px;
|
|
545
|
+
height: 1px;
|
|
546
|
+
padding: 0;
|
|
547
|
+
margin: -1px;
|
|
548
|
+
overflow: hidden;
|
|
549
|
+
clip: rect(0, 0, 0, 0);
|
|
550
|
+
white-space: nowrap;
|
|
551
|
+
border: 0;
|
|
537
552
|
}
|
|
538
553
|
|
|
539
554
|
.ds-thread-panel__title {
|
|
@@ -610,8 +625,8 @@
|
|
|
610
625
|
}
|
|
611
626
|
|
|
612
627
|
/* The description line: the caption face on the full second line, fading
|
|
613
|
-
at the trailing edge like the title above it. Indented past the
|
|
614
|
-
dot's column so both lines share a left edge.
|
|
628
|
+
at the trailing edge like the title above it. Indented past the status
|
|
629
|
+
dot's column, when the panel has one, so both lines share a left edge.
|
|
615
630
|
ds-allow(color): mask alpha keywords (black/transparent), not palette colours */
|
|
616
631
|
.ds-thread-panel__description {
|
|
617
632
|
flex-basis: 100%;
|
|
@@ -635,7 +650,7 @@
|
|
|
635
650
|
);
|
|
636
651
|
}
|
|
637
652
|
|
|
638
|
-
.ds-thread-
|
|
653
|
+
.ds-thread-panel--dotted .ds-thread-panel__description {
|
|
639
654
|
/* The dot's width plus the row's column gap. */
|
|
640
655
|
margin-left: calc(var(--gap-200) + var(--gap-200));
|
|
641
656
|
}
|
|
@@ -9,6 +9,14 @@ export interface ThreadPanelThreadAction {
|
|
|
9
9
|
/** Destructive styling (the error text colour) for delete-like actions. */
|
|
10
10
|
destructive?: boolean;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* What a thread's leading dot reports. Fill says whether the thread wants
|
|
14
|
+
* the reader, colour says what it wants, motion says it is live right now:
|
|
15
|
+
* `idle` is a hollow neutral ring, `working` a pulsing neutral dot, and
|
|
16
|
+
* `unread`, `waiting` and `error` solid dots in the info, warning and error
|
|
17
|
+
* roles.
|
|
18
|
+
*/
|
|
19
|
+
export type ThreadPanelThreadStatus = 'idle' | 'working' | 'unread' | 'waiting' | 'error';
|
|
12
20
|
export interface ThreadPanelThread {
|
|
13
21
|
/** Stable identifier: `activeThreadId` matches against it and `onThreadSelect` reports it. */
|
|
14
22
|
id: string;
|
|
@@ -24,7 +32,9 @@ export interface ThreadPanelThread {
|
|
|
24
32
|
actions?: ThreadPanelThreadAction[];
|
|
25
33
|
/** Quiet second line under the title in the caption face — a repo, an environment, a one-line summary. Overflows under the same trailing mask as the title. */
|
|
26
34
|
description?: string;
|
|
27
|
-
/** The thread
|
|
35
|
+
/** The thread's state, drawn as the dot leading the row: `idle` (nothing new, nothing running), `working` (an agent is running now), `unread` (finished, not yet seen), `waiting` (blocked on the reader) or `error` (stopped on a failure). Once any thread in the panel carries a status every row draws a dot, the rest reading `idle`, so the titles share one left edge. The dot is decorative; every state but `idle` is also read aloud through `statusLabels`. */
|
|
36
|
+
status?: ThreadPanelThreadStatus;
|
|
37
|
+
/** Legacy boolean for `status: 'unread'`, ignored when `status` is set. @deprecated Use `status: 'unread'` instead. */
|
|
28
38
|
unread?: boolean;
|
|
29
39
|
/** Trailing icon before the meta — Material Symbol name (string) or custom element (ReactNode), e.g. `cloud` for a session that lives remotely. */
|
|
30
40
|
icon?: string | React.ReactNode;
|
|
@@ -52,8 +62,10 @@ export interface ThreadPanelProject {
|
|
|
52
62
|
id: string;
|
|
53
63
|
/** The project's name. */
|
|
54
64
|
label: string;
|
|
55
|
-
/** Leading icon on the row — Material Symbol name (string) or custom element (ReactNode).
|
|
65
|
+
/** Leading icon on the row — Material Symbol name (string) or custom element (ReactNode). Omit it and the row draws the project's generated PixelAvatar, which is the default mark. */
|
|
56
66
|
icon?: string | React.ReactNode;
|
|
67
|
+
/** Seed for the generated PixelAvatar a project wears when it has no `icon`. Defaults to `label`; pass a stable id so a renamed project keeps its character. The panel spreads the inks across its projects, so neighbours differ in colour as well as shape. */
|
|
68
|
+
pixelAvatar?: string;
|
|
57
69
|
/** Small trailing annotation in the caption face, e.g. how recently the project was touched. */
|
|
58
70
|
meta?: string;
|
|
59
71
|
/** Optional href — the row renders as an `<a>` instead of a `<button>`. */
|
|
@@ -79,6 +91,8 @@ type ThreadPanelOwnProps = {
|
|
|
79
91
|
threadActions?: ThreadPanelThreadAction[];
|
|
80
92
|
/** Fires with the thread's id and the chosen action's id. */
|
|
81
93
|
onThreadAction?: (threadId: string, actionId: string) => void;
|
|
94
|
+
/** Spoken text for each thread status, read after the row's title; the dot itself is decorative. Merged over the English defaults (Working, Unread, Needs input, Failed). `idle` is silent unless given a label here. */
|
|
95
|
+
statusLabels?: Partial<Record<ThreadPanelThreadStatus, string>>;
|
|
82
96
|
/** Accessible name for a row's menu trigger; the thread's title is appended after it. */
|
|
83
97
|
threadMenuLabel?: string;
|
|
84
98
|
/** Id of the thread being renamed: its row swaps to an inline text field, prefilled with the title and selected. The host owns the state, like everything else. */
|
|
@@ -148,8 +162,8 @@ export interface ThreadPanelProps extends ThreadPanelOwnProps, Omit<React.Compon
|
|
|
148
162
|
* control rows, an optional projects section with its own new-project
|
|
149
163
|
* button, the grouped thread history (the one region that scrolls), and a
|
|
150
164
|
* footer for the profile row and host furniture like a theme toggle.
|
|
151
|
-
* Thread rows scale from a bare title to the full detail anatomy:
|
|
152
|
-
*
|
|
165
|
+
* Thread rows scale from a bare title to the full detail anatomy: a
|
|
166
|
+
* leading status dot, a quiet description line, a trailing glyph, a pin marker,
|
|
153
167
|
* and the meta caption.
|
|
154
168
|
*
|
|
155
169
|
* Fully controlled and stateless: the host owns the active thread, the
|
|
@@ -3,9 +3,25 @@ import React from "react";
|
|
|
3
3
|
import { Avatar } from "../Avatar/Avatar.js";
|
|
4
4
|
import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
|
|
5
5
|
import { Kbd } from "../Kbd/Kbd.js";
|
|
6
|
+
import { PixelAvatar } from "../PixelAvatar/PixelAvatar.js";
|
|
7
|
+
import { StatusDot } from "../StatusDot/StatusDot.js";
|
|
6
8
|
import "./ThreadPanel.css";
|
|
7
9
|
import "../../fonts/material-symbols.css";
|
|
10
|
+
import { distinctPixelInks } from "../PixelAvatar/pixelInks.js";
|
|
8
11
|
const baseClass = "ds-thread-panel";
|
|
12
|
+
const STATUS_DOTS = {
|
|
13
|
+
idle: { variant: "neutral", outline: true },
|
|
14
|
+
working: { variant: "neutral", pulse: true },
|
|
15
|
+
unread: { variant: "info" },
|
|
16
|
+
waiting: { variant: "warning" },
|
|
17
|
+
error: { variant: "error" }
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_STATUS_LABELS = {
|
|
20
|
+
working: "Working",
|
|
21
|
+
unread: "Unread",
|
|
22
|
+
waiting: "Needs input",
|
|
23
|
+
error: "Failed"
|
|
24
|
+
};
|
|
9
25
|
function ActionRow({
|
|
10
26
|
className,
|
|
11
27
|
href,
|
|
@@ -96,6 +112,7 @@ const ThreadPanel = React.forwardRef(
|
|
|
96
112
|
onThreadSelect,
|
|
97
113
|
threadActions,
|
|
98
114
|
onThreadAction,
|
|
115
|
+
statusLabels,
|
|
99
116
|
threadMenuLabel = "Thread options",
|
|
100
117
|
renamingThreadId,
|
|
101
118
|
onThreadRename,
|
|
@@ -128,12 +145,20 @@ const ThreadPanel = React.forwardRef(
|
|
|
128
145
|
className = "",
|
|
129
146
|
...rest
|
|
130
147
|
}, ref) => {
|
|
148
|
+
const dotted = groups.some(
|
|
149
|
+
(group) => group.threads.some((thread) => thread.status || thread.unread)
|
|
150
|
+
);
|
|
151
|
+
const labels = { ...DEFAULT_STATUS_LABELS, ...statusLabels };
|
|
131
152
|
const classes = [
|
|
132
153
|
baseClass,
|
|
133
154
|
expanded && `${baseClass}--expanded`,
|
|
155
|
+
dotted && `${baseClass}--dotted`,
|
|
134
156
|
className
|
|
135
157
|
].filter(Boolean).join(" ");
|
|
136
158
|
const iconClass = "material-symbols-rounded";
|
|
159
|
+
const projectInks = distinctPixelInks(
|
|
160
|
+
(projects ?? []).map((project) => project.pixelAvatar ?? project.label)
|
|
161
|
+
);
|
|
137
162
|
const showHeader = Boolean(logo || logoText || onExpandedChange);
|
|
138
163
|
const showNew = Boolean(newThreadLabel || onNewThread || newThreadHref);
|
|
139
164
|
const showFooter = Boolean(footerSlot || profile);
|
|
@@ -252,7 +277,7 @@ const ThreadPanel = React.forwardRef(
|
|
|
252
277
|
}
|
|
253
278
|
)
|
|
254
279
|
] }),
|
|
255
|
-
/* @__PURE__ */ jsx("ul", { className: `${baseClass}__list`, "aria-label": projectsLabel, children: projects.map((project) => {
|
|
280
|
+
/* @__PURE__ */ jsx("ul", { className: `${baseClass}__list`, "aria-label": projectsLabel, children: projects.map((project, projectIndex) => {
|
|
256
281
|
const active = project.id === activeProjectId;
|
|
257
282
|
return /* @__PURE__ */ jsx("li", { className: `${baseClass}__row`, children: /* @__PURE__ */ jsxs(
|
|
258
283
|
ActionRow,
|
|
@@ -266,12 +291,25 @@ const ThreadPanel = React.forwardRef(
|
|
|
266
291
|
ariaCurrent: active ? project.href ? "page" : "true" : void 0,
|
|
267
292
|
title: project.label,
|
|
268
293
|
children: [
|
|
269
|
-
/* @__PURE__ */ jsx(
|
|
294
|
+
project.icon == null ? /* @__PURE__ */ jsx(
|
|
270
295
|
"span",
|
|
271
296
|
{
|
|
272
|
-
className: `${baseClass}__project-icon
|
|
297
|
+
className: `${baseClass}__project-icon ${baseClass}__project-icon--avatar`,
|
|
273
298
|
"aria-hidden": "true",
|
|
274
|
-
children:
|
|
299
|
+
children: /* @__PURE__ */ jsx(
|
|
300
|
+
PixelAvatar,
|
|
301
|
+
{
|
|
302
|
+
name: project.pixelAvatar ?? project.label,
|
|
303
|
+
ink: projectInks[projectIndex]
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
) : /* @__PURE__ */ jsx(
|
|
308
|
+
"span",
|
|
309
|
+
{
|
|
310
|
+
className: `${baseClass}__project-icon${typeof project.icon === "string" ? ` ${iconClass}` : ""}`,
|
|
311
|
+
"aria-hidden": "true",
|
|
312
|
+
children: project.icon
|
|
275
313
|
}
|
|
276
314
|
),
|
|
277
315
|
/* @__PURE__ */ jsx("span", { className: `${baseClass}__project-label`, children: project.label }),
|
|
@@ -300,6 +338,8 @@ const ThreadPanel = React.forwardRef(
|
|
|
300
338
|
const showMenu = Boolean(
|
|
301
339
|
actions && actions.length > 0 && onThreadAction && !thread.pending
|
|
302
340
|
);
|
|
341
|
+
const status = dotted ? thread.pending ? "working" : thread.status ?? (thread.unread ? "unread" : "idle") : void 0;
|
|
342
|
+
const statusLabel = status && !thread.pending ? labels[status] : void 0;
|
|
303
343
|
const renaming = Boolean(
|
|
304
344
|
thread.id === renamingThreadId && onThreadRename
|
|
305
345
|
);
|
|
@@ -326,19 +366,20 @@ const ThreadPanel = React.forwardRef(
|
|
|
326
366
|
className: [
|
|
327
367
|
`${baseClass}__thread`,
|
|
328
368
|
active && `${baseClass}__thread--active`,
|
|
329
|
-
thread.description && !thread.pending && `${baseClass}__thread--detailed
|
|
330
|
-
thread.unread && `${baseClass}__thread--unread`
|
|
369
|
+
thread.description && !thread.pending && `${baseClass}__thread--detailed`
|
|
331
370
|
].filter(Boolean).join(" "),
|
|
332
371
|
href: thread.href,
|
|
333
372
|
onClick: onThreadSelect ? () => onThreadSelect(thread.id) : void 0,
|
|
334
373
|
ariaCurrent: active ? thread.href ? "page" : "true" : void 0,
|
|
335
374
|
title: thread.title,
|
|
336
375
|
children: [
|
|
337
|
-
|
|
338
|
-
|
|
376
|
+
status && /* @__PURE__ */ jsx(
|
|
377
|
+
StatusDot,
|
|
339
378
|
{
|
|
340
|
-
className: `${baseClass}
|
|
341
|
-
|
|
379
|
+
className: `${baseClass}__status`,
|
|
380
|
+
size: "xs",
|
|
381
|
+
decorative: true,
|
|
382
|
+
...STATUS_DOTS[status]
|
|
342
383
|
}
|
|
343
384
|
),
|
|
344
385
|
thread.pending ? (
|
|
@@ -359,6 +400,7 @@ const ThreadPanel = React.forwardRef(
|
|
|
359
400
|
}
|
|
360
401
|
)
|
|
361
402
|
) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: thread.title }),
|
|
403
|
+
statusLabel && /* @__PURE__ */ jsx("span", { className: `${baseClass}__status-label`, children: statusLabel }),
|
|
362
404
|
!thread.pending && (thread.icon || thread.pinned || thread.meta) && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__trail`, children: [
|
|
363
405
|
thread.icon && /* @__PURE__ */ jsx(
|
|
364
406
|
"span",
|