@tulipfarm/sprout 0.0.0-stage → 0.1.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 (306) 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 +78 -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-plan.d.ts +21 -0
  38. package/dist/components/chat-plan.js +35 -0
  39. package/dist/components/chat-sources.d.ts +64 -0
  40. package/dist/components/chat-sources.js +66 -0
  41. package/dist/components/checkbox.d.ts +16 -0
  42. package/dist/components/checkbox.js +17 -0
  43. package/dist/components/choice-list.d.ts +53 -0
  44. package/dist/components/choice-list.js +36 -0
  45. package/dist/components/code-block.d.ts +35 -0
  46. package/dist/components/code-block.js +95 -0
  47. package/dist/components/collapsible.d.ts +24 -0
  48. package/dist/components/collapsible.js +27 -0
  49. package/dist/components/combobox.d.ts +95 -0
  50. package/dist/components/combobox.js +106 -0
  51. package/dist/components/command.d.ts +94 -0
  52. package/dist/components/command.js +414 -0
  53. package/dist/components/context-menu.d.ts +57 -0
  54. package/dist/components/context-menu.js +60 -0
  55. package/dist/components/dialog.d.ts +60 -0
  56. package/dist/components/dialog.js +79 -0
  57. package/dist/components/direction.d.ts +6 -0
  58. package/dist/components/direction.js +6 -0
  59. package/dist/components/dropdown-menu.d.ts +62 -0
  60. package/dist/components/dropdown-menu.js +59 -0
  61. package/dist/components/field.d.ts +117 -0
  62. package/dist/components/field.js +150 -0
  63. package/dist/components/file-viewer.d.ts +120 -0
  64. package/dist/components/file-viewer.js +373 -0
  65. package/dist/components/form.d.ts +87 -0
  66. package/dist/components/form.js +55 -0
  67. package/dist/components/glass-banner.d.ts +27 -0
  68. package/dist/components/glass-banner.js +116 -0
  69. package/dist/components/gradient-banner.d.ts +37 -0
  70. package/dist/components/gradient-banner.js +43 -0
  71. package/dist/components/hover-card.d.ts +27 -0
  72. package/dist/components/hover-card.js +26 -0
  73. package/dist/components/input-group.d.ts +68 -0
  74. package/dist/components/input-group.js +88 -0
  75. package/dist/components/input-otp.d.ts +38 -0
  76. package/dist/components/input-otp.js +43 -0
  77. package/dist/components/input.d.ts +16 -0
  78. package/dist/components/input.js +18 -0
  79. package/dist/components/kbd.d.ts +12 -0
  80. package/dist/components/kbd.js +11 -0
  81. package/dist/components/label.d.ts +8 -0
  82. package/dist/components/label.js +10 -0
  83. package/dist/components/mention-list.d.ts +54 -0
  84. package/dist/components/mention-list.js +31 -0
  85. package/dist/components/menubar.d.ts +64 -0
  86. package/dist/components/menubar.js +68 -0
  87. package/dist/components/message-actions.d.ts +34 -0
  88. package/dist/components/message-actions.js +68 -0
  89. package/dist/components/message-response.d.ts +20 -0
  90. package/dist/components/message-response.js +46 -0
  91. package/dist/components/message-scroller.d.ts +71 -0
  92. package/dist/components/message-scroller.js +217 -0
  93. package/dist/components/message.d.ts +49 -0
  94. package/dist/components/message.js +59 -0
  95. package/dist/components/navigation-menu.d.ts +45 -0
  96. package/dist/components/navigation-menu.js +54 -0
  97. package/dist/components/notice-banner.d.ts +14 -0
  98. package/dist/components/notice-banner.js +11 -0
  99. package/dist/components/page-content.d.ts +7 -0
  100. package/dist/components/page-content.js +9 -0
  101. package/dist/components/page-header.d.ts +37 -0
  102. package/dist/components/page-header.js +53 -0
  103. package/dist/components/pagination.d.ts +29 -0
  104. package/dist/components/pagination.js +29 -0
  105. package/dist/components/plan-outline.d.ts +60 -0
  106. package/dist/components/plan-outline.js +108 -0
  107. package/dist/components/popover.d.ts +34 -0
  108. package/dist/components/popover.js +35 -0
  109. package/dist/components/progress.d.ts +27 -0
  110. package/dist/components/progress.js +35 -0
  111. package/dist/components/prompt-list.d.ts +23 -0
  112. package/dist/components/prompt-list.js +11 -0
  113. package/dist/components/questionnaire.d.ts +148 -0
  114. package/dist/components/questionnaire.js +1001 -0
  115. package/dist/components/radio-group.d.ts +16 -0
  116. package/dist/components/radio-group.js +21 -0
  117. package/dist/components/reasoning-effort.d.ts +24 -0
  118. package/dist/components/reasoning-effort.js +269 -0
  119. package/dist/components/resizable.d.ts +67 -0
  120. package/dist/components/resizable.js +536 -0
  121. package/dist/components/result-card.d.ts +58 -0
  122. package/dist/components/result-card.js +88 -0
  123. package/dist/components/review-card.d.ts +62 -0
  124. package/dist/components/review-card.js +49 -0
  125. package/dist/components/rich-text-editor.d.ts +51 -0
  126. package/dist/components/rich-text-editor.js +248 -0
  127. package/dist/components/schema-diagram.d.ts +52 -0
  128. package/dist/components/schema-diagram.js +226 -0
  129. package/dist/components/scroll-area.d.ts +22 -0
  130. package/dist/components/scroll-area.js +19 -0
  131. package/dist/components/search-dialog.d.ts +103 -0
  132. package/dist/components/search-dialog.js +64 -0
  133. package/dist/components/search-field.d.ts +17 -0
  134. package/dist/components/search-field.js +27 -0
  135. package/dist/components/select.d.ts +57 -0
  136. package/dist/components/select.js +57 -0
  137. package/dist/components/selection-slot.d.ts +18 -0
  138. package/dist/components/selection-slot.js +53 -0
  139. package/dist/components/separator.d.ts +48 -0
  140. package/dist/components/separator.js +54 -0
  141. package/dist/components/settings-list.d.ts +26 -0
  142. package/dist/components/settings-list.js +17 -0
  143. package/dist/components/sheet.d.ts +70 -0
  144. package/dist/components/sheet.js +77 -0
  145. package/dist/components/sidebar.d.ts +70 -0
  146. package/dist/components/sidebar.js +187 -0
  147. package/dist/components/skeleton.d.ts +31 -0
  148. package/dist/components/skeleton.js +32 -0
  149. package/dist/components/slider.d.ts +17 -0
  150. package/dist/components/slider.js +24 -0
  151. package/dist/components/spinner.d.ts +37 -0
  152. package/dist/components/spinner.js +43 -0
  153. package/dist/components/state-panel-samples.d.ts +25 -0
  154. package/dist/components/state-panel-samples.js +423 -0
  155. package/dist/components/state-panel.d.ts +35 -0
  156. package/dist/components/state-panel.js +29 -0
  157. package/dist/components/state-sample.d.ts +63 -0
  158. package/dist/components/state-sample.js +42 -0
  159. package/dist/components/switch.d.ts +12 -0
  160. package/dist/components/switch.js +14 -0
  161. package/dist/components/table-nuqs.d.ts +16 -0
  162. package/dist/components/table-nuqs.js +59 -0
  163. package/dist/components/table.d.ts +120 -0
  164. package/dist/components/table.js +663 -0
  165. package/dist/components/tabs.d.ts +37 -0
  166. package/dist/components/tabs.js +36 -0
  167. package/dist/components/textarea.d.ts +18 -0
  168. package/dist/components/textarea.js +19 -0
  169. package/dist/components/toast.d.ts +45 -0
  170. package/dist/components/toast.js +83 -0
  171. package/dist/components/toggle-group.d.ts +30 -0
  172. package/dist/components/toggle-group.js +39 -0
  173. package/dist/components/toggle.d.ts +27 -0
  174. package/dist/components/toggle.js +21 -0
  175. package/dist/components/tool-activity.d.ts +79 -0
  176. package/dist/components/tool-activity.js +226 -0
  177. package/dist/components/toolbar-status.d.ts +21 -0
  178. package/dist/components/toolbar-status.js +14 -0
  179. package/dist/components/tooltip.d.ts +31 -0
  180. package/dist/components/tooltip.js +99 -0
  181. package/dist/components/typed-text.d.ts +17 -0
  182. package/dist/components/typed-text.js +56 -0
  183. package/dist/index.d.ts +88 -0
  184. package/dist/index.js +88 -0
  185. package/dist/internal/brand-glyphs.d.ts +342 -0
  186. package/dist/internal/brand-glyphs.js +238 -0
  187. package/dist/internal/classes.d.ts +3 -0
  188. package/dist/internal/classes.js +20 -0
  189. package/dist/internal/fieldset.d.ts +6 -0
  190. package/dist/internal/fieldset.js +38 -0
  191. package/dist/internal/file-kind.d.ts +4 -0
  192. package/dist/internal/file-kind.js +62 -0
  193. package/dist/internal/languages.d.ts +7 -0
  194. package/dist/internal/languages.js +1010 -0
  195. package/dist/internal/message-scroller.d.ts +102 -0
  196. package/dist/internal/message-scroller.js +637 -0
  197. package/dist/internal/pressable.d.ts +2 -0
  198. package/dist/internal/pressable.js +29 -0
  199. package/dist/internal/syntax.d.ts +14 -0
  200. package/dist/internal/syntax.js +137 -0
  201. package/dist/styles/accordion.css +102 -0
  202. package/dist/styles/alert-dialog.css +198 -0
  203. package/dist/styles/alert.css +149 -0
  204. package/dist/styles/aspect-ratio.css +8 -0
  205. package/dist/styles/attachment.css +720 -0
  206. package/dist/styles/avatar.css +186 -0
  207. package/dist/styles/badge.css +178 -0
  208. package/dist/styles/brand-icon.css +104 -0
  209. package/dist/styles/breadcrumb.css +121 -0
  210. package/dist/styles/button-group.css +113 -0
  211. package/dist/styles/button.css +380 -0
  212. package/dist/styles/calendar.css +295 -0
  213. package/dist/styles/canvas.css +327 -0
  214. package/dist/styles/card.css +146 -0
  215. package/dist/styles/carousel.css +107 -0
  216. package/dist/styles/chart.css +227 -0
  217. package/dist/styles/chat-confirmation.css +29 -0
  218. package/dist/styles/chat-plan.css +109 -0
  219. package/dist/styles/chat-sources.css +191 -0
  220. package/dist/styles/checkbox.css +123 -0
  221. package/dist/styles/choice-list.css +343 -0
  222. package/dist/styles/code-block.css +211 -0
  223. package/dist/styles/collapsible.css +72 -0
  224. package/dist/styles/combobox.css +347 -0
  225. package/dist/styles/command.css +233 -0
  226. package/dist/styles/context-menu.css +151 -0
  227. package/dist/styles/dialog.css +237 -0
  228. package/dist/styles/dropdown-menu.css +150 -0
  229. package/dist/styles/field.css +341 -0
  230. package/dist/styles/file-viewer.css +278 -0
  231. package/dist/styles/form.css +155 -0
  232. package/dist/styles/glass-banner.css +308 -0
  233. package/dist/styles/gradient-banner.css +256 -0
  234. package/dist/styles/hover-card.css +95 -0
  235. package/dist/styles/index.css +262 -0
  236. package/dist/styles/input-group.css +290 -0
  237. package/dist/styles/input-otp.css +138 -0
  238. package/dist/styles/input.css +99 -0
  239. package/dist/styles/kbd.css +59 -0
  240. package/dist/styles/label.css +89 -0
  241. package/dist/styles/mention-list.css +166 -0
  242. package/dist/styles/menubar.css +226 -0
  243. package/dist/styles/message-actions.css +83 -0
  244. package/dist/styles/message-response.css +176 -0
  245. package/dist/styles/message-scroller.css +136 -0
  246. package/dist/styles/message.css +310 -0
  247. package/dist/styles/navigation-menu.css +291 -0
  248. package/dist/styles/notice-banner.css +68 -0
  249. package/dist/styles/page-content.css +21 -0
  250. package/dist/styles/page-header.css +156 -0
  251. package/dist/styles/pagination.css +72 -0
  252. package/dist/styles/plan-outline.css +393 -0
  253. package/dist/styles/popover.css +140 -0
  254. package/dist/styles/progress.css +93 -0
  255. package/dist/styles/prompt-list.css +165 -0
  256. package/dist/styles/questionnaire.css +323 -0
  257. package/dist/styles/radio-group.css +120 -0
  258. package/dist/styles/reasoning-effort.css +157 -0
  259. package/dist/styles/resizable.css +108 -0
  260. package/dist/styles/result-card.css +215 -0
  261. package/dist/styles/review-card.css +235 -0
  262. package/dist/styles/rich-text-editor.css +169 -0
  263. package/dist/styles/schema-diagram.css +419 -0
  264. package/dist/styles/scroll-area.css +77 -0
  265. package/dist/styles/search-dialog.css +363 -0
  266. package/dist/styles/search-field.css +17 -0
  267. package/dist/styles/select.css +285 -0
  268. package/dist/styles/selection-slot.css +31 -0
  269. package/dist/styles/separator.css +206 -0
  270. package/dist/styles/settings-list.css +81 -0
  271. package/dist/styles/sheet.css +361 -0
  272. package/dist/styles/sidebar.css +363 -0
  273. package/dist/styles/skeleton.css +80 -0
  274. package/dist/styles/slider.css +142 -0
  275. package/dist/styles/spinner.css +39 -0
  276. package/dist/styles/state-panel-samples.css +41 -0
  277. package/dist/styles/state-panel.css +182 -0
  278. package/dist/styles/state-sample.css +285 -0
  279. package/dist/styles/switch.css +128 -0
  280. package/dist/styles/table.css +604 -0
  281. package/dist/styles/tabs.css +223 -0
  282. package/dist/styles/textarea.css +78 -0
  283. package/dist/styles/themes/chatgpt.css +125 -0
  284. package/dist/styles/themes/claude.css +125 -0
  285. package/dist/styles/themes/cloudflare.css +125 -0
  286. package/dist/styles/themes/discord.css +125 -0
  287. package/dist/styles/themes/github.css +125 -0
  288. package/dist/styles/themes/grok.css +125 -0
  289. package/dist/styles/themes/linear.css +126 -0
  290. package/dist/styles/themes/neutral.css +125 -0
  291. package/dist/styles/themes/notion.css +125 -0
  292. package/dist/styles/themes/perplexity.css +125 -0
  293. package/dist/styles/themes/raycast.css +125 -0
  294. package/dist/styles/themes/spotify.css +125 -0
  295. package/dist/styles/themes/stripe.css +125 -0
  296. package/dist/styles/themes/supabase.css +125 -0
  297. package/dist/styles/themes/tulipfarm.css +125 -0
  298. package/dist/styles/themes/vercel.css +126 -0
  299. package/dist/styles/toast.css +197 -0
  300. package/dist/styles/toggle-group.css +161 -0
  301. package/dist/styles/toggle.css +119 -0
  302. package/dist/styles/tool-activity.css +396 -0
  303. package/dist/styles/toolbar-status.css +52 -0
  304. package/dist/styles/tooltip.css +122 -0
  305. package/dist/styles/typed-text.css +17 -0
  306. package/package.json +443 -4
