@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
@@ -13,12 +13,29 @@ A pan-and-zoom SVG surface over a `VectorDocument`, with each element drawn by a
13
13
  ## ARIA contract
14
14
 
15
15
  The `<svg>` is `role="application"` named by `label` (default "Vector editing
16
- canvas"). `application` is the honest role for a surface whose whole input
17
- vocabulary is pointer gestures the page interprets — it tells a screen reader
18
- to pass keys through rather than to read the SVG as a picture. It is also a
19
- promise this component alone cannot keep: an application that makes a canvas
20
- its only way to do something owes the reader a second, non-pointer path to the
21
- same edit, and that path is the page's, not this component's.
16
+ canvas"). `application` is the honest role for a surface that interprets its
17
+ own input — it tells a screen reader to pass keys through rather than to read
18
+ the SVG as a picture — and it is a promise that the surface HANDLES those
19
+ keys, which § Keys keeps.
20
+
21
+ ## Keys
22
+
23
+ When `interactive`, the `<svg>` is one Tab stop, and every keydown and keyup
24
+ on it goes to `onkey` FIRST. What a key means to the elements is the
25
+ application's, for the same reason a pointer gesture is. Only a key the
26
+ application left alone (did not `preventDefault`), and not a Ctrl, Alt or
27
+ Meta chord, falls through to the two bindings this component owns, because
28
+ it owns the viewport:
29
+
30
+ | Key | Does |
31
+ | ----------------------- | -------------------------------------------- |
32
+ | Arrow keys (`pan` tool) | pan by 40 screen pixels; Shift for 160 |
33
+ | `+` / `=`, `-` | zoom in / out about the centre of the view |
34
+
35
+ The application says which of its elements is focused through
36
+ `activedescendant` (an element id inside the surface), so DOM focus stays on
37
+ the `<svg>` and the arrow keys never scroll the page out from under the
38
+ reader.
22
39
 
23
40
  ## Two decisions worth stating
24
41
 
@@ -38,7 +55,7 @@ A pan-and-zoom SVG surface over a `VectorDocument`, with each element drawn by a
38
55
  pointer geometry.
39
56
  -->
40
57
  <script lang="ts" generics="T extends CanvasElement">import { panBy, screenToCanvas, snapToGrid, sortByZIndex, zoomAbout } from "./utils.js";
41
- let { doc, selected = new Set(), tool = "select", width = 800, height = 600, element, interactive = true, label = "Vector editing canvas", onviewportchange, oninteract, class: className, ...rest } = $props();
58
+ let { doc, selected = new Set(), tool = "select", width = 800, height = 600, element, interactive = true, label = "Vector editing canvas", activedescendant = "", describedby = "", overlay, onkey, onfocuschange, onviewportchange, oninteract, class: className, ...rest } = $props();
42
59
  // Scoped to this instance: two canvases on one page would otherwise share a
43
60
  // `<pattern id>`, and the second would paint the first's grid.
44
61
  const uid = $props.id();
@@ -168,6 +185,57 @@ const wheel = (event) => {
168
185
  };
169
186
  onviewportchange?.(zoomAbout(doc.viewport, pointer, event.deltaY > 0 ? .9 : 1.1));
170
187
  };
