@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
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The library's spacing rhythm and the layout primitives' vocabulary, as
3
+ * literal class tables.
4
+ *
5
+ * `VStack`, `HStack`, `Cluster`, `Grid` and `Fits` take tokens, not class
6
+ * names, so every app spaces things on the same scale. Tailwind only
7
+ * generates classes it can see verbatim in source, so each step is spelled
8
+ * out once here and looked up (the `ui/tones.ts` discipline).
9
+ *
10
+ * @module
11
+ */
12
+ /** Literal `gap-*` class per `Gap`; a lookup table so Tailwind's scanner sees every class. */
13
+ export const gapClass = {
14
+ none: 'gap-0',
15
+ xs: 'gap-1',
16
+ sm: 'gap-2',
17
+ md: 'gap-4',
18
+ lg: 'gap-8',
19
+ xl: 'gap-12',
20
+ };
21
+ /** Literal `items-*` class per `FlowAlign`; a lookup table so Tailwind's scanner sees every class. */
22
+ export const alignClass = {
23
+ start: 'items-start',
24
+ center: 'items-center',
25
+ end: 'items-end',
26
+ stretch: 'items-stretch',
27
+ baseline: 'items-baseline',
28
+ };
29
+ /** Literal `justify-*` class per `FlowJustify`; a lookup table so Tailwind's scanner sees every class. */
30
+ export const justifyClass = {
31
+ start: 'justify-start',
32
+ center: 'justify-center',
33
+ end: 'justify-end',
34
+ between: 'justify-between',
35
+ };
36
+ /** Literal auto-fill column template per `MinItem`; a lookup table so Tailwind's scanner sees every class. */
37
+ export const minItemClass = {
38
+ xs: 'grid-cols-[repeat(auto-fill,minmax(min(10rem,100%),1fr))]',
39
+ sm: 'grid-cols-[repeat(auto-fill,minmax(min(12rem,100%),1fr))]',
40
+ md: 'grid-cols-[repeat(auto-fill,minmax(min(16rem,100%),1fr))]',
41
+ lg: 'grid-cols-[repeat(auto-fill,minmax(min(20rem,100%),1fr))]',
42
+ };
43
+ /** Literal container-query class showing a `Fits` layout from this width up. */
44
+ export const fitsShowFrom = {
45
+ '20rem': '@min-[20rem]:block',
46
+ '28rem': '@min-[28rem]:block',
47
+ '36rem': '@min-[36rem]:block',
48
+ '48rem': '@min-[48rem]:block',
49
+ '64rem': '@min-[64rem]:block',
50
+ };
51
+ /** Literal container-query class hiding a `Fits` layout from this width up. */
52
+ export const fitsHideFrom = {
53
+ '20rem': '@min-[20rem]:hidden',
54
+ '28rem': '@min-[28rem]:hidden',
55
+ '36rem': '@min-[36rem]:hidden',
56
+ '48rem': '@min-[48rem]:hidden',
57
+ '64rem': '@min-[64rem]:hidden',
58
+ };
59
+ /**
60
+ * The classes of `Fits` layout `index` of `count`, given the widths at which
61
+ * each stops fitting (`breakpoints[i]` for layout `i`, one fewer than the
62
+ * layouts). The first shows from `breakpoints[0]` up, each next one between
63
+ * its own breakpoint and the previous one, and the last below the final
64
+ * breakpoint. Outside its band a layout is `display: none`, so it is out of
65
+ * the accessibility tree.
66
+ */
67
+ export function fitsLayoutClass(index, count, breakpoints) {
68
+ const own = breakpoints[index];
69
+ const previous = index === 0 ? undefined : breakpoints[index - 1];
70
+ const isLast = index === count - 1;
71
+ return [
72
+ isLast ? 'block' : 'hidden',
73
+ !isLast && own !== undefined ? fitsShowFrom[own] : '',
74
+ previous === undefined ? '' : fitsHideFrom[previous],
75
+ ].filter((name) => name !== '');
76
+ }
@@ -9,6 +9,7 @@
9
9
  *
