@godxjp/chat 0.0.0-stage → 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,312 @@
1
+ "use client";
2
+
3
+ // src/chat-suggestion.tsx
4
+ import * as React from "react";
5
+ import { useTranslation } from "@godxjp/ui/i18n";
6
+ import { cn } from "@godxjp/ui/lib/utils";
7
+ import { VisuallyHidden } from "@godxjp/ui/general";
8
+ import { Popover, PopoverAnchor, PopoverContent } from "@godxjp/ui/data-display";
9
+ import { Command, CommandEmpty, CommandItem, CommandList } from "@godxjp/ui/data-entry";
10
+ import { isImeComposing } from "@godxjp/ui/data-entry";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ function matchTrigger(text, caret, trigger, { allowSpaces = false, terminator } = {}) {
13
+ if (!trigger) return null;
14
+ const before = text.slice(0, caret);
15
+ const start = before.lastIndexOf(trigger);
16
+ if (start === -1) return null;
17
+ const query = before.slice(start + trigger.length);
18
+ if (allowSpaces ? /\n/.test(query) : /\s/.test(query)) return null;
19
+ if (terminator && query.includes(terminator)) return null;
20
+ const preceding = start === 0 ? "" : before[start - 1];
21
+ if (trigger.length === 1 && preceding !== "" && !/\s/.test(preceding) && preceding.charCodeAt(0) < 128) {
22
+ return null;
23
+ }
24
+ return { query, start };
25
+ }
26
+ function levelItems(items, path) {
27
+ let current = items;
28
+ for (const step of path) {
29
+ const next = current.find((item) => item.value === step)?.children;
30
+ if (!next) return current;
31
+ current = next;
32
+ }
33
+ return current;
34
+ }
35
+ function labelOf(item) {
36
+ return item.label ?? item.value;
37
+ }
38
+ function matches(item, query) {
39
+ if (!query) return true;
40
+ const needle = query.toLocaleLowerCase();
41
+ return labelOf(item).toLocaleLowerCase().includes(needle) || item.value.toLocaleLowerCase().includes(needle) || (item.description?.toLocaleLowerCase().includes(needle) ?? false);
42
+ }
43
+ function ChatSuggestion({
44
+ items,
45
+ onValueChange,
46
+ triggerCharacter = "/",
47
+ open,
48
+ defaultOpen,
49
+ onOpenChange,
50
+ children,
51
+ emptyMessage,
52
+ listLabel: listLabelProp,
53
+ id,
54
+ className,
55
+ allowSpaces = false,
56
+ terminator,
57
+ shouldFilter = true,
58
+ onQueryChange
59
+ }) {
60
+ const { t } = useTranslation();
61
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false);
62
+ const isOpen = open ?? uncontrolledOpen;
63
+ const [query, setQuery] = React.useState("");
64
+ const [path, setPath] = React.useState([]);
65
+ const [activeValue, setActiveValue] = React.useState("");
66
+ const [activeId, setActiveId] = React.useState(void 0);
67
+ const anchorRef = React.useRef(null);
68
+ const contentRef = React.useRef(null);
69
+ const [listId, setListId] = React.useState(void 0);
70
+ const field = React.useCallback(() => anchorRef.current?.querySelector("textarea") ?? null, []);
71
+ const openRef = React.useRef(isOpen);
72
+ openRef.current = isOpen;
73
+ const setOpen = React.useCallback(
74
+ (next) => {
75
+ if (openRef.current === next) return;
76
+ openRef.current = next;
77
+ if (open === void 0) setUncontrolledOpen(next);
78
+ onOpenChange?.(next);
79
+ if (!next) {
80
+ setQuery("");
81
+ setPath([]);
82
+ }
83
+ },
84
+ [open, onOpenChange]
85
+ );
86
+ const visible = React.useMemo(() => {
87
+ const level = levelItems(items, path);
88
+ return shouldFilter ? level.filter((item) => matches(item, query)) : level;
89
+ }, [items, path, query, shouldFilter]);
90
+ React.useEffect(() => {
91
+ if (isOpen) onQueryChange?.(query);
92
+ }, [isOpen, query, onQueryChange]);
93
+ const enabled = React.useMemo(() => visible.filter((item) => !item.disabled), [visible]);
94
+ React.useEffect(() => {
95
+ if (enabled.length === 0) {
96
+ setActiveValue("");
97
+ return;
98
+ }
99
+ setActiveValue(
100
+ (current) => enabled.some((item) => item.value === current) ? current : enabled[0].value
101
+ );
102
+ }, [enabled]);
103
+ React.useEffect(() => {
104
+ if (!isOpen) {
105
+ setListId(void 0);
106
+ setActiveId(void 0);
107
+ return;
108
+ }
109
+ const content = contentRef.current;
110
+ setListId(content?.querySelector("[cmdk-list]")?.id || void 0);
111
+ setActiveId(content?.querySelector('[cmdk-item][aria-selected="true"]')?.id || void 0);
112
+ });
113
+ const dismissedStartRef = React.useRef(null);
114
+ const close = React.useCallback(() => {
115
+ const node = field();
116
+ const caret = node ? node.selectionStart ?? node.value.length : 0;
117
+ const hit = node ? matchTrigger(node.value, caret, triggerCharacter, { allowSpaces, terminator }) : null;
118
+ dismissedStartRef.current = hit ? hit.start : null;
119
+ setOpen(false);
120
+ field()?.focus();
121
+ }, [field, setOpen, triggerCharacter, allowSpaces, terminator]);
122
+ const evaluate = React.useCallback(() => {
123
+ const node = field();
124
+ if (!node) return;
125
+ const caret = node.selectionStart ?? node.value.length;
126
+ const hit = matchTrigger(node.value, caret, triggerCharacter, { allowSpaces, terminator });
127
+ if (!hit) {
128
+ dismissedStartRef.current = null;
129
+ setOpen(false);
130
+ return;
131
+ }
132
+ if (hit.start === dismissedStartRef.current) {
133
+ setOpen(false);
134
+ return;
135
+ }
136
+ dismissedStartRef.current = null;
137
+ setQuery(hit.query);
138
+ setOpen(true);
139
+ }, [field, setOpen, triggerCharacter, allowSpaces, terminator]);
140
+ const onTrigger = React.useCallback(
141
+ (next) => {
142
+ if (next === false) {
143
+ setOpen(false);
144
+ return;
145
+ }
146
+ if (next === void 0) {
147
+ setQuery("");
148
+ setOpen(true);
149
+ return;
150
+ }
151
+ window.setTimeout(evaluate, 0);
152
+ },
153
+ [evaluate, setOpen]
154
+ );
155
+ const pick = React.useCallback(
156
+ (item) => {
157
+ if (item.disabled) return;
158
+ if (item.children && item.children.length > 0) {
159
+ setPath((current) => [...current, item.value]);
160
+ setQuery("");
161
+ field()?.focus();
162
+ return;
163
+ }
164
+ onValueChange?.(item.value);
165
+ setOpen(false);
166
+ field()?.focus();
167
+ },
168
+ [field, onValueChange, setOpen]
169
+ );
170
+ const move = React.useCallback(
171
+ (direction) => {
172
+ if (enabled.length === 0) return;
173
+ const index = enabled.findIndex((item) => item.value === activeValue);
174
+ const from = index === -1 ? direction === 1 ? -1 : 0 : index;
175
+ const next = (from + direction + enabled.length) % enabled.length;
176
+ setActiveValue(enabled[next].value);
177
+ },
178
+ [activeValue, enabled]
179
+ );
180
+ const onKeyDown = React.useCallback(
181
+ (event) => {
182
+ if (isImeComposing(event)) return;
183
+ if (!openRef.current) {
184
+ if (event.key.startsWith("Arrow") || event.key === "Home" || event.key === "End") {
185
+ window.setTimeout(evaluate, 0);
186
+ }
187
+ return;
188
+ }
189
+ switch (event.key) {
190
+ case "ArrowDown":
191
+ event.preventDefault();
192
+ move(1);
193
+ return;
194
+ case "ArrowUp":
195
+ event.preventDefault();
196
+ move(-1);
197
+ return;
198
+ case "Enter":
199
+ case "Tab": {
200
+ const item = enabled.find((candidate) => candidate.value === activeValue);
201
+ if (!item) return;
202
+ event.preventDefault();
203
+ pick(item);
204
+ return;
205
+ }
206
+ case "Escape":
207
+ event.preventDefault();
208
+ event.stopPropagation();
209
+ close();
210
+ return;
211
+ default:
212
+ if (event.key.startsWith("Arrow") || event.key === "Home" || event.key === "End") {
213
+ window.setTimeout(evaluate, 0);
214
+ }
215
+ }
216
+ },
217
+ [activeValue, close, enabled, evaluate, move, pick]
218
+ );
219
+ const listLabel = listLabelProp ?? t("dataEntry.chatSuggestion.label");
220
+ return /* @__PURE__ */ jsxs(Popover, { open: isOpen, onOpenChange: setOpen, children: [
221
+ /* @__PURE__ */ jsxs(
222
+ PopoverAnchor,
223
+ {
224
+ ref: anchorRef,
225
+ id,
226
+ "data-slot": "chat-suggestion",
227
+ className: cn("block w-full", className),
228
+ onPointerUp: () => window.setTimeout(evaluate, 0),
229
+ onBlurCapture: (event) => {
230
+ const next = event.relatedTarget;
231
+ if (next && anchorRef.current?.contains(next)) return;
232
+ if (openRef.current) setOpen(false);
233
+ },
234
+ children: [
235
+ children({
236
+ onTrigger,
237
+ onKeyDown
238
+ }),
239
+ /* @__PURE__ */ jsx(FieldPopupWiring, { field, open: isOpen, listId, activeId }),
240
+ /* @__PURE__ */ jsx(VisuallyHidden, { role: "status", "aria-live": "polite", children: isOpen ? t("dataEntry.chatSuggestion.count", { count: visible.length }) : "" })
241
+ ]
242
+ }
243
+ ),
244
+ /* @__PURE__ */ jsx(
245
+ PopoverContent,
246
+ {
247
+ ref: contentRef,
248
+ flush: true,
249
+ align: "start",
250
+ "aria-label": listLabel,
251
+ className: "ui-chat-suggestion-panel",
252
+ onOpenAutoFocus: (event) => event.preventDefault(),
253
+ onCloseAutoFocus: (event) => event.preventDefault(),
254
+ children: /* @__PURE__ */ jsx(
255
+ Command,
256
+ {
257
+ value: activeValue,
258
+ onValueChange: setActiveValue,
259
+ shouldFilter: false,
260
+ label: listLabel,
261
+ children: /* @__PURE__ */ jsxs(CommandList, { label: listLabel, children: [
262
+ /* @__PURE__ */ jsx(CommandEmpty, { children: emptyMessage ?? t("dataEntry.chatSuggestion.empty") }),
263
+ visible.map((item) => /* @__PURE__ */ jsxs(
264
+ CommandItem,
265
+ {
266
+ value: item.value,
267
+ disabled: item.disabled,
268
+ onSelect: () => pick(item),
269
+ children: [
270
+ item.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: item.icon }) : null,
271
+ /* @__PURE__ */ jsxs("span", { className: "ui-chat-suggestion-item-text", children: [
272
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: labelOf(item) }),
273
+ item.description ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground truncate text-xs", children: item.description }) : null
274
+ ] })
275
+ ]
276
+ },
277
+ item.value
278
+ ))
279
+ ] })
280
+ }
281
+ )
282
+ }
283
+ )
284
+ ] });
285
+ }
286
+ ChatSuggestion.displayName = "ChatSuggestion";
287
+ function FieldPopupWiring({
288
+ field,
289
+ open,
290
+ listId,
291
+ activeId
292
+ }) {
293
+ React.useEffect(() => {
294
+ const node = field();
295
+ if (!node) return;
296
+ node.setAttribute("aria-haspopup", "listbox");
297
+ node.setAttribute("aria-autocomplete", "list");
298
+ if (open && listId) {
299
+ node.setAttribute("aria-controls", listId);
300
+ if (activeId) node.setAttribute("aria-activedescendant", activeId);
301
+ else node.removeAttribute("aria-activedescendant");
302
+ } else {
303
+ node.removeAttribute("aria-controls");
304
+ node.removeAttribute("aria-activedescendant");
305
+ }
306
+ }, [activeId, field, listId, open]);
307
+ return null;
308
+ }
309
+
310
+ export {
311
+ ChatSuggestion
312
+ };
@@ -0,0 +1,374 @@
1
+ "use client";
2
+
3
+ // src/conversations.tsx
4
+ import * as React from "react";
5
+ import { ChevronDown, MoreHorizontal, Plus } from "lucide-react";
6
+ import { useTranslation } from "@godxjp/ui/i18n";
7
+ import { formatDate } from "@godxjp/ui/datetime";
8
+ import { cn } from "@godxjp/ui/lib/utils";
9
+ import { Button } from "@godxjp/ui/general";
10
+ import { Text } from "@godxjp/ui/general";
11
+ import { Separator } from "@godxjp/ui/layout";
12
+ import {
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuItem,
16
+ DropdownMenuTrigger
17
+ } from "@godxjp/ui/navigation";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ function isDivider(entry) {
20
+ return "type" in entry && entry.type === "divider";
21
+ }
22
+ function isRtl(element) {
23
+ return element?.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl";
24
+ }
25
+ function toSections(items, groupable) {
26
+ const collapsibleOption = typeof groupable === "object" ? groupable.collapsible : void 0;
27
+ const sections = [];
28
+ for (const entry of items) {
29
+ const group = !groupable || isDivider(entry) ? void 0 : entry.group;
30
+ if (group === void 0) {
31
+ const tail = sections[sections.length - 1];
32
+ if (tail && tail.group === void 0) {
33
+ tail.entries.push(entry);
34
+ continue;
35
+ }
36
+ sections.push({ entries: [entry], collapsible: false });
37
+ continue;
38
+ }
39
+ const existing = sections.find((section) => section.group === group);
40
+ if (existing) {
41
+ existing.entries.push(entry);
42
+ continue;
43
+ }
44
+ sections.push({
45
+ group,
46
+ entries: [entry],
47
+ collapsible: typeof collapsibleOption === "function" ? collapsibleOption(group) : collapsibleOption ?? false
48
+ });
49
+ }
50
+ return sections;
51
+ }
52
+ var groupNavKey = (group) => `group:${group}`;
53
+ var rowNavKey = (key) => `row:${key}`;
54
+ var Conversations = React.forwardRef(
55
+ ({
56
+ items = [],
57
+ activeKey,
58
+ defaultActiveKey,
59
+ onActiveChange,
60
+ menu,
61
+ groupable,
62
+ creation,
63
+ label,
64
+ id,
65
+ className,
66
+ ...rest
67
+ }, ref) => {
68
+ const { t } = useTranslation();
69
+ const rootRef = React.useRef(null);
70
+ const setRootRef = React.useCallback(
71
+ (node) => {
72
+ rootRef.current = node;
73
+ if (typeof ref === "function") ref(node);
74
+ else if (ref) ref.current = node;
75
+ },
76
+ [ref]
77
+ );
78
+ const [uncontrolledActive, setUncontrolledActive] = React.useState(defaultActiveKey);
79
+ const mergedActiveKey = activeKey ?? uncontrolledActive;
80
+ const sections = React.useMemo(() => toSections(items, groupable), [items, groupable]);
81
+ const groupOptions = typeof groupable === "object" ? groupable : {};
82
+ const collapsibleGroups = React.useMemo(
83
+ () => sections.filter((section) => section.collapsible && section.group).map((s) => s.group),
84
+ [sections]
85
+ );
86
+ const [uncontrolledExpanded, setUncontrolledExpanded] = React.useState(() => [
87
+ ...groupOptions.defaultExpandedKeys ?? collapsibleGroups
88
+ ]);
89
+ const expandedKeys = groupOptions.expandedKeys ? [...groupOptions.expandedKeys] : uncontrolledExpanded;
90
+ const toggleGroup = (group) => {
91
+ const next = expandedKeys.includes(group) ? expandedKeys.filter((key) => key !== group) : [...expandedKeys, group];
92
+ if (!groupOptions.expandedKeys) setUncontrolledExpanded(next);
93
+ groupOptions.onExpand?.(next);
94
+ };
95
+ const navKeys = [];
96
+ for (const section of sections) {
97
+ if (section.group && section.collapsible) {
98
+ navKeys.push(groupNavKey(section.group));
99
+ if (!expandedKeys.includes(section.group)) continue;
100
+ }
101
+ for (const entry of section.entries) {
102
+ if (!isDivider(entry) && !entry.disabled) navKeys.push(rowNavKey(entry.key));
103
+ }
104
+ }
105
+ const [focusKey, setFocusKey] = React.useState(null);
106
+ const activeNavKey = mergedActiveKey ? rowNavKey(mergedActiveKey) : void 0;
107
+ const tabStop = focusKey && navKeys.includes(focusKey) ? focusKey : activeNavKey && navKeys.includes(activeNavKey) ? activeNavKey : navKeys[0];
108
+ const nodes = React.useRef(/* @__PURE__ */ new Map());
109
+ const registerNode = (key) => (node) => {
110
+ if (node) nodes.current.set(key, node);
111
+ else nodes.current.delete(key);
112
+ };
113
+ const moveTo = (key) => {
114
+ if (!key) return;
115
+ setFocusKey(key);
116
+ nodes.current.get(key)?.focus();
117
+ };
118
+ const step = (from, delta) => {
119
+ if (navKeys.length === 0) return;
120
+ const index = navKeys.indexOf(from);
121
+ const next = index === -1 ? 0 : (index + delta + navKeys.length) % navKeys.length;
122
+ moveTo(navKeys[next]);
123
+ };
124
+ const handleNavKeyDown = (navKey) => (event) => {
125
+ const rtl = isRtl(event.currentTarget);
126
+ const forward = rtl ? "ArrowLeft" : "ArrowRight";
127
+ const backward = rtl ? "ArrowRight" : "ArrowLeft";
128
+ switch (event.key) {
129
+ case "ArrowDown":
130
+ event.preventDefault();
131
+ step(navKey, 1);
132
+ return;
133
+ case "ArrowUp":
134
+ event.preventDefault();
135
+ step(navKey, -1);
136
+ return;
137
+ case "Home":
138
+ event.preventDefault();
139
+ moveTo(navKeys[0]);
140
+ return;
141
+ case "End":
142
+ event.preventDefault();
143
+ moveTo(navKeys[navKeys.length - 1]);
144
+ return;
145
+ case forward: {
146
+ const trigger = nodes.current.get(`menu:${navKey}`);
147
+ if (!trigger) return;
148
+ event.preventDefault();
149
+ trigger.focus();
150
+ return;
151
+ }
152
+ case backward: {
153
+ if (!navKey.startsWith("menu:")) return;
154
+ event.preventDefault();
155
+ moveTo(navKey.slice("menu:".length));
156
+ return;
157
+ }
158
+ default:
159
+ }
160
+ };
161
+ const selectConversation = (item) => {
162
+ if (item.disabled) return;
163
+ if (activeKey === void 0) setUncontrolledActive(item.key);
164
+ onActiveChange?.(
165
+ item.key,
166
+ items.find((entry) => !isDivider(entry) && entry.key === item.key)
167
+ );
168
+ };
169
+ const menuFor = (item) => {
170
+ const resolved = typeof menu === "function" ? menu(item) : menu;
171
+ return resolved && resolved.items.length > 0 ? resolved : void 0;
172
+ };
173
+ const renderEntry = (entry, index) => {
174
+ if (isDivider(entry)) {
175
+ return /* @__PURE__ */ jsx("li", { className: "ui-conversations-rule", children: /* @__PURE__ */ jsx(
176
+ Separator,
177
+ {
178
+ "data-dashed": entry.dashed ? "true" : void 0,
179
+ className: "ui-conversations-separator"
180
+ }
181
+ ) }, entry.key ?? `divider-${index}`);
182
+ }
183
+ const navKey = rowNavKey(entry.key);
184
+ const active = mergedActiveKey === entry.key;
185
+ const rowMenu = entry.disabled ? void 0 : menuFor(entry);
186
+ const labelText = typeof entry.label === "string" ? entry.label : entry.key;
187
+ return /* @__PURE__ */ jsxs(
188
+ "li",
189
+ {
190
+ "data-slot": "conversations-item",
191
+ "data-active": active ? "true" : void 0,
192
+ className: "ui-conversations-item",
193
+ children: [
194
+ /* @__PURE__ */ jsxs(
195
+ Button,
196
+ {
197
+ ref: registerNode(navKey),
198
+ type: "button",
199
+ variant: "ghost",
200
+ fullWidth: true,
201
+ align: "start",
202
+ disabled: entry.disabled,
203
+ "aria-current": active ? "true" : void 0,
204
+ tabIndex: tabStop === navKey ? 0 : -1,
205
+ className: "ui-conversations-row",
206
+ onFocus: () => setFocusKey(navKey),
207
+ onClick: () => selectConversation(entry),
208
+ onKeyDown: handleNavKeyDown(navKey),
209
+ children: [
210
+ entry.icon ? /* @__PURE__ */ jsx("span", { className: "ui-conversations-icon", "aria-hidden": "true", children: entry.icon }) : null,
211
+ /* @__PURE__ */ jsxs("span", { className: "ui-conversations-line", children: [
212
+ /* @__PURE__ */ jsx(Text, { truncate: true, children: entry.label }),
213
+ entry.timestamp === void 0 ? null : (
214
+ /*
215
+ * Through `formatDate`, never through `toLocaleString` or a sliced ISO string.
216
+ * `docs/DATETIME.md` rule 1 forbids the alternatives for a measured reason: the
217
+ * tenant's locale, timezone and 12/24h choice live in AppProvider, so the same
218
+ * epoch has to read 2026/03/15 in a Japanese tenant and 15/03/2026 in a
219
+ * Vietnamese one. A component that formats its own dates cannot do that.
220
+ */
221
+ /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", tabular: true, className: "ui-conversations-timestamp", children: formatDate(new Date(entry.timestamp)) })
222
+ )
223
+ ] })
224
+ ]
225
+ }
226
+ ),
227
+ rowMenu ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
228
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
229
+ Button,
230
+ {
231
+ ref: registerNode(`menu:${navKey}`),
232
+ type: "button",
233
+ variant: "ghost",
234
+ size: "icon-sm",
235
+ tabIndex: -1,
236
+ className: "ui-conversations-menu-trigger",
237
+ "aria-label": rowMenu.triggerLabel?.(entry) ?? t("navigation.conversations.rowActions", { label: labelText }),
238
+ onKeyDown: handleNavKeyDown(`menu:${navKey}`),
239
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": "true" })
240
+ }
241
+ ) }),
242
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", children: rowMenu.items.map((command) => /* @__PURE__ */ jsxs(
243
+ DropdownMenuItem,
244
+ {
245
+ disabled: command.disabled,
246
+ variant: command.danger ? "destructive" : void 0,
247
+ onSelect: () => rowMenu.onClick?.({ key: command.key, conversation: entry }),
248
+ children: [
249
+ command.icon,
250
+ command.label
251
+ ]
252
+ },
253
+ command.key
254
+ )) })
255
+ ] }) : null
256
+ ]
257
+ },
258
+ entry.key
259
+ );
260
+ };
261
+ const railLabel = label ?? t("navigation.conversations.label");
262
+ return /* @__PURE__ */ jsxs(
263
+ "div",
264
+ {
265
+ ref: setRootRef,
266
+ id,
267
+ "data-slot": "conversations",
268
+ className: cn("ui-conversations", className),
269
+ ...rest,
270
+ children: [
271
+ creation ? /* @__PURE__ */ jsx(CreationButton, { creation }) : null,
272
+ sections.map((section, sectionIndex) => {
273
+ if (!section.group) {
274
+ return /* @__PURE__ */ jsx(
275
+ "ul",
276
+ {
277
+ "aria-label": railLabel,
278
+ className: "ui-conversations-list",
279
+ children: section.entries.map(renderEntry)
280
+ },
281
+ `run-${sectionIndex}`
282
+ );
283
+ }
284
+ const heading = typeof groupOptions.label === "function" ? groupOptions.label(section.group) : groupOptions.label ?? section.group;
285
+ const navKey = groupNavKey(section.group);
286
+ const expanded = expandedKeys.includes(section.group);
287
+ const listId = `${id ?? "ui-conversations"}-${section.group}`;
288
+ return /* @__PURE__ */ jsxs(
289
+ "div",
290
+ {
291
+ "data-slot": "conversations-group",
292
+ className: "ui-conversations-group",
293
+ children: [
294
+ section.collapsible ? /* @__PURE__ */ jsxs(
295
+ Button,
296
+ {
297
+ ref: registerNode(navKey),
298
+ type: "button",
299
+ variant: "ghost",
300
+ size: "sm",
301
+ fullWidth: true,
302
+ align: "start",
303
+ "aria-expanded": expanded,
304
+ "aria-controls": listId,
305
+ tabIndex: tabStop === navKey ? 0 : -1,
306
+ className: "ui-conversations-group-trigger",
307
+ onFocus: () => setFocusKey(navKey),
308
+ onClick: () => toggleGroup(section.group),
309
+ onKeyDown: handleNavKeyDown(navKey),
310
+ children: [
311
+ /* @__PURE__ */ jsx(
312
+ ChevronDown,
313
+ {
314
+ "aria-hidden": "true",
315
+ className: "ui-conversations-group-chevron",
316
+ "data-expanded": expanded ? "true" : void 0
317
+ }
318
+ ),
319
+ /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", weight: "medium", truncate: true, children: heading })
320
+ ]
321
+ }
322
+ ) : /* @__PURE__ */ jsx(
323
+ Text,
324
+ {
325
+ size: "xs",
326
+ tone: "muted",
327
+ weight: "medium",
328
+ className: "ui-conversations-group-label",
329
+ children: heading
330
+ }
331
+ ),
332
+ section.collapsible && !expanded ? null : /* @__PURE__ */ jsx(
333
+ "ul",
334
+ {
335
+ id: listId,
336
+ "aria-label": typeof heading === "string" ? heading : section.group,
337
+ className: "ui-conversations-list",
338
+ children: section.entries.map(renderEntry)
339
+ }
340
+ )
341
+ ]
342
+ },
343
+ section.group
344
+ );
345
+ })
346
+ ]
347
+ }
348
+ );
349
+ }
350
+ );
351
+ Conversations.displayName = "Conversations";
352
+ function CreationButton({ creation }) {
353
+ const { t } = useTranslation();
354
+ return /* @__PURE__ */ jsxs(
355
+ Button,
356
+ {
357
+ type: "button",
358
+ variant: "outline",
359
+ fullWidth: true,
360
+ align: "start",
361
+ disabled: creation.disabled,
362
+ className: "ui-conversations-creation",
363
+ onClick: () => creation.onClick?.(),
364
+ children: [
365
+ creation.icon ?? /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true" }),
366
+ creation.label ?? t("navigation.conversations.creation")
367
+ ]
368
+ }
369
+ );
370
+ }
371
+
372
+ export {
373
+ Conversations
374
+ };