@burtson-labs/ui 0.10.1 → 0.12.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 +7 -2
- package/dist/components/audio-player.d.ts +39 -0
- package/dist/components/audio-player.js +250 -0
- package/dist/components/audio-player.js.map +1 -0
- package/dist/components/chat-history.d.ts +64 -0
- package/dist/components/chat-history.js +282 -0
- package/dist/components/chat-history.js.map +1 -0
- package/dist/components/chat-layout.d.ts +42 -0
- package/dist/components/chat-layout.js +217 -0
- package/dist/components/chat-layout.js.map +1 -0
- package/dist/components/combobox.js +1 -1
- package/dist/components/composer.d.ts +16 -1
- package/dist/components/composer.js +103 -20
- package/dist/components/composer.js.map +1 -1
- package/dist/components/data-table.js +1 -1
- package/dist/components/message-actions.d.ts +56 -0
- package/dist/components/message-actions.js +181 -0
- package/dist/components/message-actions.js.map +1 -0
- package/dist/components/secret-input.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/voice-recorder.d.ts +28 -0
- package/dist/components/voice-recorder.js +240 -0
- package/dist/components/voice-recorder.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +13 -8
- package/dist/mui/index.d.ts +25 -2
- package/dist/mui/index.js +191 -13
- package/dist/mui/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/theme.css +13 -0
- package/dist/tokens.d.ts +53 -1
- package/dist/tokens.js +97 -1
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { Button } from "./button.js";
|
|
3
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "./dropdown-menu.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import AlertCircle from "@burtson-labs/icons/react/alert-circle";
|
|
7
|
+
import MoreHorizontal from "@burtson-labs/icons/react/more-horizontal";
|
|
8
|
+
import Edit from "@burtson-labs/icons/react/edit";
|
|
9
|
+
import MessageSquarePlus from "@burtson-labs/icons/react/message-square-plus";
|
|
10
|
+
import Pin from "@burtson-labs/icons/react/pin";
|
|
11
|
+
import PinOff from "@burtson-labs/icons/react/pin-off";
|
|
12
|
+
import Search from "@burtson-labs/icons/react/search";
|
|
13
|
+
import Trash from "@burtson-labs/icons/react/trash";
|
|
14
|
+
//#region src/components/chat-history.tsx
|
|
15
|
+
var LABELS = {
|
|
16
|
+
pinned: "Pinned",
|
|
17
|
+
today: "Today",
|
|
18
|
+
yesterday: "Yesterday",
|
|
19
|
+
week: "Previous 7 days",
|
|
20
|
+
older: "Older"
|
|
21
|
+
};
|
|
22
|
+
var time = (d) => new Date(d).getTime();
|
|
23
|
+
var startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
24
|
+
/**
|
|
25
|
+
* Pinned first, then Today, Yesterday, Previous 7 days and Older by the
|
|
26
|
+
* local calendar, newest first in each. Empty groups are left out.
|
|
27
|
+
*/
|
|
28
|
+
function groupConversations(items, now = /* @__PURE__ */ new Date()) {
|
|
29
|
+
const today = startOfDay(now);
|
|
30
|
+
const day = 864e5;
|
|
31
|
+
const buckets = {
|
|
32
|
+
pinned: [],
|
|
33
|
+
today: [],
|
|
34
|
+
yesterday: [],
|
|
35
|
+
week: [],
|
|
36
|
+
older: []
|
|
37
|
+
};
|
|
38
|
+
for (const item of [...items].sort((a, b) => time(b.updatedAt) - time(a.updatedAt))) {
|
|
39
|
+
if (item.pinned) {
|
|
40
|
+
buckets.pinned.push(item);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const t = startOfDay(new Date(item.updatedAt));
|
|
44
|
+
buckets[t >= today ? "today" : t >= today - day ? "yesterday" : t >= today - 7 * day ? "week" : "older"].push(item);
|
|
45
|
+
}
|
|
46
|
+
return Object.keys(buckets).filter((k) => buckets[k].length > 0).map((k) => ({
|
|
47
|
+
key: k,
|
|
48
|
+
label: LABELS[k],
|
|
49
|
+
items: buckets[k]
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
/** Case-insensitive match on every word, across title and preview. */
|
|
53
|
+
function matchesConversation(item, query) {
|
|
54
|
+
const hay = `${item.title} ${item.preview ?? ""}`.toLowerCase();
|
|
55
|
+
return query.toLowerCase().split(/\s+/).filter(Boolean).every((w) => hay.includes(w));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The conversation list beside a chat: a New chat action, search, and the
|
|
59
|
+
* history grouped by day with pinned chats on top. Up and Down move between
|
|
60
|
+
* rows, Home and End jump, Enter opens; each row has a menu for rename, pin
|
|
61
|
+
* and delete. The app owns the data; this only reports what the person did.
|
|
62
|
+
*/
|
|
63
|
+
function ChatHistory({ items, activeId, onSelect, onNewChat, onRename, onDelete, onPinChange, query: controlledQuery, onQueryChange, searchable = true, loading = false, error, onRetry, empty, now, renderItems, label = "Chat history", className, ...props }) {
|
|
64
|
+
const [ownQuery, setOwnQuery] = React.useState("");
|
|
65
|
+
const query = controlledQuery ?? ownQuery;
|
|
66
|
+
const setQuery = (q) => {
|
|
67
|
+
if (controlledQuery === void 0) setOwnQuery(q);
|
|
68
|
+
onQueryChange?.(q);
|
|
69
|
+
};
|
|
70
|
+
const [renaming, setRenaming] = React.useState(null);
|
|
71
|
+
const list = React.useRef(null);
|
|
72
|
+
const searchId = React.useId();
|
|
73
|
+
const groups = groupConversations(query.trim() ? items.filter((i) => matchesConversation(i, query)) : items, now);
|
|
74
|
+
const onRowKeyDown = (e) => {
|
|
75
|
+
const rows = Array.from(list.current?.querySelectorAll("[data-slot=\"chat-history-row\"]") ?? []);
|
|
76
|
+
const i = rows.indexOf(document.activeElement);
|
|
77
|
+
let next = -1;
|
|
78
|
+
if (e.key === "ArrowDown") next = Math.min(rows.length - 1, i + 1);
|
|
79
|
+
else if (e.key === "ArrowUp") next = Math.max(0, i - 1);
|
|
80
|
+
else if (e.key === "Home") next = 0;
|
|
81
|
+
else if (e.key === "End") next = rows.length - 1;
|
|
82
|
+
const row = next >= 0 ? rows[next] : void 0;
|
|
83
|
+
if (row) {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
row.focus();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const renderItem = (item) => /* @__PURE__ */ jsx(HistoryRow, {
|
|
89
|
+
item,
|
|
90
|
+
active: item.id === activeId,
|
|
91
|
+
renaming: renaming === item.id,
|
|
92
|
+
onSelect: () => onSelect(item.id),
|
|
93
|
+
onStartRename: onRename ? () => setRenaming(item.id) : void 0,
|
|
94
|
+
onRename: (title) => {
|
|
95
|
+
setRenaming(null);
|
|
96
|
+
if (onRename && title.trim() && title.trim() !== item.title) onRename(item.id, title.trim());
|
|
97
|
+
},
|
|
98
|
+
onDelete: onDelete ? () => onDelete(item.id) : void 0,
|
|
99
|
+
onPinChange: onPinChange ? (p) => onPinChange(item.id, p) : void 0,
|
|
100
|
+
onKeyDown: onRowKeyDown
|
|
101
|
+
}, item.id);
|
|
102
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
103
|
+
"data-slot": "chat-history",
|
|
104
|
+
"aria-label": label,
|
|
105
|
+
role: "navigation",
|
|
106
|
+
className: cn("flex h-full min-h-0 flex-col", className),
|
|
107
|
+
...props,
|
|
108
|
+
children: [(onNewChat || searchable) && /* @__PURE__ */ jsxs("div", {
|
|
109
|
+
className: "grid gap-2 p-2",
|
|
110
|
+
children: [onNewChat && /* @__PURE__ */ jsxs(Button, {
|
|
111
|
+
variant: "outline",
|
|
112
|
+
className: "justify-start",
|
|
113
|
+
onClick: onNewChat,
|
|
114
|
+
children: [/* @__PURE__ */ jsx(MessageSquarePlus, {}), " New chat"]
|
|
115
|
+
}), searchable && /* @__PURE__ */ jsxs("label", {
|
|
116
|
+
htmlFor: searchId,
|
|
117
|
+
className: "relative block",
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ jsx("span", {
|
|
120
|
+
className: "sr-only",
|
|
121
|
+
children: "Search chats"
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ jsx(Search, {
|
|
124
|
+
"aria-hidden": true,
|
|
125
|
+
className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
|
|
126
|
+
}),
|
|
127
|
+
/* @__PURE__ */ jsx("input", {
|
|
128
|
+
id: searchId,
|
|
129
|
+
type: "search",
|
|
130
|
+
value: query,
|
|
131
|
+
onChange: (e) => setQuery(e.target.value),
|
|
132
|
+
placeholder: "Search chats",
|
|
133
|
+
className: "h-8 w-full rounded-md border border-input bg-surface pr-2 pl-8 text-base outline-none placeholder:text-muted-foreground focus-visible:border-brand focus-visible:ring-[3px] focus-visible:ring-ring/15 sm:text-sm"
|
|
134
|
+
})
|
|
135
|
+
]
|
|
136
|
+
})]
|
|
137
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
138
|
+
ref: list,
|
|
139
|
+
className: "min-h-0 flex-1 overflow-y-auto px-2 pb-2",
|
|
140
|
+
children: loading ? /* @__PURE__ */ jsx("div", {
|
|
141
|
+
role: "status",
|
|
142
|
+
"aria-label": "Loading chats",
|
|
143
|
+
className: "grid gap-1.5 pt-1",
|
|
144
|
+
children: [
|
|
145
|
+
72,
|
|
146
|
+
56,
|
|
147
|
+
64,
|
|
148
|
+
48,
|
|
149
|
+
60
|
|
150
|
+
].map((w, i) => /* @__PURE__ */ jsxs("div", {
|
|
151
|
+
className: "h-8 rounded-md bg-muted/70 motion-safe:animate-pulse",
|
|
152
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
153
|
+
className: "sr-only",
|
|
154
|
+
children: "Loading"
|
|
155
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
156
|
+
"aria-hidden": true,
|
|
157
|
+
className: "ml-2 inline-block h-2.5 translate-y-3 rounded bg-muted-foreground/15",
|
|
158
|
+
style: { width: `${w}%` }
|
|
159
|
+
})]
|
|
160
|
+
}, i))
|
|
161
|
+
}) : error ? /* @__PURE__ */ jsxs("div", {
|
|
162
|
+
role: "alert",
|
|
163
|
+
className: "grid justify-items-start gap-2 p-2 text-sm",
|
|
164
|
+
children: [
|
|
165
|
+
/* @__PURE__ */ jsxs("span", {
|
|
166
|
+
className: "inline-flex items-center gap-1.5 font-medium text-destructive [&_svg]:size-4",
|
|
167
|
+
children: [/* @__PURE__ */ jsx(AlertCircle, { "aria-hidden": true }), " Chats could not load"]
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ jsx("span", {
|
|
170
|
+
className: "text-muted-foreground",
|
|
171
|
+
children: error
|
|
172
|
+
}),
|
|
173
|
+
onRetry && /* @__PURE__ */ jsx(Button, {
|
|
174
|
+
size: "sm",
|
|
175
|
+
variant: "outline",
|
|
176
|
+
onClick: onRetry,
|
|
177
|
+
children: "Try again"
|
|
178
|
+
})
|
|
179
|
+
]
|
|
180
|
+
}) : items.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
181
|
+
className: "p-3 text-sm text-muted-foreground",
|
|
182
|
+
children: empty ?? "No chats yet. Start one and it will appear here."
|
|
183
|
+
}) : groups.length === 0 ? /* @__PURE__ */ jsxs("p", {
|
|
184
|
+
role: "status",
|
|
185
|
+
className: "p-3 text-sm text-muted-foreground",
|
|
186
|
+
children: [
|
|
187
|
+
"No chats match “",
|
|
188
|
+
query,
|
|
189
|
+
"”."
|
|
190
|
+
]
|
|
191
|
+
}) : groups.map((g) => /* @__PURE__ */ jsxs("section", {
|
|
192
|
+
"aria-label": g.label,
|
|
193
|
+
className: "mt-2 first:mt-0",
|
|
194
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
195
|
+
className: "px-2 pt-2 pb-1 text-[11px] font-semibold tracking-wide text-muted-foreground uppercase",
|
|
196
|
+
children: g.label
|
|
197
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
198
|
+
className: "grid grid-cols-[minmax(0,1fr)] gap-0.5",
|
|
199
|
+
children: renderItems ? renderItems(g.items, renderItem) : g.items.map(renderItem)
|
|
200
|
+
})]
|
|
201
|
+
}, g.key))
|
|
202
|
+
})]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function HistoryRow({ item, active, renaming, onSelect, onStartRename, onRename, onDelete, onPinChange, onKeyDown }) {
|
|
206
|
+
const hasMenu = Boolean(onStartRename || onDelete || onPinChange);
|
|
207
|
+
if (renaming) return /* @__PURE__ */ jsx(RenameRow, {
|
|
208
|
+
title: item.title,
|
|
209
|
+
onDone: onRename
|
|
210
|
+
});
|
|
211
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
212
|
+
className: "group/row relative min-w-0",
|
|
213
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
214
|
+
type: "button",
|
|
215
|
+
"data-slot": "chat-history-row",
|
|
216
|
+
"aria-current": active ? "page" : void 0,
|
|
217
|
+
onClick: onSelect,
|
|
218
|
+
onKeyDown,
|
|
219
|
+
className: cn("flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-sm outline-none transition-colors hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/20 pointer-coarse:h-11", hasMenu && "pr-9", active && "bg-accent font-medium text-accent-foreground hover:bg-accent"),
|
|
220
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
221
|
+
className: "min-w-0 flex-1 truncate",
|
|
222
|
+
children: item.title || "Untitled chat"
|
|
223
|
+
})
|
|
224
|
+
}), hasMenu && /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
225
|
+
"aria-label": `Actions for ${item.title || "chat"}`,
|
|
226
|
+
className: cn("absolute top-1/2 right-1 grid size-7 -translate-y-1/2 place-items-center rounded-sm text-muted-foreground opacity-0 outline-none transition-opacity hover:bg-background hover:text-foreground focus-visible:opacity-100 focus-visible:ring-[3px] focus-visible:ring-ring/20 group-hover/row:opacity-100 data-[state=open]:opacity-100 pointer-coarse:size-9 pointer-coarse:opacity-100 [&_svg]:size-4", active && "opacity-100"),
|
|
227
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": true })
|
|
228
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
229
|
+
align: "end",
|
|
230
|
+
className: "w-40",
|
|
231
|
+
children: [
|
|
232
|
+
onStartRename && /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
233
|
+
onSelect: onStartRename,
|
|
234
|
+
children: [/* @__PURE__ */ jsx(Edit, { "aria-hidden": true }), " Rename"]
|
|
235
|
+
}),
|
|
236
|
+
onPinChange && /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
237
|
+
onSelect: () => onPinChange(!item.pinned),
|
|
238
|
+
children: [item.pinned ? /* @__PURE__ */ jsx(PinOff, { "aria-hidden": true }) : /* @__PURE__ */ jsx(Pin, { "aria-hidden": true }), item.pinned ? "Unpin" : "Pin"]
|
|
239
|
+
}),
|
|
240
|
+
onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [(onStartRename || onPinChange) && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}), /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
241
|
+
variant: "destructive",
|
|
242
|
+
onSelect: onDelete,
|
|
243
|
+
children: [/* @__PURE__ */ jsx(Trash, { "aria-hidden": true }), " Delete"]
|
|
244
|
+
})] })
|
|
245
|
+
]
|
|
246
|
+
})] })]
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** Rename in place: Enter or leaving the field saves, Escape keeps the old title. */
|
|
250
|
+
function RenameRow({ title, onDone }) {
|
|
251
|
+
const [draft, setDraft] = React.useState(title);
|
|
252
|
+
const input = React.useRef(null);
|
|
253
|
+
React.useEffect(() => {
|
|
254
|
+
input.current?.select();
|
|
255
|
+
}, []);
|
|
256
|
+
return /* @__PURE__ */ jsx("li", {
|
|
257
|
+
className: "px-1 py-0.5",
|
|
258
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
259
|
+
ref: input,
|
|
260
|
+
"aria-label": "Chat title",
|
|
261
|
+
value: draft,
|
|
262
|
+
autoFocus: true,
|
|
263
|
+
onChange: (e) => setDraft(e.target.value),
|
|
264
|
+
onBlur: () => onDone(draft),
|
|
265
|
+
onKeyDown: (e) => {
|
|
266
|
+
if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return;
|
|
267
|
+
if (e.key === "Enter") {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
onDone(draft);
|
|
270
|
+
} else if (e.key === "Escape") {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
onDone(title);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
className: "h-8 w-full rounded-md border border-brand bg-surface px-2 text-base ring-[3px] ring-ring/15 outline-none sm:text-sm"
|
|
276
|
+
})
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
export { ChatHistory, groupConversations, matchesConversation };
|
|
281
|
+
|
|
282
|
+
//# sourceMappingURL=chat-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-history.js","names":["_Fragment"],"sources":["../../src/components/chat-history.tsx"],"sourcesContent":["import AlertCircle from '@burtson-labs/icons/react/alert-circle';\nimport Edit from '@burtson-labs/icons/react/edit';\nimport MessageSquarePlus from '@burtson-labs/icons/react/message-square-plus';\nimport MoreHorizontal from '@burtson-labs/icons/react/more-horizontal';\nimport Pin from '@burtson-labs/icons/react/pin';\nimport PinOff from '@burtson-labs/icons/react/pin-off';\nimport Search from '@burtson-labs/icons/react/search';\nimport Trash from '@burtson-labs/icons/react/trash';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from './dropdown-menu';\n\nexport interface ChatHistoryItem {\n id: string;\n title: string;\n /** When the conversation last changed; decides its group. */\n updatedAt: Date | string | number;\n pinned?: boolean;\n /** A line of the latest message, searched along with the title. */\n preview?: string;\n}\n\nexport type ChatHistoryGroupKey = 'pinned' | 'today' | 'yesterday' | 'week' | 'older';\n\nexport interface ChatHistoryGroup {\n key: ChatHistoryGroupKey;\n label: string;\n items: ChatHistoryItem[];\n}\n\nconst LABELS: Record<ChatHistoryGroupKey, string> = {\n pinned: 'Pinned',\n today: 'Today',\n yesterday: 'Yesterday',\n week: 'Previous 7 days',\n older: 'Older',\n};\n\nconst time = (d: ChatHistoryItem['updatedAt']) => new Date(d).getTime();\nconst startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\n\n/**\n * Pinned first, then Today, Yesterday, Previous 7 days and Older by the\n * local calendar, newest first in each. Empty groups are left out.\n */\nexport function groupConversations(\n items: ChatHistoryItem[],\n now: Date = new Date(),\n): ChatHistoryGroup[] {\n const today = startOfDay(now);\n const day = 24 * 60 * 60 * 1000;\n const buckets: Record<ChatHistoryGroupKey, ChatHistoryItem[]> = {\n pinned: [],\n today: [],\n yesterday: [],\n week: [],\n older: [],\n };\n for (const item of [...items].sort((a, b) => time(b.updatedAt) - time(a.updatedAt))) {\n if (item.pinned) {\n buckets.pinned.push(item);\n continue;\n }\n const t = startOfDay(new Date(item.updatedAt));\n const key: ChatHistoryGroupKey =\n t >= today\n ? 'today'\n : t >= today - day\n ? 'yesterday'\n : t >= today - 7 * day\n ? 'week'\n : 'older';\n buckets[key].push(item);\n }\n return (Object.keys(buckets) as ChatHistoryGroupKey[])\n .filter((k) => buckets[k].length > 0)\n .map((k) => ({ key: k, label: LABELS[k], items: buckets[k] }));\n}\n\n/** Case-insensitive match on every word, across title and preview. */\nexport function matchesConversation(item: ChatHistoryItem, query: string): boolean {\n const hay = `${item.title} ${item.preview ?? ''}`.toLowerCase();\n return query\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .every((w) => hay.includes(w));\n}\n\nexport interface ChatHistoryProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {\n items: ChatHistoryItem[];\n activeId?: string;\n onSelect: (id: string) => void;\n /** Shows a \"New chat\" button at the top. */\n onNewChat?: () => void;\n /** Adds Rename to the row menu; the title is edited in place. */\n onRename?: (id: string, title: string) => void;\n /** Adds Delete to the row menu. Confirm or offer undo in your app. */\n onDelete?: (id: string) => void;\n /** Adds Pin / Unpin to the row menu. */\n onPinChange?: (id: string, pinned: boolean) => void;\n /** Controlled search text; leave out to let the list keep its own. */\n query?: string;\n onQueryChange?: (query: string) => void;\n /** Hides the search box, e.g. when the app searches on the server. */\n searchable?: boolean;\n loading?: boolean;\n /** Why the list could not load. Shown with Retry when `onRetry` is set. */\n error?: React.ReactNode;\n onRetry?: () => void;\n /** Shown when there are no conversations at all. */\n empty?: React.ReactNode;\n /** The clock grouping uses; pass it in tests. */\n now?: Date;\n /**\n * Virtualisation hook for long histories: receives a group's items and the\n * row renderer, returns what to render (e.g. a windowed list). Default\n * renders every row.\n */\n renderItems?: (\n items: ChatHistoryItem[],\n renderItem: (item: ChatHistoryItem) => React.ReactNode,\n ) => React.ReactNode;\n label?: string;\n}\n\n/**\n * The conversation list beside a chat: a New chat action, search, and the\n * history grouped by day with pinned chats on top. Up and Down move between\n * rows, Home and End jump, Enter opens; each row has a menu for rename, pin\n * and delete. The app owns the data; this only reports what the person did.\n */\nfunction ChatHistory({\n items,\n activeId,\n onSelect,\n onNewChat,\n onRename,\n onDelete,\n onPinChange,\n query: controlledQuery,\n onQueryChange,\n searchable = true,\n loading = false,\n error,\n onRetry,\n empty,\n now,\n renderItems,\n label = 'Chat history',\n className,\n ...props\n}: ChatHistoryProps) {\n const [ownQuery, setOwnQuery] = React.useState('');\n const query = controlledQuery ?? ownQuery;\n const setQuery = (q: string) => {\n if (controlledQuery === undefined) setOwnQuery(q);\n onQueryChange?.(q);\n };\n const [renaming, setRenaming] = React.useState<string | null>(null);\n const list = React.useRef<HTMLDivElement>(null);\n const searchId = React.useId();\n\n const visible = query.trim() ? items.filter((i) => matchesConversation(i, query)) : items;\n const groups = groupConversations(visible, now);\n\n const onRowKeyDown = (e: React.KeyboardEvent) => {\n const rows = Array.from(\n list.current?.querySelectorAll<HTMLElement>('[data-slot=\"chat-history-row\"]') ?? [],\n );\n const i = rows.indexOf(document.activeElement as HTMLElement);\n let next = -1;\n if (e.key === 'ArrowDown') next = Math.min(rows.length - 1, i + 1);\n else if (e.key === 'ArrowUp') next = Math.max(0, i - 1);\n else if (e.key === 'Home') next = 0;\n else if (e.key === 'End') next = rows.length - 1;\n const row = next >= 0 ? rows[next] : undefined;\n if (row) {\n e.preventDefault();\n row.focus();\n }\n };\n\n const renderItem = (item: ChatHistoryItem) => (\n <HistoryRow\n key={item.id}\n item={item}\n active={item.id === activeId}\n renaming={renaming === item.id}\n onSelect={() => onSelect(item.id)}\n onStartRename={onRename ? () => setRenaming(item.id) : undefined}\n onRename={(title) => {\n setRenaming(null);\n if (onRename && title.trim() && title.trim() !== item.title)\n onRename(item.id, title.trim());\n }}\n onDelete={onDelete ? () => onDelete(item.id) : undefined}\n onPinChange={onPinChange ? (p) => onPinChange(item.id, p) : undefined}\n onKeyDown={onRowKeyDown}\n />\n );\n\n return (\n <div\n data-slot=\"chat-history\"\n aria-label={label}\n role=\"navigation\"\n className={cn('flex h-full min-h-0 flex-col', className)}\n {...props}\n >\n {(onNewChat || searchable) && (\n <div className=\"grid gap-2 p-2\">\n {onNewChat && (\n <Button variant=\"outline\" className=\"justify-start\" onClick={onNewChat}>\n <MessageSquarePlus /> New chat\n </Button>\n )}\n {searchable && (\n <label htmlFor={searchId} className=\"relative block\">\n <span className=\"sr-only\">Search chats</span>\n <Search\n aria-hidden\n className=\"pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground\"\n />\n <input\n id={searchId}\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search chats\"\n className=\"h-8 w-full rounded-md border border-input bg-surface pr-2 pl-8 text-base outline-none placeholder:text-muted-foreground focus-visible:border-brand focus-visible:ring-[3px] focus-visible:ring-ring/15 sm:text-sm\"\n />\n </label>\n )}\n </div>\n )}\n <div ref={list} className=\"min-h-0 flex-1 overflow-y-auto px-2 pb-2\">\n {loading ? (\n <div role=\"status\" aria-label=\"Loading chats\" className=\"grid gap-1.5 pt-1\">\n {[72, 56, 64, 48, 60].map((w, i) => (\n <div key={i} className=\"h-8 rounded-md bg-muted/70 motion-safe:animate-pulse\">\n <span className=\"sr-only\">Loading</span>\n <span\n aria-hidden\n className=\"ml-2 inline-block h-2.5 translate-y-3 rounded bg-muted-foreground/15\"\n style={{ width: `${w}%` }}\n />\n </div>\n ))}\n </div>\n ) : error ? (\n <div role=\"alert\" className=\"grid justify-items-start gap-2 p-2 text-sm\">\n <span className=\"inline-flex items-center gap-1.5 font-medium text-destructive [&_svg]:size-4\">\n <AlertCircle aria-hidden /> Chats could not load\n </span>\n <span className=\"text-muted-foreground\">{error}</span>\n {onRetry && (\n <Button size=\"sm\" variant=\"outline\" onClick={onRetry}>\n Try again\n </Button>\n )}\n </div>\n ) : items.length === 0 ? (\n <div className=\"p-3 text-sm text-muted-foreground\">\n {empty ?? 'No chats yet. Start one and it will appear here.'}\n </div>\n ) : groups.length === 0 ? (\n <p role=\"status\" className=\"p-3 text-sm text-muted-foreground\">\n No chats match “{query}”.\n </p>\n ) : (\n groups.map((g) => (\n <section key={g.key} aria-label={g.label} className=\"mt-2 first:mt-0\">\n <h3 className=\"px-2 pt-2 pb-1 text-[11px] font-semibold tracking-wide text-muted-foreground uppercase\">\n {g.label}\n </h3>\n <ul className=\"grid grid-cols-[minmax(0,1fr)] gap-0.5\">\n {renderItems ? renderItems(g.items, renderItem) : g.items.map(renderItem)}\n </ul>\n </section>\n ))\n )}\n </div>\n </div>\n );\n}\n\nfunction HistoryRow({\n item,\n active,\n renaming,\n onSelect,\n onStartRename,\n onRename,\n onDelete,\n onPinChange,\n onKeyDown,\n}: {\n item: ChatHistoryItem;\n active: boolean;\n renaming: boolean;\n onSelect: () => void;\n onStartRename?: () => void;\n onRename: (title: string) => void;\n onDelete?: () => void;\n onPinChange?: (pinned: boolean) => void;\n onKeyDown: (e: React.KeyboardEvent) => void;\n}) {\n const hasMenu = Boolean(onStartRename || onDelete || onPinChange);\n if (renaming) return <RenameRow title={item.title} onDone={onRename} />;\n return (\n <li className=\"group/row relative min-w-0\">\n <button\n type=\"button\"\n data-slot=\"chat-history-row\"\n aria-current={active ? 'page' : undefined}\n onClick={onSelect}\n onKeyDown={onKeyDown}\n className={cn(\n 'flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-sm outline-none transition-colors hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/20 pointer-coarse:h-11',\n hasMenu && 'pr-9',\n active && 'bg-accent font-medium text-accent-foreground hover:bg-accent',\n )}\n >\n <span className=\"min-w-0 flex-1 truncate\">{item.title || 'Untitled chat'}</span>\n </button>\n {hasMenu && (\n <DropdownMenu>\n <DropdownMenuTrigger\n aria-label={`Actions for ${item.title || 'chat'}`}\n className={cn(\n 'absolute top-1/2 right-1 grid size-7 -translate-y-1/2 place-items-center rounded-sm text-muted-foreground opacity-0 outline-none transition-opacity hover:bg-background hover:text-foreground focus-visible:opacity-100 focus-visible:ring-[3px] focus-visible:ring-ring/20 group-hover/row:opacity-100 data-[state=open]:opacity-100 pointer-coarse:size-9 pointer-coarse:opacity-100 [&_svg]:size-4',\n active && 'opacity-100',\n )}\n >\n <MoreHorizontal aria-hidden />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-40\">\n {onStartRename && (\n <DropdownMenuItem onSelect={onStartRename}>\n <Edit aria-hidden /> Rename\n </DropdownMenuItem>\n )}\n {onPinChange && (\n <DropdownMenuItem onSelect={() => onPinChange(!item.pinned)}>\n {item.pinned ? <PinOff aria-hidden /> : <Pin aria-hidden />}\n {item.pinned ? 'Unpin' : 'Pin'}\n </DropdownMenuItem>\n )}\n {onDelete && (\n <>\n {(onStartRename || onPinChange) && <DropdownMenuSeparator />}\n <DropdownMenuItem variant=\"destructive\" onSelect={onDelete}>\n <Trash aria-hidden /> Delete\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </li>\n );\n}\n\n/** Rename in place: Enter or leaving the field saves, Escape keeps the old title. */\nfunction RenameRow({ title, onDone }: { title: string; onDone: (title: string) => void }) {\n const [draft, setDraft] = React.useState(title);\n const input = React.useRef<HTMLInputElement>(null);\n React.useEffect(() => {\n input.current?.select();\n }, []);\n return (\n <li className=\"px-1 py-0.5\">\n <input\n ref={input}\n aria-label=\"Chat title\"\n value={draft}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- rename was just asked for\n autoFocus\n onChange={(e) => setDraft(e.target.value)}\n onBlur={() => onDone(draft)}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return;\n if (e.key === 'Enter') {\n e.preventDefault();\n onDone(draft);\n } else if (e.key === 'Escape') {\n e.preventDefault();\n onDone(title);\n }\n }}\n className=\"h-8 w-full rounded-md border border-brand bg-surface px-2 text-base ring-[3px] ring-ring/15 outline-none sm:text-sm\"\n />\n </li>\n );\n}\n\nexport { ChatHistory };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Full screen for one element: the Fullscreen API where the browser allows
|
|
4
|
+
* it, otherwise the element is pinned over the page with CSS. Escape leaves
|
|
5
|
+
* either way. Nothing remounts, so focus and scroll stay where they were.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useFullscreen(ref: React.RefObject<HTMLElement | null>, onChange?: (fullscreen: boolean) => void): {
|
|
8
|
+
fullscreen: boolean;
|
|
9
|
+
native: boolean;
|
|
10
|
+
enter: () => Promise<void>;
|
|
11
|
+
exit: () => Promise<void>;
|
|
12
|
+
toggle: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export interface ChatLayoutProps extends Omit<React.ComponentProps<'div'>, 'title'> {
|
|
15
|
+
/** The history column, usually a ChatHistory. A sheet from the left on phones. */
|
|
16
|
+
sidebar?: React.ReactNode;
|
|
17
|
+
sidebarLabel?: string;
|
|
18
|
+
/** An optional right column: sources, files, run details. Hidden on phones. */
|
|
19
|
+
details?: React.ReactNode;
|
|
20
|
+
detailsLabel?: string;
|
|
21
|
+
/** Shown in the header: the conversation title. */
|
|
22
|
+
title?: React.ReactNode;
|
|
23
|
+
/** Header controls right of the title (model picker, share). */
|
|
24
|
+
actions?: React.ReactNode;
|
|
25
|
+
/** Controlled sidebar state; leave out to let the layout keep its own. */
|
|
26
|
+
sidebarOpen?: boolean;
|
|
27
|
+
defaultSidebarOpen?: boolean;
|
|
28
|
+
onSidebarOpenChange?: (open: boolean) => void;
|
|
29
|
+
/** Show the Full screen button. Default true. */
|
|
30
|
+
allowFullscreen?: boolean;
|
|
31
|
+
onFullscreenChange?: (fullscreen: boolean) => void;
|
|
32
|
+
/** Sidebar width, e.g. "22%" or 280 (px), as ResizablePanel sizes. */
|
|
33
|
+
sidebarSize?: number | string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The frame for a chat app: header, a resizable history sidebar (a sheet on
|
|
37
|
+
* phones that closes when a chat is picked), the conversation, an optional
|
|
38
|
+
* details column, and a Full screen mode. Put a Conversation and a Composer
|
|
39
|
+
* in `children`; the layout stacks them and keeps the composer at the bottom.
|
|
40
|
+
*/
|
|
41
|
+
declare function ChatLayout({ sidebar, sidebarLabel, details, detailsLabel, title, actions, sidebarOpen: controlledOpen, defaultSidebarOpen, onSidebarOpenChange, allowFullscreen, onFullscreenChange, sidebarSize, children, className, ...props }: ChatLayoutProps): React.JSX.Element;
|
|
42
|
+
export { ChatLayout };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { IconButton } from "./icon-button.js";
|
|
3
|
+
import { ResizablePanel, ResizablePanelGroup, ResizeHandle } from "./resizable.js";
|
|
4
|
+
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "./sheet.js";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import Maximize from "@burtson-labs/icons/react/maximize";
|
|
8
|
+
import Minimize from "@burtson-labs/icons/react/minimize";
|
|
9
|
+
import PanelLeft from "@burtson-labs/icons/react/panel-left";
|
|
10
|
+
import PanelLeftClose from "@burtson-labs/icons/react/panel-left-close";
|
|
11
|
+
//#region src/components/chat-layout.tsx
|
|
12
|
+
/**
|
|
13
|
+
* Full screen for one element: the Fullscreen API where the browser allows
|
|
14
|
+
* it, otherwise the element is pinned over the page with CSS. Escape leaves
|
|
15
|
+
* either way. Nothing remounts, so focus and scroll stay where they were.
|
|
16
|
+
*/
|
|
17
|
+
function useFullscreen(ref, onChange) {
|
|
18
|
+
const [native, setNative] = React.useState(false);
|
|
19
|
+
const [fallback, setFallback] = React.useState(false);
|
|
20
|
+
const active = native || fallback;
|
|
21
|
+
const changed = React.useRef(onChange);
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
changed.current = onChange;
|
|
24
|
+
}, [onChange]);
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
const sync = () => setNative(Boolean(ref.current && document.fullscreenElement === ref.current));
|
|
27
|
+
document.addEventListener("fullscreenchange", sync);
|
|
28
|
+
return () => document.removeEventListener("fullscreenchange", sync);
|
|
29
|
+
}, [ref]);
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (!fallback) return;
|
|
32
|
+
const onKey = (e) => {
|
|
33
|
+
if (e.key === "Escape" && !e.defaultPrevented) setFallback(false);
|
|
34
|
+
};
|
|
35
|
+
const root = document.documentElement;
|
|
36
|
+
const previous = root.style.overflow;
|
|
37
|
+
root.style.overflow = "hidden";
|
|
38
|
+
document.addEventListener("keydown", onKey);
|
|
39
|
+
return () => {
|
|
40
|
+
root.style.overflow = previous;
|
|
41
|
+
document.removeEventListener("keydown", onKey);
|
|
42
|
+
};
|
|
43
|
+
}, [fallback]);
|
|
44
|
+
const first = React.useRef(true);
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (first.current) {
|
|
47
|
+
first.current = false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
changed.current?.(active);
|
|
51
|
+
}, [active]);
|
|
52
|
+
const enter = React.useCallback(async () => {
|
|
53
|
+
const el = ref.current;
|
|
54
|
+
if (!el) return;
|
|
55
|
+
if (!(document.fullscreenEnabled && typeof el.requestFullscreen === "function" ? await el.requestFullscreen().then(() => true, () => false) : false)) setFallback(true);
|
|
56
|
+
}, [ref]);
|
|
57
|
+
const exit = React.useCallback(async () => {
|
|
58
|
+
setFallback(false);
|
|
59
|
+
if (document.fullscreenElement && typeof document.exitFullscreen === "function") await document.exitFullscreen().catch(() => void 0);
|
|
60
|
+
}, []);
|
|
61
|
+
return {
|
|
62
|
+
fullscreen: active,
|
|
63
|
+
native,
|
|
64
|
+
enter,
|
|
65
|
+
exit,
|
|
66
|
+
toggle: React.useCallback(() => active ? exit() : enter(), [
|
|
67
|
+
active,
|
|
68
|
+
enter,
|
|
69
|
+
exit
|
|
70
|
+
])
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function useDesktop(query = "(min-width: 768px)") {
|
|
74
|
+
const get = () => typeof matchMedia === "function" ? matchMedia(query).matches : true;
|
|
75
|
+
const [desktop, setDesktop] = React.useState(get);
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (typeof matchMedia !== "function") return;
|
|
78
|
+
const mq = matchMedia(query);
|
|
79
|
+
const on = () => setDesktop(mq.matches);
|
|
80
|
+
mq.addEventListener?.("change", on);
|
|
81
|
+
return () => mq.removeEventListener?.("change", on);
|
|
82
|
+
}, [query]);
|
|
83
|
+
return desktop;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The frame for a chat app: header, a resizable history sidebar (a sheet on
|
|
87
|
+
* phones that closes when a chat is picked), the conversation, an optional
|
|
88
|
+
* details column, and a Full screen mode. Put a Conversation and a Composer
|
|
89
|
+
* in `children`; the layout stacks them and keeps the composer at the bottom.
|
|
90
|
+
*/
|
|
91
|
+
function ChatLayout({ sidebar, sidebarLabel = "Chats", details, detailsLabel = "Details", title, actions, sidebarOpen: controlledOpen, defaultSidebarOpen = true, onSidebarOpenChange, allowFullscreen = true, onFullscreenChange, sidebarSize = "24%", children, className, ...props }) {
|
|
92
|
+
const root = React.useRef(null);
|
|
93
|
+
const desktop = useDesktop();
|
|
94
|
+
const [ownOpen, setOwnOpen] = React.useState(defaultSidebarOpen);
|
|
95
|
+
const [sheetOpen, setSheetOpen] = React.useState(false);
|
|
96
|
+
const open = controlledOpen ?? ownOpen;
|
|
97
|
+
const setOpen = (v) => {
|
|
98
|
+
if (controlledOpen === void 0) setOwnOpen(v);
|
|
99
|
+
onSidebarOpenChange?.(v);
|
|
100
|
+
};
|
|
101
|
+
const { fullscreen, toggle } = useFullscreen(root, onFullscreenChange);
|
|
102
|
+
const sidebarToggle = sidebar ? desktop ? /* @__PURE__ */ jsx(IconButton, {
|
|
103
|
+
variant: "ghost",
|
|
104
|
+
size: "icon-sm",
|
|
105
|
+
label: open ? `Hide ${sidebarLabel.toLowerCase()}` : `Show ${sidebarLabel.toLowerCase()}`,
|
|
106
|
+
"aria-expanded": open,
|
|
107
|
+
onClick: () => setOpen(!open),
|
|
108
|
+
children: open ? /* @__PURE__ */ jsx(PanelLeftClose, {}) : /* @__PURE__ */ jsx(PanelLeft, {})
|
|
109
|
+
}) : /* @__PURE__ */ jsx(IconButton, {
|
|
110
|
+
variant: "ghost",
|
|
111
|
+
size: "icon-sm",
|
|
112
|
+
className: "pointer-coarse:size-11",
|
|
113
|
+
label: `Open ${sidebarLabel.toLowerCase()}`,
|
|
114
|
+
"aria-expanded": sheetOpen,
|
|
115
|
+
onClick: () => setSheetOpen(true),
|
|
116
|
+
children: /* @__PURE__ */ jsx(PanelLeft, {})
|
|
117
|
+
}) : null;
|
|
118
|
+
const main = /* @__PURE__ */ jsx("div", {
|
|
119
|
+
"data-slot": "chat-layout-main",
|
|
120
|
+
className: "flex h-full min-h-0 min-w-0 flex-col",
|
|
121
|
+
children
|
|
122
|
+
});
|
|
123
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
124
|
+
ref: root,
|
|
125
|
+
"data-slot": "chat-layout",
|
|
126
|
+
"data-fullscreen": fullscreen || void 0,
|
|
127
|
+
className: cn("flex h-full min-h-0 flex-col overflow-hidden bg-background text-foreground", "data-[fullscreen]:fixed data-[fullscreen]:inset-0 data-[fullscreen]:z-50 data-[fullscreen]:h-dvh data-[fullscreen]:w-screen", className),
|
|
128
|
+
...props,
|
|
129
|
+
children: [
|
|
130
|
+
/* @__PURE__ */ jsxs("header", {
|
|
131
|
+
className: "flex h-12 shrink-0 items-center gap-1 border-b px-2",
|
|
132
|
+
children: [
|
|
133
|
+
sidebarToggle,
|
|
134
|
+
/* @__PURE__ */ jsx("div", {
|
|
135
|
+
className: "min-w-0 flex-1 truncate px-1 text-sm font-medium",
|
|
136
|
+
children: title
|
|
137
|
+
}),
|
|
138
|
+
actions,
|
|
139
|
+
allowFullscreen && /* @__PURE__ */ jsx(IconButton, {
|
|
140
|
+
variant: "ghost",
|
|
141
|
+
size: "icon-sm",
|
|
142
|
+
className: "pointer-coarse:size-11",
|
|
143
|
+
label: fullscreen ? "Exit full screen" : "Full screen",
|
|
144
|
+
"aria-pressed": fullscreen,
|
|
145
|
+
onClick: () => void toggle(),
|
|
146
|
+
children: fullscreen ? /* @__PURE__ */ jsx(Minimize, {}) : /* @__PURE__ */ jsx(Maximize, {})
|
|
147
|
+
})
|
|
148
|
+
]
|
|
149
|
+
}),
|
|
150
|
+
/* @__PURE__ */ jsx("div", {
|
|
151
|
+
className: "min-h-0 flex-1",
|
|
152
|
+
children: desktop && (sidebar || details) ? /* @__PURE__ */ jsxs(ResizablePanelGroup, {
|
|
153
|
+
orientation: "horizontal",
|
|
154
|
+
children: [
|
|
155
|
+
sidebar && open && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ResizablePanel, {
|
|
156
|
+
id: "chat-sidebar",
|
|
157
|
+
defaultSize: sidebarSize,
|
|
158
|
+
minSize: 180,
|
|
159
|
+
maxSize: "45%",
|
|
160
|
+
className: "bg-surface-muted/50",
|
|
161
|
+
children: /* @__PURE__ */ jsx("aside", {
|
|
162
|
+
"aria-label": sidebarLabel,
|
|
163
|
+
className: "h-full",
|
|
164
|
+
children: sidebar
|
|
165
|
+
})
|
|
166
|
+
}), /* @__PURE__ */ jsx(ResizeHandle, { "aria-label": `Resize ${sidebarLabel.toLowerCase()}` })] }),
|
|
167
|
+
/* @__PURE__ */ jsx(ResizablePanel, {
|
|
168
|
+
id: "chat-main",
|
|
169
|
+
minSize: "30%",
|
|
170
|
+
children: main
|
|
171
|
+
}),
|
|
172
|
+
details && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ResizeHandle, { "aria-label": `Resize ${detailsLabel.toLowerCase()}` }), /* @__PURE__ */ jsx(ResizablePanel, {
|
|
173
|
+
id: "chat-details",
|
|
174
|
+
defaultSize: "26%",
|
|
175
|
+
minSize: 200,
|
|
176
|
+
maxSize: "45%",
|
|
177
|
+
children: /* @__PURE__ */ jsx("aside", {
|
|
178
|
+
"aria-label": detailsLabel,
|
|
179
|
+
className: "h-full overflow-y-auto",
|
|
180
|
+
children: details
|
|
181
|
+
})
|
|
182
|
+
})] })
|
|
183
|
+
]
|
|
184
|
+
}) : main
|
|
185
|
+
}),
|
|
186
|
+
sidebar && !desktop && /* @__PURE__ */ jsx(Sheet, {
|
|
187
|
+
open: sheetOpen,
|
|
188
|
+
onOpenChange: setSheetOpen,
|
|
189
|
+
children: /* @__PURE__ */ jsxs(SheetContent, {
|
|
190
|
+
side: "left",
|
|
191
|
+
className: "w-[min(20rem,88vw)] gap-0 p-0",
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ jsx(SheetTitle, {
|
|
194
|
+
className: "border-b px-4 py-3.5 text-sm",
|
|
195
|
+
children: sidebarLabel
|
|
196
|
+
}),
|
|
197
|
+
/* @__PURE__ */ jsx(SheetDescription, {
|
|
198
|
+
className: "sr-only",
|
|
199
|
+
children: "Pick a conversation."
|
|
200
|
+
}),
|
|
201
|
+
/* @__PURE__ */ jsx("div", {
|
|
202
|
+
className: "min-h-0 flex-1",
|
|
203
|
+
onClickCapture: (e) => {
|
|
204
|
+
if (e.target.closest("[data-slot=\"chat-history-row\"], [data-close-sheet]")) setSheetOpen(false);
|
|
205
|
+
},
|
|
206
|
+
children: sidebar
|
|
207
|
+
})
|
|
208
|
+
]
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
export { ChatLayout, useFullscreen };
|
|
216
|
+
|
|
217
|
+
//# sourceMappingURL=chat-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-layout.js","names":["_Fragment"],"sources":["../../src/components/chat-layout.tsx"],"sourcesContent":["import Maximize from '@burtson-labs/icons/react/maximize';\nimport Minimize from '@burtson-labs/icons/react/minimize';\nimport PanelLeft from '@burtson-labs/icons/react/panel-left';\nimport PanelLeftClose from '@burtson-labs/icons/react/panel-left-close';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { IconButton } from './icon-button';\nimport { ResizablePanel, ResizablePanelGroup, ResizeHandle } from './resizable';\nimport { Sheet, SheetContent, SheetDescription, SheetTitle } from './sheet';\n\n/**\n * Full screen for one element: the Fullscreen API where the browser allows\n * it, otherwise the element is pinned over the page with CSS. Escape leaves\n * either way. Nothing remounts, so focus and scroll stay where they were.\n */\nexport function useFullscreen(\n ref: React.RefObject<HTMLElement | null>,\n onChange?: (fullscreen: boolean) => void,\n) {\n const [native, setNative] = React.useState(false);\n const [fallback, setFallback] = React.useState(false);\n const active = native || fallback;\n const changed = React.useRef(onChange);\n React.useEffect(() => {\n changed.current = onChange;\n }, [onChange]);\n\n React.useEffect(() => {\n const sync = () =>\n setNative(Boolean(ref.current && document.fullscreenElement === ref.current));\n document.addEventListener('fullscreenchange', sync);\n return () => document.removeEventListener('fullscreenchange', sync);\n }, [ref]);\n\n React.useEffect(() => {\n if (!fallback) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && !e.defaultPrevented) setFallback(false);\n };\n const root = document.documentElement;\n const previous = root.style.overflow;\n root.style.overflow = 'hidden';\n document.addEventListener('keydown', onKey);\n return () => {\n root.style.overflow = previous;\n document.removeEventListener('keydown', onKey);\n };\n }, [fallback]);\n\n const first = React.useRef(true);\n React.useEffect(() => {\n if (first.current) {\n first.current = false;\n return;\n }\n changed.current?.(active);\n }, [active]);\n\n const enter = React.useCallback(async () => {\n const el = ref.current;\n if (!el) return;\n const canNative = document.fullscreenEnabled && typeof el.requestFullscreen === 'function';\n // Denied (iframe without allowfullscreen, iOS) resolves false: use the CSS fallback.\n const ok = canNative\n ? await el.requestFullscreen().then(\n () => true,\n () => false,\n )\n : false;\n if (!ok) setFallback(true);\n }, [ref]);\n\n const exit = React.useCallback(async () => {\n setFallback(false);\n if (document.fullscreenElement && typeof document.exitFullscreen === 'function') {\n await document.exitFullscreen().catch(() => undefined);\n }\n }, []);\n\n const toggle = React.useCallback(() => (active ? exit() : enter()), [active, enter, exit]);\n return { fullscreen: active, native, enter, exit, toggle };\n}\n\nfunction useDesktop(query = '(min-width: 768px)') {\n const get = () => (typeof matchMedia === 'function' ? matchMedia(query).matches : true);\n const [desktop, setDesktop] = React.useState(get);\n React.useEffect(() => {\n if (typeof matchMedia !== 'function') return;\n const mq = matchMedia(query);\n const on = () => setDesktop(mq.matches);\n mq.addEventListener?.('change', on);\n return () => mq.removeEventListener?.('change', on);\n }, [query]);\n return desktop;\n}\n\nexport interface ChatLayoutProps extends Omit<React.ComponentProps<'div'>, 'title'> {\n /** The history column, usually a ChatHistory. A sheet from the left on phones. */\n sidebar?: React.ReactNode;\n sidebarLabel?: string;\n /** An optional right column: sources, files, run details. Hidden on phones. */\n details?: React.ReactNode;\n detailsLabel?: string;\n /** Shown in the header: the conversation title. */\n title?: React.ReactNode;\n /** Header controls right of the title (model picker, share). */\n actions?: React.ReactNode;\n /** Controlled sidebar state; leave out to let the layout keep its own. */\n sidebarOpen?: boolean;\n defaultSidebarOpen?: boolean;\n onSidebarOpenChange?: (open: boolean) => void;\n /** Show the Full screen button. Default true. */\n allowFullscreen?: boolean;\n onFullscreenChange?: (fullscreen: boolean) => void;\n /** Sidebar width, e.g. \"22%\" or 280 (px), as ResizablePanel sizes. */\n sidebarSize?: number | string;\n}\n\n/**\n * The frame for a chat app: header, a resizable history sidebar (a sheet on\n * phones that closes when a chat is picked), the conversation, an optional\n * details column, and a Full screen mode. Put a Conversation and a Composer\n * in `children`; the layout stacks them and keeps the composer at the bottom.\n */\nfunction ChatLayout({\n sidebar,\n sidebarLabel = 'Chats',\n details,\n detailsLabel = 'Details',\n title,\n actions,\n sidebarOpen: controlledOpen,\n defaultSidebarOpen = true,\n onSidebarOpenChange,\n allowFullscreen = true,\n onFullscreenChange,\n sidebarSize = '24%',\n children,\n className,\n ...props\n}: ChatLayoutProps) {\n const root = React.useRef<HTMLDivElement>(null);\n const desktop = useDesktop();\n const [ownOpen, setOwnOpen] = React.useState(defaultSidebarOpen);\n const [sheetOpen, setSheetOpen] = React.useState(false);\n const open = controlledOpen ?? ownOpen;\n const setOpen = (v: boolean) => {\n if (controlledOpen === undefined) setOwnOpen(v);\n onSidebarOpenChange?.(v);\n };\n const { fullscreen, toggle } = useFullscreen(root, onFullscreenChange);\n\n const sidebarToggle = sidebar ? (\n desktop ? (\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={open ? `Hide ${sidebarLabel.toLowerCase()}` : `Show ${sidebarLabel.toLowerCase()}`}\n aria-expanded={open}\n onClick={() => setOpen(!open)}\n >\n {open ? <PanelLeftClose /> : <PanelLeft />}\n </IconButton>\n ) : (\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"pointer-coarse:size-11\"\n label={`Open ${sidebarLabel.toLowerCase()}`}\n aria-expanded={sheetOpen}\n onClick={() => setSheetOpen(true)}\n >\n <PanelLeft />\n </IconButton>\n )\n ) : null;\n\n const main = (\n <div data-slot=\"chat-layout-main\" className=\"flex h-full min-h-0 min-w-0 flex-col\">\n {children}\n </div>\n );\n\n return (\n <div\n ref={root}\n data-slot=\"chat-layout\"\n data-fullscreen={fullscreen || undefined}\n className={cn(\n 'flex h-full min-h-0 flex-col overflow-hidden bg-background text-foreground',\n 'data-[fullscreen]:fixed data-[fullscreen]:inset-0 data-[fullscreen]:z-50 data-[fullscreen]:h-dvh data-[fullscreen]:w-screen',\n className,\n )}\n {...props}\n >\n <header className=\"flex h-12 shrink-0 items-center gap-1 border-b px-2\">\n {sidebarToggle}\n <div className=\"min-w-0 flex-1 truncate px-1 text-sm font-medium\">{title}</div>\n {actions}\n {allowFullscreen && (\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n className=\"pointer-coarse:size-11\"\n label={fullscreen ? 'Exit full screen' : 'Full screen'}\n aria-pressed={fullscreen}\n onClick={() => void toggle()}\n >\n {fullscreen ? <Minimize /> : <Maximize />}\n </IconButton>\n )}\n </header>\n <div className=\"min-h-0 flex-1\">\n {desktop && (sidebar || details) ? (\n <ResizablePanelGroup orientation=\"horizontal\">\n {sidebar && open && (\n <>\n <ResizablePanel\n id=\"chat-sidebar\"\n defaultSize={sidebarSize}\n minSize={180}\n maxSize=\"45%\"\n className=\"bg-surface-muted/50\"\n >\n <aside aria-label={sidebarLabel} className=\"h-full\">\n {sidebar}\n </aside>\n </ResizablePanel>\n <ResizeHandle aria-label={`Resize ${sidebarLabel.toLowerCase()}`} />\n </>\n )}\n <ResizablePanel id=\"chat-main\" minSize=\"30%\">\n {main}\n </ResizablePanel>\n {details && (\n <>\n <ResizeHandle aria-label={`Resize ${detailsLabel.toLowerCase()}`} />\n <ResizablePanel id=\"chat-details\" defaultSize=\"26%\" minSize={200} maxSize=\"45%\">\n <aside aria-label={detailsLabel} className=\"h-full overflow-y-auto\">\n {details}\n </aside>\n </ResizablePanel>\n </>\n )}\n </ResizablePanelGroup>\n ) : (\n main\n )}\n </div>\n {sidebar && !desktop && (\n <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>\n <SheetContent side=\"left\" className=\"w-[min(20rem,88vw)] gap-0 p-0\">\n <SheetTitle className=\"border-b px-4 py-3.5 text-sm\">{sidebarLabel}</SheetTitle>\n <SheetDescription className=\"sr-only\">Pick a conversation.</SheetDescription>\n <div\n className=\"min-h-0 flex-1\"\n // Picking a chat (or New chat) closes the sheet, as on a phone it should.\n onClickCapture={(e) => {\n const t = e.target as HTMLElement;\n if (t.closest('[data-slot=\"chat-history-row\"], [data-close-sheet]'))\n setSheetOpen(false);\n }}\n >\n {sidebar}\n </div>\n </SheetContent>\n </Sheet>\n )}\n </div>\n );\n}\n\nexport { ChatLayout };\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import { Spinner } from "./spinner.js";
|
|
3
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
3
4
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./command.js";
|
|
4
5
|
import { fieldClasses } from "./input.js";
|
|
5
|
-
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import Check from "@burtson-labs/icons/react/check";
|