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.
@@ -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__ */ jsx("div", { className: `${baseClass}__content`, ref: contentRef, children: /* @__PURE__ */ jsx(
257
- "textarea",
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
- ...rest,
260
- ref: setTextareaRef,
261
- className: `${baseClass}__textarea`,
262
- rows: 1,
263
- value: currentValue,
264
- "aria-label": ariaLabel,
265
- onChange: handleChange,
266
- onKeyDown: handleKeyDown,
267
- onPaste: handlePaste
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>>;