@tulipfarm/sprout 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/components/attachment.js +2 -2
  2. package/dist/components/brand-icon.js +4 -0
  3. package/dist/components/canvas.js +2 -2
  4. package/dist/components/chat-input.d.ts +94 -0
  5. package/dist/components/chat-input.js +279 -0
  6. package/dist/components/code-block.js +2 -33
  7. package/dist/components/file-explorer.d.ts +27 -0
  8. package/dist/components/file-explorer.js +300 -0
  9. package/dist/components/file-viewer.d.ts +18 -1
  10. package/dist/components/file-viewer.js +35 -8
  11. package/dist/components/questionnaire.js +2 -1
  12. package/dist/components/search-dialog.js +5 -4
  13. package/dist/components/state-panel.js +2 -2
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +2 -0
  16. package/dist/internal/brand-glyphs.d.ts +28 -0
  17. package/dist/internal/brand-glyphs.js +24 -0
  18. package/dist/internal/code-tokens.d.ts +4 -0
  19. package/dist/internal/code-tokens.js +34 -0
  20. package/dist/styles/accordion.css +2 -2
  21. package/dist/styles/alert-dialog.css +2 -2
  22. package/dist/styles/alert.css +2 -2
  23. package/dist/styles/attachment.css +4 -24
  24. package/dist/styles/badge.css +2 -2
  25. package/dist/styles/brand-icon.css +2 -2
  26. package/dist/styles/breadcrumb.css +2 -2
  27. package/dist/styles/button.css +2 -2
  28. package/dist/styles/calendar.css +3 -3
  29. package/dist/styles/canvas.css +4 -18
  30. package/dist/styles/carousel.css +2 -2
  31. package/dist/styles/chart.css +2 -2
  32. package/dist/styles/chat-confirmation.css +2 -2
  33. package/dist/styles/chat-input.css +240 -0
  34. package/dist/styles/chat-sources.css +2 -2
  35. package/dist/styles/checkbox.css +2 -3
  36. package/dist/styles/choice-list.css +2 -2
  37. package/dist/styles/code-block.css +2 -2
  38. package/dist/styles/collapsible.css +2 -2
  39. package/dist/styles/combobox.css +6 -6
  40. package/dist/styles/command.css +2 -2
  41. package/dist/styles/context-menu.css +3 -3
  42. package/dist/styles/dialog.css +2 -2
  43. package/dist/styles/dropdown-menu.css +1 -1
  44. package/dist/styles/field.css +2 -3
  45. package/dist/styles/file-explorer.css +517 -0
  46. package/dist/styles/file-viewer.css +113 -16
  47. package/dist/styles/hover-card.css +2 -2
  48. package/dist/styles/index.css +6 -0
  49. package/dist/styles/input-group.css +6 -11
  50. package/dist/styles/input-otp.css +2 -2
  51. package/dist/styles/input.css +3 -4
  52. package/dist/styles/mention-list.css +1 -1
  53. package/dist/styles/menubar.css +3 -3
  54. package/dist/styles/message-response.css +4 -4
  55. package/dist/styles/message-scroller.css +2 -2
  56. package/dist/styles/message.css +4 -4
  57. package/dist/styles/navigation-menu.css +2 -2
  58. package/dist/styles/page-header.css +4 -4
  59. package/dist/styles/plan-outline.css +6 -6
  60. package/dist/styles/prompt-list.css +2 -2
  61. package/dist/styles/questionnaire.css +2 -17
  62. package/dist/styles/radio-group.css +2 -3
  63. package/dist/styles/reasoning-effort.css +2 -2
  64. package/dist/styles/review-card.css +2 -2
  65. package/dist/styles/rich-text-editor.css +2 -2
  66. package/dist/styles/schema-diagram.css +4 -4
  67. package/dist/styles/scroll-area.css +2 -2
  68. package/dist/styles/search-dialog.css +0 -83
  69. package/dist/styles/select.css +3 -3
  70. package/dist/styles/separator.css +2 -2
  71. package/dist/styles/sheet.css +2 -2
  72. package/dist/styles/sidebar.css +2 -2
  73. package/dist/styles/slider.css +1 -1
  74. package/dist/styles/state-panel.css +1 -10
  75. package/dist/styles/switch.css +2 -2
  76. package/dist/styles/table.css +10 -10
  77. package/dist/styles/tabs.css +4 -4
  78. package/dist/styles/textarea.css +3 -4
  79. package/dist/styles/themes/tulipfarm.css +89 -99
  80. package/dist/styles/toast.css +2 -2
  81. package/dist/styles/toggle-group.css +2 -2
  82. package/dist/styles/toggle.css +2 -2
  83. package/dist/styles/tool-activity.css +5 -5
  84. package/package.json +10 -1
