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
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
-
import React, { useRef, useState, useLayoutEffect } from "react";
|
|
2
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import React, { useRef, useState, useLayoutEffect, useEffect } from "react";
|
|
4
|
+
import { AttachmentTile } from "../Attachment/AttachmentTile.js";
|
|
5
|
+
import { formatBytes } from "../Attachment/fileTypes.js";
|
|
4
6
|
import { CircularButton } from "../CircularButton/CircularButton.js";
|
|
5
7
|
import "../../fonts/material-symbols.css";
|
|
6
8
|
import "./Composer.css";
|
|
9
|
+
const defaultFileAnnouncement = ({
|
|
10
|
+
type,
|
|
11
|
+
names
|
|
12
|
+
}) => {
|
|
13
|
+
const list = names.join(", ");
|
|
14
|
+
if (type === "added") return `Attached ${list}`;
|
|
15
|
+
if (type === "removed") return `Removed ${list}`;
|
|
16
|
+
return `Could not attach ${list}`;
|
|
17
|
+
};
|
|
18
|
+
const dragCarriesFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files");
|
|
7
19
|
const Composer = React.forwardRef(
|
|
8
20
|
({
|
|
9
21
|
value,
|
|
@@ -17,6 +29,19 @@ const Composer = React.forwardRef(
|
|
|
17
29
|
context,
|
|
18
30
|
contextIcon,
|
|
19
31
|
contextPlacement = "inside",
|
|
32
|
+
files,
|
|
33
|
+
onFilesSelected,
|
|
34
|
+
onFileRemove,
|
|
35
|
+
onFileClick,
|
|
36
|
+
accept,
|
|
37
|
+
multiple = true,
|
|
38
|
+
attachButton = true,
|
|
39
|
+
pasteThreshold,
|
|
40
|
+
onPasteAsAttachment,
|
|
41
|
+
attachLabel = "Attach files",
|
|
42
|
+
dropLabel = "Drop files to attach",
|
|
43
|
+
filesLabel = "Attachments",
|
|
44
|
+
formatFileAnnouncement = defaultFileAnnouncement,
|
|
20
45
|
attachments,
|
|
21
46
|
actions,
|
|
22
47
|
trailingActions,
|
|
@@ -25,6 +50,7 @@ const Composer = React.forwardRef(
|
|
|
25
50
|
className = "",
|
|
26
51
|
onChange,
|
|
27
52
|
onKeyDown,
|
|
53
|
+
onPaste,
|
|
28
54
|
...rest
|
|
29
55
|
}, ref) => {
|
|
30
56
|
const baseClass = "ds-composer";
|
|
@@ -34,7 +60,17 @@ const Composer = React.forwardRef(
|
|
|
34
60
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
|
|
35
61
|
const currentValue = isControlled ? value : uncontrolledValue;
|
|
36
62
|
const disabled = Boolean(rest.disabled);
|
|
37
|
-
const
|
|
63
|
+
const queued = files ?? [];
|
|
64
|
+
const hasFiles = queued.length > 0;
|
|
65
|
+
const armed = Boolean(onFilesSelected);
|
|
66
|
+
const anyUploading = queued.some((file) => file.status === "uploading");
|
|
67
|
+
const anyReady = queued.some((file) => file.status === "ready");
|
|
68
|
+
const canSend = !disabled && !anyUploading && (currentValue.trim() !== "" || anyReady);
|
|
69
|
+
const fileInputRef = useRef(null);
|
|
70
|
+
const trayRef = useRef(null);
|
|
71
|
+
const [dragging, setDragging] = useState(false);
|
|
72
|
+
const dragDepth = useRef(0);
|
|
73
|
+
const [announcement, setAnnouncement] = useState("");
|
|
38
74
|
const setTextareaRef = (node) => {
|
|
39
75
|
textareaRef.current = node;
|
|
40
76
|
if (typeof ref === "function") ref(node);
|
|
@@ -57,6 +93,92 @@ const Composer = React.forwardRef(
|
|
|
57
93
|
observer.observe(node);
|
|
58
94
|
return () => observer.disconnect();
|
|
59
95
|
}, []);
|
|
96
|
+
const previousFiles = useRef(queued);
|
|
97
|
+
const announceRef = useRef(formatFileAnnouncement);
|
|
98
|
+
announceRef.current = formatFileAnnouncement;
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const before = previousFiles.current;
|
|
101
|
+
const now = files ?? [];
|
|
102
|
+
previousFiles.current = now;
|
|
103
|
+
if (before === now) return;
|
|
104
|
+
const beforeById = new Map(before.map((file) => [file.id, file]));
|
|
105
|
+
const nowIds = new Set(now.map((file) => file.id));
|
|
106
|
+
const added = now.filter((file) => !beforeById.has(file.id)).map((file) => file.name);
|
|
107
|
+
const removed = before.filter((file) => !nowIds.has(file.id)).map((file) => file.name);
|
|
108
|
+
const failed = now.filter((file) => file.status === "error" && beforeById.get(file.id)?.status !== "error").map((file) => file.name);
|
|
109
|
+
const parts = [
|
|
110
|
+
failed.length > 0 ? announceRef.current({ type: "failed", names: failed }) : "",
|
|
111
|
+
added.length > 0 ? announceRef.current({ type: "added", names: added }) : "",
|
|
112
|
+
removed.length > 0 ? announceRef.current({ type: "removed", names: removed }) : ""
|
|
113
|
+
].filter(Boolean);
|
|
114
|
+
if (parts.length > 0) setAnnouncement(parts.join(". "));
|
|
115
|
+
}, [files]);
|
|
116
|
+
const focusAfterRemove = useRef(null);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const index = focusAfterRemove.current;
|
|
119
|
+
if (index === null) return;
|
|
120
|
+
focusAfterRemove.current = null;
|
|
121
|
+
const buttons = trayRef.current?.querySelectorAll(
|
|
122
|
+
".ds-attachment-tile__remove"
|
|
123
|
+
);
|
|
124
|
+
const next = buttons?.[Math.min(index, (buttons?.length ?? 0) - 1)];
|
|
125
|
+
if (next) next.focus();
|
|
126
|
+
else textareaRef.current?.focus();
|
|
127
|
+
}, [files]);
|
|
128
|
+
const handleFileRemove = (id, index) => {
|
|
129
|
+
focusAfterRemove.current = index;
|
|
130
|
+
onFileRemove?.(id);
|
|
131
|
+
};
|
|
132
|
+
const handlePick = (event) => {
|
|
133
|
+
const picked = Array.from(event.target.files ?? []);
|
|
134
|
+
event.target.value = "";
|
|
135
|
+
if (picked.length > 0) onFilesSelected?.(picked);
|
|
136
|
+
};
|
|
137
|
+
const handlePaste = (event) => {
|
|
138
|
+
onPaste?.(event);
|
|
139
|
+
if (event.defaultPrevented) return;
|
|
140
|
+
const pasted = Array.from(event.clipboardData?.files ?? []);
|
|
141
|
+
if (armed && pasted.length > 0) {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
onFilesSelected?.(pasted);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (pasteThreshold === void 0 || !onPasteAsAttachment) return;
|
|
147
|
+
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
148
|
+
if (text.length >= pasteThreshold) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
onPasteAsAttachment(text);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const takesDrag = (event) => armed && !disabled && dragCarriesFiles(event);
|
|
154
|
+
const handleDragEnter = (event) => {
|
|
155
|
+
if (!takesDrag(event)) return;
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
event.stopPropagation();
|
|
158
|
+
dragDepth.current += 1;
|
|
159
|
+
setDragging(true);
|
|
160
|
+
};
|
|
161
|
+
const handleDragOver = (event) => {
|
|
162
|
+
if (!takesDrag(event)) return;
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
event.stopPropagation();
|
|
165
|
+
event.dataTransfer.dropEffect = "copy";
|
|
166
|
+
};
|
|
167
|
+
const handleDragLeave = (event) => {
|
|
168
|
+
if (!takesDrag(event)) return;
|
|
169
|
+
event.stopPropagation();
|
|
170
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
171
|
+
if (dragDepth.current === 0) setDragging(false);
|
|
172
|
+
};
|
|
173
|
+
const handleDrop = (event) => {
|
|
174
|
+
if (!takesDrag(event)) return;
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
event.stopPropagation();
|
|
177
|
+
dragDepth.current = 0;
|
|
178
|
+
setDragging(false);
|
|
179
|
+
const dropped = Array.from(event.dataTransfer.files ?? []);
|
|
180
|
+
if (dropped.length > 0) onFilesSelected?.(dropped);
|
|
181
|
+
};
|
|
60
182
|
const submit = () => {
|
|
61
183
|
if (streaming || !canSend) return;
|
|
62
184
|
onSubmit?.(currentValue);
|
|
@@ -83,6 +205,7 @@ const Composer = React.forwardRef(
|
|
|
83
205
|
aiGlow ? `${baseClass}--ai-glow` : "",
|
|
84
206
|
streaming ? `${baseClass}--streaming` : "",
|
|
85
207
|
disabled ? `${baseClass}--disabled` : "",
|
|
208
|
+
dragging ? `${baseClass}--dragging` : "",
|
|
86
209
|
// The above placement moves the text onto the chip's rail; the shell
|
|
87
210
|
// needs to know, since the chip is no longer inside it.
|
|
88
211
|
context && contextPlacement === "above" ? `${baseClass}--context-above` : "",
|
|
@@ -107,8 +230,28 @@ const Composer = React.forwardRef(
|
|
|
107
230
|
className: classes,
|
|
108
231
|
style: { "--ds-composer-max-rows": maxRows },
|
|
109
232
|
onClick: handleShellClick,
|
|
233
|
+
onDragEnter: armed ? handleDragEnter : void 0,
|
|
234
|
+
onDragOver: armed ? handleDragOver : void 0,
|
|
235
|
+
onDragLeave: armed ? handleDragLeave : void 0,
|
|
236
|
+
onDrop: armed ? handleDrop : void 0,
|
|
110
237
|
children: [
|
|
111
238
|
!contextAbove && contextChip,
|
|
239
|
+
hasFiles && /* @__PURE__ */ jsx("ul", { className: `${baseClass}__tray`, ref: trayRef, "aria-label": filesLabel, children: queued.map((file, index) => /* @__PURE__ */ jsx("li", { className: `${baseClass}__tray-item`, children: /* @__PURE__ */ jsx(
|
|
240
|
+
AttachmentTile,
|
|
241
|
+
{
|
|
242
|
+
name: file.name,
|
|
243
|
+
kind: file.kind,
|
|
244
|
+
status: file.status,
|
|
245
|
+
progress: file.progress,
|
|
246
|
+
error: file.error,
|
|
247
|
+
previewSrc: file.previewSrc,
|
|
248
|
+
previewAlt: file.previewAlt,
|
|
249
|
+
excerpt: file.excerpt,
|
|
250
|
+
meta: file.size === void 0 ? void 0 : formatBytes(file.size),
|
|
251
|
+
onClick: onFileClick ? () => onFileClick(file) : void 0,
|
|
252
|
+
onRemove: onFileRemove && !disabled ? () => handleFileRemove(file.id, index) : void 0
|
|
253
|
+
}
|
|
254
|
+
) }, file.id)) }),
|
|
112
255
|
attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
|
|
113
256
|
/* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, ref: contentRef, children: /* @__PURE__ */ jsx(
|
|
114
257
|
"textarea",
|
|
@@ -120,11 +263,24 @@ const Composer = React.forwardRef(
|
|
|
120
263
|
value: currentValue,
|
|
121
264
|
"aria-label": ariaLabel,
|
|
122
265
|
onChange: handleChange,
|
|
123
|
-
onKeyDown: handleKeyDown
|
|
266
|
+
onKeyDown: handleKeyDown,
|
|
267
|
+
onPaste: handlePaste
|
|
124
268
|
}
|
|
125
269
|
) }),
|
|
126
270
|
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__footer`, children: [
|
|
127
|
-
actions && /* @__PURE__ */
|
|
271
|
+
(actions || armed && attachButton) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__actions`, children: [
|
|
272
|
+
armed && attachButton && /* @__PURE__ */ jsx(
|
|
273
|
+
CircularButton,
|
|
274
|
+
{
|
|
275
|
+
icon: "add",
|
|
276
|
+
variant: "tertiary",
|
|
277
|
+
ariaLabel: attachLabel,
|
|
278
|
+
disabled,
|
|
279
|
+
onClick: () => fileInputRef.current?.click()
|
|
280
|
+
}
|
|
281
|
+
),
|
|
282
|
+
actions
|
|
283
|
+
] }),
|
|
128
284
|
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__trailing`, children: [
|
|
129
285
|
trailingActions,
|
|
130
286
|
streaming ? /* @__PURE__ */ jsx(
|
|
@@ -147,7 +303,24 @@ const Composer = React.forwardRef(
|
|
|
147
303
|
}
|
|
148
304
|
)
|
|
149
305
|
] })
|
|
150
|
-
] })
|
|
306
|
+
] }),
|
|
307
|
+
armed && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
308
|
+
/* @__PURE__ */ jsx(
|
|
309
|
+
"input",
|
|
310
|
+
{
|
|
311
|
+
ref: fileInputRef,
|
|
312
|
+
className: `${baseClass}__file-input`,
|
|
313
|
+
type: "file",
|
|
314
|
+
accept,
|
|
315
|
+
multiple,
|
|
316
|
+
tabIndex: -1,
|
|
317
|
+
"aria-hidden": "true",
|
|
318
|
+
onChange: handlePick
|
|
319
|
+
}
|
|
320
|
+
),
|
|
321
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__drop-hint`, "aria-hidden": "true", children: dropLabel })
|
|
322
|
+
] }),
|
|
323
|
+
(armed || hasFiles) && /* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, role: "status", children: announcement })
|
|
151
324
|
]
|
|
152
325
|
}
|
|
153
326
|
);
|
|
@@ -114,6 +114,31 @@
|
|
|
114
114
|
background-color: var(--color-action-passive-bg-hover);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/* States — selected: the current choice of a set. DropdownMenu.css owns
|
|
118
|
+
the recipe (passive wash held, full-strength text and icon, trailing
|
|
119
|
+
check); the two menus share the entry model, so they share the look. */
|
|
120
|
+
.ds-context-menu__item--selected {
|
|
121
|
+
background-color: var(--color-action-passive-bg-hover);
|
|
122
|
+
color: var(--color-text-primary);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.ds-context-menu__item--selected:hover,
|
|
126
|
+
.ds-context-menu__item--selected.ds-context-menu__item--focused {
|
|
127
|
+
background-color: var(--color-action-passive-bg-active);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ds-context-menu__item--selected .ds-context-menu__item-icon {
|
|
131
|
+
color: var(--color-icon-primary);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ds-context-menu__check {
|
|
135
|
+
--icon-size: var(--icon-size-500);
|
|
136
|
+
color: var(--color-icon-primary);
|
|
137
|
+
margin-left: auto;
|
|
138
|
+
flex-shrink: 0;
|
|
139
|
+
line-height: 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
/* States — disabled */
|
|
118
143
|
.ds-context-menu__item--disabled {
|
|
119
144
|
opacity: 0.4;
|
|
@@ -121,14 +121,17 @@ const SubEntry = ({
|
|
|
121
121
|
const itemClasses = [
|
|
122
122
|
`${baseClass}__item`,
|
|
123
123
|
entry.disabled ? `${baseClass}__item--disabled` : "",
|
|
124
|
-
entry.destructive ? `${baseClass}__item--destructive` : ""
|
|
124
|
+
entry.destructive ? `${baseClass}__item--destructive` : "",
|
|
125
|
+
entry.selected ? `${baseClass}__item--selected` : ""
|
|
125
126
|
].filter(Boolean).join(" ");
|
|
127
|
+
const isChoice = entry.selected !== void 0;
|
|
126
128
|
return /* @__PURE__ */ jsxs(
|
|
127
129
|
"li",
|
|
128
130
|
{
|
|
129
131
|
id: itemId,
|
|
130
132
|
className: itemClasses,
|
|
131
|
-
role: "menuitem",
|
|
133
|
+
role: isChoice ? "menuitemradio" : "menuitem",
|
|
134
|
+
"aria-checked": isChoice ? entry.selected : void 0,
|
|
132
135
|
"aria-disabled": entry.disabled || void 0,
|
|
133
136
|
onClick: (e) => {
|
|
134
137
|
e.stopPropagation();
|
|
@@ -142,7 +145,8 @@ const SubEntry = ({
|
|
|
142
145
|
entry.icon && /* @__PURE__ */ jsx("span", { className: `${baseClass}__item-icon material-symbols-rounded`, "aria-hidden": "true", children: entry.icon }),
|
|
143
146
|
entry.label
|
|
144
147
|
] }),
|
|
145
|
-
entry.shortcut && /* @__PURE__ */ jsx("span", { className: `${baseClass}__shortcut`, children: entry.shortcut })
|
|
148
|
+
entry.shortcut && /* @__PURE__ */ jsx("span", { className: `${baseClass}__shortcut`, children: entry.shortcut }),
|
|
149
|
+
entry.selected && /* @__PURE__ */ jsx("span", { className: `${baseClass}__check material-symbols-rounded`, "aria-hidden": "true", children: "check" })
|
|
146
150
|
]
|
|
147
151
|
}
|
|
148
152
|
);
|
|
@@ -321,14 +325,17 @@ const ContextMenu = React.forwardRef(
|
|
|
321
325
|
`${baseClass}__item`,
|
|
322
326
|
isFocused ? `${baseClass}__item--focused` : "",
|
|
323
327
|
entry.disabled ? `${baseClass}__item--disabled` : "",
|
|
324
|
-
entry.destructive ? `${baseClass}__item--destructive` : ""
|
|
328
|
+
entry.destructive ? `${baseClass}__item--destructive` : "",
|
|
329
|
+
entry.selected ? `${baseClass}__item--selected` : ""
|
|
325
330
|
].filter(Boolean).join(" ");
|
|
331
|
+
const isChoice = entry.selected !== void 0;
|
|
326
332
|
return /* @__PURE__ */ jsxs(
|
|
327
333
|
"li",
|
|
328
334
|
{
|
|
329
335
|
id: `${generatedId}-item-${myIndex}`,
|
|
330
336
|
className: itemClasses,
|
|
331
|
-
role: "menuitem",
|
|
337
|
+
role: isChoice ? "menuitemradio" : "menuitem",
|
|
338
|
+
"aria-checked": isChoice ? entry.selected : void 0,
|
|
332
339
|
"aria-disabled": entry.disabled || void 0,
|
|
333
340
|
onClick: (e) => {
|
|
334
341
|
e.stopPropagation();
|
|
@@ -349,7 +356,8 @@ const ContextMenu = React.forwardRef(
|
|
|
349
356
|
),
|
|
350
357
|
entry.label
|
|
351
358
|
] }),
|
|
352
|
-
entry.shortcut && /* @__PURE__ */ jsx("span", { className: `${baseClass}__shortcut`, children: entry.shortcut })
|
|
359
|
+
entry.shortcut && /* @__PURE__ */ jsx("span", { className: `${baseClass}__shortcut`, children: entry.shortcut }),
|
|
360
|
+
entry.selected && /* @__PURE__ */ jsx("span", { className: `${baseClass}__check material-symbols-rounded`, "aria-hidden": "true", children: "check" })
|
|
353
361
|
]
|
|
354
362
|
},
|
|
355
363
|
`item-${index}`
|
|
@@ -28,14 +28,16 @@ type DocumentChipOwnProps = {
|
|
|
28
28
|
export interface DocumentChipProps extends DocumentChipOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof DocumentChipOwnProps> {
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* DocumentChip is
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* DocumentChip is the earlier, one-row file reference: a type icon, name,
|
|
32
|
+
* metadata line, upload progress, and an optional remove button. It keeps
|
|
33
|
+
* working for existing consumers, and new work uses the attachment family:
|
|
34
|
+
* AttachmentTile for a file, Composer's `files` for a queue, and
|
|
35
|
+
* AttachmentGroup for the files on a sent message.
|
|
36
36
|
*
|
|
37
37
|
* The root is always a `<div>`; passing `onClick` turns the body into a
|
|
38
38
|
* `<button>` so click and remove coexist without nesting controls.
|
|
39
|
+
*
|
|
40
|
+
* @deprecated Use `AttachmentTile` instead.
|
|
39
41
|
*/
|
|
40
42
|
export declare const DocumentChip: React.ForwardRefExoticComponent<DocumentChipProps & React.RefAttributes<HTMLDivElement>>;
|
|
41
43
|
export {};
|
|
@@ -145,6 +145,35 @@
|
|
|
145
145
|
background-color: var(--color-action-passive-bg-hover);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/* ============================================
|
|
149
|
+
SELECTED ITEM
|
|
150
|
+
The current choice of a set: the passive wash held down, text and icon
|
|
151
|
+
at full strength, and a trailing check — three cues, none of them the
|
|
152
|
+
action colour, which a menu row has no claim on.
|
|
153
|
+
============================================ */
|
|
154
|
+
|
|
155
|
+
.ds-dropdown-menu__item--selected {
|
|
156
|
+
background-color: var(--color-action-passive-bg-hover);
|
|
157
|
+
color: var(--color-text-primary);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ds-dropdown-menu__item--selected:hover,
|
|
161
|
+
.ds-dropdown-menu__item--selected.ds-dropdown-menu__item--focused {
|
|
162
|
+
background-color: var(--color-action-passive-bg-active);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.ds-dropdown-menu__item--selected .ds-dropdown-menu__item-icon {
|
|
166
|
+
color: var(--color-icon-primary);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.ds-dropdown-menu__check {
|
|
170
|
+
--icon-size: var(--icon-size-500);
|
|
171
|
+
color: var(--color-icon-primary);
|
|
172
|
+
margin-left: auto;
|
|
173
|
+
flex-shrink: 0;
|
|
174
|
+
line-height: 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
148
177
|
/* ============================================
|
|
149
178
|
DISABLED ITEM
|
|
150
179
|
============================================ */
|
|
@@ -11,6 +11,8 @@ export interface DropdownMenuItem {
|
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
/** Destructive / danger styling (red text) for delete, remove, etc. */
|
|
13
13
|
destructive?: boolean;
|
|
14
|
+
/** Marks the item as the current choice of a set: it holds the passive wash and a trailing check, and is announced as a checked radio item. Leave undefined on plain action items; pass `false` on the unchosen members of a set so they are announced as radio items too. Ignored on an item that opens a sub-menu. */
|
|
15
|
+
selected?: boolean;
|
|
14
16
|
/** Click handler */
|
|
15
17
|
onClick?: () => void;
|
|
16
18
|
/** Nested sub-menu items */
|
|
@@ -120,14 +120,17 @@ const MenuPanel = ({
|
|
|
120
120
|
const itemClasses = [
|
|
121
121
|
`${baseClass}__item`,
|
|
122
122
|
entry.disabled ? `${baseClass}__item--disabled` : "",
|
|
123
|
-
entry.destructive ? `${baseClass}__item--destructive` : ""
|
|
123
|
+
entry.destructive ? `${baseClass}__item--destructive` : "",
|
|
124
|
+
entry.selected ? `${baseClass}__item--selected` : ""
|
|
124
125
|
].filter(Boolean).join(" ");
|
|
126
|
+
const isChoice = entry.selected !== void 0;
|
|
125
127
|
return /* @__PURE__ */ jsxs(
|
|
126
128
|
"li",
|
|
127
129
|
{
|
|
128
130
|
id: entryId,
|
|
129
131
|
className: itemClasses,
|
|
130
|
-
role: "menuitem",
|
|
132
|
+
role: isChoice ? "menuitemradio" : "menuitem",
|
|
133
|
+
"aria-checked": isChoice ? entry.selected : void 0,
|
|
131
134
|
"aria-disabled": entry.disabled || void 0,
|
|
132
135
|
onClick: (e) => {
|
|
133
136
|
e.stopPropagation();
|
|
@@ -141,7 +144,8 @@ const MenuPanel = ({
|
|
|
141
144
|
entry.icon && (typeof entry.icon === "string" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__item-icon material-symbols-rounded`, "aria-hidden": "true", children: entry.icon }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__item-icon`, "aria-hidden": "true", children: entry.icon })),
|
|
142
145
|
entry.label
|
|
143
146
|
] }),
|
|
144
|
-
entry.shortcut && /* @__PURE__ */ jsx("span", { className: `${baseClass}__shortcut`, children: entry.shortcut })
|
|
147
|
+
entry.shortcut && /* @__PURE__ */ jsx("span", { className: `${baseClass}__shortcut`, children: entry.shortcut }),
|
|
148
|
+
entry.selected && /* @__PURE__ */ jsx("span", { className: `${baseClass}__check material-symbols-rounded`, "aria-hidden": "true", children: "check" })
|
|
145
149
|
]
|
|
146
150
|
},
|
|
147
151
|
`item-${index}`
|
|
@@ -253,7 +257,7 @@ const DropdownMenu = ({
|
|
|
253
257
|
};
|
|
254
258
|
useEffect(() => {
|
|
255
259
|
if (isOpen && focusedIndex >= 0 && panelRef.current) {
|
|
256
|
-
const allItems = panelRef.current.querySelectorAll(`[role="menuitem"]`);
|
|
260
|
+
const allItems = panelRef.current.querySelectorAll(`[role="menuitem"], [role="menuitemradio"]`);
|
|
257
261
|
const el = allItems[focusedIndex];
|
|
258
262
|
el?.scrollIntoView({ block: "nearest" });
|
|
259
263
|
}
|
|
@@ -291,14 +295,17 @@ const DropdownMenu = ({
|
|
|
291
295
|
`${baseClass}__item`,
|
|
292
296
|
isFocused ? `${baseClass}__item--focused` : "",
|
|
293
297
|
entry.disabled ? `${baseClass}__item--disabled` : "",
|
|
294
|
-
entry.destructive ? `${baseClass}__item--destructive` : ""
|
|
298
|
+
entry.destructive ? `${baseClass}__item--destructive` : "",
|
|
299
|
+
entry.selected ? `${baseClass}__item--selected` : ""
|
|
295
300
|
].filter(Boolean).join(" ");
|
|
301
|
+
const isChoice = entry.selected !== void 0;
|
|
296
302
|
return /* @__PURE__ */ jsxs(
|
|
297
303
|
"li",
|
|
298
304
|
{
|
|
299
305
|
id: `${generatedId}-item-${myIndex}`,
|
|
300
306
|
className: itemClasses,
|
|
301
|
-
role: "menuitem",
|
|
307
|
+
role: isChoice ? "menuitemradio" : "menuitem",
|
|
308
|
+
"aria-checked": isChoice ? entry.selected : void 0,
|
|
302
309
|
"aria-disabled": entry.disabled || void 0,
|
|
303
310
|
onClick: (e) => {
|
|
304
311
|
e.stopPropagation();
|
|
@@ -312,7 +319,8 @@ const DropdownMenu = ({
|
|
|
312
319
|
entry.icon && (typeof entry.icon === "string" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__item-icon material-symbols-rounded`, "aria-hidden": "true", children: entry.icon }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__item-icon`, "aria-hidden": "true", children: entry.icon })),
|
|
313
320
|
entry.label
|
|
314
321
|
] }),
|
|
315
|
-
entry.shortcut && /* @__PURE__ */ jsx("span", { className: `${baseClass}__shortcut`, children: entry.shortcut })
|
|
322
|
+
entry.shortcut && /* @__PURE__ */ jsx("span", { className: `${baseClass}__shortcut`, children: entry.shortcut }),
|
|
323
|
+
entry.selected && /* @__PURE__ */ jsx("span", { className: `${baseClass}__check material-symbols-rounded`, "aria-hidden": "true", children: "check" })
|
|
316
324
|
]
|
|
317
325
|
},
|
|
318
326
|
`item-${index}`
|
|
@@ -3,14 +3,8 @@ import { jsxs, jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import React, { useState, useRef, useId } from "react";
|
|
4
4
|
import { Field } from "../Field/Field.js";
|
|
5
5
|
import "./FileInput.css";
|
|
6
|
+
import { formatBytes } from "../Attachment/fileTypes.js";
|
|
6
7
|
import "../../fonts/material-symbols.css";
|
|
7
|
-
const formatBytes = (bytes) => {
|
|
8
|
-
if (bytes === 0) return "0 B";
|
|
9
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
10
|
-
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
11
|
-
const value = bytes / Math.pow(1024, exponent);
|
|
12
|
-
return `${exponent === 0 ? value : value.toFixed(1)} ${units[exponent]}`;
|
|
13
|
-
};
|
|
14
8
|
const FileInput = React.forwardRef(
|
|
15
9
|
({
|
|
16
10
|
label,
|
|
@@ -18,7 +18,7 @@ type MessageCardOwnProps = {
|
|
|
18
18
|
actions?: React.ReactNode;
|
|
19
19
|
/** Additional CSS classes */
|
|
20
20
|
className?: string;
|
|
21
|
-
/** Body content between the description and the footer — rich content, a Prose block,
|
|
21
|
+
/** Body content between the description and the footer — rich content, a Prose block, an AttachmentGroup. */
|
|
22
22
|
children?: React.ReactNode;
|
|
23
23
|
};
|
|
24
24
|
export interface MessageCardProps extends MessageCardOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof MessageCardOwnProps> {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
|
|
37
37
|
/* ============================================
|
|
38
38
|
VARIANT — PULL (large display quote)
|
|
39
|
-
Display type
|
|
40
|
-
|
|
39
|
+
Display-tier type: no border, the scale is
|
|
40
|
+
the emphasis.
|
|
41
41
|
============================================ */
|
|
42
42
|
|
|
43
43
|
.ds-quote--pull .ds-quote__text {
|
|
@@ -22,7 +22,7 @@ export interface ShaderParams {
|
|
|
22
22
|
/**
|
|
23
23
|
* How much the composition resists shrinking with the container, 0–1.
|
|
24
24
|
*
|
|
25
|
-
* At 0 — the
|
|
25
|
+
* At 0 — the behaviour a CSS blob layer has — the whole
|
|
26
26
|
* field is fitted to whatever width it is given, so a phone shows the
|
|
27
27
|
* entire composition at phone scale: every source small, and more of them
|
|
28
28
|
* crowded into view than the look was built for. At 1 the field holds the
|
|
@@ -31,9 +31,9 @@ export interface ShaderParams {
|
|
|
31
31
|
* keeping some of the shrink so colours still bleed together on a small
|
|
32
32
|
* screen. Above the reference width it does nothing.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
* scene rather than a diagram;
|
|
36
|
-
* the whole composition, such as a small demo tile.
|
|
34
|
+
* The default sits between the two, which suits a full-viewport background,
|
|
35
|
+
* read on a phone as a scene rather than a diagram; set it to 0 wherever
|
|
36
|
+
* the point is to see the whole composition, such as a small demo tile.
|
|
37
37
|
*/
|
|
38
38
|
crop: number;
|
|
39
39
|
}
|
package/components/Stat/Stat.css
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/* ============================================
|
|
16
|
-
VALUE — display weight (300
|
|
17
|
-
|
|
16
|
+
VALUE — the display tier's weight (300 in the
|
|
17
|
+
base theme), so numerals follow the theme
|
|
18
18
|
============================================ */
|
|
19
19
|
|
|
20
20
|
.ds-stat__value {
|
package/components/registry.json
CHANGED
|
@@ -161,6 +161,42 @@
|
|
|
161
161
|
"folder": "Chart",
|
|
162
162
|
"recharts": true
|
|
163
163
|
},
|
|
164
|
+
{
|
|
165
|
+
"name": "AttachmentDropZone",
|
|
166
|
+
"label": "Attachment drop zone",
|
|
167
|
+
"slug": "attachment-drop-zone",
|
|
168
|
+
"description": "A wrapper that makes a whole surface a file drop target, with an outlined overlay while files hover over it.",
|
|
169
|
+
"category": "ai",
|
|
170
|
+
"client": true,
|
|
171
|
+
"folder": "Attachment"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "AttachmentGroup",
|
|
175
|
+
"label": "Attachment group",
|
|
176
|
+
"slug": "attachment-group",
|
|
177
|
+
"description": "The files on a sent message: one picture at its own shape, or a grid of tiles that collapses behind a count.",
|
|
178
|
+
"category": "ai",
|
|
179
|
+
"client": true,
|
|
180
|
+
"folder": "Attachment"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "AttachmentTile",
|
|
184
|
+
"label": "Attachment tile",
|
|
185
|
+
"slug": "attachment-tile",
|
|
186
|
+
"description": "One file as a square: a picture, a coloured mark for well-known formats, or a neutral badge, with upload states.",
|
|
187
|
+
"category": "ai",
|
|
188
|
+
"client": false,
|
|
189
|
+
"folder": "Attachment"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"name": "AttachmentViewer",
|
|
193
|
+
"label": "Attachment viewer",
|
|
194
|
+
"slug": "attachment-viewer",
|
|
195
|
+
"description": "A preview dialog for a message's files, with download, stepping, and plain states for files it cannot show.",
|
|
196
|
+
"category": "ai",
|
|
197
|
+
"client": true,
|
|
198
|
+
"folder": "Attachment"
|
|
199
|
+
},
|
|
164
200
|
{
|
|
165
201
|
"name": "Avatar",
|
|
166
202
|
"label": "Avatar",
|
|
@@ -369,7 +405,7 @@
|
|
|
369
405
|
"name": "Composer",
|
|
370
406
|
"label": "Composer",
|
|
371
407
|
"slug": "composer",
|
|
372
|
-
"description": "An auto-growing message input with send and stop states, a page-context note,
|
|
408
|
+
"description": "An auto-growing message input with send and stop states, a page-context note, a file tray fed by picker, paste and drop, and Enter-to-send.",
|
|
373
409
|
"category": "ai",
|
|
374
410
|
"client": true
|
|
375
411
|
},
|
|
@@ -441,7 +477,7 @@
|
|
|
441
477
|
"name": "DocumentChip",
|
|
442
478
|
"label": "Document chip",
|
|
443
479
|
"slug": "document-chip",
|
|
444
|
-
"description": "A
|
|
480
|
+
"description": "A legacy one-row file reference, superseded by the attachment tile and kept for existing consumers.",
|
|
445
481
|
"category": "ai",
|
|
446
482
|
"client": false
|
|
447
483
|
},
|