@thomasfosterau/components 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +241 -56
  2. package/dist/canvas/VectorCanvas.svelte +89 -8
  3. package/dist/canvas/VectorCanvas.svelte.d.ts +19 -1
  4. package/dist/canvas/types.d.ts +1 -1
  5. package/dist/charts/types.d.ts +1 -1
  6. package/dist/citations/list-format.js +3 -1
  7. package/dist/data/ChatBubble.svelte +48 -0
  8. package/dist/data/ChatBubble.svelte.d.ts +33 -0
  9. package/dist/data/DescriptionList.svelte +9 -3
  10. package/dist/data/DescriptionList.svelte.d.ts +1 -1
  11. package/dist/data/DescriptionRow.svelte +6 -5
  12. package/dist/data/MessageThread.svelte +29 -0
  13. package/dist/data/MessageThread.svelte.d.ts +17 -0
  14. package/dist/data/Pagination.svelte +5 -5
  15. package/dist/data/Pagination.svelte.d.ts +8 -0
  16. package/dist/data/PaginationFooter.svelte +2 -1
  17. package/dist/data/PaginationFooter.svelte.d.ts +2 -0
  18. package/dist/data/Table.svelte +9 -3
  19. package/dist/data/description-context.d.ts +12 -6
  20. package/dist/data/description-context.js +6 -5
  21. package/dist/data/index.d.ts +2 -0
  22. package/dist/data/index.js +2 -0
  23. package/dist/data/table-types.d.ts +7 -0
  24. package/dist/feedback/Dialog.svelte +19 -49
  25. package/dist/feedback/Dialog.svelte.d.ts +0 -1
  26. package/dist/feedback/DialogFrame.svelte +64 -0
  27. package/dist/feedback/DialogFrame.svelte.d.ts +23 -0
  28. package/dist/feedback/Drawer.svelte +43 -0
  29. package/dist/feedback/Drawer.svelte.d.ts +43 -0
  30. package/dist/feedback/SegmentBar.svelte.d.ts +1 -0
  31. package/dist/feedback/StatTile.svelte.d.ts +1 -0
  32. package/dist/feedback/Steps.svelte +72 -0
  33. package/dist/feedback/Steps.svelte.d.ts +61 -0
  34. package/dist/feedback/Timeline.svelte.d.ts +3 -0
  35. package/dist/feedback/index.d.ts +3 -0
  36. package/dist/feedback/index.js +2 -0
  37. package/dist/feedback/sizes.d.ts +14 -0
  38. package/dist/feedback/sizes.js +28 -0
  39. package/dist/fields/Calendar.svelte +60 -0
  40. package/dist/fields/Calendar.svelte.d.ts +11 -0
  41. package/dist/fields/CalendarMonths.svelte +86 -0
  42. package/dist/fields/CalendarMonths.svelte.d.ts +30 -0
  43. package/dist/fields/Checkbox.svelte +1 -1
  44. package/dist/fields/Checkbox.svelte.d.ts +6 -0
  45. package/dist/fields/Combobox.svelte +61 -54
  46. package/dist/fields/Combobox.svelte.d.ts +38 -3
  47. package/dist/fields/DateField.svelte +37 -0
  48. package/dist/fields/DateField.svelte.d.ts +20 -0
  49. package/dist/fields/DateInput.svelte +1 -1
  50. package/dist/fields/DateInput.svelte.d.ts +6 -0
  51. package/dist/fields/DatePicker.svelte +47 -0
  52. package/dist/fields/DatePicker.svelte.d.ts +25 -0
  53. package/dist/fields/DateRangePicker.svelte +70 -0
  54. package/dist/fields/DateRangePicker.svelte.d.ts +43 -0
  55. package/dist/fields/Dropzone.svelte +232 -0
  56. package/dist/fields/Dropzone.svelte.d.ts +103 -0
  57. package/dist/fields/Fieldset.svelte +7 -2
  58. package/dist/fields/Fieldset.svelte.d.ts +3 -2
  59. package/dist/fields/FileInput.svelte +3 -2
  60. package/dist/fields/FileInput.svelte.d.ts +6 -0
  61. package/dist/fields/FormField.svelte +27 -3
  62. package/dist/fields/ImageInput.svelte.d.ts +6 -0
  63. package/dist/fields/MultiSelect.svelte +112 -0
  64. package/dist/fields/MultiSelect.svelte.d.ts +75 -0
  65. package/dist/fields/NumberField.svelte +190 -0
  66. package/dist/fields/NumberField.svelte.d.ts +76 -0
  67. package/dist/fields/PasswordInput.svelte +95 -0
  68. package/dist/fields/PasswordInput.svelte.d.ts +58 -0
  69. package/dist/fields/PickerPopover.svelte +47 -0
  70. package/dist/fields/PickerPopover.svelte.d.ts +16 -0
  71. package/dist/fields/PinInput.svelte +83 -0
  72. package/dist/fields/PinInput.svelte.d.ts +66 -0
  73. package/dist/fields/RadioGroup.svelte +3 -0
  74. package/dist/fields/RangeCalendar.svelte +51 -0
  75. package/dist/fields/RangeCalendar.svelte.d.ts +23 -0
  76. package/dist/fields/Rating.svelte +127 -0
  77. package/dist/fields/Rating.svelte.d.ts +86 -0
  78. package/dist/fields/SearchInput.svelte +2 -1
  79. package/dist/fields/SegmentGroup.svelte +91 -0
  80. package/dist/fields/SegmentGroup.svelte.d.ts +20 -0
  81. package/dist/fields/Select.svelte +1 -1
  82. package/dist/fields/Select.svelte.d.ts +6 -0
  83. package/dist/fields/Slider.svelte +194 -0
  84. package/dist/fields/Slider.svelte.d.ts +100 -0
  85. package/dist/fields/TagsInput.svelte +256 -0
  86. package/dist/fields/TagsInput.svelte.d.ts +102 -0
  87. package/dist/fields/TextInput.svelte +1 -1
  88. package/dist/fields/TextInput.svelte.d.ts +6 -0
  89. package/dist/fields/Textarea.svelte +1 -1
  90. package/dist/fields/Textarea.svelte.d.ts +6 -0
  91. package/dist/fields/TimeField.svelte +30 -0
  92. package/dist/fields/TimeField.svelte.d.ts +19 -0
  93. package/dist/fields/Toggle.svelte +1 -1
  94. package/dist/fields/Toggle.svelte.d.ts +6 -0
  95. package/dist/fields/VideoInput.svelte.d.ts +6 -0
  96. package/dist/fields/calendar-props.d.ts +119 -0
  97. package/dist/fields/calendar-props.js +51 -0
  98. package/dist/fields/dates.d.ts +28 -0
  99. package/dist/fields/dates.js +26 -0
  100. package/dist/fields/field-types.d.ts +29 -0
  101. package/dist/fields/field-types.js +32 -0
  102. package/dist/fields/file-validation.d.ts +30 -0
  103. package/dist/fields/file-validation.js +51 -0
  104. package/dist/fields/index.d.ts +9 -0
  105. package/dist/fields/index.js +8 -0
  106. package/dist/fields/latest-search.svelte.d.ts +38 -0
  107. package/dist/fields/latest-search.svelte.js +85 -0
  108. package/dist/fields/number-format.d.ts +52 -0
  109. package/dist/fields/number-format.js +89 -0
  110. package/dist/fields/segmented-field.d.ts +128 -0
  111. package/dist/fields/segmented-field.js +52 -0
  112. package/dist/fields/slider-keys.d.ts +21 -0
  113. package/dist/fields/slider-keys.js +37 -0
  114. package/dist/formatting/Countdown.svelte +110 -0
  115. package/dist/formatting/Countdown.svelte.d.ts +35 -0
  116. package/dist/formatting/Duration.svelte +18 -0
  117. package/dist/formatting/Duration.svelte.d.ts +20 -0
  118. package/dist/formatting/Highlight.svelte +4 -0
  119. package/dist/formatting/Highlight.svelte.d.ts +4 -0
  120. package/dist/formatting/duration.d.ts +44 -0
  121. package/dist/formatting/duration.js +125 -0
  122. package/dist/formatting/index.d.ts +3 -0
  123. package/dist/formatting/index.js +3 -0
  124. package/dist/forms/EntityForm.svelte +53 -45
  125. package/dist/forms/EntityForm.svelte.d.ts +12 -3
  126. package/dist/hotkeys/index.d.ts +5 -0
  127. package/dist/hotkeys/index.js +5 -0
  128. package/dist/hotkeys/match.js +12 -10
  129. package/dist/hotkeys/types.d.ts +4 -2
  130. package/dist/index.d.ts +2 -1
  131. package/dist/index.js +2 -1
  132. package/dist/infobox/Infobox.svelte +2 -2
  133. package/dist/infobox/Infobox.svelte.d.ts +7 -0
  134. package/dist/infobox/InfoboxHeader.svelte +15 -7
  135. package/dist/infobox/InfoboxHeader.svelte.d.ts +7 -0
  136. package/dist/infobox/types.d.ts +5 -5
  137. package/dist/layout/AppShell.svelte +50 -0
  138. package/dist/layout/AppShell.svelte.d.ts +28 -0
  139. package/dist/layout/Cluster.svelte +28 -0
  140. package/dist/layout/Cluster.svelte.d.ts +27 -0
  141. package/dist/layout/Fits.svelte +27 -0
  142. package/dist/layout/Fits.svelte.d.ts +13 -0
  143. package/dist/layout/Flow.svelte +46 -0
  144. package/dist/layout/Flow.svelte.d.ts +16 -0
  145. package/dist/layout/Grid.svelte +23 -0
  146. package/dist/layout/Grid.svelte.d.ts +26 -0
  147. package/dist/layout/HStack.svelte +12 -0
  148. package/dist/layout/HStack.svelte.d.ts +34 -0
  149. package/dist/layout/Hero.svelte +76 -0
  150. package/dist/layout/Hero.svelte.d.ts +36 -0
  151. package/dist/layout/Navbar.svelte +2 -2
  152. package/dist/layout/Sidebar.svelte +1 -1
  153. package/dist/layout/Spacer.svelte +12 -0
  154. package/dist/layout/Spacer.svelte.d.ts +6 -0
  155. package/dist/layout/VStack.svelte +12 -0
  156. package/dist/layout/VStack.svelte.d.ts +29 -0
  157. package/dist/layout/index.d.ts +9 -0
  158. package/dist/layout/index.js +9 -0
  159. package/dist/layout/space.d.ts +44 -0
  160. package/dist/layout/space.js +76 -0
  161. package/dist/search/types.d.ts +3 -0
  162. package/dist/seo/og-image.d.ts +4 -3
  163. package/dist/seo/og-image.js +4 -3
  164. package/dist/tips/tip-stack.d.ts +1 -0
  165. package/dist/tips/tip-store.svelte.d.ts +2 -1
  166. package/dist/ui/Accordion.svelte +75 -0
  167. package/dist/ui/Accordion.svelte.d.ts +50 -0
  168. package/dist/ui/AccordionItem.svelte +73 -0
  169. package/dist/ui/AccordionItem.svelte.d.ts +18 -0
  170. package/dist/ui/Button.svelte +3 -2
  171. package/dist/ui/ButtonGroup.svelte +24 -0
  172. package/dist/ui/ButtonGroup.svelte.d.ts +17 -0
  173. package/dist/ui/ContextMenu.svelte +75 -0
  174. package/dist/ui/ContextMenu.svelte.d.ts +25 -0
  175. package/dist/ui/Disclosure.svelte +7 -0
  176. package/dist/ui/Disclosure.svelte.d.ts +2 -2
  177. package/dist/ui/Divider.svelte +47 -0
  178. package/dist/ui/Divider.svelte.d.ts +28 -0
  179. package/dist/ui/DropdownMenu.svelte +44 -0
  180. package/dist/ui/DropdownMenu.svelte.d.ts +30 -0
  181. package/dist/ui/Menu.svelte +17 -47
  182. package/dist/ui/Menu.svelte.d.ts +3 -24
  183. package/dist/ui/MenuCheckboxItem.svelte +33 -0
  184. package/dist/ui/MenuCheckboxItem.svelte.d.ts +20 -0
  185. package/dist/ui/MenuItem.svelte +29 -31
  186. package/dist/ui/MenuItem.svelte.d.ts +2 -2
  187. package/dist/ui/MenuRadioGroup.svelte +25 -0
  188. package/dist/ui/MenuRadioGroup.svelte.d.ts +20 -0
  189. package/dist/ui/MenuRadioItem.svelte +20 -0
  190. package/dist/ui/MenuRadioItem.svelte.d.ts +15 -0
  191. package/dist/ui/MenuSeparator.svelte +12 -11
  192. package/dist/ui/MenuSub.svelte +24 -0
  193. package/dist/ui/MenuSub.svelte.d.ts +15 -0
  194. package/dist/ui/Spinner.svelte +41 -0
  195. package/dist/ui/Spinner.svelte.d.ts +35 -0
  196. package/dist/ui/Stack.svelte +36 -0
  197. package/dist/ui/Stack.svelte.d.ts +48 -0
  198. package/dist/ui/ToggleButton.svelte +58 -0
  199. package/dist/ui/ToggleButton.svelte.d.ts +45 -0
  200. package/dist/ui/ToggleGroup.svelte +142 -0
  201. package/dist/ui/ToggleGroup.svelte.d.ts +86 -0
  202. package/dist/ui/Toolbar.svelte +40 -0
  203. package/dist/ui/Toolbar.svelte.d.ts +20 -0
  204. package/dist/ui/ToolbarButton.svelte +29 -0
  205. package/dist/ui/ToolbarButton.svelte.d.ts +21 -0
  206. package/dist/ui/ToolbarLink.svelte +21 -0
  207. package/dist/ui/ToolbarLink.svelte.d.ts +12 -0
  208. package/dist/ui/ToolbarSeparator.svelte +22 -0
  209. package/dist/ui/ToolbarSeparator.svelte.d.ts +12 -0
  210. package/dist/ui/ToolbarToggleGroup.svelte +66 -0
  211. package/dist/ui/ToolbarToggleGroup.svelte.d.ts +52 -0
  212. package/dist/ui/accordion-context.d.ts +26 -0
  213. package/dist/ui/accordion-context.js +23 -0
  214. package/dist/ui/context-menu.d.ts +13 -0
  215. package/dist/ui/context-menu.js +13 -0
  216. package/dist/ui/index.d.ts +19 -0
  217. package/dist/ui/index.js +18 -0
  218. package/dist/ui/menu-context.d.ts +13 -15
  219. package/dist/ui/menu-context.js +14 -18
  220. package/dist/ui/menu-parts/CheckboxItemPart.svelte +26 -0
  221. package/dist/ui/menu-parts/CheckboxItemPart.svelte.d.ts +5 -0
  222. package/dist/ui/menu-parts/ItemPart.svelte +29 -0
  223. package/dist/ui/menu-parts/ItemPart.svelte.d.ts +5 -0
  224. package/dist/ui/menu-parts/RadioGroupPart.svelte +24 -0
  225. package/dist/ui/menu-parts/RadioGroupPart.svelte.d.ts +5 -0
  226. package/dist/ui/menu-parts/RadioItemPart.svelte +22 -0
  227. package/dist/ui/menu-parts/RadioItemPart.svelte.d.ts +5 -0
  228. package/dist/ui/menu-parts/SeparatorPart.svelte +16 -0
  229. package/dist/ui/menu-parts/SeparatorPart.svelte.d.ts +5 -0
  230. package/dist/ui/menu-parts/SubPart.svelte +28 -0
  231. package/dist/ui/menu-parts/SubPart.svelte.d.ts +5 -0
  232. package/dist/ui/menu-parts/menu-parts.d.ts +104 -0
  233. package/dist/ui/menu-parts/menu-parts.js +31 -0
  234. package/dist/ui/menu-parts/parts.d.ts +12 -0
  235. package/dist/ui/menu-parts/parts.js +27 -0
  236. package/dist/ui/toggle-classes.d.ts +39 -0
  237. package/dist/ui/toggle-classes.js +36 -0
  238. package/dist/ui/tones.d.ts +35 -0
  239. package/dist/ui/tones.js +90 -0
  240. package/package.json +34 -18
