@tulipfarm/sprout 0.0.0-stage → 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 (314) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -2
  3. package/dist/components/accordion.d.ts +28 -0
  4. package/dist/components/accordion.js +26 -0
  5. package/dist/components/alert-dialog.d.ts +60 -0
  6. package/dist/components/alert-dialog.js +69 -0
  7. package/dist/components/alert.d.ts +30 -0
  8. package/dist/components/alert.js +35 -0
  9. package/dist/components/aspect-ratio.d.ts +7 -0
  10. package/dist/components/aspect-ratio.js +6 -0
  11. package/dist/components/attachment.d.ts +118 -0
  12. package/dist/components/attachment.js +285 -0
  13. package/dist/components/avatar.d.ts +32 -0
  14. package/dist/components/avatar.js +73 -0
  15. package/dist/components/badge.d.ts +21 -0
  16. package/dist/components/badge.js +27 -0
  17. package/dist/components/brand-icon.d.ts +28 -0
  18. package/dist/components/brand-icon.js +82 -0
  19. package/dist/components/breadcrumb.d.ts +29 -0
  20. package/dist/components/breadcrumb.js +44 -0
  21. package/dist/components/button-group.d.ts +28 -0
  22. package/dist/components/button-group.js +36 -0
  23. package/dist/components/button.d.ts +79 -0
  24. package/dist/components/button.js +93 -0
  25. package/dist/components/calendar.d.ts +27 -0
  26. package/dist/components/calendar.js +107 -0
  27. package/dist/components/canvas.d.ts +78 -0
  28. package/dist/components/canvas.js +180 -0
  29. package/dist/components/card.d.ts +43 -0
  30. package/dist/components/card.js +42 -0
  31. package/dist/components/carousel.d.ts +57 -0
  32. package/dist/components/carousel.js +422 -0
  33. package/dist/components/chart.d.ts +112 -0
  34. package/dist/components/chart.js +172 -0
  35. package/dist/components/chat-confirmation.d.ts +42 -0
  36. package/dist/components/chat-confirmation.js +62 -0
  37. package/dist/components/chat-input.d.ts +94 -0
  38. package/dist/components/chat-input.js +279 -0
  39. package/dist/components/chat-plan.d.ts +21 -0
  40. package/dist/components/chat-plan.js +35 -0
  41. package/dist/components/chat-sources.d.ts +64 -0
  42. package/dist/components/chat-sources.js +66 -0
  43. package/dist/components/checkbox.d.ts +16 -0
  44. package/dist/components/checkbox.js +17 -0
  45. package/dist/components/choice-list.d.ts +53 -0
  46. package/dist/components/choice-list.js +36 -0
  47. package/dist/components/code-block.d.ts +35 -0
  48. package/dist/components/code-block.js +64 -0
  49. package/dist/components/collapsible.d.ts +24 -0
  50. package/dist/components/collapsible.js +27 -0
  51. package/dist/components/combobox.d.ts +95 -0
  52. package/dist/components/combobox.js +106 -0
  53. package/dist/components/command.d.ts +94 -0
  54. package/dist/components/command.js +414 -0
  55. package/dist/components/context-menu.d.ts +57 -0
  56. package/dist/components/context-menu.js +60 -0
  57. package/dist/components/dialog.d.ts +60 -0
  58. package/dist/components/dialog.js +79 -0
  59. package/dist/components/direction.d.ts +6 -0
  60. package/dist/components/direction.js +6 -0
  61. package/dist/components/dropdown-menu.d.ts +62 -0
  62. package/dist/components/dropdown-menu.js +59 -0
  63. package/dist/components/field.d.ts +117 -0
  64. package/dist/components/field.js +150 -0
  65. package/dist/components/file-explorer.d.ts +27 -0
  66. package/dist/components/file-explorer.js +300 -0
  67. package/dist/components/file-viewer.d.ts +137 -0
  68. package/dist/components/file-viewer.js +400 -0
  69. package/dist/components/form.d.ts +87 -0
  70. package/dist/components/form.js +55 -0
  71. package/dist/components/glass-banner.d.ts +27 -0
  72. package/dist/components/glass-banner.js +116 -0
  73. package/dist/components/gradient-banner.d.ts +37 -0
  74. package/dist/components/gradient-banner.js +43 -0
  75. package/dist/components/hover-card.d.ts +27 -0
  76. package/dist/components/hover-card.js +26 -0
  77. package/dist/components/input-group.d.ts +68 -0
  78. package/dist/components/input-group.js +88 -0
  79. package/dist/components/input-otp.d.ts +38 -0
  80. package/dist/components/input-otp.js +43 -0
  81. package/dist/components/input.d.ts +16 -0
  82. package/dist/components/input.js +18 -0
  83. package/dist/components/kbd.d.ts +12 -0
  84. package/dist/components/kbd.js +11 -0
  85. package/dist/components/label.d.ts +8 -0
  86. package/dist/components/label.js +10 -0
  87. package/dist/components/mention-list.d.ts +54 -0
  88. package/dist/components/mention-list.js +31 -0
  89. package/dist/components/menubar.d.ts +64 -0
  90. package/dist/components/menubar.js +68 -0
  91. package/dist/components/message-actions.d.ts +34 -0
  92. package/dist/components/message-actions.js +68 -0
  93. package/dist/components/message-response.d.ts +20 -0
  94. package/dist/components/message-response.js +46 -0
  95. package/dist/components/message-scroller.d.ts +71 -0
  96. package/dist/components/message-scroller.js +217 -0
  97. package/dist/components/message.d.ts +49 -0
  98. package/dist/components/message.js +59 -0
  99. package/dist/components/navigation-menu.d.ts +45 -0
  100. package/dist/components/navigation-menu.js +54 -0
  101. package/dist/components/notice-banner.d.ts +14 -0
  102. package/dist/components/notice-banner.js +11 -0
  103. package/dist/components/page-content.d.ts +7 -0
  104. package/dist/components/page-content.js +9 -0
  105. package/dist/components/page-header.d.ts +37 -0
  106. package/dist/components/page-header.js +53 -0
  107. package/dist/components/pagination.d.ts +29 -0
  108. package/dist/components/pagination.js +29 -0
  109. package/dist/components/plan-outline.d.ts +60 -0
  110. package/dist/components/plan-outline.js +108 -0
  111. package/dist/components/popover.d.ts +34 -0
  112. package/dist/components/popover.js +35 -0
  113. package/dist/components/progress.d.ts +27 -0
  114. package/dist/components/progress.js +35 -0
  115. package/dist/components/prompt-list.d.ts +23 -0
  116. package/dist/components/prompt-list.js +11 -0
  117. package/dist/components/questionnaire.d.ts +148 -0
  118. package/dist/components/questionnaire.js +1002 -0
  119. package/dist/components/radio-group.d.ts +16 -0
  120. package/dist/components/radio-group.js +21 -0
  121. package/dist/components/reasoning-effort.d.ts +24 -0
  122. package/dist/components/reasoning-effort.js +269 -0
  123. package/dist/components/resizable.d.ts +67 -0
  124. package/dist/components/resizable.js +536 -0
  125. package/dist/components/result-card.d.ts +58 -0
  126. package/dist/components/result-card.js +88 -0
  127. package/dist/components/review-card.d.ts +62 -0
  128. package/dist/components/review-card.js +49 -0
  129. package/dist/components/rich-text-editor.d.ts +51 -0
  130. package/dist/components/rich-text-editor.js +248 -0
  131. package/dist/components/schema-diagram.d.ts +52 -0
  132. package/dist/components/schema-diagram.js +226 -0
  133. package/dist/components/scroll-area.d.ts +22 -0
  134. package/dist/components/scroll-area.js +19 -0
  135. package/dist/components/search-dialog.d.ts +103 -0
  136. package/dist/components/search-dialog.js +65 -0
  137. package/dist/components/search-field.d.ts +17 -0
  138. package/dist/components/search-field.js +27 -0
  139. package/dist/components/select.d.ts +57 -0
  140. package/dist/components/select.js +57 -0
  141. package/dist/components/selection-slot.d.ts +18 -0
  142. package/dist/components/selection-slot.js +53 -0
  143. package/dist/components/separator.d.ts +48 -0
  144. package/dist/components/separator.js +54 -0
  145. package/dist/components/settings-list.d.ts +26 -0
  146. package/dist/components/settings-list.js +17 -0
  147. package/dist/components/sheet.d.ts +70 -0
  148. package/dist/components/sheet.js +77 -0
  149. package/dist/components/sidebar.d.ts +70 -0
  150. package/dist/components/sidebar.js +187 -0
  151. package/dist/components/skeleton.d.ts +31 -0
  152. package/dist/components/skeleton.js +32 -0
  153. package/dist/components/slider.d.ts +17 -0
  154. package/dist/components/slider.js +24 -0
  155. package/dist/components/spinner.d.ts +37 -0
  156. package/dist/components/spinner.js +43 -0
  157. package/dist/components/state-panel-samples.d.ts +25 -0
  158. package/dist/components/state-panel-samples.js +423 -0
  159. package/dist/components/state-panel.d.ts +35 -0
  160. package/dist/components/state-panel.js +29 -0
  161. package/dist/components/state-sample.d.ts +63 -0
  162. package/dist/components/state-sample.js +42 -0
  163. package/dist/components/switch.d.ts +12 -0
  164. package/dist/components/switch.js +14 -0
  165. package/dist/components/table-nuqs.d.ts +16 -0
  166. package/dist/components/table-nuqs.js +59 -0
  167. package/dist/components/table.d.ts +120 -0
  168. package/dist/components/table.js +663 -0
  169. package/dist/components/tabs.d.ts +37 -0
  170. package/dist/components/tabs.js +36 -0
  171. package/dist/components/textarea.d.ts +18 -0
  172. package/dist/components/textarea.js +19 -0
  173. package/dist/components/toast.d.ts +45 -0
  174. package/dist/components/toast.js +83 -0
  175. package/dist/components/toggle-group.d.ts +30 -0
  176. package/dist/components/toggle-group.js +39 -0
  177. package/dist/components/toggle.d.ts +27 -0
  178. package/dist/components/toggle.js +21 -0
  179. package/dist/components/tool-activity.d.ts +79 -0
  180. package/dist/components/tool-activity.js +226 -0
  181. package/dist/components/toolbar-status.d.ts +21 -0
  182. package/dist/components/toolbar-status.js +14 -0
  183. package/dist/components/tooltip.d.ts +31 -0
  184. package/dist/components/tooltip.js +99 -0
  185. package/dist/components/typed-text.d.ts +17 -0
  186. package/dist/components/typed-text.js +56 -0
  187. package/dist/index.d.ts +90 -0
  188. package/dist/index.js +90 -0
  189. package/dist/internal/brand-glyphs.d.ts +370 -0
  190. package/dist/internal/brand-glyphs.js +262 -0
  191. package/dist/internal/classes.d.ts +3 -0
  192. package/dist/internal/classes.js +20 -0
  193. package/dist/internal/code-tokens.d.ts +4 -0
  194. package/dist/internal/code-tokens.js +34 -0
  195. package/dist/internal/fieldset.d.ts +6 -0
  196. package/dist/internal/fieldset.js +38 -0
  197. package/dist/internal/file-kind.d.ts +4 -0
  198. package/dist/internal/file-kind.js +62 -0
  199. package/dist/internal/languages.d.ts +7 -0
  200. package/dist/internal/languages.js +1010 -0
  201. package/dist/internal/message-scroller.d.ts +102 -0
  202. package/dist/internal/message-scroller.js +637 -0
  203. package/dist/internal/pressable.d.ts +2 -0
  204. package/dist/internal/pressable.js +29 -0
  205. package/dist/internal/syntax.d.ts +14 -0
  206. package/dist/internal/syntax.js +137 -0
  207. package/dist/styles/accordion.css +102 -0
  208. package/dist/styles/alert-dialog.css +198 -0
  209. package/dist/styles/alert.css +149 -0
  210. package/dist/styles/aspect-ratio.css +8 -0
  211. package/dist/styles/attachment.css +700 -0
  212. package/dist/styles/avatar.css +186 -0
  213. package/dist/styles/badge.css +178 -0
  214. package/dist/styles/brand-icon.css +104 -0
  215. package/dist/styles/breadcrumb.css +121 -0
  216. package/dist/styles/button-group.css +113 -0
  217. package/dist/styles/button.css +380 -0
  218. package/dist/styles/calendar.css +295 -0
  219. package/dist/styles/canvas.css +313 -0
  220. package/dist/styles/card.css +146 -0
  221. package/dist/styles/carousel.css +107 -0
  222. package/dist/styles/chart.css +227 -0
  223. package/dist/styles/chat-confirmation.css +29 -0
  224. package/dist/styles/chat-input.css +240 -0
  225. package/dist/styles/chat-plan.css +109 -0
  226. package/dist/styles/chat-sources.css +191 -0
  227. package/dist/styles/checkbox.css +122 -0
  228. package/dist/styles/choice-list.css +343 -0
  229. package/dist/styles/code-block.css +211 -0
  230. package/dist/styles/collapsible.css +72 -0
  231. package/dist/styles/combobox.css +347 -0
  232. package/dist/styles/command.css +233 -0
  233. package/dist/styles/context-menu.css +151 -0
  234. package/dist/styles/dialog.css +237 -0
  235. package/dist/styles/dropdown-menu.css +150 -0
  236. package/dist/styles/field.css +340 -0
  237. package/dist/styles/file-explorer.css +517 -0
  238. package/dist/styles/file-viewer.css +375 -0
  239. package/dist/styles/form.css +155 -0
  240. package/dist/styles/glass-banner.css +308 -0
  241. package/dist/styles/gradient-banner.css +256 -0
  242. package/dist/styles/hover-card.css +95 -0
  243. package/dist/styles/index.css +268 -0
  244. package/dist/styles/input-group.css +285 -0
  245. package/dist/styles/input-otp.css +138 -0
  246. package/dist/styles/input.css +98 -0
  247. package/dist/styles/kbd.css +59 -0
  248. package/dist/styles/label.css +89 -0
  249. package/dist/styles/mention-list.css +166 -0
  250. package/dist/styles/menubar.css +226 -0
  251. package/dist/styles/message-actions.css +83 -0
  252. package/dist/styles/message-response.css +176 -0
  253. package/dist/styles/message-scroller.css +136 -0
  254. package/dist/styles/message.css +310 -0
  255. package/dist/styles/navigation-menu.css +291 -0
  256. package/dist/styles/notice-banner.css +68 -0
  257. package/dist/styles/page-content.css +21 -0
  258. package/dist/styles/page-header.css +156 -0
  259. package/dist/styles/pagination.css +72 -0
  260. package/dist/styles/plan-outline.css +393 -0
  261. package/dist/styles/popover.css +140 -0
  262. package/dist/styles/progress.css +93 -0
  263. package/dist/styles/prompt-list.css +165 -0
  264. package/dist/styles/questionnaire.css +308 -0
  265. package/dist/styles/radio-group.css +119 -0
  266. package/dist/styles/reasoning-effort.css +157 -0
  267. package/dist/styles/resizable.css +108 -0
  268. package/dist/styles/result-card.css +215 -0
  269. package/dist/styles/review-card.css +235 -0
  270. package/dist/styles/rich-text-editor.css +169 -0
  271. package/dist/styles/schema-diagram.css +419 -0
  272. package/dist/styles/scroll-area.css +77 -0
  273. package/dist/styles/search-dialog.css +280 -0
  274. package/dist/styles/search-field.css +17 -0
  275. package/dist/styles/select.css +285 -0
  276. package/dist/styles/selection-slot.css +31 -0
  277. package/dist/styles/separator.css +206 -0
  278. package/dist/styles/settings-list.css +81 -0
  279. package/dist/styles/sheet.css +361 -0
  280. package/dist/styles/sidebar.css +363 -0
  281. package/dist/styles/skeleton.css +80 -0
  282. package/dist/styles/slider.css +142 -0
  283. package/dist/styles/spinner.css +39 -0
  284. package/dist/styles/state-panel-samples.css +41 -0
  285. package/dist/styles/state-panel.css +173 -0
  286. package/dist/styles/state-sample.css +285 -0
  287. package/dist/styles/switch.css +128 -0
  288. package/dist/styles/table.css +604 -0
  289. package/dist/styles/tabs.css +223 -0
  290. package/dist/styles/textarea.css +77 -0
  291. package/dist/styles/themes/chatgpt.css +125 -0
  292. package/dist/styles/themes/claude.css +125 -0
  293. package/dist/styles/themes/cloudflare.css +125 -0
  294. package/dist/styles/themes/discord.css +125 -0
  295. package/dist/styles/themes/github.css +125 -0
  296. package/dist/styles/themes/grok.css +125 -0
  297. package/dist/styles/themes/linear.css +126 -0
  298. package/dist/styles/themes/neutral.css +125 -0
  299. package/dist/styles/themes/notion.css +125 -0
  300. package/dist/styles/themes/perplexity.css +125 -0
  301. package/dist/styles/themes/raycast.css +125 -0
  302. package/dist/styles/themes/spotify.css +125 -0
  303. package/dist/styles/themes/stripe.css +125 -0
  304. package/dist/styles/themes/supabase.css +125 -0
  305. package/dist/styles/themes/tulipfarm.css +115 -0
  306. package/dist/styles/themes/vercel.css +126 -0
  307. package/dist/styles/toast.css +197 -0
  308. package/dist/styles/toggle-group.css +161 -0
  309. package/dist/styles/toggle.css +119 -0
  310. package/dist/styles/tool-activity.css +396 -0
  311. package/dist/styles/toolbar-status.css +52 -0
  312. package/dist/styles/tooltip.css +122 -0
  313. package/dist/styles/typed-text.css +17 -0
  314. package/package.json +452 -4
