@godxjp/chat 32.0.0 → 32.0.1

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.
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ // src/welcome.tsx
4
+ import * as React from "react";
5
+ import { cn } from "@godxjp/ui/lib/utils";
6
+ import { Heading, Text } from "@godxjp/ui/general";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function isImageUrl(icon) {
9
+ return typeof icon === "string" && icon.startsWith("http");
10
+ }
11
+ var Welcome = React.forwardRef(
12
+ ({ icon, title, description, extra, variant = "filled", id, className, ...rest }, ref) => {
13
+ return /* @__PURE__ */ jsxs(
14
+ "div",
15
+ {
16
+ ref,
17
+ id,
18
+ "data-slot": "welcome",
19
+ "data-variant": variant,
20
+ className: cn("ui-welcome", className),
21
+ ...rest,
22
+ children: [
23
+ icon ? /* @__PURE__ */ jsx("div", { "data-slot": "welcome-icon", className: "ui-welcome-icon", "aria-hidden": "true", children: isImageUrl(icon) ? /* @__PURE__ */ jsx("img", { src: icon, alt: "" }) : icon }) : null,
24
+ /* @__PURE__ */ jsxs("div", { "data-slot": "welcome-content", className: "ui-welcome-content", children: [
25
+ title || extra ? /* @__PURE__ */ jsxs("div", { "data-slot": "welcome-title-row", className: "ui-welcome-title-row", children: [
26
+ title ? /* @__PURE__ */ jsx(Heading, { level: 4, className: "ui-welcome-title", children: title }) : null,
27
+ extra ? /* @__PURE__ */ jsx("div", { "data-slot": "welcome-extra", className: "ui-welcome-extra", children: extra }) : null
28
+ ] }) : null,
29
+ description ? /* @__PURE__ */ jsx(Text, { size: "sm", tone: "muted", className: "ui-welcome-description", children: description }) : null
30
+ ] })
31
+ ]
32
+ }
33
+ );
34
+ }
35
+ );
36
+ Welcome.displayName = "Welcome";
37
+
38
+ export {
39
+ Welcome
40
+ };
@@ -0,0 +1,369 @@
1
+ "use client";
2
+
3
+ // src/attachments.tsx
4
+ import * as React from "react";
5
+ import { FileIcon, ImagePlus, Plus, X } from "lucide-react";
6
+ import { useTranslation } from "@godxjp/ui/i18n";
7
+ import { formatBytes } from "@godxjp/ui/admin";
8
+ import { cn } from "@godxjp/ui/lib/utils";
9
+ import { Button } from "@godxjp/ui/general";
10
+ import { Progress } from "@godxjp/ui/data-display";
11
+ import { createUploadItem } from "@godxjp/ui/data-entry";
12
+ import { readDroppedFiles } from "@godxjp/ui/data-entry";
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
+ var AttachmentContext = React.createContext({ disabled: false });
15
+ function toAttachmentItem(item) {
16
+ const status = item.status === "uploading" ? "uploading" : item.status === "done" ? "done" : item.status === "error" ? "error" : item.status === "removed" ? "removed" : void 0;
17
+ return {
18
+ uid: item.uid,
19
+ name: item.name,
20
+ size: item.size,
21
+ status,
22
+ percent: item.percent,
23
+ url: item.url,
24
+ thumbUrl: item.previewUrl,
25
+ originFileObj: item.file,
26
+ response: item.response,
27
+ error: item.error
28
+ };
29
+ }
30
+ function resolvePlaceholder(placeholder, type) {
31
+ if (!placeholder) return {};
32
+ return typeof placeholder === "function" ? placeholder(type) : placeholder;
33
+ }
34
+ function splitFileName(name) {
35
+ const match = /^(.*)\.[^.]+$/.exec(name ?? "");
36
+ return match ? [match[1], (name ?? "").slice(match[1].length)] : [name ?? "", ""];
37
+ }
38
+ function fileMatchesAccept(file, accept) {
39
+ if (!accept) return true;
40
+ return accept.split(",").some((rule) => {
41
+ const trimmed = rule.trim().toLowerCase();
42
+ if (trimmed.endsWith("/*")) return file.type.toLowerCase().startsWith(trimmed.slice(0, -1));
43
+ return file.type.toLowerCase() === trimmed || trimmed.startsWith(".") && file.name.toLowerCase().endsWith(trimmed);
44
+ });
45
+ }
46
+ function PlaceholderUploader({
47
+ type,
48
+ placeholder,
49
+ onOpen,
50
+ onFiles,
51
+ className,
52
+ style,
53
+ hidden
54
+ }) {
55
+ const { t } = useTranslation();
56
+ const { disabled } = React.useContext(AttachmentContext);
57
+ const [dragActive, setDragActive] = React.useState(false);
58
+ if (hidden) return null;
59
+ const title = placeholder.title ?? t("dataEntry.attachments.placeholderTitle");
60
+ const description = placeholder.description ?? t("dataEntry.attachments.placeholderDescription");
61
+ const icon = placeholder.icon ?? /* @__PURE__ */ jsx(ImagePlus, { "aria-hidden": "true" });
62
+ return /* @__PURE__ */ jsxs(
63
+ "div",
64
+ {
65
+ className: cn("ui-attachments-placeholder", className),
66
+ style,
67
+ "data-drag-active": dragActive ? "true" : void 0,
68
+ "data-disabled": disabled ? "true" : void 0,
69
+ "aria-hidden": disabled ? true : void 0,
70
+ onDragEnter: (event) => {
71
+ event.preventDefault();
72
+ if (!disabled) setDragActive(true);
73
+ },
74
+ onDragOver: (event) => event.preventDefault(),
75
+ onDragLeave: (event) => {
76
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragActive(false);
77
+ },
78
+ onDrop: async (event) => {
79
+ event.preventDefault();
80
+ event.stopPropagation();
81
+ setDragActive(false);
82
+ if (disabled) return;
83
+ const files = await readDroppedFiles(event.dataTransfer, false);
84
+ if (files.length) onFiles(files);
85
+ },
86
+ onClick: () => {
87
+ if (!disabled) onOpen();
88
+ },
89
+ onKeyDown: (event) => {
90
+ if (disabled) return;
91
+ if (event.key === "Enter" || event.key === " ") {
92
+ event.preventDefault();
93
+ onOpen();
94
+ }
95
+ },
96
+ role: "button",
97
+ tabIndex: disabled ? -1 : 0,
98
+ "aria-label": String(title),
99
+ children: [
100
+ /* @__PURE__ */ jsx("span", { className: "ui-attachments-placeholder-icon", children: icon }),
101
+ title ? /* @__PURE__ */ jsx("span", { className: "ui-attachments-placeholder-title", children: title }) : null,
102
+ description ? /* @__PURE__ */ jsx("span", { className: "ui-attachments-placeholder-description", children: description }) : null,
103
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("dataEntry.attachments.dropLabel", { type }) })
104
+ ]
105
+ }
106
+ );
107
+ }
108
+ function AttachmentCard({
109
+ item,
110
+ description,
111
+ onRemove,
112
+ classNames
113
+ }) {
114
+ const { t } = useTranslation();
115
+ const { disabled } = React.useContext(AttachmentContext);
116
+ const preview = item.thumbUrl ?? item.url;
117
+ const [namePrefix, nameSuffix] = splitFileName(item.name);
118
+ const desc = description ?? item.description ?? (item.status === "uploading" ? `${item.percent ?? 0}%` : item.status === "error" ? typeof item.response === "string" ? item.response : t("dataEntry.attachments.error") : item.size ? formatBytes(item.size) : "");
119
+ return /* @__PURE__ */ jsxs(
120
+ "li",
121
+ {
122
+ className: cn("ui-attachments-card", classNames?.card, classNames?.file),
123
+ "data-status": item.status,
124
+ children: [
125
+ preview ? /* @__PURE__ */ jsx("img", { src: preview, alt: "", className: "ui-attachments-card-preview", "aria-hidden": "true" }) : /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-placeholder", "aria-hidden": "true", children: /* @__PURE__ */ jsx(FileIcon, {}) }),
126
+ /* @__PURE__ */ jsxs("span", { className: "ui-attachments-card-content", children: [
127
+ /* @__PURE__ */ jsxs("span", { className: "ui-attachments-card-name", title: item.name, children: [
128
+ /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-name-prefix", children: namePrefix }),
129
+ /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-name-suffix", children: nameSuffix })
130
+ ] }),
131
+ desc ? /* @__PURE__ */ jsx(
132
+ "span",
133
+ {
134
+ className: "ui-attachments-card-meta",
135
+ title: typeof desc === "string" ? desc : void 0,
136
+ children: desc
137
+ }
138
+ ) : null
139
+ ] }),
140
+ item.status === "uploading" && item.percent != null ? /* @__PURE__ */ jsx("span", { className: "ui-attachments-card-progress", children: /* @__PURE__ */ jsx(
141
+ Progress,
142
+ {
143
+ size: "sm",
144
+ value: item.percent,
145
+ "aria-label": t("dataEntry.attachments.uploading")
146
+ }
147
+ ) }) : null,
148
+ !disabled && onRemove ? /* @__PURE__ */ jsx(
149
+ Button,
150
+ {
151
+ type: "button",
152
+ variant: "ghost",
153
+ size: "icon-xs",
154
+ className: "ui-attachments-card-remove",
155
+ "aria-label": t("dataEntry.attachments.remove", { name: item.name }),
156
+ onClick: (event) => {
157
+ event.stopPropagation();
158
+ onRemove();
159
+ },
160
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
161
+ }
162
+ ) : null
163
+ ]
164
+ }
165
+ );
166
+ }
167
+ var Attachments = React.forwardRef(
168
+ ({
169
+ className,
170
+ rootClassName,
171
+ classNames = {},
172
+ styles = {},
173
+ items: controlledItems,
174
+ onChange,
175
+ onRemove,
176
+ overflow,
177
+ placeholder,
178
+ getDropContainer,
179
+ disabled = false,
180
+ maxCount,
181
+ accept,
182
+ multiple,
183
+ children
184
+ }, ref) => {
185
+ const { t } = useTranslation();
186
+ const containerRef = React.useRef(null);
187
+ const inputRef = React.useRef(null);
188
+ const [internalItems, setInternalItems] = React.useState([]);
189
+ const [dropActive, setDropActive] = React.useState(false);
190
+ const items = controlledItems ?? internalItems;
191
+ const setItems = React.useCallback(
192
+ (next, file) => {
193
+ if (controlledItems === void 0) setInternalItems(next);
194
+ onChange?.({ file, fileList: next });
195
+ },
196
+ [controlledItems, onChange]
197
+ );
198
+ const openPicker = React.useCallback(() => {
199
+ inputRef.current?.click();
200
+ }, []);
201
+ const dispatchFiles = React.useCallback(
202
+ (files) => {
203
+ const accepted = files.filter((file) => fileMatchesAccept(file, accept));
204
+ if (!accepted.length) return;
205
+ const room = maxCount == null ? accepted.length : Math.max(0, maxCount - items.length);
206
+ const picked = accepted.slice(0, room);
207
+ const next = [
208
+ ...items,
209
+ ...picked.map((file) => {
210
+ const uploadItem = createUploadItem(file);
211
+ return toAttachmentItem(uploadItem);
212
+ })
213
+ ];
214
+ const last = next[next.length - 1];
215
+ if (last) setItems(next, last);
216
+ },
217
+ [accept, items, maxCount, setItems]
218
+ );
219
+ React.useImperativeHandle(ref, () => ({
220
+ nativeElement: containerRef.current,
221
+ fileNativeElement: inputRef.current,
222
+ upload: (file) => dispatchFiles([file]),
223
+ select: (options) => {
224
+ const input = inputRef.current;
225
+ if (!input) return;
226
+ input.multiple = options?.multiple ?? false;
227
+ input.accept = options?.accept ?? accept ?? "";
228
+ input.click();
229
+ }
230
+ }));
231
+ const handleRemove = async (item) => {
232
+ const ret = await Promise.resolve(typeof onRemove === "function" ? onRemove(item) : onRemove);
233
+ if (ret === false) return;
234
+ const next = items.filter((entry) => entry.uid !== item.uid);
235
+ setItems(next, { ...item, status: "removed" });
236
+ };
237
+ const showExtension = !disabled && (maxCount == null || maxCount > items.length) && items.length > 0;
238
+ const inlinePlaceholder = resolvePlaceholder(placeholder, "inline");
239
+ const dropPlaceholder = resolvePlaceholder(placeholder, "drop");
240
+ const hasFiles = items.length > 0;
241
+ const fileInput = /* @__PURE__ */ jsx(
242
+ "input",
243
+ {
244
+ ref: inputRef,
245
+ type: "file",
246
+ className: cn("ui-attachments-input", "sr-only"),
247
+ accept,
248
+ multiple: multiple ?? (maxCount == null || maxCount > 1),
249
+ disabled,
250
+ "aria-label": t("dataEntry.attachments.inputLabel"),
251
+ onChange: (event) => {
252
+ const files = event.target.files ? Array.from(event.target.files) : [];
253
+ event.target.value = "";
254
+ dispatchFiles(files);
255
+ }
256
+ }
257
+ );
258
+ if (children) {
259
+ return /* @__PURE__ */ jsxs(AttachmentContext.Provider, { value: { disabled }, children: [
260
+ fileInput,
261
+ /* @__PURE__ */ jsx(
262
+ "div",
263
+ {
264
+ ref: containerRef,
265
+ className: cn("ui-attachments", rootClassName, classNames.root),
266
+ style: styles.root,
267
+ children
268
+ }
269
+ ),
270
+ /* @__PURE__ */ jsx(
271
+ PlaceholderUploader,
272
+ {
273
+ type: "drop",
274
+ placeholder: dropPlaceholder,
275
+ onOpen: openPicker,
276
+ onFiles: dispatchFiles,
277
+ className: classNames.placeholder,
278
+ style: styles.placeholder
279
+ }
280
+ )
281
+ ] });
282
+ }
283
+ const dropScope = getDropContainer?.() ? "viewport" : "control";
284
+ return /* @__PURE__ */ jsx(AttachmentContext.Provider, { value: { disabled }, children: /* @__PURE__ */ jsxs(
285
+ "div",
286
+ {
287
+ ref: containerRef,
288
+ className: cn("ui-attachments", className, rootClassName, classNames.root),
289
+ style: styles.root,
290
+ dir: "auto",
291
+ onDragEnter: (event) => {
292
+ event.preventDefault();
293
+ if (!disabled) setDropActive(true);
294
+ },
295
+ onDragOver: (event) => event.preventDefault(),
296
+ onDragLeave: (event) => {
297
+ if (!event.currentTarget.contains(event.relatedTarget)) setDropActive(false);
298
+ },
299
+ onDrop: async (event) => {
300
+ event.preventDefault();
301
+ setDropActive(false);
302
+ if (disabled) return;
303
+ const files = await readDroppedFiles(event.dataTransfer, false);
304
+ if (files.length) dispatchFiles(files);
305
+ },
306
+ children: [
307
+ fileInput,
308
+ hasFiles ? /* @__PURE__ */ jsxs(
309
+ "ul",
310
+ {
311
+ className: cn("ui-attachments-list", classNames.list),
312
+ style: styles.list,
313
+ "data-overflow": overflow ?? "wrap",
314
+ children: [
315
+ items.map((item) => /* @__PURE__ */ jsx(
316
+ AttachmentCard,
317
+ {
318
+ item,
319
+ onRemove: () => handleRemove(item),
320
+ classNames
321
+ },
322
+ item.uid
323
+ )),
324
+ showExtension ? /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
325
+ "button",
326
+ {
327
+ type: "button",
328
+ className: cn("ui-attachments-upload-btn", classNames.upload),
329
+ style: styles.upload,
330
+ disabled,
331
+ "aria-label": t("dataEntry.attachments.add"),
332
+ onClick: openPicker,
333
+ children: /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true" })
334
+ }
335
+ ) }) : null
336
+ ]
337
+ }
338
+ ) : null,
339
+ /* @__PURE__ */ jsx(
340
+ PlaceholderUploader,
341
+ {
342
+ type: "inline",
343
+ placeholder: inlinePlaceholder,
344
+ onOpen: openPicker,
345
+ onFiles: dispatchFiles,
346
+ className: classNames.placeholder,
347
+ style: hasFiles ? { display: "none" } : styles.placeholder,
348
+ hidden: hasFiles
349
+ }
350
+ ),
351
+ /* @__PURE__ */ jsx(
352
+ "div",
353
+ {
354
+ className: "ui-attachments-drop-layer",
355
+ "data-scope": dropScope,
356
+ "data-active": dropActive ? "true" : void 0,
357
+ "aria-hidden": "true"
358
+ }
359
+ )
360
+ ]
361
+ }
362
+ ) });
363
+ }
364
+ );
365
+ Attachments.displayName = "Attachments";
366
+
367
+ export {
368
+ Attachments
369
+ };
@@ -0,0 +1,232 @@
1
+ "use client";
2
+
3
+ // src/chat-bubble.tsx
4
+ import * as React from "react";
5
+ import { ArrowDown } from "lucide-react";
6
+ import { useTranslation } from "@godxjp/ui/i18n";
7
+ import { useMediaQuery } from "@godxjp/ui/hooks";
8
+ import { cn } from "@godxjp/ui/lib/utils";
9
+ import { Skeleton } from "@godxjp/ui/feedback";
10
+ import { Button } from "@godxjp/ui/general";
11
+ import { Text } from "@godxjp/ui/general";
12
+ import { VisuallyHidden } from "@godxjp/ui/general";
13
+ import { ScrollArea } from "@godxjp/ui/data-display";
14
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ var BODY_SIZE = {
16
+ xs: "2xs",
17
+ sm: "xs",
18
+ md: "sm",
19
+ lg: "lg"
20
+ };
21
+ var META_SIZE = {
22
+ xs: "2xs",
23
+ sm: "2xs",
24
+ md: "xs",
25
+ lg: "sm"
26
+ };
27
+ var TYPING_STEP = 1;
28
+ var TYPING_INTERVAL = 50;
29
+ function resolveTyping(typing) {
30
+ if (typing === void 0 || typing === false) {
31
+ return { enabled: false, step: TYPING_STEP, interval: TYPING_INTERVAL };
32
+ }
33
+ if (typing === true) return { enabled: true, step: TYPING_STEP, interval: TYPING_INTERVAL };
34
+ return {
35
+ enabled: true,
36
+ // A non-positive step would never reach the end of the string and a non-positive interval
37
+ // would spin the timer — both are "the animation never completes", which is the one outcome
38
+ // a reader can neither read nor dismiss.
39
+ step: typing.step != null && typing.step > 0 ? Math.floor(typing.step) : TYPING_STEP,
40
+ interval: typing.interval != null && typing.interval > 0 ? typing.interval : TYPING_INTERVAL
41
+ };
42
+ }
43
+ function useRevealedLength(text, enabled, step, interval) {
44
+ const full = text?.length ?? 0;
45
+ const [revealed, setRevealed] = React.useState(() => enabled ? 0 : full);
46
+ React.useEffect(() => {
47
+ if (!enabled || text === void 0) {
48
+ setRevealed(text?.length ?? 0);
49
+ return void 0;
50
+ }
51
+ setRevealed(0);
52
+ let shown = 0;
53
+ const timer = setInterval(() => {
54
+ shown = Math.min(text.length, shown + step);
55
+ setRevealed(shown);
56
+ if (shown >= text.length) clearInterval(timer);
57
+ }, interval);
58
+ return () => {
59
+ clearInterval(timer);
60
+ };
61
+ }, [text, enabled, step, interval]);
62
+ return Math.min(revealed, full);
63
+ }
64
+ var TONE_KEY = {
65
+ info: "chat.bubble.tone.info",
66
+ success: "chat.bubble.tone.success",
67
+ warning: "chat.bubble.tone.warning",
68
+ destructive: "chat.bubble.tone.destructive"
69
+ };
70
+ var ChatBubble = React.forwardRef(
71
+ ({
72
+ children,
73
+ placement = "start",
74
+ variant = "filled",
75
+ avatar,
76
+ header,
77
+ footer,
78
+ loading = false,
79
+ typing,
80
+ size = "md",
81
+ tone = "default",
82
+ className,
83
+ id,
84
+ ...props
85
+ }, ref) => {
86
+ const { t } = useTranslation();
87
+ const generatedId = React.useId();
88
+ const headerId = `${id ?? generatedId}-header`;
89
+ const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
90
+ const text = typeof children === "string" ? children : void 0;
91
+ const { enabled, step, interval } = resolveTyping(typing);
92
+ const animates = enabled && !loading && text !== void 0 && !reduceMotion;
93
+ const revealed = useRevealedLength(text, animates, step, interval);
94
+ const streaming = animates && revealed < (text?.length ?? 0);
95
+ const body = loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
96
+ /* @__PURE__ */ jsxs("div", { className: "ui-chat-bubble-skeleton", "aria-hidden": "true", children: [
97
+ /* @__PURE__ */ jsx(Skeleton, { className: "ui-chat-bubble-skeleton-line" }),
98
+ /* @__PURE__ */ jsx(Skeleton, { className: "ui-chat-bubble-skeleton-line ui-chat-bubble-skeleton-line--short" })
99
+ ] }),
100
+ /* @__PURE__ */ jsx(VisuallyHidden, { children: t("chat.bubble.loading") })
101
+ ] }) : streaming ? /* @__PURE__ */ jsxs(Fragment, { children: [
102
+ /* @__PURE__ */ jsxs(Text, { as: "div", size: BODY_SIZE[size], whitespace: "pre-wrap", "aria-hidden": "true", children: [
103
+ text?.slice(0, revealed),
104
+ /* @__PURE__ */ jsx("span", { className: "ui-chat-bubble-caret" })
105
+ ] }),
106
+ /* @__PURE__ */ jsx(VisuallyHidden, { children: t("chat.bubble.typing") })
107
+ ] }) : /* @__PURE__ */ jsx(
108
+ Text,
109
+ {
110
+ as: "div",
111
+ size: BODY_SIZE[size],
112
+ whitespace: text !== void 0 ? "pre-wrap" : void 0,
113
+ children
114
+ }
115
+ );
116
+ return /* @__PURE__ */ jsxs(
117
+ "article",
118
+ {
119
+ ref,
120
+ id,
121
+ "data-slot": "chat-bubble",
122
+ "data-placement": placement,
123
+ "data-variant": variant,
124
+ "data-size": size,
125
+ "data-tone": tone,
126
+ "aria-busy": loading || streaming ? true : void 0,
127
+ "aria-labelledby": header != null ? headerId : void 0,
128
+ className: cn("ui-chat-bubble", className),
129
+ ...props,
130
+ children: [
131
+ avatar != null ? /* @__PURE__ */ jsx("span", { "data-slot": "chat-bubble-avatar", className: "ui-chat-bubble-avatar", children: avatar }) : null,
132
+ /* @__PURE__ */ jsxs("div", { "data-slot": "chat-bubble-column", className: "ui-chat-bubble-column", children: [
133
+ header != null ? /* @__PURE__ */ jsx(
134
+ Text,
135
+ {
136
+ as: "div",
137
+ id: headerId,
138
+ className: "ui-chat-bubble-header",
139
+ size: META_SIZE[size],
140
+ tone: "muted",
141
+ weight: "medium",
142
+ children: header
143
+ }
144
+ ) : null,
145
+ /* @__PURE__ */ jsxs("div", { "data-slot": "chat-bubble-body", className: "ui-chat-bubble-body", children: [
146
+ tone !== "default" ? /* @__PURE__ */ jsx(VisuallyHidden, { children: t(TONE_KEY[tone]) }) : null,
147
+ body
148
+ ] }),
149
+ footer != null ? /* @__PURE__ */ jsx(Text, { as: "div", className: "ui-chat-bubble-footer", size: META_SIZE[size], tone: "muted", children: footer }) : null
150
+ ] })
151
+ ]
152
+ }
153
+ );
154
+ }
155
+ );
156
+ ChatBubble.displayName = "ChatBubble";
157
+ var ChatBubbleList = React.forwardRef(
158
+ ({ items, roles, autoScroll = true, label, className, id, ...props }, ref) => {
159
+ const { t } = useTranslation();
160
+ const viewportRef = React.useRef(null);
161
+ const [anchored, setAnchored] = React.useState(true);
162
+ const seen = React.useRef(items.length);
163
+ React.useEffect(() => {
164
+ if (anchored) seen.current = items.length;
165
+ }, [anchored, items.length]);
166
+ const pending = anchored ? 0 : Math.max(0, items.length - seen.current);
167
+ const jumpToLatest = React.useCallback(() => {
168
+ const viewport = viewportRef.current;
169
+ if (viewport) {
170
+ const bottom = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
171
+ if (typeof viewport.scrollTo === "function") {
172
+ viewport.scrollTo({ top: bottom, behavior: "instant" });
173
+ } else {
174
+ viewport.scrollTop = bottom;
175
+ }
176
+ }
177
+ setAnchored(true);
178
+ }, []);
179
+ return /* @__PURE__ */ jsxs(
180
+ "div",
181
+ {
182
+ ref,
183
+ id,
184
+ "data-slot": "chat-bubble-list",
185
+ "data-anchored": anchored ? "" : void 0,
186
+ className: cn("ui-chat-bubble-list", className),
187
+ ...props,
188
+ children: [
189
+ /* @__PURE__ */ jsx(
190
+ ScrollArea,
191
+ {
192
+ className: "ui-chat-bubble-list-scroller",
193
+ anchor: autoScroll ? "bottom" : "none",
194
+ viewportRef,
195
+ onAnchoredChange: setAnchored,
196
+ children: /* @__PURE__ */ jsx(
197
+ "div",
198
+ {
199
+ role: "log",
200
+ "aria-live": "polite",
201
+ "aria-label": label ?? t("chat.list.label"),
202
+ "data-slot": "chat-bubble-list-feed",
203
+ className: "ui-chat-bubble-list-feed",
204
+ children: items.map(({ id: messageId, role, content, ...message }) => /* @__PURE__ */ jsx(
205
+ ChatBubble,
206
+ {
207
+ id: messageId,
208
+ ...role != null ? roles?.[role] : void 0,
209
+ ...message,
210
+ children: content
211
+ },
212
+ messageId
213
+ ))
214
+ }
215
+ )
216
+ }
217
+ ),
218
+ anchored ? null : /* @__PURE__ */ jsx("div", { "data-slot": "chat-bubble-list-jump", className: "ui-chat-bubble-list-jump", children: /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", variant: "secondary", shape: "pill", onClick: jumpToLatest, children: [
219
+ /* @__PURE__ */ jsx(ArrowDown, { "aria-hidden": "true" }),
220
+ pending > 0 ? t("chat.list.newMessages", { count: pending }) : t("chat.list.jumpToLatest")
221
+ ] }) })
222
+ ]
223
+ }
224
+ );
225
+ }
226
+ );
227
+ ChatBubbleList.displayName = "ChatBubbleList";
228
+
229
+ export {
230
+ ChatBubble,
231
+ ChatBubbleList
232
+ };
@@ -0,0 +1,42 @@
1
+ import * as React from 'react';
2
+ import { ConversationsProp } from '@godxjp/ui/props/components';
3
+ export { ConversationsCreationProp, ConversationsDividerProp, ConversationsEntryProp, ConversationsGroupableProp, ConversationsItemProp, ConversationsMenuItemProp, ConversationsMenuProp, ConversationsProp, ConversationsProp as ConversationsProps } from '@godxjp/ui/props/components';
4
+
5
+ /**
6
+ * Conversations — the session rail of a chat surface (Ant Design X `Conversations`).
7
+ *
8
+ * ## What is ported, and the one half that is deliberately not
9
+ *
10
+ * Every prop a caller passes keeps Ant X's spelling and semantics: `items` (conversations and
11
+ * `{ type: "divider" }` rules), `activeKey` / `defaultActiveKey` / `onActiveChange`, `menu` (flat
12
+ * or per-row), `groupable` (with `label`, `collapsible` and the `expandedKeys` triad) and
13
+ * `creation`. The two knobs left out are `styles` / `classNames` — the semantic style maps — and
14
+ * they are left out by a standing decision of this repository, not by omission: a free-form style
15
+ * hole freezes internal DOM slot names into public API and is unmeasurable by the audit
16
+ * (docs/WHAT-BELONGS-HERE.md · "Lỗ kiểu dáng tự do"; docs/DESIGN-AUTHORITY.md · "A knob that only
17
+ * a fork could reach is not parity either"). The rail is retuned through
18
+ * `src/tokens/components/conversations.css` instead. `prefixCls`, `rootClassName`,
19
+ * `menu.getPopupContainer`, `menu.trigger` and `shortcutKeys` are out for the same reason or
20
+ * because the subsystem behind them does not exist here (see the PR for the per-prop ledger).
21
+ *
22
+ * ## What this adds, because Ant X's rail has no keyboard model at all
23
+ *
24
+ * Measured in `@ant-design/x@2.9.0`: `es/conversations/Item.js` renders a bare
25
+ * `<li title … onClick>`. No `role`, no `tabIndex`, no key handling — the rail is unreachable by
26
+ * keyboard, and a screen reader meets twelve list items with no hint that any of them is a
27
+ * control or that one of them is current. Neither is the hand-rolled alternative the issue
28
+ * describes (a stack of `Button`s) any better on the axis that matters: it is one tab stop PER
29
+ * conversation.
30
+ *
31
+ * So the rows here are real `Button`s under ONE roving tabindex, the APG composite-widget rule:
32
+ *
33
+ * - the whole rail — group headings included — is a single tab stop;
34
+ * - `↑` / `↓` walk it and wrap, `Home` / `End` jump to its ends;
35
+ * - the logical FORWARD arrow (`→` in LTR, `←` in RTL) steps from a row onto its overflow menu,
36
+ * and the backward arrow steps back, so a row's second action costs no extra tab stop;
37
+ * - the current conversation carries `aria-current="true"`, which is what makes "current" survive
38
+ * a theme that does not tint (WCAG 1.4.1 — never colour alone).
39
+ */
40
+ declare const Conversations: React.ForwardRefExoticComponent<ConversationsProp & React.RefAttributes<HTMLDivElement>>;
41
+
42
+ export { Conversations };
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import {
3
+ Conversations
4
+ } from "./chunk-IWWOS4BL.js";
5
+ export {
6
+ Conversations
7
+ };