@@ -0,0 +1,300 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useDeferredValue, useEffect, useId, useMemo, useRef, useState, } from "react";
4
+ import { ChevronRight } from "reicon-react/icons/ChevronRight";
5
+ import { FileText } from "reicon-react/icons/FileText";
6
+ import { Folder } from "reicon-react/icons/Folder";
7
+ import { FolderOpen } from "reicon-react/icons/FolderOpen";
8
+ import { MinusSquare } from "reicon-react/icons/MinusSquare";
9
+ import { X } from "reicon-react/icons/X";
10
+ import { cn } from "../internal/classes.js";
11
+ import { tokenClass, useCodeTokens } from "../internal/code-tokens.js";
12
+ import { codeLanguageNames } from "../internal/languages.js";
13
+ import { languageForFilename } from "../internal/syntax.js";
14
+ import { SearchField } from "./search-field.js";
15
+ import { StatePanel } from "./state-panel.js";
16
+ /** Search stops after this many matching lines, so one common word cannot flood the list. */
17
+ export const fileExplorerMaxMatches = 500;
18
+ const nameOf = (path) => path.slice(path.lastIndexOf("/") + 1);
19
+ const folderOf = (path) => path.slice(0, Math.max(0, path.lastIndexOf("/")));
20
+ const order = (a, b) => a.kind === b.kind
21
+ ? a.name.localeCompare(b.name, undefined, { numeric: true })
22
+ : a.kind === "folder"
23
+ ? -1
24
+ : 1;
25
+ function ancestors(path) {
26
+ const parts = path.split("/").slice(0, -1);
27
+ return parts.map((_, index) => parts.slice(0, index + 1).join("/"));
28
+ }
29
+ function buildTree(files) {
30
+ const root = [];
31
+ const folders = new Map([["", root]]);
32
+ for (const file of files) {
33
+ for (const folder of ancestors(file.path)) {
34
+ if (folders.has(folder))
35
+ continue;
36
+ const children = [];
37
+ folders.get(folderOf(folder)).push({
38
+ kind: "folder",
39
+ path: folder,
40
+ name: nameOf(folder),
41
+ children,
42
+ });
43
+ folders.set(folder, children);
44
+ }
45
+ folders
46
+ .get(folderOf(file.path))
47
+ .push({ kind: "file", path: file.path, name: nameOf(file.path), file });
48
+ }
49
+ for (const children of folders.values())
50
+ children.sort(order);
51
+ return root;
52
+ }
53
+ function visibleRows(nodes, expanded, depth = 0) {
54
+ return nodes.flatMap((node, index) => [
55
+ { node, depth, size: nodes.length, position: index + 1 },
56
+ ...(node.kind === "folder" && expanded.has(node.path)
57
+ ? visibleRows(node.children, expanded, depth + 1)
58
+ : []),
59
+ ]);
60
+ }
61
+ function rangesIn(text, needle) {
62
+ const haystack = text.toLowerCase();
63
+ const ranges = [];
64
+ for (let at = haystack.indexOf(needle); at >= 0; at = haystack.indexOf(needle, at + needle.length))
65
+ ranges.push([at, at + needle.length]);
66
+ return ranges;
67
+ }
68
+ const excerptLead = 32;
69
+ function searchFiles(files, query) {
70
+ const needle = query.toLowerCase();
71
+ const names = files.filter((file) => file.path.toLowerCase().includes(needle));
72
+ const hits = [];
73
+ let total = 0;
74
+ for (const file of files) {
75
+ if (!("content" in file) || total >= fileExplorerMaxMatches)
76
+ continue;
77
+ const lines = [];
78
+ file.content.split("\n").forEach((raw, index) => {
79
+ if (total >= fileExplorerMaxMatches)
80
+ return;
81
+ const ranges = rangesIn(raw, needle);
82
+ if (!ranges.length)
83
+ return;
84
+ const indent = raw.length - raw.trimStart().length;
85
+ const cut = Math.max(indent, ranges[0][0] - excerptLead);
86
+ const prefix = cut > indent ? "…" : "";
87
+ const shift = cut - prefix.length;
88
+ lines.push({
89
+ line: index + 1,
90
+ text: prefix + raw.slice(cut).trimEnd(),
91
+ ranges: ranges.map(([start, end]) => [start - shift, end - shift]),
92
+ });
93
+ total += 1;
94
+ });
95
+ if (lines.length)
96
+ hits.push({ file, lines });
97
+ }
98
+ return { names, hits, total, capped: total >= fileExplorerMaxMatches };
99
+ }
100
+ /** Splits text pieces at the match ranges and wraps the matches in `mark`. */
101
+ function marked(pieces, ranges) {
102
+ const out = [];
103
+ let offset = 0;
104
+ pieces.forEach((piece, index) => {
105
+ const end = offset + piece.content.length;
106
+ const cuts = new Set([offset, end]);
107
+ for (const [start, stop] of ranges)
108
+ for (const cut of [start, stop])
109
+ if (cut > offset && cut < end)
110
+ cuts.add(cut);
111
+ const sorted = [...cuts].sort((a, b) => a - b);
112
+ for (let at = 0; at < sorted.length - 1; at += 1) {
113
+ const from = sorted[at];
114
+ const text = piece.content.slice(from - offset, sorted[at + 1] - offset);
115
+ const span = (_jsx("span", { className: piece.className, children: text }, `${index}-${from}`));
116
+ out.push(ranges.some(([start, stop]) => from >= start && from < stop) ? (_jsx("mark", { className: "sprout-file-explorer-mark", children: span }, `${index}-${from}`)) : (span));
117
+ }
118
+ offset = end;
119
+ });
120
+ return out;
121
+ }
122
+ const glyphs = {
123
+ typescript: ["TS", "info"],
124
+ tsx: ["TSX", "info"],
125
+ javascript: ["JS", "warning"],
126
+ jsx: ["JSX", "warning"],
127
+ json: ["{ }", "warning"],
128
+ jsonc: ["{ }", "warning"],
129
+ json5: ["{ }", "warning"],
130
+ markdown: ["MD", "accent"],
131
+ mdx: ["MDX", "accent"],
132
+ python: ["PY", "info"],
133
+ yaml: ["YML", "danger"],
134
+ toml: ["TML", "danger"],
135
+ css: ["CSS", "accent"],
136
+ scss: ["SCS", "accent"],
137
+ html: ["< >", "danger"],
138
+ xml: ["< >", "warning"],
139
+ shellscript: ["SH", "success"],
140
+ dotenv: ["ENV", "success"],
141
+ sql: ["SQL", "accent"],
142
+ rust: ["RS", "danger"],
143
+ go: ["GO", "info"],
144
+ ruby: ["RB", "danger"],
145
+ };
146
+ /** A 16px tile with a short language label, tinted by language. Decorative. */
147
+ function FileGlyph({ name }) {
148
+ const language = languageForFilename(name);
149
+ const extension = name.includes(".") ? (name.split(".").at(-1) ?? "") : "";
150
+ const [label, tone] = (language && glyphs[language]) ?? [
151
+ extension.slice(0, 3).toUpperCase(),
152
+ "muted",
153
+ ];
154
+ if (!label)
155
+ return (_jsx(FileText, { size: 16, "aria-hidden": "true", className: "sprout-file-explorer-glyph" }));
156
+ return (_jsxs("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", focusable: "false", "data-tone": tone, className: "sprout-file-explorer-glyph", children: [_jsx("rect", { x: "0.5", y: "1.5", width: "15", height: "13", rx: "3", className: "sprout-file-explorer-glyph-tile" }), _jsx("text", { x: "8", y: "10.8", textAnchor: "middle", fontSize: label.length > 2 ? 6 : 7.4, fontWeight: "700", className: "sprout-file-explorer-glyph-label", children: label })] }));
157
+ }
158
+ function CodeView({ file, query, line, }) {
159
+ const language = languageForFilename(file.path);
160
+ const tokens = useCodeTokens(file.content, language);
161
+ const needle = query.toLowerCase();
162
+ const current = useRef(null);
163
+ useEffect(() => {
164
+ current.current?.scrollIntoView({ block: "center" });
165
+ }, [line, file.path]);
166
+ const lines = useMemo(() => tokens?.map((row) => row.map((token) => ({
167
+ content: token.content,
168
+ className: tokenClass(token),
169
+ }))) ?? file.content.split("\n").map((content) => [{ content }]), [tokens, file.content]);
170
+ // ponytail: one DOM row per line; virtualize if files past ~20k lines matter.
171
+ return (_jsx("div", { className: "sprout-file-explorer-code", tabIndex: 0, role: "region", "aria-label": file.path, "data-highlighted": tokens ? "" : undefined, children: _jsx("pre", { className: "sprout-file-explorer-pre", children: _jsx("code", { children: lines.map((pieces, index) => {
172
+ const text = pieces.map((piece) => piece.content).join("");
173
+ return (_jsxs("div", { ref: index + 1 === line ? current : undefined, "data-current": index + 1 === line ? "" : undefined, className: "sprout-file-explorer-line", children: [_jsx("span", { "aria-hidden": "true", className: "sprout-file-explorer-line-number", children: index + 1 }), _jsxs("span", { className: "sprout-file-explorer-line-text", children: [needle
174
+ ? marked(pieces, rangesIn(text, needle))
175
+ : pieces.map((piece, at) => (_jsx("span", { className: piece.className, children: piece.content }, at))), "\n"] })] }, index));
176
+ }) }) }) }));
177
+ }
178
+ /**
179
+ * A read-only code explorer: a folder tree, open tabs, and a syntax-colored
180
+ * file pane. Search filters file names and lists matching lines across every
181
+ * file; picking a line opens the file there.
182
+ */
183
+ export function FileExplorer({ files, label = "Files", defaultPath, className, ...props }) {
184
+ const tree = useMemo(() => buildTree(files), [files]);
185
+ const byPath = useMemo(() => new Map(files.map((file) => [file.path, file])), [files]);
186
+ const [expanded, setExpanded] = useState(() => new Set(defaultPath ? ancestors(defaultPath) : []));
187
+ const [tabs, setTabs] = useState(defaultPath && byPath.has(defaultPath) ? [defaultPath] : []);
188
+ const [active, setActive] = useState(tabs[0] ?? null);
189
+ const [focused, setFocused] = useState(active);
190
+ const [line, setLine] = useState(null);
191
+ const [query, setQuery] = useState("");
192
+ const deferred = useDeferredValue(query.trim());
193
+ const results = useMemo(() => (deferred ? searchFiles(files, deferred) : null), [files, deferred]);
194
+ const rows = useMemo(() => visibleRows(tree, expanded), [tree, expanded]);
195
+ const treeRef = useRef(null);
196
+ const id = useId();
197
+ const toggle = (path, open) => setExpanded((current) => {
198
+ const next = new Set(current);
199
+ if (open ?? !next.has(path))
200
+ next.add(path);
201
+ else
202
+ next.delete(path);
203
+ return next;
204
+ });
205
+ const open = (path, at = null) => {
206
+ setTabs((current) => {
207
+ if (current.includes(path))
208
+ return current;
209
+ const next = [...current];
210
+ next.splice(active ? next.indexOf(active) + 1 : next.length, 0, path);
211
+ return next;
212
+ });
213
+ setActive(path);
214
+ setFocused(path);
215
+ setLine(at);
216
+ setExpanded((current) => new Set([...current, ...ancestors(path)]));
217
+ };
218
+ const close = (path) => {
219
+ const index = tabs.indexOf(path);
220
+ const next = tabs.filter((tab) => tab !== path);
221
+ setTabs(next);
222
+ if (active === path) {
223
+ setActive(next[Math.min(index, next.length - 1)] ?? null);
224
+ setLine(null);
225
+ }
226
+ };
227
+ const focusRow = (path) => {
228
+ if (!path)
229
+ return;
230
+ setFocused(path);
231
+ treeRef.current
232
+ ?.querySelector(`[data-path="${CSS.escape(path)}"]`)
233
+ ?.focus();
234
+ };
235
+ const onTreeKeyDown = (event) => {
236
+ const index = rows.findIndex((row) => row.node.path === focused);
237
+ const row = rows[index];
238
+ if (!row)
239
+ return;
240
+ const { node } = row;
241
+ const isOpen = node.kind === "folder" && expanded.has(node.path);
242
+ switch (event.key) {
243
+ case "ArrowDown":
244
+ focusRow(rows[index + 1]?.node.path);
245
+ break;
246
+ case "ArrowUp":
247
+ focusRow(rows[index - 1]?.node.path);
248
+ break;
249
+ case "Home":
250
+ focusRow(rows[0]?.node.path);
251
+ break;
252
+ case "End":
253
+ focusRow(rows.at(-1)?.node.path);
254
+ break;
255
+ case "ArrowRight":
256
+ if (node.kind !== "folder")
257
+ return;
258
+ if (isOpen)
259
+ focusRow(rows[index + 1]?.node.path);
260
+ else
261
+ toggle(node.path, true);
262
+ break;
263
+ case "ArrowLeft":
264
+ if (isOpen)
265
+ toggle(node.path, false);
266
+ else
267
+ focusRow(folderOf(node.path) || undefined);
268
+ break;
269
+ case "Enter":
270
+ case " ":
271
+ if (node.kind === "folder")
272
+ toggle(node.path);
273
+ else
274
+ open(node.path);
275
+ break;
276
+ default:
277
+ return;
278
+ }
279
+ event.preventDefault();
280
+ };
281
+ const activeFile = active ? byPath.get(active) : undefined;
282
+ const language = activeFile ? languageForFilename(activeFile.path) : null;
283
+ const rovingPath = focused && rows.some((row) => row.node.path === focused)
284
+ ? focused
285
+ : rows[0]?.node.path;
286
+ return (_jsxs("div", { "data-slot": "file-explorer", ...props, className: cn("sprout-file-explorer", className), children: [_jsxs("aside", { className: "sprout-file-explorer-sidebar", "aria-label": label, children: [_jsxs("div", { className: "sprout-file-explorer-sidebar-header", children: [_jsx("span", { className: "sprout-file-explorer-title", children: label }), _jsx("button", { type: "button", className: "sprout-file-explorer-icon-button", "aria-label": "Collapse all folders", title: "Collapse all folders", disabled: expanded.size === 0, onClick: () => setExpanded(new Set()), children: _jsx(MinusSquare, { size: 14, "aria-hidden": "true" }) })] }), _jsx("div", { className: "sprout-file-explorer-search", children: _jsx(SearchField, { label: `Search ${label.toLowerCase()}`, placeholder: "Search names and text", value: query, maxLength: 120, onChange: setQuery, className: "max-w-none" }) }), results ? (_jsxs("div", { className: "sprout-file-explorer-results", "aria-busy": deferred !== query.trim() || undefined, children: [_jsx("p", { role: "status", className: "sprout-file-explorer-summary", children: results.names.length || results.total
287
+ ? `${results.names.length} ${results.names.length === 1 ? "file name" : "file names"}, ${results.capped ? `first ${results.total}` : results.total} ${results.total === 1 ? "line" : "lines"}`
288
+ : `No files or lines match “${deferred}”.` }), results.names.length > 0 && (_jsxs("section", { "aria-labelledby": `${id}-names`, children: [_jsx("h3", { id: `${id}-names`, className: "sprout-file-explorer-group", children: "File names" }), _jsx("ul", { className: "sprout-file-explorer-list", children: results.names.map((file) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "sprout-file-explorer-result", "aria-current": file.path === active || undefined, onClick: () => open(file.path), children: [_jsx(FileGlyph, { name: file.path }), _jsx("span", { className: "sprout-file-explorer-name", children: marked([{ content: nameOf(file.path) }], rangesIn(nameOf(file.path), deferred.toLowerCase())) }), _jsx("span", { className: "sprout-file-explorer-dir", children: folderOf(file.path) })] }) }, file.path))) })] })), results.hits.length > 0 && (_jsxs("section", { "aria-labelledby": `${id}-lines`, children: [_jsx("h3", { id: `${id}-lines`, className: "sprout-file-explorer-group", children: "Text in files" }), _jsx("ul", { className: "sprout-file-explorer-list", children: results.hits.map(({ file, lines }) => (_jsxs("li", { children: [_jsxs("button", { type: "button", className: "sprout-file-explorer-result", onClick: () => open(file.path, lines[0].line), children: [_jsx(FileGlyph, { name: file.path }), _jsx("span", { className: "sprout-file-explorer-name", children: nameOf(file.path) }), _jsx("span", { className: "sprout-file-explorer-dir", children: folderOf(file.path) }), _jsx("span", { className: "sprout-file-explorer-count", children: lines.length })] }), _jsx("ul", { className: "sprout-file-explorer-list", children: lines.map((hit) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "sprout-file-explorer-hit", "aria-label": `${file.path}, line ${hit.line}: ${hit.text}`, "aria-current": (file.path === active && hit.line === line) ||
289
+ undefined, onClick: () => open(file.path, hit.line), children: [_jsx("span", { className: "sprout-file-explorer-hit-line", children: hit.line }), _jsx("span", { className: "sprout-file-explorer-hit-text", children: marked([{ content: hit.text }], hit.ranges) })] }) }, hit.line))) })] }, file.path))) })] }))] })) : (_jsxs("div", { ref: treeRef, role: "tree", "aria-label": label, className: "sprout-file-explorer-tree", onKeyDown: onTreeKeyDown, children: [rows.length === 0 && (_jsx("p", { className: "sprout-file-explorer-summary", children: "No files yet." })), rows.map(({ node, depth, size, position }) => {
290
+ const folder = node.kind === "folder";
291
+ const isOpen = folder && expanded.has(node.path);
292
+ return (_jsxs("div", { role: "treeitem", "data-path": node.path, "data-kind": node.kind, "aria-level": depth + 1, "aria-setsize": size, "aria-posinset": position, "aria-expanded": folder ? isOpen : undefined, "aria-selected": folder ? undefined : node.path === active, tabIndex: node.path === rovingPath ? 0 : -1, className: "sprout-file-explorer-row", style: { "--depth": depth }, onFocus: () => setFocused(node.path), onClick: () => (folder ? toggle(node.path) : open(node.path)), children: [Array.from({ length: depth }, (_, at) => (_jsx("span", { "aria-hidden": "true", className: "sprout-file-explorer-guide" }, at))), _jsx("span", { "aria-hidden": "true", className: "sprout-file-explorer-chevron", children: folder && _jsx(ChevronRight, { size: 12 }) }), folder ? (isOpen ? (_jsx(FolderOpen, { size: 16, "aria-hidden": "true", className: "sprout-file-explorer-folder" })) : (_jsx(Folder, { size: 16, "aria-hidden": "true", className: "sprout-file-explorer-folder" }))) : (_jsx(FileGlyph, { name: node.name })), _jsx("span", { className: "sprout-file-explorer-name", title: node.path, children: node.name })] }, node.path));
293
+ })] }))] }), _jsxs("section", { className: "sprout-file-explorer-editor", "aria-label": "Editor", children: [tabs.length > 0 && (_jsx("div", { className: "sprout-file-explorer-tabs", "aria-label": "Open files", children: tabs.map((path) => (_jsxs("div", { className: "sprout-file-explorer-tab", "data-active": path === active ? "" : undefined, children: [_jsxs("button", { type: "button", className: "sprout-file-explorer-tab-open", "aria-current": path === active || undefined, title: path, onClick: () => {
294
+ setActive(path);
295
+ setLine(null);
296
+ }, onAuxClick: (event) => {
297
+ if (event.button === 1)
298
+ close(path);
299
+ }, children: [_jsx(FileGlyph, { name: path }), nameOf(path)] }), _jsx("button", { type: "button", className: "sprout-file-explorer-tab-close", "aria-label": `Close ${nameOf(path)}`, onClick: () => close(path), children: _jsx(X, { size: 12, "aria-hidden": "true" }) })] }, path))) })), activeFile ? (_jsxs(_Fragment, { children: [_jsx("nav", { className: "sprout-file-explorer-crumbs", "aria-label": "File path", children: _jsx("ol", { children: activeFile.path.split("/").map((part, index, parts) => (_jsxs("li", { "aria-current": index === parts.length - 1 ? "page" : undefined, children: [index > 0 && (_jsx(ChevronRight, { size: 10, "aria-hidden": "true", className: "sprout-file-explorer-crumb-separator" })), part] }, index))) }) }), _jsx("div", { className: "sprout-file-explorer-body", children: "content" in activeFile ? (activeFile.content === "" ? (_jsx("p", { className: "sprout-file-explorer-summary", children: "This file is empty." })) : (_jsx(CodeView, { file: activeFile, query: results ? deferred : "", line: line }, activeFile.path))) : (_jsx(StatePanel, { kind: "unavailable", heading: "h3", title: "File cannot be displayed", description: activeFile.unavailable })) }), _jsxs("div", { className: "sprout-file-explorer-status", children: [_jsx("span", { children: language ? codeLanguageNames[language] : "Plain text" }), "content" in activeFile && (_jsxs("span", { children: [activeFile.content.split("\n").length.toLocaleString(), " lines"] })), line !== null && _jsxs("span", { children: ["Line ", line] }), _jsx("span", { className: "sprout-file-explorer-status-end", children: "Read only" })] })] })) : (_jsx(StatePanel, { kind: "empty", heading: "h3", title: "Open a file", description: "Choose a file in the tree, or search names and text.", icon: FileText }))] })] }));
300
+ }
@@ -65,6 +65,23 @@ interface FileViewerBaseProps {
65
65
  renderers?: FileRenderers | undefined;
66
66
  /** Where focus goes on close. Defaults to the element that had focus when the viewer opened. */
67
67
  finalFocus?: DialogContentProps["finalFocus"];
68
+ /** Your header controls before Download, such as who is viewing and a Share button. */
69
+ actions?: ReactNode;
70
+ /** `DropdownMenuItem`s added to the More actions menu after Reload. */
71
+ menu?: ReactNode;
72
+ /**
73
+ * A side panel for comments, activity, sharing, or file details. Adds a
74
+ * header toggle. It stays mounted while hidden and across file steps, so
75
+ * tabs and drafts inside it survive.
76
+ */
77
+ panel?: ReactNode;
78
+ /** The panel's accessible name and toggle label. Defaults to “Details”. */
79
+ panelLabel?: string;
80
+ /** Controls whether the panel shows. */
81
+ panelOpen?: boolean;
82
+ /** Whether the panel starts open. Defaults to open on screens 1024px and wider. */
83
+ defaultPanelOpen?: boolean;
84
+ onPanelOpenChange?: (open: boolean) => void;
68
85
  }
