@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.
- package/README.md +241 -56
- package/dist/canvas/VectorCanvas.svelte +89 -8
- package/dist/canvas/VectorCanvas.svelte.d.ts +19 -1
- package/dist/canvas/types.d.ts +1 -1
- package/dist/charts/types.d.ts +1 -1
- package/dist/citations/list-format.js +3 -1
- package/dist/data/ChatBubble.svelte +48 -0
- package/dist/data/ChatBubble.svelte.d.ts +33 -0
- package/dist/data/DescriptionList.svelte +9 -3
- package/dist/data/DescriptionList.svelte.d.ts +1 -1
- package/dist/data/DescriptionRow.svelte +6 -5
- package/dist/data/MessageThread.svelte +29 -0
- package/dist/data/MessageThread.svelte.d.ts +17 -0
- package/dist/data/Pagination.svelte +5 -5
- package/dist/data/Pagination.svelte.d.ts +8 -0
- package/dist/data/PaginationFooter.svelte +2 -1
- package/dist/data/PaginationFooter.svelte.d.ts +2 -0
- package/dist/data/Table.svelte +9 -3
- package/dist/data/description-context.d.ts +12 -6
- package/dist/data/description-context.js +6 -5
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +2 -0
- package/dist/data/table-types.d.ts +7 -0
- package/dist/feedback/Dialog.svelte +19 -49
- package/dist/feedback/Dialog.svelte.d.ts +0 -1
- package/dist/feedback/DialogFrame.svelte +64 -0
- package/dist/feedback/DialogFrame.svelte.d.ts +23 -0
- package/dist/feedback/Drawer.svelte +43 -0
- package/dist/feedback/Drawer.svelte.d.ts +43 -0
- package/dist/feedback/SegmentBar.svelte.d.ts +1 -0
- package/dist/feedback/StatTile.svelte.d.ts +1 -0
- package/dist/feedback/Steps.svelte +72 -0
- package/dist/feedback/Steps.svelte.d.ts +61 -0
- package/dist/feedback/Timeline.svelte.d.ts +3 -0
- package/dist/feedback/index.d.ts +3 -0
- package/dist/feedback/index.js +2 -0
- package/dist/feedback/sizes.d.ts +14 -0
- package/dist/feedback/sizes.js +28 -0
- package/dist/fields/Calendar.svelte +60 -0
- package/dist/fields/Calendar.svelte.d.ts +11 -0
- package/dist/fields/CalendarMonths.svelte +86 -0
- package/dist/fields/CalendarMonths.svelte.d.ts +30 -0
- package/dist/fields/Checkbox.svelte +1 -1
- package/dist/fields/Checkbox.svelte.d.ts +6 -0
- package/dist/fields/Combobox.svelte +61 -54
- package/dist/fields/Combobox.svelte.d.ts +38 -3
- package/dist/fields/DateField.svelte +37 -0
- package/dist/fields/DateField.svelte.d.ts +20 -0
- package/dist/fields/DateInput.svelte +1 -1
- package/dist/fields/DateInput.svelte.d.ts +6 -0
- package/dist/fields/DatePicker.svelte +47 -0
- package/dist/fields/DatePicker.svelte.d.ts +25 -0
- package/dist/fields/DateRangePicker.svelte +70 -0
- package/dist/fields/DateRangePicker.svelte.d.ts +43 -0
- package/dist/fields/Dropzone.svelte +232 -0
- package/dist/fields/Dropzone.svelte.d.ts +103 -0
- package/dist/fields/Fieldset.svelte +7 -2
- package/dist/fields/Fieldset.svelte.d.ts +3 -2
- package/dist/fields/FileInput.svelte +3 -2
- package/dist/fields/FileInput.svelte.d.ts +6 -0
- package/dist/fields/FormField.svelte +27 -3
- package/dist/fields/ImageInput.svelte.d.ts +6 -0
- package/dist/fields/MultiSelect.svelte +112 -0
- package/dist/fields/MultiSelect.svelte.d.ts +75 -0
- package/dist/fields/NumberField.svelte +190 -0
- package/dist/fields/NumberField.svelte.d.ts +76 -0
- package/dist/fields/PasswordInput.svelte +95 -0
- package/dist/fields/PasswordInput.svelte.d.ts +58 -0
- package/dist/fields/PickerPopover.svelte +47 -0
- package/dist/fields/PickerPopover.svelte.d.ts +16 -0
- package/dist/fields/PinInput.svelte +83 -0
- package/dist/fields/PinInput.svelte.d.ts +66 -0
- package/dist/fields/RadioGroup.svelte +3 -0
- package/dist/fields/RangeCalendar.svelte +51 -0
- package/dist/fields/RangeCalendar.svelte.d.ts +23 -0
- package/dist/fields/Rating.svelte +127 -0
- package/dist/fields/Rating.svelte.d.ts +86 -0
- package/dist/fields/SearchInput.svelte +2 -1
- package/dist/fields/SegmentGroup.svelte +91 -0
- package/dist/fields/SegmentGroup.svelte.d.ts +20 -0
- package/dist/fields/Select.svelte +1 -1
- package/dist/fields/Select.svelte.d.ts +6 -0
- package/dist/fields/Slider.svelte +194 -0
- package/dist/fields/Slider.svelte.d.ts +100 -0
- package/dist/fields/TagsInput.svelte +256 -0
- package/dist/fields/TagsInput.svelte.d.ts +102 -0
- package/dist/fields/TextInput.svelte +1 -1
- package/dist/fields/TextInput.svelte.d.ts +6 -0
- package/dist/fields/Textarea.svelte +1 -1
- package/dist/fields/Textarea.svelte.d.ts +6 -0
- package/dist/fields/TimeField.svelte +30 -0
- package/dist/fields/TimeField.svelte.d.ts +19 -0
- package/dist/fields/Toggle.svelte +1 -1
- package/dist/fields/Toggle.svelte.d.ts +6 -0
- package/dist/fields/VideoInput.svelte.d.ts +6 -0
- package/dist/fields/calendar-props.d.ts +119 -0
- package/dist/fields/calendar-props.js +51 -0
- package/dist/fields/dates.d.ts +28 -0
- package/dist/fields/dates.js +26 -0
- package/dist/fields/field-types.d.ts +29 -0
- package/dist/fields/field-types.js +32 -0
- package/dist/fields/file-validation.d.ts +30 -0
- package/dist/fields/file-validation.js +51 -0
- package/dist/fields/index.d.ts +9 -0
- package/dist/fields/index.js +8 -0
- package/dist/fields/latest-search.svelte.d.ts +38 -0
- package/dist/fields/latest-search.svelte.js +85 -0
- package/dist/fields/number-format.d.ts +52 -0
- package/dist/fields/number-format.js +89 -0
- package/dist/fields/segmented-field.d.ts +128 -0
- package/dist/fields/segmented-field.js +52 -0
- package/dist/fields/slider-keys.d.ts +21 -0
- package/dist/fields/slider-keys.js +37 -0
- package/dist/formatting/Countdown.svelte +110 -0
- package/dist/formatting/Countdown.svelte.d.ts +35 -0
- package/dist/formatting/Duration.svelte +18 -0
- package/dist/formatting/Duration.svelte.d.ts +20 -0
- package/dist/formatting/Highlight.svelte +4 -0
- package/dist/formatting/Highlight.svelte.d.ts +4 -0
- package/dist/formatting/duration.d.ts +44 -0
- package/dist/formatting/duration.js +125 -0
- package/dist/formatting/index.d.ts +3 -0
- package/dist/formatting/index.js +3 -0
- package/dist/forms/EntityForm.svelte +53 -45
- package/dist/forms/EntityForm.svelte.d.ts +12 -3
- package/dist/hotkeys/index.d.ts +5 -0
- package/dist/hotkeys/index.js +5 -0
- package/dist/hotkeys/match.js +12 -10
- package/dist/hotkeys/types.d.ts +4 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/infobox/Infobox.svelte +2 -2
- package/dist/infobox/Infobox.svelte.d.ts +7 -0
- package/dist/infobox/InfoboxHeader.svelte +15 -7
- package/dist/infobox/InfoboxHeader.svelte.d.ts +7 -0
- package/dist/infobox/types.d.ts +5 -5
- package/dist/layout/AppShell.svelte +50 -0
- package/dist/layout/AppShell.svelte.d.ts +28 -0
- package/dist/layout/Cluster.svelte +28 -0
- package/dist/layout/Cluster.svelte.d.ts +27 -0
- package/dist/layout/Fits.svelte +27 -0
- package/dist/layout/Fits.svelte.d.ts +13 -0
- package/dist/layout/Flow.svelte +46 -0
- package/dist/layout/Flow.svelte.d.ts +16 -0
- package/dist/layout/Grid.svelte +23 -0
- package/dist/layout/Grid.svelte.d.ts +26 -0
- package/dist/layout/HStack.svelte +12 -0
- package/dist/layout/HStack.svelte.d.ts +34 -0
- package/dist/layout/Hero.svelte +76 -0
- package/dist/layout/Hero.svelte.d.ts +36 -0
- package/dist/layout/Navbar.svelte +2 -2
- package/dist/layout/Sidebar.svelte +1 -1
- package/dist/layout/Spacer.svelte +12 -0
- package/dist/layout/Spacer.svelte.d.ts +6 -0
- package/dist/layout/VStack.svelte +12 -0
- package/dist/layout/VStack.svelte.d.ts +29 -0
- package/dist/layout/index.d.ts +9 -0
- package/dist/layout/index.js +9 -0
- package/dist/layout/space.d.ts +44 -0
- package/dist/layout/space.js +76 -0
- package/dist/search/types.d.ts +3 -0
- package/dist/seo/og-image.d.ts +4 -3
- package/dist/seo/og-image.js +4 -3
- package/dist/tips/tip-stack.d.ts +1 -0
- package/dist/tips/tip-store.svelte.d.ts +2 -1
- package/dist/ui/Accordion.svelte +75 -0
- package/dist/ui/Accordion.svelte.d.ts +50 -0
- package/dist/ui/AccordionItem.svelte +73 -0
- package/dist/ui/AccordionItem.svelte.d.ts +18 -0
- package/dist/ui/Button.svelte +3 -2
- package/dist/ui/ButtonGroup.svelte +24 -0
- package/dist/ui/ButtonGroup.svelte.d.ts +17 -0
- package/dist/ui/ContextMenu.svelte +75 -0
- package/dist/ui/ContextMenu.svelte.d.ts +25 -0
- package/dist/ui/Disclosure.svelte +7 -0
- package/dist/ui/Disclosure.svelte.d.ts +2 -2
- package/dist/ui/Divider.svelte +47 -0
- package/dist/ui/Divider.svelte.d.ts +28 -0
- package/dist/ui/DropdownMenu.svelte +44 -0
- package/dist/ui/DropdownMenu.svelte.d.ts +30 -0
- package/dist/ui/Menu.svelte +17 -47
- package/dist/ui/Menu.svelte.d.ts +3 -24
- package/dist/ui/MenuCheckboxItem.svelte +33 -0
- package/dist/ui/MenuCheckboxItem.svelte.d.ts +20 -0
- package/dist/ui/MenuItem.svelte +29 -31
- package/dist/ui/MenuItem.svelte.d.ts +2 -2
- package/dist/ui/MenuRadioGroup.svelte +25 -0
- package/dist/ui/MenuRadioGroup.svelte.d.ts +20 -0
- package/dist/ui/MenuRadioItem.svelte +20 -0
- package/dist/ui/MenuRadioItem.svelte.d.ts +15 -0
- package/dist/ui/MenuSeparator.svelte +12 -11
- package/dist/ui/MenuSub.svelte +24 -0
- package/dist/ui/MenuSub.svelte.d.ts +15 -0
- package/dist/ui/Spinner.svelte +41 -0
- package/dist/ui/Spinner.svelte.d.ts +35 -0
- package/dist/ui/Stack.svelte +36 -0
- package/dist/ui/Stack.svelte.d.ts +48 -0
- package/dist/ui/ToggleButton.svelte +58 -0
- package/dist/ui/ToggleButton.svelte.d.ts +45 -0
- package/dist/ui/ToggleGroup.svelte +142 -0
- package/dist/ui/ToggleGroup.svelte.d.ts +86 -0
- package/dist/ui/Toolbar.svelte +40 -0
- package/dist/ui/Toolbar.svelte.d.ts +20 -0
- package/dist/ui/ToolbarButton.svelte +29 -0
- package/dist/ui/ToolbarButton.svelte.d.ts +21 -0
- package/dist/ui/ToolbarLink.svelte +21 -0
- package/dist/ui/ToolbarLink.svelte.d.ts +12 -0
- package/dist/ui/ToolbarSeparator.svelte +22 -0
- package/dist/ui/ToolbarSeparator.svelte.d.ts +12 -0
- package/dist/ui/ToolbarToggleGroup.svelte +66 -0
- package/dist/ui/ToolbarToggleGroup.svelte.d.ts +52 -0
- package/dist/ui/accordion-context.d.ts +26 -0
- package/dist/ui/accordion-context.js +23 -0
- package/dist/ui/context-menu.d.ts +13 -0
- package/dist/ui/context-menu.js +13 -0
- package/dist/ui/index.d.ts +19 -0
- package/dist/ui/index.js +18 -0
- package/dist/ui/menu-context.d.ts +13 -15
- package/dist/ui/menu-context.js +14 -18
- package/dist/ui/menu-parts/CheckboxItemPart.svelte +26 -0
- package/dist/ui/menu-parts/CheckboxItemPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/ItemPart.svelte +29 -0
- package/dist/ui/menu-parts/ItemPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/RadioGroupPart.svelte +24 -0
- package/dist/ui/menu-parts/RadioGroupPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/RadioItemPart.svelte +22 -0
- package/dist/ui/menu-parts/RadioItemPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/SeparatorPart.svelte +16 -0
- package/dist/ui/menu-parts/SeparatorPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/SubPart.svelte +28 -0
- package/dist/ui/menu-parts/SubPart.svelte.d.ts +5 -0
- package/dist/ui/menu-parts/menu-parts.d.ts +104 -0
- package/dist/ui/menu-parts/menu-parts.js +31 -0
- package/dist/ui/menu-parts/parts.d.ts +12 -0
- package/dist/ui/menu-parts/parts.js +27 -0
- package/dist/ui/toggle-classes.d.ts +39 -0
- package/dist/ui/toggle-classes.js +36 -0
- package/dist/ui/tones.d.ts +35 -0
- package/dist/ui/tones.js +90 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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
|
-
{...
|
|
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
|
-
|
|
212
|
-
is not used, because `name` here means "post the selection"
|
|
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
|
-
/**
|
|
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>
|