@@ -2,6 +2,10 @@ interface Segment {
2
2
  text: string;
3
3
  match: boolean;
4
4
  }
5
+ /**
6
+ * Split `text` into segments, each flagged `match` when it is a case-insensitive hit for one of `query`'s whitespace-separated tokens.
7
+ * A blank `query` or `text` returns the whole text as one unmatched segment.
8
+ */
5
9
  export declare function tokenise(text: string, query: string): Segment[];
6
10
  interface Props {
7
11
  /** The full text to render. */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Lengths of time: the value type, its ISO 8601 form for `<time datetime>`,
3
+ * and a locale-aware formatter that `Duration` and `Countdown` share and an
4
+ * app can call in plain TypeScript.
5
+ *
6
+ * `formatDuration` uses `Intl.DurationFormat` where the runtime has it
7
+ * (Baseline 2025) and otherwise composes per-unit `Intl.NumberFormat` unit
8
+ * strings with an `Intl.ListFormat` of `type: 'unit'`, so the fallback is
9
+ * still localised, just less idiomatic.
10
+ *
11
+ * @module
12
+ */
13
+ /** A length of time; any subset of fields. Mirrors `Intl.DurationFormat`'s input. */
14
+ export interface DurationValue {
15
+ readonly years?: number | undefined;
16
+ readonly months?: number | undefined;
17
+ readonly weeks?: number | undefined;
18
+ readonly days?: number | undefined;
19
+ readonly hours?: number | undefined;
20
+ readonly minutes?: number | undefined;
21
+ readonly seconds?: number | undefined;
22
+ readonly milliseconds?: number | undefined;
23
+ }
24
+ /** How a duration is spelled: `'2 hours, 30 minutes'`, `'2 hr, 30 min'`, `'2h 30m'` or `'2:30:00'`. */
25
+ export type DurationStyle = 'long' | 'short' | 'narrow' | 'digital';
26
+ /**
27
+ * Split milliseconds into days, hours, minutes, seconds and milliseconds,
28
+ * leaving out the zero fields (`9_309_000` → `{ hours: 2, minutes: 35,
29
+ * seconds: 9 }`). Days are the largest unit: months and years have no fixed
30
+ * length. Negative input is treated as zero.
31
+ */
32
+ export declare function durationFromMilliseconds(ms: number): DurationValue;
33
+ /**
34
+ * The ISO 8601 duration string for `<time datetime>`: `{ hours: 2,
35
+ * minutes: 30 }` → `'PT2H30M'`. Milliseconds join the seconds as a
36
+ * fraction; an empty duration is `'PT0S'`.
37
+ */
38
+ export declare function toIsoDuration(value: DurationValue): string;
39
+ /**
40
+ * Format a duration for `locales` in `style`, through `Intl.DurationFormat`
41
+ * where available and a localised `NumberFormat` + `ListFormat` composition
42
+ * otherwise.
43
+ */
44
+ export declare function formatDuration(value: DurationValue, locales?: string | readonly string[], style?: DurationStyle): string;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Lengths of time: the value type, its ISO 8601 form for `<time datetime>`,
3
+ * and a locale-aware formatter that `Duration` and `Countdown` share and an
4
+ * app can call in plain TypeScript.
5
+ *
6
+ * `formatDuration` uses `Intl.DurationFormat` where the runtime has it
7
+ * (Baseline 2025) and otherwise composes per-unit `Intl.NumberFormat` unit
8
+ * strings with an `Intl.ListFormat` of `type: 'unit'`, so the fallback is
9
+ * still localised, just less idiomatic.
10
+ *
11
+ * @module
12
+ */
13
+ /** The fields, largest first, with the `Intl` unit each formats as. */
14
+ const FIELDS = [
15
+ { field: 'years', unit: 'year' },
16
+ { field: 'months', unit: 'month' },
17
+ { field: 'weeks', unit: 'week' },
18
+ { field: 'days', unit: 'day' },
19
+ { field: 'hours', unit: 'hour' },
20
+ { field: 'minutes', unit: 'minute' },
21
+ { field: 'seconds', unit: 'second' },
22
+ { field: 'milliseconds', unit: 'millisecond' },
23
+ ];
24
+ const SECOND = 1000;
25
+ const MINUTE = 60 * SECOND;
26
+ const HOUR = 60 * MINUTE;
27
+ const DAY = 24 * HOUR;
28
+ /**
29
+ * Split milliseconds into days, hours, minutes, seconds and milliseconds,
30
+ * leaving out the zero fields (`9_309_000` → `{ hours: 2, minutes: 35,
31
+ * seconds: 9 }`). Days are the largest unit: months and years have no fixed
32
+ * length. Negative input is treated as zero.
33
+ */
34
+ export function durationFromMilliseconds(ms) {
35
+ let rest = Math.max(0, Math.round(ms));
36
+ const parts = {};
37
+ for (const [field, size] of [
38
+ ['days', DAY],
39
+ ['hours', HOUR],
40
+ ['minutes', MINUTE],
41
+ ['seconds', SECOND],
42
+ ['milliseconds', 1],
43
+ ]) {
44
+ const count = Math.floor(rest / size);
45
+ rest -= count * size;
46
+ if (count > 0)
47
+ parts[field] = count;
48
+ }
49
+ return parts;
50
+ }
51
+ /**
52
+ * The ISO 8601 duration string for `<time datetime>`: `{ hours: 2,
53
+ * minutes: 30 }` → `'PT2H30M'`. Milliseconds join the seconds as a
54
+ * fraction; an empty duration is `'PT0S'`.
55
+ */
56
+ export function toIsoDuration(value) {
57
+ const amount = (field) => value[field] ?? 0;
58
+ const date = [
59
+ amount('years') > 0 ? `${amount('years')}Y` : '',
60
+ amount('months') > 0 ? `${amount('months')}M` : '',
61
+ amount('weeks') > 0 ? `${amount('weeks')}W` : '',
62
+ amount('days') > 0 ? `${amount('days')}D` : '',
63
+ ].join('');
64
+ const seconds = amount('seconds') + amount('milliseconds') / 1000;
65
+ const time = [
66
+ amount('hours') > 0 ? `${amount('hours')}H` : '',
67
+ amount('minutes') > 0 ? `${amount('minutes')}M` : '',
68
+ seconds > 0 ? `${Number(seconds.toFixed(3))}S` : '',
69
+ ].join('');
70
+ if (date === '' && time === '')
71
+ return 'PT0S';
72
+ return `P${date}${time === '' ? '' : `T${time}`}`;
73
+ }
74
+ /** `Intl.DurationFormat` when the runtime has it. */
75
+ function nativeDurationFormat() {
76
+ const intl = Intl;
77
+ return intl.DurationFormat;
78
+ }
79
+ /** `value` with the `undefined` and zero fields dropped, as `Intl.DurationFormat` wants. */
80
+ function presentFields(value) {
81
+ return Object.fromEntries(FIELDS.filter(({ field }) => (value[field] ?? 0) !== 0).map(({ field }) => [
82
+ field,
83
+ value[field],
84
+ ]));
85
+ }
86
+ /** `H:MM:SS` from the hours, minutes and seconds, the larger units folded into hours. */
87
+ function digital(value) {
88
+ const hours = (value.days ?? 0) * 24 + (value.hours ?? 0);
89
+ const pad = (n) => String(n).padStart(2, '0');
90
+ return `${hours}:${pad(value.minutes ?? 0)}:${pad(value.seconds ?? 0)}`;
91
+ }
92
+ /**
93
+ * Format a duration for `locales` in `style`, through `Intl.DurationFormat`
94
+ * where available and a localised `NumberFormat` + `ListFormat` composition
95
+ * otherwise.
96
+ */
97
+ export function formatDuration(value, locales, style = 'short') {
98
+ const present = presentFields(value);
99
+ const Native = nativeDurationFormat();
100
+ if (Native !== undefined) {
101
+ return new Native(locales, { style }).format(present);
102
+ }
103
+ if (style === 'digital')
104
+ return digital(value);
105
+ const unitDisplay = style;
106
+ // A mutable copy: TS6's `Intl.ListFormat` (svelte-check) takes no readonly array.
107
+ const tags = typeof locales === 'object' ? [...locales] : locales;
108
+ const pieces = FIELDS.filter(({ field }) => present[field] !== undefined).map(({ field, unit }) => new Intl.NumberFormat(tags, {
109
+ style: 'unit',
110
+ unit,
111
+ unitDisplay,
112
+ }).format(present[field] ?? 0));
113
+ return new Intl.ListFormat(tags, {
114
+ type: 'unit',
115
+ style: style === 'long' ? 'long' : style === 'short' ? 'short' : 'narrow',
116
+ }).format(pieces.length === 0
117
+ ? [
118
+ new Intl.NumberFormat(tags, {
119
+ style: 'unit',
120
+ unit: 'second',
121
+ unitDisplay,
122
+ }).format(0),
123
+ ]
124
+ : pieces);
125
+ }
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * @module
6
6
  */
7
+ export { default as Countdown } from './Countdown.svelte';
7
8
  export { default as DateTime } from './DateTime.svelte';
9
+ export { default as Duration } from './Duration.svelte';
10
+ export * from './duration.ts';
8
11
  export { default as NumberFormat } from './Number.svelte';
9
12
  export { default as FormatList } from './FormatList.svelte';
10
13
  export { default as RelativeTime } from './RelativeTime.svelte';
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * @module
6
6
  */
7
+ export { default as Countdown } from './Countdown.svelte';
7
8
  export { default as DateTime } from './DateTime.svelte';
9
+ export { default as Duration } from './Duration.svelte';
10
+ export * from "./duration.js";
8
11
  // `NumberFormat`, not `Number`: a named export called `Number` would shadow
9
12
  // the global constructor for anyone importing it by that name.
10
13
  export { default as NumberFormat } from './Number.svelte';
@@ -34,6 +34,10 @@ Renders an `@thomasfosterau/effect-forms-svelte` `defineEntityForm` definition t
34
34
  enhancement); a SvelteKit app passes its `$app/forms`-backed `enhanceForm`,
