rift-ds 1.6.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/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/ShaderField/ShaderField.d.ts +3 -3
- 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 +20 -11
- package/components/ThreadPanel/ThreadPanel.d.ts +15 -3
- package/components/ThreadPanel/ThreadPanel.js +30 -6
- package/components/ThreadTabs/ThreadTabs.css +18 -8
- package/components/ThreadTabs/ThreadTabs.d.ts +8 -2
- package/components/ThreadTabs/ThreadTabs.js +24 -5
- package/components/registry.json +9 -1
- package/components/registry.json.d.ts +9 -1
- package/components/registry.json.js +1 -1
- package/index.d.ts +1 -0
- package/index.js +11 -0
- package/package.json +5 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import React, { useRef, useLayoutEffect } from "react";
|
|
4
|
+
import "../../fonts/material-symbols.css";
|
|
5
|
+
import "./Mention.css";
|
|
6
|
+
import { mentionOptionId, mentionListboxId, splitMentions } from "./mentions.js";
|
|
7
|
+
import { filterMentionItems, getMentionQuery, insertMention } from "./mentions.js";
|
|
8
|
+
const Mention = React.forwardRef(
|
|
9
|
+
({ trigger = "@", className = "", children, ...rest }, ref) => /* @__PURE__ */ jsxs(
|
|
10
|
+
"span",
|
|
11
|
+
{
|
|
12
|
+
...rest,
|
|
13
|
+
ref,
|
|
14
|
+
className: ["ds-mention", className].filter(Boolean).join(" "),
|
|
15
|
+
"data-trigger": trigger,
|
|
16
|
+
children: [
|
|
17
|
+
/* @__PURE__ */ jsx("span", { className: "ds-mention__trigger", children: trigger }),
|
|
18
|
+
children
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
Mention.displayName = "Mention";
|
|
24
|
+
const MentionText = React.forwardRef(
|
|
25
|
+
({ text, sources, className = "", ...rest }, ref) => /* @__PURE__ */ jsx(
|
|
26
|
+
"span",
|
|
27
|
+
{
|
|
28
|
+
...rest,
|
|
29
|
+
ref,
|
|
30
|
+
className: ["ds-mention-text", className].filter(Boolean).join(" "),
|
|
31
|
+
children: splitMentions(text, sources).map(
|
|
32
|
+
(segment, index) => segment.type === "mention" ? (
|
|
33
|
+
// Segments never reorder: the text is re-split whole on every change
|
|
34
|
+
/* @__PURE__ */ jsx(Mention, { trigger: segment.trigger, children: segment.item.label }, index)
|
|
35
|
+
) : /* @__PURE__ */ jsx(React.Fragment, { children: segment.text }, index)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
MentionText.displayName = "MentionText";
|
|
41
|
+
const renderLabel = (label, query) => {
|
|
42
|
+
const needle = query.trim().toLowerCase();
|
|
43
|
+
const at = needle === "" ? -1 : label.toLowerCase().indexOf(needle);
|
|
44
|
+
if (at < 0) return label;
|
|
45
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
46
|
+
label.slice(0, at),
|
|
47
|
+
/* @__PURE__ */ jsx("span", { className: "ds-mention-menu__match", children: label.slice(at, at + needle.length) }),
|
|
48
|
+
label.slice(at + needle.length)
|
|
49
|
+
] });
|
|
50
|
+
};
|
|
51
|
+
const MentionMenu = React.forwardRef(
|
|
52
|
+
({
|
|
53
|
+
items,
|
|
54
|
+
activeId,
|
|
55
|
+
onActiveChange,
|
|
56
|
+
onSelect,
|
|
57
|
+
query = "",
|
|
58
|
+
label = "Suggestions",
|
|
59
|
+
emptyText,
|
|
60
|
+
className = "",
|
|
61
|
+
id,
|
|
62
|
+
onMouseDown,
|
|
63
|
+
...rest
|
|
64
|
+
}, ref) => {
|
|
65
|
+
const baseClass = "ds-mention-menu";
|
|
66
|
+
const generatedId = React.useId();
|
|
67
|
+
const menuId = id ?? generatedId;
|
|
68
|
+
const listRef = useRef(null);
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
const list = listRef.current;
|
|
71
|
+
if (!list || activeId === void 0) return;
|
|
72
|
+
const row = list.querySelector('[aria-selected="true"]');
|
|
73
|
+
if (!row) return;
|
|
74
|
+
const top = row.offsetTop;
|
|
75
|
+
const bottom = top + row.offsetHeight;
|
|
76
|
+
if (top < list.scrollTop) list.scrollTop = top;
|
|
77
|
+
else if (bottom > list.scrollTop + list.clientHeight) {
|
|
78
|
+
list.scrollTop = bottom - list.clientHeight;
|
|
79
|
+
}
|
|
80
|
+
}, [activeId, items]);
|
|
81
|
+
if (items.length === 0 && !emptyText) return null;
|
|
82
|
+
const handleMouseDown = (event) => {
|
|
83
|
+
onMouseDown?.(event);
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
};
|
|
86
|
+
return /* @__PURE__ */ jsx(
|
|
87
|
+
"div",
|
|
88
|
+
{
|
|
89
|
+
...rest,
|
|
90
|
+
ref,
|
|
91
|
+
id: menuId,
|
|
92
|
+
className: [baseClass, query.trim() !== "" ? `${baseClass}--filtered` : "", className].filter(Boolean).join(" "),
|
|
93
|
+
onMouseDown: handleMouseDown,
|
|
94
|
+
children: items.length === 0 ? /* @__PURE__ */ jsx("div", { className: `${baseClass}__empty`, role: "status", children: emptyText }) : /* @__PURE__ */ jsx(
|
|
95
|
+
"div",
|
|
96
|
+
{
|
|
97
|
+
ref: listRef,
|
|
98
|
+
id: mentionListboxId(menuId),
|
|
99
|
+
className: `${baseClass}__list`,
|
|
100
|
+
role: "listbox",
|
|
101
|
+
"aria-label": label,
|
|
102
|
+
tabIndex: 0,
|
|
103
|
+
children: items.map((item) => {
|
|
104
|
+
const active = item.id === activeId;
|
|
105
|
+
return /* @__PURE__ */ jsxs(
|
|
106
|
+
"div",
|
|
107
|
+
{
|
|
108
|
+
id: mentionOptionId(menuId, item.id),
|
|
109
|
+
className: [
|
|
110
|
+
`${baseClass}__option`,
|
|
111
|
+
active ? `${baseClass}__option--active` : "",
|
|
112
|
+
item.disabled ? `${baseClass}__option--disabled` : ""
|
|
113
|
+
].filter(Boolean).join(" "),
|
|
114
|
+
role: "option",
|
|
115
|
+
"aria-selected": active,
|
|
116
|
+
"aria-disabled": item.disabled || void 0,
|
|
117
|
+
onMouseMove: item.disabled || active ? void 0 : () => onActiveChange?.(item.id),
|
|
118
|
+
onClick: item.disabled ? void 0 : () => onSelect?.(item),
|
|
119
|
+
children: [
|
|
120
|
+
item.icon !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: typeof item.icon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: item.icon }) : item.icon }),
|
|
121
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: renderLabel(item.label, query) }),
|
|
122
|
+
item.description && /* @__PURE__ */ jsx("span", { className: `${baseClass}__description`, children: item.description })
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
item.id
|
|
126
|
+
);
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
MentionMenu.displayName = "MentionMenu";
|
|
135
|
+
export {
|
|
136
|
+
Mention,
|
|
137
|
+
MentionMenu,
|
|
138
|
+
MentionText,
|
|
139
|
+
filterMentionItems,
|
|
140
|
+
getMentionQuery,
|
|
141
|
+
insertMention,
|
|
142
|
+
mentionListboxId,
|
|
143
|
+
mentionOptionId,
|
|
144
|
+
splitMentions
|
|
145
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** One thing a person can reference: a teammate, a file, a skill. */
|
|
3
|
+
export type MentionItem = {
|
|
4
|
+
/** Stable identity, unique within its source. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** The name shown in the menu and written into the text after the trigger character. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** A quieter second phrase on the same row: a role, a path, what a skill does. */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Leading mark: a Material Symbol name, or any node (an Avatar). Decorative. */
|
|
11
|
+
icon?: string | React.ReactNode;
|
|
12
|
+
/** Extra words the filter matches on besides the label. */
|
|
13
|
+
keywords?: string[];
|
|
14
|
+
/** Listed but not choosable. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
/** Everything one trigger character opens: `@` for entities, `/` for skills. */
|
|
18
|
+
export type MentionSource = {
|
|
19
|
+
/** The single character that opens this source. */
|
|
20
|
+
trigger: string;
|
|
21
|
+
/** The items it offers. */
|
|
22
|
+
items: MentionItem[];
|
|
23
|
+
/** Accessible name for this source's menu ("People", "Skills"). */
|
|
24
|
+
label?: string;
|
|
25
|
+
};
|
|
26
|
+
/** A trigger being typed: where it starts, and what has been typed after it. */
|
|
27
|
+
export type MentionQuery = {
|
|
28
|
+
/** The trigger character that opened it. */
|
|
29
|
+
trigger: string;
|
|
30
|
+
/** The text typed after the trigger, up to the caret. */
|
|
31
|
+
query: string;
|
|
32
|
+
/** Index of the trigger character. */
|
|
33
|
+
start: number;
|
|
34
|
+
/** Index of the caret. */
|
|
35
|
+
end: number;
|
|
36
|
+
};
|
|
37
|
+
/** One run of text from `splitMentions`: plain words, or a recognised mention. */
|
|
38
|
+
export type MentionSegment = {
|
|
39
|
+
type: 'text';
|
|
40
|
+
text: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'mention';
|
|
43
|
+
text: string;
|
|
44
|
+
trigger: string;
|
|
45
|
+
item: MentionItem;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Reads the mention being typed at the caret, if there is one: a trigger
|
|
49
|
+
* character that opens a word, followed by no whitespace up to the caret.
|
|
50
|
+
* A trigger in the middle of a word (the slash in a path, the at-sign in an
|
|
51
|
+
* address) opens nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getMentionQuery(text: string, caret: number, triggers: string[]): MentionQuery | null;
|
|
54
|
+
/**
|
|
55
|
+
* Narrows and ranks a source's items for a query: labels that start with it
|
|
56
|
+
* first, then labels where a later word starts with it, then labels that
|
|
57
|
+
* contain it, then keyword matches. An empty query returns every item in its
|
|
58
|
+
* given order.
|
|
59
|
+
*/
|
|
60
|
+
export declare function filterMentionItems(items: MentionItem[], query: string): MentionItem[];
|
|
61
|
+
/**
|
|
62
|
+
* Splits text into plain runs and the mentions it contains. A mention is a
|
|
63
|
+
* trigger character followed by the exact label of one of that source's
|
|
64
|
+
* items, standing as its own word. Nothing is stored alongside the text: a
|
|
65
|
+
* mention is recognised wherever its name is written, and stops being one
|
|
66
|
+
* the moment the name is edited.
|
|
67
|
+
*/
|
|
68
|
+
export declare function splitMentions(text: string, sources: MentionSource[]): MentionSegment[];
|
|
69
|
+
/**
|
|
70
|
+
* Writes a chosen item over the query being typed: the trigger, the label,
|
|
71
|
+
* and one trailing space, so the next word starts clean. Returns the new
|
|
72
|
+
* text and where the caret belongs in it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function insertMention(text: string, query: Pick<MentionQuery, 'trigger' | 'start' | 'end'>, item: MentionItem): {
|
|
75
|
+
value: string;
|
|
76
|
+
caret: number;
|
|
77
|
+
};
|
|
78
|
+
/** The DOM id of one option inside a MentionMenu, for `aria-activedescendant`. */
|
|
79
|
+
export declare const mentionOptionId: (menuId: string, itemId: string) => string;
|
|
80
|
+
/** The DOM id of the listbox inside a MentionMenu, for `aria-controls`. */
|
|
81
|
+
export declare const mentionListboxId: (menuId: string) => string;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const MAX_QUERY_LENGTH = 48;
|
|
2
|
+
const isSpace = (char) => char === void 0 || /\s/.test(char);
|
|
3
|
+
function getMentionQuery(text, caret, triggers) {
|
|
4
|
+
for (let index = caret - 1; index >= 0 && caret - index <= MAX_QUERY_LENGTH + 1; index -= 1) {
|
|
5
|
+
const char = text[index];
|
|
6
|
+
if (isSpace(char)) return null;
|
|
7
|
+
if (triggers.includes(char) && isSpace(text[index - 1])) {
|
|
8
|
+
return { trigger: char, query: text.slice(index + 1, caret), start: index, end: caret };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function filterMentionItems(items, query) {
|
|
14
|
+
const needle = query.trim().toLowerCase();
|
|
15
|
+
if (needle === "") return items;
|
|
16
|
+
const rank = (item) => {
|
|
17
|
+
const label = item.label.toLowerCase();
|
|
18
|
+
if (label.startsWith(needle)) return 0;
|
|
19
|
+
if (label.split(/[\s\-_:./]+/).some((word) => word.startsWith(needle))) return 1;
|
|
20
|
+
if (label.includes(needle)) return 2;
|
|
21
|
+
if (item.keywords?.some((keyword) => keyword.toLowerCase().includes(needle))) return 3;
|
|
22
|
+
return -1;
|
|
23
|
+
};
|
|
24
|
+
return items.map((item, index) => ({ item, index, rank: rank(item) })).filter((entry) => entry.rank >= 0).sort((a, b) => a.rank - b.rank || a.index - b.index).map((entry) => entry.item);
|
|
25
|
+
}
|
|
26
|
+
const OPENERS = /* @__PURE__ */ new Set(["(", "[", "{", '"', "“", "‘", "'"]);
|
|
27
|
+
const continuesName = (char) => char !== void 0 && /[\p{L}\p{N}_-]/u.test(char);
|
|
28
|
+
function splitMentions(text, sources) {
|
|
29
|
+
const candidates = sources.flatMap(
|
|
30
|
+
(source) => source.items.map((item) => ({
|
|
31
|
+
trigger: source.trigger,
|
|
32
|
+
item,
|
|
33
|
+
token: `${source.trigger}${item.label}`
|
|
34
|
+
}))
|
|
35
|
+
).sort((a, b) => b.token.length - a.token.length);
|
|
36
|
+
const triggers = new Set(sources.map((source) => source.trigger));
|
|
37
|
+
const segments = [];
|
|
38
|
+
let plainFrom = 0;
|
|
39
|
+
let index = 0;
|
|
40
|
+
while (index < text.length) {
|
|
41
|
+
const before = text[index - 1];
|
|
42
|
+
if (triggers.has(text[index]) && (isSpace(before) || OPENERS.has(before))) {
|
|
43
|
+
const match = candidates.find(
|
|
44
|
+
(candidate) => text.startsWith(candidate.token, index) && !continuesName(text[index + candidate.token.length])
|
|
45
|
+
);
|
|
46
|
+
if (match) {
|
|
47
|
+
if (index > plainFrom) segments.push({ type: "text", text: text.slice(plainFrom, index) });
|
|
48
|
+
segments.push({
|
|
49
|
+
type: "mention",
|
|
50
|
+
text: match.token,
|
|
51
|
+
trigger: match.trigger,
|
|
52
|
+
item: match.item
|
|
53
|
+
});
|
|
54
|
+
index += match.token.length;
|
|
55
|
+
plainFrom = index;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
index += 1;
|
|
60
|
+
}
|
|
61
|
+
if (plainFrom < text.length) segments.push({ type: "text", text: text.slice(plainFrom) });
|
|
62
|
+
return segments;
|
|
63
|
+
}
|
|
64
|
+
function insertMention(text, query, item) {
|
|
65
|
+
const before = text.slice(0, query.start);
|
|
66
|
+
const after = text.slice(query.end);
|
|
67
|
+
const token = `${query.trigger}${item.label}`;
|
|
68
|
+
const spacer = after.startsWith(" ") ? "" : " ";
|
|
69
|
+
return {
|
|
70
|
+
value: `${before}${token}${spacer}${after}`,
|
|
71
|
+
caret: before.length + token.length + 1
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const mentionOptionId = (menuId, itemId) => `${menuId}-option-${itemId}`;
|
|
75
|
+
const mentionListboxId = (menuId) => `${menuId}-listbox`;
|
|
76
|
+
export {
|
|
77
|
+
filterMentionItems,
|
|
78
|
+
getMentionQuery,
|
|
79
|
+
insertMention,
|
|
80
|
+
mentionListboxId,
|
|
81
|
+
mentionOptionId,
|
|
82
|
+
splitMentions
|
|
83
|
+
};
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
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
3
|
/**
|
|
7
4
|
* How the renderer has resolved.
|
|
8
5
|
*
|
|
@@ -66,3 +63,6 @@ export interface ShaderFieldProps extends ShaderFieldOwnProps, Omit<React.Compon
|
|
|
66
63
|
* the clipping and any mask.
|
|
67
64
|
*/
|
|
68
65
|
export declare const ShaderField: React.ForwardRefExoticComponent<ShaderFieldProps & React.RefAttributes<HTMLCanvasElement>>;
|
|
66
|
+
export type { ShaderBlob, ShaderParams, ShaderFieldParams, ShaderFieldState, } from './useShaderField';
|
|
67
|
+
export { useShaderField, DEFAULT_SHADER_BLOBS, DEFAULT_SHADER_PARAMS } from './useShaderField';
|
|
68
|
+
export { BLOB_COUNT } from './field.glsl';
|
|
@@ -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";
|
|
@@ -531,15 +531,24 @@
|
|
|
531
531
|
row-gap: 0;
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
/* The
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
.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 {
|
|
538
537
|
flex: none;
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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;
|
|
543
552
|
}
|
|
544
553
|
|
|
545
554
|
.ds-thread-panel__title {
|
|
@@ -616,8 +625,8 @@
|
|
|
616
625
|
}
|
|
617
626
|
|
|
618
627
|
/* The description line: the caption face on the full second line, fading
|
|
619
|
-
at the trailing edge like the title above it. Indented past the
|
|
620
|
-
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.
|
|
621
630
|
ds-allow(color): mask alpha keywords (black/transparent), not palette colours */
|
|
622
631
|
.ds-thread-panel__description {
|
|
623
632
|
flex-basis: 100%;
|
|
@@ -641,7 +650,7 @@
|
|
|
641
650
|
);
|
|
642
651
|
}
|
|
643
652
|
|
|
644
|
-
.ds-thread-
|
|
653
|
+
.ds-thread-panel--dotted .ds-thread-panel__description {
|
|
645
654
|
/* The dot's width plus the row's column gap. */
|
|
646
655
|
margin-left: calc(var(--gap-200) + var(--gap-200));
|
|
647
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;
|
|
@@ -81,6 +91,8 @@ type ThreadPanelOwnProps = {
|
|
|
81
91
|
threadActions?: ThreadPanelThreadAction[];
|
|
82
92
|
/** Fires with the thread's id and the chosen action's id. */
|
|
83
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>>;
|
|
84
96
|
/** Accessible name for a row's menu trigger; the thread's title is appended after it. */
|
|
85
97
|
threadMenuLabel?: string;
|
|
86
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. */
|
|
@@ -150,8 +162,8 @@ export interface ThreadPanelProps extends ThreadPanelOwnProps, Omit<React.Compon
|
|
|
150
162
|
* control rows, an optional projects section with its own new-project
|
|
151
163
|
* button, the grouped thread history (the one region that scrolls), and a
|
|
152
164
|
* footer for the profile row and host furniture like a theme toggle.
|
|
153
|
-
* Thread rows scale from a bare title to the full detail anatomy:
|
|
154
|
-
*
|
|
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,
|
|
155
167
|
* and the meta caption.
|
|
156
168
|
*
|
|
157
169
|
* Fully controlled and stateless: the host owns the active thread, the
|