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 CHANGED
@@ -13,13 +13,11 @@ An open source React design system built for AI products and coding agents: comp
13
13
 
14
14
  ## Why Rift DS
15
15
 
16
- - **One package, zero dependencies.** React is the only required peer dependency; recharts is optional and only for the charts entry. No configuration API, no providers, no build-tool integration: theming is plain CSS custom properties, so it works in any bundler, in any stack, beside anything you already use.
17
- - **Free, open source, consumed your way.** MIT end to end, no pro tier, no paywalled components. Install the npm package, pull single components as source through the shadcn CLI, or clone the repo and own every line; all three are first-class paths.
18
- - **A few primitives re-theme everything.** Every semantic token chains to a primitive, so a handful of overrides cascade through every component, the charts, and the ambient background, in both colour modes. Your brand is a short CSS block, not a fork.
19
- - **The shipped themes prove it.** Each one is the same system under different primitives, saved as a complete look: colour modes, a type pairing with self-hosted fonts, type weight and tracking, icon weight, radius, density, motion, elevation, the chart palette. Setting `data-brand` on the root element applies all of it with zero runtime JavaScript, the build holds every theme's action colours to WCAG AA in both modes, and the playground builds yours the same way.
20
- - **The AI product set is in the box.** Composer, streaming chat thread, tool calls, reasoning, thread panel and tabs: the components an assistant UI needs, the same set this repo's own site chat is built from.
21
- - **Legible to coding agents.** One command installs the agent skill and prints the MCP connect line, and the MCP tools and per-component markdown are generated from the same JSDoc that builds the shipped `.d.ts`: an agent reads the exact contract npm ships, no key, no account.
22
- - **Docs that cannot lie.** Every count, list, and prop table is generated from source registries and build-validated; CI fails on drift, and every component story ships through an accessibility audit. What the docs say is what ships.
16
+ - **<!-- component-count -->150<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. React is the only required peer dependency.
17
+ - **AI patterns and templates, ready to use.** Composer, streaming chat thread, tool calls, reasoning and thread panel are in the box, and the docs site has complete template screens built from them.
18
+ - **Fully themeable.** The shipped themes are complete looks to start from, each in light and dark. One `data-brand` attribute applies a theme, and a few CSS overrides make your own.
19
+ - **Ready for coding agents.** One command installs the agent skills, and a public MCP endpoint serves every component's props, so your agent builds with the library as it actually is. No key, no account.
20
+ - **Free, consumed your way.** MIT, with no pro tier. Install the npm package, pull single components as source through the shadcn CLI, or clone the repo.
23
21
 
24
22
  ## Documentation
25
23
 
@@ -42,7 +40,7 @@ React 19+ is a peer dependency. The package is ESM-only and resolved via `export
42
40
 
43
41
  ## Set up your agent
44
42
 
45
- One command teaches a coding agent the system: it installs the generated agent skill into your project and prints the MCP connect line.
43
+ One command teaches a coding agent the system: it installs the generated agent skills into your project and prints the MCP connect line.
46
44
 
47
45
  ```bash
48
46
  npx rift-ds init
@@ -73,13 +71,13 @@ import 'rift-ds/tokens/presets/presets.css';
73
71
 
74
72
  ## Components
75
73
 
76
- <!-- component-count -->149<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
74
+ <!-- component-count -->150<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
77
75
 
78
76
  <details>
79
77
  <summary>The full list</summary>
80
78
 
81
79
  <!-- component-list:start -->
82
- Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
80
+ Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
83
81
  <!-- component-list:end -->
84
82
 
85
83
  </details>