@@ -0,0 +1,400 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createContext, Suspense, useContext, useEffect, useId, useMemo, useRef, useState, } from "react";
3
+ import { ArrowUpRight } from "reicon-react/icons/ArrowUpRight";
4
+ import { ChevronLeft } from "reicon-react/icons/ChevronLeft";
5
+ import { ChevronRight } from "reicon-react/icons/ChevronRight";
6
+ import { Download } from "reicon-react/icons/Download";
7
+ import { MoreH } from "reicon-react/icons/MoreH";
8
+ import { SidebarRight } from "reicon-react/icons/SidebarRight";
9
+ import { X } from "reicon-react/icons/X";
10
+ import { cn } from "../internal/classes.js";
11
+ import { fileKind } from "../internal/file-kind.js";
12
+ import { Button } from "./button.js";
13
+ import { CodeBlock, languageForFilename } from "./code-block.js";
14
+ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, } from "./dialog.js";
15
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu.js";
16
+ import { Spinner } from "./spinner.js";
17
+ import { StatePanel } from "./state-panel.js";
18
+ const RenderersContext = createContext({});
19
+ /** Registers file renderers once for every viewer below it. */
20
+ export function FileRenderersProvider({ renderers, children, }) {
21
+ const parent = useContext(RenderersContext);
22
+ const value = useMemo(() => ({ ...parent, ...renderers }), [parent, renderers]);
23
+ return _jsx(RenderersContext, { value: value, children: children });
24
+ }
25
+ function useRenderers(own) {
26
+ const shared = useContext(RenderersContext);
27
+ return own ? { ...shared, ...own } : shared;
28
+ }
29
+ export const maxViewableFileBytes = 50 * 1024 * 1024;
30
+ export function viewableFileKind(name, mediaType) {
31
+ return fileKind(name, mediaType);
32
+ }
33
+ /** Recognizes a docs.google.com Docs, Sheets, or Slides link. */
34
+ export function googleDocument(url) {
35
+ let parsed;
36
+ try {
37
+ parsed = new URL(url);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ if (parsed.protocol !== "https:" ||
43
+ parsed.hostname !== "docs.google.com" ||
44
+ parsed.username ||
45
+ parsed.password ||
46
+ parsed.port)
47
+ return null;
48
+ const match = /^\/(document|spreadsheets|presentation)\/d\/([a-zA-Z0-9_-]+)(?:\/|$)/.exec(parsed.pathname);
49
+ if (!match)
50
+ return null;
51
+ const [, family, id] = match;
52
+ const canonical = `https://docs.google.com/${family}/d/${id}`;
53
+ const endpoint = family === "presentation" ? "embed" : "preview";
54
+ return {
55
+ url: `${canonical}/edit`,
56
+ embedUrl: `${canonical}/${endpoint}${family === "spreadsheets" ? "?widget=true&headers=false" : ""}`,
57
+ mediaType: `application/vnd.google-apps.${family === "spreadsheets" ? "spreadsheet" : family}`,
58
+ };
59
+ }
60
+ /** Reads the document link from a .gdoc, .gsheet, or .gslides shortcut. */
61
+ export function googleShortcut(text) {
62
+ const value = JSON.parse(text);
63
+ if (typeof value !== "object" ||
64
+ value === null ||
65
+ !("url" in value) ||
66
+ typeof value.url !== "string")
67
+ throw new Error("This Google shortcut does not contain a document link.");
68
+ const document = googleDocument(value.url);
69
+ if (!document)
70
+ throw new Error("This shortcut does not contain a supported Google Docs, Sheets, or Slides link.");
71
+ return document.url;
72
+ }
73
+ const rasterTypes = new Set([
74
+ "image/png",
75
+ "image/jpeg",
76
+ "image/gif",
77
+ "image/webp",
78
+ "image/avif",
79
+ "image/bmp",
80
+ "image/x-icon",
81
+ "image/vnd.microsoft.icon",
82
+ "image/heic",
83
+ "image/heif",
84
+ "image/tiff",
85
+ ]);
86
+ /**
87
+ * The type to give a same-origin blob URL. Stored media types are
88
+ * untrusted, so anything a browser could run as a document, including SVG,
89
+ * becomes `application/octet-stream`.
90
+ */
91
+ export function renderableType(name, mediaType) {
92
+ const kind = fileKind(name, mediaType);
93
+ if (kind === "pdf")
94
+ return "application/pdf";
95
+ const type = mediaType.trim().toLowerCase();
96
+ return kind === "image" && rasterTypes.has(type)
97
+ ? type
98
+ : "application/octet-stream";
99
+ }
100
+ function useFileContent(file, attempt = 0, enabled = true) {
101
+ const [loaded, setLoaded] = useState({ kind: "loading" });
102
+ const { source, name, mediaType } = file;
103
+ useEffect(() => {
104
+ if (!enabled)
105
+ return;
106
+ let cancelled = false;
107
+ let objectUrl;
108
+ const controller = new AbortController();
109
+ const timeout = setTimeout(() => controller.abort(), 15000);
110
+ setLoaded({ kind: "loading" });
111
+ const load = async () => {
112
+ let blob;
113
+ if (source.kind === "local")
114
+ blob = source.file;
115
+ else {
116
+ const response = await fetch(source.url, { signal: controller.signal });
117
+ if (!response.ok)
118
+ throw new Error("The file could not be loaded. Check the link and try again.");
119
+ blob = await response.blob();
120
+ }
121
+ if (blob.size > maxViewableFileBytes)
122
+ throw new Error("This file is larger than the 50 MB local viewing limit.");
123
+ const bytes = await blob.arrayBuffer();
124
+ if (cancelled)
125
+ return;
126
+ objectUrl = URL.createObjectURL(new Blob([bytes], {
127
+ type: renderableType(name, blob.type || mediaType),
128
+ }));
129
+ setLoaded({ kind: "ready", bytes, url: objectUrl });
130
+ };
131
+ void load()
132
+ .catch((error) => {
133
+ if (!cancelled)
134
+ setLoaded({
135
+ kind: "error",
136
+ message: error instanceof Error && error.name !== "AbortError"
137
+ ? error.message
138
+ : "The file took too long to load. Try again.",
139
+ });
140
+ })
141
+ .finally(() => clearTimeout(timeout));
142
+ return () => {
143
+ cancelled = true;
144
+ clearTimeout(timeout);
145
+ controller.abort();
146
+ if (objectUrl)
147
+ URL.revokeObjectURL(objectUrl);
148
+ };
149
+ }, [source, name, mediaType, attempt, enabled]);
150
+ return loaded;
151
+ }
152
+ const ownsArrowKeys = 'input, textarea, select, iframe, [contenteditable="true"], [role="region"], [role="slider"]';
153
+ /** A full-screen dialog that opens a local or linked file, or steps through several. */
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
+ };
164
+ const current = files ? (files[index] ?? null) : file;
165
+ const count = files?.length ?? 0;
166
+ const refocus = useRef(null);
167
+ const goTo = (target) => () => {
168
+ const button = document.activeElement?.closest("[data-step]");
169
+ const popup = button?.closest("[data-file-viewer]");
170
+ refocus.current =
171
+ button && popup ? { popup, direction: button.dataset.step ?? "" } : null;
172
+ onIndexChange?.(target);
173
+ };
174
+ const step = files && count > 1
175
+ ? {
176
+ position: index + 1,
177
+ count,
178
+ previous: index > 0 ? goTo(index - 1) : undefined,
179
+ next: index < count - 1 ? goTo(index + 1) : undefined,
180
+ }
181
+ : undefined;
182
+ // Each step remounts the file to reset its state, which destroys the focused step button.
183
+ useEffect(() => {
184
+ const pending = refocus.current;
185
+ refocus.current = null;
186
+ if (!pending)
187
+ return;
188
+ const enabled = ":not(:disabled):not([aria-disabled='true'])";
189
+ const target = pending.popup.querySelector(`[data-step="${pending.direction}"]${enabled}`) ?? pending.popup.querySelector(`[data-step]${enabled}`);
190
+ target?.focus();
191
+ }, [index]);
192
+ const handleKeyDown = (event) => {
193
+ if (!step ||
194
+ event.defaultPrevented ||
195
+ event.altKey ||
196
+ event.ctrlKey ||
197
+ event.metaKey)
198
+ return;
199
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
200
+ return;
201
+ if (event.target.closest(ownsArrowKeys))
202
+ return;
203
+ const move = event.key === "ArrowLeft" ? step.previous : step.next;
204
+ if (!move)
205
+ return;
206
+ event.preventDefault();
207
+ move();
208
+ };
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 }))] }) }));
217
+ }
218
+ function sourceLabel(file, google) {
219
+ return google
220
+ ? "Google document"
221
+ : file.source.kind === "local"
222
+ ? "Local file · Not uploaded"
223
+ : "Linked file";
224
+ }
225
+ function SelectedFile({ file, renderers, step, actions, menu, panel, }) {
226
+ const [attempt, setAttempt] = useState(0);
227
+ const retry = () => setAttempt((value) => value + 1);
228
+ const google = file.source.kind === "remote" ? googleDocument(file.source.url) : null;
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 })) })] }));
231
+ }
232
+ /** Loads a local or linked file and shows it inline, with reload and download. */
233
+ export function ViewableFileContent({ file, renderers, className, ...props }) {
234
+ const [attempt, setAttempt] = useState(0);
235
+ const retry = () => setAttempt((value) => value + 1);
236
+ const google = file.source.kind === "remote" ? googleDocument(file.source.url) : null;
237
+ return (_jsx("div", { "data-slot": "viewable-file-content", ...props, className: cn("sprout-file-viewer-surface", className), children: google ? (_jsx(EmbeddedFile, { url: google.embedUrl, name: file.name, google: true, onRetry: retry }, `${google.embedUrl}:${attempt}`)) : (_jsx(LocalFile, { file: file, renderers: renderers, onRetry: retry }, attempt)) }));
238
+ }
239
+ function LocalFile({ file, renderers, onRetry, }) {
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, }) {
244
+ if (loaded.kind === "loading")
245
+ return _jsx(LoadingFile, {});
246
+ if (loaded.kind === "error")
247
+ return _jsx(FileError, { message: loaded.message, onRetry: onRetry });
248
+ return (_jsx(FileContent, { name: file.name, mediaType: file.mediaType, bytes: loaded.bytes, url: loaded.url, onRetry: onRetry, renderers: renderers }));
249
+ }
250
+ const textKinds = [
251
+ "text",
252
+ "code",
253
+ "json",
254
+ "markdown",
255
+ "google",
256
+ ];
257
+ /** Shows bytes you already loaded: images, PDF, text, JSON, Markdown, Google shortcuts, and more. */
258
+ export function FileContent({ name, mediaType, bytes, url, onRetry, renderers: own, }) {
259
+ const renderers = useRenderers(own);
260
+ const kind = fileKind(name, mediaType);
261
+ const text = useMemo(() => {
262
+ if (!textKinds.includes(kind))
263
+ return null;
264
+ try {
265
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
266
+ }
267
+ catch {
268
+ return undefined;
269
+ }
270
+ }, [bytes, kind]);
271
+ if (text === undefined)
272
+ return (_jsx(FileError, { message: "This file is not valid UTF-8 text. Download it to open in another application.", onRetry: onRetry }));
273
+ const unavailable = (_jsx(StatePanel, { kind: "unavailable", title: "This format cannot be displayed locally", description: name.toLowerCase().endsWith(".ppt")
274
+ ? "Save this older PowerPoint file as .pptx to view the slides here. You can still download the original."
275
+ : "Download the original file to open it in a compatible application." }));
276
+ let body;
277
+ if (bytes.byteLength === 0)
278
+ body = (_jsx(StatePanel, { kind: "empty", title: "This file is empty", description: "The file contains no data to display." }));
279
+ else if (kind === "image")
280
+ body = _jsx(ImageFile, { url: url, name: name, onRetry: onRetry });
281
+ else if (kind === "pdf")
282
+ body = _jsx(EmbeddedFile, { url: url, name: name, onRetry: onRetry });
283
+ else if (kind === "google")
284
+ body = _jsx(GoogleShortcut, { text: text ?? "", name: name, onRetry: onRetry });
285
+ else if (kind === "sheet")
286
+ body = renderers.sheet ? (_jsx(Suspense, { fallback: _jsx(LoadingFile, {}), children: renderers.sheet({ name, bytes, onRetry }) })) : (unavailable);
287
+ else if (kind === "document" || kind === "slides")
288
+ body = renderers.office ? (_jsx(Suspense, { fallback: _jsx(LoadingFile, {}), children: renderers.office({ name, bytes, kind, onRetry }) })) : (unavailable);
289
+ else if (text !== null)
290
+ body = (_jsx(TextFile, { text: text, name: name, kind: kind, renderMarkdown: renderers.markdown }));
291
+ else
292
+ body = unavailable;
293
+ return (_jsx("div", { "data-slot": "file-content", "data-kind": kind, className: "sprout-file-content", children: body }));
294
+ }
295
+ export function LoadingFile({ className, ...props }) {
296
+ return (_jsx(StatePanel, { kind: "loading", title: "Opening file", description: "Preparing the document viewer.", "data-file-state": "loading", ...props, className: className }));
297
+ }
298
+ export function FileError({ message, onRetry, className, ...props }) {
299
+ return (_jsx(StatePanel, { kind: "error", title: "Could not open this file", description: message, "data-file-state": "error", ...props, className: className, children: _jsx(Button, { variant: "outline", onClick: onRetry, children: "Try again" }) }));
300
+ }
301
+ function codeLanguageFor(name, kind) {
302
+ return kind === "code" ? (languageForFilename(name) ?? "text") : kind;
303
+ }
304
+ const previewTextKinds = [
305
+ "text",
306
+ "code",
307
+ "json",
308
+ "markdown",
309
+ ];
310
+ const previewBytes = 16 * 1024;
311
+ const previewLines = 24;
312
+ /** Whether `FilePreview` can show this file: images, text, code, JSON, and Markdown, but not Google shortcuts. */
313
+ export function isPreviewableFile(file) {
314
+ if (file.source.kind === "remote" && googleDocument(file.source.url))
315
+ return false;
316
+ const kind = fileKind(file.name, file.mediaType);
317
+ return kind === "image" || previewTextKinds.includes(kind);
318
+ }
319
+ /**
320
+ * A small, non-interactive peek at a file: the image, or the first lines
321
+ * of text and code with syntax colors. Made for hover cards and thumbnails.
322
+ * Renders nothing for files `isPreviewableFile` rejects.
323
+ */
324
+ export function FilePreview({ file, className, ...props }) {
325
+ if (!isPreviewableFile(file))
326
+ return null;
327
+ const kind = fileKind(file.name, file.mediaType);
328
+ return (_jsx("div", { "data-slot": "file-preview", "data-kind": kind, ...props, className: cn("sprout-file-preview", className), children: kind === "image" && file.source.kind === "remote" ? (_jsx("img", { src: file.source.url, alt: file.name, className: "sprout-file-preview-image" })) : (_jsx(LoadedPreview, { file: file, kind: kind })) }));
329
+ }
330
+ function LoadedPreview({ file, kind, }) {
331
+ const loaded = useFileContent(file);
332
+ const text = useMemo(() => {
333
+ if (loaded.kind !== "ready" || kind === "image")
334
+ return null;
335
+ const head = new TextDecoder().decode(loaded.bytes.slice(0, previewBytes));
336
+ return head.split("\n").slice(0, previewLines).join("\n");
337
+ }, [loaded, kind]);
338
+ if (loaded.kind === "loading")
339
+ return (_jsxs("span", { className: "sprout-file-preview-state", children: [_jsx(Spinner, { size: "sm", "aria-hidden": "true" }), "Loading preview\u2026"] }));
340
+ if (loaded.kind === "error")
341
+ return (_jsx("span", { className: "sprout-file-preview-state", children: "Preview unavailable" }));
342
+ if (text === null)
343
+ return (_jsx("img", { src: loaded.url, alt: file.name, className: "sprout-file-preview-image" }));
344
+ if (!text.trim())
345
+ return (_jsx("span", { className: "sprout-file-preview-state", children: "This file is empty" }));
346
+ return (_jsx(CodeBlock, { code: text, language: codeLanguageFor(file.name, kind), variant: "preview", lineNumbers: false, wrap: kind === "text" || kind === "markdown" }));
347
+ }
348
+ function TextFile({ text, name, kind, renderMarkdown, }) {
349
+ const [source, setSource] = useState(false);
350
+ const json = useMemo(() => {
351
+ if (kind !== "json")
352
+ return null;
353
+ try {
354
+ return JSON.stringify(JSON.parse(text), null, 2);
355
+ }
356
+ catch {
357
+ return undefined;
358
+ }
359
+ }, [kind, text]);
360
+ const markdown = kind === "markdown" && renderMarkdown;
361
+ const prose = kind === "text" || kind === "markdown";
362
+ return (_jsxs("div", { className: "sprout-file-text", "data-format": markdown && !source ? "formatted" : "source", children: [markdown && (_jsx("div", { className: "sprout-file-text-bar", children: _jsx(Button, { variant: "outline", size: "sm", "aria-pressed": source, onClick: () => setSource((value) => !value), children: source ? "Show formatted" : "Show source" }) })), json === undefined && (_jsx("p", { role: "alert", className: "sprout-file-text-alert", children: "This file contains invalid JSON. The original text is shown below." })), markdown && !source ? (_jsx("div", { className: "sprout-file-text-scroll", tabIndex: 0, role: "region", "aria-label": name, children: _jsx("div", { className: "sprout-file-text-page", children: _jsx(Suspense, { fallback: _jsx(LoadingFile, {}), children: renderMarkdown({ text }) }) }) })) : (_jsx(CodeBlock, { code: json ?? text, language: codeLanguageFor(name, kind), variant: "file", wrap: prose }))] }));
363
+ }
364
+ function ImageFile({ url, name, onRetry, }) {
365
+ const [state, setState] = useState("loading");
366
+ const [zoom, setZoom] = useState(false);
367
+ if (state === "error")
368
+ return (_jsx(FileError, { message: "Your browser could not decode this image. Try a PNG, JPEG, GIF, WebP, AVIF, or BMP version.", onRetry: onRetry }));
369
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sprout-file-viewer-toolbar", "data-align": "center", children: _jsx(Button, { variant: "outline", size: "sm", "aria-pressed": zoom, onClick: () => setZoom((value) => !value), children: zoom ? "Fit image" : "Actual size" }) }), state === "loading" && _jsx(LoadingFile, {}), _jsx("div", { className: "sprout-file-image-stage", "data-zoom": zoom ? "actual" : "fit", tabIndex: zoom ? 0 : undefined, role: zoom ? "region" : undefined, "aria-label": zoom ? `${name} at actual size` : undefined, children: _jsx("img", { src: url, alt: name, onLoad: () => setState("ready"), onError: () => setState("error"), className: "sprout-file-image", "data-state": state }) })] }));
370
+ }
371
+ function GoogleShortcut({ text, name, onRetry, }) {
372
+ let document;
373
+ try {
374
+ document = googleDocument(googleShortcut(text));
375
+ }
376
+ catch (error) {
377
+ return (_jsx(FileError, { message: error instanceof Error
378
+ ? error.message
379
+ : "Invalid Google document shortcut.", onRetry: onRetry }));
380
+ }
381
+ if (!document)
382
+ return null;
383
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sprout-file-google-link", children: _jsx("a", { href: document.url, target: "_blank", rel: "noopener noreferrer", children: "Open in Google" }) }), _jsx(EmbeddedFile, { url: document.embedUrl, name: name, google: true, onRetry: onRetry })] }));
384
+ }
385
+ function EmbeddedFile({ url, name, google = false, onRetry, }) {
386
+ const [state, setState] = useState("loading");
387
+ const timer = useRef(null);
388
+ useEffect(() => {
389
+ timer.current = setTimeout(() => setState((value) => (value === "loading" ? "slow" : value)), 12000);
390
+ return () => {
391
+ if (timer.current)
392
+ clearTimeout(timer.current);
393
+ };
394
+ }, []);
395
+ return (_jsxs("div", { "data-slot": "file-embed", "data-state": state, className: "sprout-file-embed", children: [google && (_jsx("p", { className: "sprout-file-embed-notice", children: "Google controls access to this document. If it cannot open here, use Open in Google to sign in or request access." })), state === "loading" && (_jsx("div", { className: "sprout-file-embed-loading", children: _jsx(LoadingFile, {}) })), state === "slow" && (_jsxs("div", { className: "sprout-file-embed-slow", children: [_jsx("p", { role: "status", children: "The embedded viewer is taking longer than expected." }), _jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, children: "Reload viewer" })] })), _jsx("iframe", { title: name, src: url, className: "sprout-file-embed-frame", referrerPolicy: "no-referrer", onLoad: () => {
396
+ if (timer.current)
397
+ clearTimeout(timer.current);
398
+ setState("loaded");
399
+ }, onError: () => setState("slow"), allowFullScreen: true })] }));
400
+ }
@@ -0,0 +1,87 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import { type ButtonBlockerFix as ActionBlockerFix, type ButtonState as ActionControlState } from "./button.js";
3
+ export declare const formFooterLayoutNames: readonly ["bar", "stack", "compact"];
4
+ export declare const formSectionLevelNames: readonly [2, 3];
5
+ export type FormFooterLayout = (typeof formFooterLayoutNames)[number];
6
+ export type FormSectionLevel = (typeof formSectionLevelNames)[number];
7
+ export type FormBlockerFix = ActionBlockerFix;
8
+ /** `blocked` keeps the submit focusable and shows the reason, with an optional fix, under the buttons. */
9
+ export type FormActionState = ActionControlState;
10
+ export interface FormProps {
11
+ id?: string | undefined;
12
+ /** Locks every Field, ChoiceGroup, and CheckboxField inside. */
13
+ disabled?: boolean | undefined;
14
+ /** Return `"invalid"` after revealing errors; Form then focuses the first invalid control. */
15
+ onSubmit: () => "invalid" | void;
16
+ /** The form's accessible name. */
17
+ label?: string | undefined;
18
+ className?: string | undefined;
19
+ children: ReactNode;
20
+ }
21
+ /**
22
+ * A `noValidate` form that prevents the native submit, locks the fields inside
23
+ * while `disabled`, and moves focus to the first invalid control after a
24
+ * failed submit.
25
+ */
26
+ export declare function Form({ id, disabled, onSubmit, label, className, children, }: FormProps): import("react").JSX.Element;
27
+ export interface FormSectionProps {
28
+ title: string;
29
+ description?: string | undefined;
30
+ /** A short count or status badge beside the title, such as `3 / 20`. */
31
+ meta?: string | undefined;
32
+ /** Region name override; the title names the region otherwise. */
33
+ label?: string | undefined;
34
+ id?: string | undefined;
35
+ level?: FormSectionLevel | undefined;
36
+ className?: string | undefined;
37
+ children: ReactNode;
38
+ }
39
+ /** A bordered, named region of a form with a title, description, and optional meta badge. */
40
+ export declare function FormSection({ title, description, meta, label, id, level, className, children, }: FormSectionProps): import("react").JSX.Element;
41
+ export type FormCancel =
42
+ /** Shown only while the form is dirty and not busy. */
43
+ {
44
+ kind: "reset";
45
+ label: string;
46
+ onReset: () => void;
47
+ }
48
+ /** A link element, such as a router `<Link>`, rendered through Button `render`. */
49
+ | {
50
+ kind: "leave";
51
+ label: string;
52
+ link: ReactElement;
53
+ };
54
+ export interface FormFooterSubmit {
55
+ label: string;
56
+ pendingLabel: string;
57
+ state: FormActionState;
58
+ }
59
+ export interface FormFooterSecondarySubmit {
60
+ label: string;
61
+ disabled: boolean;
62
+ onSelect: () => void;
63
+ }
64
+ export interface FormFooterProps {
65
+ /** The id of the form to submit, for a footer rendered outside it. */
66
+ form?: string | undefined;
67
+ /** Replaces the unsaved-changes and note status on the left of `bar` and `compact`. */
68
+ summary?: ReactNode;
69
+ submit: FormFooterSubmit;
70
+ /** An outline submit beside the main one, such as Save draft. */
71
+ secondarySubmit?: FormFooterSecondarySubmit | undefined;
72
+ dirty: boolean;
73
+ cancel?: FormCancel | undefined;
74
+ note?: string | undefined;
75
+ error?: string | undefined;
76
+ /** `bar` sits under a divider, `stack` gives full-width large buttons, `compact` fits cards and chat. */
77
+ layout?: FormFooterLayout | undefined;
78
+ className?: string | undefined;
79
+ /** Extra actions beside the buttons in `compact`; feedback below the footer otherwise. */
80
+ children?: ReactNode;
81
+ }
82
+ /**
83
+ * The save area of a form: the submit with busy and blocked states, an
84
+ * optional secondary submit and cancel, an unsaved-changes line, a note, and
85
+ * an error alert.
86
+ */
87
+ export declare function FormFooter({ form, summary, submit, secondarySubmit, dirty, cancel, note, error, layout, className, children, }: FormFooterProps): import("react").JSX.Element;
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState, } from "react";
4
+ import { cn } from "../internal/classes.js";
5
+ import { Alert } from "./alert.js";
6
+ import { Badge } from "./badge.js";
7
+ import { Button, jumpTo, useActionControl, } from "./button.js";
8
+ import { FieldsDisabled } from "./field.js";
9
+ export const formFooterLayoutNames = ["bar", "stack", "compact"];
10
+ export const formSectionLevelNames = [2, 3];
11
+ /**
12
+ * A `noValidate` form that prevents the native submit, locks the fields inside
13
+ * while `disabled`, and moves focus to the first invalid control after a
14
+ * failed submit.
15
+ */
16
+ export function Form({ id, disabled = false, onSubmit, label, className, children, }) {
17
+ const formRef = useRef(null);
18
+ const [failedSubmits, setFailedSubmits] = useState(0);
19
+ useEffect(() => {
20
+ const invalid = failedSubmits > 0
21
+ ? formRef.current?.querySelector('[aria-invalid="true"]')
22
+ : null;
23
+ if (invalid)
24
+ jumpTo(invalid);
25
+ }, [failedSubmits]);
26
+ return (_jsx(FieldsDisabled, { value: disabled, children: _jsx("form", { id: id, ref: formRef, "data-slot": "form", noValidate: true, "aria-label": label, className: cn("sprout-form", className), onSubmit: (event) => {
27
+ event.preventDefault();
28
+ if (onSubmit() === "invalid")
29
+ setFailedSubmits((count) => count + 1);
30
+ }, children: children }) }));
31
+ }
32
+ /** A bordered, named region of a form with a title, description, and optional meta badge. */
33
+ export function FormSection({ title, description, meta, label, id, level = 2, className, children, }) {
34
+ const Heading = level === 3 ? "h3" : "h2";
35
+ return (_jsxs("section", { id: id, "data-slot": "form-section", "aria-label": label ?? title, className: cn("sprout-form-section", className), children: [_jsxs("div", { className: "sprout-form-section-header", children: [_jsxs("div", { className: "sprout-form-section-text", children: [_jsx(Heading, { className: "sprout-form-section-title", children: title }), description && (_jsx("p", { className: "sprout-form-section-description", children: description }))] }), meta && _jsx(Badge, { variant: "secondary", children: meta })] }), _jsx("div", { className: "sprout-form-section-content", children: children })] }));
36
+ }
37
+ /**
38
+ * The save area of a form: the submit with busy and blocked states, an
39
+ * optional secondary submit and cancel, an unsaved-changes line, a note, and
40
+ * an error alert.
41
+ */
42
+ export function FormFooter({ form, summary, submit, secondarySubmit, dirty, cancel, note, error, layout = "bar", className, children, }) {
43
+ const action = useActionControl(submit.state);
44
+ const stack = layout === "stack";
45
+ const compact = layout === "compact";
46
+ const size = stack ? "lg" : "default";
47
+ const wide = stack ? "sprout-form-footer-wide" : undefined;
48
+ const save = (_jsx(Button, { type: "submit", form: form, size: size, className: wide, ...action.button, children: submit.state.kind === "busy" ? submit.pendingLabel : submit.label }));
49
+ const secondary = secondarySubmit && (_jsx(Button, { type: "submit", form: form, size: size, variant: "outline", className: wide, disabled: secondarySubmit.disabled, onClick: secondarySubmit.onSelect, children: secondarySubmit.label }));
50
+ const cancelButton = !cancel ? null : cancel.kind === "leave" ? (_jsx(Button, { variant: "outline", size: size, className: wide, nativeButton: false, render: cancel.link, children: cancel.label })) : dirty && submit.state.kind !== "busy" ? (_jsx(Button, { variant: "outline", size: size, className: wide, onClick: cancel.onReset, children: cancel.label })) : null;
51
+ const status = (_jsxs(_Fragment, { children: [dirty && (_jsxs("p", { className: "sprout-form-footer-dirty", children: [_jsx("span", { "aria-hidden": "true", className: "sprout-form-footer-dirty-dot" }), "Unsaved changes"] })), note && _jsx("p", { className: "sprout-form-footer-note", children: note })] }));
52
+ return (_jsxs("div", { "data-slot": "form-footer", "data-layout": layout, className: cn("sprout-form-footer", className), children: [stack ? (_jsxs(_Fragment, { children: [status, _jsxs("div", { className: "sprout-form-footer-stack", children: [save, secondary, cancelButton] })] })) : (_jsxs("div", { className: "sprout-form-footer-row", children: [_jsx("div", { className: compact && !summary
53
+ ? "sprout-form-footer-sr-only"
54
+ : "sprout-form-footer-status", children: summary ?? status }), _jsxs("div", { className: "sprout-form-footer-actions", children: [compact && children, cancelButton, secondary, save] })] })), action.reason, error && _jsx(Alert, { variant: "destructive", children: error }), !compact && children] }));
55
+ }
@@ -0,0 +1,27 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ export interface GlassBannerProps extends Omit<ComponentProps<"div">, "title" | "children"> {
3
+ /** The headline on the glass. */
4
+ title: ReactNode;
5
+ /** A quieter line under the title. It also names the chip list. */
6
+ caption?: ReactNode;
7
+ /** `GlassChip` items. Up to three float around the art on wide banners. */
8
+ children: ReactNode;
9
+ }
10
+ /**
11
+ * A frosted showcase banner with draggable glass chips. The bubble art is
12
+ * hidden from assistive technology; the chips are announced as a list named
13
+ * by the caption, or by the title when there is no caption.
14
+ */
15
+ export declare function GlassBanner({ title, caption, className, children, ...props }: GlassBannerProps): import("react").JSX.Element;
16
+ export interface GlassChipProps extends Omit<ComponentProps<"li">, "children"> {
17
+ /** A 28px round badge before the text, such as a brand glyph or icon. */
18
+ icon?: ReactNode;
19
+ children: ReactNode;
20
+ }
21
+ /**
22
+ * A glass pill that can be dragged with a pointer and springs back on
23
+ * release, stretching with its speed. The drag is play only: chips are not
24
+ * controls and take no focus. With reduced motion the chip neither floats
25
+ * nor springs; it snaps home on release.
26
+ */
27
+ export declare function GlassChip({ icon, className, children, ref, ...props }: GlassChipProps): import("react").JSX.Element;