188
+ /** Screen pixels one arrow press pans by with the `pan` tool; Shift multiplies it. */
189
+ const PAN_STEP = 40;
190
+ const COARSE_PAN = 4;
191
+ /** Which way the view moves for each arrow: right reveals more of the right. */
192
+ const PAN_DIRECTIONS = new Map([
193
+ ["ArrowLeft", {
194
+ x: 1,
195
+ y: 0
196
+ }],
197
+ ["ArrowRight", {
198
+ x: -1,
199
+ y: 0
200
+ }],
201
+ ["ArrowUp", {
202
+ x: 0,
203
+ y: 1
204
+ }],
205
+ ["ArrowDown", {
206
+ x: 0,
207
+ y: -1
208
+ }]
209
+ ]);
210
+ /** The zoom factor per key; `=` is `+` without the Shift. */
211
+ const ZOOM_KEYS = new Map([
212
+ ["+", 1.1],
213
+ ["=", 1.1],
214
+ ["-", .9]
215
+ ]);
216
+ /** The view's own keys, for whatever the application did not claim. */
217
+ const viewKey = (event) => {
218
+ const factor = ZOOM_KEYS.get(event.key);
219
+ if (factor !== undefined) {
220
+ onviewportchange?.(zoomAbout(doc.viewport, {
221
+ x: width / 2,
222
+ y: height / 2
223
+ }, factor));
224
+ return true;
225
+ }
226
+ const direction = PAN_DIRECTIONS.get(event.key);
227
+ if (tool !== "pan" || direction === undefined) return false;
228
+ const step = PAN_STEP * (event.shiftKey ? COARSE_PAN : 1);
229
+ onviewportchange?.(panBy(doc.viewport, direction.x * step, direction.y * step));
230
+ return true;
231
+ };
232
+ const key = (event) => {
233
+ if (!interactive) return;
234
+ onkey?.(event);
235
+ if (event.type !== "keydown" || event.defaultPrevented) return;
236
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
237
+ if (viewKey(event)) event.preventDefault();
238
+ };
171
239
  </script>
172
240
 
173
241
  <div