69
86
  interface SingleFileProps {
70
87
  file: ViewableFile | null;
@@ -81,7 +98,7 @@ interface FileSetProps {
81
98
  }
82
99
  export type FileViewerProps = FileViewerBaseProps & (SingleFileProps | FileSetProps);
83
100
  /** A full-screen dialog that opens a local or linked file, or steps through several. */
84
- export declare function FileViewer({ file, files, index, onIndexChange, open, onOpenChange, renderers, finalFocus, }: FileViewerProps): import("react").JSX.Element;
101
+ export declare function FileViewer({ file, files, index, onIndexChange, open, onOpenChange, renderers, finalFocus, actions, menu, panel, panelLabel, panelOpen, defaultPanelOpen, onPanelOpenChange, }: FileViewerProps): import("react").JSX.Element;
85
102
  export interface ViewableFileContentProps extends HTMLAttributes<HTMLDivElement> {
86
103
  file: ViewableFile;
87
104
  renderers?: FileRenderers | undefined;
@@ -1,15 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, Suspense, useContext, useEffect, useMemo, useRef, useState, } from "react";
2
+ import { createContext, Suspense, useContext, useEffect, useId, useMemo, useRef, useState, } from "react";
3
3
  import { ArrowUpRight } from "reicon-react/icons/ArrowUpRight";
4
4
  import { ChevronLeft } from "reicon-react/icons/ChevronLeft";
5
5
  import { ChevronRight } from "reicon-react/icons/ChevronRight";
6
6
  import { Download } from "reicon-react/icons/Download";
7
+ import { MoreH } from "reicon-react/icons/MoreH";
8
+ import { SidebarRight } from "reicon-react/icons/SidebarRight";
7
9
  import { X } from "reicon-react/icons/X";
8
10
  import { cn } from "../internal/classes.js";
9
11
  import { fileKind } from "../internal/file-kind.js";
10
12
  import { Button } from "./button.js";
11
13
  import { CodeBlock, languageForFilename } from "./code-block.js";
12
14
  import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, } from "./dialog.js";
