@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,133 @@
1
+ /**
2
+ * Pure key-matching helpers for the hotkey registry. No Svelte, no DOM
3
+ * mutation — just functions over `KeyboardEvent`s, trigger grammars and the
4
+ * sequence buffer, so the matching rules stay testable and `HotkeyProvider`
5
+ * stays thin.
6
+ *
7
+ * Trigger model:
8
+ *
9
+ * - **Modifier chords** (`'$mod+k'`) — held together. `$mod` is ⌘ on macOS
10
+ * and Ctrl elsewhere. Matched on their own; never buffered.
11
+ * - **Plain keys / sequences** (`'e'`, `'/'`, `['g', 'p']`) — pressed in
12
+ * order with no modifier held. A plain single key is just a sequence of
13
+ * length one, so matching is uniform.
14
+ *
15
+ * @module
16
+ */
17
+ /** Sequence buffer entries expire after this many milliseconds. */
18
+ export const SEQUENCE_TIMEOUT_MS = 1000;
19
+ /** Whether the runtime platform treats `$mod` as ⌘ (macOS / iOS). */
20
+ export function isApplePlatform() {
21
+ // `globalThis.navigator` rather than the bare identifier: on a runtime
22
+ // without one, a direct reference would throw before the guard could run.
23
+ if (typeof globalThis.navigator === 'undefined')
24
+ return false;
25
+ return /mac|iphone|ipad|ipod/i.test(globalThis.navigator.platform);
26
+ }
27
+ /**
28
+ * Whether a keyboard event's target is an editable surface — a form field or
29
+ * a contenteditable region (which covers a rich-text editor's root). Hotkeys
30
+ * without `allowInEditable` stay inert there so typing never fires actions.
31
+ */
32
+ export function isEditableTarget(target) {
33
+ if (!(target instanceof HTMLElement))
34
+ return false;
35
+ return target.matches('input, textarea, select') || target.isContentEditable;
36
+ }
37
+ /** Whether a trigger is in the modifier-chord form (`'$mod+…'`). */
38
+ function isModifierChord(trigger) {
39
+ return typeof trigger === 'string' && trigger.includes('+');
40
+ }
41
+ /** Normalise a plain-key trigger to its sequence form. `undefined` for a chord. */
42
+ function asSequence(trigger) {
43
+ if (typeof trigger === 'string') {
44
+ return isModifierChord(trigger) ? undefined : [trigger];
45
+ }
46
+ return trigger;
47
+ }
48
+ /** Whether the event matches a `'$mod+key'` chord. */
49
+ // Modifier matching is a flat run of guard comparisons (mod / alt / shift /
50
+ // cross-platform exclusion); each early return is one rule of the chord
51
+ // grammar, and extracting them would obscure the contract.
52
+ function eventMatchesChord(event, chord) {
53
+ const parts = chord.split('+');
54
+ const key = parts.at(-1) ?? '';
55
+ // A plain array, not a `Set`: a chord carries at most three modifiers, so
56
+ // three `includes` scans beat building a hashed collection on every
57
+ // keystroke.
58
+ const modifiers = parts.slice(0, -1);
59
+ const wantsMod = modifiers.includes('$mod');
60
+ const wantsAlt = modifiers.includes('alt');
61
+ const wantsShift = modifiers.includes('shift');
62
+ const apple = isApplePlatform();
63
+ const modPressed = apple ? event.metaKey : event.ctrlKey;
64
+ // The non-$mod modifier must not also be held, so e.g. Ctrl+K on macOS
65
+ // doesn't trigger a ⌘K hotkey.
66
+ const otherModPressed = apple ? event.ctrlKey : event.metaKey;
67
+ if (wantsMod !== modPressed || otherModPressed)
68
+ return false;
69
+ if (wantsAlt !== event.altKey)
70
+ return false;
71
+ if (wantsShift && !event.shiftKey)
72
+ return false;
73
+ return event.key.toLowerCase() === key.toLowerCase();
74
+ }
75
+ /** Whether an event carries a chord modifier (⌘ / Ctrl / Alt). */
76
+ export function hasChordModifier(event) {
77
+ return event.metaKey || event.ctrlKey || event.altKey;
78
+ }
79
+ /**
80
+ * Whether an event's key can participate in a sequence: a single printable
81
+ * character pressed without chord modifiers.
82
+ */
83
+ export function isSequenceKey(event) {
84
+ return !hasChordModifier(event) && event.key.length === 1;
85
+ }
86
+ /**
87
+ * Find the action whose modifier chord matches the event. `actions` must
88
+ * already be in precedence order (page before global, later registrations
89
+ * first).
90
+ */
91
+ export function matchChord(event, actions) {
92
+ return actions.find(({ action }) => typeof action.trigger === 'string' &&
93
+ action.trigger.includes('+') &&
94
+ eventMatchesChord(event, action.trigger))?.action;
95
+ }
96
+ // A single-pass scan that classifies every trigger as exact-match / prefix /
97
+ // miss in one loop; splitting it into separate passes would be simpler per
98
+ // function but iterate the registrations three times per keystroke.
99
+ /**
100
+ * Match a candidate key buffer against the active plain-key triggers.
101
+ * `actions` must already be in precedence order.
102
+ */
103
+ export function matchSequence(buffer, actions) {
104
+ return actions.reduce((match, { action }) => {
105
+ const trigger = action.trigger;
106
+ if (trigger === undefined)
107
+ return match;
108
+ const sequence = asSequence(trigger);
109
+ if (sequence === undefined)
110
+ return match;
111
+ if (sequence.length === buffer.length) {
112
+ return match.exact === undefined && sequence.every((key, i) => key === buffer[i])
113
+ ? { ...match, exact: action }
114
+ : match;
115
+ }
116
+ return sequence.length > buffer.length && buffer.every((key, i) => key === sequence[i])
117
+ ? { ...match, isPrefix: true }
118
+ : match;
119
+ }, { exact: undefined, isPrefix: false });
120
+ }
121
+ /** Project a trigger into its display form for help overlays and badges. */
122
+ export function triggerDisplay(trigger) {
123
+ if (typeof trigger === 'string') {
124
+ if (isModifierChord(trigger)) {
125
+ return {
126
+ keys: trigger.split('+').map((part) => (part === '$mod' ? part : part.toUpperCase())),
127
+ sequential: false,
128
+ };
129
+ }
130
+ return { keys: [trigger.toUpperCase()], sequential: false };
131
+ }
132
+ return { keys: trigger.map((key) => key.toUpperCase()), sequential: true };
133
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The reactive hotkey / command registry.
3
+ *
4
+ * One instance lives in the app's Svelte context. The root layout registers
5
+ * `global` actions; pages register `page` actions that are torn down when the
6
+ * page unmounts. The registry is the single source of truth for three
7
+ * consumers:
8
+ *
9
+ * - `HotkeyProvider` — matches keyboard events against active triggers.
10
+ * - `CommandPalette` — lists palette-visible actions.
11
+ * - `HotkeyHelpDialog` — lists actions that carry triggers.
12
+ *
13
+ * Role gating happens here: actions registered with `requiresAdmin` are
14
+ * dropped from every read surface when the viewer is not an administrator, so
15
+ * non-admin users never see (or can fire) them.
16
+ *
17
+ * @module
18
+ */
19
+ import type { CommandAction, HotkeyScope, RegisteredAction } from './types.ts';
20
+ /** What `createHotkeyRegistry` needs from the app: the admin check and the dev flag. */
21
+ export interface HotkeyRegistryOptions {
22
+ /** Whether the current viewer is an administrator. Read lazily so it can be reactive. */
23
+ isAdmin: () => boolean;
24
+ /**
25
+ * Whether this is a dev build. When true, registering a batch whose trigger
26
+ * shadows an existing one logs a console warning. An app wires this to its
27
+ * bundler's dev flag (SvelteKit's `$app/environment` `dev`); defaults to
28
+ * `false`.
29
+ */
30
+ dev?: boolean | undefined;
31
+ }
32
+ /** The reactive set of registered hotkey actions that `HotkeyProvider`, `CommandPalette` and `HotkeyHelpDialog` read; page registrations win over global ones. */
33
+ export declare class HotkeyRegistry {
34
+ #private;
35
+ /** Whether the command palette is open. Bound by `CommandPalette`. */
36
+ paletteOpen: boolean;
37
+ /** Whether the hotkey help overlay is open. Bound by `HotkeyHelpDialog`. */
38
+ helpOpen: boolean;
39
+ constructor(options: HotkeyRegistryOptions);
40
+ /**
41
+ * Register a batch of actions under a scope. Returns an unregister
42
+ * function; callers are expected to invoke it on unmount (the
43
+ * `registerHotkeys` helper wires this to `$effect` cleanup).
44
+ */
45
+ register(scope: HotkeyScope, actions: readonly CommandAction[]): () => void;
46
+ /**
47
+ * Role-filtered active registrations in match-precedence order: page scope
48
+ * before global, later registrations first within a scope.
49
+ */
50
+ readonly active: readonly RegisteredAction[];
51
+ /** Active actions that should appear in the command palette, in registration order. */
52
+ readonly paletteActions: readonly RegisteredAction[];
53
+ /** Active actions that carry a keyboard trigger, for the help overlay. */
54
+ readonly hotkeys: readonly RegisteredAction[];
55
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The reactive hotkey / command registry.
3
+ *
4
+ * One instance lives in the app's Svelte context. The root layout registers
5
+ * `global` actions; pages register `page` actions that are torn down when the
6
+ * page unmounts. The registry is the single source of truth for three
7
+ * consumers:
8
+ *
9
+ * - `HotkeyProvider` — matches keyboard events against active triggers.
10
+ * - `CommandPalette` — lists palette-visible actions.
11
+ * - `HotkeyHelpDialog` — lists actions that carry triggers.
12
+ *
13
+ * Role gating happens here: actions registered with `requiresAdmin` are
14
+ * dropped from every read surface when the viewer is not an administrator, so
15
+ * non-admin users never see (or can fire) them.
16
+ *
17
+ * @module
18
+ */
19
+ /**
20
+ * Whether two triggers denote the same keystroke. A structural comparison
21
+ * rather than a JSON round-trip: a chord is a string and a sequence is an
22
+ * array, so the two forms never collide.
23
+ */
24
+ function sameTrigger(self, that) {
25
+ return typeof self === 'string' || typeof that === 'string'
26
+ ? self === that
27
+ : self.length === that.length && self.every((key, index) => key === that[index]);
28
+ }
29
+ /** A trigger rendered for the dev warning below. */
30
+ function formatTrigger(trigger) {
31
+ return typeof trigger === 'string' ? `"${trigger}"` : `["${trigger.join('", "')}"]`;
32
+ }
33
+ /** Dev-only console warning when a new registration shadows an existing trigger. */
34
+ function warnOnTriggerConflicts(added, existing) {
35
+ for (const entry of added) {
36
+ const trigger = entry.action.trigger;
37
+ if (trigger === undefined)
38
+ continue;
39
+ const conflict = existing.find((other) => other.action.trigger !== undefined && sameTrigger(other.action.trigger, trigger));
40
+ if (conflict) {
41
+ // A dev-build diagnostic: it has to reach the browser console the
42
+ // developer is looking at, which is why this is the one `console` call
43
+ // in the package.
44
+ console.warn(`[hotkeys] Trigger ${formatTrigger(trigger)} of "${entry.action.id}" ` +
45
+ `shadows "${conflict.action.id}" (${conflict.scope}). ` +
46
+ `Page-scope and later registrations win.`);
47
+ }
48
+ }
49
+ }
50
+ /** Page scope sorts before global scope. */
51
+ const scopeRank = (entry) => (entry.scope === 'page' ? 0 : 1);
52
+ /**
53
+ * Match precedence: page scope first, then the most recent registration.
54
+ * A plain comparator — the composed `Order` this was ported from is the only
55
+ * thing that changed.
56
+ */
57
+ const byMatchPrecedence = (a, b) => scopeRank(a) - scopeRank(b) || b.order - a.order;
58
+ /** Ascending registration order (earlier registrations first). */
59
+ const byOrder = (a, b) => a.order - b.order;
60
+ /** The reactive set of registered hotkey actions that `HotkeyProvider`, `CommandPalette` and `HotkeyHelpDialog` read; page registrations win over global ones. */
61
+ export class HotkeyRegistry {
62
+ #registrations = $state([]);
63
+ #nextOrder = 0;
64
+ #isAdmin;
65
+ #dev;
66
+ /** Whether the command palette is open. Bound by `CommandPalette`. */
67
+ paletteOpen = $state(false);
68
+ /** Whether the hotkey help overlay is open. Bound by `HotkeyHelpDialog`. */
69
+ helpOpen = $state(false);
70
+ constructor(options) {
71
+ this.#isAdmin = options.isAdmin;
72
+ this.#dev = options.dev ?? false;
73
+ }
74
+ /**
75
+ * Register a batch of actions under a scope. Returns an unregister
76
+ * function; callers are expected to invoke it on unmount (the
77
+ * `registerHotkeys` helper wires this to `$effect` cleanup).
78
+ */
79
+ register(scope, actions) {
80
+ const added = actions.map((action) => ({ scope, action, order: this.#nextOrder++ }));
81
+ if (this.#dev)
82
+ warnOnTriggerConflicts(added, this.#registrations);
83
+ this.#registrations = [...this.#registrations, ...added];
84
+ return () => {
85
+ // Remove by the stable monotonic `order`, NOT object identity. `$state`
86
+ // deep-proxies `#registrations`, so entries read back here are proxies
87
+ // that never `===` the raw objects captured in `added` — a `Set(added)`
88
+ // membership test would match nothing and remove nothing, leaking the
89
+ // batch. `registerHotkeys` wires this closure to `$effect` cleanup, so
90
+ // every reactive re-registration would otherwise accumulate duplicates.
91
+ const orders = new Set(added.map((entry) => entry.order));
92
+ this.#registrations = this.#registrations.filter((entry) => !orders.has(entry.order));
93
+ };
94
+ }
95
+ /**
96
+ * Role-filtered active registrations in match-precedence order: page scope
97
+ * before global, later registrations first within a scope.
98
+ */
99
+ active = $derived(this.#registrations
100
+ .filter((entry) => !entry.action.requiresAdmin || this.#isAdmin())
101
+ .toSorted(byMatchPrecedence));
102
+ /** Active actions that should appear in the command palette, in registration order. */
103
+ paletteActions = $derived(this.#registrations
104
+ .filter((entry) => (!entry.action.requiresAdmin || this.#isAdmin()) && entry.action.paletteVisible !== false)
105
+ .toSorted(byOrder));
106
+ /** Active actions that carry a keyboard trigger, for the help overlay. */
107
+ hotkeys = $derived(this.#registrations
108
+ .filter((entry) => (!entry.action.requiresAdmin || this.#isAdmin()) && entry.action.trigger !== undefined)
109
+ .toSorted(byOrder));
110
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The registration contract shared by the hotkey registry and the command
3
+ * palette.
4
+ *
5
+ * A {@link CommandAction} is the unit of both systems: a labelled, runnable
6
+ * action that may carry a keyboard trigger (making it a hotkey) and may
7
+ * appear in the command palette (making it discoverable). Pages and the
8
+ * global shell register actions through the registry; the palette and the
9
+ * help overlay read the same registrations back, so there is exactly one
10
+ * source of truth for "what can the viewer do right now".
11
+ *
12
+ * Every optional member is declared `T | undefined`: the workspace compiles
13
+ * with `exactOptionalPropertyTypes`, and these objects are written as plain
14
+ * literals by application code that forwards its own optional values.
15
+ *
16
+ * @module
17
+ */
18
+ /**
19
+ * A single-keystroke trigger.
20
+ *
21
+ * Grammar: zero or more `+`-separated modifiers followed by a key, e.g.
22
+ * `'$mod+k'`, `'/'`, `'?'`, `'e'`. `$mod` resolves to ⌘ on macOS and Ctrl
23
+ * elsewhere. Shift is never written explicitly — shifted characters (like
24
+ * `?`) already arrive shifted in `KeyboardEvent.key`.
25
+ */
26
+ export type HotkeyChord = string;
27
+ /**
28
+ * A multi-keystroke trigger pressed in order, e.g. `['g', 'p']`. Only plain
29
+ * (unmodified) printable keys participate in sequences.
30
+ */
31
+ export type HotkeySequence = readonly string[];
32
+ /** A hotkey trigger: a chord pressed together (`'$mod+k'`) or a sequence pressed in turn (`['g', 'p']`). */
33
+ export type HotkeyTrigger = HotkeyChord | HotkeySequence;
34
+ /** Palette grouping. Drives section headings and ordering. */
35
+ export type CommandActionGroup = 'actions' | 'navigation';
36
+ /**
37
+ * Where a registration lives. Page registrations are torn down when the
38
+ * registering page unmounts and win over global registrations on trigger
39
+ * conflicts.
40
+ */
41
+ export type HotkeyScope = 'global' | 'page';
42
+ /** One action the hotkey registry and the command palette offer: an id, a label, what it runs and, optionally, its trigger. */
43
+ export interface CommandAction {
44
+ /** Stable unique id, e.g. `'nav.posts'`, `'posts.create'`. */
45
+ id: string;
46
+ /** Human-readable label. An i18n app resolves its message at registration. */
47
+ label: string;
48
+ /** What runs on activation (navigate, focus, open a dialog, …). */
49
+ run: () => void | Promise<void>;
50
+ /** Palette section. */
51
+ group: CommandActionGroup;
52
+ /** Optional keyboard trigger. Actions without one are palette-only. */
53
+ trigger?: HotkeyTrigger | undefined;
54
+ /**
55
+ * When true the registry drops the action entirely for non-admin viewers —
56
+ * it neither fires as a hotkey nor appears in the palette.
57
+ */
58
+ requiresAdmin?: boolean | undefined;
59
+ /** Set false to keep an action hotkey-only (hidden from the palette). */
60
+ paletteVisible?: boolean | undefined;
61
+ /**
62
+ * Allow the trigger to fire while focus is inside an editable element
63
+ * (input / textarea / contenteditable). Reserved for the palette opener;
64
+ * everything else stays inert while typing.
65
+ */
66
+ allowInEditable?: boolean | undefined;
67
+ }
68
+ /** A registration entry: an action plus the scope it was registered under. */
69
+ export interface RegisteredAction {
70
+ scope: HotkeyScope;
71
+ action: CommandAction;
72
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The registration contract shared by the hotkey registry and the command
3
+ * palette.
4
+ *
5
+ * A {@link CommandAction} is the unit of both systems: a labelled, runnable
6
+ * action that may carry a keyboard trigger (making it a hotkey) and may
7
+ * appear in the command palette (making it discoverable). Pages and the
8
+ * global shell register actions through the registry; the palette and the
9
+ * help overlay read the same registrations back, so there is exactly one
10
+ * source of truth for "what can the viewer do right now".
11
+ *
12
+ * Every optional member is declared `T | undefined`: the workspace compiles
13
+ * with `exactOptionalPropertyTypes`, and these objects are written as plain
14
+ * literals by application code that forwards its own optional values.
15
+ *
16
+ * @module
17
+ */
18
+ export {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Accessible Svelte 5 UI primitives.
3
+ *
4
+ * Everything exported here is app-agnostic: it takes props and snippets,
5
+ * spreads the native attributes of the element it renders, reads no global
6
+ * state, and gets every user-facing string from a prop with an English
7
+ * default. Styling is daisyUI semantic classes over the tokens in
8
+ * `./theme.css`, which ships beside this barrel as the package's
9
+ * `./theme.css` export. The one injected dependency is
10
+ * {@link ComponentsContext}: locale, href resolution, navigation and the
11
+ * current-page check.
12
+ *
13
+ * The heavier families — `citations`, `canvas`, `charts`, `tips`, `seo` —
14
+ * are separate entry points (`@thomasfosterau/components/<family>`), as is
15
+ * `EntityForm` (`@thomasfosterau/components/entity-form`, which needs the
16
+ * optional peer `@thomasfosterau/effect-forms-svelte`); none is re-exported
17
+ * here. The maps family lives one package over, in
18
+ * `@thomasfosterau/components-mapbox`: its behaviour needs a specific
19
+ * third-party engine (`mapbox-gl`), and a component that needs one lives in
20
+ * its own package so this one stays free of it.
21
+ *
22
+ * @module @thomasfosterau/components
23
+ */
24
+ export * from './context.svelte.ts';
25
+ export * from './ui/index.ts';
26
+ export * from './feedback/index.ts';
27
+ export * from './layout/index.ts';
28
+ export * from './fields/index.ts';
29
+ export * from './forms/index.ts';
30
+ export * from './data/index.ts';
31
+ export * from './infobox/index.ts';
32
+ export * from './formatting/index.ts';
33
+ export * from './links/index.ts';
34
+ export * from './media/index.ts';
35
+ export * from './hotkeys/index.ts';
36
+ export * from './search/index.ts';
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Accessible Svelte 5 UI primitives.
3
+ *
4
+ * Everything exported here is app-agnostic: it takes props and snippets,
5
+ * spreads the native attributes of the element it renders, reads no global
6
+ * state, and gets every user-facing string from a prop with an English
7
+ * default. Styling is daisyUI semantic classes over the tokens in
8
+ * `./theme.css`, which ships beside this barrel as the package's
9
+ * `./theme.css` export. The one injected dependency is
10
+ * {@link ComponentsContext}: locale, href resolution, navigation and the
11
+ * current-page check.
12
+ *
13
+ * The heavier families — `citations`, `canvas`, `charts`, `tips`, `seo` —
14
+ * are separate entry points (`@thomasfosterau/components/<family>`), as is
15
+ * `EntityForm` (`@thomasfosterau/components/entity-form`, which needs the
16
+ * optional peer `@thomasfosterau/effect-forms-svelte`); none is re-exported
17
+ * here. The maps family lives one package over, in
18
+ * `@thomasfosterau/components-mapbox`: its behaviour needs a specific
19
+ * third-party engine (`mapbox-gl`), and a component that needs one lives in
20
+ * its own package so this one stays free of it.
21
+ *
22
+ * @module @thomasfosterau/components
23
+ */
24
+ export * from "./context.svelte.js";
25
+ export * from "./ui/index.js";
26
+ export * from "./feedback/index.js";
27
+ export * from "./layout/index.js";
28
+ export * from "./fields/index.js";
29
+ export * from "./forms/index.js";
30
+ export * from "./data/index.js";
31
+ export * from "./infobox/index.js";
32
+ export * from "./formatting/index.js";
33
+ export * from "./links/index.js";
34
+ export * from "./media/index.js";
35
+ export * from "./hotkeys/index.js";
36
+ export * from "./search/index.js";
@@ -0,0 +1,79 @@
1
+ <!-- @component
2
+ The summary rail beside an entity page’s prose: an optional header and image above a body of labelled facts.
3
+ -->
4
+ <!--
5
+ Infobox — the summary rail beside an entity page's prose: an optional
6
+ header, an optional image, and a body of labelled facts.
7
+
8
+ Two ways to fill it, both live at once. The composable path — `children`
9
+ taking `InfoboxSection`/`InfoboxRow` (or anything else):
10
+ a page builds bespoke markup, one piece of markup per concern, and a fix
11
+ to row layout lands once because there is only one row layout. `rows` renders through the SAME `InfoboxSection`/
12
+ `InfoboxRow`/`InfoboxImage` primitives `children` would use — never a
13
+ second copy of the markup — so both paths stay in lock step. Both are
14
+ accepted together: `rows` first, then `children` below them.
15
+
16
+ `float`: floats the box to the right of the article's prose; instead give it its own grid column in the
17
+ page layout and never float it. Off by default, since a grid-column aside
18
+ is the more common shape here, and opt-in per page with `float`.
19
+
20
+ ARIA contract: `aria-label={title ?? label}` names the region — a real
21
+ `title` is more specific than the generic default, so it wins.
22
+ `data-testid="infobox"` is kept for e2e parity with the source apps.
23
+ -->
24
+ <script lang="ts">import Link from "../ui/Link.svelte";
25
+ import InfoboxHeader from "./InfoboxHeader.svelte";
26
+ import InfoboxImage from "./InfoboxImage.svelte";
27
+ import InfoboxRow from "./InfoboxRow.svelte";
28
+ import InfoboxSection from "./InfoboxSection.svelte";
29
+ import { infoboxRowIsPresent } from "./types.js";
30
+ let { title, subtitle, label = "Key facts", image, rows, float = false, children, class: className, ...rest } = $props();
31
+ const presentRows = $derived((rows ?? []).filter(infoboxRowIsPresent));
32
+ const imageRows = $derived(presentRows.filter((row) => row.kind === "image"));
33
+ const bodyRows = $derived(presentRows.filter((row) => row.kind !== "image"));
34
+ </script>
35
+
36
+ <aside
37
+ {...rest}
38
+ data-testid="infobox"
39
+ aria-label={title ?? label}
40
+ class={[
41
+ 'card card-border bg-base-100 shadow-sm',
42
+ float && 'float-right mb-4 ml-4 w-full sm:w-80',
43
+ className,
44
+ ]}
45
+ >
46
+ {#if title !== undefined}
47
+ <InfoboxHeader {title} {subtitle} />
48
+ {/if}
49
+ {#if image !== undefined}
50
+ <InfoboxImage {...image} />
51
+ {/if}
52
+ {#each imageRows as row (row.src)}
53
+ <InfoboxImage {...row} />
54
+ {/each}
55
+ {#if bodyRows.length > 0}
56
+ <InfoboxSection>
57
+ {#each bodyRows as row (row.label)}
58
+ {#if row.kind === 'fact'}
59
+ <InfoboxRow label={row.label} value={row.value} />
60
+ {:else if row.kind === 'link'}
61
+ <InfoboxRow label={row.label} value={row.text} href={row.href} external={row.external} />
62
+ {:else if row.kind === 'links'}
63
+ <InfoboxRow label={row.label}>
64
+ <ul class="flex flex-col gap-1">
65
+ {#each row.links ?? [] as link (link.href)}
66
+ <li><Link href={link.href}>{link.label}</Link></li>
67
+ {/each}
68
+ </ul>
69
+ </InfoboxRow>
70
+ {:else if row.kind === 'custom'}
71
+ <InfoboxRow label={row.label}>
72
+ {@render row.content()}
73
+ </InfoboxRow>
74
+ {/if}
75
+ {/each}
76
+ </InfoboxSection>
77
+ {/if}
78
+ {@render children?.()}
79
+ </aside>
@@ -0,0 +1,26 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type InfoboxImageSpec, type InfoboxRowSpec as Row } from './types.ts';
4
+ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'title'> {
5
+ /** Heading of the rail; also its accessible name. */
6
+ title?: string | undefined;
7
+ /** A line under the title. */
8
+ subtitle?: string | undefined;
9
+ /**
10
+ * Accessible name for the region when there is no `title`.
11
+ * @defaultValue `'Key facts'`
12
+ */
13
+ label?: string | undefined;
14
+ /** Renders an `InfoboxImage` above the body. */
15
+ image?: InfoboxImageSpec | undefined;
16
+ /** Rendered through the primitives below, inside one `InfoboxSection`. */
17
+ rows?: readonly Row[] | undefined;
18
+ /** float-right rail; off renders in normal flow (a grid column). */
19
+ float?: boolean | undefined;
20
+ /** Extra composed markup — `InfoboxSection`/`InfoboxRow`, or anything else. Rendered after `rows`. */
21
+ children?: Snippet<[]> | undefined;
22
+ }
23
+ /** The summary rail beside an entity page’s prose: an optional header and image above a body of labelled facts. */
24
+ declare const Infobox: import("svelte").Component<Props, {}, "">;
25
+ type Infobox = ReturnType<typeof Infobox>;
26
+ export default Infobox;
@@ -0,0 +1,27 @@
1
+ <!-- @component
2
+ The title block at the top of an `Infobox`.
3
+ -->
4
+ <!--
5
+ InfoboxHeader — the title block at the top of an `Infobox`.
6
+
7
+ An `h3` rather than an `h2`: the box sits beside an article's prose, and
8
+ its title names the summary, not a section of the document. Promoting it
9
+ to the level a real section heading would use puts a phantom entry in the
10
+ page's outline between two real ones — the box is deliberately outside
11
+ the outline.
12
+ -->
13
+ <script lang="ts">let { title, subtitle, class: className, ...rest } = $props();
14
+ export {};
15
+ </script>
16
+
17
+ <div
18
+ {...rest}
19
+ class={["border-base-300 bg-base-300 border-b px-4 py-3", className]}
20
+ >
21
+ <h3 class="text-center text-lg font-bold select-none">{title}</h3>
22
+ {#if subtitle !== undefined && subtitle !== ""}
23
+ <p class="text-base-content/60 mt-1 text-center text-sm select-none">
24
+ {subtitle}
25
+ </p>
26
+ {/if}
27
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { HTMLAttributes } from "svelte/elements";
2
+ interface Props extends HTMLAttributes<HTMLDivElement> {
3
+ /** The infobox heading text. */
4
+ title: string;
5
+ /** A line under the heading. */
6
+ subtitle?: string | undefined;
7
+ }
8
+ /** The title block at the top of an `Infobox`. */
9
+ declare const InfoboxHeader: import("svelte").Component<Props, {}, "">;
10
+ type InfoboxHeader = ReturnType<typeof InfoboxHeader>;
11
+ export default InfoboxHeader;
@@ -0,0 +1,38 @@
1
+ <!-- @component
2
+ The picture at the top of an `Infobox`, with its caption.
3
+ -->
4
+ <!--
5
+ InfoboxImage — the picture at the top of an `Infobox`, with its caption.
6
+
7
+ `width`/`height` are passed through so the browser can reserve the box
8
+ before the bytes arrive — the difference between a page that settles and
9
+ one that jumps under the reader's eye — but only reserve that space when
10
+ BOTH are present; the caller supplies them together or not at all.
11
+
12
+ `eager` (default off: an infobox usually sits beside the prose rather than
13
+ above it, so its picture is rarely the first thing painted) sets
14
+ `loading="eager"` and `fetchpriority="high"` together.
15
+ -->
16
+ <script lang="ts">let { src, alt, caption, width, height, eager = false, class: className, ...rest } = $props();
17
+ const loading = $derived(eager ? "eager" : "lazy");
18
+ const fetchpriority = $derived(eager ? "high" : undefined);
19
+ export {};
20
+ </script>
21
+
22
+ <figure class="border-base-300 border-b">
23
+ <img
24
+ {...rest}
25
+ {src}
26
+ {alt}
27
+ {width}
28
+ {height}
29
+ {loading}
30
+ {fetchpriority}
31
+ class={['h-auto w-full', className]}
32
+ />
33
+ {#if caption !== undefined && caption !== ''}
34
+ <figcaption class="text-base-content/60 px-4 py-2 text-center text-xs italic">
35
+ {caption}
36
+ </figcaption>
37
+ {/if}
38
+ </figure>