rift-ds 1.3.0 → 1.5.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 +3 -3
- package/components/Attachment/Attachment.css +1036 -0
- package/components/Attachment/AttachmentDropZone.d.ts +39 -0
- package/components/Attachment/AttachmentDropZone.js +76 -0
- package/components/Attachment/AttachmentGroup.d.ts +60 -0
- package/components/Attachment/AttachmentGroup.js +179 -0
- package/components/Attachment/AttachmentTile.d.ts +80 -0
- package/components/Attachment/AttachmentTile.js +143 -0
- package/components/Attachment/AttachmentViewer.d.ts +66 -0
- package/components/Attachment/AttachmentViewer.js +215 -0
- package/components/Attachment/fileTypes.d.ts +101 -0
- package/components/Attachment/fileTypes.js +182 -0
- package/components/Attachment/typeMark.d.ts +11 -0
- package/components/Attachment/typeMark.js +51 -0
- package/components/Attachment/useAttachments.d.ts +65 -0
- package/components/Attachment/useAttachments.js +201 -0
- package/components/ChatMessage/ChatMessage.css +24 -0
- package/components/ChatMessage/ChatMessage.d.ts +6 -0
- package/components/ChatMessage/ChatMessage.js +8 -5
- package/components/Composer/Composer.css +95 -2
- package/components/Composer/Composer.d.ts +57 -8
- package/components/Composer/Composer.js +179 -6
- package/components/ContextMenu/ContextMenu.css +25 -0
- package/components/ContextMenu/ContextMenu.js +14 -6
- package/components/DocumentChip/DocumentChip.d.ts +7 -5
- package/components/DropdownMenu/DropdownMenu.css +29 -0
- package/components/DropdownMenu/DropdownMenu.d.ts +2 -0
- package/components/DropdownMenu/DropdownMenu.js +15 -7
- package/components/FileInput/FileInput.js +1 -7
- package/components/MessageCard/MessageCard.d.ts +1 -1
- package/components/Quote/Quote.css +2 -2
- package/components/ShaderField/useShaderField.d.ts +4 -4
- package/components/Stat/Stat.css +2 -2
- package/components/registry.json +38 -2
- package/components/registry.json.d.ts +38 -2
- package/components/registry.json.js +1 -1
- package/fonts/presets/fredoka-300-700-latin-ext.woff2 +0 -0
- package/fonts/presets/fredoka-300-700-latin.woff2 +0 -0
- package/fonts/presets/manrope-300-800-latin-ext.woff2 +0 -0
- package/fonts/presets/manrope-300-800-latin.woff2 +0 -0
- package/fonts/presets/nunito-300-800-latin-ext.woff2 +0 -0
- package/fonts/presets/nunito-300-800-latin.woff2 +0 -0
- package/fonts/presets/playfair-display-400-800-latin-ext.woff2 +0 -0
- package/fonts/presets/playfair-display-400-800-latin.woff2 +0 -0
- package/fonts/presets/plus-jakarta-sans-300-800-latin-ext.woff2 +0 -0
- package/fonts/presets/plus-jakarta-sans-300-800-latin.woff2 +0 -0
- package/fonts/presets/source-sans-3-300-700-latin.woff2 +0 -0
- package/index.d.ts +4 -0
- package/index.js +19 -0
- package/package.json +9 -1
- package/tokens/presets/contrast.css +9 -9
- package/tokens/presets/coral.css +6 -6
- package/tokens/presets/forest.css +10 -10
- package/tokens/presets/gold.css +119 -11
- package/tokens/presets/mono.css +2 -2
- package/tokens/presets/pink.css +104 -24
- package/tokens/presets/terminal.css +13 -13
- package/tokens/presets/violet.css +114 -26
- package/tokens/presets/warm.css +10 -10
- package/tokens/presets/zest.css +10 -10
- package/tokens/registry.json +4 -0
- package/tokens/registry.json.d.ts +4 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +6 -0
- package/tokens/tokens-light.css +12 -0
- package/tokens/tokens-primitives.css +10 -0
- package/tokens/tokens-typography.css +1 -1
- package/fonts/presets/fraunces-300-700-latin-ext.woff2 +0 -0
- package/fonts/presets/fraunces-300-700-latin.woff2 +0 -0
- package/fonts/presets/work-sans-300-700-latin-ext.woff2 +0 -0
- package/fonts/presets/work-sans-300-700-latin.woff2 +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
3
|
+
import { matchesAccept, getFileKind } from "./fileTypes.js";
|
|
4
|
+
function revokeAttachmentUrls(items) {
|
|
5
|
+
for (const item of items) {
|
|
6
|
+
if (item.previewSrc?.startsWith("blob:")) URL.revokeObjectURL(item.previewSrc);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const DEFAULT_EXCERPT_LENGTH = 240;
|
|
10
|
+
let nextId = 0;
|
|
11
|
+
const createId = () => `attachment-${Date.now().toString(36)}-${(nextId++).toString(36)}`;
|
|
12
|
+
const fileIdentity = (file) => `${file.name}:${file.size}:${file.lastModified}`;
|
|
13
|
+
function useAttachments(options = {}) {
|
|
14
|
+
const [items, setItems] = useState([]);
|
|
15
|
+
const itemsRef = useRef([]);
|
|
16
|
+
const sourcesRef = useRef(/* @__PURE__ */ new Map());
|
|
17
|
+
const abortsRef = useRef(/* @__PURE__ */ new Map());
|
|
18
|
+
const optionsRef = useRef(options);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
optionsRef.current = options;
|
|
21
|
+
});
|
|
22
|
+
const commit = useCallback((next) => {
|
|
23
|
+
itemsRef.current = next;
|
|
24
|
+
setItems(next);
|
|
25
|
+
}, []);
|
|
26
|
+
const patch = useCallback(
|
|
27
|
+
(id, change) => {
|
|
28
|
+
if (!itemsRef.current.some((item) => item.id === id)) return;
|
|
29
|
+
commit(itemsRef.current.map((item) => item.id === id ? { ...item, ...change } : item));
|
|
30
|
+
},
|
|
31
|
+
[commit]
|
|
32
|
+
);
|
|
33
|
+
const startUpload = useCallback(
|
|
34
|
+
(id, file) => {
|
|
35
|
+
const upload = optionsRef.current.upload;
|
|
36
|
+
if (!upload) return;
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
abortsRef.current.set(id, controller);
|
|
39
|
+
upload(file, {
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
onProgress: (percent) => {
|
|
42
|
+
if (!controller.signal.aborted)
|
|
43
|
+
patch(id, { progress: Math.max(0, Math.min(100, percent)) });
|
|
44
|
+
}
|
|
45
|
+
}).then(
|
|
46
|
+
() => {
|
|
47
|
+
if (controller.signal.aborted) return;
|
|
48
|
+
abortsRef.current.delete(id);
|
|
49
|
+
patch(id, { status: "ready", progress: void 0, error: void 0 });
|
|
50
|
+
},
|
|
51
|
+
(reason) => {
|
|
52
|
+
if (controller.signal.aborted) return;
|
|
53
|
+
abortsRef.current.delete(id);
|
|
54
|
+
patch(id, {
|
|
55
|
+
status: "error",
|
|
56
|
+
progress: void 0,
|
|
57
|
+
error: reason instanceof Error && reason.message !== "" ? reason.message : void 0
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
[patch]
|
|
63
|
+
);
|
|
64
|
+
const add = useCallback(
|
|
65
|
+
(files) => {
|
|
66
|
+
const { accept, maxSize, maxCount, allowDuplicates, upload, onReject } = optionsRef.current;
|
|
67
|
+
const rejections = [];
|
|
68
|
+
const accepted = [];
|
|
69
|
+
const known = /* @__PURE__ */ new Set();
|
|
70
|
+
for (const source of sourcesRef.current.values()) {
|
|
71
|
+
if (typeof source !== "string") known.add(fileIdentity(source));
|
|
72
|
+
}
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
if (accept && !matchesAccept(file, accept)) {
|
|
75
|
+
rejections.push({ file, reason: "type" });
|
|
76
|
+
} else if (maxSize !== void 0 && file.size > maxSize) {
|
|
77
|
+
rejections.push({ file, reason: "size" });
|
|
78
|
+
} else if (!allowDuplicates && known.has(fileIdentity(file))) {
|
|
79
|
+
rejections.push({ file, reason: "duplicate" });
|
|
80
|
+
} else if (maxCount !== void 0 && itemsRef.current.length + accepted.length >= maxCount) {
|
|
81
|
+
rejections.push({ file, reason: "count" });
|
|
82
|
+
} else {
|
|
83
|
+
known.add(fileIdentity(file));
|
|
84
|
+
const kind = getFileKind(file);
|
|
85
|
+
accepted.push({
|
|
86
|
+
file,
|
|
87
|
+
item: {
|
|
88
|
+
id: createId(),
|
|
89
|
+
name: file.name,
|
|
90
|
+
kind,
|
|
91
|
+
size: file.size,
|
|
92
|
+
mimeType: file.type || void 0,
|
|
93
|
+
status: upload ? "uploading" : "ready",
|
|
94
|
+
previewSrc: kind === "image" ? URL.createObjectURL(file) : void 0
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (accepted.length > 0) {
|
|
100
|
+
for (const { item, file } of accepted) sourcesRef.current.set(item.id, file);
|
|
101
|
+
commit([...itemsRef.current, ...accepted.map(({ item }) => item)]);
|
|
102
|
+
for (const { item, file } of accepted) startUpload(item.id, file);
|
|
103
|
+
}
|
|
104
|
+
if (rejections.length > 0) onReject?.(rejections);
|
|
105
|
+
return rejections;
|
|
106
|
+
},
|
|
107
|
+
[commit, startUpload]
|
|
108
|
+
);
|
|
109
|
+
const addText = useCallback(
|
|
110
|
+
(text, name = "Pasted text") => {
|
|
111
|
+
const { maxCount, excerptLength = DEFAULT_EXCERPT_LENGTH } = optionsRef.current;
|
|
112
|
+
if (maxCount !== void 0 && itemsRef.current.length >= maxCount) return;
|
|
113
|
+
const id = createId();
|
|
114
|
+
sourcesRef.current.set(id, text);
|
|
115
|
+
commit([
|
|
116
|
+
...itemsRef.current,
|
|
117
|
+
{
|
|
118
|
+
id,
|
|
119
|
+
name,
|
|
120
|
+
kind: "pasted",
|
|
121
|
+
// Bytes, not characters: the size line should match what a file of
|
|
122
|
+
// this text would weigh.
|
|
123
|
+
size: new Blob([text]).size,
|
|
124
|
+
mimeType: "text/plain",
|
|
125
|
+
status: "ready",
|
|
126
|
+
excerpt: text.trim().slice(0, excerptLength)
|
|
127
|
+
}
|
|
128
|
+
]);
|
|
129
|
+
},
|
|
130
|
+
[commit]
|
|
131
|
+
);
|
|
132
|
+
const release = useCallback((dropped) => {
|
|
133
|
+
for (const item of dropped) {
|
|
134
|
+
abortsRef.current.get(item.id)?.abort();
|
|
135
|
+
abortsRef.current.delete(item.id);
|
|
136
|
+
sourcesRef.current.delete(item.id);
|
|
137
|
+
}
|
|
138
|
+
revokeAttachmentUrls(dropped);
|
|
139
|
+
}, []);
|
|
140
|
+
const remove = useCallback(
|
|
141
|
+
(id) => {
|
|
142
|
+
const dropped = itemsRef.current.filter((item) => item.id === id);
|
|
143
|
+
if (dropped.length === 0) return;
|
|
144
|
+
release(dropped);
|
|
145
|
+
commit(itemsRef.current.filter((item) => item.id !== id));
|
|
146
|
+
},
|
|
147
|
+
[commit, release]
|
|
148
|
+
);
|
|
149
|
+
const retry = useCallback(
|
|
150
|
+
(id) => {
|
|
151
|
+
const source = sourcesRef.current.get(id);
|
|
152
|
+
if (source === void 0 || typeof source === "string") return;
|
|
153
|
+
if (!optionsRef.current.upload) return;
|
|
154
|
+
patch(id, { status: "uploading", progress: void 0, error: void 0 });
|
|
155
|
+
startUpload(id, source);
|
|
156
|
+
},
|
|
157
|
+
[patch, startUpload]
|
|
158
|
+
);
|
|
159
|
+
const clear = useCallback(() => {
|
|
160
|
+
release(itemsRef.current);
|
|
161
|
+
commit([]);
|
|
162
|
+
}, [commit, release]);
|
|
163
|
+
const take = useCallback(() => {
|
|
164
|
+
const taken = itemsRef.current;
|
|
165
|
+
const files = [];
|
|
166
|
+
for (const item of taken) {
|
|
167
|
+
const source = sourcesRef.current.get(item.id);
|
|
168
|
+
if (source !== void 0 && typeof source !== "string") files.push(source);
|
|
169
|
+
abortsRef.current.get(item.id)?.abort();
|
|
170
|
+
}
|
|
171
|
+
abortsRef.current.clear();
|
|
172
|
+
sourcesRef.current.clear();
|
|
173
|
+
commit([]);
|
|
174
|
+
return { items: taken, files };
|
|
175
|
+
}, [commit]);
|
|
176
|
+
const getSource = useCallback((id) => sourcesRef.current.get(id), []);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const aborts = abortsRef.current;
|
|
179
|
+
return () => {
|
|
180
|
+
for (const controller of aborts.values()) controller.abort();
|
|
181
|
+
aborts.clear();
|
|
182
|
+
revokeAttachmentUrls(itemsRef.current);
|
|
183
|
+
itemsRef.current = [];
|
|
184
|
+
};
|
|
185
|
+
}, []);
|
|
186
|
+
return {
|
|
187
|
+
items,
|
|
188
|
+
add,
|
|
189
|
+
addText,
|
|
190
|
+
remove,
|
|
191
|
+
retry,
|
|
192
|
+
clear,
|
|
193
|
+
take,
|
|
194
|
+
getSource,
|
|
195
|
+
isUploading: items.some((item) => item.status === "uploading")
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
revokeAttachmentUrls,
|
|
200
|
+
useAttachments
|
|
201
|
+
};
|
|
@@ -87,6 +87,30 @@
|
|
|
87
87
|
color: var(--color-text-tertiary);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/* ============================================
|
|
91
|
+
ATTACHMENTS
|
|
92
|
+
Files sent with the turn: outside the bubble, above the text, on the
|
|
93
|
+
speaker's side (the body's own alignment places them).
|
|
94
|
+
============================================ */
|
|
95
|
+
|
|
96
|
+
.ds-chat-message__attachments {
|
|
97
|
+
display: flex;
|
|
98
|
+
/* The full row, not shrink-to-fit: the group inside sizes itself against
|
|
99
|
+
this width, and a row that took its width from the group would give it
|
|
100
|
+
nothing to measure */
|
|
101
|
+
width: 100%;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ds-chat-message--user .ds-chat-message__attachments {
|
|
106
|
+
justify-content: flex-end;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* A compact thread seats smaller tiles */
|
|
110
|
+
.ds-chat-message--compact .ds-chat-message__attachments {
|
|
111
|
+
--ds-attachment-tile-size: 96px;
|
|
112
|
+
}
|
|
113
|
+
|
|
90
114
|
/* ============================================
|
|
91
115
|
CONTENT
|
|
92
116
|
============================================ */
|
|
@@ -49,6 +49,12 @@ type ChatMessageOwnProps = {
|
|
|
49
49
|
* feedback row — rather than a secondary affordance.
|
|
50
50
|
*/
|
|
51
51
|
showActions?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Files sent with this turn, drawn outside the bubble on the speaker's
|
|
54
|
+
* side, above the text. Pass an AttachmentGroup. A turn that carries
|
|
55
|
+
* files and no text draws no bubble at all.
|
|
56
|
+
*/
|
|
57
|
+
attachments?: React.ReactNode;
|
|
52
58
|
/** Footer slot under the content — a sources row, an edited note. */
|
|
53
59
|
footer?: React.ReactNode;
|
|
54
60
|
/** Additional CSS classes */
|
|
@@ -16,6 +16,7 @@ const ChatMessage = React.forwardRef(
|
|
|
16
16
|
pendingLabel = "Waiting for a reply",
|
|
17
17
|
actions,
|
|
18
18
|
showActions = false,
|
|
19
|
+
attachments,
|
|
19
20
|
footer,
|
|
20
21
|
className = "",
|
|
21
22
|
children,
|
|
@@ -24,6 +25,10 @@ const ChatMessage = React.forwardRef(
|
|
|
24
25
|
const baseClass = "ds-chat-message";
|
|
25
26
|
const hasBubble = bubble ?? role === "user";
|
|
26
27
|
const hideAvatar = grouped || !showAvatar;
|
|
28
|
+
const hasChildren = React.Children.toArray(children).some(
|
|
29
|
+
(child) => typeof child !== "string" || child.trim() !== ""
|
|
30
|
+
);
|
|
31
|
+
const showContent = pending || hasChildren || !attachments;
|
|
27
32
|
const classes = [
|
|
28
33
|
baseClass,
|
|
29
34
|
`${baseClass}--${role}`,
|
|
@@ -39,10 +44,7 @@ const ChatMessage = React.forwardRef(
|
|
|
39
44
|
avatar !== void 0 && /* @__PURE__ */ jsx(
|
|
40
45
|
"span",
|
|
41
46
|
{
|
|
42
|
-
className: [
|
|
43
|
-
`${baseClass}__gutter`,
|
|
44
|
-
hideAvatar ? `${baseClass}__gutter--hidden` : ""
|
|
45
|
-
].filter(Boolean).join(" "),
|
|
47
|
+
className: [`${baseClass}__gutter`, hideAvatar ? `${baseClass}__gutter--hidden` : ""].filter(Boolean).join(" "),
|
|
46
48
|
children: avatar
|
|
47
49
|
}
|
|
48
50
|
),
|
|
@@ -51,7 +53,8 @@ const ChatMessage = React.forwardRef(
|
|
|
51
53
|
author && /* @__PURE__ */ jsx("span", { className: `${baseClass}__author`, children: author }),
|
|
52
54
|
timestamp && /* @__PURE__ */ jsx("span", { className: `${baseClass}__timestamp`, children: timestamp })
|
|
53
55
|
] }),
|
|
54
|
-
/* @__PURE__ */ jsx("div", { className: `${baseClass}
|
|
56
|
+
attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
|
|
57
|
+
showContent && /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children: pending ? /* @__PURE__ */ jsxs("span", { className: `${baseClass}__pending`, role: "status", children: [
|
|
55
58
|
/* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, children: pendingLabel }),
|
|
56
59
|
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__dots`, "aria-hidden": "true", children: [
|
|
57
60
|
/* @__PURE__ */ jsx("span", { className: `${baseClass}__dot` }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* ============================================
|
|
2
2
|
COMPOSER COMPONENT
|
|
3
|
-
The chat input shell:
|
|
3
|
+
The chat input shell: file tray, auto-growing textarea, send/stop
|
|
4
4
|
============================================ */
|
|
5
5
|
|
|
6
6
|
/* The focus treatment animates a registered custom property, exactly as
|
|
@@ -319,7 +319,100 @@
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
/* ============================================
|
|
322
|
-
|
|
322
|
+
FILE TRAY
|
|
323
|
+
The queued files: one row of square tiles that scrolls sideways rather
|
|
324
|
+
than growing the shell.
|
|
325
|
+
============================================ */
|
|
326
|
+
|
|
327
|
+
.ds-composer__tray {
|
|
328
|
+
/* How far the tiles sit in from the shell's edge. The tile corner below
|
|
329
|
+
is derived from it, so the two can only move together. */
|
|
330
|
+
--ds-composer-tray-inset: var(--padding-300);
|
|
331
|
+
/* Concentric with the shell: its radius, less the border, less the inset.
|
|
332
|
+
Floored so a squared theme never computes a negative corner. */
|
|
333
|
+
--ds-attachment-tile-radius: max(
|
|
334
|
+
var(--radius-100),
|
|
335
|
+
calc(var(--ds-composer-radius) - var(--border-025) - var(--ds-composer-tray-inset))
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
display: flex;
|
|
339
|
+
/* A scroller may be squeezed by the column it sits in; the tray keeps
|
|
340
|
+
its tiles' full height */
|
|
341
|
+
flex: none;
|
|
342
|
+
gap: var(--gap-200);
|
|
343
|
+
margin: 0;
|
|
344
|
+
/* The inset on every side but the foot, where the text zone's own top
|
|
345
|
+
padding takes over and only a sliver is kept so the scrollbar of an
|
|
346
|
+
overflowing row does not touch the tiles. The top inset also houses the part of a remove
|
|
347
|
+
button that hangs above its tile: a scroller clips what leaves it. */
|
|
348
|
+
padding: var(--ds-composer-tray-inset) var(--ds-composer-tray-inset) var(--padding-100);
|
|
349
|
+
list-style: none;
|
|
350
|
+
overflow-x: auto;
|
|
351
|
+
/* Tiles are objects, not text: the shell's text cursor stops at them */
|
|
352
|
+
cursor: default;
|
|
353
|
+
scrollbar-width: thin;
|
|
354
|
+
scrollbar-color: var(--color-bg-container-secondary) transparent;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.ds-composer__tray-item {
|
|
358
|
+
display: flex;
|
|
359
|
+
flex: none;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* ============================================
|
|
363
|
+
DROP HINT
|
|
364
|
+
While files hover over the shell it says what letting go will do.
|
|
365
|
+
============================================ */
|
|
366
|
+
|
|
367
|
+
.ds-composer--dragging {
|
|
368
|
+
border-color: var(--color-input-border-selected);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.ds-composer__drop-hint {
|
|
372
|
+
position: absolute;
|
|
373
|
+
inset: 0;
|
|
374
|
+
z-index: 1;
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
justify-content: center;
|
|
378
|
+
border-radius: inherit;
|
|
379
|
+
background-color: var(--color-bg-container-primary-semi);
|
|
380
|
+
font-family: var(--font-paragraph-sm-emphasis-family);
|
|
381
|
+
font-size: var(--font-paragraph-sm-emphasis-size);
|
|
382
|
+
font-weight: var(--font-paragraph-sm-emphasis-weight);
|
|
383
|
+
line-height: var(--font-paragraph-sm-emphasis-line-height);
|
|
384
|
+
letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
|
|
385
|
+
color: var(--color-text-primary);
|
|
386
|
+
/* Never a target: the shell hears the drag, and a hint that caught the
|
|
387
|
+
pointer would fire a leave on everything under it */
|
|
388
|
+
pointer-events: none;
|
|
389
|
+
opacity: 0;
|
|
390
|
+
transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.ds-composer--dragging .ds-composer__drop-hint {
|
|
394
|
+
opacity: 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* The picker itself is never seen: the attach button opens it */
|
|
398
|
+
.ds-composer__file-input {
|
|
399
|
+
display: none;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.ds-composer__sr-only {
|
|
403
|
+
position: absolute;
|
|
404
|
+
width: 1px;
|
|
405
|
+
height: 1px;
|
|
406
|
+
padding: 0;
|
|
407
|
+
margin: -1px;
|
|
408
|
+
overflow: hidden;
|
|
409
|
+
clip: rect(0, 0, 0, 0);
|
|
410
|
+
white-space: nowrap;
|
|
411
|
+
border: 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* ============================================
|
|
415
|
+
ATTACHMENTS ROW (legacy free-form slot)
|
|
323
416
|
============================================ */
|
|
324
417
|
|
|
325
418
|
.ds-composer__attachments {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
+
import { AttachmentItem } from '../Attachment/fileTypes';
|
|
2
3
|
/** Props owned by Composer itself — everything else falls through to the <textarea>. */
|
|
3
4
|
type ComposerOwnProps = {
|
|
4
5
|
/** Current value for controlled use. Pair with `onValueChange`. */
|
|
@@ -13,10 +14,12 @@ type ComposerOwnProps = {
|
|
|
13
14
|
onValueChange?: (value: string) => void;
|
|
14
15
|
/**
|
|
15
16
|
* Fires with the current value on Enter (without Shift) and on the send
|
|
16
|
-
* button — never while `streaming`,
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* button — never while `streaming`, never while a file in `files` is
|
|
18
|
+
* still uploading, and never when there is nothing to send. Text is
|
|
19
|
+
* enough, and so is a ready file on its own, in which case the value is
|
|
20
|
+
* an empty string. Composer does not clear the value: the consumer owns
|
|
21
|
+
* it and clears it after a successful submit. Shadows the native
|
|
22
|
+
* `onSubmit` attribute, which never fires on a textarea anyway.
|
|
20
23
|
*/
|
|
21
24
|
onSubmit?: (value: string) => void;
|
|
22
25
|
/**
|
|
@@ -63,11 +66,57 @@ type ComposerOwnProps = {
|
|
|
63
66
|
*/
|
|
64
67
|
contextIcon?: string | React.ReactNode;
|
|
65
68
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
69
|
+
* The files queued on this message, drawn as a scrolling row of square
|
|
70
|
+
* tiles above the textarea. Controlled by the caller: Composer draws the
|
|
71
|
+
* list and reports what was added or removed, and never stores a file.
|
|
72
|
+
* `useAttachments` is the usual owner.
|
|
73
|
+
*/
|
|
74
|
+
files?: AttachmentItem[];
|
|
75
|
+
/**
|
|
76
|
+
* Fires with the files a person picked, pasted or dropped. Providing it
|
|
77
|
+
* arms all three: the attach button and its picker, file paste in the
|
|
78
|
+
* textarea, and drop on the shell.
|
|
79
|
+
*/
|
|
80
|
+
onFilesSelected?: (files: File[]) => void;
|
|
81
|
+
/** Fires with the id of the file whose remove button was pressed. Its presence renders the buttons. */
|
|
82
|
+
onFileRemove?: (id: string) => void;
|
|
83
|
+
/** Fires with a queued file when its tile is pressed. Its presence makes the tiles buttons. */
|
|
84
|
+
onFileClick?: (item: AttachmentItem) => void;
|
|
85
|
+
/** File types the picker offers, in the native `accept` syntax. */
|
|
86
|
+
accept?: string;
|
|
87
|
+
/** Whether the picker allows several files at once. */
|
|
88
|
+
multiple?: boolean;
|
|
89
|
+
/** Whether the built-in attach button shows when `onFilesSelected` is set. */
|
|
90
|
+
attachButton?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Pasted text at least this many characters long becomes an attachment
|
|
93
|
+
* instead of landing in the textarea. Off unless set, and inert without
|
|
94
|
+
* `onPasteAsAttachment`.
|
|
95
|
+
*/
|
|
96
|
+
pasteThreshold?: number;
|
|
97
|
+
/** Receives pasted text that met `pasteThreshold`. */
|
|
98
|
+
onPasteAsAttachment?: (text: string) => void;
|
|
99
|
+
/** Accessible label for the attach button. */
|
|
100
|
+
attachLabel?: string;
|
|
101
|
+
/** Text shown on the shell while files are dragged over it. */
|
|
102
|
+
dropLabel?: string;
|
|
103
|
+
/** Accessible label for the list of queued files. */
|
|
104
|
+
filesLabel?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Builds the sentence announced to assistive technology when files join
|
|
107
|
+
* the queue, leave it, or fail.
|
|
108
|
+
*/
|
|
109
|
+
formatFileAnnouncement?: (change: {
|
|
110
|
+
type: 'added' | 'removed' | 'failed';
|
|
111
|
+
names: string[];
|
|
112
|
+
}) => string;
|
|
113
|
+
/**
|
|
114
|
+
* Free-form row rendered above the textarea, after any `files`.
|
|
115
|
+
*
|
|
116
|
+
* @deprecated Use `files` with `onFilesSelected` and `onFileRemove`, which draw the queue as tiles.
|
|
68
117
|
*/
|
|
69
118
|
attachments?: React.ReactNode;
|
|
70
|
-
/** Leading actions on the left of the action bar
|
|
119
|
+
/** Leading actions on the left of the action bar, after the attach button (a model picker). */
|
|
71
120
|
actions?: React.ReactNode;
|
|
72
121
|
/**
|
|
73
122
|
* Trailing actions on the right of the action bar, just before the send
|
|
@@ -85,7 +134,7 @@ export interface ComposerProps extends ComposerOwnProps, Omit<React.ComponentPro
|
|
|
85
134
|
}
|
|
86
135
|
/**
|
|
87
136
|
* Composer is the chat input shell: an optional context note ("Looking at
|
|
88
|
-
* “Page name”"),
|
|
137
|
+
* “Page name”"), a tray of queued files, an auto-growing textarea, a leading
|
|
89
138
|
* actions slot, and a trailing send button — the one sanctioned
|
|
90
139
|
* primary-action teal in the chat set, because sending a message is a
|
|
91
140
|
* genuine primary CTA. While `streaming`, send becomes stop and Enter
|