15
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu.js";
13
16
  import { Spinner } from "./spinner.js";
14
17
  import { StatePanel } from "./state-panel.js";
15
18
  const RenderersContext = createContext({});
@@ -94,10 +97,12 @@ export function renderableType(name, mediaType) {
94
97
  ? type
95
98
  : "application/octet-stream";
96
99
  }
97
- function useFileContent(file) {
100
+ function useFileContent(file, attempt = 0, enabled = true) {
98
101
  const [loaded, setLoaded] = useState({ kind: "loading" });
99
102
  const { source, name, mediaType } = file;
100
103
  useEffect(() => {
104
+ if (!enabled)
105
+ return;
101
106
  let cancelled = false;
102
107
  let objectUrl;
103
108
  const controller = new AbortController();
@@ -141,12 +146,21 @@ function useFileContent(file) {
141
146
  if (objectUrl)
142
147
  URL.revokeObjectURL(objectUrl);
143
148
  };
144
- }, [source, name, mediaType]);
149
+ }, [source, name, mediaType, attempt, enabled]);
145
150
  return loaded;
146
151
  }
147
152
  const ownsArrowKeys = 'input, textarea, select, iframe, [contenteditable="true"], [role="region"], [role="slider"]';
148
153
  /** A full-screen dialog that opens a local or linked file, or steps through several. */