@@ -0,0 +1,1001 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { mergeProps } from "@base-ui/react/merge-props";
4
+ import { useRender } from "@base-ui/react/use-render";
5
+ import { createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState, } from "react";
6
+ import { Check } from "reicon-react/icons/Check";
7
+ import { cn } from "../internal/classes.js";
8
+ import { buttonVariants, } from "./button.js";
9
+ export const questionnaireItemStatusNames = [
10
+ "unanswered",
11
+ "answered",
12
+ "skipped",
13
+ ];
14
+ export const questionnaireShortcutModeNames = ["letters", "numbers"];
15
+ function hasText(value) {
16
+ return Array.isArray(value)
17
+ ? value.some((part) => String(part).trim().length > 0)
18
+ : value != null && String(value).trim().length > 0;
19
+ }
20
+ function shortcutKeys(mode) {
21
+ if (mode === "letters")
22
+ return Array.from({ length: 26 }, (_, index) => String.fromCharCode(65 + index));
23
+ if (mode === "numbers")
24
+ return Array.from({ length: 9 }, (_, index) => String(index + 1));
25
+ return [];
26
+ }
27
+ function shortcutFromKey(key, mode) {
28
+ const normalized = mode === "letters" ? key.toUpperCase() : key;
29
+ return shortcutKeys(mode).includes(normalized) ? normalized : null;
30
+ }
31
+ function keyShortcuts(shortcut, enter) {
32
+ return ([shortcut, enter ? "Enter" : null].filter(Boolean).join(" ") || undefined);
33
+ }
34
+ function isAnswered(control) {
35
+ return control.type === "choice"
36
+ ? control.element.checked
37
+ : control.element.hasAttribute("name") && hasText(control.element.value);
38
+ }
39
+ function isEmptyTextInput(control) {
40
+ return (control?.type === "input" &&
41
+ ["email", "password", "search", "tel", "text", "url"].includes(control.element.type) &&
42
+ !hasText(control.element.value));
43
+ }
44
+ function isTextEntry(element) {
45
+ if (element instanceof HTMLTextAreaElement ||
46
+ element instanceof HTMLSelectElement)
47
+ return true;
48
+ if (element instanceof HTMLInputElement)
49
+ return !["button", "checkbox", "radio", "reset", "submit"].includes(element.type);
50
+ return element instanceof HTMLElement && element.isContentEditable;
51
+ }
52
+ function isRadio(element) {
53
+ return element instanceof HTMLInputElement && element.type === "radio";
54
+ }
55
+ function byDocumentOrder(a, b) {
56
+ if (a.element === b.element)
57
+ return 0;
58
+ const position = a.element.compareDocumentPosition(b.element);
59
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING)
60
+ return -1;
61
+ if (position & Node.DOCUMENT_POSITION_PRECEDING)
62
+ return 1;
63
+ return 0;
64
+ }
65
+ function assignRef(ref, value) {
66
+ if (typeof ref === "function")
67
+ ref(value);
68
+ else if (ref)
69
+ ref.current = value;
70
+ }
71
+ function initialItem(definitions, defaultItem) {
72
+ if (!definitions)
73
+ return defaultItem ?? null;
74
+ const match = defaultItem
75
+ ? definitions.itemByName.get(defaultItem)
76
+ : undefined;
77
+ return match && !match.disabled
78
+ ? match.name
79
+ : (definitions.enabledItems[0]?.name ?? null);
80
+ }
81
+ function shortcutsForDefinition(definition, mode) {
82
+ const map = new Map();
83
+ if (!definition || !mode)
84
+ return map;
85
+ const keys = shortcutKeys(mode);
86
+ let index = 0;
87
+ for (const choice of definition.choices ?? []) {
88
+ if (choice.disabled)
89
+ continue;
90
+ const key = keys[index];
91
+ if (!key)
92
+ break;
93
+ map.set(choice.value, key);
94
+ index += 1;
95
+ }
96
+ return map;
97
+ }
98
+ const RootContext = createContext(null);
99
+ const ItemContext = createContext(null);
100
+ function useRoot(part) {
101
+ const context = useContext(RootContext);
102
+ if (!context)
103
+ throw new Error(`${part} must be used within a Questionnaire.`);
104
+ return context;
105
+ }
106
+ function useItem(part) {
107
+ const context = useContext(ItemContext);
108
+ if (!context)
109
+ throw new Error(`${part} must be used within a QuestionnaireItem.`);
110
+ return context;
111
+ }
112
+ /**
113
+ * A form that asks one question at a time. It moves between items with
114
+ * Next, Previous, and Skip, checks required answers before moving on, and
115
+ * submits after the last item.
116
+ */
117
+ export function Questionnaire({ className, defaultItem, item, items, noValidate = true, onItemChange, onReset, onSubmit, ref, shortcuts, ...props }) {
118
+ const definitions = useMemo(() => items === undefined
119
+ ? null
120
+ : {
121
+ items,
122
+ enabledItems: items.filter((entry) => !entry.disabled),
123
+ itemByName: new Map(items.map((entry) => [entry.name, entry])),
124
+ }, [items]);
125
+ const [registered, setRegistered] = useState([]);
126
+ const [internalItem, setInternalItem] = useState(() => initialItem(definitions, defaultItem));
127
+ const [form, setForm] = useState(null);
128
+ const [domVersion, setDomVersion] = useState(0);
129
+ const pending = useRef(null);
130
+ const controlled = item !== undefined;
131
+ const activeName = controlled ? item : internalItem;
132
+ const previousActive = useRef(activeName);
133
+ const nativeValidation = noValidate === false;
134
+ const shortcutMode = shortcuts ?? null;
135
+ useLayoutEffect(() => {
136
+ if (!form || typeof MutationObserver === "undefined")
137
+ return;
138
+ const observer = new MutationObserver(() => setDomVersion((version) => version + 1));
139
+ observer.observe(form, { childList: true, subtree: true });
140
+ return () => observer.disconnect();
141
+ }, [form]);
142
+ const enabledRegistered = useMemo(() => registered.filter((entry) => !entry.disabled).sort(byDocumentOrder),
143
+ // eslint-disable-next-line react-hooks/exhaustive-deps
144
+ [registered, domVersion]);
145
+ const registeredByName = useMemo(() => new Map(enabledRegistered.map((entry) => [entry.name, entry])), [enabledRegistered]);
146
+ const sequence = definitions?.enabledItems ?? enabledRegistered;
147
+ const index = sequence.findIndex((entry) => entry.name === activeName);
148
+ const active = index < 0 || !activeName
149
+ ? null
150
+ : (registeredByName.get(activeName) ?? null);
151
+ const activeDefinition = activeName
152
+ ? definitions?.itemByName.get(activeName)
153
+ : undefined;
154
+ const activeRequired = index < 0
155
+ ? null
156
+ : activeDefinition
157
+ ? !!activeDefinition.required
158
+ : (active?.required ?? false);
159
+ const activeStatus = index < 0 ? null : (active?.status ?? (activeName ? "unanswered" : null));
160
+ const ordered = useMemo(() => definitions
161
+ ? definitions.enabledItems.flatMap((entry) => {
162
+ const match = registeredByName.get(entry.name);
163
+ return match ? [match] : [];
164
+ })
165
+ : enabledRegistered, [definitions, registeredByName, enabledRegistered]);
166
+ const total = sequence.length;
167
+ const current = index < 0 ? 0 : index + 1;
168
+ const first = total > 0 && index === 0;
169
+ const last = total > 0 && index === total - 1;
170
+ const go = useCallback((name, target = "item") => {
171
+ if (name === activeName)
172
+ return;
173
+ pending.current = { name, target };
174
+ if (!controlled)
175
+ setInternalItem(name);
176
+ onItemChange?.(name);
177
+ }, [activeName, controlled, onItemChange]);
178
+ useLayoutEffect(() => {
179
+ if (total === 0)
180
+ return;
181
+ if (index < 0) {
182
+ if (!controlled && activeName === null) {
183
+ setInternalItem(sequence[0].name);
184
+ return;
185
+ }
186
+ go(sequence[0].name);
187
+ return;
188
+ }
189
+ const request = pending.current;
190
+ const changed = previousActive.current !== activeName;
191
+ previousActive.current = activeName;
192
+ if (!request || request.name !== activeName) {
193
+ if (controlled && changed) {
194
+ pending.current = null;
195
+ active?.focus();
196
+ }
197
+ return;
198
+ }
199
+ if (request.target === "invalid")
200
+ active?.focusInvalid();
201
+ else
202
+ active?.focus();
203
+ pending.current = null;
204
+ }, [active, activeName, controlled, index, sequence, go, total]);
205
+ const registerItem = useCallback((entry) => {
206
+ setRegistered((list) => [
207
+ ...list.filter((other) => other.element !== entry.element && other.name !== entry.name),
208
+ entry,
209
+ ]);
210
+ return () => setRegistered((list) => list.filter((other) => other !== entry));
211
+ }, []);
212
+ const goPrevious = useCallback(() => {
213
+ if (index <= 0)
214
+ return;
215
+ go(sequence[index - 1].name);
216
+ }, [index, sequence, go]);
217
+ const goNext = useCallback(() => {
218
+ if (!active || index >= total - 1)
219
+ return;
220
+ if (!active.validate()) {
221
+ active.focusInvalid();
222
+ return;
223
+ }
224
+ go(sequence[index + 1].name);
225
+ }, [active, index, sequence, go, total]);
226
+ const advance = useCallback(() => {
227
+ if (!active)
228
+ return;
229
+ if (!active.validate()) {
230
+ active.focusInvalid();
231
+ return;
232
+ }
233
+ if (last) {
234
+ form?.requestSubmit();
235
+ return;
236
+ }
237
+ go(sequence[index + 1].name);
238
+ }, [active, index, last, sequence, form, go]);
239
+ const skipCurrent = useCallback(() => {
240
+ if (!active || active.required)
241
+ return;
242
+ active.skip();
243
+ if (!last) {
244
+ go(sequence[index + 1].name);
245
+ return;
246
+ }
247
+ queueMicrotask(() => form?.requestSubmit());
248
+ }, [active, index, last, sequence, form, go]);
249
+ function handleReset(event) {
250
+ onReset?.(event);
251
+ if (event.defaultPrevented)
252
+ return;
253
+ for (const entry of registered)
254
+ entry.reset();
255
+ const target = definitions
256
+ ? initialItem(definitions, defaultItem)
257
+ : (enabledRegistered.find((entry) => entry.name === defaultItem)?.name ??
258
+ enabledRegistered[0]?.name);
259
+ if (target)
260
+ go(target);
261
+ }
262
+ function handleSubmit(event) {
263
+ const invalid = ordered.find((entry) => !entry.validate());
264
+ if (invalid) {
265
+ event.preventDefault();
266
+ go(invalid.name, "invalid");
267
+ if (invalid.name === activeName) {
268
+ invalid.focusInvalid();
269
+ pending.current = null;
270
+ }
271
+ return;
272
+ }
273
+ onSubmit?.(event);
274
+ }
275
+ function handleKeyDown(event) {
276
+ const target = event.target;
277
+ if (event.defaultPrevented ||
278
+ event.nativeEvent.isComposing ||
279
+ event.keyCode === 229 ||
280
+ !active ||
281
+ !(target instanceof Element))
282
+ return;
283
+ if (event.key === "Enter" &&
284
+ (event.metaKey || event.ctrlKey) &&
285
+ !event.altKey &&
286
+ !event.shiftKey) {
287
+ event.preventDefault();
288
+ if (!event.repeat)
289
+ advance();
290
+ return;
291
+ }
292
+ if (event.metaKey || event.ctrlKey || event.altKey)
293
+ return;
294
+ if ((event.key === "ArrowUp" || event.key === "ArrowDown") &&
295
+ active.moveAnswerFocus(target, event.key === "ArrowDown" ? "next" : "previous")) {
296
+ event.preventDefault();
297
+ return;
298
+ }
299
+ if ((event.key === "ArrowLeft" || event.key === "ArrowRight") &&
300
+ !isTextEntry(target) &&
301
+ !isRadio(target)) {
302
+ event.preventDefault();
303
+ if (event.repeat)
304
+ return;
305
+ if (event.key === "ArrowLeft")
306
+ goPrevious();
307
+ else if (active.status !== "unanswered")
308
+ goNext();
309
+ return;
310
+ }
311
+ if (event.key === "Enter") {
312
+ const control = active.getAnswerByElement(target);
313
+ if (!control)
314
+ return;
315
+ event.preventDefault();
316
+ if (!event.repeat && isAnswered(control))
317
+ advance();
318
+ return;
319
+ }
320
+ if (!shortcutMode || isTextEntry(target))
321
+ return;
322
+ const key = shortcutFromKey(event.key, shortcutMode);
323
+ const control = key ? active.getAnswerByShortcut(key) : null;
324
+ if (!control)
325
+ return;
326
+ event.preventDefault();
327
+ if (event.repeat)
328
+ return;
329
+ control.element.focus();
330
+ if (control.type === "choice")
331
+ control.element.click();
332
+ }
333
+ const context = useMemo(() => ({
334
+ current,
335
+ first,
336
+ last,
337
+ total,
338
+ activeItemName: activeName,
339
+ activeItemRequired: activeRequired,
340
+ activeItemStatus: activeStatus,
341
+ domVersion,
342
+ goNext: advance,
343
+ goPrevious,
344
+ skipCurrent,
345
+ itemDefinitionByName: definitions?.itemByName ?? null,
346
+ nativeValidation,
347
+ registerItem,
348
+ shortcuts: shortcutMode,
349
+ }), [
350
+ current,
351
+ first,
352
+ last,
353
+ total,
354
+ activeName,
355
+ activeRequired,
356
+ activeStatus,
357
+ domVersion,
358
+ advance,
359
+ goPrevious,
360
+ skipCurrent,
361
+ definitions,
362
+ nativeValidation,
363
+ registerItem,
364
+ shortcutMode,
365
+ ]);
366
+ const handleRef = useCallback((element) => {
367
+ setForm(element);
368
+ assignRef(ref, element);
369
+ }, [ref]);
370
+ const element = useRender({
371
+ defaultTagName: "form",
372
+ props: mergeProps({
373
+ "data-slot": "questionnaire",
374
+ "data-shortcuts": shortcutMode ?? undefined,
375
+ className: cn("sprout-questionnaire", className),
376
+ noValidate,
377
+ onKeyDown: handleKeyDown,
378
+ onReset: handleReset,
379
+ onSubmit: handleSubmit,
380
+ ref: handleRef,
381
+ }, props),
382
+ state: { current, first, last, total },
383
+ });
384
+ return _jsx(RootContext.Provider, { value: context, children: element });
385
+ }
386
+ /** "Question 2 of 3", announced politely as the step changes. */
387
+ export function QuestionnaireProgress({ children, className, render, ...props }) {
388
+ const { current, first, last, total } = useRoot("QuestionnaireProgress");
389
+ const text = total ? `Question ${current} of ${total}` : undefined;
390
+ return useRender({
391
+ defaultTagName: "div",
392
+ render,
393
+ props: mergeProps({
394
+ "data-slot": "questionnaire-progress",
395
+ className: cn("sprout-questionnaire-progress", className),
396
+ role: "progressbar",
397
+ "aria-label": "Questionnaire progress",
398
+ "aria-live": "polite",
399
+ "aria-valuemin": total ? 1 : undefined,
400
+ "aria-valuemax": total || undefined,
401
+ "aria-valuenow": total ? current : undefined,
402
+ "aria-valuetext": text,
403
+ children: children ?? text,
404
+ }, props),
405
+ state: { current, first, last, total },
406
+ });
407
+ }
408
+ /** One question. Only the current item is shown; the rest are hidden and inert. */
409
+ export function QuestionnaireItem({ "aria-describedby": ariaDescribedBy, "aria-keyshortcuts": ariaKeyShortcuts, children, className, disabled = false, invalid = false, multiple = false, name, onStatusChange, ref, required = false, ...props }) {
410
+ const root = useRoot("QuestionnaireItem");
411
+ const { activeItemName, domVersion, first, last, itemDefinitionByName, nativeValidation, registerItem, shortcuts, } = root;
412
+ const [element, setElement] = useState(null);
413
+ const [controls, setControls] = useState([]);
414
+ const [attempted, setAttempted] = useState(false);
415
+ const [selected, setSelected] = useState([]);
416
+ const [skipped, setSkipped] = useState(false);
417
+ const [resetVersion, setResetVersion] = useState(0);
418
+ const [descriptionIds, setDescriptionIds] = useState([]);
419
+ const [errorIds, setErrorIds] = useState([]);
420
+ const defaults = useRef([]);
421
+ const multipleRef = useRef(multiple);
422
+ const previousMultiple = useRef(multiple);
423
+ multipleRef.current = multiple;
424
+ const active = !disabled && activeItemName === name;
425
+ const sorted = useMemo(() => [...controls].sort(byDocumentOrder),
426
+ // eslint-disable-next-line react-hooks/exhaustive-deps
427
+ [controls, domVersion]);
428
+ const enabled = useMemo(() => sorted.filter((control) => !control.disabled), [sorted]);
429
+ const answered = enabled.some((control) => selected.includes(control.id));
430
+ const status = skipped
431
+ ? "skipped"
432
+ : answered
433
+ ? "answered"
434
+ : "unanswered";
435
+ const skippedOptional = status === "skipped" && !required;
436
+ const valid = disabled || skippedOptional || (!invalid && status === "answered");
437
+ const showInvalid = !disabled && !skippedOptional && (invalid || (attempted && !valid));
438
+ const hasInputAnswer = enabled.some((control) => control.type === "input");
439
+ const lastStatus = useRef(status);
440
+ const definition = itemDefinitionByName?.get(name);
441
+ const shortcutByChoiceValue = useMemo(() => itemDefinitionByName
442
+ ? shortcutsForDefinition(definition, shortcuts)
443
+ : null, [definition, itemDefinitionByName, shortcuts]);
444
+ const shortcutByAnswerId = useMemo(() => {
445
+ if (shortcutByChoiceValue)
446
+ return new Map();
447
+ const keys = shortcutKeys(shortcuts);
448
+ const choices = enabled.filter((control) => control.type === "choice");
449
+ return new Map(choices
450
+ .slice(0, keys.length)
451
+ .map((control, position) => [control.id, keys[position]]));
452
+ }, [enabled, shortcutByChoiceValue, shortcuts]);
453
+ useLayoutEffect(() => {
454
+ if (lastStatus.current === status)
455
+ return;
456
+ lastStatus.current = status;
457
+ onStatusChange?.(status);
458
+ }, [onStatusChange, status]);
459
+ const registerAnswerControl = useCallback((control) => {
460
+ setControls((list) => [
461
+ ...list.filter((other) => other.element !== control.element && other.id !== control.id),
462
+ control,
463
+ ]);
464
+ return () => setControls((list) => list.filter((other) => other !== control));
465
+ }, []);
466
+ const select = useCallback((id, on) => {
467
+ setSelected((list) => {
468
+ if (!on)
469
+ return list.filter((other) => other !== id);
470
+ if (!multiple)
471
+ return [id];
472
+ return list.includes(id) ? list : [...list, id];
473
+ });
474
+ }, [multiple]);
475
+ const setAnswerSelectionFromInteraction = useCallback((id, on) => {
476
+ setSkipped(false);
477
+ select(id, on);
478
+ }, [select]);
479
+ const syncControlledAnswerSelection = useCallback((id, on) => {
480
+ if (on)
481
+ setSkipped(false);
482
+ select(id, on);
483
+ }, [select]);
484
+ const registerAnswerSelection = useCallback((id, initial) => {
485
+ if (initial) {
486
+ defaults.current = [
487
+ ...defaults.current.filter((other) => other !== id),
488
+ id,
489
+ ];
490
+ setSelected((list) => {
491
+ if (multipleRef.current)
492
+ return list.includes(id) ? list : [...list, id];
493
+ return list.length ? list : [id];
494
+ });
495
+ }
496
+ return () => {
497
+ defaults.current = defaults.current.filter((other) => other !== id);
498
+ setSelected((list) => list.filter((other) => other !== id));
499
+ };
500
+ }, []);
501
+ const setAnswerDefault = useCallback((id, on) => {
502
+ if (on) {
503
+ if (!defaults.current.includes(id))
504
+ defaults.current = [...defaults.current, id];
505
+ return;
506
+ }
507
+ defaults.current = defaults.current.filter((other) => other !== id);
508
+ }, []);
509
+ const registerDescription = useCallback((id) => {
510
+ setDescriptionIds((list) => (list.includes(id) ? list : [...list, id]));
511
+ return () => setDescriptionIds((list) => list.filter((other) => other !== id));
512
+ }, []);
513
+ const registerError = useCallback((id) => {
514
+ setErrorIds((list) => (list.includes(id) ? list : [...list, id]));
515
+ return () => setErrorIds((list) => list.filter((other) => other !== id));
516
+ }, []);
517
+ const validate = useCallback(() => {
518
+ setAttempted(true);
519
+ if (!valid)
520
+ return false;
521
+ if (!nativeValidation)
522
+ return true;
523
+ const failing = enabled.find((control) => isAnswered(control) &&
524
+ control.element.willValidate &&
525
+ !control.element.validity.valid);
526
+ if (!failing)
527
+ return true;
528
+ failing.element.focus();
529
+ failing.element.reportValidity();
530
+ return false;
531
+ }, [enabled, nativeValidation, valid]);
532
+ const focus = useCallback(() => element?.focus(), [element]);
533
+ const focusInvalid = useCallback(() => {
534
+ const filled = element?.querySelector("input[data-filled][name]:not(:disabled)");
535
+ const firstField = element?.querySelector("input:not([type=hidden]):not(:disabled), textarea:not(:disabled)");
536
+ (filled ?? firstField ?? element)?.focus();
537
+ }, [element]);
538
+ const reset = useCallback(() => {
539
+ setAttempted(false);
540
+ setSkipped(false);
541
+ setSelected(multiple ? [...defaults.current] : defaults.current.slice(0, 1));
542
+ setResetVersion((version) => version + 1);
543
+ }, [multiple]);
544
+ const skip = useCallback(() => {
545
+ if (required)
546
+ return;
547
+ setSelected([]);
548
+ setSkipped(true);
549
+ }, [required]);
550
+ useLayoutEffect(() => {
551
+ const wasMultiple = previousMultiple.current;
552
+ previousMultiple.current = multiple;
553
+ if (!wasMultiple || multiple)
554
+ return;
555
+ setSelected((list) => {
556
+ const keep = enabled.find((control) => list.includes(control.id));
557
+ return keep ? [keep.id] : [];
558
+ });
559
+ }, [enabled, multiple]);
560
+ const getAnswerByElement = useCallback((target) => enabled.find((control) => control.element === target) ?? null, [enabled]);
561
+ const getAnswerByShortcut = useCallback((shortcut) => {
562
+ if (shortcutByChoiceValue) {
563
+ const value = [...shortcutByChoiceValue.entries()].find(([, key]) => key === shortcut)?.[0];
564
+ return (enabled.find((control) => control.type === "choice" && control.value === value) ?? null);
565
+ }
566
+ const id = [...shortcutByAnswerId.entries()].find(([, key]) => key === shortcut)?.[0];
567
+ return enabled.find((control) => control.id === id) ?? null;
568
+ }, [enabled, shortcutByAnswerId, shortcutByChoiceValue]);
569
+ const moveAnswerFocus = useCallback((target, direction) => {
570
+ const position = enabled.findIndex((control) => control.element === target);
571
+ const currentControl = position < 0 ? null : (enabled[position] ?? null);
572
+ if (!enabled.length ||
573
+ (isTextEntry(target) && !isEmptyTextInput(currentControl)) ||
574
+ (position < 0 && target !== element))
575
+ return false;
576
+ const next = position < 0
577
+ ? (enabled.find(isAnswered) ??
578
+ (direction === "next" ? enabled[0] : enabled[enabled.length - 1]))
579
+ : enabled[(position + (direction === "next" ? 1 : -1) + enabled.length) %
580
+ enabled.length];
581
+ if (!next ||
582
+ next.element === target ||
583
+ (position >= 0 && isRadio(target) && isRadio(next.element)))
584
+ return false;
585
+ next.element.focus();
586
+ if (next.type === "choice" && isRadio(next.element))
587
+ next.element.click();
588
+ return true;
589
+ }, [element, enabled]);
590
+ useLayoutEffect(() => {
591
+ if (!element)
592
+ return;
593
+ return registerItem({
594
+ name,
595
+ element,
596
+ disabled,
597
+ required,
598
+ status,
599
+ choices: sorted.flatMap((control) => control.type === "choice"
600
+ ? [{ value: control.value, disabled: control.ownDisabled }]
601
+ : []),
602
+ focus,
603
+ focusInvalid,
604
+ validate,
605
+ reset,
606
+ skip,
607
+ getAnswerByElement,
608
+ getAnswerByShortcut,
609
+ moveAnswerFocus,
610
+ });
611
+ }, [
612
+ sorted,
613
+ disabled,
614
+ element,
615
+ focus,
616
+ focusInvalid,
617
+ getAnswerByElement,
618
+ getAnswerByShortcut,
619
+ moveAnswerFocus,
620
+ name,
621
+ registerItem,
622
+ required,
623
+ reset,
624
+ skip,
625
+ status,
626
+ validate,
627
+ ]);
628
+ const context = useMemo(() => ({
629
+ disabled,
630
+ hasInputAnswer,
631
+ invalid: showInvalid,
632
+ multiple,
633
+ name,
634
+ required,
635
+ resetVersion,
636
+ selectedAnswerIds: selected,
637
+ shortcutByAnswerId,
638
+ shortcutByChoiceValue,
639
+ status,
640
+ registerAnswerControl,
641
+ registerAnswerSelection,
642
+ registerDescription,
643
+ registerError,
644
+ setAnswerDefault,
645
+ setAnswerSelectionFromInteraction,
646
+ syncControlledAnswerSelection,
647
+ }), [
648
+ disabled,
649
+ hasInputAnswer,
650
+ showInvalid,
651
+ multiple,
652
+ name,
653
+ required,
654
+ resetVersion,
655
+ selected,
656
+ shortcutByAnswerId,
657
+ shortcutByChoiceValue,
658
+ status,
659
+ registerAnswerControl,
660
+ registerAnswerSelection,
661
+ registerDescription,
662
+ registerError,
663
+ setAnswerDefault,
664
+ setAnswerSelectionFromInteraction,
665
+ syncControlledAnswerSelection,
666
+ ]);
667
+ const handleRef = useCallback((node) => {
668
+ setElement(node);
669
+ assignRef(ref, node);
670
+ }, [ref]);
671
+ const describedBy = [...descriptionIds, ...(showInvalid ? errorIds : []), ariaDescribedBy]
672
+ .filter(Boolean)
673
+ .join(" ") || undefined;
674
+ const itemShortcuts = [
675
+ ariaKeyShortcuts,
676
+ active ? "Meta+Enter Control+Enter" : undefined,
677
+ active && enabled.length ? "ArrowUp ArrowDown" : undefined,
678
+ active && !first ? "ArrowLeft" : undefined,
679
+ active && !last && status !== "unanswered" ? "ArrowRight" : undefined,
680
+ ]
681
+ .filter(Boolean)
682
+ .join(" ") || undefined;
683
+ const state = {
684
+ active,
685
+ disabled,
686
+ invalid: showInvalid,
687
+ multiple,
688
+ required,
689
+ status,
690
+ };
691
+ return (_jsx(ItemContext.Provider, { value: context, children: _jsx("fieldset", { "data-slot": "questionnaire-item", "data-active": active ? "" : undefined, "data-disabled": disabled ? "" : undefined, "data-invalid": showInvalid ? "" : undefined, "data-multiple": multiple ? "" : undefined, "data-required": required ? "" : undefined, "data-status": state.status, ...props, ref: handleRef, className: cn("sprout-questionnaire-item", className), "aria-describedby": describedBy, "aria-invalid": showInvalid || undefined, "aria-keyshortcuts": itemShortcuts, disabled: disabled, hidden: !active, inert: !active, tabIndex: -1, children: children }) }));
692
+ }
693
+ /** The question, as the item's `legend`. */
694
+ export function QuestionnaireTitle({ className, render, ...props }) {
695
+ useItem("QuestionnaireTitle");
696
+ return useRender({
697
+ defaultTagName: "legend",
698
+ render,
699
+ props: mergeProps({
700
+ "data-slot": "questionnaire-title",
701
+ className: cn("sprout-questionnaire-title", className),
702
+ }, props),
703
+ });
704
+ }
705
+ /** Help text under the question. The item references it with `aria-describedby`. */
706
+ export function QuestionnaireDescription({ className, id, render, ...props }) {
707
+ const { registerDescription } = useItem("QuestionnaireDescription");
708
+ const generated = useId();
709
+ const resolved = id ?? generated;
710
+ useLayoutEffect(() => registerDescription(resolved), [resolved, registerDescription]);
711
+ return useRender({
712
+ defaultTagName: "p",
713
+ render,
714
+ props: mergeProps({
715
+ "data-slot": "questionnaire-description",
716
+ id: resolved,
717
+ className: cn("sprout-questionnaire-description", className),
718
+ }, props),
719
+ });
720
+ }
721
+ /** Stacks the item's choices. */
722
+ export function QuestionnaireChoices({ className, render, ...props }) {
723
+ useItem("QuestionnaireChoices");
724
+ const { shortcuts } = useRoot("QuestionnaireChoices");
725
+ return useRender({
726
+ defaultTagName: "div",
727
+ render,
728
+ props: mergeProps({
729
+ "data-slot": "questionnaire-choices",
730
+ className: cn("sprout-questionnaire-choices", className),
731
+ }, props),
732
+ state: { shortcuts },
733
+ });
734
+ }
735
+ /**
736
+ * One answer row. A radio in single-answer items and a checkbox in
737
+ * `multiple` items; the whole row is the hit area.
738
+ */
739
+ export function QuestionnaireChoice({ checked, children, className, defaultChecked = false, disabled: ownDisabled = false, onChange, render, value, ...props }) {
740
+ const item = useItem("QuestionnaireChoice");
741
+ const { disabled: itemDisabled, hasInputAnswer, invalid, multiple, name, registerAnswerControl, registerAnswerSelection, required, resetVersion, selectedAnswerIds, setAnswerDefault, setAnswerSelectionFromInteraction, shortcutByAnswerId, shortcutByChoiceValue, status, syncControlledAnswerSelection, } = item;
742
+ const id = useId();
743
+ const [input, setInput] = useState(null);
744
+ const initialChecked = useRef(defaultChecked);
745
+ const controlled = checked !== undefined;
746
+ const disabled = itemDisabled || ownDisabled;
747
+ const isChecked = controlled
748
+ ? status === "skipped"
749
+ ? false
750
+ : checked
751
+ : selectedAnswerIds.includes(id);
752
+ const type = multiple ? "checkbox" : "radio";
753
+ const shortcut = shortcutByChoiceValue?.get(value) ?? shortcutByAnswerId.get(id) ?? null;
754
+ useLayoutEffect(() => registerAnswerSelection(id, initialChecked.current), [id, registerAnswerSelection]);
755
+ useLayoutEffect(() => setAnswerDefault(id, defaultChecked), [id, defaultChecked, setAnswerDefault]);
756
+ useLayoutEffect(() => {
757
+ if (!input)
758
+ return;
759
+ return registerAnswerControl({
760
+ type: "choice",
761
+ id,
762
+ element: input,
763
+ disabled,
764
+ ownDisabled,
765
+ value,
766
+ });
767
+ }, [id, ownDisabled, disabled, input, registerAnswerControl, value]);
768
+ useLayoutEffect(() => {
769
+ if (controlled)
770
+ syncControlledAnswerSelection(id, checked);
771
+ }, [id, controlled, checked, resetVersion, syncControlledAnswerSelection]);
772
+ useLayoutEffect(() => {
773
+ if (!input)
774
+ return;
775
+ input.defaultChecked = controlled ? checked : defaultChecked;
776
+ if (resetVersion > 0)
777
+ input.checked = isChecked;
778
+ }, [isChecked, controlled, checked, defaultChecked, input, resetVersion]);
779
+ function handleChange(event) {
780
+ onChange?.(event);
781
+ if (event.defaultPrevented)
782
+ return;
783
+ if (!controlled) {
784
+ setAnswerSelectionFromInteraction(id, event.target.checked);
785
+ return;
786
+ }
787
+ if (status === "skipped" && checked === event.target.checked)
788
+ setAnswerSelectionFromInteraction(id, checked);
789
+ }
790
+ const state = {
791
+ checked: isChecked,
792
+ disabled,
793
+ invalid,
794
+ shortcut,
795
+ type,
796
+ };
797
+ return useRender({
798
+ defaultTagName: "label",
799
+ render,
800
+ props: mergeProps({
801
+ "data-slot": "questionnaire-choice",
802
+ 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
+ }, props),
805
+ state,
806
+ stateAttributesMapping: {
807
+ checked: (on) => on ? { "data-checked": "" } : { "data-unchecked": "" },
808
+ },
809
+ });
810
+ }
811
+ /** A muted second line inside a choice. */
812
+ export function QuestionnaireChoiceDescription({ className, ...props }) {
813
+ return (_jsx("span", { "data-slot": "questionnaire-choice-description", className: cn("sprout-questionnaire-choice-description", className), ...props }));
814
+ }
815
+ /**
816
+ * A typed answer. It counts as answered once it has text, and is only sent
817
+ * with the form while it does.
818
+ */
819
+ export function QuestionnaireInput({ className, defaultValue, disabled: ownDisabled = false, onChange, ref, render, type = "text", value, ...props }) {
820
+ const { disabled: itemDisabled, invalid, name, registerAnswerControl, registerAnswerSelection, resetVersion, selectedAnswerIds, setAnswerDefault, setAnswerSelectionFromInteraction, syncControlledAnswerSelection, } = useItem("QuestionnaireInput");
821
+ const id = useId();
822
+ const element = useRef(null);
823
+ const initialFilled = useRef(hasText(defaultValue));
824
+ const controlled = value !== undefined;
825
+ const defaultFilled = hasText(defaultValue);
826
+ const valueFilled = hasText(value);
827
+ const [filledState, setFilledState] = useState(defaultFilled);
828
+ const disabled = itemDisabled || ownDisabled;
829
+ const filled = controlled ? valueFilled : filledState;
830
+ const selected = selectedAnswerIds.includes(id);
831
+ useLayoutEffect(() => registerAnswerSelection(id, initialFilled.current), [id, registerAnswerSelection]);
832
+ useLayoutEffect(() => setAnswerDefault(id, defaultFilled), [defaultFilled, id, setAnswerDefault]);
833
+ useLayoutEffect(() => {
834
+ const node = element.current;
835
+ if (!node)
836
+ return;
837
+ return registerAnswerControl({
838
+ type: "input",
839
+ id,
840
+ element: node,
841
+ disabled,
842
+ });
843
+ }, [disabled, id, registerAnswerControl]);
844
+ useLayoutEffect(() => {
845
+ if (controlled) {
846
+ syncControlledAnswerSelection(id, valueFilled);
847
+ return;
848
+ }
849
+ if (resetVersion > 0)
850
+ setFilledState(defaultFilled);
851
+ }, [
852
+ controlled,
853
+ valueFilled,
854
+ value,
855
+ defaultFilled,
856
+ id,
857
+ resetVersion,
858
+ syncControlledAnswerSelection,
859
+ ]);
860
+ useLayoutEffect(() => {
861
+ const node = element.current;
862
+ if (node && controlled)
863
+ node.defaultValue = String(value);
864
+ }, [controlled, value]);
865
+ function handleChange(event) {
866
+ onChange?.(event);
867
+ if (event.defaultPrevented || controlled)
868
+ return;
869
+ const nowFilled = event.target.value.trim().length > 0;
870
+ setFilledState(nowFilled);
871
+ setAnswerSelectionFromInteraction(id, nowFilled);
872
+ }
873
+ const handleRef = useCallback((node) => {
874
+ element.current = node;
875
+ assignRef(ref, node);
876
+ }, [ref]);
877
+ const input = useRender({
878
+ defaultTagName: "input",
879
+ render,
880
+ props: mergeProps({
881
+ "data-slot": "questionnaire-input",
882
+ className: cn("sprout-input", "sprout-questionnaire-input", className),
883
+ "aria-invalid": invalid || undefined,
884
+ "aria-keyshortcuts": keyShortcuts(null, !disabled && filled && selected),
885
+ defaultValue: controlled ? undefined : defaultValue,
886
+ disabled,
887
+ form: selected ? undefined : "",
888
+ id,
889
+ name: selected ? name : undefined,
890
+ onChange: handleChange,
891
+ ref: handleRef,
892
+ type,
893
+ value: controlled ? value : undefined,
894
+ }, props),
895
+ state: { disabled, filled, invalid },
896
+ stateAttributesMapping: {
897
+ filled: (on) => on ? { "data-filled": "" } : { "data-empty": "" },
898
+ },
899
+ });
900
+ return (_jsx("div", { "data-slot": "questionnaire-input-wrapper", className: "sprout-questionnaire-input-wrapper", children: input }));
901
+ }
902
+ /** Shown after someone tries to move on without a valid answer. */
903
+ export function QuestionnaireError({ children, className, id, render, ...props }) {
904
+ const { invalid, registerError, required } = useItem("QuestionnaireError");
905
+ const generated = useId();
906
+ const resolved = id ?? generated;
907
+ useLayoutEffect(() => registerError(resolved), [resolved, registerError]);
908
+ return useRender({
909
+ defaultTagName: "p",
910
+ render,
911
+ props: mergeProps({
912
+ "data-slot": "questionnaire-error",
913
+ className: cn("sprout-questionnaire-error", className),
914
+ id: resolved,
915
+ hidden: !invalid,
916
+ role: invalid ? "alert" : undefined,
917
+ children: children ??
918
+ (required
919
+ ? "Choose an answer to continue."
920
+ : "Choose an answer or skip this question."),
921
+ }, props),
922
+ state: { invalid },
923
+ });
924
+ }
925
+ /** Previous on the start side; Skip and Next or Submit on the end side. */
926
+ export function QuestionnaireActions({ className, ...props }) {
927
+ return (_jsx("div", { "data-slot": "questionnaire-actions", className: cn("sprout-questionnaire-actions", className), ...props }));
928
+ }
929
+ function useNavigationButton(slot, { children, className, disabled = false, onClick, render, size = "default", tabIndex, type, variant, ...props }, { label, visible, status, shortcut, action, }) {
930
+ const resolvedVariant = variant ?? (slot === "next" || slot === "submit" ? "default" : "outline");
931
+ const activeShortcut = visible && !disabled ? (shortcut ?? null) : null;
932
+ function handleClick(event) {
933
+ onClick?.(event);
934
+ if (!event.defaultPrevented)
935
+ action?.();
936
+ }
937
+ return useRender({
938
+ defaultTagName: "button",
939
+ render,
940
+ props: mergeProps({
941
+ "data-slot": `questionnaire-${slot}`,
942
+ "data-size": size,
943
+ "data-variant": resolvedVariant,
944
+ className: cn(buttonVariants({ variant: resolvedVariant, size }), "sprout-questionnaire-action", `sprout-questionnaire-${slot}`, className),
945
+ "aria-hidden": !visible || undefined,
946
+ "aria-keyshortcuts": activeShortcut ?? undefined,
947
+ children: children ?? label,
948
+ disabled,
949
+ hidden: !visible,
950
+ inert: !visible,
951
+ onClick: action ? handleClick : onClick,
952
+ tabIndex: visible ? tabIndex : -1,
953
+ type: type ?? (slot === "submit" ? "submit" : "button"),
954
+ }, props),
955
+ state: { disabled, shortcut: activeShortcut, status, visible },
956
+ stateAttributesMapping: {
957
+ visible: (on) => on ? { "data-visible": "" } : { "data-hidden": "" },
958
+ },
959
+ });
960
+ }
961
+ /** Goes back one item. Hidden on the first item. */
962
+ export function QuestionnairePrevious(props) {
963
+ const root = useRoot("QuestionnairePrevious");
964
+ return useNavigationButton("previous", props, {
965
+ label: "Previous",
966
+ visible: root.total > 1 && !root.first,
967
+ status: root.activeItemStatus,
968
+ action: root.goPrevious,
969
+ });
970
+ }
971
+ /** Clears the answer and moves on. Shown only on optional items; submits on the last one. */
972
+ export function QuestionnaireSkip(props) {
973
+ const root = useRoot("QuestionnaireSkip");
974
+ return useNavigationButton("skip", props, {
975
+ label: "Skip",
976
+ visible: root.activeItemRequired === false,
977
+ status: root.activeItemStatus,
978
+ action: root.skipCurrent,
979
+ });
980
+ }
981
+ /** Checks the answer and moves on. Hidden on the last item. */
982
+ export function QuestionnaireNext(props) {
983
+ const root = useRoot("QuestionnaireNext");
984
+ return useNavigationButton("next", props, {
985
+ label: "Next",
986
+ visible: root.total > 1 && !root.last,
987
+ status: root.activeItemStatus,
988
+ shortcut: "Enter",
989
+ action: root.goNext,
990
+ });
991
+ }
992
+ /** Submits the form. Shown only on the last item. */
993
+ export function QuestionnaireSubmit(props) {
994
+ const root = useRoot("QuestionnaireSubmit");
995
+ return useNavigationButton("submit", props, {
996
+ label: "Submit",
997
+ visible: root.total > 0 && root.last,
998
+ status: root.activeItemStatus,
999
+ shortcut: "Enter",
1000
+ });
1001
+ }