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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
-
import React, { useRef, useState, useLayoutEffect, useEffect } from "react";
|
|
3
|
+
import React, { useRef, useState, useId, useLayoutEffect, useEffect } from "react";
|
|
4
4
|
import { AttachmentTile } from "../Attachment/AttachmentTile.js";
|
|
5
5
|
import { formatBytes } from "../Attachment/fileTypes.js";
|
|
6
6
|
import { CircularButton } from "../CircularButton/CircularButton.js";
|
|
7
|
+
import { MentionText, MentionMenu } from "../Mention/Mention.js";
|
|
7
8
|
import "../../fonts/material-symbols.css";
|
|
8
9
|
import "./Composer.css";
|
|
10
|
+
import { getMentionQuery, filterMentionItems, mentionOptionId, mentionListboxId, insertMention } from "../Mention/mentions.js";
|
|
9
11
|
const defaultFileAnnouncement = ({
|
|
10
12
|
type,
|
|
11
13
|
names
|
|
@@ -15,6 +17,11 @@ const defaultFileAnnouncement = ({
|
|
|
15
17
|
if (type === "removed") return `Removed ${list}`;
|
|
16
18
|
return `Could not attach ${list}`;
|
|
17
19
|
};
|
|
20
|
+
const defaultMentionAnnouncement = (count) => count === 1 ? "1 suggestion" : `${count} suggestions`;
|
|
21
|
+
const replaceRange = (node, text, start, end) => {
|
|
22
|
+
node.setRangeText(text, start, end, "end");
|
|
23
|
+
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
24
|
+
};
|
|
18
25
|
const dragCarriesFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files");
|
|
19
26
|
const Composer = React.forwardRef(
|
|
20
27
|
({
|
|
@@ -43,6 +50,10 @@ const Composer = React.forwardRef(
|
|
|
43
50
|
filesLabel = "Attachments",
|
|
44
51
|
formatFileAnnouncement = defaultFileAnnouncement,
|
|
45
52
|
attachments,
|
|
53
|
+
mentions,
|
|
54
|
+
onMentionSelect,
|
|
55
|
+
mentionPlacement = "top",
|
|
56
|
+
formatMentionAnnouncement = defaultMentionAnnouncement,
|
|
46
57
|
actions,
|
|
47
58
|
trailingActions,
|
|
48
59
|
sendLabel = "Send message",
|
|
@@ -51,6 +62,10 @@ const Composer = React.forwardRef(
|
|
|
51
62
|
onChange,
|
|
52
63
|
onKeyDown,
|
|
53
64
|
onPaste,
|
|
65
|
+
onSelect,
|
|
66
|
+
onFocus,
|
|
67
|
+
onBlur,
|
|
68
|
+
onScroll,
|
|
54
69
|
...rest
|
|
55
70
|
}, ref) => {
|
|
56
71
|
const baseClass = "ds-composer";
|
|
@@ -71,6 +86,27 @@ const Composer = React.forwardRef(
|
|
|
71
86
|
const [dragging, setDragging] = useState(false);
|
|
72
87
|
const dragDepth = useRef(0);
|
|
73
88
|
const [announcement, setAnnouncement] = useState("");
|
|
89
|
+
const mentionSources = mentions ?? [];
|
|
90
|
+
const mentionsArmed = mentionSources.length > 0;
|
|
91
|
+
const mentionMenuId = useId();
|
|
92
|
+
const backdropTextRef = useRef(null);
|
|
93
|
+
const shellRef = useRef(null);
|
|
94
|
+
const mentionMenuRef = useRef(null);
|
|
95
|
+
const [caret, setCaret] = useState(null);
|
|
96
|
+
const [focused, setFocused] = useState(false);
|
|
97
|
+
const [activeMention, setActiveMention] = useState(0);
|
|
98
|
+
const [dismissedAt, setDismissedAt] = useState(null);
|
|
99
|
+
const pendingCaret = useRef(null);
|
|
100
|
+
const mentionQuery = mentionsArmed && focused && !disabled && caret !== null ? getMentionQuery(
|
|
101
|
+
currentValue,
|
|
102
|
+
caret,
|
|
103
|
+
mentionSources.map((source) => source.trigger)
|
|
104
|
+
) : null;
|
|
105
|
+
const mentionSource = mentionQuery ? mentionSources.find((source) => source.trigger === mentionQuery.trigger) : void 0;
|
|
106
|
+
const mentionMatches = mentionQuery && mentionSource ? filterMentionItems(mentionSource.items, mentionQuery.query) : [];
|
|
107
|
+
const choosable = mentionMatches.filter((item) => !item.disabled);
|
|
108
|
+
const mentionOpen = mentionQuery !== null && choosable.length > 0 && dismissedAt !== mentionQuery.start;
|
|
109
|
+
const activeItem = mentionOpen ? choosable[Math.min(activeMention, choosable.length - 1)] : void 0;
|
|
74
110
|
const setTextareaRef = (node) => {
|
|
75
111
|
textareaRef.current = node;
|
|
76
112
|
if (typeof ref === "function") ref(node);
|
|
@@ -89,10 +125,59 @@ const Composer = React.forwardRef(
|
|
|
89
125
|
if (!node || !content) return;
|
|
90
126
|
const observer = new ResizeObserver(() => {
|
|
91
127
|
content.style.setProperty("--ds-composer-text-height", `${node.offsetHeight}px`);
|
|
128
|
+
content.style.setProperty("--ds-composer-text-width", `${node.clientWidth}px`);
|
|
92
129
|
});
|
|
93
130
|
observer.observe(node);
|
|
94
131
|
return () => observer.disconnect();
|
|
95
132
|
}, []);
|
|
133
|
+
useLayoutEffect(() => {
|
|
134
|
+
const node = textareaRef.current;
|
|
135
|
+
if (!node) return;
|
|
136
|
+
if (pendingCaret.current !== null) {
|
|
137
|
+
const next = pendingCaret.current;
|
|
138
|
+
pendingCaret.current = null;
|
|
139
|
+
node.setSelectionRange(next, next);
|
|
140
|
+
setCaret(next);
|
|
141
|
+
}
|
|
142
|
+
if (!mentionsArmed) return;
|
|
143
|
+
contentRef.current?.style.setProperty("--ds-composer-text-width", `${node.clientWidth}px`);
|
|
144
|
+
if (backdropTextRef.current) {
|
|
145
|
+
backdropTextRef.current.style.transform = `translateY(${-node.scrollTop}px)`;
|
|
146
|
+
}
|
|
147
|
+
}, [currentValue, mentionsArmed]);
|
|
148
|
+
const mentionStart = mentionOpen && mentionQuery ? mentionQuery.start : null;
|
|
149
|
+
useLayoutEffect(() => {
|
|
150
|
+
const menu = mentionMenuRef.current;
|
|
151
|
+
const shell2 = shellRef.current;
|
|
152
|
+
const overlay = backdropTextRef.current;
|
|
153
|
+
if (mentionStart === null || !menu || !shell2 || !overlay) return;
|
|
154
|
+
const walker = document.createTreeWalker(overlay, NodeFilter.SHOW_TEXT);
|
|
155
|
+
let remaining = mentionStart;
|
|
156
|
+
let node = walker.nextNode();
|
|
157
|
+
while (node && remaining >= (node.textContent ?? "").length) {
|
|
158
|
+
remaining -= (node.textContent ?? "").length;
|
|
159
|
+
node = walker.nextNode();
|
|
160
|
+
}
|
|
161
|
+
if (!node) return;
|
|
162
|
+
const range = document.createRange();
|
|
163
|
+
range.setStart(node, remaining);
|
|
164
|
+
range.setEnd(node, remaining + 1);
|
|
165
|
+
const triggerBox = range.getBoundingClientRect();
|
|
166
|
+
const triggerLeft = triggerBox.left;
|
|
167
|
+
const shellBox = shell2.getBoundingClientRect();
|
|
168
|
+
const menuBox = menu.getBoundingClientRect();
|
|
169
|
+
const firstCell = menu.querySelector('[role="option"] > *');
|
|
170
|
+
const inset = firstCell ? firstCell.getBoundingClientRect().left - menuBox.left : 0;
|
|
171
|
+
const room = Math.max(0, shell2.clientWidth - menu.offsetWidth);
|
|
172
|
+
const left = Math.min(Math.max(0, triggerLeft - shellBox.left - shell2.clientLeft - inset), room);
|
|
173
|
+
menu.style.left = `${left}px`;
|
|
174
|
+
const origin = shellBox.top + shell2.clientTop;
|
|
175
|
+
menu.style.setProperty("--ds-composer-mention-line-top", `${triggerBox.top - origin}px`);
|
|
176
|
+
menu.style.setProperty(
|
|
177
|
+
"--ds-composer-mention-line-bottom",
|
|
178
|
+
`${triggerBox.bottom - origin}px`
|
|
179
|
+
);
|
|
180
|
+
}, [mentionStart, currentValue]);
|
|
96
181
|
const previousFiles = useRef(queued);
|
|
97
182
|
const announceRef = useRef(formatFileAnnouncement);
|
|
98
183
|
announceRef.current = formatFileAnnouncement;
|
|
@@ -183,13 +268,81 @@ const Composer = React.forwardRef(
|
|
|
183
268
|
if (streaming || !canSend) return;
|
|
184
269
|
onSubmit?.(currentValue);
|
|
185
270
|
};
|
|
271
|
+
const readCaret = (node) => node.selectionStart === node.selectionEnd ? node.selectionStart : null;
|
|
186
272
|
const handleChange = (e) => {
|
|
187
273
|
if (!isControlled) setUncontrolledValue(e.target.value);
|
|
274
|
+
if (mentionsArmed) {
|
|
275
|
+
setCaret(readCaret(e.target));
|
|
276
|
+
setActiveMention(0);
|
|
277
|
+
}
|
|
188
278
|
onChange?.(e);
|
|
189
279
|
onValueChange?.(e.target.value);
|
|
190
280
|
};
|
|
281
|
+
const handleSelect = (e) => {
|
|
282
|
+
onSelect?.(e);
|
|
283
|
+
if (!mentionsArmed) return;
|
|
284
|
+
const next = readCaret(e.currentTarget);
|
|
285
|
+
setCaret(next);
|
|
286
|
+
if (dismissedAt !== null && next !== null) {
|
|
287
|
+
const query = getMentionQuery(
|
|
288
|
+
e.currentTarget.value,
|
|
289
|
+
next,
|
|
290
|
+
mentionSources.map((source) => source.trigger)
|
|
291
|
+
);
|
|
292
|
+
if (query?.start !== dismissedAt) setDismissedAt(null);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const handleFocus = (e) => {
|
|
296
|
+
onFocus?.(e);
|
|
297
|
+
setFocused(true);
|
|
298
|
+
if (mentionsArmed) setCaret(readCaret(e.currentTarget));
|
|
299
|
+
};
|
|
300
|
+
const handleBlur = (e) => {
|
|
301
|
+
onBlur?.(e);
|
|
302
|
+
setFocused(false);
|
|
303
|
+
};
|
|
304
|
+
const handleScroll = (e) => {
|
|
305
|
+
onScroll?.(e);
|
|
306
|
+
if (backdropTextRef.current) {
|
|
307
|
+
backdropTextRef.current.style.transform = `translateY(${-e.currentTarget.scrollTop}px)`;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const chooseMention = (item) => {
|
|
311
|
+
const node = textareaRef.current;
|
|
312
|
+
if (!node || !mentionQuery) return;
|
|
313
|
+
const next = insertMention(currentValue, mentionQuery, item);
|
|
314
|
+
pendingCaret.current = next.caret;
|
|
315
|
+
const tail = currentValue.length - mentionQuery.end;
|
|
316
|
+
replaceRange(
|
|
317
|
+
node,
|
|
318
|
+
next.value.slice(mentionQuery.start, next.value.length - tail),
|
|
319
|
+
mentionQuery.start,
|
|
320
|
+
mentionQuery.end
|
|
321
|
+
);
|
|
322
|
+
onMentionSelect?.(item, mentionQuery.trigger);
|
|
323
|
+
};
|
|
191
324
|
const handleKeyDown = (e) => {
|
|
192
325
|
onKeyDown?.(e);
|
|
326
|
+
if (mentionOpen && activeItem && mentionQuery && !e.nativeEvent.isComposing) {
|
|
327
|
+
const at = choosable.indexOf(activeItem);
|
|
328
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
const step = e.key === "ArrowDown" ? 1 : -1;
|
|
331
|
+
setActiveMention((at + step + choosable.length) % choosable.length);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (e.key === "Enter" && !e.shiftKey || e.key === "Tab") {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
chooseMention(activeItem);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (e.key === "Escape") {
|
|
340
|
+
e.preventDefault();
|
|
341
|
+
e.stopPropagation();
|
|
342
|
+
setDismissedAt(mentionQuery.start);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
193
346
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
194
347
|
e.preventDefault();
|
|
195
348
|
submit();
|
|
@@ -206,6 +359,7 @@ const Composer = React.forwardRef(
|
|
|
206
359
|
streaming ? `${baseClass}--streaming` : "",
|
|
207
360
|
disabled ? `${baseClass}--disabled` : "",
|
|
208
361
|
dragging ? `${baseClass}--dragging` : "",
|
|
362
|
+
mentionsArmed ? `${baseClass}--mentions` : "",
|
|
209
363
|
// The above placement moves the text onto the chip's rail; the shell
|
|
210
364
|
// needs to know, since the chip is no longer inside it.
|
|
211
365
|
context && contextPlacement === "above" ? `${baseClass}--context-above` : "",
|
|
@@ -227,6 +381,7 @@ const Composer = React.forwardRef(
|
|
|
227
381
|
const shell = /* @__PURE__ */ jsxs(
|
|
228
382
|
"div",
|
|
229
383
|
{
|
|
384
|
+
ref: shellRef,
|
|
230
385
|
className: classes,
|
|
231
386
|
style: { "--ds-composer-max-rows": maxRows },
|
|
232
387
|
onClick: handleShellClick,
|
|
@@ -253,20 +408,48 @@ const Composer = React.forwardRef(
|
|
|
253
408
|
}
|
|
254
409
|
) }, file.id)) }),
|
|
255
410
|
attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
|
|
256
|
-
/* @__PURE__ */
|
|
257
|
-
|
|
411
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__content`, ref: contentRef, children: [
|
|
412
|
+
/* @__PURE__ */ jsx(
|
|
413
|
+
"textarea",
|
|
414
|
+
{
|
|
415
|
+
...rest,
|
|
416
|
+
ref: setTextareaRef,
|
|
417
|
+
className: `${baseClass}__textarea`,
|
|
418
|
+
rows: 1,
|
|
419
|
+
value: currentValue,
|
|
420
|
+
"aria-label": ariaLabel,
|
|
421
|
+
"aria-autocomplete": mentionsArmed ? "list" : void 0,
|
|
422
|
+
"aria-controls": mentionOpen ? mentionListboxId(mentionMenuId) : void 0,
|
|
423
|
+
"aria-activedescendant": activeItem ? mentionOptionId(mentionMenuId, activeItem.id) : void 0,
|
|
424
|
+
onChange: handleChange,
|
|
425
|
+
onKeyDown: handleKeyDown,
|
|
426
|
+
onPaste: handlePaste,
|
|
427
|
+
onSelect: handleSelect,
|
|
428
|
+
onFocus: handleFocus,
|
|
429
|
+
onBlur: handleBlur,
|
|
430
|
+
onScroll: handleScroll
|
|
431
|
+
}
|
|
432
|
+
),
|
|
433
|
+
mentionsArmed && /* The words a person sees. The textarea beneath keeps the caret,
|
|
434
|
+
the selection and every keystroke, with its own text
|
|
435
|
+
transparent; this copy of the same string is where a mention
|
|
436
|
+
gets its colour. Decorative: the textarea is the control. */
|
|
437
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__backdrop`, "aria-hidden": "true", children: /* @__PURE__ */ jsx("div", { className: `${baseClass}__backdrop-text`, ref: backdropTextRef, children: /* @__PURE__ */ jsx(MentionText, { text: currentValue, sources: mentionSources }) }) })
|
|
438
|
+
] }),
|
|
439
|
+
mentionOpen && activeItem && mentionQuery && /* @__PURE__ */ jsx(
|
|
440
|
+
MentionMenu,
|
|
258
441
|
{
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
className: `${baseClass}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
442
|
+
ref: mentionMenuRef,
|
|
443
|
+
id: mentionMenuId,
|
|
444
|
+
className: `${baseClass}__mention-menu ${baseClass}__mention-menu--${mentionPlacement}`,
|
|
445
|
+
items: mentionMatches,
|
|
446
|
+
activeId: activeItem.id,
|
|
447
|
+
query: mentionQuery.query,
|
|
448
|
+
label: mentionSource?.label,
|
|
449
|
+
onActiveChange: (id) => setActiveMention(Math.max(0, choosable.findIndex((item) => item.id === id))),
|
|
450
|
+
onSelect: chooseMention
|
|
268
451
|
}
|
|
269
|
-
)
|
|
452
|
+
),
|
|
270
453
|
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__footer`, children: [
|
|
271
454
|
(actions || armed && attachButton) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__actions`, children: [
|
|
272
455
|
armed && attachButton && /* @__PURE__ */ jsx(
|
|
@@ -320,7 +503,9 @@ const Composer = React.forwardRef(
|
|
|
320
503
|
),
|
|
321
504
|
/* @__PURE__ */ jsx("div", { className: `${baseClass}__drop-hint`, "aria-hidden": "true", children: dropLabel })
|
|
322
505
|
] }),
|
|
323
|
-
(armed || hasFiles) && /* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, role: "status", children: announcement })
|
|
506
|
+
(armed || hasFiles) && /* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, role: "status", children: announcement }),
|
|
507
|
+
mentionsArmed && /* The menu opens without moving focus, so its arrival is said here */
|
|
508
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, role: "status", children: mentionOpen ? formatMentionAnnouncement(choosable.length) : "" })
|
|
324
509
|
]
|
|
325
510
|
}
|
|
326
511
|
);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
MENTION COMPONENT
|
|
3
|
+
A reference written into a sentence, and the menu a trigger opens:
|
|
4
|
+
`@` for entities, `/` for skills.
|
|
5
|
+
============================================ */
|
|
6
|
+
|
|
7
|
+
/* ============================================
|
|
8
|
+
TAG
|
|
9
|
+
Composer lays this exact tag over the text of its textarea, so nothing
|
|
10
|
+
here may move a glyph off its advance: no weight, no spacing, and a
|
|
11
|
+
chip whose padding is paid back in margin.
|
|
12
|
+
============================================ */
|
|
13
|
+
|
|
14
|
+
.ds-mention {
|
|
15
|
+
/* How far the chip's fill reaches past the glyphs on each side. Padding
|
|
16
|
+
out, margin back in: the box grows and the text does not move. */
|
|
17
|
+
--ds-mention-bleed: var(--padding-050);
|
|
18
|
+
margin: 0 calc(-1 * var(--ds-mention-bleed));
|
|
19
|
+
padding: 0 var(--ds-mention-bleed);
|
|
20
|
+
/* The theme's action colour in its text form, the tone a tertiary button
|
|
21
|
+
writes its label in. A mention is the one word in a sentence that was
|
|
22
|
+
chosen from a menu and points at something, so it wears the colour that
|
|
23
|
+
means "this does something", and it moves with the theme where a fixed
|
|
24
|
+
hue blended into the themes built near it. Behind it, a wash of the
|
|
25
|
+
same tone, barely there: enough to give the tag an edge, never enough
|
|
26
|
+
to read as a chip sitting on the line. The wash carries the tag in a
|
|
27
|
+
theme whose action colour is a neutral. */
|
|
28
|
+
background-color: var(
|
|
29
|
+
--ds-mention-bg,
|
|
30
|
+
color-mix(in srgb, var(--color-action-primary-text-tertiary) 10%, transparent)
|
|
31
|
+
);
|
|
32
|
+
border-radius: var(--radius-100);
|
|
33
|
+
/* A name that wraps keeps its fill and corners on both lines */
|
|
34
|
+
box-decoration-break: clone;
|
|
35
|
+
/* Two parts in five of the body colour are mixed in. A button label is
|
|
36
|
+
large and bold enough to carry an action colour that sits near the
|
|
37
|
+
page; a name set in running text is not, and unmixed it fell under
|
|
38
|
+
3:1 in the themes whose action colour is closest to their surface.
|
|
39
|
+
At this ratio it clears 4.5:1 on the page in every shipped theme and
|
|
40
|
+
both modes, and still reads as the action colour. */
|
|
41
|
+
color: var(
|
|
42
|
+
--ds-mention-color,
|
|
43
|
+
color-mix(in srgb, var(--color-action-primary-text-tertiary) 60%, var(--color-text-primary))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* The trigger is punctuation; the name is the word */
|
|
48
|
+
.ds-mention__trigger {
|
|
49
|
+
opacity: 0.64;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ============================================
|
|
53
|
+
MENU
|
|
54
|
+
The floating-surface recipe the chat set's other menus use.
|
|
55
|
+
============================================ */
|
|
56
|
+
|
|
57
|
+
.ds-mention-menu {
|
|
58
|
+
box-sizing: border-box;
|
|
59
|
+
/* Consumer hooks: how wide the menu is, and how many rows show before
|
|
60
|
+
the list scrolls */
|
|
61
|
+
width: var(--ds-mention-menu-width, 320px);
|
|
62
|
+
max-width: 100%;
|
|
63
|
+
/* Page background, not container: the container fill is semi-transparent
|
|
64
|
+
in dark mode, and a floating panel must not show text through itself */
|
|
65
|
+
background-color: var(--color-bg-page-primary);
|
|
66
|
+
border: var(--border-025) solid var(--color-bg-container-border);
|
|
67
|
+
border-radius: var(--radius-300);
|
|
68
|
+
box-shadow: var(--shadow-floating);
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
animation: ds-mention-menu-appear var(--motion-duration-fast) var(--motion-ease-entrance);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@keyframes ds-mention-menu-appear {
|
|
74
|
+
from {
|
|
75
|
+
opacity: 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.ds-mention-menu__list {
|
|
80
|
+
/* Anchors each row's offsetTop, which is how the highlighted row is kept
|
|
81
|
+
in view */
|
|
82
|
+
position: relative;
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
gap: var(--gap-050);
|
|
86
|
+
padding: var(--padding-100);
|
|
87
|
+
/* Rows × (one line, its block padding, and the gap under it), plus half a
|
|
88
|
+
row more, so a longer list is cut mid-row and reads as scrollable */
|
|
89
|
+
max-height: calc(
|
|
90
|
+
(var(--ds-mention-menu-max-rows, 8) + 0.5) *
|
|
91
|
+
(var(--font-paragraph-sm-line-height) + 2 * var(--padding-150) + var(--gap-050)) +
|
|
92
|
+
var(--padding-100)
|
|
93
|
+
);
|
|
94
|
+
overflow-y: auto;
|
|
95
|
+
scrollbar-width: thin;
|
|
96
|
+
scrollbar-color: var(--color-bg-container-secondary) transparent;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.ds-mention-menu__list:focus-visible {
|
|
100
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
101
|
+
outline-offset: -2px;
|
|
102
|
+
border-radius: var(--radius-300);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ============================================
|
|
106
|
+
OPTION
|
|
107
|
+
============================================ */
|
|
108
|
+
|
|
109
|
+
.ds-mention-menu__option {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
flex: none;
|
|
113
|
+
gap: var(--gap-200);
|
|
114
|
+
padding: var(--padding-150) var(--padding-200);
|
|
115
|
+
border-radius: var(--radius-200);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-family: var(--font-paragraph-sm-family);
|
|
118
|
+
font-size: var(--font-paragraph-sm-size);
|
|
119
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
120
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
121
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
122
|
+
color: var(--color-text-primary);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* One highlight, shared by the pointer and the arrow keys: the pointer
|
|
126
|
+
moves it rather than drawing a second one. DropdownMenu's focused wash,
|
|
127
|
+
a translucent neutral, because it is the only sign of where Enter will
|
|
128
|
+
land and an opaque surface step can equal the panel's own fill in a
|
|
129
|
+
theme that flattens the two. */
|
|
130
|
+
.ds-mention-menu__option--active {
|
|
131
|
+
background-color: var(--color-action-passive-bg-hover);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ds-mention-menu__option--disabled {
|
|
135
|
+
opacity: 0.4;
|
|
136
|
+
cursor: not-allowed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ds-mention-menu__icon {
|
|
140
|
+
display: inline-flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
flex: none;
|
|
144
|
+
/* The row's own line box, so an icon never makes its row taller than a
|
|
145
|
+
row without one */
|
|
146
|
+
height: var(--font-paragraph-sm-line-height);
|
|
147
|
+
color: var(--color-icon-secondary);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.ds-mention-menu__icon .material-symbols-rounded {
|
|
151
|
+
--icon-size: var(--icon-size-500);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ds-mention-menu__label {
|
|
155
|
+
flex: none;
|
|
156
|
+
max-width: 100%;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
text-overflow: ellipsis;
|
|
159
|
+
white-space: nowrap;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* While a query narrows the list, the typed part of each name holds full
|
|
163
|
+
strength and the rest steps back. Colour carries it as well as weight,
|
|
164
|
+
because a theme may set both paragraph weights the same. */
|
|
165
|
+
.ds-mention-menu--filtered .ds-mention-menu__label {
|
|
166
|
+
color: var(--color-text-secondary);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.ds-mention-menu__match {
|
|
170
|
+
font-weight: var(--font-paragraph-sm-emphasis-weight);
|
|
171
|
+
color: var(--color-text-primary);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.ds-mention-menu__description {
|
|
175
|
+
flex: 1 1 0;
|
|
176
|
+
min-width: 0;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
text-overflow: ellipsis;
|
|
179
|
+
white-space: nowrap;
|
|
180
|
+
color: var(--color-text-tertiary);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ============================================
|
|
184
|
+
EMPTY
|
|
185
|
+
============================================ */
|
|
186
|
+
|
|
187
|
+
.ds-mention-menu__empty {
|
|
188
|
+
padding: var(--padding-300) var(--padding-400);
|
|
189
|
+
font-family: var(--font-paragraph-sm-family);
|
|
190
|
+
font-size: var(--font-paragraph-sm-size);
|
|
191
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
192
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
193
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
194
|
+
color: var(--color-text-tertiary);
|
|
195
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { MentionItem, MentionSource } from './mentions';
|
|
3
|
+
export * from './mentions';
|
|
4
|
+
type MentionOwnProps = {
|
|
5
|
+
/** The character that opened the mention, drawn a step quieter than the name. */
|
|
6
|
+
trigger?: string;
|
|
7
|
+
/** Additional CSS classes */
|
|
8
|
+
className?: string;
|
|
9
|
+
/** The name: a person, a file, a skill. */
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
};
|
|
12
|
+
export interface MentionProps extends MentionOwnProps, Omit<React.ComponentPropsWithoutRef<'span'>, keyof MentionOwnProps> {
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Mention is a reference written into a sentence: `@` and a name for an
|
|
16
|
+
* entity, `/` and a name for a skill. It is colour and nothing else, with
|
|
17
|
+
* no padding, fill or weight of its own, so the same tag reads correctly
|
|
18
|
+
* inside a message and can be laid exactly over the text of an input while
|
|
19
|
+
* it is being typed.
|
|
20
|
+
*/
|
|
21
|
+
export declare const Mention: React.ForwardRefExoticComponent<MentionProps & React.RefAttributes<HTMLSpanElement>>;
|
|
22
|
+
type MentionTextOwnProps = {
|
|
23
|
+
/** The plain text, exactly as it was typed or sent. */
|
|
24
|
+
text: string;
|
|
25
|
+
/** The sources whose items count as mentions in this text. */
|
|
26
|
+
sources: MentionSource[];
|
|
27
|
+
/** Additional CSS classes */
|
|
28
|
+
className?: string;
|
|
29
|
+
};
|
|
30
|
+
export interface MentionTextProps extends MentionTextOwnProps, Omit<React.ComponentPropsWithoutRef<'span'>, keyof MentionTextOwnProps | 'children'> {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* MentionText draws a plain string with every mention in it as a Mention
|
|
34
|
+
* tag: the sent half of the pattern, for a message that was typed with
|
|
35
|
+
* mentions in it. The text is the only record. Nothing else needs storing.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MentionText: React.ForwardRefExoticComponent<MentionTextProps & React.RefAttributes<HTMLSpanElement>>;
|
|
38
|
+
type MentionMenuOwnProps = {
|
|
39
|
+
/** The items to list, already filtered and ordered. */
|
|
40
|
+
items: MentionItem[];
|
|
41
|
+
/** The `id` of the highlighted item: the one Enter would choose. */
|
|
42
|
+
activeId?: string;
|
|
43
|
+
/** Fires with an item's `id` when the pointer moves onto it. */
|
|
44
|
+
onActiveChange?: (id: string) => void;
|
|
45
|
+
/** Fires with the item a person chose. */
|
|
46
|
+
onSelect?: (item: MentionItem) => void;
|
|
47
|
+
/** The text typed so far. The part of each label that matches it is drawn stronger. */
|
|
48
|
+
query?: string;
|
|
49
|
+
/** Accessible name for the list. */
|
|
50
|
+
label?: string;
|
|
51
|
+
/** Shown in place of the list when `items` is empty. Without it an empty menu renders nothing. */
|
|
52
|
+
emptyText?: React.ReactNode;
|
|
53
|
+
/** Additional CSS classes */
|
|
54
|
+
className?: string;
|
|
55
|
+
};
|
|
56
|
+
export interface MentionMenuProps extends MentionMenuOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof MentionMenuOwnProps | 'children'> {
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* MentionMenu is the list a trigger character opens. It never takes focus:
|
|
60
|
+
* the caret stays in the field the person is typing in, which owns the
|
|
61
|
+
* arrow keys and points `aria-activedescendant` at the highlighted row
|
|
62
|
+
* (`mentionOptionId` builds the id). The menu draws the rows, follows the
|
|
63
|
+
* pointer, and reports a choice.
|
|
64
|
+
*
|
|
65
|
+
* Composer wires all of this itself through its `mentions` prop. Reach for
|
|
66
|
+
* MentionMenu directly to build the pattern on another field.
|
|
67
|
+
*/
|
|
68
|
+
export declare const MentionMenu: React.ForwardRefExoticComponent<MentionMenuProps & React.RefAttributes<HTMLDivElement>>;
|