149
- export function FileViewer({ file, files, index = 0, onIndexChange, open, onOpenChange, renderers, finalFocus, }) {
154
+ export function FileViewer({ file, files, index = 0, onIndexChange, open, onOpenChange, renderers, finalFocus, actions, menu, panel, panelLabel = "Details", panelOpen, defaultPanelOpen, onPanelOpenChange, }) {
155
+ const panelId = useId();
156
+ const [ownPanelOpen, setOwnPanelOpen] = useState(() => defaultPanelOpen ??
157
+ (typeof window !== "undefined" &&
158
+ !!window.matchMedia?.("(min-width: 1024px)").matches));
159
+ const panelShown = !!panel && (panelOpen ?? ownPanelOpen);
160
+ const togglePanel = () => {
161
+ setOwnPanelOpen(!panelShown);
162
+ onPanelOpenChange?.(!panelShown);
163
+ };
150
164
  const current = files ? (files[index] ?? null) : file;
151
165
  const count = files?.length ?? 0;
152
166
  const refocus = useRef(null);
@@ -192,7 +206,14 @@ export function FileViewer({ file, files, index = 0, onIndexChange, open, onOpen
192
206
  event.preventDefault();
193
207
  move();
194
208
  };
195
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { size: "fullscreen", showCloseButton: false, "data-file-viewer": "", className: "sprout-file-viewer", finalFocus: finalFocus, onKeyDown: handleKeyDown, children: current && open && (_jsx(SelectedFile, { file: current, renderers: renderers, step: step }, step ? index : undefined)) }) }));
209
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "fullscreen", showCloseButton: false, "data-file-viewer": "", "data-panel": panelShown ? "open" : undefined, className: "sprout-file-viewer", finalFocus: finalFocus, onKeyDown: handleKeyDown, children: [current && open && (_jsx(SelectedFile, { file: current, renderers: renderers, step: step, actions: actions, menu: menu, panel: panel
210
+ ? {
211
+ id: panelId,
212
+ label: panelLabel,
213
+ open: panelShown,
214
+ toggle: togglePanel,
215
+ }
216
+ : undefined }, step ? index : undefined)), current && open && panel && (_jsx("aside", { id: panelId, "aria-label": panelLabel, hidden: !panelShown, "data-slot": "file-viewer-panel", className: "sprout-file-viewer-panel", children: panel }))] }) }));
196
217
  }
