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.
Files changed (68) hide show
  1. package/README.md +2 -2
  2. package/components/Attachment/Attachment.css +1036 -0
  3. package/components/Attachment/AttachmentDropZone.d.ts +39 -0
  4. package/components/Attachment/AttachmentDropZone.js +76 -0
  5. package/components/Attachment/AttachmentGroup.d.ts +60 -0
  6. package/components/Attachment/AttachmentGroup.js +179 -0
  7. package/components/Attachment/AttachmentTile.d.ts +80 -0
  8. package/components/Attachment/AttachmentTile.js +143 -0
  9. package/components/Attachment/AttachmentViewer.d.ts +66 -0
  10. package/components/Attachment/AttachmentViewer.js +215 -0
  11. package/components/Attachment/fileTypes.d.ts +101 -0
  12. package/components/Attachment/fileTypes.js +182 -0
  13. package/components/Attachment/typeMark.d.ts +11 -0
  14. package/components/Attachment/typeMark.js +51 -0
  15. package/components/Attachment/useAttachments.d.ts +65 -0
  16. package/components/Attachment/useAttachments.js +201 -0
  17. package/components/Chart/BarChart.js +1 -1
  18. package/components/Chart/ComboChart.js +1 -1
  19. package/components/Chart/Treemap.js +83 -6
  20. package/components/ChatMessage/ChatMessage.css +24 -0
  21. package/components/ChatMessage/ChatMessage.d.ts +6 -0
  22. package/components/ChatMessage/ChatMessage.js +8 -5
  23. package/components/ColorPicker/ColorPicker.js +1 -1
  24. package/components/Composer/Composer.css +140 -5
  25. package/components/Composer/Composer.d.ts +71 -14
  26. package/components/Composer/Composer.js +204 -11
  27. package/components/DatePicker/DatePicker.css +1 -1
  28. package/components/DocumentChip/DocumentChip.d.ts +7 -5
  29. package/components/EventCalendar/EventCalendar.css +2 -2
  30. package/components/FileInput/FileInput.css +1 -1
  31. package/components/FileInput/FileInput.js +1 -7
  32. package/components/Globe/Globe.d.ts +5 -3
  33. package/components/Globe/Globe.js +18 -2
  34. package/components/Inspector/Inspector.css +108 -3
  35. package/components/Inspector/InspectorColorPicker.d.ts +43 -0
  36. package/components/Inspector/InspectorColorPicker.js +51 -0
  37. package/components/Instructions/Instructions.css +4 -4
  38. package/components/MessageCard/MessageCard.d.ts +1 -1
  39. package/components/Prose/Prose.css +27 -0
  40. package/components/Sparkline/Sparkline.css +2 -2
  41. package/components/Spinner/Spinner.css +1 -1
  42. package/components/Timeline/Timeline.css +1 -1
  43. package/components/WorldMap/land.d.ts +1 -1
  44. package/components/registry.json +48 -3
  45. package/components/registry.json.d.ts +48 -3
  46. package/components/registry.json.js +1 -1
  47. package/index.d.ts +5 -0
  48. package/index.js +21 -0
  49. package/package.json +9 -1
  50. package/tokens/motion.d.ts +2 -0
  51. package/tokens/motion.js +2 -0
  52. package/tokens/presets/contrast.css +2 -4
  53. package/tokens/presets/coral.css +2 -4
  54. package/tokens/presets/forest.css +2 -4
  55. package/tokens/presets/gold.css +4 -4
  56. package/tokens/presets/mono.css +0 -2
  57. package/tokens/presets/pink.css +4 -4
  58. package/tokens/presets/terminal.css +5 -4
  59. package/tokens/presets/violet.css +2 -4
  60. package/tokens/presets/warm.css +3 -4
  61. package/tokens/presets/zest.css +2 -4
  62. package/tokens/registry.json +5 -0
  63. package/tokens/registry.json.d.ts +5 -0
  64. package/tokens/registry.json.js +1 -1
  65. package/tokens/tokens-dark.css +14 -5
  66. package/tokens/tokens-light.css +16 -2
  67. package/tokens/tokens-motion.css +2 -0
  68. 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`, and never when the trimmed value is
17
- * empty. Composer does not clear the value: the consumer owns it and clears
18
- * it after a successful submit. Shadows the native `onSubmit` attribute,
19
- * which never fires on a textarea anyway.
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 at the
42
- * very top of the shell, above any attachments — the "what the model is
43
- * looking at" line a chat host pins over the message ("Looking at
44
- * “Page name”"). One line: a note too long for the shell truncates with
45
- * an ellipsis. Composer owns the chip's chrome; the caller passes the
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
- * Attachment row rendered above the textarea (DocumentChips). Fully
59
- * controlled by the caller — Composer never owns the list.
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 (attach button, model picker). */
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”"), an attachments row, an auto-growing textarea, a leading
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 canSend = !disabled && currentValue.trim() !== "";
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
- return /* @__PURE__ */ jsxs(
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
- context && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__context`, children: [
96
- 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 }),
97
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__context-text`, children: context })
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__ */ jsx("div", { className: `${baseClass}__actions`, children: actions }),
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";
@@ -138,7 +138,7 @@
138
138
 
139
139
  /* Today indicator */
140
140
  .ds-date-picker__cell--today {
141
- border: var(--border-025) solid var(--color-action-primary-bg);
141
+ border: var(--border-025) solid var(--color-bg-page-inverse);
142
142
  }
143
143
 
144
144
  /* Selected */
@@ -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 a compact file reference: a two-line tile with a type
32
- * icon, name, metadata line, upload progress, and an optional remove
33
- * button. It references documents attached to chat messages or queued
34
- * above a composer — Chip stays the one-line pill for attributes, and
35
- * FileInput stays the form control that owns selection.
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 teal, which stays reserved
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 teal selection chip (the selected item of a mutually
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-action-primary-bg);
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. Pauses while the pointer
58
- * is over the globe or it has focus, and never runs under
59
- * `prefers-reduced-motion`. `0` switches it off.
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) => {