@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,21 @@
1
+ import type { HTMLImgAttributes } from 'svelte/elements';
2
+ interface Props extends Omit<HTMLImgAttributes, 'src' | 'alt' | 'width' | 'height' | 'loading' | 'class'> {
3
+ /** Image URL. */
4
+ src: string;
5
+ /** Alternative text; `''` only where the image is decoration. */
6
+ alt: string;
7
+ /** Rendered as the `<figcaption>`. */
8
+ caption?: string | undefined;
9
+ /** Intrinsic width in pixels; reserves the box. */
10
+ width?: number | undefined;
11
+ /** Intrinsic height in pixels; reserves the box. */
12
+ height?: number | undefined;
13
+ /** Eager-loads at high `fetchpriority` — for the page's LCP candidate. */
14
+ eager?: boolean | undefined;
15
+ /** Extra classes, merged last. */
16
+ class?: string | undefined;
17
+ }
18
+ /** The picture at the top of an `Infobox`, with its caption. */
19
+ declare const InfoboxImage: import("svelte").Component<Props, {}, "">;
20
+ type InfoboxImage = ReturnType<typeof InfoboxImage>;
21
+ export default InfoboxImage;
@@ -0,0 +1,50 @@
1
+ <!-- @component
2
+ One label/value pair inside an `InfoboxSection`.
3
+ -->
4
+ <!--
5
+ InfoboxRow — one label/value pair inside an `InfoboxSection`.
6
+
7
+ Absent by default: a row with neither a `value` nor `children` renders no
8
+ DOM at all, so a page can build a run of rows straight off sparse resource
9
+ attributes without guarding each one — the same "has something to show"
10
+ contract `infoboxRowIsPresent` gives the typed `rows` convenience on
11
+ `Infobox`. `showEmpty` opts back: an em
12
+ dash captioned by `emptyLabel`, which says "asked, and there is no answer"
13
+ rather than "nobody asked" — for a section that always lists the same
14
+ facts, where a gap is itself informative.
15
+
16
+ `href` wraps the row's content in the shared `Link`, so an in-app path
17
+ resolves through the components context exactly like every other link in
18
+ the library; `external` is `Link`'s own external-link contract, passed
19
+ straight through.
20
+ -->
21
+ <script lang="ts">import Link from "../ui/Link.svelte";
22
+ let { label, value, children, href, external, emptyLabel = "unrecorded", showEmpty = false, class: className, ...rest } = $props();
23
+ const hasValue = $derived(value !== undefined && value !== "");
24
+ const present = $derived(children !== undefined || hasValue);
25
+ </script>
26
+
27
+ {#if present || showEmpty}
28
+ <div
29
+ {...rest}
30
+ class={[
31
+ 'hover:bg-base-100 grid grid-cols-3 gap-2 px-4 py-2 transition-colors',
32
+ className,
33
+ ]}
34
+ >
35
+ <dt class="text-base-content/70 col-span-1 text-sm font-semibold select-none">{label}</dt>
36
+ <dd class="col-span-2 text-sm">
37
+ {#if present && href !== undefined}
38
+ <Link {href} external={external ?? false}>
39
+ {#if children}{@render children()}{:else}{value}{/if}
40
+ </Link>
41
+ {:else if present && children}
42
+ {@render children()}
43
+ {:else if present}
44
+ {value}
45
+ {:else}
46
+ <span class="text-base-content/40" aria-label={emptyLabel}>&mdash;</span>
47
+ {/if}
48
+ </dd>
49
+ </div>
50
+ {/if}
@@ -0,0 +1,25 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
4
+ /** The term, rendered as the `<dt>`. */
5
+ label: string;
6
+ /** Plain text or number; `children` wins where both are supplied. */
7
+ value?: string | number | undefined;
8
+ /** Richer markup — a formatted quantity, a badge, more than one link. */
9
+ children?: Snippet<[]> | undefined;
10
+ /** Wraps the row's content in a `Link` resolved through the components context. */
11
+ href?: string | undefined;
12
+ /** Passed through to `Link` — an off-site destination. */
13
+ external?: boolean | undefined;
14
+ /**
15
+ * Stands in for the value, captioned by `aria-label`, when `showEmpty` and the row has nothing.
16
+ * @defaultValue `'unrecorded'`
17
+ */
18
+ emptyLabel?: string | undefined;
19
+ /** Render the row even when empty, showing `emptyLabel` instead of nothing. */
20
+ showEmpty?: boolean | undefined;
21
+ }
22
+ /** One label/value pair inside an `InfoboxSection`. */
23
+ declare const InfoboxRow: import("svelte").Component<Props, {}, "">;
24
+ type InfoboxRow = ReturnType<typeof InfoboxRow>;
25
+ export default InfoboxRow;
@@ -0,0 +1,24 @@
1
+ <!-- @component
2
+ A titled group of `InfoboxRow`s, with dividing rules between them.
3
+ -->
4
+ <!--
5
+ InfoboxSection — a titled group of `InfoboxRow`s, with the dividing rules
6
+ between them.
7
+
8
+ The `<dl>` lives here rather than on each row: a definition list whose
9
+ terms and definitions are split across a dozen sibling lists is not one
10
+ list, and a screen reader reading "list of 1 item" twelve times is the
11
+ audible version of that mistake. Rows contribute the `<dt>`/`<dd>` pairs.
12
+ -->
13
+ <script lang="ts">let { title, children, class: className, ...rest } = $props();
14
+ export {};
15
+ </script>
16
+
17
+ {#if title !== undefined && title !== ''}
18
+ <div class="border-base-300 bg-base-300 border-b px-4 py-2">
19
+ <h4 class="text-sm font-bold select-none">{title}</h4>
20
+ </div>
21
+ {/if}
22
+ <dl {...rest} class={['divide-base-300 divide-y', className]}>
23
+ {@render children()}
24
+ </dl>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLDListElement> {
4
+ /** Names the group; omitted for an untitled run of rows. */
5
+ title?: string | undefined;
6
+ /** The rows — `InfoboxRow`s, or anything else that belongs in a `<dl>`. */
7
+ children: Snippet<[]>;
8
+ }
9
+ /** A titled group of `InfoboxRow`s, with dividing rules between them. */
10
+ declare const InfoboxSection: import("svelte").Component<Props, {}, "">;
11
+ type InfoboxSection = ReturnType<typeof InfoboxSection>;
12
+ export default InfoboxSection;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * infobox — the family that fills an entity page's summary rail, two ways
3
+ * at once.
4
+ *
5
+ * Composed primitives (`InfoboxHeader`, `InfoboxImage`, `InfoboxSection`,
6
+ * `InfoboxRow`) let a page build bespoke markup. A
7
+ * typed `rows` convenience on `Infobox` itself — the `InfoboxRowSpec` union
8
+ * and `infoboxRowIsPresent` — lets a page instead hand over a
9
+ * `$derived<InfoboxRowSpec[]>` built from sparse entity attributes; `rows` renders through the very
10
+ * same primitives, so a fix to row layout lands in one place regardless of
11
+ * which API a caller used to reach it.
12
+ *
13
+ * @module
14
+ */
15
+ export { default as Infobox } from './Infobox.svelte';
16
+ export { default as InfoboxHeader } from './InfoboxHeader.svelte';
17
+ export { default as InfoboxImage } from './InfoboxImage.svelte';
18
+ export { default as InfoboxRow } from './InfoboxRow.svelte';
19
+ export { default as InfoboxSection } from './InfoboxSection.svelte';
20
+ export { infoboxRowIsPresent } from './types.ts';
21
+ export type { InfoboxCustomRow, InfoboxFactRow, InfoboxImageRow, InfoboxImageSpec, InfoboxLinkEntry, InfoboxLinkRow, InfoboxLinksRow, InfoboxRowSpec, } from './types.ts';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * infobox — the family that fills an entity page's summary rail, two ways
3
+ * at once.
4
+ *
5
+ * Composed primitives (`InfoboxHeader`, `InfoboxImage`, `InfoboxSection`,
6
+ * `InfoboxRow`) let a page build bespoke markup. A
7
+ * typed `rows` convenience on `Infobox` itself — the `InfoboxRowSpec` union
8
+ * and `infoboxRowIsPresent` — lets a page instead hand over a
9
+ * `$derived<InfoboxRowSpec[]>` built from sparse entity attributes; `rows` renders through the very
10
+ * same primitives, so a fix to row layout lands in one place regardless of
11
+ * which API a caller used to reach it.
12
+ *
13
+ * @module
14
+ */
15
+ export { default as Infobox } from './Infobox.svelte';
16
+ export { default as InfoboxHeader } from './InfoboxHeader.svelte';
17
+ export { default as InfoboxImage } from './InfoboxImage.svelte';
18
+ export { default as InfoboxRow } from './InfoboxRow.svelte';
19
+ export { default as InfoboxSection } from './InfoboxSection.svelte';
20
+ export { infoboxRowIsPresent } from "./types.js";
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The shapes the `infobox` family passes around: the image spec both
3
+ * `InfoboxImage` and an `image`-kind row accept, the row union `Infobox`'s
4
+ * `rows` convenience renders, and the single "has something to show" rule
5
+ * the component and its tests share.
6
+ *
7
+ * @module
8
+ */
9
+ import type { Snippet } from 'svelte';
10
+ /** The picture at the top of an `Infobox`. */
11
+ export interface InfoboxImageSpec {
12
+ readonly src: string;
13
+ /** Empty for a purely decorative image; a description otherwise. */
14
+ readonly alt: string;
15
+ /** Shown beneath the picture — a photographer, a provenance, a scale. */
16
+ readonly caption?: string | undefined;
17
+ /** Intrinsic dimensions; supplying both is what stops the layout shifting. */
18
+ readonly width?: number | undefined;
19
+ readonly height?: number | undefined;
20
+ /**
21
+ * Eager-loads at high `fetchpriority`. Set when this image is the page's
22
+ * LCP candidate — typically an infobox's own identifying picture.
23
+ */
24
+ readonly eager?: boolean | undefined;
25
+ }
26
+ /** A single identifying fact (e.g. publication year, ISBN, page count). */
27
+ export interface InfoboxFactRow {
28
+ readonly kind: 'fact';
29
+ readonly label: string;
30
+ /** `undefined` or an empty string skips the row. */
31
+ readonly value: string | number | undefined;
32
+ }
33
+ /** A single named link, rendered as one row with its own visible text. */
34
+ export interface InfoboxLinkRow {
35
+ readonly kind: 'link';
36
+ readonly label: string;
37
+ readonly text: string;
38
+ /** `undefined` or an empty string skips the row. */
39
+ readonly href: string | undefined;
40
+ /** Passed through to the underlying `Link` — an off-site destination. */
41
+ readonly external?: boolean | undefined;
42
+ }
43
+ /** One entry of an `InfoboxLinksRow`. */
44
+ export interface InfoboxLinkEntry {
45
+ readonly label: string;
46
+ readonly href: string;
47
+ }
48
+ /** A named outbound relationship rendered as one or more links. */
49
+ export interface InfoboxLinksRow {
50
+ readonly kind: 'links';
51
+ readonly label: string;
52
+ /** `undefined` or an empty list skips the row. */
53
+ readonly links: readonly InfoboxLinkEntry[] | undefined;
54
+ }
55
+ /**
56
+ * A hero/identifying image row. Renders above the `<dl>` as an
57
+ * `InfoboxImage`, not as a `dt`/`dd` pair — the same shape `Infobox`'s own
58
+ * `image` prop takes, offered as a row for the callers who build the whole
59
+ * infobox from one `rows` array.
60
+ */
61
+ export interface InfoboxImageRow extends InfoboxImageSpec {
62
+ readonly kind: 'image';
63
+ }
64
+ /** A row whose value is arbitrary markup (a badge, a formatted quantity). */
65
+ export interface InfoboxCustomRow {
66
+ readonly kind: 'custom';
67
+ readonly label: string;
68
+ readonly content: Snippet<[]>;
69
+ }
70
+ /**
71
+ * Discriminated union of every row kind `Infobox`'s `rows` prop accepts.
72
+ *
73
+ * Named `InfoboxRowSpec` rather than `InfoboxRow` (design.md's own name for
74
+ * it) because the family already has an `InfoboxRow` COMPONENT: Svelte's
75
+ * compiled default export carries a type meaning as well as a value one, so
76
+ * a value export and a type export cannot share that identifier in the same
77
+ * module without a "Duplicate identifier" error. `*Spec` matches
78
+ * `InfoboxImageSpec`, the sibling type this union's `image` member reuses.
79
+ */
80
+ export type InfoboxRowSpec = InfoboxFactRow | InfoboxLinkRow | InfoboxLinksRow | InfoboxImageRow | InfoboxCustomRow;
81
+ /**
82
+ * Whether a row carries renderable content — the single definition of
83
+ * "empty" `Infobox` and its unit tests share. A `custom` row is always
84
+ * present: its snippet is the caller's decision, not this function's.
85
+ */
86
+ export declare function infoboxRowIsPresent(row: InfoboxRowSpec): boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The shapes the `infobox` family passes around: the image spec both
3
+ * `InfoboxImage` and an `image`-kind row accept, the row union `Infobox`'s
4
+ * `rows` convenience renders, and the single "has something to show" rule
5
+ * the component and its tests share.
6
+ *
7
+ * @module
8
+ */
9
+ /**
10
+ * Whether a row carries renderable content — the single definition of
11
+ * "empty" `Infobox` and its unit tests share. A `custom` row is always
12
+ * present: its snippet is the caller's decision, not this function's.
13
+ */
14
+ export function infoboxRowIsPresent(row) {
15
+ switch (row.kind) {
16
+ case 'fact':
17
+ return row.value !== undefined && row.value !== '';
18
+ case 'link':
19
+ return row.href !== undefined && row.href !== '';
20
+ case 'links':
21
+ return row.links !== undefined && row.links.length > 0;
22
+ case 'image':
23
+ return row.src !== '';
24
+ case 'custom':
25
+ return true;
26
+ }
27
+ }
@@ -0,0 +1,52 @@
1
+ <!-- @component
2
+ The ancestor trail above a page title, as daisyUI `breadcrumbs` inside a labelled navigation landmark.
3
+ -->
4
+ <!--
5
+ Breadcrumbs — the ancestor trail above a page title, as daisyUI's
6
+ `breadcrumbs` inside a labelled navigation landmark.
7
+
8
+ ARIA contract: a `<nav aria-label>` wrapping an `<ol>`, because a trail is
9
+ ordered and the order is the meaning — "Home, then Elections, then this".
10
+ Ancestors are links; the TRAILING crumb is rendered as text and carries
11
+ `aria-current="page"`, even when the caller gave it an `href`. A link to the
12
+ page you are already on is a link that does nothing, and `aria-current` is
13
+ what says "this is where you are" to someone who cannot see the trail.
14
+
15
+ `item` lets a caller render a crumb's contents themselves — an icon beside
16
+ the label, a `<Badge>`, a truncated middle. The `<li>` and the
17
+ `aria-current` stay this component's either way: when `item` is supplied the
18
+ attribute moves to the `<li>`, so the contract holds whatever the snippet
19
+ renders inside it.
20
+
21
+ Hrefs resolve through the components context, so a base path or a locale
22
+ prefix is applied once by the app rather than at every call site.
23
+ -->
24
+ <script lang="ts">import { getComponentsContext } from "../context.svelte.js";
25
+ let { items, label = "Breadcrumb", item, class: className, ...rest } = $props();
26
+ const context = getComponentsContext();
27
+ </script>
28
+
29
+ <nav
30
+ {...rest}
31
+ aria-label={label}
32
+ class={['breadcrumbs text-base-content/70 text-sm', className]}
33
+ >
34
+ <ol>
35
+ {#each items as crumb, index (index)}
36
+ {@const current = index === items.length - 1}
37
+ {#if item}
38
+ <li aria-current={current ? 'page' : undefined}>{@render item(crumb, index)}</li>
39
+ {:else}
40
+ <li>
41
+ {#if crumb.href !== undefined && !current}
42
+ <a href={context.href(crumb.href)} class="link link-hover">{crumb.label}</a>
43
+ {:else}
44
+ <span class="text-base-content" aria-current={current ? 'page' : undefined}>
45
+ {crumb.label}
46
+ </span>
47
+ {/if}
48
+ </li>
49
+ {/if}
50
+ {/each}
51
+ </ol>
52
+ </nav>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { Crumb } from './types.ts';
4
+ interface Props extends HTMLAttributes<HTMLElement> {
5
+ /** Ordered ancestors, ending with the current page. */
6
+ items: readonly Crumb[];
7
+ /**
8
+ * Accessible name of the navigation landmark.
9
+ * @defaultValue `'Breadcrumb'`
10
+ */
11
+ label?: string | undefined;
12
+ /** Render one crumb's contents. The `<li>` wrapper is still this component's. */
13
+ item?: Snippet<[Crumb, number]> | undefined;
14
+ }
15
+ /** The ancestor trail above a page title, as daisyUI `breadcrumbs` inside a labelled navigation landmark. */
16
+ declare const Breadcrumbs: import("svelte").Component<Props, {}, "">;
17
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
18
+ export default Breadcrumbs;
@@ -0,0 +1,50 @@
1
+ <!-- @component
2
+ The two-column detail-page shell: a header across the top, a body column and a summary aside.
3
+ -->
4
+ <!--
5
+ DetailLayout — the two-column detail-page shell: a header across the top, a
6
+ body column, and a summary aside beside it.
7
+
8
+ The reflow is the whole point. On a wide viewport the aside sits to the
9
+ RIGHT of the body (a third of the width, the Wikipedia infobox position); on
10
+ a phone there is no beside, and the useful order is title → aside → body,
11
+ because the aside is the facts-at-a-glance block and a reader who has to
12
+ scroll past the whole article to reach it will never reach it. That is done
13
+ by putting the aside BEFORE the body in the column flow (`order-first`) and
14
+ overriding to explicit grid placement from `lg` — not by rendering it twice,
15
+ which would duplicate every id and every heading in the accessibility tree.
16
+
17
+ ARIA contract: the aside is an `<aside aria-label>` — a `complementary`
18
+ landmark, which is exactly what it is: content related to the page's main
19
+ content and meaningful on its own. It is named because a landmark a reader
20
+ cannot tell apart from another landmark is a landmark they will not use.
21
+ `header` is a snippet rather than a set of title/subtitle props: it takes a
22
+ `PageHeader`, which already owns the heading contract.
23
+
24
+ This renders no `<main>`. It is placed INSIDE `Main` by the page, because a
25
+ page that is two columns of a wider shell must not open a second landmark.
26
+ `data-testid="detail-body"` / `"detail-infobox"` are carried over.
27
+ -->
28
+ <script lang="ts">let { header, body, aside, asideLabel = "Summary", class: className, ...rest } = $props();
29
+ export {};
30
+ </script>
31
+
32
+ <div {...rest} class={['space-y-8', className]}>
33
+ {@render header()}
34
+
35
+ <div class="flex flex-col gap-8 lg:grid lg:grid-cols-3 lg:items-start">
36
+ <div data-testid="detail-body" class="min-w-0 space-y-8 lg:col-span-2 lg:col-start-1 lg:row-start-1">
37
+ {@render body()}
38
+ </div>
39
+
40
+ {#if aside}
41
+ <aside
42
+ data-testid="detail-infobox"
43
+ aria-label={asideLabel}
44
+ class="order-first min-w-0 lg:order-none lg:col-start-3 lg:row-start-1"
45
+ >
46
+ {@render aside()}
47
+ </aside>
48
+ {/if}
49
+ </div>
50
+ </div>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLDivElement> {
4
+ /** The page header block — normally a `PageHeader`. */
5
+ header: Snippet<[]>;
6
+ /** The main body column. */
7
+ body: Snippet<[]>;
8
+ /** The summary column — an infobox, a fact list, a map. */
9
+ aside?: Snippet<[]> | undefined;
10
+ /**
11
+ * Accessible name of the `<aside>` landmark.
12
+ * @defaultValue `'Summary'`
13
+ */
14
+ asideLabel?: string | undefined;
15
+ }
16
+ /** The two-column detail-page shell: a header across the top, a body column and a summary aside. */
17
+ declare const DetailLayout: import("svelte").Component<Props, {}, "">;
18
+ type DetailLayout = ReturnType<typeof DetailLayout>;
19
+ export default DetailLayout;
@@ -0,0 +1,40 @@
1
+ <!-- @component
2
+ The row of filter options above a list, as links rather than a form.
3
+ -->
4
+ <!--
5
+ FilterBar — the row of filter options above a list, as links rather than a
6
+ form.
7
+
8
+ Links, deliberately: a filtered view is then a URL an operator can bookmark,
9
+ paste into a message, or reload — which a `<select>` that mutates client
10
+ state is not. `href` is supplied per option by the page, because a cursor,
11
+ a sort order and a search term all have to travel with the filter and only
12
+ the page knows what the rest of its query string is.
13
+
14
+ `active` is the caller's to decide for the same reason: only the page knows
15
+ which axis this row filters on.
16
+
17
+ ARIA contract: a `<nav aria-label>` — each surface filters on a different
18
+ axis ("Filter by status", "Filter by licence") and an unnamed second
19
+ navigation landmark on a page is one a reader cannot tell from the first.
20
+ The active option carries `aria-current="page"` as well as daisyUI's
21
+ `btn-active`, because the highlight is a colour and the attribute is what
22
+ says so out loud. The options are anchors, not buttons, so the `btn` class
23
+ here is a costume, not a role.
24
+ -->
25
+ <script lang="ts">import { getComponentsContext } from "../context.svelte.js";
26
+ let { options, label, class: className, ...rest } = $props();
27
+ const context = getComponentsContext();
28
+ </script>
29
+
30
+ <nav {...rest} aria-label={label} class={['flex flex-wrap items-center gap-2', className]}>
31
+ {#each options as option (option.key)}
32
+ <a
33
+ href={context.href(option.href)}
34
+ class={['btn btn-sm', option.active && 'btn-active']}
35
+ aria-current={option.active ? 'page' : undefined}
36
+ >
37
+ {option.label}
38
+ </a>
39
+ {/each}
40
+ </nav>
@@ -0,0 +1,12 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { FilterOption } from './types.ts';
3
+ interface Props extends HTMLAttributes<HTMLElement> {
4
+ /** The filters; the current one gets `aria-current`. */
5
+ options: readonly FilterOption[];
6
+ /** Accessible name for the row — say which axis it filters on. */
7
+ label: string;
8
+ }
9
+ /** The row of filter options above a list, as links rather than a form. */
10
+ declare const FilterBar: import("svelte").Component<Props, {}, "">;
11
+ type FilterBar = ReturnType<typeof FilterBar>;
12
+ export default FilterBar;
@@ -0,0 +1,32 @@
1
+ <!-- @component
2
+ The site footer: a `contentinfo` landmark with columns of links and an optional aside for the brand and attribution notices.
3
+ -->
4
+ <!--
5
+ Footer — the site footer: a `contentinfo` landmark holding the columns of
6
+ links an app puts at the bottom of every page, and an optional `aside` for
7
+ the brand block and the attribution notices (a data licence, a copyright
8
+ line) that belong beside them rather than in them.
9
+
10
+ ARIA contract: `<footer>` at the top of the document is the `contentinfo`
11
+ landmark — the one a screen-reader user jumps to for "the small print".
12
+ There is one per page, so a footer INSIDE an `<article>` or a `<section>` is
13
+ not this component. `aside` renders first, which is where the attribution
14
+ block reads: the columns are navigation, and navigation should not be
15
+ preceded by nothing.
16
+
17
+ Structure only. The columns are the app's — typically `<nav>` blocks with a
18
+ `footer-title` heading each, which is what makes daisyUI's grid line up.
19
+ -->
20
+ <script lang="ts">let { children, aside, class: className, ...rest } = $props();
21
+ export {};
22
+ </script>
23
+
24
+ <footer
25
+ {...rest}
26
+ class={['footer sm:footer-horizontal bg-base-200 text-base-content mt-auto p-10', className]}
27
+ >
28
+ {#if aside}
29
+ <aside>{@render aside()}</aside>
30
+ {/if}
31
+ {@render children()}
32
+ </footer>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLElement> {
4
+ /** The columns — `<nav>` blocks with `footer-title` headings. */
5
+ children: Snippet<[]>;
6
+ /** Brand and attribution block, rendered before the columns. */
7
+ aside?: Snippet<[]> | undefined;
8
+ }
9
+ /** The site footer: a `contentinfo` landmark with columns of links and an optional aside for the brand and attribution notices. */
10
+ declare const Footer: import("svelte").Component<Props, {}, "">;
11
+ type Footer = ReturnType<typeof Footer>;
12
+ export default Footer;
@@ -0,0 +1,30 @@
1
+ <!-- @component
2
+ The page’s content column and the `<main>` landmark.
3
+ -->
4
+ <!--
5
+ Main — the page's content column, and the `<main>` landmark that names it.
6
+
7
+ Every app in the survey had a component called `Main` and none of them
8
+ rendered a `<main>`: each one was a `<section>`, because each was mounted
9
+ inside a shell that had already opened the real `<main>`. That is a correct
10
+ thing to want and a bad thing to leave implicit, so the element is a prop.
11
+ `as="main"` is the default because a page that needs a landmark and does not
12
+ get one is the failure that matters; `as="section"` is for the nested case.
13
+
14
+ ARIA contract: exactly one `<main>` per rendered page — the landmark a
15
+ screen-reader user jumps to with a single keystroke, and the target of a
16
+ skip link. There must never be two, which is what `as="section"` exists to
17
+ prevent. A `<section>` is a `region` landmark only when it has an accessible
18
+ name, so pass `aria-label`/`aria-labelledby` (spread through `rest`) when
19
+ the nested block is one worth jumping to, and nothing when it is not.
20
+ -->
21
+ <script lang="ts">let { as = "main", children, class: className, ...rest } = $props();
22
+ export {};
23
+ </script>
24
+
25
+ <!-- `min-w-0` is the one class worth imposing: a content column is nearly
26
+ always a flex or grid child, and without it a wide table or a long
27
+ unbroken URL inside pushes the whole layout sideways. -->
28
+ <svelte:element this={as} {...rest} class={['min-w-0', className]}>
29
+ {@render children()}
30
+ </svelte:element>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLElement> {
4
+ /**
5
+ * `main`, or `section` when a `<main>` is already open.
6
+ * @defaultValue `'main'`
7
+ */
8
+ as?: 'main' | 'section' | undefined;
9
+ /** The page content, inside the landmark and the content column. */
10
+ children: Snippet<[]>;
11
+ }
12
+ /** The page’s content column and the `<main>` landmark. */
13
+ declare const Main: import("svelte").Component<Props, {}, "">;
14
+ type Main = ReturnType<typeof Main>;
15
+ export default Main;
@@ -0,0 +1,68 @@
1
+ <!-- @component
2
+ The site banner: brand, primary links and account controls, with a disclosure for what does not fit on a narrow viewport.
3
+ -->
4
+ <!--
5
+ Navbar — the site's banner: the brand, the primary links, the account
6
+ controls, and on a narrow viewport the disclosure that holds whatever did
7
+ not fit.
8
+
9
+ The three regions are snippets (`start` / `center` / `end`) because the
10
+ content is the app's — its brand mark, its search form, its `Menu
11
+ mode="dropdown"` account menu. What this owns is the structure: the banner
12
+ landmark, the named navigation inside it, the responsive bar, and the
13
+ toggle.
14
+
15
+ The toggle is bits-ui's `Collapsible`, not a hand-rolled button and a
16
+ `hidden` div (which is what three of the four apps had) and not a
17
+ `<details>`. `<details>` would be tempting — it is the native disclosure —
18
+ but its ARIA is implicit: the browser maps the open state internally and
19
+ exposes no `aria-controls`, so the toggle cannot say WHICH region it opens,
20
+ and a `<summary>` cannot be styled as a `btn` without fighting the marker.
21
+ `Collapsible` gives the pairing this needs and nothing else: a real
22
+ `<button type="button">` carrying `aria-expanded` and `aria-controls`
23
+ pointing at the panel's id, and the panel kept in the DOM with `hidden` when
24
+ closed so the reference always resolves.
25
+
26
+ ARIA contract: `<header>` at the top of the document is the `banner`
27
+ landmark; the `<nav aria-label>` inside it is the named navigation. The
28
+ toggle is an icon button with a real accessible name (`menuLabel`) — its
29
+ glyph is `aria-hidden`, so the name never changes as the state does; the
30
+ state is `aria-expanded`'s job, not the label's. `menuOpen` is bindable, so
31
+ an app can close the panel after a route change.
32
+ -->
33
+ <script lang="ts">import { Collapsible } from "bits-ui";
34
+ import { Menu as MenuIcon } from "@lucide/svelte";
35
+ import VisuallyHidden from "../ui/VisuallyHidden.svelte";
36
+ let { label = "Primary", menuLabel = "Menu", menuOpen = $bindable(false), start, center, end, menu, class: className, ...rest } = $props();
37
+ </script>
38
+
39
+ <Collapsible.Root bind:open={menuOpen}>
40
+ {#snippet child({ props })}
41
+ <header {...rest} {...props} class={['bg-base-200 border-base-300 border-b', className]}>
42
+ <nav aria-label={label} class="navbar mx-auto max-w-7xl px-4 sm:px-6">
43
+ <div class="navbar-start min-w-0">{@render start()}</div>
44
+ {#if center}
45
+ <div class="navbar-center hidden lg:flex">{@render center()}</div>
46
+ {/if}
47
+ <div class="navbar-end gap-2">
48
+ {#if end}{@render end()}{/if}
49
+ {#if menu}
50
+ <Collapsible.Trigger>
51
+ {#snippet child({ props: triggerProps })}
52
+ <button {...triggerProps} class="btn btn-ghost btn-square lg:hidden">
53
+ <MenuIcon class="size-5" aria-hidden="true" />
54
+ <VisuallyHidden>{menuLabel}</VisuallyHidden>
55
+ </button>
56
+ {/snippet}
57
+ </Collapsible.Trigger>
58
+ {/if}
59
+ </div>
60
+ </nav>
61
+ {#if menu}
62
+ <Collapsible.Content class="border-base-300 border-t px-4 py-3 lg:hidden">
63
+ {@render menu()}
64
+ </Collapsible.Content>
65
+ {/if}
66
+ </header>
67
+ {/snippet}
68
+ </Collapsible.Root>