197
218
  function sourceLabel(file, google) {
198
219
  return google
@@ -201,9 +222,12 @@ function sourceLabel(file, google) {
201
222
  ? "Local file · Not uploaded"
202
223
  : "Linked file";
203
224
  }
204
- function SelectedFile({ file, renderers, step, }) {
225
+ function SelectedFile({ file, renderers, step, actions, menu, panel, }) {
226
+ const [attempt, setAttempt] = useState(0);
227
+ const retry = () => setAttempt((value) => value + 1);
205
228
  const google = file.source.kind === "remote" ? googleDocument(file.source.url) : null;
206
- return (_jsxs(_Fragment, { children: [_jsxs("header", { "data-slot": "file-viewer-header", className: "sprout-file-viewer-header", children: [_jsxs("div", { className: "sprout-file-viewer-heading", children: [_jsx(DialogTitle, { children: _jsx("span", { className: "sprout-file-viewer-truncate", title: file.name, children: file.name }) }), _jsx(DialogDescription, { children: _jsx("span", { className: "sprout-file-viewer-truncate", children: sourceLabel(file, !!google) }) })] }), step && (_jsxs("div", { "data-slot": "file-viewer-steps", className: "sprout-file-viewer-steps", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Previous file", "data-step": "previous", disabled: !step.previous, onClick: step.previous, children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) }), _jsxs("span", { className: "sprout-file-viewer-position", "aria-hidden": "true", children: [step.position, " of ", step.count] }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Next file", "data-step": "next", disabled: !step.next, onClick: step.next, children: _jsx(ChevronRight, { size: 16, "aria-hidden": "true" }) }), _jsxs("span", { "aria-live": "polite", className: "sprout-file-viewer-sr-only", children: [file.name, ", file ", step.position, " of ", step.count] })] })), google && (_jsxs(Button, { variant: "outline", size: "sm", nativeButton: false, render: _jsx("a", { href: google.url, target: "_blank", rel: "noopener noreferrer" }), children: [_jsx(ArrowUpRight, { size: 16, "aria-hidden": "true" }), "Open in Google"] })), _jsx(DialogClose, { render: _jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Close file viewer" }), children: _jsx(X, { size: 20, "aria-hidden": "true" }) })] }), _jsx(ViewableFileContent, { file: file, renderers: renderers })] }));
229
+ const loaded = useFileContent(file, attempt, !google);
230
+ return (_jsxs(_Fragment, { children: [_jsxs("header", { "data-slot": "file-viewer-header", className: "sprout-file-viewer-header", children: [_jsxs("div", { className: "sprout-file-viewer-heading", children: [_jsx(DialogTitle, { children: _jsx("span", { className: "sprout-file-viewer-truncate", title: file.name, children: file.name }) }), _jsx(DialogDescription, { children: _jsx("span", { className: "sprout-file-viewer-truncate", children: sourceLabel(file, !!google) }) })] }), _jsxs("div", { "data-slot": "file-viewer-controls", className: "sprout-file-viewer-controls", children: [step && (_jsxs("div", { "data-slot": "file-viewer-steps", className: "sprout-file-viewer-steps", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Previous file", "data-step": "previous", disabled: !step.previous, onClick: step.previous, children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) }), _jsxs("span", { className: "sprout-file-viewer-position", "aria-hidden": "true", children: [step.position, " of ", step.count] }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Next file", "data-step": "next", disabled: !step.next, onClick: step.next, children: _jsx(ChevronRight, { size: 16, "aria-hidden": "true" }) }), _jsxs("span", { "aria-live": "polite", className: "sprout-file-viewer-sr-only", children: [file.name, ", file ", step.position, " of ", step.count] })] })), actions && (_jsx("div", { "data-slot": "file-viewer-actions", className: "sprout-file-viewer-actions", children: actions })), google ? (_jsxs(Button, { variant: "outline", size: "sm", nativeButton: false, render: _jsx("a", { href: google.url, target: "_blank", rel: "noopener noreferrer" }), children: [_jsx(ArrowUpRight, { size: 16, "aria-hidden": "true" }), "Open in Google"] })) : loaded.kind === "ready" ? (_jsxs(Button, { variant: "outline", size: "sm", "aria-label": "Download", nativeButton: false, render: _jsx("a", { href: loaded.url, download: file.name }), children: [_jsx(Download, { size: 16, "aria-hidden": "true" }), _jsx("span", { className: "sprout-file-viewer-label", children: "Download" })] })) : (_jsxs(Button, { variant: "outline", size: "sm", "aria-label": "Download", disabled: true, children: [_jsx(Download, { size: 16, "aria-hidden": "true" }), _jsx("span", { className: "sprout-file-viewer-label", children: "Download" })] })), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "More actions" }), children: _jsx(MoreH, { size: 16, "aria-hidden": "true" }) }), _jsxs(DropdownMenuContent, { align: "end", size: "menu", children: [_jsx(DropdownMenuItem, { onClick: retry, children: google ? "Reload viewer" : "Reload file" }), menu && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), menu] }))] })] })] }), _jsxs("div", { "data-slot": "file-viewer-window", className: "sprout-file-viewer-window", children: [panel && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": panel.label, "aria-expanded": panel.open, "aria-controls": panel.id, onClick: panel.toggle, children: _jsx(SidebarRight, { size: 16, "aria-hidden": "true" }) })), _jsx(DialogClose, { render: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Close file viewer" }), children: _jsx(X, { size: 18, "aria-hidden": "true" }) })] })] }), _jsx("div", { "data-slot": "viewable-file-content", className: "sprout-file-viewer-surface", children: google ? (_jsx(EmbeddedFile, { url: google.embedUrl, name: file.name, google: true, onRetry: retry }, `${google.embedUrl}:${attempt}`)) : (_jsx(FileBody, { loaded: loaded, file: file, renderers: renderers, onRetry: retry })) })] }));
207
231
  }
208
232
  /** Loads a local or linked file and shows it inline, with reload and download. */
209
233
  export function ViewableFileContent({ file, renderers, className, ...props }) {
@@ -214,11 +238,14 @@ export function ViewableFileContent({ file, renderers, className, ...props }) {
214
238
  }
215
239
  function LocalFile({ file, renderers, onRetry, }) {
216
240
  const loaded = useFileContent(file);
241
+ return (_jsxs(_Fragment, { children: [loaded.kind === "ready" && (_jsxs("div", { "data-slot": "file-viewer-toolbar", className: "sprout-file-viewer-toolbar", "data-align": "end", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: onRetry, children: "Reload file" }), _jsxs(Button, { variant: "outline", size: "sm", nativeButton: false, render: _jsx("a", { href: loaded.url, download: file.name }), children: [_jsx(Download, { size: 16, "aria-hidden": "true" }), "Download"] })] })), _jsx(FileBody, { loaded: loaded, file: file, renderers: renderers, onRetry: onRetry })] }));
242
+ }
243
+ function FileBody({ loaded, file, renderers, onRetry, }) {
217
244
  if (loaded.kind === "loading")
218
245
  return _jsx(LoadingFile, {});
219
246
  if (loaded.kind === "error")
220
247
  return _jsx(FileError, { message: loaded.message, onRetry: onRetry });
221
- return (_jsxs(_Fragment, { children: [_jsxs("div", { "data-slot": "file-viewer-toolbar", className: "sprout-file-viewer-toolbar", "data-align": "end", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: onRetry, children: "Reload file" }), _jsxs(Button, { variant: "outline", size: "sm", nativeButton: false, render: _jsx("a", { href: loaded.url, download: file.name }), children: [_jsx(Download, { size: 16, "aria-hidden": "true" }), "Download"] })] }), _jsx(FileContent, { name: file.name, mediaType: file.mediaType, bytes: loaded.bytes, url: loaded.url, onRetry: onRetry, renderers: renderers })] }));
248
+ return (_jsx(FileContent, { name: file.name, mediaType: file.mediaType, bytes: loaded.bytes, url: loaded.url, onRetry: onRetry, renderers: renderers }));
222
249
  }