package/bin/rift-ds.mjs CHANGED
@@ -2,18 +2,28 @@
2
2
  /**
3
3
  * The package's one command: `npx rift-ds init`.
4
4
  *
5
- * Fetches the generated agent skill (SKILL.md + references/components.md)
6
- * from the live site into the project's .claude/skills/ folder, then prints
7
- * the MCP connect line. The files are fetched rather than bundled because
8
- * they regenerate with every site deploy, while the package moves only on a
9
- * release — a bundled copy would always be the staler one. Re-running the
10
- * command overwrites the pair; that is the refresh story.
5
+ * Fetches the generated agent skills from the live site into the
6
+ * project's .claude/skills/ folder, one folder per skill, then prints the
7
+ * MCP connect line. Which skills exist and which files each holds comes
8
+ * from the site's /skill/manifest.json, never from a list in this file:
9
+ * the skills regenerate with every site deploy while the package moves
10
+ * only on a release, so a bundled list (or a bundled copy of the files)
11
+ * would always be the staler one, and a skill added to the site reaches
12
+ * this command with no new version of the package. Re-running overwrites
13
+ * what is there; that is the refresh story.
14
+ *
15
+ * The manifest is data from the network that decides where this command
16
+ * writes, so every name and path in it is checked before anything touches
17
+ * the disk: a skill name is one kebab-case folder, a file is a plain
18
+ * relative path, and nothing may climb out of the target folder.
11
19
  *
12
20
  * Zero dependencies by design: global fetch plus node:fs, so npx never
13
21
  * installs anything beyond the package itself. SITE_ORIGIN, SKILL_NAME
14
22
  * and MCP_SERVER_NAME are stamped by build-package.mjs from
15
23
  * scripts/brand.mjs (the one home for brand facts); running this file
16
24
  * straight from source fails fast rather than guessing a domain.
25
+ * SKILL_NAME is the library skill, kept only for --out without --skill,
26
+ * which has always meant that one.
17
27
  */
18
28
  import { mkdirSync, writeFileSync } from 'node:fs';
19
29
  import { dirname, join, resolve } from 'node:path';
@@ -21,46 +31,100 @@ import { dirname, join, resolve } from 'node:path';
21
31
  const SITE_ORIGIN = 'https://rift-ds.com';
22
32
  const SKILL_NAME = 'rift-design-system';
23
33
  const MCP_SERVER_NAME = 'rift-ds';
24
- const SKILL_FILES = ['SKILL.md', 'references/components.md'];
25
- const DEFAULT_OUT = join('.claude', 'skills', SKILL_NAME);
34
+ const MANIFEST_URL = `${SITE_ORIGIN}/skill/manifest.json`;
35
+ const DEFAULT_DIR = join('.claude', 'skills');
26
36
 
27
- const USAGE = `Usage: npx rift-ds init [--out <dir>]
37
+ const USAGE = `Usage: npx rift-ds init [--skill <name>] [--dir <dir>]
28
38
 
29
- Fetches the agent skill for this package from ${SITE_ORIGIN} into
30
- ${DEFAULT_OUT}/ so a skill-capable coding agent carries the
31
- library's install, theming and catalogue rules into every session.
32
- Re-run any time to refresh; the files regenerate with every site deploy.
39
+ Fetches the agent skills for this package from ${SITE_ORIGIN} into
40
+ ${DEFAULT_DIR}/, one folder per skill, so a skill-capable coding agent
41
+ carries the library's install, theming and catalogue rules into every
42
+ session. Re-run any time to refresh; the files regenerate with every
43
+ site deploy.
33
44
 
34
45
  Options:
35
- --out <dir> Write the skill somewhere else (default: ${DEFAULT_OUT})
36
- --help Show this message`;
46
+ --skill <name> Install one skill instead of all of them
47
+ --dir <dir> Folder that receives one folder per skill (default: ${DEFAULT_DIR})
48
+ --out <dir> Deprecated: write one skill's files straight into <dir>
49
+ (${SKILL_NAME} unless --skill names another). Use --dir.
50
+ --help Show this message
51
+
52
+ The list of skills is at ${MANIFEST_URL}`;
37
53
 
38
54
  function fail(message) {
39
55
  console.error(`✗ ${message}`);
40
56
  process.exit(1);
41
57
  }
42
58
 