@@ -176,6 +244,10 @@ const wheel = (event) => {
176
244
  style:height="{height}px"
177
245
  style:touch-action="none"
178
246
  >
247
+ <!-- Svelte's a11y table files `application` under the non-interactive roles;
248
+ ARIA defines it as the role that takes focus and handles its own keys,
249
+ which is exactly what the tabindex and the key handlers are for. -->
250
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
179
251
  <svg
180
252
  {...rest}
181
253
  bind:this={surface}
@@ -183,7 +255,14 @@ const wheel = (event) => {
183
255
  {height}
184
256
  role="application"
185
257
  aria-label={label}
186
- class="cursor-crosshair"
258
+ aria-activedescendant={activedescendant === '' ? undefined : activedescendant}
259
+ aria-describedby={describedby === '' ? undefined : describedby}
260
+ tabindex={interactive ? 0 : undefined}
261
+ class="focus-visible:outline-primary cursor-crosshair focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2"
262
+ onkeydown={key}
263
+ onkeyup={key}
264
+ onfocus={() => onfocuschange?.(true)}
265
+ onblur={() => onfocuschange?.(false)}
187
266
  onpointerdown={down}
188
267
  onpointermove={move}
189
268
  onpointerup={up}
@@ -234,6 +313,8 @@ const wheel = (event) => {
234
313
  {/if}
235
314
  </g>
236
315
  {/each}
316
+
317
+ {@render overlay?.()}
237
318
  </g>
238
319
  </svg>
239
320
  </div>
@@ -2,7 +2,7 @@ import type { Snippet } from 'svelte';
2
2
  import type { SVGAttributes } from 'svelte/elements';
3
3
  import type { CanvasElement, Point, VectorDocument, Viewport } from './types.ts';
4
4
  declare function $$render<T extends CanvasElement>(): {
5
- props: Omit<SVGAttributes<SVGSVGElement>, "class" | "height" | "width"> & {
5
+ props: Omit<SVGAttributes<SVGSVGElement>, "class" | "onfocus" | "onblur" | "onkeydown" | "onkeyup" | "height" | "width"> & {
6
6
  /** The document to draw: its elements, viewport and grid. */
7
7
  doc: VectorDocument<T>;
8
8
  /**
@@ -37,6 +37,24 @@ declare function $$render<T extends CanvasElement>(): {
37
37
  * @defaultValue `'Vector editing canvas'`
38
38
  */
39
39
  label?: string | undefined;
40
+ /**
41
+ * The id of the element that has keyboard focus inside the surface, for
42
+ * `aria-activedescendant`; `''` for none.
43
+ * @defaultValue `''`
44
+ */
45
+ activedescendant?: string | undefined;
46
+ /**
47
+ * The id of an element describing the surface's keys, for
48
+ * `aria-describedby`; `''` for none.
49
+ * @defaultValue `''`
50
+ */
51
+ describedby?: string | undefined;
52
+ /** Drawn above every element, in canvas coordinates: a cursor, a guide. */
53
+ overlay?: Snippet<[]> | undefined;
54
+ /** Every keydown and keyup on the surface, before its own keys; `preventDefault` claims the key. */
55
+ onkey?: ((event: KeyboardEvent) => void) | undefined;
56
+ /** Whether the surface has keyboard focus, each time that changes. */
57
+ onfocuschange?: ((focused: boolean) => void) | undefined;
40
58
  /**
41
59
  * The viewport after a pan or a zoom.
42
60
  *
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * @module @thomasfosterau/components/canvas/types
12
12
  */
13
- /** Stable identity for an element. */
13
+ /** Stable identity for a canvas element. */
14
14
  export type ElementId = string;
15
15
  /** A point in canvas coordinates — the document's own space, not the screen's. */
16
16
  export interface Point {
@@ -24,5 +24,5 @@ export interface ChartSeries {
24
24
  */
25
25
  readonly points: readonly ChartPoint[];
26
26
  }
27
- /** How the magnitude is drawn. */
27
+ /** How a `Chart` draws its values: as a line, a filled area, or bars. */
28
28
  export type ChartKind = 'line' | 'area' | 'bar';
@@ -18,7 +18,9 @@
18
18
  * the index it was asked to format.
19
19
  */
20
20
  export const formatListParts = (items, options = {}) => {
21
- const format = new Intl.ListFormat(options.locales, {
21
+ // A mutable copy: TS6's `Intl.ListFormat` (svelte-check) takes no readonly array.
22
+ const { locales } = options;
23
+ const format = new Intl.ListFormat(typeof locales === 'object' ? [...locales] : locales, {
22
24
  style: options.style ?? 'long',
23
25
  type: options.type ?? 'conjunction',
24
26
  });
@@ -0,0 +1,48 @@
1
+ <!-- @component
2
+ One message of a `MessageThread`: author, time, avatar, the body and a status line, as a daisyUI `chat` bubble.
3
+ -->
4
+ <!--
5
+ ChatBubble — an `<li>` holding an `<article>` named by its header, so a
6
+ reader moves message to message and hears "Ada Lovelace, 3 minutes ago"
7
+ before the body. The time is a real `<time datetime>` (`DateTime`, or
8
+ `RelativeTime` with the absolute instant in `title`). The avatar is
9
+ decorative (`aria-hidden`): the author's name is already text.
10
+
11
+ `side` is visual only: messages from the viewer sit at the end, and the
12
+ author string ("You") is what says whose they are, never the position or
13
+ the colour. The `<article>` is the daisyUI `chat` grid itself, so its
14
+ image / header / bubble / footer are its direct children, as
15
+ `chat.css` requires.
16
+ -->
17
+ <script lang="ts">import DateTime from "../formatting/DateTime.svelte";
18
+ import RelativeTime from "../formatting/RelativeTime.svelte";
19
+ import { chatBubbleTone } from "../ui/tones.js";
20
+ let { author, sentAt, side = "start", tone, relative = false, locales, avatar, footer, children, class: className, ...rest } = $props();
21
+ const uid = $props.id();
22
+ const headerId = `${uid}-header`;
23
+ const sideClass = {
24
+ start: "chat-start",
25
+ end: "chat-end"
26
+ };
27
+ </script>
28
+
29
+ <li>
30
+ <article {...rest} aria-labelledby={headerId} class={['chat', sideClass[side], className]}>
31
+ {#if avatar}
32
+ <div class="chat-image avatar" aria-hidden="true">{@render avatar()}</div>
33
+ {/if}
34
+ <div class="chat-header" id={headerId}>
35
+ {author}{#if sentAt !== undefined}<span class="sr-only">,</span>
36
+ {#if relative}
37
+ <RelativeTime value={sentAt} {locales} class="text-xs opacity-60" />
38
+ {:else}
39
+ <DateTime value={sentAt} {locales} dateStyle="medium" timeStyle="short" class="text-xs opacity-60" />
40
+ {/if}
41
+ {/if}
42
+ </div>
43
+ <div class={['chat-bubble', tone !== undefined && chatBubbleTone[tone]]}>{@render children()}</div>
44
+ {#if footer}
45
+ <div class="chat-footer opacity-60">{@render footer()}</div>
46
+ {/if}
47
+ </article>
48
+ </li>
@@ -0,0 +1,33 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../ui/tones.ts';
4
+ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
5
+ /** Display name of the author; part of the message's accessible name. */
6
+ author: string;
7
+ /** When it was sent: a `Date`, epoch milliseconds or an ISO string. */
8
+ sentAt?: Date | number | string | undefined;
9
+ /**
10
+ * `'end'` for the viewer's own messages.
11
+ * @defaultValue `'start'`
12
+ */
13
+ side?: 'start' | 'end' | undefined;
14
+ /** Bubble colour. Omitted, daisyUI's neutral bubble. */
15
+ tone?: Tone | undefined;
16
+ /**
17
+ * Show `sentAt` relative ("3 minutes ago") rather than absolute.
18
+ * @defaultValue `false`
19
+ */
20
+ relative?: boolean | undefined;
21
+ /** Locale(s) for the time; falls back to the components context. */
22
+ locales?: string | readonly string[] | undefined;
23
+ /** The avatar (an `Avatar` or an image); decorative here. */
24
+ avatar?: Snippet<[]> | undefined;
25
+ /** Status line under the bubble ("Delivered", "Edited"). */
26
+ footer?: Snippet<[]> | undefined;
27
+ /** The message body. */
28
+ children: Snippet<[]>;
29
+ }
30
+ /** One message of a `MessageThread`: author, time, avatar, the body and a status line, as a daisyUI `chat` bubble. */
31
+ declare const ChatBubble: import("svelte").Component<Props, {}, "">;
32
+ type ChatBubble = ReturnType<typeof ChatBubble>;
33
+ export default ChatBubble;
@@ -1,10 +1,14 @@
1
1
  <!-- @component
2
- Label/value pairs as a `grid`, a bordered `card`, `divided` rows or `inline` pairs.
2
+ Label/value pairs as a `grid`, a bordered `card`, `divided` rows, `inline` pairs, or `bare` pairs in a layout the caller supplies.
3
3
  -->
4
4
  <!--
5
5
  DescriptionList — label / value pairs, in the four shapes a detail page
6
6
  wants them: a two-column `grid`, a bordered `card` with the labels in a
7
- fixed gutter, `divided` rows, or `inline` pairs running across a line.
7
+ fixed gutter, `divided` rows, or `inline` pairs running across a line —
8
+ or `bare`, which adds no layout of its own: the `<dl>` takes only the
9
+ caller's `class` (say `grid grid-cols-[auto_1fr] gap-x-4`) and every
10
+ row's `<dt>`/`<dd>` is a direct child of it, so a consumer-owned grid can
11
+ mix `DescriptionRow`s with its own `<dt>`/`<dd>` pairs and keep its columns.
8
12
 
9
13
  One component rather than the two the apps had. They had split it by how the
10
14
  VALUE arrives — one took rows as strings, the other took them as markup —
@@ -30,7 +34,9 @@ const listClass = {
30
34
  grid: "grid grid-cols-1 gap-3 sm:grid-cols-2",
31
35
  card: "card card-border card-body bg-base-100 grid grid-cols-1 gap-x-6 gap-y-3 sm:grid-cols-[12rem_1fr]",
32
36
  divided: "divide-base-300 divide-y",
33
- inline: "flex flex-wrap gap-x-6 gap-y-2"
37
+ inline: "flex flex-wrap gap-x-6 gap-y-2",
38
+ // The caller's `class` is the whole layout.
39
+ bare: ""
34
40
  };
35
41
  // A getter, not a snapshot: the rows re-read it, so a list whose `variant`
36
42
  // changes re-styles the rows it already mounted.
@@ -20,7 +20,7 @@ interface Props extends HTMLAttributes<HTMLElement> {
20
20
  /** The rows — `DescriptionRow`s, or any other `<dt>`/`<dd>` pairs. */
21
21
  children: Snippet<[]>;
22
22
  }
23
- /** Label/value pairs as a `grid`, a bordered `card`, `divided` rows or `inline` pairs. */
23
+ /** Label/value pairs as a `grid`, a bordered `card`, `divided` rows, `inline` pairs, or `bare` pairs in a layout the caller supplies. */
24
24
  declare const DescriptionList: import("svelte").Component<Props, {}, "">;
25
25
  type DescriptionList = ReturnType<typeof DescriptionList>;
26
26
  export default DescriptionList;
@@ -12,10 +12,11 @@ One label and its value inside a `DescriptionList`.
12
12
  Both settings come from the enclosing list by default and can be overridden
13
13
  per row.
14
14
 
15
- The `card` variant's `<dt>` and `<dd>` are siblings of every other row's,
16
- directly in the list's two-column grid: a wrapper would break the alignment
17
- the grid exists for. Every other variant wraps the pair, so the two stay
18
- together when the rows wrap.
15
+ The `card` and `bare` variants' `<dt>` and `<dd>` are siblings of every
16
+ other row's, directly in the `<dl>` (the list's own two-column grid for
17
+ `card`, whatever layout the caller gave it for `bare`): a wrapper would
18
+ break the alignment the grid exists for. Every other variant wraps the
19
+ pair, so the two stay together when the rows wrap.
19
20
 
20
21
  ARIA contract: the `<dt>`/`<dd>` pairing is the whole contract — nothing is
21
22
  added to it, and nothing needs to be.
@@ -43,7 +44,7 @@ const ddClass = $derived([
43
44
  {/snippet}
44
45
 
45
46
  {#if visible}
46
- {#if style().variant === 'card'}
47
+ {#if style().variant === 'card' || style().variant === 'bare'}
47
48
  {@render pair()}
48
49
  {:else if style().variant === 'inline'}
49
50
  <div class="flex items-baseline gap-2">{@render pair()}</div>
@@ -0,0 +1,29 @@
1
+ <!-- @component
2
+ A conversation: a named region holding an ordered list of `ChatBubble`s.
3
+ -->
4
+ <!--
5
+ MessageThread — comments on a planting, a support thread, a note from the
6
+ garden coordinator. A `<section aria-label>` (a `region` a reader can jump
7
+ to) around an `<ol>`, because order is the content. Its children are
8
+ `ChatBubble`s, each an `<li>`.
9
+
10
+ `live` is for a thread that grows while it is read (live chat): the
11
+ section becomes `role="log"`, which announces appended messages politely.
12
+ Off by default, because a static comment history must not read itself
13
+ out as it renders.
14
+ -->
15
+ <script lang="ts">let { label, live = false, children, class: className, ...rest } = $props();
16
+ export {};
17
+ </script>
18
+
19
+ <section
20
+ {...rest}
21
+ aria-label={label}
22
+ role={live ? 'log' : undefined}
23
+ aria-live={live ? 'polite' : undefined}
24
+ class={className}
25
+ >
26
+ <ol class="flex list-none flex-col gap-1 p-0">
27
+ {@render children()}
28
+ </ol>
29
+ </section>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
4
+ /** Accessible name, e.g. `'Comments on Bed 4'`. */
5
+ label: string;
6
+ /**
7
+ * Announce new messages as they arrive (`role="log"`).
8
+ * @defaultValue `false`
9
+ */
10
+ live?: boolean | undefined;
11
+ /** The `ChatBubble`s, oldest first. */
12
+ children: Snippet<[]>;
13
+ }
14
+ /** A conversation: a named region holding an ordered list of `ChatBubble`s. */
15
+ declare const MessageThread: import("svelte").Component<Props, {}, "">;
16
+ type MessageThread = ReturnType<typeof MessageThread>;
17
+ export default MessageThread;
@@ -33,7 +33,7 @@ Offset-based page navigation: numbered links with gaps or a bare Previous/Next p
33
33
  import { getComponentsContext } from "../context.svelte.js";
34
34
  import { buttonSize } from "../ui/tones.js";
35
35
  import { pageWindow } from "./pagination.js";
36
- let { offset, limit, total, variant = "window", href, onPage, siblings = 1, label = "Pagination", previousLabel = "Previous", nextLabel = "Next", pageLabel = (page) => `Page ${page}`, size = "sm", showRange = false, rangeLabel, statusLabel, locales = getComponentsContext().locale(), class: className, ...rest } = $props();
36
+ let { offset, limit, total, variant = "window", href, onPage, siblings = 1, label = "Pagination", previousLabel = "Previous", nextLabel = "Next", pageLabel = (page) => `Page ${page}`, formatPage = (page) => String(page), size = "sm", showRange = false, rangeLabel, statusLabel, locales = getComponentsContext().locale(), class: className, ...rest } = $props();
37
37
  const context = getComponentsContext();
38
38
  const view = $derived(pageWindow({
39
39
  offset,
@@ -42,7 +42,7 @@ const view = $derived(pageWindow({
42
42
  // The formatter and the formatted string are separate `$derived`s so the
43
43
  // (relatively expensive) `Intl.NumberFormat` is not rebuilt when only the
44
44
  // offset moves. Counts are quantities and get grouped; the page NUMBERS
45
- // below are identifiers and stay bare.
45
+ // below are identifiers and stay bare unless `formatPage` says otherwise.
46
46
  const counts = $derived(new Intl.NumberFormat(locales));
47
47
  const rangeText = $derived(rangeLabel === undefined ? `Showing ${counts.format(view.from)}–${counts.format(view.to)} of ${counts.format(total)}` : rangeLabel(view.from, view.to, total));
48
48
  /** A target offset's href, resolved through the context. */
@@ -104,11 +104,11 @@ const inertClass = $derived(["btn btn-disabled join-item", buttonSize[size]]);
104
104
  {:else if item.current}
105
105
  <span class={['btn btn-active join-item', buttonSize[size]]} aria-current="page">
106
106
  <span class="sr-only">{pageLabel(item.page)}</span>
107
- <span aria-hidden="true">{item.page}</span>
107
+ <span aria-hidden="true">{formatPage(item.page)}</span>
108
108
  </span>
109
109
  {:else if href !== undefined}
110
110
  <a href={linkTo(item.offset)} class={itemClass} aria-label={pageLabel(item.page)}>
111
- <span aria-hidden="true">{item.page}</span>
111
+ <span aria-hidden="true">{formatPage(item.page)}</span>
112
112
  </a>
113
113
  {:else}
114
114
  <button
@@ -119,7 +119,7 @@ const inertClass = $derived(["btn btn-disabled join-item", buttonSize[size]]);
119
119
  onPage?.(item.offset);
120
120
  }}
121
121
  >
122
- <span aria-hidden="true">{item.page}</span>
122
+ <span aria-hidden="true">{formatPage(item.page)}</span>
123
123
  </button>
124
124
  {/if}
125
125
  </li>
@@ -45,6 +45,14 @@ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
45
45
  * @defaultValue `"Page <page>"`
46
46
  */
47
47
  pageLabel?: ((page: number) => string) | undefined;
48
+ /**
49
+ * How a page number is written on its control — the visible digits only;
50
+ * `pageLabel` is the accessible name. Page numbers are identifiers, so they
51
+ * are bare by default; pass e.g. `(page) => new Intl.NumberFormat('en-AU').format(page)`
52
+ * to group them ("1,000").
53
+ * @defaultValue `String(page)`
54
+ */
55
+ formatPage?: ((page: number) => string) | undefined;
48
56
  /**
49
57
  * daisyUI size step.
50
58
  * @defaultValue `'sm'`
@@ -18,7 +18,7 @@ The count line and the pager that sit under a paginated index page.
18
18
  -->
19
19
  <script lang="ts">import Pagination from "./Pagination.svelte";
20
20
  import ResultCount from "./ResultCount.svelte";
21
- let { shown, offset, limit, total, href, onPage, variant = "window", siblings = 1, size = "sm", label = "Pagination", previousLabel = "Previous", nextLabel = "Next", pageLabel, countLabel, locales, class: className, ...rest } = $props();
21
+ let { shown, offset, limit, total, href, onPage, variant = "window", siblings = 1, size = "sm", label = "Pagination", previousLabel = "Previous", nextLabel = "Next", pageLabel, formatPage, countLabel, locales, class: className, ...rest } = $props();
22
22
  </script>
23
23
 
24
24
  <div {...rest} class={['space-y-4', className]}>
@@ -36,6 +36,7 @@ let { shown, offset, limit, total, href, onPage, variant = "window", siblings =
36
36
  {previousLabel}
37
37
  {nextLabel}
38
38
  {pageLabel}
39
+ {formatPage}
39
40
  {locales}
40
41
  />
41
42
  </div>
@@ -45,6 +45,8 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
45
45
  nextLabel?: string | undefined;
46
46
  /** How a numbered control is named; see `Pagination`. */
47
47
  pageLabel?: ((page: number) => string) | undefined;
48
+ /** Passed to `Pagination`: how a page number's visible digits are written. */
49
+ formatPage?: ((page: number) => string) | undefined;
48
50
  /** The count line, when "Showing x of y" is not the sentence wanted. */
49
51
  countLabel?: ((shown: number, total: number) => string) | undefined;
50
52
  /**
@@ -76,6 +76,10 @@ const rowAttributes = (row) => onSelect === undefined ? {} : {
76
76
  };
77
77
  </script>
78
78
 
79
+ {#snippet cellBody(column: TableColumn<T>, row: T)}
80
+ {#if column.cell}{@render column.cell(row)}{:else}{column.value(row)}{/if}
81
+ {/snippet}
82
+
79
83
  <div class={['overflow-x-auto', containerClass]}>
80
84
  <table
81
85
  {...rest}
@@ -128,9 +132,11 @@ const rowAttributes = (row) => onSelect === undefined ? {} : {
128
132
  {#each rows as row (rowKey(row))}
129
133
  <tr class="hover:bg-base-200" {...rowAttributes(row)}>
130
134
  {#each columns as column (column.key)}
131
- <td class={columnClass(column)}>
132
- {#if column.cell}{@render column.cell(row)}{:else}{column.value(row)}{/if}
133
- </td>
135
+ {#if column.rowHeader}
136
+ <th scope="row" class={['font-normal', columnClass(column)]}>{@render cellBody(column, row)}</th>
137
+ {:else}
138
+ <td class={columnClass(column)}>{@render cellBody(column, row)}</td>
139
+ {/if}
134
140
  {/each}
135
141
  </tr>
136
142
  {/each}
@@ -2,11 +2,12 @@
2
2
  * How a {@link DescriptionRow} learns which {@link DescriptionList} it is
3
3
  * inside.
4
4
  *
5
- * The four list variants render their rows differently — a `grid` row is a
6
- * wrapper holding a stacked `<dt>`/`<dd>` pair, while a `card` row's `<dt>`
7
- * and `<dd>` are siblings in the list's own two-column grid, because a
8
- * wrapper would break the alignment the grid exists for. A call site writes
9
- * the same `<DescriptionRow>` either way.
5
+ * The list variants render their rows differently — a `grid` row is a
6
+ * wrapper holding a stacked `<dt>`/`<dd>` pair, while a `card` or `bare`
7
+ * row's `<dt>` and `<dd>` are direct children of the `<dl>`, because a
8
+ * wrapper would break the alignment of a two-column grid (the list's own for
9
+ * `card`, the consumer's for `bare`). A call site writes the same
10
+ * `<DescriptionRow>` either way.
10
11
  *
11
12
  * The variant travels down as context rather than as a prop so a caller
12
13
  * cannot get a list and its rows out of step, and so the list stays the
@@ -17,7 +18,12 @@
17
18
  */
18
19
  /** The presentation a `DescriptionList` asks its rows for. */
19
20
  export interface DescriptionListStyle {
20
- readonly variant: 'grid' | 'card' | 'divided' | 'inline';
21
+ /**
22
+ * The layout. `bare` adds no layout classes to the list and renders each
23
+ * row's `<dt>`/`<dd>` as direct children of the `<dl>`, for a consumer that
24
+ * lays the list out itself.
25
+ */
26
+ readonly variant: 'grid' | 'card' | 'divided' | 'inline' | 'bare';
21
27
  /** Render values in a fixed-width face that breaks anywhere. */
22
28
  readonly mono: boolean;
23
29
  /** Render a row whose value is empty, using `emptyLabel`. */
@@ -2,11 +2,12 @@
2
2
  * How a {@link DescriptionRow} learns which {@link DescriptionList} it is
3
3
  * inside.
4
4
  *
5
- * The four list variants render their rows differently — a `grid` row is a
6
- * wrapper holding a stacked `<dt>`/`<dd>` pair, while a `card` row's `<dt>`
7
- * and `<dd>` are siblings in the list's own two-column grid, because a
8
- * wrapper would break the alignment the grid exists for. A call site writes
9
- * the same `<DescriptionRow>` either way.
5
+ * The list variants render their rows differently — a `grid` row is a
6
+ * wrapper holding a stacked `<dt>`/`<dd>` pair, while a `card` or `bare`
7
+ * row's `<dt>` and `<dd>` are direct children of the `<dl>`, because a
8
+ * wrapper would break the alignment of a two-column grid (the list's own for
9
+ * `card`, the consumer's for `bare`). A call site writes the same
10
+ * `<DescriptionRow>` either way.
10
11
  *
11
12
  * The variant travels down as context rather than as a prop so a caller
12
13
  * cannot get a list and its rows out of step, and so the list stays the
@@ -16,12 +16,14 @@
16
16
  *
17
17
  * @module
18
18
  */
19
+ export { default as ChatBubble } from './ChatBubble.svelte';
19
20
  export { default as DataGrid } from './DataGrid.svelte';
20
21
  export { default as DescriptionList } from './DescriptionList.svelte';
21
22
  export { default as DescriptionRow } from './DescriptionRow.svelte';
22
23
  export { default as DiffText } from './DiffText.svelte';
23
24
  export { default as ItemList } from './ItemList.svelte';
24
25
  export { default as LinkListItem } from './LinkListItem.svelte';
26
+ export { default as MessageThread } from './MessageThread.svelte';
25
27
  export { default as Pagination } from './Pagination.svelte';
26
28
  export { default as PaginationFooter } from './PaginationFooter.svelte';
27
29
  export { default as ResultCount } from './ResultCount.svelte';
@@ -16,12 +16,14 @@
16
16
  *
17
17
  * @module
18
18
  */
19
+ export { default as ChatBubble } from './ChatBubble.svelte';
19
20
  export { default as DataGrid } from './DataGrid.svelte';
20
21
  export { default as DescriptionList } from './DescriptionList.svelte';
21
22
  export { default as DescriptionRow } from './DescriptionRow.svelte';
22
23
  export { default as DiffText } from './DiffText.svelte';
23
24
  export { default as ItemList } from './ItemList.svelte';
24
25
  export { default as LinkListItem } from './LinkListItem.svelte';
26
+ export { default as MessageThread } from './MessageThread.svelte';
25
27
  export { default as Pagination } from './Pagination.svelte';
26
28
  export { default as PaginationFooter } from './PaginationFooter.svelte';
27
29
  export { default as ResultCount } from './ResultCount.svelte';
@@ -34,6 +34,13 @@ export interface TableColumn<T> {
34
34
  readonly cell?: Snippet<[T]> | undefined;
35
35
  /** Whether the header offers a sort control on this column. */
36
36
  readonly sortable?: boolean | undefined;
37
+ /**
38
+ * Render this column's body cells as `<th scope="row">` — the row's header,
39
+ * so a screen reader moving down another column announces it with each cell
40
+ * ("Jane Smith, 48,000"). Give it to the column that names the row, usually
41
+ * one per table.
42
+ */
43
+ readonly rowHeader?: boolean | undefined;
37
44
  /** Right-align numeric columns. */
38
45
  readonly align?: 'start' | 'end' | undefined;
39
46
  /** Extra classes on this column's header and body cells. */