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
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
|
-
-
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
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
|
|
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 -->
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
25
|
-
const
|
|
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 [--
|
|
37
|
+
const USAGE = `Usage: npx rift-ds init [--skill <name>] [--dir <dir>]
|
|
28
38
|
|
|
29
|
-
Fetches the agent
|
|
30
|
-
${
|
|
31
|
-
library's install, theming and catalogue rules into every
|
|
32
|
-
Re-run any time to refresh; the files regenerate with every
|
|
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
|
-
--
|
|
36
|
-
--
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
193
|
-
|
|
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
|
|
308
|
-
|
|
309
|
-
|
|
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(--
|
|
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
|
/**
|