35
35
  which this package cannot import.
36
36
 
37
+ **Form-level messages.** The store's root errors and the caller's
38
+ `messages` (a failure the store never saw, such as a server action's
39
+ `fail(500, { message })`) share one `FormErrors` block, store errors first.
40
+
37
41
  Every string that is not the definition's own (labels, submit labels and
38
42
  option labels are its message thunks) is a prop with an English default.
39
43
  -->
@@ -45,7 +49,7 @@ import TextInput from "../fields/TextInput.svelte";
45
49
  import Textarea from "../fields/Textarea.svelte";
46
50
  import FormActions from "./FormActions.svelte";
47
51
  import FormErrors from "./FormErrors.svelte";
48
- let { definition, mode, cancelHref, action, initial = {}, initialRows = {}, fieldProps = {}, enhance = (form) => form.props, cancelLabel = "Cancel", pendingLabel = "Saving…", selectPlaceholder = "Choose…", requiredLabel, class: className, ...rest } = $props();
52
+ let { definition, mode, cancelHref, action, initial = {}, initialRows = {}, fieldProps = {}, enhance = (form) => form.props, cancelLabel = "Cancel", pendingLabel = "Saving…", selectPlaceholder = "Choose…", requiredLabel, messages, class: className, ...rest } = $props();
49
53
  // Configure-once, at mount (see the header): these reads are deliberately
