@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,86 @@
1
+ <!-- @component
2
+ The header, month grids and day cells `Calendar` and `RangeCalendar` share; internal, not exported.
3
+ -->
4
+ <!--
5
+ CalendarMonths — the body of a bits-ui calendar, written once for both
6
+ roots. `parts` is the bits-ui namespace in use (`Calendar` or
7
+ `RangeCalendar`); both expose the same sub-components with the same props,
8
+ so one template serves both, and the day styling — the part a reader sees
9
+ — cannot drift between the single-date and the range calendar.
10
+
11
+ Each month `<table role="grid">` is named by its own month and year
12
+ ("October 2026"), so a two-month calendar's grids are told apart. The
13
+ visible heading is bits-ui's, `aria-hidden` because the month is already
14
+ in the `application`'s name, which bits-ui updates as the reader pages. An unavailable day is struck through as well as
15
+ `aria-disabled`: colour is never the only signal.
16
+ -->
17
+ <script lang="ts">import { DateFormatter, getLocalTimeZone } from "@internationalized/date";
18
+ import { ChevronLeft, ChevronRight } from "@lucide/svelte";
19
+ let { parts, months, weekdays, locale, previousLabel, nextLabel, day } = $props();
20
+ const P = $derived(parts);
21
+ const monthName = $derived(new DateFormatter(locale, {
22
+ month: "long",
23
+ year: "numeric"
24
+ }));
25
+ const nameOf = (month) => monthName.format(month.value.toDate(getLocalTimeZone()));
26
+ // One literal class list (Tailwind scans it as written). Range-middle wins
27
+ // over selected, so a range reads as one band with two solid ends.
28
+ const dayClass = [
29
+ "btn btn-ghost btn-sm btn-square font-normal",
30
+ "data-today:ring-1 data-today:ring-primary data-today:ring-inset",
31
+ "[&[data-selected]:not([data-range-middle])]:bg-primary [&[data-selected]:not([data-range-middle])]:text-primary-content",
32
+ "data-range-middle:bg-primary/15 data-range-middle:rounded-none",
33
+ "data-outside-month:text-base-content/40",
34
+ "data-unavailable:text-base-content/40 data-unavailable:line-through",
35
+ "data-disabled:text-base-content/30 data-disabled:pointer-events-none"
36
+ ];
37
+ </script>
38
+
39
+ <P.Header class="flex items-center justify-between gap-2 pb-2">
40
+ <!-- bits-ui writes its own English aria-label after the caller's props, so
41
+ the button is rendered through `child` to put `previousLabel` last. -->
42
+ <P.PrevButton>
43
+ {#snippet child({ props })}
44
+ <button {...props} class="btn btn-ghost btn-sm btn-square" aria-label={previousLabel}>
45
+ <ChevronLeft class="size-4" aria-hidden="true" />
46
+ </button>
47
+ {/snippet}
48
+ </P.PrevButton>
49
+ <P.Heading class="text-sm font-medium" />
50
+ <P.NextButton>
51
+ {#snippet child({ props })}
52
+ <button {...props} class="btn btn-ghost btn-sm btn-square" aria-label={nextLabel}>
53
+ <ChevronRight class="size-4" aria-hidden="true" />
54
+ </button>
55
+ {/snippet}
56
+ </P.NextButton>
57
+ </P.Header>
58
+ <div class="flex flex-col gap-4 sm:flex-row">
59
+ {#each months as month (month.value.toString())}
60
+ <P.Grid class="w-full border-collapse select-none" aria-label={nameOf(month)}>
61
+ <P.GridHead>
62
+ <P.GridRow class="flex">
63
+ {#each weekdays as weekday, index (index)}
64
+ <P.HeadCell class="text-base-content/60 w-8 text-xs font-normal">{weekday}</P.HeadCell>
65
+ {/each}
66
+ </P.GridRow>
67
+ </P.GridHead>
68
+ <P.GridBody>
69
+ {#each month.weeks as week, index (index)}
70
+ <P.GridRow class="flex">
71
+ {#each week as date (date.toString())}
72
+ <P.Cell {date} month={month.value} class="p-0">
73
+ {#snippet children({ selected, unavailable })}
74
+ <P.Day class={dayClass}>
75
+ {date.day}
76
+ {#if day}{@render day({ date, selected, unavailable })}{/if}
77
+ </P.Day>
78
+ {/snippet}
79
+ </P.Cell>
80
+ {/each}
81
+ </P.GridRow>
82
+ {/each}
83
+ </P.GridBody>
84
+ </P.Grid>
85
+ {/each}
86
+ </div>
@@ -0,0 +1,30 @@
1
+ import { type DateValue } from '@internationalized/date';
2
+ import type { Calendar, DatePicker, DateRangePicker, RangeCalendar } from 'bits-ui';
3
+ import type { Snippet } from 'svelte';
4
+ /** One month of the calendar, as bits-ui's root snippet hands it over. */
5
+ interface Month {
6
+ readonly value: DateValue;
7
+ readonly weeks: readonly (readonly DateValue[])[];
8
+ }
9
+ /** What the `day` snippet receives for one cell. */
10
+ interface DayState {
11
+ readonly date: DateValue;
12
+ readonly selected: boolean;
13
+ readonly unavailable: boolean;
14
+ }
15
+ interface Props {
16
+ /** The bits-ui namespace whose parts to render. */
17
+ parts: typeof Calendar | typeof RangeCalendar | typeof DatePicker | typeof DateRangePicker;
18
+ months: readonly Month[];
19
+ weekdays: readonly string[];
20
+ /** The resolved single locale. */
21
+ locale: string;
22
+ previousLabel: string;
23
+ nextLabel: string;
24
+ /** Extra decoration inside a day cell. */
25
+ day?: Snippet<[DayState]> | undefined;
26
+ }
27
+ /** The header, month grids and day cells `Calendar` and `RangeCalendar` share; internal, not exported. */
28
+ declare const CalendarMonths: import("svelte").Component<Props, {}, "">;
29
+ type CalendarMonths = ReturnType<typeof CalendarMonths>;
30
+ export default CalendarMonths;
@@ -17,7 +17,7 @@ A daisyUI `checkbox` with its label in the same `<label>`, so the whole row is t
17
17
  hint and errors when there is one.
18
18
  -->
19
19
  <script lang="ts">import { checkboxSize } from "./field-types.js";
20
- let { label, checked = $bindable(false), size = "md", invalid = false, describedBy = "", class: className, ...rest } = $props();
20
+ let { label, checked = $bindable(false), size = "md", invalid = false, describedBy = "", labelId: _labelId, class: className, ...rest } = $props();
21
21
  </script>
22
22
 
23
23
  <label class={['label cursor-pointer justify-start gap-3', className]}>
@@ -17,6 +17,12 @@ interface Props extends Omit<HTMLInputAttributes, 'type' | 'checked' | 'size'> {
17
17
  * @defaultValue `''`
18
18
  */
19
19
  describedBy?: string | undefined;
20
+ /**
21
+ * `FieldControl`: the visible label's id. Not used — this control is
22
+ * labelable, so the `FormField`'s `<label for>` already names it; taken
23
+ * here so a spread `FieldControl` does not leak it as an attribute.
24
+ */
25
+ labelId?: string | undefined;
20
26
  }
21
27
  /** A daisyUI `checkbox` with its label in the same `<label>`, so the whole row is the hit target. */
22
28
  declare const Checkbox: import("svelte").Component<Props, {}, "checked">;
@@ -12,7 +12,8 @@ A single-select typeahead over a static list or an asynchronous search, on bits-
12
12
  bits-ui's here. What is NOT bits-ui's, and is what this file is actually
13
13
  about, is the SEARCH LIFECYCLE.
14
14
 
15
- The search lifecycle, in the order the problems bite:
15
+ The search lifecycle (`latest-search.svelte.ts`, shared with `TagsInput`),
16
+ in the order the problems bite:
16
17
 
17
18
  debounce a keystroke does not start a request; `debounceMs` of
18
19
  quiet does.
@@ -49,21 +50,34 @@ A single-select typeahead over a static list or an asynchronous search, on bits-
49
50
  COMMITS. The fall-through happens where it matters — a query that matched
50
51
  nothing, or a panel the reader never opened — which is exactly the case a
51
52
  no-JS search form depends on.
53
+
54
+ `postedValue="query"` turns the widget into a search box, and changes two
55
+ things. The `name` moves onto the visible `<input>`, so the form posts the
56
+ typed text, and it does so with JavaScript off because the server-rendered
57
+ input is the posted control. And Enter submits the form unless the reader
58
+ MOVED to an option with the keyboard (`navigated`): bits-ui's
59
+ auto-highlight of the first candidate does not count, so typing a query and
60
+ pressing Enter searches for that query instead of opening the first
61
+ suggestion. Clicking an option still commits it.
52
62
  -->
53
63
  <script lang="ts" generics="T">import { Combobox } from "bits-ui";
54
64
  import { inputSize } from "./field-types.js";
55
- let { value = $bindable(), items, onSearch, debounceMs = 150, getId, getLabel, getBadge, label, placeholder, name, emptyLabel = "No matches", pendingLabel = "Searching…", errorMessage, id: idProp, describedBy = "", invalid = false, required = false, disabled = false, size = "md", class: className, item, footer, empty } = $props();
65
+ import { LatestSearch } from "./latest-search.svelte.js";
66
+ let { value = $bindable(), items, onSearch, debounceMs = 150, getId, getLabel, getBadge, label, placeholder, name, postedValue = "id", initialQuery, onQueryChange, onSelect, emptyLabel = "No matches", pendingLabel = "Searching…", errorMessage, id: idProp, describedBy = "", labelId: _labelId, invalid = false, required = false, disabled = false, size = "md", class: className, inputClass, hideLabel = false, portal = true, item, footer, empty, ...rest } = $props();
56
67
  const uid = $props.id();
57
68
  const inputId = $derived(idProp ?? uid);
58
- let query = $state("");
69
+ // Only the initial value seeds the text; later changes come from typing.
70
+ // svelte-ignore state_referenced_locally
71
+ let query = $state(initialQuery ?? "");
59
72
  let open = $state(false);
60
- let results = $state([]);
61
- let pending = $state(false);
62
- let searchError = $state();
73
+ const searcher = new LatestSearch(() => onSearch, () => debounceMs);
74
+ const results = $derived(searcher.results);
75
+ const pending = $derived(searcher.pending);
76
+ const searchError = $derived(searcher.error);
63
77
  /** The id of the row bits-ui currently has under the cursor, if any. */
64
78
  let highlighted = $state();
65
- let timer;
66
- let inFlight;
79
+ /** The reader moved the cursor with the keyboard since they last typed. */
80
+ let navigated = false;
67
81
  /** Static mode filters locally; async mode shows whatever last settled. */
68
82
  const visible = $derived.by(() => {
69
83
  if (onSearch !== undefined) return results;
@@ -77,57 +91,45 @@ const selectedLabel = $derived.by(() => {
77
91
  const match = items.find((entry) => getId(entry) === value);
78
92
  return match === undefined ? undefined : getLabel(match);
79
93
  });
94
+ /** Seeds bits-ui's input text once, at mount. */
95
+ const initialText = $derived(selectedLabel ?? initialQuery);
96
+ const postsQuery = $derived(postedValue === "query");
80
97
  const visibleError = $derived(errorMessage ?? searchError);
81
- const search = (text) => {
82
- if (onSearch === undefined) return;
83
- inFlight?.abort();
84
- const own = new AbortController();
85
- inFlight = own;
86
- pending = true;
87
- searchError = undefined;
88
- void onSearch(text, own.signal).then((next) => {
89
- // A superseded request must not write: `inFlight` is the identity
90
- // check, `signal.aborted` catches the caller aborting its own work.
91
- if (own.signal.aborted || inFlight !== own) return;
92
- results = next;
93
- pending = false;
94
- }).catch((error) => {
95
- if (own.signal.aborted || inFlight !== own) return;
96
- pending = false;
97
- searchError = error instanceof Error ? error.message : String(error);
98
- });
99
- };
100
- const schedule = (text) => {
101
- if (timer !== undefined) clearTimeout(timer);
102
- timer = setTimeout(() => {
103
- timer = undefined;
104
- search(text);
105
- }, debounceMs);
106
- };
107
- $effect(() => () => {
108
- if (timer !== undefined) clearTimeout(timer);
109
- inFlight?.abort();
110
- });
98
+ $effect(() => () => searcher.dispose());
111
99
  const typed = (event) => {
112
100
  query = event.currentTarget.value;
101
+ navigated = false;
102
+ onQueryChange?.(query);
113
103
  if (onSearch === undefined) {
114
104
  open = true;
115
105
  return;
116
106
  }
117
107
  if (query.trim() === "") {
118
- if (timer !== undefined) clearTimeout(timer);
119
- inFlight?.abort();
120
- inFlight = undefined;
121
- pending = false;
122
- results = [];
108
+ searcher.clear();
123
109
  open = false;
124
110
  return;
125
111
  }
126
112
  open = true;
127
- schedule(query);
113
+ searcher.schedule(query);
128
114
  };
115
+ const navigationKeys = new Set([
116
+ "ArrowDown",
117
+ "ArrowUp",
118
+ "Home",
119
+ "End",
120
+ "PageDown",
121
+ "PageUp"
122
+ ]);
129
123
  const keydown = (event) => {
130
- if (event.key !== "Enter" || highlighted !== undefined) return;
124
+ if (navigationKeys.has(event.key)) {
125
+ navigated = true;
126
+ return;
127
+ }
128
+ if (event.key !== "Enter") return;
129
+ // In query mode a highlight the reader did not move to is bits-ui's
130
+ // auto-highlight, not a choice: treat it as nothing highlighted.
131
+ const committing = highlighted !== undefined && (!postsQuery || navigated);
132
+ if (committing) return;
131
133
  // Nothing to commit. Close first so bits-ui's own Enter branch takes the
132
134
  // closed path, which returns without `preventDefault` — that is what lets
133
135
  // a wrapping form submit.
@@ -135,8 +137,8 @@ const keydown = (event) => {
135
137
  };
136
138
  </script>
137
139
 
138
- <div class={['fieldset w-full', className]}>
139
- <label class="label" for={inputId}>{label}</label>
140
+ <div {...rest} class={['fieldset w-full', className]}>
141
+ <label class={['label', hideLabel && 'sr-only']} for={inputId}>{label}</label>
140
142
 
141
143
  <Combobox.Root
142
144
  type="single"
@@ -144,23 +146,27 @@ const keydown = (event) => {
144
146
  value={value ?? ''}
145
147
  onValueChange={(next) => {
146
148
  value = next === '' ? undefined : next;
149
+ if (next === '' || onSelect === undefined) return;
150
+ const chosen = visible.find((entry) => getId(entry) === next);
151
+ if (chosen !== undefined) onSelect(chosen);
147
152
  }}
148
153
  {disabled}
149
154
  {required}
150
155
  >
151
156
  <Combobox.Input
152
157
  id={inputId}
153
- {...selectedLabel === undefined ? {} : { defaultValue: selectedLabel }}
158
+ {...initialText === undefined ? {} : { defaultValue: initialText }}
159
+ {...postsQuery && name !== undefined ? { name } : {}}
154
160
  {placeholder}
155
161
  autocomplete="off"
156
162
  aria-describedby={describedBy === '' ? undefined : describedBy}
157
163
  aria-invalid={invalid ? 'true' : undefined}
158
164
  oninput={typed}
159
165
  onkeydown={keydown}
160
- class={['input w-full', inputSize[size], invalid && 'input-error']}
166
+ class={['input w-full', inputSize[size], invalid && 'input-error', inputClass]}
161
167
  />
162
168
 
163
- <Combobox.Portal>
169
+ <Combobox.Portal disabled={!portal}>
164
170
  <Combobox.Content
165
171
  sideOffset={4}
166
172
  class="menu bg-base-100 rounded-box border-base-300 z-50 max-h-72 min-w-64 overflow-auto border p-2 shadow-md"
@@ -208,11 +214,12 @@ const keydown = (event) => {
208
214
  </Combobox.Root>
209
215
 
210
216
  <!--
211
- The posted value is the id, not the typed text: bits-ui's own hidden input
212
- is not used, because `name` here means "post the selection" and nothing
213
- else about the widget should depend on it.
217
+ In id mode the posted value is the id, not the typed text: bits-ui's own
218
+ hidden input is not used, because `name` here means "post the selection"
219
+ and nothing else about the widget should depend on it. In query mode the
220
+ visible input above carries `name` instead.
214
221
  -->
215
- {#if name !== undefined}
222
+ {#if name !== undefined && !postsQuery}
216
223
  <input type="hidden" {name} value={value ?? ''} />
217
224
  {/if}
218
225
 
@@ -1,8 +1,9 @@
1
1
  import { Combobox } from 'bits-ui';
2
2
  import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { type FieldSize } from './field-types.ts';
4
5
  declare function $$render<T>(): {
5
- props: {
6
+ props: Omit<HTMLAttributes<HTMLDivElement>, "class" | "id" | "children"> & {
6
7
  /** The selected item's id. */
7
8
  value?: string | undefined;
8
9
  /** A static list, filtered here by label. Ignored when `onSearch` is set. */
@@ -27,8 +28,26 @@ declare function $$render<T>(): {
27
28
  label: string;
28
29
  /** Placeholder in the input. */
29
30
  placeholder?: string | undefined;
30
- /** Posted field name. Set it to render a hidden input carrying the id. */
31
+ /**
32
+ * Posted field name. With `postedValue="id"` it renders a hidden input
33
+ * carrying the selected id; with `postedValue="query"` it goes on the
34
+ * visible input, which posts the typed text.
35
+ */
31
36
  name?: string | undefined;
37
+ /**
38
+ * What the field under `name` posts: the selected item's id, or the typed
39
+ * query text. `'query'` makes the input itself the posted control (so a
40
+ * plain GET search form works with JavaScript off) and makes Enter submit
41
+ * the form unless the reader arrowed to an option.
42
+ * @defaultValue `'id'`
43
+ */
44
+ postedValue?: "id" | "query" | undefined;
45
+ /** Text in the input at mount, such as the current search. A selected item's label wins. */
46
+ initialQuery?: string | undefined;
47
+ /** Called with the raw input text on every keystroke. */
48
+ onQueryChange?: ((query: string) => void) | undefined;
49
+ /** Called with the item the reader committed (click, or Enter on a highlighted row). */
50
+ onSelect?: ((item: T) => void) | undefined;
32
51
  /**
33
52
  * Shown when a settled search returned nothing.
34
53
  * @defaultValue `'No matches'`
@@ -48,6 +67,12 @@ declare function $$render<T>(): {
48
67
  * @defaultValue `''`
49
68
  */
50
69
  describedBy?: string | undefined;
70
+ /**
71
+ * `FieldControl`: the visible label's id. Not used — this control is
72
+ * labelable, so the `FormField`'s `<label for>` already names it; taken
73
+ * here so a spread `FieldControl` does not leak it as an attribute.
74
+ */
75
+ labelId?: string | undefined;
51
76
  /** `FieldControl`: the field is in error. */
52
77
  invalid?: boolean | undefined;
53
78
  /** `FieldControl`: the field must be filled. */
@@ -59,8 +84,18 @@ declare function $$render<T>(): {
59
84
  * @defaultValue `'md'`
60
85
  */
61
86
  size?: FieldSize | undefined;
62
- /** Extra classes, merged last. */
87
+ /** Extra classes on the root, merged last. */
63
88
  class?: string | undefined;
89
+ /** Extra classes on the `<input>`, merged last (e.g. daisyUI `join-item`). */
90
+ inputClass?: string | undefined;
91
+ /** Keep the label as the input's accessible name but hide it visually. */
92
+ hideLabel?: boolean | undefined;
93
+ /**
94
+ * Portal the listbox to `<body>`. `false` renders it inside this
95
+ * component's root instead; it still floats over the layout.
96
+ * @defaultValue `true`
97
+ */
98
+ portal?: boolean | undefined;
64
99
  /** Replace a row's contents. */
65
100
  item?: Snippet<[T]> | undefined;
66
101
  /** Rendered after the rows — a "create this" row, a "see all" link. */
@@ -0,0 +1,37 @@
1
+ <!-- @component
2
+ Segmented day / month / year entry on bits-ui `DateField`, in the locale's order.
3
+ -->
4
+ <!--
5
+ DateField — a date typed into segments (day, month, year; optionally hour
6
+ and minute), each a `role="spinbutton"`: digits to type, ↑ / ↓ to step,
7
+ ← / → and auto-advance between segments, Backspace to clear. The order
8
+ and separators follow the locale (en-AU day/month/year, en-US
9
+ month/day/year).
10
+
11
+ `DateInput` (the native `<input type="date">`) stays the default for a
12
+ single date on a form. Reach for this when a design needs the same
13
+ control on every browser, or a time as well as a date.
14
+
15
+ ARIA contract: see `SegmentGroup` — a `role="group"` named by the
16
+ `FormField` label through `labelId`, `aria-describedby` its hint and
17
+ errors, `aria-invalid` when in error. With `name`, bits-ui posts the ISO
18
+ value (`2026-10-14`) through a hidden input.
19
+ -->
20
+ <script lang="ts">import { DateField } from "bits-ui";
21
+ import { getComponentsContext } from "../context.svelte.js";
22
+ import { definedProps } from "./calendar-props.js";
23
+ import SegmentGroup from "./SegmentGroup.svelte";
24
+ import { segmentedField } from "./segmented-field.js";
25
+ let { value = $bindable(), minValue, maxValue, granularity = "day", ...shared } = $props();
26
+ const context = getComponentsContext();
27
+ const field = $derived(segmentedField(shared, context.locale()));
28
+ </script>
29
+
30
+ <DateField.Root
31
+ bind:value={() => value as DateValue, (next) => (value = next)}
32
+ {...field.root}
33
+ {...definedProps({ minValue, maxValue })}
34
+ {granularity}
35
+ >
36
+ <SegmentGroup parts={DateField} group={field.group} />
37
+ </DateField.Root>
@@ -0,0 +1,20 @@
1
+ import type { DateValue } from '@internationalized/date';
2
+ import { DateField } from 'bits-ui';
3
+ import { type SegmentedFieldProps } from './segmented-field.ts';
4
+ interface Props extends SegmentedFieldProps {
5
+ /** The date; `undefined` while empty or incomplete. Bindable. */
6
+ value?: DateValue | undefined;
7
+ /** Earliest valid date. */
8
+ minValue?: DateValue | undefined;
9
+ /** Latest valid date. */
10
+ maxValue?: DateValue | undefined;
11
+ /**
12
+ * The smallest segment shown.
13
+ * @defaultValue `'day'`
14
+ */
15
+ granularity?: 'day' | 'hour' | 'minute' | 'second' | undefined;
16
+ }
17
+ /** Segmented day / month / year entry on bits-ui `DateField`, in the locale's order. */
18
+ declare const DateField: import("svelte").Component<Props, {}, "value">;
19
+ type DateField = ReturnType<typeof DateField>;
20
+ export default DateField;
@@ -16,7 +16,7 @@ A native `<input type="date">` in daisyUI `input` styling, with `FieldControl` w
16
16
  picker that people already know.
17
17
  -->
18
18
  <script lang="ts">import { inputSize } from "./field-types.js";
19
- let { value = $bindable(""), min, max, size = "md", invalid = false, describedBy = "", class: className, ...rest } = $props();
19
+ let { value = $bindable(""), min, max, size = "md", invalid = false, describedBy = "", labelId: _labelId, class: className, ...rest } = $props();
20
20
  </script>
21
21
 
22
22
  <input
@@ -22,6 +22,12 @@ interface Props extends Omit<HTMLInputAttributes, 'type' | 'value' | 'size' | 'm
22
22
  * @defaultValue `''`
23
23
  */
24
24
  describedBy?: string | undefined;
25
+ /**
26
+ * `FieldControl`: the visible label's id. Not used — this control is
27
+ * labelable, so the `FormField`'s `<label for>` already names it; taken
28
+ * here so a spread `FieldControl` does not leak it as an attribute.
29
+ */
30
+ labelId?: string | undefined;
25
31
  }
26
32
  /** A native `<input type="date">` in daisyUI `input` styling, with `FieldControl` wiring. */
27
33
  declare const DateInput: import("svelte").Component<Props, {}, "value">;
@@ -0,0 +1,47 @@
1
+ <!-- @component
2
+ A segmented date field with a button that opens a calendar popover, on bits-ui `DatePicker`.
3
+ -->
4
+ <!--
5
+ DatePicker — `DateField`'s segments plus a calendar button: type the date,
6
+ or open the month and pick it. Selecting a day closes the popover
7
+ (`closeOnDateSelect`) and returns focus to the button.
8
+
9
+ Reach for it when the reader benefits from seeing the month (which day
10
+ of the week, what is unavailable); for a plain date on a form the
11
+ native `DateInput` remains the default.
12
+
13
+ ARIA contract: the segment group as `DateField` (named by the
14
+ `FormField` label through `labelId`); the trigger and dialog as
15
+ `PickerPopover`; the grid as `Calendar`.
16
+ -->
17
+ <script lang="ts">import { DatePicker } from "bits-ui";
18
+ import { getComponentsContext } from "../context.svelte.js";
19
+ import { definedProps } from "./calendar-props.js";
20
+ import PickerPopover from "./PickerPopover.svelte";
21
+ import SegmentGroup from "./SegmentGroup.svelte";
22
+ import { pickerField } from "./segmented-field.js";
23
+ let { value = $bindable(), open = $bindable(false), minValue, maxValue, granularity = "day", closeOnDateSelect = true, ...shared } = $props();
24
+ const context = getComponentsContext();
25
+ const picker = $derived(pickerField(shared, context.locale()));
26
+ </script>
27
+
28
+ <DatePicker.Root
29
+ bind:value={() => value as DateValue, (next) => (value = next)}
30
+ bind:open
31
+ {...picker.root}
32
+ {...definedProps({ minValue, maxValue })}
33
+ {granularity}
34
+ {closeOnDateSelect}
35
+ >
36
+ <SegmentGroup parts={DatePicker} group={picker.group}>
37
+ {#snippet trailing()}
38
+ <PickerPopover
39
+ parts={DatePicker}
40
+ triggerLabel={picker.triggerLabel}
41
+ calendarLabel={picker.calendarLabel}
42
+ months={picker.months}
43
+ disabled={picker.group.disabled}
44
+ />
45
+ {/snippet}
46
+ </SegmentGroup>
47
+ </DatePicker.Root>
@@ -0,0 +1,25 @@
1
+ import type { DateValue } from '@internationalized/date';
2
+ import { DatePicker } from 'bits-ui';
3
+ import { type PickerProps, type SegmentedFieldProps } from './segmented-field.ts';
4
+ interface Props extends SegmentedFieldProps, PickerProps {
5
+ /** The date; `undefined` while empty or incomplete. Bindable. */
6
+ value?: DateValue | undefined;
7
+ /** Earliest valid date. */
8
+ minValue?: DateValue | undefined;
9
+ /** Latest valid date. */
10
+ maxValue?: DateValue | undefined;
11
+ /**
12
+ * The smallest segment shown.
13
+ * @defaultValue `'day'`
14
+ */
15
+ granularity?: 'day' | 'hour' | 'minute' | 'second' | undefined;
16
+ /**
17
+ * Close the popover when a day is picked.
18
+ * @defaultValue `true`
19
+ */
20
+ closeOnDateSelect?: boolean | undefined;
21
+ }
22
+ /** A segmented date field with a button that opens a calendar popover, on bits-ui `DatePicker`. */
23
+ declare const DatePicker: import("svelte").Component<Props, {}, "value" | "open">;
24
+ type DatePicker = ReturnType<typeof DatePicker>;
25
+ export default DatePicker;
@@ -0,0 +1,70 @@
1
+ <!-- @component
2
+ Two segmented date fields, a start and an end, with a range-calendar popover, on bits-ui `DateRangePicker`.
3
+ -->
4
+ <!--
5
+ DateRangePicker — a start and an end date typed into segments, or picked
6
+ as a span on a `RangeCalendar` in a popover. `minDays` / `maxDays`
7
+ constrain the span while it is chosen.
8
+
9
+ ARIA contract: the whole control is one `role="group"` named by the
10
+ `FormField` label (`labelId`) and described by its hint and errors, so
11
+ the error text reaches it (a written-out `group` does not take
12
+ `aria-invalid`; `invalid` draws the error border). Inside it, the two halves are groups named
13
+ `startLabel` / `endLabel`, so a segment reads "day, <label> Start date".
14
+ The dash between them is `aria-hidden`. The trigger and dialog are
15
+ `PickerPopover`'s.
16
+ -->
17
+ <script lang="ts">import { DateRangePicker } from "bits-ui";
18
+ import { getComponentsContext } from "../context.svelte.js";
19
+ import { definedProps } from "./calendar-props.js";
20
+ import { inputSize } from "./field-types.js";
21
+ import PickerPopover from "./PickerPopover.svelte";
22
+ import SegmentGroup from "./SegmentGroup.svelte";
23
+ import { pickerField } from "./segmented-field.js";
24
+ let { value = $bindable(), open = $bindable(false), minValue, maxValue, minDays, maxDays, closeOnRangeSelect = true, startName, endName, startLabel = "Start date", endLabel = "End date", ...shared } = $props();
25
+ const context = getComponentsContext();
26
+ const picker = $derived(pickerField(shared, context.locale()));
27
+ const group = $derived(picker.group);
28
+ const { class: className, ...attributes } = $derived(group.attributes);
29
+ </script>
30
+
31
+ <DateRangePicker.Root
32
+ bind:value={
33
+ () => value ?? { start: undefined, end: undefined },
34
+ (next) => (value = { start: next.start, end: next.end })
35
+ }
36
+ bind:open
37
+ {...picker.root}
38
+ {...definedProps({ minValue, maxValue, minDays, maxDays })}
39
+ {closeOnRangeSelect}
40
+ >
41
+ {#snippet child({ props })}
42
+ <div
43
+ {...attributes}
44
+ {...props}
45
+ {...group.id === undefined ? {} : { id: group.id }}
46
+ role="group"
47
+ aria-labelledby={group.labelId}
48
+ aria-describedby={group.describedBy === '' ? undefined : group.describedBy}
49
+ class={[
50
+ 'input inline-flex w-auto items-center gap-0 tabular-nums',
51
+ inputSize[group.size],
52
+ group.invalid && 'input-error',
53
+ 'data-invalid:input-error',
54
+ group.disabled && 'opacity-50',
55
+ className,
56
+ ]}
57
+ >
58
+ <SegmentGroup parts={DateRangePicker} {group} range={{ type: 'start', label: startLabel, name: startName }} />
59
+ <span aria-hidden="true" class="text-base-content/60 px-1.5">–</span>
60
+ <SegmentGroup parts={DateRangePicker} {group} range={{ type: 'end', label: endLabel, name: endName }} />
61
+ <PickerPopover
62
+ parts={DateRangePicker}
63
+ triggerLabel={picker.triggerLabel}
64
+ calendarLabel={picker.calendarLabel}
65
+ months={picker.months}
66
+ disabled={group.disabled}
67
+ />
68
+ </div>
69
+ {/snippet}
70
+ </DateRangePicker.Root>