223
250
  const textKinds = [
224
251
  "text",
@@ -6,6 +6,7 @@ import { createContext, useCallback, useContext, useId, useLayoutEffect, useMemo
6
6
  import { Check } from "reicon-react/icons/Check";
7
7
  import { cn } from "../internal/classes.js";
8
8
  import { buttonVariants, } from "./button.js";
9
+ import { Kbd } from "./kbd.js";
9
10
  export const questionnaireItemStatusNames = [
10
11
  "unanswered",
11
12
  "answered",
@@ -800,7 +801,7 @@ export function QuestionnaireChoice({ checked, children, className, defaultCheck
800
801
  props: mergeProps({
801
802
  "data-slot": "questionnaire-choice",
802
803
  className: cn("sprout-questionnaire-choice", className),
803
- children: (_jsxs(_Fragment, { children: [_jsx("input", { "data-slot": "questionnaire-choice-input", className: "sprout-questionnaire-choice-input", ref: setInput, id: id, type: type, name: status === "skipped" ? undefined : name, value: value, checked: isChecked, disabled: disabled, required: required && !multiple && !hasInputAnswer, "aria-invalid": invalid || undefined, "aria-keyshortcuts": keyShortcuts(shortcut, !disabled && isChecked), "data-checked": isChecked ? "" : undefined, "data-unchecked": isChecked ? undefined : "", onChange: handleChange }), _jsxs("span", { "aria-hidden": "true", "data-slot": "questionnaire-choice-indicator", className: "sprout-questionnaire-choice-indicator", children: [_jsx("span", { className: "sprout-questionnaire-choice-dot" }), _jsx(Check, { className: "sprout-questionnaire-choice-check" })] }), _jsx("span", { "data-slot": "questionnaire-choice-label", className: "sprout-questionnaire-choice-label", children: children }), shortcut !== null && (_jsx("span", { "aria-hidden": "true", "data-slot": "questionnaire-choice-shortcut", className: "sprout-questionnaire-choice-shortcut", children: shortcut }))] })),
804
+ children: (_jsxs(_Fragment, { children: [_jsx("input", { "data-slot": "questionnaire-choice-input", className: "sprout-questionnaire-choice-input", ref: setInput, id: id, type: type, name: status === "skipped" ? undefined : name, value: value, checked: isChecked, disabled: disabled, required: required && !multiple && !hasInputAnswer, "aria-invalid": invalid || undefined, "aria-keyshortcuts": keyShortcuts(shortcut, !disabled && isChecked), "data-checked": isChecked ? "" : undefined, "data-unchecked": isChecked ? undefined : "", onChange: handleChange }), _jsxs("span", { "aria-hidden": "true", "data-slot": "questionnaire-choice-indicator", className: "sprout-questionnaire-choice-indicator", children: [_jsx("span", { className: "sprout-questionnaire-choice-dot" }), _jsx(Check, { className: "sprout-questionnaire-choice-check" })] }), _jsx("span", { "data-slot": "questionnaire-choice-label", className: "sprout-questionnaire-choice-label", children: children }), shortcut !== null && (_jsx(Kbd, { variant: "outline", "aria-hidden": "true", "data-slot": "questionnaire-choice-shortcut", className: "sprout-questionnaire-choice-shortcut", children: shortcut }))] })),
804
805
  }, props),
805
806
  state,
806
807
  stateAttributesMapping: {
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Toggle } from "@base-ui/react/toggle";
4
3
  import { Children, Fragment, isValidElement, } from "react";
5
4
  import { X } from "reicon-react/icons/X";
6
5
  import { cn } from "../internal/classes.js";
7
6
  import { Button } from "./button.js";
7
+ import { Kbd } from "./kbd.js";
8
+ import { Toggle } from "./toggle.js";
8
9
  import { Command, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command.js";
9
10
  import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, } from "./dialog.js";
10
11
  /**
@@ -24,7 +25,7 @@ export function SearchFilters({ label, children }) {
24
25
  }
25
26
  /** A pressable filter chip. */
26
27
  export function SearchFilter({ pressed, onPressedChange, icon, children, }) {
27
- return (_jsxs(Toggle, { "data-slot": "search-filter", pressed: pressed, onPressedChange: (next) => onPressedChange(next), className: "sprout-search-filter", children: [icon, children] }));
28
+ return (_jsxs(Toggle, { "data-slot": "search-filter", variant: "outline", size: "sm", pressed: pressed, onPressedChange: (next) => onPressedChange(next), children: [icon, children] }));
28
29
  }
29
30
  /** The scrolling results area. It keeps a steady height so typing does not resize the dialog. */
30
31
  export function SearchList({ label, children }) {
@@ -42,7 +43,7 @@ export function SearchItemIcon({ children }) {
42
43
  }
43
44
  /** One result. The active item shows an Enter key hint. */
44
45
  export function SearchItem({ value, onSelect, media, title, meta, detail, trailing, }) {
45
- return (_jsxs(CommandItem, { value: value, onSelect: () => onSelect(), className: "sprout-search-item", children: [_jsx("span", { "data-slot": "search-item-media", className: "sprout-search-item-media", children: media }), _jsxs("span", { className: "sprout-search-item-text", children: [_jsxs("span", { className: "sprout-search-item-line", children: [_jsx("span", { "data-slot": "search-item-title", className: "sprout-search-item-title", children: title }), meta && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "sprout-search-item-dot", children: "\u00B7" }), _jsx("span", { "data-slot": "search-item-meta", className: "sprout-search-item-meta", children: meta })] }))] }), detail && (_jsx("span", { "data-slot": "search-item-detail", className: "sprout-search-item-detail", children: detail }))] }), trailing && (_jsx("span", { "data-slot": "search-item-trailing", className: "sprout-search-item-trailing", children: trailing })), _jsx("kbd", { "aria-hidden": "true", className: "sprout-search-kbd sprout-search-item-enter", children: "\u21B5" })] }));
46
+ return (_jsxs(CommandItem, { value: value, onSelect: () => onSelect(), className: "sprout-search-item", children: [_jsx("span", { "data-slot": "search-item-media", className: "sprout-search-item-media", children: media }), _jsxs("span", { className: "sprout-search-item-text", children: [_jsxs("span", { className: "sprout-search-item-line", children: [_jsx("span", { "data-slot": "search-item-title", className: "sprout-search-item-title", children: title }), meta && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "sprout-search-item-dot", children: "\u00B7" }), _jsx("span", { "data-slot": "search-item-meta", className: "sprout-search-item-meta", children: meta })] }))] }), detail && (_jsx("span", { "data-slot": "search-item-detail", className: "sprout-search-item-detail", children: detail }))] }), trailing && (_jsx("span", { "data-slot": "search-item-trailing", className: "sprout-search-item-trailing", children: trailing })), _jsx(Kbd, { variant: "outline", "aria-hidden": "true", className: "sprout-search-item-enter", children: "\u21B5" })] }));
46
47
  }
47
48
  function tiles(media) {
48
49
  const nodes = isValidElement(media) && media.type === Fragment
@@ -60,5 +61,5 @@ export function SearchFooter({ children }) {
60
61
  }
61
62
  /** A key and what it does. */
62
63
  export function SearchHint({ keys, children }) {
63
- return (_jsxs("span", { "data-slot": "search-hint", className: "sprout-search-hint", children: [_jsx("kbd", { className: "sprout-search-kbd", children: keys }), children] }));
64
+ return (_jsxs("span", { "data-slot": "search-hint", className: "sprout-search-hint", children: [_jsx(Kbd, { variant: "outline", children: keys }), children] }));
64
65
  }
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Check } from "reicon-react/icons/Check";
3
3
  import { Inbox } from "reicon-react/icons/Inbox";
4
- import { Loader } from "reicon-react/icons/Loader";
5
4
  import { Warning } from "reicon-react/icons/Warning";
6
5
  import { cn } from "../internal/classes.js";
