rift-ds 1.2.0 → 1.4.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 +2 -2
- 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/Chart/BarChart.js +1 -1
- package/components/Chart/ComboChart.js +1 -1
- package/components/Chart/Treemap.js +83 -6
- package/components/ChatMessage/ChatMessage.css +24 -0
- package/components/ChatMessage/ChatMessage.d.ts +6 -0
- package/components/ChatMessage/ChatMessage.js +8 -5
- package/components/ColorPicker/ColorPicker.js +1 -1
- package/components/Composer/Composer.css +140 -5
- package/components/Composer/Composer.d.ts +71 -14
- package/components/Composer/Composer.js +204 -11
- package/components/DatePicker/DatePicker.css +1 -1
- package/components/DocumentChip/DocumentChip.d.ts +7 -5
- package/components/EventCalendar/EventCalendar.css +2 -2
- package/components/FileInput/FileInput.css +1 -1
- package/components/FileInput/FileInput.js +1 -7
- package/components/Globe/Globe.d.ts +5 -3
- package/components/Globe/Globe.js +18 -2
- package/components/Inspector/Inspector.css +108 -3
- package/components/Inspector/InspectorColorPicker.d.ts +43 -0
- package/components/Inspector/InspectorColorPicker.js +51 -0
- package/components/Instructions/Instructions.css +4 -4
- package/components/MessageCard/MessageCard.d.ts +1 -1
- package/components/Prose/Prose.css +27 -0
- package/components/Sparkline/Sparkline.css +2 -2
- package/components/Spinner/Spinner.css +1 -1
- package/components/Timeline/Timeline.css +1 -1
- package/components/WorldMap/land.d.ts +1 -1
- package/components/registry.json +48 -3
- package/components/registry.json.d.ts +48 -3
- package/components/registry.json.js +1 -1
- package/index.d.ts +5 -0
- package/index.js +21 -0
- package/package.json +9 -1
- package/tokens/motion.d.ts +2 -0
- package/tokens/motion.js +2 -0
- package/tokens/presets/contrast.css +2 -4
- package/tokens/presets/coral.css +2 -4
- package/tokens/presets/forest.css +2 -4
- package/tokens/presets/gold.css +4 -4
- package/tokens/presets/mono.css +0 -2
- package/tokens/presets/pink.css +4 -4
- package/tokens/presets/terminal.css +5 -4
- package/tokens/presets/violet.css +2 -4
- package/tokens/presets/warm.css +3 -4
- package/tokens/presets/zest.css +2 -4
- package/tokens/registry.json +5 -0
- package/tokens/registry.json.d.ts +5 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +14 -5
- package/tokens/tokens-light.css +16 -2
- package/tokens/tokens-motion.css +2 -0
- package/tokens/tokens-primitives.css +10 -0
|
@@ -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
|
/**
|
|
@@ -38,14 +41,22 @@ type ComposerOwnProps = {
|
|
|
38
41
|
*/
|
|
39
42
|
aiGlow?: boolean;
|
|
40
43
|
/**
|
|
41
|
-
* Contextual note rendered as a full-width, non-interactive chip
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* text.
|
|
44
|
+
* Contextual note rendered as a full-width, non-interactive chip — the
|
|
45
|
+
* "what the model is looking at" line a chat host pins over the message
|
|
46
|
+
* ("Looking at “Page name”"). `contextPlacement` decides where it sits.
|
|
47
|
+
* One line: a note too long for the shell truncates with an ellipsis.
|
|
48
|
+
* Composer owns the chip's chrome; the caller passes the text.
|
|
47
49
|
*/
|
|
48
50
|
context?: React.ReactNode;
|
|
51
|
+
/**
|
|
52
|
+
* Where the `context` chip sits. `inside` (the default) pins it at the very
|
|
53
|
+
* top of the shell, above any attachments. `above` lifts it out of the
|
|
54
|
+
* shell into its own bar a small gap above it, so it reads as what the
|
|
55
|
+
* model can see rather than part of the message being typed. Either way
|
|
56
|
+
* its icon starts on the shell's text rail, and a click on it focuses the
|
|
57
|
+
* textarea.
|
|
58
|
+
*/
|
|
59
|
+
contextPlacement?: 'inside' | 'above';
|
|
49
60
|
/**
|
|
50
61
|
* Icon at the left of the context chip — Material Symbol name (string,
|
|
51
62
|
* e.g. `visibility`, `article`) or custom element (ReactNode). Decorative
|
|
@@ -55,11 +66,57 @@ type ComposerOwnProps = {
|
|
|
55
66
|
*/
|
|
56
67
|
contextIcon?: string | React.ReactNode;
|
|
57
68
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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.
|
|
60
117
|
*/
|
|
61
118
|
attachments?: React.ReactNode;
|
|
62
|
-
/** 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). */
|
|
63
120
|
actions?: React.ReactNode;
|
|
64
121
|
/**
|
|
65
122
|
* Trailing actions on the right of the action bar, just before the send
|
|
@@ -77,7 +134,7 @@ export interface ComposerProps extends ComposerOwnProps, Omit<React.ComponentPro
|
|
|
77
134
|
}
|
|
78
135
|
/**
|
|
79
136
|
* Composer is the chat input shell: an optional context note ("Looking at
|
|
80
|
-
* “Page name”"),
|
|
137
|
+
* “Page name”"), a tray of queued files, an auto-growing textarea, a leading
|
|
81
138
|
* actions slot, and a trailing send button — the one sanctioned
|
|
82
139
|
* primary-action teal in the chat set, because sending a message is a
|
|
83
140
|
* genuine primary CTA. While `streaming`, send becomes stop and Enter
|
|
@@ -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,
|
|
@@ -16,6 +28,20 @@ const Composer = React.forwardRef(
|
|
|
16
28
|
aiGlow = false,
|
|
17
29
|
context,
|
|
18
30
|
contextIcon,
|
|
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,
|
|
19
45
|
attachments,
|
|
20
46
|
actions,
|
|
21
47
|
trailingActions,
|
|
@@ -24,6 +50,7 @@ const Composer = React.forwardRef(
|
|
|
24
50
|
className = "",
|
|
25
51
|
onChange,
|
|
26
52
|
onKeyDown,
|
|
53
|
+
onPaste,
|
|
27
54
|
...rest
|
|
28
55
|
}, ref) => {
|
|
29
56
|
const baseClass = "ds-composer";
|
|
@@ -33,7 +60,17 @@ const Composer = React.forwardRef(
|
|
|
33
60
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
|
|
34
61
|
const currentValue = isControlled ? value : uncontrolledValue;
|
|
35
62
|
const disabled = Boolean(rest.disabled);
|
|
36
|
-
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("");
|
|
37
74
|
const setTextareaRef = (node) => {
|
|
38
75
|
textareaRef.current = node;
|
|
39
76
|
if (typeof ref === "function") ref(node);
|
|
@@ -56,6 +93,92 @@ const Composer = React.forwardRef(
|
|
|
56
93
|
observer.observe(node);
|
|
57
94
|
return () => observer.disconnect();
|
|
58
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
|
+
};
|
|
59
182
|
const submit = () => {
|
|
60
183
|
if (streaming || !canSend) return;
|
|
61
184
|
onSubmit?.(currentValue);
|
|
@@ -82,20 +205,53 @@ const Composer = React.forwardRef(
|
|
|
82
205
|
aiGlow ? `${baseClass}--ai-glow` : "",
|
|
83
206
|
streaming ? `${baseClass}--streaming` : "",
|
|
84
207
|
disabled ? `${baseClass}--disabled` : "",
|
|
208
|
+
dragging ? `${baseClass}--dragging` : "",
|
|
209
|
+
// The above placement moves the text onto the chip's rail; the shell
|
|
210
|
+
// needs to know, since the chip is no longer inside it.
|
|
211
|
+
context && contextPlacement === "above" ? `${baseClass}--context-above` : "",
|
|
85
212
|
className
|
|
86
213
|
].filter(Boolean).join(" ");
|
|
87
214
|
const ariaLabel = rest["aria-label"] ?? (rest["aria-labelledby"] !== void 0 ? void 0 : "Message");
|
|
88
|
-
|
|
215
|
+
const contextAbove = contextPlacement === "above";
|
|
216
|
+
const contextChip = context ? /* @__PURE__ */ jsxs(
|
|
217
|
+
"div",
|
|
218
|
+
{
|
|
219
|
+
className: `${baseClass}__context${contextAbove ? ` ${baseClass}__context--above` : ""}`,
|
|
220
|
+
onClick: contextAbove ? handleShellClick : void 0,
|
|
221
|
+
children: [
|
|
222
|
+
contextIcon && /* @__PURE__ */ jsx("span", { className: `${baseClass}__context-icon`, "aria-hidden": "true", children: typeof contextIcon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: contextIcon }) : contextIcon }),
|
|
223
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__context-text`, children: context })
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
) : null;
|
|
227
|
+
const shell = /* @__PURE__ */ jsxs(
|
|
89
228
|
"div",
|
|
90
229
|
{
|
|
91
230
|
className: classes,
|
|
92
231
|
style: { "--ds-composer-max-rows": maxRows },
|
|
93
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,
|
|
94
237
|
children: [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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)) }),
|
|
99
255
|
attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
|
|
100
256
|
/* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, ref: contentRef, children: /* @__PURE__ */ jsx(
|
|
101
257
|
"textarea",
|
|
@@ -107,11 +263,24 @@ const Composer = React.forwardRef(
|
|
|
107
263
|
value: currentValue,
|
|
108
264
|
"aria-label": ariaLabel,
|
|
109
265
|
onChange: handleChange,
|
|
110
|
-
onKeyDown: handleKeyDown
|
|
266
|
+
onKeyDown: handleKeyDown,
|
|
267
|
+
onPaste: handlePaste
|
|
111
268
|
}
|
|
112
269
|
) }),
|
|
113
270
|
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__footer`, children: [
|
|
114
|
-
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
|
+
] }),
|
|
115
284
|
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__trailing`, children: [
|
|
116
285
|
trailingActions,
|
|
117
286
|
streaming ? /* @__PURE__ */ jsx(
|
|
@@ -134,10 +303,34 @@ const Composer = React.forwardRef(
|
|
|
134
303
|
}
|
|
135
304
|
)
|
|
136
305
|
] })
|
|
137
|
-
] })
|
|
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 })
|
|
138
324
|
]
|
|
139
325
|
}
|
|
140
326
|
);
|
|
327
|
+
if (contextAbove && contextChip) {
|
|
328
|
+
return /* @__PURE__ */ jsxs("div", { className: `${baseClass}-group`, children: [
|
|
329
|
+
contextChip,
|
|
330
|
+
shell
|
|
331
|
+
] });
|
|
332
|
+
}
|
|
333
|
+
return shell;
|
|
141
334
|
}
|
|
142
335
|
);
|
|
143
336
|
Composer.displayName = "Composer";
|
|
@@ -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 {};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
cell height.
|
|
8
8
|
|
|
9
9
|
Event accents use the core accent roles —
|
|
10
|
-
never the action
|
|
10
|
+
never the action colour, which stays reserved
|
|
11
11
|
for actions.
|
|
12
12
|
============================================ */
|
|
13
13
|
|
|
@@ -190,7 +190,7 @@ button.ds-event-calendar__day:focus-visible {
|
|
|
190
190
|
letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
/* Selected — the
|
|
193
|
+
/* Selected — the action-colour selection chip (the selected item of a mutually
|
|
194
194
|
exclusive set, DatePicker's convention). Declared after the today block
|
|
195
195
|
so a selected today reads as selected. */
|
|
196
196
|
.ds-event-calendar__cell--selected .ds-event-calendar__day {
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
display: block;
|
|
179
179
|
height: 100%;
|
|
180
180
|
border-radius: var(--radius-pill);
|
|
181
|
-
background-color: var(--color-
|
|
181
|
+
background-color: var(--color-core-accent-mint);
|
|
182
182
|
transition: width var(--motion-duration-base) var(--motion-ease-standard);
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -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,
|
|
@@ -54,9 +54,11 @@ type GlobeOwnProps = {
|
|
|
54
54
|
/** Fires whenever the view changes — drag, keys, or the auto-rotation. */
|
|
55
55
|
onRotationChange?: (rotation: GlobeRotation) => void;
|
|
56
56
|
/**
|
|
57
|
-
* Spin slowly on its own, in degrees per second
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* Spin slowly on its own, in degrees per second at the shipped motion
|
|
58
|
+
* tempo. The speed follows `--motion-duration-orbit`, so a theme that
|
|
59
|
+
* stretches or shortens its durations slows or quickens the spin with
|
|
60
|
+
* them. Pauses while the pointer is over the globe or it has focus, and
|
|
61
|
+
* never runs under `prefers-reduced-motion`. `0` switches it off.
|
|
60
62
|
*/
|
|
61
63
|
autoRotate?: number;
|
|
62
64
|
/** Drag to rotate, and rotate with the arrow keys (or W, A, S, D) when focused. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
3
|
import React, { useId, useState, useRef, useCallback, useSyncExternalStore, useEffect } from "react";
|
|
4
|
+
import { MOTION_ORBIT_REFERENCE_MS } from "../../tokens/motion.js";
|
|
4
5
|
import "./Globe.css";
|
|
5
6
|
const VIEW = 480;
|
|
6
7
|
const CENTER = VIEW / 2;
|
|
@@ -11,6 +12,15 @@ const DRAG_DEGREES_PER_RADIUS = 90;
|
|
|
11
12
|
const KEY_STEP = 10;
|
|
12
13
|
const MAX_LAT = 80;
|
|
13
14
|
const MARKER = 5;
|
|
15
|
+
const ORBIT_READ_INTERVAL_MS = 250;
|
|
16
|
+
const readOrbitTempo = (node) => {
|
|
17
|
+
if (!node) return 1;
|
|
18
|
+
const raw = getComputedStyle(node).getPropertyValue("--motion-duration-orbit").trim();
|
|
19
|
+
const m = raw.match(/^([\d.]+)(ms|s)$/);
|
|
20
|
+
if (!m) return 1;
|
|
21
|
+
const ms = parseFloat(m[1]) * (m[2] === "s" ? 1e3 : 1);
|
|
22
|
+
return ms > 0 ? MOTION_ORBIT_REFERENCE_MS / Math.max(ms, MOTION_ORBIT_REFERENCE_MS / 20) : 1;
|
|
23
|
+
};
|
|
14
24
|
const DEG = Math.PI / 180;
|
|
15
25
|
const toVec = (lat, lng) => {
|
|
16
26
|
const φ = lat * DEG;
|
|
@@ -148,22 +158,28 @@ const Globe = React.forwardRef(
|
|
|
148
158
|
getReducedMotion,
|
|
149
159
|
getServerReducedMotion
|
|
150
160
|
);
|
|
161
|
+
const rootRef = useRef(null);
|
|
151
162
|
const spinning = autoRotate !== 0 && !paused && !reducedMotion;
|
|
152
163
|
useEffect(() => {
|
|
153
164
|
if (!spinning) return;
|
|
154
165
|
let frame = 0;
|
|
155
166
|
let last = performance.now();
|
|
167
|
+
let tempo = readOrbitTempo(rootRef.current);
|
|
168
|
+
let lastRead = last;
|
|
156
169
|
const tick = (now) => {
|
|
157
170
|
const dt = (now - last) / 1e3;
|
|
158
171
|
last = now;
|
|
172
|
+
if (now - lastRead >= ORBIT_READ_INTERVAL_MS) {
|
|
173
|
+
tempo = readOrbitTempo(rootRef.current);
|
|
174
|
+
lastRead = now;
|
|
175
|
+
}
|
|
159
176
|
const [lng, lat] = rotationRef.current;
|
|
160
|
-
commit([lng - autoRotate * dt, lat]);
|
|
177
|
+
commit([lng - autoRotate * tempo * dt, lat]);
|
|
161
178
|
frame = requestAnimationFrame(tick);
|
|
162
179
|
};
|
|
163
180
|
frame = requestAnimationFrame(tick);
|
|
164
181
|
return () => cancelAnimationFrame(frame);
|
|
165
182
|
}, [spinning, autoRotate, commit]);
|
|
166
|
-
const rootRef = useRef(null);
|
|
167
183
|
const drag = useRef(null);
|
|
168
184
|
const [dragging, setDragging] = useState(false);
|
|
169
185
|
const setRef = (node) => {
|