@thomasfosterau/components 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 (340) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +817 -2
  3. package/dist/canvas/VectorCanvas.svelte +239 -0
  4. package/dist/canvas/VectorCanvas.svelte.d.ts +87 -0
  5. package/dist/canvas/history.d.ts +105 -0
  6. package/dist/canvas/history.js +147 -0
  7. package/dist/canvas/index.d.ts +15 -0
  8. package/dist/canvas/index.js +14 -0
  9. package/dist/canvas/types.d.ts +91 -0
  10. package/dist/canvas/types.js +13 -0
  11. package/dist/canvas/utils.d.ts +66 -0
  12. package/dist/canvas/utils.js +114 -0
  13. package/dist/charts/Chart.svelte +300 -0
  14. package/dist/charts/Chart.svelte.d.ts +50 -0
  15. package/dist/charts/index.d.ts +11 -0
  16. package/dist/charts/index.js +10 -0
  17. package/dist/charts/types.d.ts +28 -0
  18. package/dist/charts/types.js +6 -0
  19. package/dist/citations/BookCitation.svelte +53 -0
  20. package/dist/citations/BookCitation.svelte.d.ts +41 -0
  21. package/dist/citations/ChapterCitation.svelte +38 -0
  22. package/dist/citations/ChapterCitation.svelte.d.ts +50 -0
  23. package/dist/citations/Citation.svelte +47 -0
  24. package/dist/citations/Citation.svelte.d.ts +24 -0
  25. package/dist/citations/CreditList.svelte +69 -0
  26. package/dist/citations/CreditList.svelte.d.ts +47 -0
  27. package/dist/citations/DOILink.svelte +28 -0
  28. package/dist/citations/DOILink.svelte.d.ts +19 -0
  29. package/dist/citations/Footnote.svelte +39 -0
  30. package/dist/citations/Footnote.svelte.d.ts +17 -0
  31. package/dist/citations/FootnoteList.svelte +36 -0
  32. package/dist/citations/FootnoteList.svelte.d.ts +21 -0
  33. package/dist/citations/ISBNLink.svelte +29 -0
  34. package/dist/citations/ISBNLink.svelte.d.ts +19 -0
  35. package/dist/citations/ISSNLink.svelte +29 -0
  36. package/dist/citations/ISSNLink.svelte.d.ts +19 -0
  37. package/dist/citations/Publisher.svelte +34 -0
  38. package/dist/citations/Publisher.svelte.d.ts +14 -0
  39. package/dist/citations/ReferenceList.svelte +53 -0
  40. package/dist/citations/ReferenceList.svelte.d.ts +21 -0
  41. package/dist/citations/ReportCitation.svelte +41 -0
  42. package/dist/citations/ReportCitation.svelte.d.ts +36 -0
  43. package/dist/citations/ScholarlyArticleCitation.svelte +64 -0
  44. package/dist/citations/ScholarlyArticleCitation.svelte.d.ts +41 -0
  45. package/dist/citations/ThesisCitation.svelte +35 -0
  46. package/dist/citations/ThesisCitation.svelte.d.ts +29 -0
  47. package/dist/citations/bibliography.svelte.d.ts +55 -0
  48. package/dist/citations/bibliography.svelte.js +78 -0
  49. package/dist/citations/citation.d.ts +71 -0
  50. package/dist/citations/citation.js +121 -0
  51. package/dist/citations/csl.d.ts +102 -0
  52. package/dist/citations/csl.js +20 -0
  53. package/dist/citations/index.d.ts +31 -0
  54. package/dist/citations/index.js +36 -0
  55. package/dist/citations/list-format.d.ts +34 -0
  56. package/dist/citations/list-format.js +38 -0
  57. package/dist/context.svelte.d.ts +46 -0
  58. package/dist/context.svelte.js +51 -0
  59. package/dist/data/DataGrid.svelte +150 -0
  60. package/dist/data/DataGrid.svelte.d.ts +118 -0
  61. package/dist/data/DescriptionList.svelte +47 -0
  62. package/dist/data/DescriptionList.svelte.d.ts +26 -0
  63. package/dist/data/DescriptionRow.svelte +55 -0
  64. package/dist/data/DescriptionRow.svelte.d.ts +21 -0
  65. package/dist/data/DiffText.svelte +29 -0
  66. package/dist/data/DiffText.svelte.d.ts +19 -0
  67. package/dist/data/ItemList.svelte +80 -0
  68. package/dist/data/ItemList.svelte.d.ts +52 -0
  69. package/dist/data/LinkListItem.svelte +46 -0
  70. package/dist/data/LinkListItem.svelte.d.ts +21 -0
  71. package/dist/data/Pagination.svelte +136 -0
  72. package/dist/data/Pagination.svelte.d.ts +71 -0
  73. package/dist/data/PaginationFooter.svelte +41 -0
  74. package/dist/data/PaginationFooter.svelte.d.ts +59 -0
  75. package/dist/data/ResultCount.svelte +27 -0
  76. package/dist/data/ResultCount.svelte.d.ts +18 -0
  77. package/dist/data/Table.svelte +145 -0
  78. package/dist/data/Table.svelte.d.ts +75 -0
  79. package/dist/data/TreeList.svelte +41 -0
  80. package/dist/data/TreeList.svelte.d.ts +17 -0
  81. package/dist/data/TreeListItem.svelte +29 -0
  82. package/dist/data/TreeListItem.svelte.d.ts +10 -0
  83. package/dist/data/description-context.d.ts +33 -0
  84. package/dist/data/description-context.js +35 -0
  85. package/dist/data/diff.d.ts +14 -0
  86. package/dist/data/diff.js +10 -0
  87. package/dist/data/index.d.ts +35 -0
  88. package/dist/data/index.js +35 -0
  89. package/dist/data/list-item.d.ts +30 -0
  90. package/dist/data/list-item.js +19 -0
  91. package/dist/data/pagination.d.ts +81 -0
  92. package/dist/data/pagination.js +79 -0
  93. package/dist/data/table-types.d.ts +64 -0
  94. package/dist/data/table-types.js +35 -0
  95. package/dist/feedback/ConfirmDialog.svelte +79 -0
  96. package/dist/feedback/ConfirmDialog.svelte.d.ts +82 -0
  97. package/dist/feedback/Dialog.svelte +77 -0
  98. package/dist/feedback/Dialog.svelte.d.ts +37 -0
  99. package/dist/feedback/LoadingBoundary.svelte +61 -0
  100. package/dist/feedback/LoadingBoundary.svelte.d.ts +34 -0
  101. package/dist/feedback/ProgressBar.svelte +46 -0
  102. package/dist/feedback/ProgressBar.svelte.d.ts +28 -0
  103. package/dist/feedback/SegmentBar.svelte +83 -0
  104. package/dist/feedback/SegmentBar.svelte.d.ts +36 -0
  105. package/dist/feedback/StatTile.svelte +77 -0
  106. package/dist/feedback/StatTile.svelte.d.ts +49 -0
  107. package/dist/feedback/Stats.svelte +18 -0
  108. package/dist/feedback/Stats.svelte.d.ts +16 -0
  109. package/dist/feedback/Timeline.svelte +86 -0
  110. package/dist/feedback/Timeline.svelte.d.ts +52 -0
  111. package/dist/feedback/Toaster.svelte +78 -0
  112. package/dist/feedback/Toaster.svelte.d.ts +32 -0
  113. package/dist/feedback/index.d.ts +31 -0
  114. package/dist/feedback/index.js +27 -0
  115. package/dist/feedback/sizes.d.ts +27 -0
  116. package/dist/feedback/sizes.js +41 -0
  117. package/dist/feedback/toaster.d.ts +48 -0
  118. package/dist/feedback/toaster.js +29 -0
  119. package/dist/fields/Checkbox.svelte +33 -0
  120. package/dist/fields/Checkbox.svelte.d.ts +24 -0
  121. package/dist/fields/CheckboxGroup.svelte +41 -0
  122. package/dist/fields/CheckboxGroup.svelte.d.ts +40 -0
  123. package/dist/fields/Combobox.svelte +222 -0
  124. package/dist/fields/Combobox.svelte.d.ts +98 -0
  125. package/dist/fields/DateInput.svelte +31 -0
  126. package/dist/fields/DateInput.svelte.d.ts +29 -0
  127. package/dist/fields/FieldErrors.svelte +30 -0
  128. package/dist/fields/FieldErrors.svelte.d.ts +11 -0
  129. package/dist/fields/Fieldset.svelte +48 -0
  130. package/dist/fields/Fieldset.svelte.d.ts +26 -0
  131. package/dist/fields/FileInput.svelte +67 -0
  132. package/dist/fields/FileInput.svelte.d.ts +33 -0
  133. package/dist/fields/FormField.svelte +54 -0
  134. package/dist/fields/FormField.svelte.d.ts +29 -0
  135. package/dist/fields/ImageInput.svelte +87 -0
  136. package/dist/fields/ImageInput.svelte.d.ts +39 -0
  137. package/dist/fields/PushToggle.svelte +93 -0
  138. package/dist/fields/PushToggle.svelte.d.ts +55 -0
  139. package/dist/fields/RadioGroup.svelte +85 -0
  140. package/dist/fields/RadioGroup.svelte.d.ts +41 -0
  141. package/dist/fields/SearchInput.svelte +57 -0
  142. package/dist/fields/SearchInput.svelte.d.ts +33 -0
  143. package/dist/fields/Select.svelte +36 -0
  144. package/dist/fields/Select.svelte.d.ts +29 -0
  145. package/dist/fields/TextInput.svelte +26 -0
  146. package/dist/fields/TextInput.svelte.d.ts +30 -0
  147. package/dist/fields/Textarea.svelte +22 -0
  148. package/dist/fields/Textarea.svelte.d.ts +25 -0
  149. package/dist/fields/Toggle.svelte +72 -0
  150. package/dist/fields/Toggle.svelte.d.ts +32 -0
  151. package/dist/fields/VideoInput.svelte +35 -0
  152. package/dist/fields/VideoInput.svelte.d.ts +40 -0
  153. package/dist/fields/field-types.d.ts +51 -0
  154. package/dist/fields/field-types.js +60 -0
  155. package/dist/fields/index.d.ts +34 -0
  156. package/dist/fields/index.js +34 -0
  157. package/dist/formatting/DateTime.svelte +35 -0
  158. package/dist/formatting/DateTime.svelte.d.ts +26 -0
  159. package/dist/formatting/FormatList.svelte +31 -0
  160. package/dist/formatting/FormatList.svelte.d.ts +46 -0
  161. package/dist/formatting/Highlight.svelte +47 -0
  162. package/dist/formatting/Highlight.svelte.d.ts +15 -0
  163. package/dist/formatting/Number.svelte +35 -0
  164. package/dist/formatting/Number.svelte.d.ts +43 -0
  165. package/dist/formatting/RelativeTime.svelte +69 -0
  166. package/dist/formatting/RelativeTime.svelte.d.ts +26 -0
  167. package/dist/formatting/Year.svelte +39 -0
  168. package/dist/formatting/Year.svelte.d.ts +21 -0
  169. package/dist/formatting/index.d.ts +12 -0
  170. package/dist/formatting/index.js +14 -0
  171. package/dist/forms/EntityForm.svelte +177 -0
  172. package/dist/forms/EntityForm.svelte.d.ts +57 -0
  173. package/dist/forms/FormActions.svelte +29 -0
  174. package/dist/forms/FormActions.svelte.d.ts +37 -0
  175. package/dist/forms/FormErrors.svelte +21 -0
  176. package/dist/forms/FormErrors.svelte.d.ts +16 -0
  177. package/dist/forms/HiddenField.svelte +22 -0
  178. package/dist/forms/HiddenField.svelte.d.ts +11 -0
  179. package/dist/forms/SearchForm.svelte +43 -0
  180. package/dist/forms/SearchForm.svelte.d.ts +36 -0
  181. package/dist/forms/entity-form.d.ts +12 -0
  182. package/dist/forms/entity-form.js +12 -0
  183. package/dist/forms/index.d.ts +14 -0
  184. package/dist/forms/index.js +14 -0
  185. package/dist/hotkeys/HotkeyBadge.svelte +39 -0
  186. package/dist/hotkeys/HotkeyBadge.svelte.d.ts +25 -0
  187. package/dist/hotkeys/HotkeyHelpDialog.svelte +68 -0
  188. package/dist/hotkeys/HotkeyHelpDialog.svelte.d.ts +35 -0
  189. package/dist/hotkeys/HotkeyProvider.svelte +92 -0
  190. package/dist/hotkeys/HotkeyProvider.svelte.d.ts +4 -0
  191. package/dist/hotkeys/context.svelte.d.ts +47 -0
  192. package/dist/hotkeys/context.svelte.js +72 -0
  193. package/dist/hotkeys/index.d.ts +23 -0
  194. package/dist/hotkeys/index.js +22 -0
  195. package/dist/hotkeys/match.d.ts +65 -0
  196. package/dist/hotkeys/match.js +133 -0
  197. package/dist/hotkeys/registry.svelte.d.ts +55 -0
  198. package/dist/hotkeys/registry.svelte.js +110 -0
  199. package/dist/hotkeys/types.d.ts +72 -0
  200. package/dist/hotkeys/types.js +18 -0
  201. package/dist/index.d.ts +36 -0
  202. package/dist/index.js +36 -0
  203. package/dist/infobox/Infobox.svelte +79 -0
  204. package/dist/infobox/Infobox.svelte.d.ts +26 -0
  205. package/dist/infobox/InfoboxHeader.svelte +27 -0
  206. package/dist/infobox/InfoboxHeader.svelte.d.ts +11 -0
  207. package/dist/infobox/InfoboxImage.svelte +38 -0
  208. package/dist/infobox/InfoboxImage.svelte.d.ts +21 -0
  209. package/dist/infobox/InfoboxRow.svelte +50 -0
  210. package/dist/infobox/InfoboxRow.svelte.d.ts +25 -0
  211. package/dist/infobox/InfoboxSection.svelte +24 -0
  212. package/dist/infobox/InfoboxSection.svelte.d.ts +12 -0
  213. package/dist/infobox/index.d.ts +21 -0
  214. package/dist/infobox/index.js +20 -0
  215. package/dist/infobox/types.d.ts +86 -0
  216. package/dist/infobox/types.js +27 -0
  217. package/dist/layout/Breadcrumbs.svelte +52 -0
  218. package/dist/layout/Breadcrumbs.svelte.d.ts +18 -0
  219. package/dist/layout/DetailLayout.svelte +50 -0
  220. package/dist/layout/DetailLayout.svelte.d.ts +19 -0
  221. package/dist/layout/FilterBar.svelte +40 -0
  222. package/dist/layout/FilterBar.svelte.d.ts +12 -0
  223. package/dist/layout/Footer.svelte +32 -0
  224. package/dist/layout/Footer.svelte.d.ts +12 -0
  225. package/dist/layout/Main.svelte +30 -0
  226. package/dist/layout/Main.svelte.d.ts +15 -0
  227. package/dist/layout/Navbar.svelte +68 -0
  228. package/dist/layout/Navbar.svelte.d.ts +31 -0
  229. package/dist/layout/PageHeader.svelte +77 -0
  230. package/dist/layout/PageHeader.svelte.d.ts +33 -0
  231. package/dist/layout/Section.svelte +72 -0
  232. package/dist/layout/Section.svelte.d.ts +40 -0
  233. package/dist/layout/Sidebar.svelte +54 -0
  234. package/dist/layout/Sidebar.svelte.d.ts +27 -0
  235. package/dist/layout/Tabs.svelte +85 -0
  236. package/dist/layout/Tabs.svelte.d.ts +42 -0
  237. package/dist/layout/index.d.ts +29 -0
  238. package/dist/layout/index.js +29 -0
  239. package/dist/layout/types.d.ts +52 -0
  240. package/dist/layout/types.js +27 -0
  241. package/dist/links/ContextLinks.svelte +28 -0
  242. package/dist/links/ContextLinks.svelte.d.ts +20 -0
  243. package/dist/links/EmailLink.svelte +26 -0
  244. package/dist/links/EmailLink.svelte.d.ts +20 -0
  245. package/dist/links/index.d.ts +7 -0
  246. package/dist/links/index.js +7 -0
  247. package/dist/media/Figure.svelte +26 -0
  248. package/dist/media/Figure.svelte.d.ts +12 -0
  249. package/dist/media/Image.svelte +76 -0
  250. package/dist/media/Image.svelte.d.ts +41 -0
  251. package/dist/media/Video.svelte +65 -0
  252. package/dist/media/Video.svelte.d.ts +29 -0
  253. package/dist/media/image-dimensions.d.ts +28 -0
  254. package/dist/media/image-dimensions.js +34 -0
  255. package/dist/media/index.d.ts +10 -0
  256. package/dist/media/index.js +10 -0
  257. package/dist/search/CommandPalette.svelte +257 -0
  258. package/dist/search/CommandPalette.svelte.d.ts +59 -0
  259. package/dist/search/index.d.ts +13 -0
  260. package/dist/search/index.js +13 -0
  261. package/dist/search/types.d.ts +37 -0
  262. package/dist/search/types.js +20 -0
  263. package/dist/seo/Head.svelte +190 -0
  264. package/dist/seo/Head.svelte.d.ts +86 -0
  265. package/dist/seo/JsonLd.svelte +30 -0
  266. package/dist/seo/JsonLd.svelte.d.ts +20 -0
  267. package/dist/seo/index.d.ts +18 -0
  268. package/dist/seo/index.js +17 -0
  269. package/dist/seo/json-ld.d.ts +33 -0
  270. package/dist/seo/json-ld.js +29 -0
  271. package/dist/seo/og-image.d.ts +71 -0
  272. package/dist/seo/og-image.js +81 -0
  273. package/dist/seo/types.d.ts +43 -0
  274. package/dist/seo/types.js +6 -0
  275. package/dist/theme.css +161 -0
  276. package/dist/tips/PopoverTip.svelte +80 -0
  277. package/dist/tips/PopoverTip.svelte.d.ts +33 -0
  278. package/dist/tips/Tip.svelte +91 -0
  279. package/dist/tips/Tip.svelte.d.ts +25 -0
  280. package/dist/tips/index.d.ts +15 -0
  281. package/dist/tips/index.js +14 -0
  282. package/dist/tips/tip-stack.d.ts +25 -0
  283. package/dist/tips/tip-stack.js +37 -0
  284. package/dist/tips/tip-store.svelte.d.ts +74 -0
  285. package/dist/tips/tip-store.svelte.js +158 -0
  286. package/dist/tips/types.d.ts +46 -0
  287. package/dist/tips/types.js +6 -0
  288. package/dist/ui/Alert.svelte +61 -0
  289. package/dist/ui/Alert.svelte.d.ts +37 -0
  290. package/dist/ui/Badge.svelte +39 -0
  291. package/dist/ui/Badge.svelte.d.ts +32 -0
  292. package/dist/ui/Button.svelte +43 -0
  293. package/dist/ui/Button.svelte.d.ts +29 -0
  294. package/dist/ui/Card.svelte +49 -0
  295. package/dist/ui/Card.svelte.d.ts +44 -0
  296. package/dist/ui/Chip.svelte +58 -0
  297. package/dist/ui/Chip.svelte.d.ts +21 -0
  298. package/dist/ui/ConfirmButton.svelte +75 -0
  299. package/dist/ui/ConfirmButton.svelte.d.ts +48 -0
  300. package/dist/ui/Disclosure.svelte +39 -0
  301. package/dist/ui/Disclosure.svelte.d.ts +22 -0
  302. package/dist/ui/EmptyState.svelte +37 -0
  303. package/dist/ui/EmptyState.svelte.d.ts +22 -0
  304. package/dist/ui/ExternalLink.svelte +38 -0
  305. package/dist/ui/ExternalLink.svelte.d.ts +34 -0
  306. package/dist/ui/Heading.svelte +58 -0
  307. package/dist/ui/Heading.svelte.d.ts +22 -0
  308. package/dist/ui/Link.svelte +34 -0
  309. package/dist/ui/Link.svelte.d.ts +20 -0
  310. package/dist/ui/LinkButton.svelte +51 -0
  311. package/dist/ui/LinkButton.svelte.d.ts +33 -0
  312. package/dist/ui/Menu.svelte +54 -0
  313. package/dist/ui/Menu.svelte.d.ts +34 -0
  314. package/dist/ui/MenuItem.svelte +75 -0
  315. package/dist/ui/MenuItem.svelte.d.ts +21 -0
  316. package/dist/ui/MenuSeparator.svelte +26 -0
  317. package/dist/ui/MenuSeparator.svelte.d.ts +8 -0
  318. package/dist/ui/Popover.svelte +63 -0
  319. package/dist/ui/Popover.svelte.d.ts +40 -0
  320. package/dist/ui/ShareButton.svelte +64 -0
  321. package/dist/ui/ShareButton.svelte.d.ts +39 -0
  322. package/dist/ui/Skeleton.svelte +43 -0
  323. package/dist/ui/Skeleton.svelte.d.ts +22 -0
  324. package/dist/ui/ThemeToggle.svelte +65 -0
  325. package/dist/ui/ThemeToggle.svelte.d.ts +24 -0
  326. package/dist/ui/Tooltip.svelte +54 -0
  327. package/dist/ui/Tooltip.svelte.d.ts +35 -0
  328. package/dist/ui/VisuallyHidden.svelte +23 -0
  329. package/dist/ui/VisuallyHidden.svelte.d.ts +10 -0
  330. package/dist/ui/colour-token.d.ts +41 -0
  331. package/dist/ui/colour-token.js +49 -0
  332. package/dist/ui/index.d.ts +44 -0
  333. package/dist/ui/index.js +44 -0
  334. package/dist/ui/menu-context.d.ts +22 -0
  335. package/dist/ui/menu-context.js +26 -0
  336. package/dist/ui/slug.d.ts +19 -0
  337. package/dist/ui/slug.js +25 -0
  338. package/dist/ui/tones.d.ts +68 -0
  339. package/dist/ui/tones.js +150 -0
  340. package/package.json +101 -3