43
- async function init(outDir) {
59
+ async function get(url) {
60
+ let response;
61
+ try {
62
+ response = await fetch(url);
63
+ } catch (error) {
64
+ fail(`could not reach ${url} (${error?.message ?? error}). Check your network and retry.`);
65
+ }
66
+ if (!response.ok) {
67
+ fail(`${url} answered ${response.status}. The site may be mid-deploy; retry in a minute.`);
68
+ }
69
+ return response.text();
70
+ }
71
+
72
+ /** The manifest's skills, with every name and path checked before use. */
73
+ async function readManifest() {
74
+ let manifest;
75
+ try {
76
+ manifest = JSON.parse(await get(MANIFEST_URL));
77
+ } catch {
78
+ fail(`${MANIFEST_URL} is not valid JSON. The site may be mid-deploy; retry in a minute.`);
79
+ }
80
+ const skills = manifest?.skills;
81
+ if (!Array.isArray(skills) || skills.length === 0) {
82
+ fail(`${MANIFEST_URL} lists no skills. Update the package (npm install rift-ds@latest) and retry.`);
83
+ }
84
+ const segment = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
85
+ for (const skill of skills) {
86
+ const filesOk =
87
+ Array.isArray(skill?.files) &&
88
+ skill.files.length > 0 &&
89
+ skill.files.every(
90
+ (file) => typeof file === 'string' && file.split('/').every((part) => segment.test(part))
91
+ );
92
+ if (typeof skill?.name !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(skill.name) || !filesOk) {
93
+ fail(`${MANIFEST_URL} holds an entry this command will not write to disk. Nothing was installed.`);
94
+ }
95
+ }
96
+ return skills;
97
+ }
98
+
99
+ async function init({ only, dir, out }) {
44
100
  if (SITE_ORIGIN.startsWith('__')) {
45
101
  fail('this copy was not built: run the published bin (npx rift-ds init).');
46
102
  }
47
- for (const file of SKILL_FILES) {
48
- const url = `${SITE_ORIGIN}/skill/${SKILL_NAME}/${file}`;
49
- let response;
50
- try {
51
- response = await fetch(url);
52
- } catch (error) {
53
- fail(`could not reach ${url} (${error?.message ?? error}). Check your network and retry.`);
54
- }
55
- if (!response.ok) {
56
- fail(`${url} answered ${response.status}. The site may be mid-deploy; retry in a minute.`);
103
+ const skills = await readManifest();
104
+ const wanted = only ?? (out ? SKILL_NAME : null);
105
+ const selected = wanted ? skills.filter((skill) => skill.name === wanted) : skills;
106
+ if (selected.length === 0) {
107
+ fail(`no skill named "${wanted}". Available: ${skills.map((skill) => skill.name).join(', ')}.`);
108
+ }
109
+ for (const skill of selected) {
110
+ const target = out ?? join(dir, skill.name);
111
+ for (const file of skill.files) {
112
+ const text = await get(`${SITE_ORIGIN}/skill/${skill.name}/${file}`);
113
+ const path = join(target, ...file.split('/'));
114
+ mkdirSync(dirname(path), { recursive: true });
115
+ writeFileSync(path, text);
116
+ console.log(`▸ ${path}`);
57
117
  }
58
- const target = join(outDir, file);
59
- mkdirSync(dirname(target), { recursive: true });
60
- writeFileSync(target, await response.text());
61
- console.log(`▸ ${target}`);
62
118
  }
63
- console.log(`✓ Agent skill installed. Skill-capable agents load it automatically.`);
119
+ const names = selected.map((skill) => skill.name).join(', ');
120
+ console.log(
121
+ selected.length === 1
122
+ ? `✓ Agent skill installed: ${names}. Skill-capable agents load it automatically.`
123
+ : `✓ ${selected.length} agent skills installed: ${names}. Skill-capable agents load them automatically.`
124
+ );
125
+ if (out) {
126
+ console.log(`\n--out is deprecated. Use --dir to choose the folder that holds the skill folders.`);
127
+ }
64
128
  console.log(`\nFor on-demand lookups too, connect the MCP endpoint:`);
65
129
  console.log(` claude mcp add --transport http ${MCP_SERVER_NAME} ${SITE_ORIGIN}/api/mcp`);
66
130
  }
@@ -73,10 +137,20 @@ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
73
137
  if (args[0] !== 'init') {
74
138
  fail(`unknown command "${args[0]}"\n\n${USAGE}`);
75
139
  }
76
- const outFlag = args.indexOf('--out');
77
- let outDir = DEFAULT_OUT;
78
- if (outFlag !== -1) {
79
- if (!args[outFlag + 1]) fail('--out needs a directory');
80
- outDir = resolve(args[outFlag + 1]);
140
+ /** The value after a flag, or undefined when the flag is absent. */
141
+ function option(flag, what) {
142
+ const at = args.indexOf(flag);
143
+ if (at === -1) return undefined;
144
+ const value = args[at + 1];
145
+ if (!value || value.startsWith('--')) fail(`${flag} needs ${what}`);
146
+ return value;
81
147
  }
82
- await init(outDir);
148
+ const only = option('--skill', 'a skill name');
149
+ const dirFlag = option('--dir', 'a directory');
150
+ const outFlag = option('--out', 'a directory');
151
+ if (dirFlag && outFlag) fail('use --dir or --out, not both');
152
+ await init({
153
+ only,
154
+ dir: dirFlag ? resolve(dirFlag) : DEFAULT_DIR,
155
+ out: outFlag ? resolve(outFlag) : undefined,
156
+ });
@@ -189,8 +189,8 @@
189
189
  color: var(--color-text-secondary);
190
190
  }
191
191
 
192
- /* ThreadPanel's unread dot, restated at the same size: the gap scale,
193
- because no icon token is that small. */
192
+ /* StatusDot's `xs` mark restated at the same size: the gap scale, because
193
+ no icon token is that small. */
194
194
  .ds-agent-rail__status-dot {
195
195
  width: var(--gap-200);
196
196
  height: var(--gap-200);
@@ -674,6 +674,20 @@ button.ds-attachment-group__single:hover {
674
674
  height: auto;
675
675
  }
676
676
 
677
+ /* With its dimensions known the picture holds its final box from the first
678
+ frame: its own width, or the width the height cap allows at its ratio,
679
+ whichever is smaller. The ratio cap sits in the width rather than beside
680
+ the row's percentage in max-width, because a percentage there drops out
681
+ of the frame's shrink-to-fit measure and the frame would size to the
682
+ uncapped width. The height follows from the ratio the width and height
683
+ attributes carry, so nothing moves when the pixels arrive. */
684
+ .ds-attachment-group__single-image--sized {
685
+ width: min(
686
+ var(--ds-attachment-single-width, 100%),
687
+ calc(var(--ds-attachment-single-max-height, 320px) * var(--ds-attachment-single-ratio, 1))
688
+ );
689
+ }
690
+
677
691
  .ds-attachment-group__more:focus-visible,
678
692
  .ds-attachment-group__less:focus-visible,
679
693
  button.ds-attachment-group__single:focus-visible {
@@ -1,7 +1,5 @@
1
1
  import { default as React } from 'react';
2
2
  import { AttachmentItem } from './fileTypes';
3
- export { revokeAttachmentUrls, useAttachments } from './useAttachments';
4
- export type { AttachmentRejection, AttachmentRejectionReason, AttachmentUploadContext, UseAttachmentsOptions, UseAttachmentsResult, } from './useAttachments';
5
3
  /** Props owned by AttachmentGroup itself — everything else falls through to the root element. */
6
4
  type AttachmentGroupOwnProps = {
7
5
  /** The files on the message, in order. */
@@ -58,3 +56,5 @@ export interface AttachmentGroupProps extends AttachmentGroupOwnProps, Omit<Reac
58
56
  * a record and opens nothing.
59
57
  */
60
58
  export declare const AttachmentGroup: React.ForwardRefExoticComponent<AttachmentGroupProps & React.RefAttributes<HTMLDivElement>>;
59
+ export { revokeAttachmentUrls, useAttachments } from './useAttachments';
60
+ export type { AttachmentRejection, AttachmentRejectionReason, AttachmentUploadContext, UseAttachmentsOptions, UseAttachmentsResult, } from './useAttachments';
@@ -63,14 +63,19 @@ const AttachmentGroup = React.forwardRef(
63
63
  let layout;
64
64
  if (single) {
65
65
  const onClick = clickHandler(single);
66
+ const sized = Boolean(single.width && single.height);
66
67
  const image = /* @__PURE__ */ jsx(
67
68
  "img",
68
69
  {
69
- className: `${baseClass}__single-image`,
70
+ className: [`${baseClass}__single-image`, sized && `${baseClass}__single-image--sized`].filter(Boolean).join(" "),
70
71
  src: single.previewSrc,
71
72
  alt: single.previewAlt ?? single.name,
72
73
  width: single.width,
73
- height: single.height
74
+ height: single.height,
75
+ style: sized ? {
76
+ "--ds-attachment-single-width": `${single.width}px`,
77
+ "--ds-attachment-single-ratio": single.width / single.height
78
+ } : void 0
74
79
  }
75
80
  );
76
81
  const badge = /* @__PURE__ */ jsx("span", { className: `${baseClass}__single-mark`, "aria-hidden": "true", children: renderTypeMark(single.name, single.kind, { form: "badge" }) });
@@ -41,6 +41,9 @@
41
41
  background-color: var(--color-input-bg-primary);
42
42
  border: var(--border-025) solid var(--color-input-border-primary);
43
43
  border-radius: var(--ds-composer-radius);
44
+ /* Where the typed text starts and ends. One value, because the mention
45
+ overlay has to sit on exactly the rail the textarea does. */
46
+ --ds-composer-text-inset: var(--padding-500);
44
47
  box-shadow: var(--shadow-floating);
45
48
  transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
46
49
  /* The whole shell is the input affordance — clicking anywhere that is
@@ -304,9 +307,12 @@
304
307
  margin-left: calc(-1 * var(--ds-composer-context-icon-bearing, var(--icon-size-500) * 4 / 24));
305
308
  }
306
309
 
307
- .ds-composer--context-above .ds-composer__attachments,
308
- .ds-composer--context-above .ds-composer__content {
309
- padding-inline: calc(var(--padding-200) + var(--padding-400));
310
+ .ds-composer--context-above {
311
+ --ds-composer-text-inset: calc(var(--padding-200) + var(--padding-400));
312
+ }
313
+
314
+ .ds-composer--context-above .ds-composer__attachments {
315
+ padding-inline: var(--ds-composer-text-inset);
310
316
  }
311
317
 
312
318
  /* The text owns the truncation: one line, ellipsized only when the note
@@ -439,7 +445,7 @@
439
445
  align-items: flex-start;
440
446
  box-sizing: border-box;
441
447
  width: 100%;
442
- padding: var(--padding-400) var(--padding-500) var(--padding-200);
448
+ padding: var(--padding-400) var(--ds-composer-text-inset) var(--padding-200);
443
449
  /* Growing and shrinking is animated, which needs two pixel heights to run
444
450
  between — `auto` has nothing to interpolate. So the zone states its own
445
451
  height: the measured textarea (published as --ds-composer-text-height on
@@ -507,6 +513,76 @@
507
513
  }
508
514
  }
509
515
 
516
+ /* ============================================
517
+ MENTIONS — armed by the `mentions` prop
518
+ A textarea cannot colour part of its own text, so the words are drawn
519
+ twice: the textarea keeps the caret, the selection and the keystrokes
520
+ with its text transparent, and an overlay of the same string, in the
521
+ same type on the same rail, is what a person reads. Only colour differs
522
+ between the two, so they can never drift apart by a glyph.
523
+ ============================================ */
524
+
525
+ .ds-composer--mentions .ds-composer__content {
526
+ position: relative;
527
+ }
528
+
529
+ .ds-composer--mentions .ds-composer__textarea {
530
+ /* Above the overlay: a tag carries a fill, and the caret and the
531
+ selection highlight must never be painted under it */
532
+ position: relative;
533
+ z-index: 1;
534
+ color: transparent;
535
+ caret-color: var(--color-input-text-primary);
536
+ }
537
+
538
+ .ds-composer__backdrop {
539
+ position: absolute;
540
+ top: var(--padding-400);
541
+ left: var(--ds-composer-text-inset);
542
+ /* The textarea's measured box: its height, and its client width, which
543
+ is what its text wraps at once a scrollbar takes its share */
544
+ width: var(
545
+ --ds-composer-text-width,
546
+ calc(100% - 2 * var(--ds-composer-text-inset))
547
+ );
548
+ height: var(--ds-composer-text-height, var(--font-paragraph-line-height));
549
+ overflow: hidden;
550
+ pointer-events: none;
551
+ }
552
+
553
+ .ds-composer__backdrop-text {
554
+ font-family: var(--font-paragraph-family);
555
+ font-size: var(--font-paragraph-size);
556
+ font-weight: var(--font-paragraph-weight);
557
+ line-height: var(--font-paragraph-line-height);
558
+ letter-spacing: var(--font-paragraph-letter-spacing);
559
+ color: var(--color-input-text-primary);
560
+ /* A textarea's own wrapping rules */
561
+ white-space: pre-wrap;
562
+ overflow-wrap: break-word;
563
+ }
564
+
565
+ /* The menu a trigger opens. It grows out of the line the trigger is on:
566
+ `left` and the two line offsets are set inline from where the trigger
567
+ character sits, and the menu rests a hair clear of that line, over the
568
+ text before it (top) or after it (bottom). Until the first measurement
569
+ the fallbacks seat it against the shell's edge. */
570
+ .ds-composer__mention-menu {
571
+ position: absolute;
572
+ left: 0;
573
+ z-index: 10;
574
+ /* A list, not a text field */
575
+ cursor: default;
576
+ }
577
+
578
+ .ds-composer__mention-menu--top {
579
+ bottom: calc(100% - var(--ds-composer-mention-line-top, 0px) + var(--gap-100));
580
+ }
581
+
582
+ .ds-composer__mention-menu--bottom {
583
+ top: calc(var(--ds-composer-mention-line-bottom, 100%) + var(--gap-100));
584
+ }
585
+
510
586
  /* ============================================
511
587
  FOOTER ROW
512
588
  ============================================ */
@@ -560,3 +636,11 @@
560
636
  cursor: not-allowed;
561
637
  color: var(--color-input-text-disabled);
562
638
  }
639
+
640
+ .ds-composer--disabled .ds-composer__backdrop-text {
641
+ color: var(--color-input-text-disabled);
642
+ }
643
+
644
+ .ds-composer--disabled.ds-composer--mentions .ds-composer__textarea {
645
+ color: transparent;
646
+ }
@@ -1,5 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { AttachmentItem } from '../Attachment/fileTypes';
3
+ import { MentionItem, MentionSource } from '../Mention/Mention';
3
4
  /** Props owned by Composer itself — everything else falls through to the <textarea>. */
4
5
  type ComposerOwnProps = {
5
6
  /** Current value for controlled use. Pair with `onValueChange`. */
@@ -116,6 +117,26 @@ type ComposerOwnProps = {
116
117
  * @deprecated Use `files` with `onFilesSelected` and `onFileRemove`, which draw the queue as tiles.
117
118
  */
118
119
  attachments?: React.ReactNode;
120
+ /**
121
+ * What a person can reference while typing, one source per trigger
122
+ * character: `@` for entities, `/` for skills. Typing a trigger at the
123
+ * start of a word opens a menu of that source's items, filtered as the
124
+ * word grows. Arrows move through it, Enter or Tab writes the choice into
125
+ * the text, Escape closes it. A mention is drawn in the tag colour
126
+ * wherever its exact name stands in the text; the value stays a plain
127
+ * string.
128
+ */
129
+ mentions?: MentionSource[];
130
+ /** Fires with the item a person chose from the mention menu, and the trigger that opened it. */
131
+ onMentionSelect?: (item: MentionItem, trigger: string) => void;
132
+ /**
133
+ * Which side of the trigger's line the mention menu opens on: `top` lays
134
+ * it over the text above, `bottom` over the text below. `top` suits a
135
+ * composer at the foot of a view.
136
+ */
137
+ mentionPlacement?: 'top' | 'bottom';
138
+ /** Builds the sentence announced to assistive technology when the mention menu opens or its matches change. */
139
+ formatMentionAnnouncement?: (count: number) => string;
119
140
  /** Leading actions on the left of the action bar, after the attach button (a model picker). */
120
141
  actions?: React.ReactNode;
121
142
  /**