6
+ import { Spinner } from "./spinner.js";
7
7
  export const statePanelKindNames = [
8
8
  "empty",
9
9
  "loading",
@@ -17,7 +17,7 @@ const roles = {
17
17
  success: "status",
18
18
  };
19
19
  function Media({ kind, Icon }) {
20
- return (_jsx("div", { "data-slot": "state-panel-media", className: "sprout-state-panel-media", "aria-hidden": "true", children: kind === "loading" ? (_jsx(Loader, { size: 16, className: "sprout-state-panel-spinner" })) : kind === "success" ? (_jsx(Check, { size: 20 })) : kind === "error" ? (_jsx(Warning, { size: 20, className: "sprout-state-panel-danger" })) : (_jsx(Icon, { size: 20 })) }));
20
+ return (_jsx("div", { "data-slot": "state-panel-media", className: "sprout-state-panel-media", "aria-hidden": "true", children: kind === "loading" ? (_jsx(Spinner, { "aria-hidden": "true" })) : kind === "success" ? (_jsx(Check, { size: 20 })) : kind === "error" ? (_jsx(Warning, { size: 20, className: "sprout-state-panel-danger" })) : (_jsx(Icon, { size: 20 })) }));
21
21
  }
22
22
  export function StatePanel({ kind, title, description, heading: Heading = "h2", icon = Inbox, children, sample, className, ...props }) {
23
23
  const media = icon === null && (kind === "empty" || kind === "unavailable") ? null : (_jsx(Media, { kind: kind, Icon: icon ?? Inbox }));
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./components/card.js";
15
15
  export * from "./components/carousel.js";
16
16
  export * from "./components/chart.js";
17
17
  export * from "./components/chat-confirmation.js";
18
+ export * from "./components/chat-input.js";
18
19
  export * from "./components/chat-plan.js";
19
20
  export * from "./components/chat-sources.js";
20
21
  export * from "./components/checkbox.js";
@@ -28,6 +29,7 @@ export * from "./components/dialog.js";
28
29
  export * from "./components/direction.js";
29
30
  export * from "./components/dropdown-menu.js";
30
31
  export * from "./components/field.js";
32
+ export * from "./components/file-explorer.js";
31
33
  export * from "./components/file-viewer.js";
32
34
  export * from "./components/form.js";
33
35
  export * from "./components/glass-banner.js";
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * from "./components/card.js";
15
15
  export * from "./components/carousel.js";
16
16
  export * from "./components/chart.js";
17
17
  export * from "./components/chat-confirmation.js";
18
+ export * from "./components/chat-input.js";
18
19
  export * from "./components/chat-plan.js";
19
20
  export * from "./components/chat-sources.js";
20
21
  export * from "./components/checkbox.js";
@@ -28,6 +29,7 @@ export * from "./components/dialog.js";
28
29
  export * from "./components/direction.js";
29
30
  export * from "./components/dropdown-menu.js";
30
31
  export * from "./components/field.js";
32
+ export * from "./components/file-explorer.js";
31
33
  export * from "./components/file-viewer.js";
32
34
  export * from "./components/form.js";
33
35
  export * from "./components/glass-banner.js";
@@ -37,6 +37,34 @@ export declare const brandGlyphs: {
37
37
  readonly evenOdd: true;
38
38
  }];
39
39
  };
40
+ readonly gemini: {
41
+ readonly viewBox: "0 0 24 24";
42
+ readonly paths: readonly [{
43
+ readonly d: "M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z";
44
+ readonly evenOdd: true;
45
+ }];
46
+ };
47
+ readonly mistral: {
48
+ readonly viewBox: "0 0 24 24";
49
+ readonly paths: readonly [{
50
+ readonly d: "M3.428 3.4h3.429v3.428h3.429v3.429h-.002 3.431V6.828h3.427V3.4h3.43v13.714H24v3.429H13.714v-3.428h-3.428v-3.429h-3.43v3.428h3.43v3.429H0v-3.429h3.428V3.4zm10.286 13.715h3.428v-3.429h-3.427v3.429z";
51
+ readonly evenOdd: true;
52
+ }];
53
+ };
54
+ readonly meta: {
55
+ readonly viewBox: "0 0 24 24";
56
+ readonly paths: readonly [{
57
+ readonly d: "M6.897 4c1.915 0 3.516.932 5.43 3.376l.282-.373c.19-.246.383-.484.58-.71l.313-.35C14.588 4.788 15.792 4 17.225 4c1.273 0 2.469.557 3.491 1.516l.218.213c1.73 1.765 2.917 4.71 3.053 8.026l.011.392.002.25c0 1.501-.28 2.759-.818 3.7l-.14.23-.108.153c-.301.42-.664.758-1.086 1.009l-.265.142-.087.04a3.493 3.493 0 01-.302.118 4.117 4.117 0 01-1.33.208c-.524 0-.996-.067-1.438-.215-.614-.204-1.163-.56-1.726-1.116l-.227-.235c-.753-.812-1.534-1.976-2.493-3.586l-1.43-2.41-.544-.895-1.766 3.13-.343.592C7.597 19.156 6.227 20 4.356 20c-1.21 0-2.205-.42-2.936-1.182l-.168-.184c-.484-.573-.837-1.311-1.043-2.189l-.067-.32a8.69 8.69 0 01-.136-1.288L0 14.468c.002-.745.06-1.49.174-2.23l.1-.573c.298-1.53.828-2.958 1.536-4.157l.209-.34c1.177-1.83 2.789-3.053 4.615-3.16L6.897 4zm-.033 2.615l-.201.01c-.83.083-1.606.673-2.252 1.577l-.138.199-.01.018c-.67 1.017-1.185 2.378-1.456 3.845l-.004.022a12.591 12.591 0 00-.207 2.254l.002.188c.004.18.017.36.04.54l.043.291c.092.503.257.908.486 1.208l.117.137c.303.323.698.492 1.17.492 1.1 0 1.796-.676 3.696-3.641l2.175-3.4.454-.701-.139-.198C9.11 7.3 8.084 6.616 6.864 6.616zm10.196-.552l-.176.007c-.635.048-1.223.359-1.82.933l-.196.198c-.439.462-.887 1.064-1.367 1.807l.266.398c.18.274.362.56.55.858l.293.475 1.396 2.335.695 1.114c.583.926 1.03 1.6 1.408 2.082l.213.262c.282.326.529.54.777.673l.102.05c.227.1.457.138.718.138.176.002.35-.023.518-.073.338-.104.61-.32.813-.637l.095-.163.077-.162c.194-.459.29-1.06.29-1.785l-.006-.449c-.08-2.871-.938-5.372-2.2-6.798l-.176-.189c-.67-.683-1.444-1.074-2.27-1.074z";
58
+ readonly evenOdd: true;
59
+ }];
60
+ };
61
+ readonly qwen: {
62
+ readonly viewBox: "0 0 24 24";
63
+ readonly paths: readonly [{
64
+ readonly d: "M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z";
65
+ readonly evenOdd: true;
66
+ }];
67
+ };
40
68
  readonly google: {
41
69
  readonly viewBox: "0 0 24 24";
42
70
  readonly paths: readonly [{