@@ -0,0 +1,222 @@
1
+ <!-- @component
2
+ A single-select typeahead over a static list or an asynchronous search, on bits-ui’s `Combobox`.
3
+ -->
4
+ <!--
5
+ Combobox — a single-select typeahead over a static list OR an asynchronous
6
+ search, on bits-ui's `Combobox`.
7
+
8
+ The previous generation of this component hand-rolled the APG combobox
9
+ pattern: an `<input role="combobox">`, a `<ul role="listbox">`, an
10
+ `activeIndex`, wrap-around arrow keys, Home/End, and a blur timeout to let a
11
+ pointerdown on an option land before the list closed. All of that is
12
+ bits-ui's here. What is NOT bits-ui's, and is what this file is actually
13
+ about, is the SEARCH LIFECYCLE.
14
+
15
+ The search lifecycle, in the order the problems bite:
16
+
17
+ debounce a keystroke does not start a request; `debounceMs` of
18
+ quiet does.
19
+ latest-wins each request gets an `AbortController`, and starting a
20
+ new one aborts the last. A superseded request is
21
+ cancelled rather than merely ignored, so a slow first
22
+ response cannot overwrite a fast second one.
23
+ stale-while- the previous results stay on screen while the next
24
+ revalidate request is in flight, with `pending` true. Clearing the
25
+ list on every keystroke makes the dropdown strobe and
26
+ costs the reader the thing they were about to pick.
27
+
28
+ ARIA contract: bits-ui gives the input `role="combobox"`,
29
+ `aria-autocomplete="list"`, `aria-expanded` and `aria-activedescendant`
30
+ pointing at the highlighted option; the panel is `role="listbox"` and each
31
+ row `role="option"` with `aria-selected`. The visible `<label>` names the
32
+ input through `for`/`id`; `errorMessage` is a `role="alert"` paragraph the
33
+ input is NOT described by, because a search failure is about the list rather
34
+ than about the value.
35
+
36
+ One behaviour is preserved from the hand-rolled version on purpose:
37
+ **Enter with nothing highlighted must not prevent a wrapping `<form>` from
38
+ submitting.** A search box that sits in a plain GET form has to keep working
39
+ when the list is empty or the reader never opened it. bits-ui's input
40
+ `preventDefault`s Enter whenever the panel is open, so the handler below
41
+ closes the panel first when nothing is highlighted: with the panel closed,
42
+ bits-ui treats Enter as an interaction key and returns without touching the
43
+ event, and the form submits. `highlighted` is tracked through the items'
44
+ own `onHighlight` / `onUnhighlight` callbacks, which is the only place
45
+ bits-ui surfaces it.
46
+
47
+ Note what that means in practice: bits-ui puts the cursor on the first
48
+ candidate as soon as typing produces one, so with matches on screen Enter
49
+ COMMITS. The fall-through happens where it matters — a query that matched
50
+ nothing, or a panel the reader never opened — which is exactly the case a
51
+ no-JS search form depends on.
52
+ -->
53
+ <script lang="ts" generics="T">import { Combobox } from "bits-ui";
54
+ import { inputSize } from "./field-types.js";
55
+ let { value = $bindable(), items, onSearch, debounceMs = 150, getId, getLabel, getBadge, label, placeholder, name, emptyLabel = "No matches", pendingLabel = "Searching…", errorMessage, id: idProp, describedBy = "", invalid = false, required = false, disabled = false, size = "md", class: className, item, footer, empty } = $props();
56
+ const uid = $props.id();
57
+ const inputId = $derived(idProp ?? uid);
58
+ let query = $state("");
59
+ let open = $state(false);
60
+ let results = $state([]);
61
+ let pending = $state(false);
62
+ let searchError = $state();
63
+ /** The id of the row bits-ui currently has under the cursor, if any. */
64
+ let highlighted = $state();
65
+ let timer;
66
+ let inFlight;
67
+ /** Static mode filters locally; async mode shows whatever last settled. */
68
+ const visible = $derived.by(() => {
69
+ if (onSearch !== undefined) return results;
70
+ const all = items ?? [];
71
+ const needle = query.trim().toLowerCase();
72
+ if (needle === "") return all;
73
+ return all.filter((entry) => getLabel(entry).toLowerCase().includes(needle));
74
+ });
75
+ const selectedLabel = $derived.by(() => {
76
+ if (value === undefined || items === undefined) return undefined;
77
+ const match = items.find((entry) => getId(entry) === value);
78
+ return match === undefined ? undefined : getLabel(match);
79
+ });
80
+ const visibleError = $derived(errorMessage ?? searchError);
81
+ const search = (text) => {
82
+ if (onSearch === undefined) return;
83
+ inFlight?.abort();
84
+ const own = new AbortController();
85
+ inFlight = own;
86
+ pending = true;
87
+ searchError = undefined;
88
+ void onSearch(text, own.signal).then((next) => {
89
+ // A superseded request must not write: `inFlight` is the identity
90
+ // check, `signal.aborted` catches the caller aborting its own work.
91
+ if (own.signal.aborted || inFlight !== own) return;
92
+ results = next;
93
+ pending = false;
94
+ }).catch((error) => {
95
+ if (own.signal.aborted || inFlight !== own) return;
96
+ pending = false;
97
+ searchError = error instanceof Error ? error.message : String(error);
98
+ });
99
+ };
100
+ const schedule = (text) => {
101
+ if (timer !== undefined) clearTimeout(timer);
102
+ timer = setTimeout(() => {
103
+ timer = undefined;
104
+ search(text);
105
+ }, debounceMs);
106
+ };
107
+ $effect(() => () => {
108
+ if (timer !== undefined) clearTimeout(timer);
109
+ inFlight?.abort();
110
+ });
111
+ const typed = (event) => {
112
+ query = event.currentTarget.value;
113
+ if (onSearch === undefined) {
114
+ open = true;
115
+ return;
116
+ }
117
+ if (query.trim() === "") {
118
+ if (timer !== undefined) clearTimeout(timer);
119
+ inFlight?.abort();
120
+ inFlight = undefined;
121
+ pending = false;
122
+ results = [];
123
+ open = false;
124
+ return;
125
+ }
126
+ open = true;
127
+ schedule(query);
128
+ };
129
+ const keydown = (event) => {
130
+ if (event.key !== "Enter" || highlighted !== undefined) return;
131
+ // Nothing to commit. Close first so bits-ui's own Enter branch takes the
132
+ // closed path, which returns without `preventDefault` — that is what lets
133
+ // a wrapping form submit.
134
+ open = false;
135
+ };
136
+ </script>
137
+
138
+ <div class={['fieldset w-full', className]}>
139
+ <label class="label" for={inputId}>{label}</label>
140
+
141
+ <Combobox.Root
142
+ type="single"
143
+ bind:open
144
+ value={value ?? ''}
145
+ onValueChange={(next) => {
146
+ value = next === '' ? undefined : next;
147
+ }}
148
+ {disabled}
149
+ {required}
150
+ >
151
+ <Combobox.Input
152
+ id={inputId}
153
+ {...selectedLabel === undefined ? {} : { defaultValue: selectedLabel }}
154
+ {placeholder}
155
+ autocomplete="off"
156
+ aria-describedby={describedBy === '' ? undefined : describedBy}
157
+ aria-invalid={invalid ? 'true' : undefined}
158
+ oninput={typed}
159
+ onkeydown={keydown}
160
+ class={['input w-full', inputSize[size], invalid && 'input-error']}
161
+ />
162
+
163
+ <Combobox.Portal>
164
+ <Combobox.Content
165
+ sideOffset={4}
166
+ class="menu bg-base-100 rounded-box border-base-300 z-50 max-h-72 min-w-64 overflow-auto border p-2 shadow-md"
167
+ >
168
+ <Combobox.Viewport>
169
+ {#each visible as entry (getId(entry))}
170
+ {@const entryId = getId(entry)}
171
+ {@const badge = getBadge?.(entry)}
172
+ <Combobox.Item
173
+ value={entryId}
174
+ label={getLabel(entry)}
175
+ onHighlight={() => (highlighted = entryId)}
176
+ onUnhighlight={() => {
177
+ if (highlighted === entryId) highlighted = undefined;
178
+ }}
179
+ class="rounded-field data-highlighted:bg-base-200 data-disabled:opacity-50 flex cursor-pointer items-center justify-between gap-3 px-3 py-2"
180
+ >
181
+ {#if item}
182
+ {@render item(entry)}
183
+ {:else}
184
+ <span>{getLabel(entry)}</span>
185
+ {#if badge !== undefined}
186
+ <span class="badge badge-ghost badge-sm">{badge}</span>
187
+ {/if}
188
+ {/if}
189
+ </Combobox.Item>
190
+ {/each}
191
+
192
+ {#if visible.length === 0}
193
+ {#if pending}
194
+ <p class="fieldset-label px-3 py-2">{pendingLabel}</p>
195
+ {:else if empty}
196
+ {@render empty({ query })}
197
+ {:else}
198
+ <p class="fieldset-label px-3 py-2">{emptyLabel}</p>
199
+ {/if}
200
+ {/if}
201
+
202
+ {#if footer}
203
+ {@render footer({ query, pending })}
204
+ {/if}
205
+ </Combobox.Viewport>
206
+ </Combobox.Content>
207
+ </Combobox.Portal>
208
+ </Combobox.Root>
209
+
210
+ <!--
211
+ The posted value is the id, not the typed text: bits-ui's own hidden input
212
+ is not used, because `name` here means "post the selection" and nothing
213
+ else about the widget should depend on it.
214
+ -->
215
+ {#if name !== undefined}
216
+ <input type="hidden" {name} value={value ?? ''} />
217
+ {/if}
218
+
219
+ {#if visibleError !== undefined}
220
+ <p class="text-error pt-1 text-sm" role="alert">{visibleError}</p>
221
+ {/if}
222
+ </div>
@@ -0,0 +1,98 @@
1
+ import { Combobox } from 'bits-ui';
2
+ import type { Snippet } from 'svelte';
3
+ import { type FieldSize } from './field-types.ts';
4
+ declare function $$render<T>(): {
5
+ props: {
6
+ /** The selected item's id. */
7
+ value?: string | undefined;
8
+ /** A static list, filtered here by label. Ignored when `onSearch` is set. */
9
+ items?: readonly T[] | undefined;
10
+ /**
11
+ * Fetch matches for a query. Debounced by `debounceMs`; `signal` aborts
12
+ * when a newer query supersedes this one, so pass it to `fetch`.
13
+ */
14
+ onSearch?: ((query: string, signal: AbortSignal) => Promise<readonly T[]>) | undefined;
15
+ /**
16
+ * Quiet period before a search starts, in milliseconds.
17
+ * @defaultValue `150`
18
+ */
19
+ debounceMs?: number | undefined;
20
+ /** The item's stable id — what `value` holds and what the hidden input posts. */
21
+ getId: (item: T) => string;
22
+ /** The item's human-readable text. */
23
+ getLabel: (item: T) => string;
24
+ /** Optional short chip on the right of a row. */
25
+ getBadge?: ((item: T) => string | undefined) | undefined;
26
+ /** Visible label; names the input. */
27
+ label: string;
28
+ /** Placeholder in the input. */
29
+ placeholder?: string | undefined;
30
+ /** Posted field name. Set it to render a hidden input carrying the id. */
31
+ name?: string | undefined;
32
+ /**
33
+ * Shown when a settled search returned nothing.
34
+ * @defaultValue `'No matches'`
35
+ */
36
+ emptyLabel?: string | undefined;
37
+ /**
38
+ * Shown while the first search for a query is in flight.
39
+ * @defaultValue `'Searching…'`
40
+ */
41
+ pendingLabel?: string | undefined;
42
+ /** A caller-owned failure, rendered as an alert under the input. */
43
+ errorMessage?: string | undefined;
44
+ /** `FieldControl`: the input's id. */
45
+ id?: string | undefined;
46
+ /**
47
+ * `FieldControl`: ids for `aria-describedby`.
48
+ * @defaultValue `''`
49
+ */
50
+ describedBy?: string | undefined;
51
+ /** `FieldControl`: the field is in error. */
52
+ invalid?: boolean | undefined;
53
+ /** `FieldControl`: the field must be filled. */
54
+ required?: boolean | undefined;
55
+ /** Disables the input. */
56
+ disabled?: boolean | undefined;
57
+ /**
58
+ * daisyUI size step.
59
+ * @defaultValue `'md'`
60
+ */
61
+ size?: FieldSize | undefined;
62
+ /** Extra classes, merged last. */
63
+ class?: string | undefined;
64
+ /** Replace a row's contents. */
65
+ item?: Snippet<[T]> | undefined;
66
+ /** Rendered after the rows — a "create this" row, a "see all" link. */
67
+ footer?: Snippet<[{
68
+ query: string;
69
+ pending: boolean;
70
+ }]> | undefined;
71
+ /** Replaces `emptyLabel` when a settled search returned nothing. */
72
+ empty?: Snippet<[{
73
+ query: string;
74
+ }]> | undefined;
75
+ };
76
+ exports: {};
77
+ bindings: "value";
78
+ slots: {};
79
+ events: {};
80
+ };
81
+ declare class __sveltets_Render<T> {
82
+ props(): ReturnType<typeof $$render<T>>['props'];
83
+ events(): ReturnType<typeof $$render<T>>['events'];
84
+ slots(): ReturnType<typeof $$render<T>>['slots'];
85
+ bindings(): "value";
86
+ exports(): {};
87
+ }
88
+ interface $$IsomorphicComponent {
89
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
90
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
91
+ } & ReturnType<__sveltets_Render<T>['exports']>;
92
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
93
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
94
+ }
95
+ /** A single-select typeahead over a static list or an asynchronous search, on bits-ui’s `Combobox`. */
96
+ declare const Combobox: $$IsomorphicComponent;
97
+ type Combobox<T> = InstanceType<typeof Combobox<T>>;
98
+ export default Combobox;
@@ -0,0 +1,31 @@
1
+ <!-- @component
2
+ A native `<input type="date">` in daisyUI `input` styling, with `FieldControl` wiring.
3
+ -->
4
+ <!--
5
+ DateInput — the native `<input type="date">` in daisyUI `input` styling.
6
+
7
+ `value` holds the ISO calendar date (`YYYY-MM-DD`) the control speaks
8
+ natively, which is also the shape a JSON API's date fields take, so no
9
+ conversion sits between the two.
10
+
11
+ Native on purpose, and the reason is worth stating: bits-ui's `DateField`
12
+ is a segmented control with its own locale handling and its own keyboard
13
+ contract, which is the right answer when a design needs a calendar popover
14
+ and the wrong one for a single date on a form — the platform control is
15
+ keyboard-complete, localises itself, and on a phone opens the system date
16
+ picker that people already know.
17
+ -->
18
+ <script lang="ts">import { inputSize } from "./field-types.js";
19
+ let { value = $bindable(""), min, max, size = "md", invalid = false, describedBy = "", class: className, ...rest } = $props();
20
+ </script>
21
+
22
+ <input
23
+ {...rest}
24
+ type="date"
25
+ bind:value
26
+ {min}
27
+ {max}
28
+ aria-invalid={invalid ? 'true' : undefined}
29
+ aria-describedby={describedBy === '' ? undefined : describedBy}
30
+ class={['input w-full', inputSize[size], invalid && 'input-error', className]}
31
+ />
@@ -0,0 +1,29 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import { type FieldSize } from './field-types.ts';
3
+ interface Props extends Omit<HTMLInputAttributes, 'type' | 'value' | 'size' | 'min' | 'max'> {
4
+ /**
5
+ * ISO calendar date, `YYYY-MM-DD`; empty when unset.
6
+ * @defaultValue `''`
7
+ */
8
+ value?: string | undefined;
9
+ /** Earliest selectable date, ISO. */
10
+ min?: string | undefined;
11
+ /** Latest selectable date, ISO. */
12
+ max?: string | undefined;
13
+ /**
14
+ * daisyUI size step.
15
+ * @defaultValue `'md'`
16
+ */
17
+ size?: FieldSize | undefined;
18
+ /** `FieldControl`: the field is in error — sets `aria-invalid` and the error style. */
19
+ invalid?: boolean | undefined;
20
+ /**
21
+ * `FieldControl`: space-separated ids for `aria-describedby`.
22
+ * @defaultValue `''`
23
+ */
24
+ describedBy?: string | undefined;
25
+ }
26
+ /** A native `<input type="date">` in daisyUI `input` styling, with `FieldControl` wiring. */
27
+ declare const DateInput: import("svelte").Component<Props, {}, "value">;
28
+ type DateInput = ReturnType<typeof DateInput>;
29
+ export default DateInput;
@@ -0,0 +1,30 @@
1
+ <!-- @component
2
+ The validation messages under one control: one `<p>` per message inside a single container.
3
+ -->
4
+ <!--
5
+ FieldErrors — the validation messages under one control: one `<p>` per
6
+ message inside a single container.
7
+
8
+ ARIA contract: the container is the live region (`role="alert"`), not each
9
+ paragraph — a field that fails three rules should interrupt the reader once
10
+ and then read all three, which is what one region holding three paragraphs
11
+ does and what three regions do not. It carries the `id` a control points
12
+ `aria-describedby` at, so the messages are also read on demand when focus
13
+ reaches the control.
14
+
15
+ Nothing is rendered while `errors` is empty. That is deliberate: a live
16
+ region has to be IN the document before its content changes for the change
17
+ to be announced, and inserting a populated region announces it, so the
18
+ cheapest correct thing is to mount it only when there is something to say.
19
+ -->
20
+ <script lang="ts">let { errors, id, class: className, ...rest } = $props();
21
+ export {};
22
+ </script>
23
+
24
+ {#if errors.length > 0}
25
+ <div {...rest} {id} role="alert" class={className}>
26
+ {#each errors as message, index (index)}
27
+ <p class="fieldset-label text-error">{message}</p>
28
+ {/each}
29
+ </div>
30
+ {/if}
@@ -0,0 +1,11 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLAttributes<HTMLDivElement> {
3
+ /** Messages to render, one paragraph each. */
4
+ errors: readonly string[];
5
+ /** The id a control's `aria-describedby` points at. */
6
+ id?: string | undefined;
7
+ }
8
+ /** The validation messages under one control: one `<p>` per message inside a single container. */
9
+ declare const FieldErrors: import("svelte").Component<Props, {}, "">;
10
+ type FieldErrors = ReturnType<typeof FieldErrors>;
11
+ export default FieldErrors;
@@ -0,0 +1,48 @@
1
+ <!-- @component
2
+ The `FormField` frame — legend, hint, errors — for a group of controls rather than one.
3
+ -->
4
+ <!--
5
+ Fieldset — the same frame as `FormField`, for a GROUP of controls rather
6
+ than one.
7
+
8
+ A group of related checkboxes or radios is a `<fieldset>` with a `<legend>`,
9
+ not a label pointing at one input: there is no single control for a `for`
10
+ attribute to name, and the legend is what assistive technology repeats as it
11
+ moves between the members. So the hint and the errors hang off the
12
+ `<fieldset>` itself through `aria-describedby`. There is no `aria-invalid`:
13
+ the implicit `group` role does not support it, so the invalid state is a
14
+ `data-invalid` hook for styling and the error region is what announces it.
15
+
16
+ `CheckboxGroup` is built on this. `RadioGroup` is not — bits-ui's radio
17
+ group renders its own `role="radiogroup"` element, and a role inside a
18
+ `<fieldset>` would be named by nothing, so it names itself from a heading it
19
+ renders instead. That is the one place the two frames diverge.
20
+ -->
21
+ <script lang="ts">import FieldErrors from "./FieldErrors.svelte";
22
+ let { legend, hint, errors = [], required = false, requiredLabel = "(required)", children, class: className, ...rest } = $props();
23
+ const uid = $props.id();
24
+ const hintId = `${uid}-hint`;
25
+ const errorId = `${uid}-error`;
26
+ const invalid = $derived(errors.length > 0);
27
+ const describedBy = $derived([hint === undefined ? "" : hintId, invalid ? errorId : ""].filter((part) => part !== "").join(" "));
28
+ </script>
29
+
30
+ <fieldset
31
+ {...rest}
32
+ aria-describedby={describedBy === '' ? undefined : describedBy}
33
+ data-invalid={invalid ? '' : undefined}
34
+ class={['fieldset', className]}
35
+ >
36
+ <legend class="fieldset-legend">
37
+ {legend}
38
+ {#if required}
39
+ <span class="text-error" aria-hidden="true">*</span>
40
+ <span class="sr-only">{requiredLabel}</span>
41
+ {/if}
42
+ </legend>
43
+ {@render children()}
44
+ {#if hint !== undefined}
45
+ <p class="fieldset-label" id={hintId}>{hint}</p>
46
+ {/if}
47
+ <FieldErrors {errors} id={errorId} />
48
+ </fieldset>
@@ -0,0 +1,26 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, 'children'> {
4
+ /** The group's `<legend>`; names every control inside it. */
5
+ legend: string;
6
+ /** Guidance rendered under the legend. */
7
+ hint?: string | undefined;
8
+ /**
9
+ * Validation messages; any at all marks the group invalid.
10
+ * @defaultValue `[]`
11
+ */
12
+ errors?: readonly string[] | undefined;
13
+ /** Marks the group required and reads `requiredLabel` after the legend. */
14
+ required?: boolean | undefined;
15
+ /**
16
+ * Read out after the legend when `required`.
17
+ * @defaultValue `'(required)'`
18
+ */
19
+ requiredLabel?: string | undefined;
20
+ /** The grouped controls. */
21
+ children: Snippet<[]>;
22
+ }
23
+ /** The `FormField` frame — legend, hint, errors — for a group of controls rather than one. */
24
+ declare const Fieldset: import("svelte").Component<Props, {}, "">;
25
+ type Fieldset = ReturnType<typeof Fieldset>;
26
+ export default Fieldset;
@@ -0,0 +1,67 @@
1
+ <!-- @component
2
+ A native file input in daisyUI `file-input` styling, with the chosen `File` bindable and a client-side size cap.
3
+ -->
4
+ <!--
5
+ FileInput — a native `<input type="file">` in daisyUI `file-input` styling,
6
+ with the chosen `File` exposed as a bindable prop and a client-side size cap.
7
+
8
+ It does NOT upload anything. The transfer belongs to the caller — a form
9
+ POST, a direct-to-storage PUT, a resumable client — and a component every
10
+ page imports has no business carrying one. `ImageInput` and `VideoInput`
11
+ build on this: the selection and the messaging are shared, the transfer is
12
+ not.
13
+
14
+ `maxBytes` is enforced here as well as on the server because a file rejected
15
+ after the transfer is a uniquely expensive way to say no. An over-size file
16
+ is cleared from the control rather than left sitting in it, so what the
17
+ control shows and what `file` holds cannot disagree.
18
+
19
+ ARIA contract: a native file input. `aria-invalid` is set when the field is
20
+ invalid OR the chosen file was too large, and the too-large message is an
21
+ `alert` region the input's `aria-describedby` also points at, so the reason
22
+ is announced and is available again on focus.
23
+ -->
24
+ <script lang="ts">import FieldErrors from "./FieldErrors.svelte";
25
+ import { fileInputSize } from "./field-types.js";
26
+ let { file = $bindable(), accept, maxBytes, tooLargeMessage = "That file is too large.", onSelect, size = "md", invalid = false, describedBy = "", class: className, ...rest } = $props();
27
+ const uid = $props.id();
28
+ const tooLargeId = `${uid}-too-large`;
29
+ let tooLarge = $state(false);
30
+ const described = $derived([describedBy, tooLarge ? tooLargeId : ""].filter((part) => part !== "").join(" "));
31
+ const choose = (control) => {
32
+ const chosen = control.files?.[0];
33
+ tooLarge = false;
34
+ if (chosen === undefined) {
35
+ file = undefined;
36
+ onSelect?.(undefined);
37
+ return;
38
+ }
39
+ if (maxBytes !== undefined && chosen.size > maxBytes) {
40
+ tooLarge = true;
41
+ // Clear the control too: leaving the name of a file that was refused
42
+ // showing in the field is how someone submits and wonders what happened.
43
+ control.value = "";
44
+ file = undefined;
45
+ onSelect?.(undefined);
46
+ return;
47
+ }
48
+ file = chosen;
49
+ onSelect?.(chosen);
50
+ };
51
+ </script>
52
+
53
+ <input
54
+ {...rest}
55
+ type="file"
56
+ {accept}
57
+ onchange={(event) => choose(event.currentTarget)}
58
+ aria-invalid={invalid || tooLarge ? 'true' : undefined}
59
+ aria-describedby={described === '' ? undefined : described}
60
+ class={[
61
+ 'file-input w-full',
62
+ fileInputSize[size],
63
+ (invalid || tooLarge) && 'file-input-error',
64
+ className,
65
+ ]}
66
+ />
67
+ <FieldErrors errors={tooLarge ? [tooLargeMessage] : []} id={tooLargeId} />
@@ -0,0 +1,33 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import { type FieldSize } from './field-types.ts';
3
+ interface Props extends Omit<HTMLInputAttributes, 'type' | 'value' | 'files' | 'size'> {
4
+ /** The chosen file; `undefined` when the control is empty. */
5
+ file?: File | undefined;
6
+ /** `accept` attribute value. */
7
+ accept?: string | undefined;
8
+ /** Largest file to accept, in bytes. Omitted, there is no cap. */
9
+ maxBytes?: number | undefined;
10
+ /**
11
+ * Shown when the chosen file exceeds `maxBytes`.
12
+ * @defaultValue `'That file is too large.'`
13
+ */
14
+ tooLargeMessage?: string | undefined;
15
+ /** Called with the accepted file, or `undefined` when the choice cleared it. */
16
+ onSelect?: ((file: File | undefined) => void) | undefined;
17
+ /**
18
+ * daisyUI size step.
19
+ * @defaultValue `'md'`
20
+ */
21
+ size?: FieldSize | undefined;
22
+ /** `FieldControl`: the field is in error — sets `aria-invalid` and the error style. */
23
+ invalid?: boolean | undefined;
24
+ /**
25
+ * `FieldControl`: space-separated ids for `aria-describedby`.
26
+ * @defaultValue `''`
27
+ */
28
+ describedBy?: string | undefined;
29
+ }
30
+ /** A native file input in daisyUI `file-input` styling, with the chosen `File` bindable and a client-side size cap. */
31
+ declare const FileInput: import("svelte").Component<Props, {}, "file">;
32
+ type FileInput = ReturnType<typeof FileInput>;
33
+ export default FileInput;
@@ -0,0 +1,54 @@
1
+ <!-- @component
2
+ The label, hint and errors frame around one control, handing it the `FieldControl` wiring.
3
+ -->
4
+ <!--
5
+ FormField — the label + hint + errors frame around one control.
6
+
7
+ The control is a snippet that receives a `FieldControl` (`id`,
8
+ `describedBy`, `invalid`, `required`) to spread onto the primitive, so the
9
+ label's `for`, the control's `aria-describedby` and `aria-invalid`, and the
10
+ error region's `role="alert"` all stay wired together from a single `id`.
11
+ Neither side has to know the other's markup, which is what lets a caller
12
+ drop any of the field primitives — or their own `<input>` — into the slot.
13
+
14
+ ARIA contract: `<label for>` names the control (so clicking the text focuses
15
+ it); `describedBy` lists the hint's id then the error region's id, in that
16
+ reading order, and is empty when there is neither; `invalid` is true exactly
17
+ when there is at least one error. The required marker is a `*` that is
18
+ `aria-hidden` beside a visually-hidden `requiredLabel`, both inside the
19
+ `<label>`, so the accessible name reads "Scientific name (required)" rather
20
+ than "Scientific name star".
21
+
22
+ `errors` is a LIST. A field that fails several rules says so; collapsing
23
+ them to the first message is a decision the call site should make, not one
24
+ this frame makes for it.
25
+ -->
26
+ <script lang="ts">import FieldErrors from "./FieldErrors.svelte";
27
+ let { id: idProp, label, hint, errors = [], required = false, requiredLabel = "(required)", children, class: className, ...rest } = $props();
28
+ const uid = $props.id();
29
+ const id = $derived(idProp ?? uid);
30
+ const hintId = $derived(`${id}-hint`);
31
+ const errorId = $derived(`${id}-error`);
32
+ const invalid = $derived(errors.length > 0);
33
+ const control = $derived({
34
+ id,
35
+ describedBy: [hint === undefined ? "" : hintId, invalid ? errorId : ""].filter((part) => part !== "").join(" "),
36
+ invalid,
37
+ required
38
+ });
39
+ </script>
40
+
41
+ <div {...rest} class={['fieldset', className]}>
42
+ <label class="label" for={id}>
43
+ <span>{label}</span>
44
+ {#if required}
45
+ <span class="text-error" aria-hidden="true">*</span>
46
+ <span class="sr-only">{requiredLabel}</span>
47
+ {/if}
48
+ </label>
49
+ {@render children(control)}
50
+ {#if hint !== undefined}
51
+ <p class="fieldset-label" id={hintId}>{hint}</p>
52
+ {/if}
53
+ <FieldErrors {errors} id={errorId} />
54
+ </div>