50
54
  // not reactive.
51
55
  // svelte-ignore state_referenced_locally
@@ -56,20 +60,21 @@ const configured = {
56
60
  initialRows
57
61
  };
58
62
  const baseDefaults = configured.mode === "create" ? configured.definition.createDefaults : configured.definition.updateDefaults;
63
+ const mintedId = configured.mode === "create" && configured.definition.clientId ? { id: crypto.randomUUID() } : undefined;
59
64
  const defaults = {
60
65
  ...baseDefaults,
61
- ...configured.initial
66
+ ...configured.initial,
67
+ ...mintedId
62
68
  };
63
- if (configured.mode === "create" && configured.definition.clientId) {
64
- defaults["id"] = crypto.randomUUID();
65
- }
66
69
  const form = createForm({
67
70
  schema: configured.mode === "create" ? configured.definition.createInput : configured.definition.updateInput,
68
71
  defaults
69
72
  });
70
73
  const entries = configured.definition.entriesFor(configured.mode);
71
- // `defineEntityForm` validated every entry key against the derived
72
- // all-string input schema, which the path-typed accessor cannot see.
74
+ // SAFETY: `defineEntityForm` validated every entry key against the derived
75
+ // all-string input schema. The definition types that schema `Schema.Top`,
76
+ // whose path-typed accessor admits no key (`never`), so the key is asserted
77
+ // into it and the handle typed by this function's own return type.
73
78
  const handle = (key) => form.field(key);