10
10
  * @module
11
11
  */
12
+ /** One row of the command palette: an id, a label, the group it sits in, and what activating it does. */
12
13
  export interface CommandItem {
13
14
  /** Stable id. Unique within its group; it is what bits-ui ranks and selects on. */
14
15
  id: string;
@@ -27,7 +28,9 @@ export interface CommandItem {
27
28
  }
28
29
  /** A palette section: an id rows point at, and the heading it renders. */
29
30
  export interface CommandGroup {
31
+ /** What a `CommandItem`'s `group` names. */
30
32
  id: string;
33
+ /** The section heading. */
31
34
  label: string;
32
35
  }
33
36
  /**
@@ -22,13 +22,14 @@ export declare const OG_SUBTITLE_PARAM = "d";
22
22
  /** Query param carrying the cache-busting version token. */
23
23
  export declare const OG_VERSION_PARAM = "v";
24
24
  /**
25
- * Length caps on the title / description carried in the URL. A renderer
26
- * should re-apply them defensively; here they keep the emitted URL short.
25
+ * Longest title, in characters, carried in a share-card URL. A renderer
26
+ * should re-apply this cap (and `OG_SUBTITLE_MAX_LENGTH`) defensively; here
27
+ * they keep the emitted URL short.
27
28
  */
28
29
  export declare const OG_TITLE_MAX_LENGTH = 120;
29
30
  /** Longest description, in characters, carried in a share-card URL; see `OG_TITLE_MAX_LENGTH`. */
30
31
  export declare const OG_SUBTITLE_MAX_LENGTH = 200;
31
- /** The rendered card's size — the 1.91:1 Open Graph standard. */
32
+ /** Share-card width in pixels — the 1.91:1 Open Graph standard, with `OG_IMAGE_HEIGHT`. */
32
33
  export declare const OG_IMAGE_WIDTH = 1200;
33
34
  /** Share-card height in pixels; see `OG_IMAGE_WIDTH`. */
34
35
  export declare const OG_IMAGE_HEIGHT = 630;
@@ -22,13 +22,14 @@ export const OG_SUBTITLE_PARAM = 'd';
22
22
  /** Query param carrying the cache-busting version token. */
23
23
  export const OG_VERSION_PARAM = 'v';
24
24
  /**
25
- * Length caps on the title / description carried in the URL. A renderer
26
- * should re-apply them defensively; here they keep the emitted URL short.
25
+ * Longest title, in characters, carried in a share-card URL. A renderer
26
+ * should re-apply this cap (and `OG_SUBTITLE_MAX_LENGTH`) defensively; here
27
+ * they keep the emitted URL short.
27
28
  */
28
29
  export const OG_TITLE_MAX_LENGTH = 120;
29
30
  /** Longest description, in characters, carried in a share-card URL; see `OG_TITLE_MAX_LENGTH`. */
30
31
  export const OG_SUBTITLE_MAX_LENGTH = 200;
31
- /** The rendered card's size — the 1.91:1 Open Graph standard. */
32
+ /** Share-card width in pixels — the 1.91:1 Open Graph standard, with `OG_IMAGE_HEIGHT`. */
32
33
  export const OG_IMAGE_WIDTH = 1200;
33
34
  /** Share-card height in pixels; see `OG_IMAGE_WIDTH`. */
34
35
  export const OG_IMAGE_HEIGHT = 630;
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module
9
9
  */
10
+ /** Which tips are on screen, most recent last, so Escape closes only the topmost one. */
10
11
  export interface TipStack {
11
12
  /** Mark a tip visible, moving it to the top if it was already there. */
12
13
  push: (id: string) => void;
@@ -16,7 +16,8 @@
16
16
  */
17
17
  import type { TipConfig, TipState } from './types.ts';
18
18
  /**
19
- * The subset of `Storage` this needs, so a test can pass a plain object.
19
+ * The subset of the DOM `Storage` interface a tip store reads and writes, so a
20
+ * test can pass a plain object.
20
21
  *
21
22
  * The signatures are the DOM `Storage` interface's own, `null` included: this
22
23
  * type exists so `globalThis.localStorage` satisfies it structurally.
@@ -0,0 +1,75 @@
1
+ <!-- @component
2
+ A stack of headed, collapsible sections on bits-ui `Accordion`, with single- or multi-open behaviour.
3
+ -->
4
+ <!--
5
+ Accordion — a vertical stack of `AccordionItem`s, each a real heading whose
6
+ button expands its panel. `type="single"` keeps at most one open (an FAQ, a
7
+ care guide by season); `type="multiple"` lets any number open.
8
+
9
+ Why not a stack of `Disclosure`s: `<summary>` has an implicit button role
10
+ whose children are presentational, so a heading inside it drops out of the
11
+ heading tree, and native `<details name>` groups have no "which one is
12
+ open" value and no "keep one open" rule. A single `Disclosure` stays
13
+ native; this is for the heading-structured, coordinated case.
14
+
15
+ ARIA and keyboard contract (APG Accordion): bits-ui gives each trigger
16
+ `aria-expanded`, Enter / Space to toggle, ↑ / ↓ and Home / End between
17
+ headers. We render each header as a native `<h{headingLevel}>`, wire
18
+ `aria-controls` from the button to its panel, and make each panel a
19
+ `region` labelled by its button. With `required`, the open item's button
20
+ reports `aria-disabled="true"`, so a reader isn't offered an action that
21
+ does nothing. With `searchable` (the default) a closed panel keeps
22
+ `hidden="until-found"`, so find-in-page still reaches it and opens it, the
23
+ one advantage `<details>` had.
24
+
25
+ `headingLevel`, `variant`, `joined` and `searchable` reach the items
26
+ through context (`accordion-context.ts`), so they cannot drift per item.
27
+ -->
28
+ <script lang="ts">import { Accordion } from "bits-ui";
29
+ import { setAccordionContext } from "./accordion-context.js";
30
+ let { type, value = $bindable(), required = false, headingLevel = 3, variant = "arrow", joined = true, searchable = true, disabled = false, children, class: className, ...rest } = $props();
31
+ function openValues() {
32
+ if (value === undefined || value === "") return [];
33
+ return typeof value === "string" ? [value] : value;
34
+ }
35
+ setAccordionContext({
36
+ headingLevel: () => headingLevel,
37
+ variant: () => variant,
38
+ joined: () => joined,
39
+ searchable: () => searchable,
40
+ isOpen: (item) => openValues().includes(item),
41
+ isLocked: (item) => type === "single" && required && value === item
42
+ });
43
+ const rootClass = $derived([
44
+ "w-full",
45
+ joined && "join join-vertical",
46
+ className
47
+ ]);
48
+ </script>
49
+
50
+ {#snippet root({ props }: { props: Record<string, unknown> })}
51
+ <div {...rest} {...props} class={rootClass}>{@render children()}</div>
52
+ {/snippet}
53
+
54
+ {#if type === 'single'}
55
+ <Accordion.Root
56
+ type="single"
57
+ {disabled}
58
+ bind:value={
59
+ () => (typeof value === 'string' ? value : ''),
60
+ (next) => {
61
+ // `required`: a change to '' is the open item closing; refuse it.
62
+ if (required && next === '') return;
63
+ value = next;
64
+ }
65
+ }
66
+ child={root}
67
+ />
68
+ {:else}
69
+ <Accordion.Root
70
+ type="multiple"
71
+ {disabled}
72
+ bind:value={() => [...openValues()], (next) => (value = next)}
73
+ child={root}
74
+ />
75
+ {/if}
@@ -0,0 +1,50 @@
1
+ import { Accordion } from 'bits-ui';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import type { DisclosureVariant, HeadingLevel } from './tones.ts';
5
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> & {
6
+ /**
7
+ * The level of each item's heading.
8
+ * @defaultValue `3`
9
+ */
10
+ headingLevel?: HeadingLevel | undefined;
11
+ /**
12
+ * The expand marker, shared with `Disclosure`.
13
+ * @defaultValue `'arrow'`
14
+ */
15
+ variant?: DisclosureVariant | undefined;
16
+ /**
17
+ * Join the items into one bordered block (daisyUI `join join-vertical`).
18
+ * @defaultValue `true`
19
+ */
20
+ joined?: boolean | undefined;
21
+ /**
22
+ * Keep closed panels reachable by find-in-page (`hidden="until-found"`).
23
+ * @defaultValue `true`
24
+ */
25
+ searchable?: boolean | undefined;
26
+ /** Disable every item. */
27
+ disabled?: boolean | undefined;
28
+ /** The `AccordionItem`s. */
29
+ children: Snippet<[]>;
30
+ } & ({
31
+ /** At most one item open. */
32
+ type: 'single';
33
+ /** The open item's value, `''` for none. Bindable. */
34
+ value?: string | undefined;
35
+ /**
36
+ * Refuse to close the open item, so exactly one stays open once one is.
37
+ * @defaultValue `false`
38
+ */
39
+ required?: boolean | undefined;
40
+ } | {
41
+ /** Any number of items open. */
42
+ type: 'multiple';
43
+ /** The open items' values. Bindable. */
44
+ value?: readonly string[] | undefined;
45
+ required?: undefined;
46
+ });
47
+ /** A stack of headed, collapsible sections on bits-ui `Accordion`, with single- or multi-open behaviour. */
48
+ declare const Accordion: import("svelte").Component<Props, {}, "value">;
49
+ type Accordion = ReturnType<typeof Accordion>;
50
+ export default Accordion;
@@ -0,0 +1,73 @@
1
+ <!-- @component
2
+ One headed, collapsible section of an `Accordion`.
3
+ -->
4
+ <!--
5
+ AccordionItem — a heading whose button expands a panel. Must sit inside an
6
+ `Accordion`, which supplies the heading level, the marker and the open
7
+ state (see `accordion-context.ts`).
8
+
9
+ The header is a native `<h{level}>` rendered through bits-ui's `child`
10
+ snippet, so the redundant `role="heading"` / `aria-level` bits-ui would put
11
+ on a `<div>` are dropped. The daisyUI marker is the heading's `::after`,
12
+ outside the button, so it never joins the button's accessible name.
13
+ -->
14
+ <script lang="ts">import { Accordion } from "bits-ui";
15
+ import { getAccordionContext } from "./accordion-context.js";
16
+ import { collapseVariant } from "./tones.js";
17
+ const uid = $props.id();
18
+ let { value = uid, title, disabled, contentClass, children, class: className, ...rest } = $props();
19
+ const accordion = getAccordionContext();
20
+ const triggerId = `${uid}-trigger`;
21
+ const contentId = `${uid}-content`;
22
+ const open = $derived(accordion.isOpen(value));
23
+ const locked = $derived(accordion.isLocked(value));
24
+ </script>
25
+
26
+ <Accordion.Item {value} {...disabled === undefined ? {} : { disabled }}>
27
+ {#snippet child({ props: itemProps })}
28
+ <div
29
+ {...rest}
30
+ {...itemProps}
31
+ class={[
32
+ 'collapse border-base-300 bg-base-100 border',
33
+ open ? 'collapse-open' : 'collapse-close',
34
+ collapseVariant[accordion.variant()],
35
+ accordion.joined() && 'join-item',
36
+ className,
37
+ ]}
38
+ >
39
+ <Accordion.Header level={accordion.headingLevel()}>
40
+ {#snippet child({ props })}
41
+ {@const { role: _role, 'aria-level': _level, ...headerProps } = props}
42
+ <svelte:element
43
+ this={`h${accordion.headingLevel()}`}
44
+ {...headerProps}
45
+ class="collapse-title p-0 text-base font-medium"
46
+ >
47
+ <Accordion.Trigger id={triggerId}>
48
+ {#snippet child({ props: triggerProps })}
49
+ <button
50
+ {...triggerProps}
51
+ aria-controls={contentId}
52
+ {...locked ? { 'aria-disabled': 'true' } : {}}
53
+ class="w-full cursor-pointer px-4 py-4 pe-12 text-start disabled:cursor-not-allowed disabled:opacity-60"
54
+ >
55
+ {#if typeof title === 'string'}{title}{:else}{@render title()}{/if}
56
+ </button>
57
+ {/snippet}
58
+ </Accordion.Trigger>
59
+ </svelte:element>
60
+ {/snippet}
61
+ </Accordion.Header>
62
+ <Accordion.Content
63
+ id={contentId}
64
+ role="region"
65
+ aria-labelledby={triggerId}
66
+ hiddenUntilFound={accordion.searchable()}
67
+ class={['collapse-content', contentClass]}
68
+ >
69
+ {@render children()}
70
+ </Accordion.Content>
71
+ </div>
72
+ {/snippet}
73
+ </Accordion.Item>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
4
+ /** Identifies the item in the `Accordion`'s `value`; defaults to a generated id. */
5
+ value?: string | undefined;
6
+ /** The header text, or a snippet for richer markup. */
7
+ title: string | Snippet<[]>;
8
+ /** Disable this item's button. */
9
+ disabled?: boolean | undefined;
10
+ /** Extra classes on the panel. */
11
+ contentClass?: string | undefined;
12
+ /** The panel's content. */
13
+ children: Snippet<[]>;
14
+ }
15
+ /** One headed, collapsible section of an `Accordion`. */
16
+ declare const AccordionItem: import("svelte").Component<Props, {}, "">;
17
+ type AccordionItem = ReturnType<typeof AccordionItem>;
18
+ export default AccordionItem;
@@ -16,7 +16,8 @@ The daisyUI `btn` with tone, variant and size; `loading` disables it and sets `a
16
16
  `type` defaults to `button`: a button inside a form that does not say what
17
17
  it is submits it, which is almost never what a call site means.
18
18
  -->
19
- <script lang="ts">import { buttonSize, buttonTone, buttonVariant } from "./tones.js";
19
+ <script lang="ts">import Spinner from "./Spinner.svelte";
20
+ import { buttonSize, buttonTone, buttonVariant } from "./tones.js";
20
21
  let { tone, variant = "solid", size = "md", loading = false, block = false, icon, children, type = "button", disabled = false, class: className, ...rest } = $props();
21
22
  </script>
22
23
 
@@ -35,7 +36,7 @@ let { tone, variant = "solid", size = "md", loading = false, block = false, icon
35
36
  ]}
36
37
  >
37
38
  {#if loading}
38
- <span class="loading loading-spinner loading-sm" aria-hidden="true"></span>
39
+ <Spinner decorative size="sm" />
39
40
  {:else if icon}
40
41
  {@render icon()}
41
42
  {/if}
@@ -0,0 +1,24 @@
1
+ <!-- @component
2
+ A named cluster of independent buttons in a daisyUI `join`; each stays its own Tab stop.
3
+ -->
4
+ <!--
5
+ ButtonGroup — "Save | Save as… | Discard" on a form: presentational, a
6
+ `role="group"` with a name, no keyboard behaviour by design. For a row of
7
+ controls that should be ONE Tab stop with arrow keys, use `Toolbar`.
8
+
9
+ Children join the bar by taking `class="join-item"` themselves. The
10
+ group does not reach into them through a context, so `Button` stays
11
+ free of any group coupling.
12
+ -->
13
+ <script lang="ts">let { label, orientation = "horizontal", children, class: className, ...rest } = $props();
14
+ const orientationClass = {
15
+ horizontal: "",
16
+ vertical: "join-vertical",
17
+ responsive: "join-vertical sm:join-horizontal"
18
+ };
19
+ export {};
20
+ </script>
21
+
22
+ <div {...rest} role="group" aria-label={label} class={['join', orientationClass[orientation], className]}>
23
+ {@render children()}
24
+ </div>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
4
+ /** Accessible name of the group ("Save options"). */
5
+ label: string;
6
+ /**
7
+ * `responsive` stacks below `sm`.
8
+ * @defaultValue `'horizontal'`
9
+ */
10
+ orientation?: 'horizontal' | 'vertical' | 'responsive' | undefined;
11
+ /** The buttons, each with `class="join-item"`. */
12
+ children: Snippet<[]>;
13
+ }
14
+ /** A named cluster of independent buttons in a daisyUI `join`; each stays its own Tab stop. */
15
+ declare const ButtonGroup: import("svelte").Component<Props, {}, "">;
16
+ type ButtonGroup = ReturnType<typeof ButtonGroup>;
17
+ export default ButtonGroup;
@@ -0,0 +1,75 @@
1
+ <!-- @component
2
+ A menu that opens at the pointer on a right-click (or long-press) of a region, on bits-ui's `ContextMenu`.
3
+ -->
4
+ <!--
5
+ ContextMenu — the actions for one thing (a row in a list, a node on a
6
+ canvas, an image), opened where the reader right-clicks it, long-presses it
7
+ on touch, or presses Shift+F10 / the Menu key while it has focus. Built on
8
+ bits-ui's `ContextMenu`, which owns the menu at the pointer, roving focus,
9
+ typeahead, → / ← for submenus, Escape and outside click to close, and focus
10
+ return. Its children are the same `MenuItem`, `MenuSeparator`,
11
+ `MenuCheckboxItem`, `MenuRadioGroup` / `MenuRadioItem` and `MenuSub` that a
12
+ `DropdownMenu` takes; this component lends them the bits-ui parts through
13
+ context (`menu-parts/`).
14
+
15
+ Never the only route to an action. A context menu is invisible until it is
16
+ summoned, and touch screen-reader users (and many keyboard users) never
17
+ discover it. Put every action somewhere visible too: a row's "More"
18
+ `DropdownMenu`, a toolbar.
19
+
20
+ The region is the caller's own element: the `trigger` snippet receives
21
+ `props` to spread onto it. Those props make it focusable (`tabindex="0"`,
22
+ which a natively focusable element does not mind; pass a `tabindex` of your
23
+ own after the spread to override it), because browsers fire `contextmenu`
24
+ for the keyboard only on the focused element. Shift+F10 and the Menu key
25
+ are handled here rather than left to the browser, so they open the menu at
26
+ the region's corner the same way everywhere.
27
+
28
+ ARIA contract: the content is `role="menu"` named by `label`; the daisyUI
29
+ `menu` `<ul>` and its `<li>`s are `role="none"`, exactly as in
30
+ `DropdownMenu`, so `role="menu"` owns its items. `disabled` lets the
31
+ browser's own context menu show instead.
32
+
33
+ Not part of the root barrel: it is its own entry,
34
+ `@thomasfosterau/components/context-menu`, so the menu runtime loads only on
35
+ the pages that use it.
36
+ -->
37
+ <script lang="ts">import { ContextMenu } from "bits-ui";
38
+ import { menuParts } from "./menu-parts/parts.js";
39
+ import { setMenuParts } from "./menu-parts/menu-parts.js";
40
+ let { label, open = $bindable(false), disabled = false, class: className, trigger, children } = $props();
41
+ setMenuParts(menuParts("context"));
42
+ // Shift+F10 and the Menu key: open at the region's bottom-left corner by
43
+ // dispatching the `contextmenu` event bits-ui's trigger listens for.
44
+ // `preventDefault` stops the browser from generating its own as well.
45
+ const openFromKeyboard = (event) => {
46
+ if (disabled) return;
47
+ const isMenuKey = event.key === "ContextMenu" || event.shiftKey && event.key === "F10";
48
+ if (!isMenuKey) return;
49
+ event.preventDefault();
50
+ const region = event.currentTarget;
51
+ if (!(region instanceof HTMLElement)) return;
52
+ const rect = region.getBoundingClientRect();
53
+ region.dispatchEvent(new MouseEvent("contextmenu", {
54
+ bubbles: true,
55
+ cancelable: true,
56
+ clientX: rect.left,
57
+ clientY: rect.bottom
58
+ }));
59
+ };
60
+ </script>
61
+
62
+ <ContextMenu.Root bind:open>
63
+ <ContextMenu.Trigger {disabled} tabindex={0} onkeydown={openFromKeyboard}>
64
+ {#snippet child({ props })}
65
+ {@render trigger({ props })}
66
+ {/snippet}
67
+ </ContextMenu.Trigger>
68
+ <ContextMenu.Portal>
69
+ <ContextMenu.Content aria-label={label} class="z-50">
70
+ <ul role="none" class={['menu bg-base-100 rounded-box w-56 p-2 shadow-md', className]}>
71
+ {@render children()}
72
+ </ul>
73
+ </ContextMenu.Content>
74
+ </ContextMenu.Portal>
75
+ </ContextMenu.Root>
@@ -0,0 +1,25 @@
1
+ import { ContextMenu } from 'bits-ui';
2
+ import type { Snippet } from 'svelte';
3
+ interface Props {
4
+ /** Accessible name of the open menu ("Actions for Rosa canina"). */
5
+ label: string;
6
+ /** Whether the menu is open. Bindable. */
7
+ open?: boolean | undefined;
8
+ /**
9
+ * Turn the menu off; the browser's own context menu shows instead.
10
+ * @defaultValue `false`
11
+ */
12
+ disabled?: boolean | undefined;
13
+ /** Extra classes on the `<ul class="menu">`. */
14
+ class?: string | undefined;
15
+ /** The region the menu belongs to — the caller's own element, spreading `props`. */
16
+ trigger: Snippet<[{
17
+ props: Record<string, unknown>;
18
+ }]>;
19
+ /** The menu's items. */
20
+ children: Snippet<[]>;
21
+ }
22
+ /** A menu that opens at the pointer on a right-click (or long-press) of a region, on bits-ui's `ContextMenu`. */
23
+ declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
24
+ type ContextMenu = ReturnType<typeof ContextMenu>;
25
+ export default ContextMenu;
@@ -16,6 +16,13 @@ A native `<details>` wearing daisyUI’s `collapse`.
16
16
  a second copy can only be wrong). `open` is bindable, so a caller can drive
17
17
  the state, and the four apps' "open this section because its form failed"
18
18
  case is a one-line binding.
19
+
20
+ Exclusive groups are native too: give several `Disclosure`s the same
21
+ `name` (spread onto `<details>`) and the browser keeps at most one open.
22
+ What a `Disclosure` cannot carry is a heading, because `<summary>`'s
23
+ children are presentational, so a heading inside it drops out of the
24
+ heading tree. For a stack of headed sections, or a bindable "which one is
25
+ open", use `Accordion`.
19
26
  -->
20
27
  <script lang="ts">import { collapseVariant } from "./tones.js";
21
28
  let { open = $bindable(false), title, variant = "arrow", contentClass, children, class: className, ...rest } = $props();
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { HTMLDetailsAttributes } from 'svelte/elements';
3
3
  import { type DisclosureVariant } from './tones.ts';
4
- interface Props extends Omit<HTMLAttributes<HTMLDetailsElement>, 'title'> {
4
+ interface Props extends Omit<HTMLDetailsAttributes, 'title' | 'open'> {
5
5
  /** Whether the section is expanded. Bindable. */
6
6
  open?: boolean | undefined;
7
7
  /** The summary — its own text, or a snippet for richer markup. */
@@ -0,0 +1,47 @@
1
+ <!-- @component
2
+ A horizontal or vertical rule between content, optionally labelled ("or"), in daisyUI's `divider` styles.
3
+ -->
4
+ <!--
5
+ Divider — a rule between sections or side-by-side items, optionally with a
6
+ short label on it ("or" between two sign-in methods).
7
+
8
+ Hand-rolled, not on bits-ui's `Separator` (AGENTS.md asks for the reason):
9
+ that primitive's whole contribution is `role="separator"` +
10
+ `aria-orientation`, or `role="none"` when decorative — two attributes,
11
+ written here directly, with no bits-ui on the always-loaded path.
12
+
13
+ ARIA contract:
14
+ - Unlabelled: `role="separator"`; vertical adds
15
+ `aria-orientation="vertical"` (horizontal is the implicit default). It
16
+ is a `<div>`, not a native `<hr>`, because daisyUI draws the rule with
17
+ `::before` / `::after`, which a void `<hr>` cannot render.
18
+ - Labelled: NOT a separator. A non-focusable separator's children are
19
+ presentational, so the label would be lost to assistive technology. The
20
+ wrapper is `role="none"` and the text reads in flow — "Sign in with a
21
+ passkey / or / Continue with email" (WAI-ARIA 1.2, separator).
22
+ - `decorative`: `aria-hidden="true"`, nothing announced.
23
+
24
+ Not `MenuSeparator`, which must be an `<li>` inside a menu and defers to
25
+ bits-ui's menu separator for roving focus; use that one inside `Menu`.
26
+ -->
27
+ <script lang="ts">import { dividerAlign, dividerOrientation, dividerTone } from "./tones.js";
28
+ let { orientation = "horizontal", decorative = false, align = "center", tone, children, class: className, ...rest } = $props();
29
+ const classes = $derived([
30
+ "divider",
31
+ dividerOrientation[orientation],
32
+ dividerAlign[align],
33
+ tone !== undefined && dividerTone[tone],
34
+ className
35
+ ]);
36
+ const role = $derived(decorative || children !== undefined ? "none" : "separator");
37
+ </script>
38
+
39
+ <div
40
+ {...rest}
41
+ {role}
42
+ aria-orientation={role === 'separator' && orientation === 'vertical' ? 'vertical' : undefined}
43
+ aria-hidden={decorative ? 'true' : undefined}
44
+ class={classes}
45
+ >
46
+ {#if children}<span>{@render children()}</span>{/if}
47
+ </div>
@@ -0,0 +1,28 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type DividerAlign, type Tone } from './tones.ts';
4
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
5
+ /**
6
+ * Which way the rule runs: `vertical` sits between side-by-side items.
7
+ * @defaultValue `'horizontal'`
8
+ */
9
+ orientation?: 'horizontal' | 'vertical' | undefined;
10
+ /**
11
+ * The rule marks no real boundary: hide it from assistive technology.
12
+ * @defaultValue `false`
13
+ */
14
+ decorative?: boolean | undefined;
15
+ /**
16
+ * Where a label sits along the rule.
17
+ * @defaultValue `'center'`
18
+ */
19
+ align?: DividerAlign | undefined;
20
+ /** Semantic colour of the rule. */
21
+ tone?: Tone | undefined;
22
+ /** Short text on the rule ("or"). */
23
+ children?: Snippet<[]> | undefined;
24
+ }
25
+ /** A horizontal or vertical rule between content, optionally labelled ("or"), in daisyUI's `divider` styles. */
26
+ declare const Divider: import("svelte").Component<Props, {}, "">;
27
+ type Divider = ReturnType<typeof Divider>;
28
+ export default Divider;