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
|
@@ -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
|
+
};
|
|
@@ -39,7 +39,7 @@ const BarChart = ({
|
|
|
39
39
|
const subtitleId = useId();
|
|
40
40
|
const baseClass = "ds-chart";
|
|
41
41
|
const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
|
|
42
|
-
const resolvedBarColor = barColor || getCSSVar("--color-
|
|
42
|
+
const resolvedBarColor = barColor || getCSSVar("--color-chart-series-1", "#0E6E8F");
|
|
43
43
|
const textSecondary = getCSSVar("--color-text-secondary", "#303030");
|
|
44
44
|
const gridColor = getCSSVar("--color-divider", "rgba(214, 214, 214, 0.8)");
|
|
45
45
|
const cursorColor = getCSSVar("--color-bg-container-secondary", "#F1F1F1");
|
|
@@ -45,7 +45,7 @@ const ComboChart = ({
|
|
|
45
45
|
const baseClass = "ds-chart";
|
|
46
46
|
const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
|
|
47
47
|
const seriesColors = getChartSeriesColors();
|
|
48
|
-
const resolvedBarColor = barColor || getCSSVar("--color-
|
|
48
|
+
const resolvedBarColor = barColor || getCSSVar("--color-chart-series-1", "#0E6E8F");
|
|
49
49
|
const resolvedLineColor = lineColor || seriesColors[1];
|
|
50
50
|
const textSecondary = getCSSVar("--color-text-secondary", "#303030");
|
|
51
51
|
const gridColor = getCSSVar("--color-divider", "rgba(214, 214, 214, 0.8)");
|
|
@@ -1,22 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useId, useCallback } from "react";
|
|
3
|
+
import { useId, useCallback, useState, useEffect, useRef } from "react";
|
|
3
4
|
import { ResponsiveContainer, Treemap as Treemap$1, Tooltip } from "recharts";
|
|
4
5
|
import { getChartSeriesColors } from "./palette.js";
|
|
5
6
|
import "./Chart.css";
|
|
6
7
|
function getCSSVar(name, fallback) {
|
|
7
8
|
return `var(${name}, ${fallback})`;
|
|
8
9
|
}
|
|
10
|
+
let colorProbe;
|
|
11
|
+
function toRgb(value) {
|
|
12
|
+
const input = value.trim();
|
|
13
|
+
if (!input || input === "none") return null;
|
|
14
|
+
if (colorProbe === void 0) {
|
|
15
|
+
colorProbe = document.createElement("canvas").getContext("2d");
|
|
16
|
+
}
|
|
17
|
+
if (!colorProbe) return null;
|
|
18
|
+
colorProbe.fillStyle = "#000000";
|
|
19
|
+
colorProbe.fillStyle = input;
|
|
20
|
+
const out = String(colorProbe.fillStyle);
|
|
21
|
+
if (out.startsWith("#")) {
|
|
22
|
+
return [1, 3, 5].map((i) => parseInt(out.slice(i, i + 2), 16));
|
|
23
|
+
}
|
|
24
|
+
const m = out.match(/rgba?\(([^)]+)\)/);
|
|
25
|
+
if (!m) return null;
|
|
26
|
+
const [r, g, b] = m[1].split(/[\s,/]+/).map(Number);
|
|
27
|
+
return [r, g, b];
|
|
28
|
+
}
|
|
29
|
+
const luminance = ([r, g, b]) => {
|
|
30
|
+
const lin = (c) => {
|
|
31
|
+
const s = c / 255;
|
|
32
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
33
|
+
};
|
|
34
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
35
|
+
};
|
|
36
|
+
const contrast = (a, b) => {
|
|
37
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((p, q) => q - p);
|
|
38
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
39
|
+
};
|
|
40
|
+
const AA_TEXT = 4.5;
|
|
41
|
+
function pickLabelStyle(rect) {
|
|
42
|
+
const style = getComputedStyle(rect);
|
|
43
|
+
const fill = toRgb(style.fill);
|
|
44
|
+
const primary = toRgb(style.getPropertyValue("--color-text-primary"));
|
|
45
|
+
const onInverse = toRgb(style.getPropertyValue("--color-text-on-inverse"));
|
|
46
|
+
if (!fill || !primary || !onInverse) return { tone: "primary", halo: false };
|
|
47
|
+
const primaryRatio = contrast(primary, fill);
|
|
48
|
+
const onInverseRatio = contrast(onInverse, fill);
|
|
49
|
+
const tone = primaryRatio >= onInverseRatio ? "primary" : "on-inverse";
|
|
50
|
+
return { tone, halo: Math.max(primaryRatio, onInverseRatio) < AA_TEXT };
|
|
51
|
+
}
|
|
52
|
+
function useThemeVersion() {
|
|
53
|
+
const [version, setVersion] = useState(0);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const observer = new MutationObserver(() => setVersion((v) => v + 1));
|
|
56
|
+
observer.observe(document.documentElement, {
|
|
57
|
+
attributes: true,
|
|
58
|
+
attributeFilter: ["data-theme", "data-brand", "style", "class"]
|
|
59
|
+
});
|
|
60
|
+
return () => observer.disconnect();
|
|
61
|
+
}, []);
|
|
62
|
+
return version;
|
|
63
|
+
}
|
|
9
64
|
function TreemapContent(props) {
|
|
10
|
-
const { x, y, width, height, name, index, color } = props;
|
|
11
|
-
|
|
65
|
+
const { x, y, width, height, name, index, color, themeVersion } = props;
|
|
66
|
+
const rectRef = useRef(null);
|
|
67
|
+
const [label, setLabel] = useState({ tone: "primary", halo: false });
|
|
12
68
|
const seriesColors = getChartSeriesColors();
|
|
13
69
|
const fill = color || seriesColors[index % seriesColors.length];
|
|
14
|
-
const
|
|
70
|
+
const visible = width >= 4 && height >= 4;
|
|
15
71
|
const showLabel = width > 50 && height > 28;
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!showLabel || !rectRef.current) return;
|
|
74
|
+
setLabel(pickLabelStyle(rectRef.current));
|
|
75
|
+
}, [fill, showLabel, themeVersion]);
|
|
76
|
+
if (!visible) return null;
|
|
77
|
+
const primaryText = getCSSVar("--color-text-primary", "#050505");
|
|
78
|
+
const onInverseText = getCSSVar("--color-text-on-inverse", "#F1F1F1");
|
|
79
|
+
const textColor = label.tone === "primary" ? primaryText : onInverseText;
|
|
80
|
+
const haloColor = label.tone === "primary" ? onInverseText : primaryText;
|
|
16
81
|
return /* @__PURE__ */ jsxs("g", { children: [
|
|
17
82
|
/* @__PURE__ */ jsx(
|
|
18
83
|
"rect",
|
|
19
84
|
{
|
|
85
|
+
ref: rectRef,
|
|
20
86
|
x,
|
|
21
87
|
y,
|
|
22
88
|
width,
|
|
@@ -33,7 +99,17 @@ function TreemapContent(props) {
|
|
|
33
99
|
y: y + height / 2,
|
|
34
100
|
textAnchor: "middle",
|
|
35
101
|
dominantBaseline: "central",
|
|
36
|
-
style: {
|
|
102
|
+
style: {
|
|
103
|
+
fill: textColor,
|
|
104
|
+
fontSize: 12,
|
|
105
|
+
fontWeight: 600,
|
|
106
|
+
...label.halo && {
|
|
107
|
+
stroke: haloColor,
|
|
108
|
+
strokeWidth: 3,
|
|
109
|
+
strokeLinejoin: "round",
|
|
110
|
+
paintOrder: "stroke"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
37
113
|
children: name
|
|
38
114
|
}
|
|
39
115
|
)
|
|
@@ -62,6 +138,7 @@ const Treemap = ({
|
|
|
62
138
|
}) => {
|
|
63
139
|
const titleId = useId();
|
|
64
140
|
const subtitleId = useId();
|
|
141
|
+
const themeVersion = useThemeVersion();
|
|
65
142
|
const baseClass = "ds-chart";
|
|
66
143
|
const classes = [baseClass, bare ? `${baseClass}--bare` : "", className].filter(Boolean).join(" ");
|
|
67
144
|
const renderTooltip = useCallback(
|
|
@@ -93,7 +170,7 @@ const Treemap = ({
|
|
|
93
170
|
data,
|
|
94
171
|
dataKey,
|
|
95
172
|
aspectRatio: 4 / 3,
|
|
96
|
-
content: /* @__PURE__ */ jsx(TreemapContent, {}),
|
|
173
|
+
content: /* @__PURE__ */ jsx(TreemapContent, { themeVersion }),
|
|
97
174
|
children: /* @__PURE__ */ jsx(Tooltip, { content: renderTooltip })
|
|
98
175
|
}
|
|
99
176
|
) }) })
|
|
@@ -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` }),
|
|
@@ -243,7 +243,7 @@ const ColorPicker = React.forwardRef(
|
|
|
243
243
|
"aria-hidden": "true"
|
|
244
244
|
}
|
|
245
245
|
),
|
|
246
|
-
showText && /* @__PURE__ */ jsx("span", { className: `${baseClass}__trigger-text`, children: displayHex })
|
|
246
|
+
showText && /* @__PURE__ */ jsx("span", { id: `${inputId}-value`, className: `${baseClass}__trigger-text`, children: displayHex })
|
|
247
247
|
]
|
|
248
248
|
}
|
|
249
249
|
),
|
|
@@ -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
|
|
@@ -224,9 +224,10 @@
|
|
|
224
224
|
|
|
225
225
|
/* ============================================
|
|
226
226
|
CONTEXT CHIP
|
|
227
|
-
A full-width, non-interactive note
|
|
228
|
-
the
|
|
229
|
-
|
|
227
|
+
A full-width, non-interactive note — what the model is currently looking
|
|
228
|
+
at. At the very top of the shell by default, or lifted above it as its
|
|
229
|
+
own bar (contextPlacement="above", styled at the end of this section).
|
|
230
|
+
Composer owns the chrome; the caller passes the text.
|
|
230
231
|
============================================ */
|
|
231
232
|
|
|
232
233
|
.ds-composer__context {
|
|
@@ -267,6 +268,47 @@
|
|
|
267
268
|
--icon-size: var(--icon-size-500);
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
/* ---------- contextPlacement="above" ----------
|
|
272
|
+
The chip leaves the shell for its own bar, full width, a small gap clear
|
|
273
|
+
of the shell's top edge, and the layout aligns on one optical rail: the
|
|
274
|
+
bar's --padding-200 ring plus the --padding-400 pill inset (24px). The
|
|
275
|
+
typed text, the attachments, the chip's icon ink and a ModelPicker
|
|
276
|
+
trigger's label all start on it. The inside placement keeps its own
|
|
277
|
+
geometry above, untouched. */
|
|
278
|
+
|
|
279
|
+
/* The bar and the shell travel as one block, so a host's gap never lands
|
|
280
|
+
between them */
|
|
281
|
+
.ds-composer-group {
|
|
282
|
+
display: flex;
|
|
283
|
+
flex-direction: column;
|
|
284
|
+
gap: var(--gap-200);
|
|
285
|
+
width: 100%;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.ds-composer__context--above {
|
|
289
|
+
margin: 0;
|
|
290
|
+
/* Outside the shell, so the shell's custom properties do not reach it:
|
|
291
|
+
the send button's raw 40px, restated */
|
|
292
|
+
height: 40px;
|
|
293
|
+
/* The rail, plus the shell border this bar does not sit inside */
|
|
294
|
+
padding: 0 var(--padding-400) 0
|
|
295
|
+
calc(var(--padding-200) + var(--padding-400) + var(--border-025));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* Optical alignment: a glyph's ink starts inside its box, so the box is
|
|
299
|
+
pulled back by that side bearing to land the ink on the rail. The
|
|
300
|
+
default fits the portrait document glyphs a context note usually
|
|
301
|
+
carries (ink starts 4/24 into the box); the fallback is a consumer hook
|
|
302
|
+
for a glyph with a different bearing. */
|
|
303
|
+
.ds-composer__context--above .ds-composer__context-icon {
|
|
304
|
+
margin-left: calc(-1 * var(--ds-composer-context-icon-bearing, var(--icon-size-500) * 4 / 24));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.ds-composer--context-above .ds-composer__attachments,
|
|
308
|
+
.ds-composer--context-above .ds-composer__content {
|
|
309
|
+
padding-inline: calc(var(--padding-200) + var(--padding-400));
|
|
310
|
+
}
|
|
311
|
+
|
|
270
312
|
/* The text owns the truncation: one line, ellipsized only when the note
|
|
271
313
|
actually outgrows the shell — an ellipsis is a truncation mark, not
|
|
272
314
|
punctuation. The icon beside it never shrinks. */
|
|
@@ -277,7 +319,100 @@
|
|
|
277
319
|
}
|
|
278
320
|
|
|
279
321
|
/* ============================================
|
|
280
|
-
|
|
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)
|
|
281
416
|
============================================ */
|
|
282
417
|
|
|
283
418
|
.ds-composer__attachments {
|