74
79
  const getField = (key) => handle(key).value ?? "";
75
80
  const setField = (key, next) => {
@@ -97,10 +102,47 @@ const inputMode = (entry) => entry.options.inputmode ?? (configured.definition.f
97
102
  const showHiddenId = configured.mode === "update" || configured.definition.clientId;
98
103
  const submitLabel = $derived(mode === "create" ? definition.submit.create() : definition.submit.update());
99
104
  const gated = $derived(entries.some((entry) => entry._tag === "widget" && entry.options.gate === true && getField(entry.key) === ""));
100
- const rootErrors = $derived(form.errors._errors ?? []);
105
+ // The empty path is the form's own (non-field) messages.
106
+ // The caller's `messages` (a server fault with no form state) follow them.
107
+ const rootErrors = $derived([...form.errorsAt([]), ...messages ?? []]);
101
108
  const fieldId = (key) => `entity-form-${key}`;
109
+ // The `FormField` wrapper is the same for a select and a scalar entry.
110
+ const fieldAttrs = (entry) => ({
111
+ id: fieldId(entry.key),
112
+ label: entry.options.label(),
113
+ required: entry.options.required === true,
114
+ errors: handle(entry.key).visibleErrors,
115
+ requiredLabel
116
+ });
102
117
  </script>
103
118
 
119
+ <!-- A scalar entry's control: a textarea, a date input or a text input. -->
120
+ {#snippet scalarInput(entry: ScalarEntry, control: FieldControl)}
121
+ {#if entry.multiline}
122
+ <Textarea
123
+ {...control}
124
+ name={entry.key}
125
+ placeholder={entry.options.placeholder?.()}
126
+ bind:value={() => getField(entry.key), (next: string) => setField(entry.key, next)}
127
+ />
128
+ {:else if entry.options.type === 'date'}
129
+ <DateInput
130
+ {...control}
131
+ name={entry.key}
132
+ bind:value={() => getField(entry.key), (next: string) => setField(entry.key, next)}
133
+ />
134
+ {:else}
135
+ <TextInput
136
+ {...control}
137
+ name={entry.key}
138
+ type={entry.options.type === 'url' ? 'url' : 'text'}
139
+ inputmode={inputMode(entry)}
140
+ placeholder={entry.options.placeholder?.()}
141
+ bind:value={() => getField(entry.key), (next: string) => setField(entry.key, next)}
142
+ />
143
+ {/if}
144
+ {/snippet}
145
+
104
146
  <form {...rest} {...enhance(form)} {action} class={['space-y-4', className]}>
105
147
  {#if showHiddenId}
106
148
  <input type="hidden" name="id" value={getField('id')} />
@@ -112,13 +154,7 @@ const fieldId = (key) => `entity-form-${key}`;
112
154
  {#if entry._tag === 'widget'}
113
155
  <entry.component {...entry.props()} {...fieldProps[entry.key] ?? {}} {...bridge(entry)} />
114
156
  {:else if entry._tag === 'select'}
115
- <FormField
116
- id={fieldId(entry.key)}
117
- label={entry.options.label()}
118
- required={entry.options.required === true}
119
- errors={handle(entry.key).visibleErrors}
120
- {requiredLabel}
121
- >
157
+ <FormField {...fieldAttrs(entry)}>
122
158
  {#snippet children(control)}
123
159
  <Select
124
160
  {...control}
@@ -130,37 +166,9 @@ const fieldId = (key) => `entity-form-${key}`;
130
166
  {/snippet}
131
167
  </FormField>
132
168
  {:else}
133
- <FormField
134
- id={fieldId(entry.key)}
135
- label={entry.options.label()}
136
- required={entry.options.required === true}
137
- errors={handle(entry.key).visibleErrors}
138
- {requiredLabel}
139
- >
169
+ <FormField {...fieldAttrs(entry)}>
140
170
  {#snippet children(control)}
141
- {#if entry.multiline}
142
- <Textarea
143
- {...control}
144
- name={entry.key}
145
- placeholder={entry.options.placeholder?.()}
146
- bind:value={() => getField(entry.key), (next: string) => setField(entry.key, next)}
147
- />
148
- {:else if entry.options.type === 'date'}
149
- <DateInput
150
- {...control}
151
- name={entry.key}
152
- bind:value={() => getField(entry.key), (next: string) => setField(entry.key, next)}
153
- />
154
- {:else}
155
- <TextInput
156
- {...control}
157
- name={entry.key}
158
- type={entry.options.type === 'url' ? 'url' : 'text'}
159
- inputmode={inputMode(entry)}
160
- placeholder={entry.options.placeholder?.()}
161
- bind:value={() => getField(entry.key), (next: string) => setField(entry.key, next)}
162
- />
163
- {/if}
171
+ {@render scalarInput(entry, control)}
164
172
  {/snippet}
165
173
  </FormField>
166
174
  {/if}
@@ -1,6 +1,8 @@
1
- import { createForm, type EntityFormDefinition } from '@thomasfosterau/effect-forms-svelte';
1
+ import { createForm, type EntityFormDefinition, type WidgetEntry } from '@thomasfosterau/effect-forms-svelte';
2
2
  import type { HTMLFormAttributes } from 'svelte/elements';
3
3
  type Store = ReturnType<typeof createForm>;
4
+ /** The props a widget entry receives: its definition's `props()` shape, owned by the form library. */
5
+ type WidgetProps = ReturnType<WidgetEntry['props']>;
4
6
  interface Props extends Omit<HTMLFormAttributes, 'action' | 'method' | 'children'> {
5
7
  /** The form definition from `defineEntityForm`. */
6
8
  definition: EntityFormDefinition;
@@ -24,12 +26,12 @@ interface Props extends Omit<HTMLFormAttributes, 'action' | 'method' | 'children
24
26
  * Render-time extra props per widget key (a picker's load-resolved `options`).
25
27
  * @defaultValue `{}`
26
28
  */
27
- fieldProps?: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined;
29
+ fieldProps?: Readonly<Record<string, Readonly<WidgetProps>>> | undefined;
28
30
  /**
29
31
  * The spreadable `<form>` attributes for this store.
30
32
  * @defaultValue the store's own `form.props`
31
33
  */
32
- enhance?: ((form: Store) => Record<string | symbol, unknown>) | undefined;
34
+ enhance?: ((form: Store) => Store['props']) | undefined;
33
35
  /**
34
36
  * Text of the cancel link.
35
37
  * @defaultValue `'Cancel'`
@@ -50,6 +52,13 @@ interface Props extends Omit<HTMLFormAttributes, 'action' | 'method' | 'children
50
52
  * @defaultValue `'(required)'` (from `FormField`)
51
53
  */
52
54
  requiredLabel?: string | undefined;
55
+ /**
56
+ * Form-level messages from outside the store — a server action's failure
57
+ * message that carries no form state (`page.form?.message`, wrapped in an
58
+ * array). Rendered in the same `FormErrors` block as the store's own root
59
+ * errors, after them, one `role="alert"` each. The text is shown as given.
60
+ */
61
+ messages?: readonly string[] | undefined;
53
62
  }
54
63
  /** Renders an `@thomasfosterau/effect-forms-svelte` `defineEntityForm` definition through this library’s field primitives. */
55
64
  declare const EntityForm: import("svelte").Component<Props, {}, "">;
@@ -12,6 +12,11 @@
12
12
  * registered, and the two components take their own chrome copy
13
13
  * (`title`, `closeLabel`, `commandLabel`, …) with English defaults.
14
14
  *
15
+ * The family is re-exported from the root barrel and is also its own entry,
16
+ * `@thomasfosterau/components/hotkeys`, so a consumer can
17
+ * `import('@thomasfosterau/components/hotkeys')` to load `HotkeyHelpDialog`
18
+ * lazily, on the first `?`, instead of in its entry chunk.
19
+ *
15
20
  * @module
16
21
  */
17
22
  export { HotkeyRegistryMissing, createHotkeyRegistry, getHotkeyRegistry, registerHotkeys, tryGetHotkeyRegistry, } from './context.svelte.ts';
@@ -12,6 +12,11 @@
12
12
  * registered, and the two components take their own chrome copy
13
13
  * (`title`, `closeLabel`, `commandLabel`, …) with English defaults.
14
14
  *
15
+ * The family is re-exported from the root barrel and is also its own entry,
16
+ * `@thomasfosterau/components/hotkeys`, so a consumer can
17
+ * `import('@thomasfosterau/components/hotkeys')` to load `HotkeyHelpDialog`
18
+ * lazily, on the first `?`, instead of in its entry chunk.
19
+ *
15
20
  * @module
16
21
  */
17
22
  export { HotkeyRegistryMissing, createHotkeyRegistry, getHotkeyRegistry, registerHotkeys, tryGetHotkeyRegistry, } from "./context.svelte.js";
@@ -101,22 +101,24 @@ export function matchChord(event, actions) {
101
101
  * `actions` must already be in precedence order.
102
102
  */
103
103
  export function matchSequence(buffer, actions) {
104
- return actions.reduce((match, { action }) => {
104
+ let exact;
105
+ let isPrefix = false;
106
+ for (const { action } of actions) {
105
107
  const trigger = action.trigger;
106
108
  if (trigger === undefined)
107
- return match;
109
+ continue;
108
110
  const sequence = asSequence(trigger);
109
111
  if (sequence === undefined)
110
- return match;
112
+ continue;
111
113
  if (sequence.length === buffer.length) {
112
- return match.exact === undefined && sequence.every((key, i) => key === buffer[i])
113
- ? { ...match, exact: action }
114
- : match;
114
+ if (exact === undefined && sequence.every((key, i) => key === buffer[i]))
115
+ exact = action;
115
116
  }
116
- return sequence.length > buffer.length && buffer.every((key, i) => key === sequence[i])
117
- ? { ...match, isPrefix: true }
118
- : match;
119
- }, { exact: undefined, isPrefix: false });
117
+ else if (sequence.length > buffer.length && buffer.every((key, i) => key === sequence[i])) {
118
+ isPrefix = true;
119
+ }
120
+ }
121
+ return { exact, isPrefix };
120
122
  }
121
123
  /** Project a trigger into its display form for help overlays and badges. */
122
124
  export function triggerDisplay(trigger) {
@@ -31,10 +31,10 @@ export type HotkeyChord = string;
31
31
  export type HotkeySequence = readonly string[];
32
32
  /** A hotkey trigger: a chord pressed together (`'$mod+k'`) or a sequence pressed in turn (`['g', 'p']`). */
33
33
  export type HotkeyTrigger = HotkeyChord | HotkeySequence;
34
- /** Palette grouping. Drives section headings and ordering. */
34
+ /** The command-palette section a `CommandAction` is listed under. Drives section headings and ordering. */
35
35
  export type CommandActionGroup = 'actions' | 'navigation';
36
36
  /**
37
- * Where a registration lives. Page registrations are torn down when the
37
+ * Where a hotkey registration lives: `global` for the app shell, `page` for the mounted page. Page registrations are torn down when the
38
38
  * registering page unmounts and win over global registrations on trigger
39
39
  * conflicts.
40
40
  */
@@ -67,6 +67,8 @@ export interface CommandAction {
67
67
  }
68
68
  /** A registration entry: an action plus the scope it was registered under. */
69
69
  export interface RegisteredAction {
70
+ /** The scope it was registered under. */
70
71
  scope: HotkeyScope;
72
+ /** The action as registered. */
71
73
  action: CommandAction;
72
74
  }
package/dist/index.d.ts CHANGED
@@ -14,7 +14,8 @@
14
14
  * are separate entry points (`@thomasfosterau/components/<family>`), as is
15
15
  * `EntityForm` (`@thomasfosterau/components/entity-form`, which needs the
16
16
  * optional peer `@thomasfosterau/effect-forms-svelte`); none is re-exported
17
- * here. The maps family lives one package over, in
17
+ * here. `hotkeys` is both: re-exported here and its own
18
+ * `@thomasfosterau/components/hotkeys` entry, for a lazily loaded help dialog. The maps family lives one package over, in
18
19
  * `@thomasfosterau/components-mapbox`: its behaviour needs a specific
19
20
  * third-party engine (`mapbox-gl`), and a component that needs one lives in
20
21
  * its own package so this one stays free of it.
package/dist/index.js CHANGED
@@ -14,7 +14,8 @@
14
14
  * are separate entry points (`@thomasfosterau/components/<family>`), as is
15
15
  * `EntityForm` (`@thomasfosterau/components/entity-form`, which needs the
16
16
  * optional peer `@thomasfosterau/effect-forms-svelte`); none is re-exported
17
- * here. The maps family lives one package over, in
17
+ * here. `hotkeys` is both: re-exported here and its own
18
+ * `@thomasfosterau/components/hotkeys` entry, for a lazily loaded help dialog. The maps family lives one package over, in
18
19
  * `@thomasfosterau/components-mapbox`: its behaviour needs a specific
19
20
  * third-party engine (`mapbox-gl`), and a component that needs one lives in
20
21
  * its own package so this one stays free of it.
@@ -27,7 +27,7 @@ import InfoboxImage from "./InfoboxImage.svelte";
27
27
  import InfoboxRow from "./InfoboxRow.svelte";
28
28
  import InfoboxSection from "./InfoboxSection.svelte";
29
29
  import { infoboxRowIsPresent } from "./types.js";
30
- let { title, subtitle, label = "Key facts", image, rows, float = false, children, class: className, ...rest } = $props();
30
+ let { title, subtitle, headingLevel, label = "Key facts", image, rows, float = false, children, class: className, ...rest } = $props();
31
31
  const presentRows = $derived((rows ?? []).filter(infoboxRowIsPresent));
32
32
  const imageRows = $derived(presentRows.filter((row) => row.kind === "image"));
33
33
  const bodyRows = $derived(presentRows.filter((row) => row.kind !== "image"));
@@ -44,7 +44,7 @@ const bodyRows = $derived(presentRows.filter((row) => row.kind !== "image"));
44
44
  ]}
45
45
  >
46
46
  {#if title !== undefined}
47
- <InfoboxHeader {title} {subtitle} />
47
+ <InfoboxHeader {title} {subtitle} {headingLevel} />
48
48
  {/if}
49
49
  {#if image !== undefined}
50
50
  <InfoboxImage {...image} />
@@ -1,11 +1,18 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { HeadingLevel } from '../ui/tones.ts';
3
4
  import { type InfoboxImageSpec, type InfoboxRowSpec as Row } from './types.ts';
4
5
  interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'title'> {
5
6
  /** Heading of the rail; also its accessible name. */
6
7
  title?: string | undefined;
7
8
  /** A line under the title. */
8
9
  subtitle?: string | undefined;
10
+ /**
11
+ * The title's heading level (`InfoboxHeader`'s). Keep the default beside
12
+ * prose; raise it when the box is a section of the page in its own right.
13
+ * @defaultValue `3`
14
+ */
15
+ headingLevel?: Exclude<HeadingLevel, 1> | undefined;
9
16
  /**
10
17
  * Accessible name for the region when there is no `title`.
11
18
  * @defaultValue `'Key facts'`
@@ -4,13 +4,19 @@ The title block at the top of an `Infobox`.
4
4
  <!--
5
5
  InfoboxHeader — the title block at the top of an `Infobox`.
6
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.
7
+ An `h3` by default rather than an `h2`: the box sits beside an article's
8
+ prose, and its title names the summary, not a section of the document.
9
+ Promoting it to the level a real section heading would use puts a phantom
10
+ entry in the page's outline between two real ones — the box is
11
+ deliberately outside the outline.
12
+
13
+ `headingLevel` raises (or lowers) it for a page that uses the box as a
14
+ section in its own right, where `h3` would skip a level (`h1` → `h3`) or
15
+ read as a sub-heading of the section before it. Only the tag changes; the
16
+ classes are the same at every level. `1` is excluded: the box never titles
17
+ the page.
12
18
  -->
13
- <script lang="ts">let { title, subtitle, class: className, ...rest } = $props();
19
+ <script lang="ts">let { title, subtitle, headingLevel = 3, class: className, ...rest } = $props();
14
20
  export {};
15
21
  </script>
16
22
 
@@ -18,7 +24,9 @@ export {};
18
24
  {...rest}
19
25
  class={["border-base-300 bg-base-300 border-b px-4 py-3", className]}
20
26
  >
21
- <h3 class="text-center text-lg font-bold select-none">{title}</h3>
27
+ <svelte:element this={`h${headingLevel}`} class="text-center text-lg font-bold select-none">
28
+ {title}
29
+ </svelte:element>
22
30
  {#if subtitle !== undefined && subtitle !== ""}
23
31
  <p class="text-base-content/60 mt-1 text-center text-sm select-none">
24
32
  {subtitle}
@@ -1,9 +1,16 @@
1
1
  import type { HTMLAttributes } from "svelte/elements";
2
+ import type { HeadingLevel } from "../ui/tones.ts";
2
3
  interface Props extends HTMLAttributes<HTMLDivElement> {
3
4
  /** The infobox heading text. */
4
5
  title: string;
5
6
  /** A line under the heading. */
6
7
  subtitle?: string | undefined;
8
+ /**
9
+ * The heading's level. Keep the default when the box sits beside prose;
10
+ * raise it to the page's section level when the box is a section itself.
11
+ * @defaultValue `3`
12
+ */
13
+ headingLevel?: Exclude<HeadingLevel, 1> | undefined;
7
14
  }
8
15
  /** The title block at the top of an `Infobox`. */
9
16
  declare const InfoboxHeader: import("svelte").Component<Props, {}, "">;
@@ -23,14 +23,14 @@ export interface InfoboxImageSpec {
23
23
  */
24
24
  readonly eager?: boolean | undefined;
25
25
  }
26
- /** A single identifying fact (e.g. publication year, ISBN, page count). */
26
+ /** An `Infobox` row holding a single identifying fact (e.g. publication year, ISBN, page count). */
27
27
  export interface InfoboxFactRow {
28
28
  readonly kind: 'fact';
29
29
  readonly label: string;
30
30
  /** `undefined` or an empty string skips the row. */
31
31
  readonly value: string | number | undefined;
32
32
  }
33
- /** A single named link, rendered as one row with its own visible text. */
33
+ /** An `Infobox` row holding a single named link, with its own visible text. */
34
34
  export interface InfoboxLinkRow {
35
35
  readonly kind: 'link';
36
36
  readonly label: string;
@@ -45,7 +45,7 @@ export interface InfoboxLinkEntry {
45
45
  readonly label: string;
46
46
  readonly href: string;
47
47
  }
48
- /** A named outbound relationship rendered as one or more links. */
48
+ /** An `Infobox` row for a named outbound relationship, rendered as one or more links. */
49
49
  export interface InfoboxLinksRow {
50
50
  readonly kind: 'links';
51
51
  readonly label: string;
@@ -53,7 +53,7 @@ export interface InfoboxLinksRow {
53
53
  readonly links: readonly InfoboxLinkEntry[] | undefined;
54
54
  }
55
55
  /**
56
- * A hero/identifying image row. Renders above the `<dl>` as an
56
+ * An `Infobox` row for the hero/identifying image. Renders above the `<dl>` as an
57
57
  * `InfoboxImage`, not as a `dt`/`dd` pair — the same shape `Infobox`'s own
58
58
  * `image` prop takes, offered as a row for the callers who build the whole
59
59
  * infobox from one `rows` array.
@@ -61,7 +61,7 @@ export interface InfoboxLinksRow {
61
61
  export interface InfoboxImageRow extends InfoboxImageSpec {
62
62
  readonly kind: 'image';
63
63
  }
64
- /** A row whose value is arbitrary markup (a badge, a formatted quantity). */
64
+ /** An `Infobox` row whose value is arbitrary markup (a badge, a formatted quantity). */
65
65
  export interface InfoboxCustomRow {
66
66
  readonly kind: 'custom';
67
67
  readonly label: string;