@c9up/nebula 0.1.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/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/adapters/css.d.ts +33 -0
- package/dist/adapters/css.js +67 -0
- package/dist/adapters/index.d.ts +16 -0
- package/dist/adapters/index.js +27 -0
- package/dist/adapters/tailwind.d.ts +25 -0
- package/dist/adapters/tailwind.js +122 -0
- package/dist/adapters/types.d.ts +49 -0
- package/dist/adapters/types.js +21 -0
- package/dist/adapters/unocss.d.ts +17 -0
- package/dist/adapters/unocss.js +107 -0
- package/dist/atoms/AspectRatio.d.ts +17 -0
- package/dist/atoms/AspectRatio.js +19 -0
- package/dist/atoms/Avatar.d.ts +23 -0
- package/dist/atoms/Avatar.js +46 -0
- package/dist/atoms/Badge.d.ts +25 -0
- package/dist/atoms/Badge.js +28 -0
- package/dist/atoms/Button.d.ts +58 -0
- package/dist/atoms/Button.js +67 -0
- package/dist/atoms/Checkbox.d.ts +41 -0
- package/dist/atoms/Checkbox.js +47 -0
- package/dist/atoms/Input.d.ts +36 -0
- package/dist/atoms/Input.js +50 -0
- package/dist/atoms/Kbd.d.ts +14 -0
- package/dist/atoms/Kbd.js +17 -0
- package/dist/atoms/Label.d.ts +24 -0
- package/dist/atoms/Label.js +26 -0
- package/dist/atoms/Marker.d.ts +42 -0
- package/dist/atoms/Marker.js +55 -0
- package/dist/atoms/NativeSelect.d.ts +43 -0
- package/dist/atoms/NativeSelect.js +81 -0
- package/dist/atoms/Progress.d.ts +21 -0
- package/dist/atoms/Progress.js +48 -0
- package/dist/atoms/ScrollArea.d.ts +28 -0
- package/dist/atoms/ScrollArea.js +37 -0
- package/dist/atoms/Separator.d.ts +16 -0
- package/dist/atoms/Separator.js +21 -0
- package/dist/atoms/Skeleton.d.ts +13 -0
- package/dist/atoms/Skeleton.js +18 -0
- package/dist/atoms/Slider.d.ts +30 -0
- package/dist/atoms/Slider.js +40 -0
- package/dist/atoms/Spinner.d.ts +17 -0
- package/dist/atoms/Spinner.js +20 -0
- package/dist/atoms/Switch.d.ts +24 -0
- package/dist/atoms/Switch.js +41 -0
- package/dist/atoms/Textarea.d.ts +28 -0
- package/dist/atoms/Textarea.js +36 -0
- package/dist/atoms/Toggle.d.ts +38 -0
- package/dist/atoms/Toggle.js +45 -0
- package/dist/atoms/index.d.ts +32 -0
- package/dist/atoms/index.js +32 -0
- package/dist/cli/add.d.ts +44 -0
- package/dist/cli/add.js +99 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/registry.d.ts +31 -0
- package/dist/cli/registry.js +83 -0
- package/dist/config.d.ts +81 -0
- package/dist/config.js +43 -0
- package/dist/configure.d.ts +34 -0
- package/dist/configure.js +85 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +41 -0
- package/dist/lib/children.d.ts +28 -0
- package/dist/lib/children.js +27 -0
- package/dist/lib/cn.d.ts +13 -0
- package/dist/lib/cn.js +13 -0
- package/dist/lib/cva.d.ts +83 -0
- package/dist/lib/cva.js +178 -0
- package/dist/lib/icons.d.ts +44 -0
- package/dist/lib/icons.js +79 -0
- package/dist/lib/id.d.ts +31 -0
- package/dist/lib/id.js +41 -0
- package/dist/lib/index.d.ts +12 -0
- package/dist/lib/index.js +12 -0
- package/dist/lib/motion.d.ts +31 -0
- package/dist/lib/motion.js +39 -0
- package/dist/lib/props.d.ts +48 -0
- package/dist/lib/props.js +59 -0
- package/dist/lib/styled.d.ts +31 -0
- package/dist/lib/styled.js +39 -0
- package/dist/molecules/Accordion.d.ts +35 -0
- package/dist/molecules/Accordion.js +117 -0
- package/dist/molecules/Alert.d.ts +30 -0
- package/dist/molecules/Alert.js +35 -0
- package/dist/molecules/Attachment.d.ts +39 -0
- package/dist/molecules/Attachment.js +103 -0
- package/dist/molecules/Breadcrumb.d.ts +29 -0
- package/dist/molecules/Breadcrumb.js +73 -0
- package/dist/molecules/Bubble.d.ts +56 -0
- package/dist/molecules/Bubble.js +92 -0
- package/dist/molecules/ButtonGroup.d.ts +23 -0
- package/dist/molecules/ButtonGroup.js +30 -0
- package/dist/molecules/Card.d.ts +17 -0
- package/dist/molecules/Card.js +17 -0
- package/dist/molecules/Collapsible.d.ts +40 -0
- package/dist/molecules/Collapsible.js +71 -0
- package/dist/molecules/Empty.d.ts +16 -0
- package/dist/molecules/Empty.js +16 -0
- package/dist/molecules/Field.d.ts +54 -0
- package/dist/molecules/Field.js +82 -0
- package/dist/molecules/InputGroup.d.ts +33 -0
- package/dist/molecules/InputGroup.js +35 -0
- package/dist/molecules/InputOTP.d.ts +33 -0
- package/dist/molecules/InputOTP.js +140 -0
- package/dist/molecules/Item.d.ts +41 -0
- package/dist/molecules/Item.js +48 -0
- package/dist/molecules/Message.d.ts +43 -0
- package/dist/molecules/Message.js +82 -0
- package/dist/molecules/Pagination.d.ts +41 -0
- package/dist/molecules/Pagination.js +144 -0
- package/dist/molecules/RadioGroup.d.ts +32 -0
- package/dist/molecules/RadioGroup.js +65 -0
- package/dist/molecules/Resizable.d.ts +37 -0
- package/dist/molecules/Resizable.js +136 -0
- package/dist/molecules/Table.d.ts +37 -0
- package/dist/molecules/Table.js +78 -0
- package/dist/molecules/Tabs.d.ts +37 -0
- package/dist/molecules/Tabs.js +109 -0
- package/dist/molecules/ToggleGroup.d.ts +37 -0
- package/dist/molecules/ToggleGroup.js +68 -0
- package/dist/molecules/Typography.d.ts +26 -0
- package/dist/molecules/Typography.js +67 -0
- package/dist/molecules/index.d.ts +37 -0
- package/dist/molecules/index.js +37 -0
- package/dist/organisms/AlertDialog.d.ts +40 -0
- package/dist/organisms/AlertDialog.js +104 -0
- package/dist/organisms/Calendar.d.ts +96 -0
- package/dist/organisms/Calendar.js +353 -0
- package/dist/organisms/Carousel.d.ts +29 -0
- package/dist/organisms/Carousel.js +128 -0
- package/dist/organisms/Chart.d.ts +55 -0
- package/dist/organisms/Chart.js +189 -0
- package/dist/organisms/Combobox.d.ts +41 -0
- package/dist/organisms/Combobox.js +102 -0
- package/dist/organisms/Command.d.ts +46 -0
- package/dist/organisms/Command.js +171 -0
- package/dist/organisms/CommandDialog.d.ts +42 -0
- package/dist/organisms/CommandDialog.js +116 -0
- package/dist/organisms/ContextMenu.d.ts +29 -0
- package/dist/organisms/ContextMenu.js +102 -0
- package/dist/organisms/DataTable.d.ts +50 -0
- package/dist/organisms/DataTable.js +257 -0
- package/dist/organisms/DatePicker.d.ts +34 -0
- package/dist/organisms/DatePicker.js +115 -0
- package/dist/organisms/DateRangePicker.d.ts +34 -0
- package/dist/organisms/DateRangePicker.js +122 -0
- package/dist/organisms/Dialog.d.ts +50 -0
- package/dist/organisms/Dialog.js +116 -0
- package/dist/organisms/Drawer.d.ts +40 -0
- package/dist/organisms/Drawer.js +131 -0
- package/dist/organisms/DropdownMenu.d.ts +30 -0
- package/dist/organisms/DropdownMenu.js +91 -0
- package/dist/organisms/Form.d.ts +114 -0
- package/dist/organisms/Form.js +151 -0
- package/dist/organisms/HoverCard.d.ts +30 -0
- package/dist/organisms/HoverCard.js +68 -0
- package/dist/organisms/Menubar.d.ts +28 -0
- package/dist/organisms/Menubar.js +147 -0
- package/dist/organisms/MessageScroller.d.ts +36 -0
- package/dist/organisms/MessageScroller.js +124 -0
- package/dist/organisms/NavigationMenu.d.ts +41 -0
- package/dist/organisms/NavigationMenu.js +139 -0
- package/dist/organisms/Popover.d.ts +33 -0
- package/dist/organisms/Popover.js +58 -0
- package/dist/organisms/Questionnaire.d.ts +74 -0
- package/dist/organisms/Questionnaire.js +235 -0
- package/dist/organisms/Select.d.ts +47 -0
- package/dist/organisms/Select.js +250 -0
- package/dist/organisms/Sheet.d.ts +34 -0
- package/dist/organisms/Sheet.js +107 -0
- package/dist/organisms/Sidebar.d.ts +100 -0
- package/dist/organisms/Sidebar.js +228 -0
- package/dist/organisms/Toaster.d.ts +67 -0
- package/dist/organisms/Toaster.js +189 -0
- package/dist/organisms/Tooltip.d.ts +39 -0
- package/dist/organisms/Tooltip.js +94 -0
- package/dist/organisms/index.d.ts +40 -0
- package/dist/organisms/index.js +40 -0
- package/dist/primitives/controllable.d.ts +38 -0
- package/dist/primitives/controllable.js +43 -0
- package/dist/primitives/dismissable.d.ts +53 -0
- package/dist/primitives/dismissable.js +158 -0
- package/dist/primitives/floating.d.ts +116 -0
- package/dist/primitives/floating.js +289 -0
- package/dist/primitives/floatingSurface.d.ts +56 -0
- package/dist/primitives/floatingSurface.js +142 -0
- package/dist/primitives/focusTrap.d.ts +47 -0
- package/dist/primitives/focusTrap.js +101 -0
- package/dist/primitives/focusable.d.ts +53 -0
- package/dist/primitives/focusable.js +109 -0
- package/dist/primitives/index.d.ts +27 -0
- package/dist/primitives/index.js +27 -0
- package/dist/primitives/menu.d.ts +102 -0
- package/dist/primitives/menu.js +356 -0
- package/dist/primitives/modalSurface.d.ts +54 -0
- package/dist/primitives/modalSurface.js +146 -0
- package/dist/primitives/portal.d.ts +34 -0
- package/dist/primitives/portal.js +42 -0
- package/dist/primitives/presence.d.ts +45 -0
- package/dist/primitives/presence.js +152 -0
- package/dist/primitives/rovingFocus.d.ts +47 -0
- package/dist/primitives/rovingFocus.js +176 -0
- package/dist/primitives/scrollLock.d.ts +21 -0
- package/dist/primitives/scrollLock.js +75 -0
- package/dist/primitives/typeahead.d.ts +39 -0
- package/dist/primitives/typeahead.js +123 -0
- package/dist/templates/AppShell.d.ts +30 -0
- package/dist/templates/AppShell.js +42 -0
- package/dist/templates/AuthLayout.d.ts +23 -0
- package/dist/templates/AuthLayout.js +42 -0
- package/dist/templates/SettingsLayout.d.ts +27 -0
- package/dist/templates/SettingsLayout.js +42 -0
- package/dist/templates/index.d.ts +16 -0
- package/dist/templates/index.js +16 -0
- package/nebula.css +2 -0
- package/package.json +92 -0
- package/registry.json +1107 -0
- package/src/adapters/css.ts +75 -0
- package/src/adapters/index.ts +40 -0
- package/src/adapters/tailwind.ts +134 -0
- package/src/adapters/types.ts +53 -0
- package/src/adapters/unocss.ts +116 -0
- package/src/atoms/AspectRatio.ts +28 -0
- package/src/atoms/Avatar.ts +67 -0
- package/src/atoms/Badge.ts +46 -0
- package/src/atoms/Button.ts +96 -0
- package/src/atoms/Checkbox.ts +76 -0
- package/src/atoms/Input.ts +76 -0
- package/src/atoms/Kbd.ts +28 -0
- package/src/atoms/Label.ts +38 -0
- package/src/atoms/Marker.ts +79 -0
- package/src/atoms/NativeSelect.ts +115 -0
- package/src/atoms/Progress.ts +64 -0
- package/src/atoms/ScrollArea.ts +53 -0
- package/src/atoms/Separator.ts +36 -0
- package/src/atoms/Skeleton.ts +24 -0
- package/src/atoms/Slider.ts +65 -0
- package/src/atoms/Spinner.ts +28 -0
- package/src/atoms/Switch.ts +56 -0
- package/src/atoms/Textarea.ts +58 -0
- package/src/atoms/Toggle.ts +67 -0
- package/src/atoms/index.ts +62 -0
- package/src/cli/add.ts +126 -0
- package/src/cli/index.ts +22 -0
- package/src/cli/registry.ts +116 -0
- package/src/config.ts +102 -0
- package/src/configure.ts +127 -0
- package/src/index.ts +53 -0
- package/src/lib/children.ts +43 -0
- package/src/lib/cn.ts +14 -0
- package/src/lib/cva.ts +249 -0
- package/src/lib/icons.ts +138 -0
- package/src/lib/id.ts +44 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/motion.ts +48 -0
- package/src/lib/props.ts +83 -0
- package/src/lib/styled.ts +60 -0
- package/src/molecules/Accordion.ts +161 -0
- package/src/molecules/Alert.ts +58 -0
- package/src/molecules/Attachment.ts +132 -0
- package/src/molecules/Breadcrumb.ts +107 -0
- package/src/molecules/Bubble.ts +134 -0
- package/src/molecules/ButtonGroup.ts +55 -0
- package/src/molecules/Card.ts +42 -0
- package/src/molecules/Collapsible.ts +89 -0
- package/src/molecules/Empty.ts +43 -0
- package/src/molecules/Field.ts +131 -0
- package/src/molecules/InputGroup.ts +62 -0
- package/src/molecules/InputOTP.ts +166 -0
- package/src/molecules/Item.ts +89 -0
- package/src/molecules/Message.ts +127 -0
- package/src/molecules/Pagination.ts +197 -0
- package/src/molecules/RadioGroup.ts +104 -0
- package/src/molecules/Resizable.ts +166 -0
- package/src/molecules/Table.ts +118 -0
- package/src/molecules/Tabs.ts +148 -0
- package/src/molecules/ToggleGroup.ts +109 -0
- package/src/molecules/Typography.ts +94 -0
- package/src/molecules/index.ts +144 -0
- package/src/organisms/AlertDialog.ts +133 -0
- package/src/organisms/Calendar.ts +470 -0
- package/src/organisms/Carousel.ts +161 -0
- package/src/organisms/Chart.ts +257 -0
- package/src/organisms/Combobox.ts +140 -0
- package/src/organisms/Command.ts +211 -0
- package/src/organisms/CommandDialog.ts +149 -0
- package/src/organisms/ContextMenu.ts +125 -0
- package/src/organisms/DataTable.ts +328 -0
- package/src/organisms/DatePicker.ts +141 -0
- package/src/organisms/DateRangePicker.ts +149 -0
- package/src/organisms/Dialog.ts +162 -0
- package/src/organisms/Drawer.ts +167 -0
- package/src/organisms/DropdownMenu.ts +112 -0
- package/src/organisms/Form.ts +217 -0
- package/src/organisms/HoverCard.ts +92 -0
- package/src/organisms/Menubar.ts +167 -0
- package/src/organisms/MessageScroller.ts +145 -0
- package/src/organisms/NavigationMenu.ts +184 -0
- package/src/organisms/Popover.ts +84 -0
- package/src/organisms/Questionnaire.ts +322 -0
- package/src/organisms/Select.ts +286 -0
- package/src/organisms/Sheet.ts +144 -0
- package/src/organisms/Sidebar.ts +336 -0
- package/src/organisms/Toaster.ts +246 -0
- package/src/organisms/Tooltip.ts +124 -0
- package/src/organisms/index.ts +134 -0
- package/src/primitives/controllable.ts +64 -0
- package/src/primitives/dismissable.ts +195 -0
- package/src/primitives/floating.ts +453 -0
- package/src/primitives/floatingSurface.ts +209 -0
- package/src/primitives/focusTrap.ts +132 -0
- package/src/primitives/focusable.ts +108 -0
- package/src/primitives/index.ts +88 -0
- package/src/primitives/menu.ts +491 -0
- package/src/primitives/modalSurface.ts +199 -0
- package/src/primitives/portal.ts +61 -0
- package/src/primitives/presence.ts +185 -0
- package/src/primitives/rovingFocus.ts +212 -0
- package/src/primitives/scrollLock.ts +79 -0
- package/src/primitives/typeahead.ts +156 -0
- package/src/templates/AppShell.ts +56 -0
- package/src/templates/AuthLayout.ts +66 -0
- package/src/templates/SettingsLayout.ts +63 -0
- package/src/templates/index.ts +21 -0
- package/theme.css +151 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DatePicker — a Calendar in a popover, behind a button.
|
|
3
|
+
*
|
|
4
|
+
* shadcn composes this out of Popover, Button and Calendar rather than
|
|
5
|
+
* shipping it; nebula ships it, because the composition has three details that
|
|
6
|
+
* are easy to get wrong and pointless to rediscover per project.
|
|
7
|
+
*
|
|
8
|
+
* The popover closes on selection — a date picker that stays open after a
|
|
9
|
+
* choice leaves the user hunting for the way out. Focus returns to the
|
|
10
|
+
* trigger, so the tab order does not restart at the top of the page. And the
|
|
11
|
+
* trigger reads the chosen date through `Intl.DateTimeFormat`, so it renders
|
|
12
|
+
* in the user's locale rather than an American ordering everyone else has to
|
|
13
|
+
* decode.
|
|
14
|
+
*
|
|
15
|
+
* A hidden input carries the value as ISO `YYYY-MM-DD`, which is what a server
|
|
16
|
+
* wants — never the localised string on the button.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
20
|
+
import { buttonVariants } from "../atoms/Button.js";
|
|
21
|
+
import { cn } from "../lib/cn.js";
|
|
22
|
+
import { CalendarIcon } from "../lib/icons.js";
|
|
23
|
+
import { uid } from "../lib/id.js";
|
|
24
|
+
import { zoomInOut } from "../lib/motion.js";
|
|
25
|
+
import { type Reactive, read, readOr } from "../lib/props.js";
|
|
26
|
+
import { controllable } from "../primitives/controllable.js";
|
|
27
|
+
import { floatingSurface } from "../primitives/floatingSurface.js";
|
|
28
|
+
import { focusSilently } from "../primitives/focusable.js";
|
|
29
|
+
import { Calendar, startOfDay } from "./Calendar.js";
|
|
30
|
+
|
|
31
|
+
export interface DatePickerProps {
|
|
32
|
+
name?: string;
|
|
33
|
+
value?: Reactive<Date | undefined>;
|
|
34
|
+
defaultValue?: Date;
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
min?: Date;
|
|
37
|
+
max?: Date;
|
|
38
|
+
disabled?: Reactive<boolean>;
|
|
39
|
+
invalid?: Reactive<boolean>;
|
|
40
|
+
locale?: string;
|
|
41
|
+
class?: Reactive<string>;
|
|
42
|
+
onValueChange?: (date: Date) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `YYYY-MM-DD` from a local date, without a timezone round-trip. */
|
|
46
|
+
export function toISODate(date: Date): string {
|
|
47
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
48
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
49
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const DatePicker = component<DatePickerProps>((props) => {
|
|
53
|
+
const triggerId = uid("date-picker-trigger");
|
|
54
|
+
const contentId = uid("date-picker-content");
|
|
55
|
+
const open = signal(false);
|
|
56
|
+
|
|
57
|
+
const selection = controllable<Date | undefined>({
|
|
58
|
+
value: props.value,
|
|
59
|
+
initial: props.defaultValue,
|
|
60
|
+
onChange: (next) => {
|
|
61
|
+
if (next !== undefined) props.onValueChange?.(next);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const format = new Intl.DateTimeFormat(props.locale, { dateStyle: "medium" });
|
|
66
|
+
|
|
67
|
+
function choose(date: Date): void {
|
|
68
|
+
selection.set(startOfDay(date));
|
|
69
|
+
open(false);
|
|
70
|
+
focusSilently(document.getElementById(triggerId));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
floatingSurface({
|
|
74
|
+
anchor: () => document.getElementById(triggerId),
|
|
75
|
+
open: () => open(),
|
|
76
|
+
onClose: () => open(false),
|
|
77
|
+
placement: "bottom-start",
|
|
78
|
+
offset: 4,
|
|
79
|
+
trapFocus: true,
|
|
80
|
+
// The cursor cell, not the first focusable — landing on the previous-month
|
|
81
|
+
// arrow means the first arrow key pages the calendar instead of moving a
|
|
82
|
+
// day, which is not what a keyboard user reaching a date grid expects.
|
|
83
|
+
initialFocus: (content) => content.querySelector("[data-cursor]"),
|
|
84
|
+
content: () =>
|
|
85
|
+
html`<div
|
|
86
|
+
data-slot="date-picker-content"
|
|
87
|
+
id="${contentId}"
|
|
88
|
+
role="dialog"
|
|
89
|
+
aria-label="Choose a date"
|
|
90
|
+
class="${cn(
|
|
91
|
+
"bg-popover text-popover-foreground z-50 rounded-md border p-0 shadow-md outline-none",
|
|
92
|
+
zoomInOut,
|
|
93
|
+
)}"
|
|
94
|
+
>
|
|
95
|
+
${Calendar({
|
|
96
|
+
value: () => selection.current(),
|
|
97
|
+
defaultMonth: selection.current(),
|
|
98
|
+
min: props.min,
|
|
99
|
+
max: props.max,
|
|
100
|
+
locale: props.locale,
|
|
101
|
+
onValueChange: choose,
|
|
102
|
+
})}
|
|
103
|
+
</div>`,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return html`<div data-slot="date-picker" class="${() => cn("inline-flex", read(props.class))}">
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
id="${triggerId}"
|
|
110
|
+
data-slot="date-picker-trigger"
|
|
111
|
+
aria-haspopup="dialog"
|
|
112
|
+
aria-expanded="${() => (open() ? "true" : "false")}"
|
|
113
|
+
aria-controls="${() => (open() ? contentId : undefined)}"
|
|
114
|
+
aria-invalid="${() => (read(props.invalid) === true ? "true" : undefined)}"
|
|
115
|
+
data-placeholder="${() => (selection.current() === undefined ? "" : undefined)}"
|
|
116
|
+
?disabled="${() => readOr(props.disabled, false)}"
|
|
117
|
+
class="${buttonVariants({
|
|
118
|
+
variant: "outline",
|
|
119
|
+
class:
|
|
120
|
+
"w-full justify-start gap-2 font-normal data-[placeholder]:text-muted-foreground",
|
|
121
|
+
})}"
|
|
122
|
+
@click="${() => open(!open())}"
|
|
123
|
+
>
|
|
124
|
+
${CalendarIcon({ class: "size-4" })}
|
|
125
|
+
<span>${() => {
|
|
126
|
+
const current = selection.current();
|
|
127
|
+
return current === undefined
|
|
128
|
+
? (props.placeholder ?? "Pick a date")
|
|
129
|
+
: format.format(current);
|
|
130
|
+
}}</span>
|
|
131
|
+
</button>
|
|
132
|
+
<input
|
|
133
|
+
type="hidden"
|
|
134
|
+
name="${props.name}"
|
|
135
|
+
.value="${() => {
|
|
136
|
+
const current = selection.current();
|
|
137
|
+
return current === undefined ? "" : toISODate(current);
|
|
138
|
+
}}"
|
|
139
|
+
/>
|
|
140
|
+
</div>`;
|
|
141
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DateRangePicker — a Calendar in range mode, behind a button.
|
|
3
|
+
*
|
|
4
|
+
* The same shell as `DatePicker`, and deliberately so: the two differ in what
|
|
5
|
+
* they collect, not in how they behave. Both close on completion, both return
|
|
6
|
+
* focus to the trigger, both format through `Intl` and both post ISO dates.
|
|
7
|
+
*
|
|
8
|
+
* The one behavioural difference is when to close. A single date is done in
|
|
9
|
+
* one click; a range is not done until the second, so the popover stays open
|
|
10
|
+
* between them. Closing on the first click and reopening for the second is the
|
|
11
|
+
* usual bug here, and it makes the control feel broken.
|
|
12
|
+
*
|
|
13
|
+
* Two hidden inputs rather than one comma-joined field. A server parsing
|
|
14
|
+
* `from`/`to` separately never has to split a string, and an empty `to` is
|
|
15
|
+
* unambiguous.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
19
|
+
import { buttonVariants } from "../atoms/Button.js";
|
|
20
|
+
import { cn } from "../lib/cn.js";
|
|
21
|
+
import { CalendarIcon } from "../lib/icons.js";
|
|
22
|
+
import { uid } from "../lib/id.js";
|
|
23
|
+
import { zoomInOut } from "../lib/motion.js";
|
|
24
|
+
import { type Reactive, read, readOr } from "../lib/props.js";
|
|
25
|
+
import { controllable } from "../primitives/controllable.js";
|
|
26
|
+
import { floatingSurface } from "../primitives/floatingSurface.js";
|
|
27
|
+
import { focusSilently } from "../primitives/focusable.js";
|
|
28
|
+
import { Calendar, type DateRange, startOfDay } from "./Calendar.js";
|
|
29
|
+
import { toISODate } from "./DatePicker.js";
|
|
30
|
+
|
|
31
|
+
export interface DateRangePickerProps {
|
|
32
|
+
/** Base name for the hidden inputs — `"stay"` posts `stay_from` / `stay_to`. */
|
|
33
|
+
name?: string;
|
|
34
|
+
value?: Reactive<DateRange | undefined>;
|
|
35
|
+
defaultValue?: DateRange;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
min?: Date;
|
|
38
|
+
max?: Date;
|
|
39
|
+
disabled?: Reactive<boolean>;
|
|
40
|
+
invalid?: Reactive<boolean>;
|
|
41
|
+
locale?: string;
|
|
42
|
+
class?: Reactive<string>;
|
|
43
|
+
/** Fires on both clicks — `to` is absent after the first. */
|
|
44
|
+
onValueChange?: (range: DateRange) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const DateRangePicker = component<DateRangePickerProps>((props) => {
|
|
48
|
+
const triggerId = uid("date-range-trigger");
|
|
49
|
+
const contentId = uid("date-range-content");
|
|
50
|
+
const open = signal(false);
|
|
51
|
+
|
|
52
|
+
const selection = controllable<DateRange | undefined>({
|
|
53
|
+
value: props.value,
|
|
54
|
+
initial: props.defaultValue,
|
|
55
|
+
onChange: (next) => {
|
|
56
|
+
if (next !== undefined) props.onValueChange?.(next);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const format = new Intl.DateTimeFormat(props.locale, { dateStyle: "medium" });
|
|
61
|
+
|
|
62
|
+
function pick(range: DateRange): void {
|
|
63
|
+
selection.set({
|
|
64
|
+
from: startOfDay(range.from),
|
|
65
|
+
to: range.to === undefined ? undefined : startOfDay(range.to),
|
|
66
|
+
});
|
|
67
|
+
// Only a completed span closes the popover.
|
|
68
|
+
if (range.to === undefined) return;
|
|
69
|
+
open(false);
|
|
70
|
+
focusSilently(document.getElementById(triggerId));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function label(): string {
|
|
74
|
+
const current = selection.current();
|
|
75
|
+
if (current === undefined) return props.placeholder ?? "Pick a date range";
|
|
76
|
+
if (current.to === undefined) return `${format.format(current.from)} — …`;
|
|
77
|
+
return `${format.format(current.from)} — ${format.format(current.to)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
floatingSurface({
|
|
81
|
+
anchor: () => document.getElementById(triggerId),
|
|
82
|
+
open: () => open(),
|
|
83
|
+
onClose: () => open(false),
|
|
84
|
+
placement: "bottom-start",
|
|
85
|
+
offset: 4,
|
|
86
|
+
trapFocus: true,
|
|
87
|
+
initialFocus: (content) => content.querySelector("[data-cursor]"),
|
|
88
|
+
content: () =>
|
|
89
|
+
html`<div
|
|
90
|
+
data-slot="date-range-picker-content"
|
|
91
|
+
id="${contentId}"
|
|
92
|
+
role="dialog"
|
|
93
|
+
aria-label="Choose a date range"
|
|
94
|
+
class="${cn(
|
|
95
|
+
"bg-popover text-popover-foreground z-50 rounded-md border p-0 shadow-md outline-none",
|
|
96
|
+
zoomInOut,
|
|
97
|
+
)}"
|
|
98
|
+
>
|
|
99
|
+
${Calendar({
|
|
100
|
+
mode: "range",
|
|
101
|
+
range: () => selection.current(),
|
|
102
|
+
defaultMonth: selection.current()?.from,
|
|
103
|
+
min: props.min,
|
|
104
|
+
max: props.max,
|
|
105
|
+
locale: props.locale,
|
|
106
|
+
onRangeChange: pick,
|
|
107
|
+
})}
|
|
108
|
+
</div>`,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const isoOf = (date: Date | undefined): string =>
|
|
112
|
+
date === undefined ? "" : toISODate(date);
|
|
113
|
+
|
|
114
|
+
return html`<div
|
|
115
|
+
data-slot="date-range-picker"
|
|
116
|
+
class="${() => cn("inline-flex", read(props.class))}"
|
|
117
|
+
>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
id="${triggerId}"
|
|
121
|
+
data-slot="date-range-picker-trigger"
|
|
122
|
+
aria-haspopup="dialog"
|
|
123
|
+
aria-expanded="${() => (open() ? "true" : "false")}"
|
|
124
|
+
aria-controls="${() => (open() ? contentId : undefined)}"
|
|
125
|
+
aria-invalid="${() => (read(props.invalid) === true ? "true" : undefined)}"
|
|
126
|
+
data-placeholder="${() => (selection.current() === undefined ? "" : undefined)}"
|
|
127
|
+
?disabled="${() => readOr(props.disabled, false)}"
|
|
128
|
+
class="${buttonVariants({
|
|
129
|
+
variant: "outline",
|
|
130
|
+
class:
|
|
131
|
+
"w-full justify-start gap-2 font-normal data-[placeholder]:text-muted-foreground",
|
|
132
|
+
})}"
|
|
133
|
+
@click="${() => open(!open())}"
|
|
134
|
+
>
|
|
135
|
+
${CalendarIcon({ class: "size-4" })}
|
|
136
|
+
<span>${label}</span>
|
|
137
|
+
</button>
|
|
138
|
+
<input
|
|
139
|
+
type="hidden"
|
|
140
|
+
name="${props.name === undefined ? undefined : `${props.name}_from`}"
|
|
141
|
+
.value="${() => isoOf(selection.current()?.from)}"
|
|
142
|
+
/>
|
|
143
|
+
<input
|
|
144
|
+
type="hidden"
|
|
145
|
+
name="${props.name === undefined ? undefined : `${props.name}_to`}"
|
|
146
|
+
.value="${() => isoOf(selection.current()?.to)}"
|
|
147
|
+
/>
|
|
148
|
+
</div>`;
|
|
149
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialog — a modal window over the page.
|
|
3
|
+
*
|
|
4
|
+
* `modalSurface` supplies the contract (trap, scroll lock, backdrop dismissal,
|
|
5
|
+
* hiding the page from assistive technology); this component supplies the
|
|
6
|
+
* markup and the trigger.
|
|
7
|
+
*
|
|
8
|
+
* The `panel` callback matters more than it looks. The overlay's root spans
|
|
9
|
+
* the viewport so the backdrop can, which means containment has to be answered
|
|
10
|
+
* against the panel — check the root and every click on the page counts as
|
|
11
|
+
* inside, and clicking the backdrop silently stops closing the dialog.
|
|
12
|
+
*
|
|
13
|
+
* `aria-labelledby` points at the title and `aria-describedby` at the
|
|
14
|
+
* description, so a screen reader announces what the dialog is for on open. A
|
|
15
|
+
* dialog with neither announces only "dialog", which is why `title` is
|
|
16
|
+
* required rather than optional — `srOnlyTitle` is there for designs that show
|
|
17
|
+
* no visible heading.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
21
|
+
import type { Child } from "../lib/children.js";
|
|
22
|
+
import { type Slot, slot } from "../lib/children.js";
|
|
23
|
+
import { cn } from "../lib/cn.js";
|
|
24
|
+
import { XIcon } from "../lib/icons.js";
|
|
25
|
+
import { uid } from "../lib/id.js";
|
|
26
|
+
import { fadeInOut, zoomInOut } from "../lib/motion.js";
|
|
27
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
28
|
+
import { controllable } from "../primitives/controllable.js";
|
|
29
|
+
import { modalSurface } from "../primitives/modalSurface.js";
|
|
30
|
+
|
|
31
|
+
export const dialogBackdropClasses = "fixed inset-0 z-50 bg-black/50";
|
|
32
|
+
|
|
33
|
+
export const dialogPanelClasses =
|
|
34
|
+
"bg-background fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg outline-none sm:max-w-lg";
|
|
35
|
+
|
|
36
|
+
export interface DialogProps {
|
|
37
|
+
/** Rendered inside the trigger button. Omit to drive `open` yourself. */
|
|
38
|
+
trigger?: Slot;
|
|
39
|
+
/** Announced on open. Hide it visually with `srOnlyTitle`. */
|
|
40
|
+
title: Child;
|
|
41
|
+
description?: Child;
|
|
42
|
+
children?: Slot;
|
|
43
|
+
/** Actions, laid out bottom-right. */
|
|
44
|
+
footer?: Slot;
|
|
45
|
+
open?: Reactive<boolean>;
|
|
46
|
+
defaultOpen?: boolean;
|
|
47
|
+
onOpenChange?: (open: boolean) => void;
|
|
48
|
+
/** Keep the title for screen readers but out of the layout. */
|
|
49
|
+
srOnlyTitle?: boolean;
|
|
50
|
+
/** Hide the corner close button. Escape and the backdrop still work. */
|
|
51
|
+
hideCloseButton?: boolean;
|
|
52
|
+
class?: Reactive<string>;
|
|
53
|
+
triggerClass?: Reactive<string>;
|
|
54
|
+
contentClass?: Reactive<string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Dialog = component<DialogProps>((props) => {
|
|
58
|
+
const triggerId = uid("dialog-trigger");
|
|
59
|
+
const panelId = uid("dialog-panel");
|
|
60
|
+
const titleId = uid("dialog-title");
|
|
61
|
+
const descriptionId = uid("dialog-description");
|
|
62
|
+
|
|
63
|
+
const state = controllable<boolean>({
|
|
64
|
+
value: props.open,
|
|
65
|
+
initial: props.defaultOpen ?? false,
|
|
66
|
+
onChange: props.onOpenChange,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
modalSurface({
|
|
70
|
+
open: () => state.current(),
|
|
71
|
+
onClose: () => state.set(false),
|
|
72
|
+
panel: (root) => root.querySelector(`#${CSS.escape(panelId)}`),
|
|
73
|
+
returnFocus: () => document.getElementById(triggerId),
|
|
74
|
+
content: () =>
|
|
75
|
+
html`<div data-slot="dialog-overlay" class="${cn("fixed inset-0 z-50", fadeInOut)}">
|
|
76
|
+
<div data-slot="dialog-backdrop" class="${dialogBackdropClasses}"></div>
|
|
77
|
+
<div
|
|
78
|
+
data-slot="dialog-content"
|
|
79
|
+
id="${panelId}"
|
|
80
|
+
role="dialog"
|
|
81
|
+
aria-modal="true"
|
|
82
|
+
aria-labelledby="${titleId}"
|
|
83
|
+
aria-describedby="${props.description === undefined ? undefined : descriptionId}"
|
|
84
|
+
tabindex="-1"
|
|
85
|
+
class="${cn(dialogPanelClasses, zoomInOut, read(props.contentClass))}"
|
|
86
|
+
>
|
|
87
|
+
<div data-slot="dialog-header" class="flex flex-col gap-2 text-center sm:text-left">
|
|
88
|
+
<h2
|
|
89
|
+
id="${titleId}"
|
|
90
|
+
data-slot="dialog-title"
|
|
91
|
+
class="${cn(
|
|
92
|
+
"text-lg leading-none font-semibold",
|
|
93
|
+
props.srOnlyTitle === true ? "sr-only" : "",
|
|
94
|
+
)}"
|
|
95
|
+
>${props.title}</h2>
|
|
96
|
+
${
|
|
97
|
+
props.description === undefined
|
|
98
|
+
? null
|
|
99
|
+
: html`<p
|
|
100
|
+
id="${descriptionId}"
|
|
101
|
+
data-slot="dialog-description"
|
|
102
|
+
class="text-muted-foreground text-sm"
|
|
103
|
+
>${props.description}</p>`
|
|
104
|
+
}
|
|
105
|
+
</div>
|
|
106
|
+
${slot(props.children)}
|
|
107
|
+
${
|
|
108
|
+
props.footer === undefined
|
|
109
|
+
? null
|
|
110
|
+
: html`<div
|
|
111
|
+
data-slot="dialog-footer"
|
|
112
|
+
class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
|
|
113
|
+
>${slot(props.footer)}</div>`
|
|
114
|
+
}
|
|
115
|
+
${
|
|
116
|
+
props.hideCloseButton === true
|
|
117
|
+
? null
|
|
118
|
+
: html`<button
|
|
119
|
+
type="button"
|
|
120
|
+
data-slot="dialog-close"
|
|
121
|
+
class="ring-offset-background focus-visible:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
122
|
+
@click="${() => state.set(false)}"
|
|
123
|
+
>
|
|
124
|
+
${XIcon({ class: "size-4" })}
|
|
125
|
+
<span class="sr-only">Close</span>
|
|
126
|
+
</button>`
|
|
127
|
+
}
|
|
128
|
+
</div>
|
|
129
|
+
</div>`,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (props.trigger === undefined) {
|
|
133
|
+
// No trigger: the dialog is driven entirely by the `open` prop. Something
|
|
134
|
+
// still has to be in the tree for the component to mount into, and the
|
|
135
|
+
// surface itself lives in a portal.
|
|
136
|
+
return html`<span data-slot="dialog" hidden></span>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return html`<button
|
|
140
|
+
type="button"
|
|
141
|
+
data-slot="dialog-trigger"
|
|
142
|
+
id="${triggerId}"
|
|
143
|
+
aria-haspopup="dialog"
|
|
144
|
+
aria-expanded="${() => (state.current() ? "true" : "false")}"
|
|
145
|
+
class="${() => cn(read(props.triggerClass))}"
|
|
146
|
+
@click="${() => state.set(true)}"
|
|
147
|
+
>${slot(props.trigger)}</button>`;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/** A dialog driven purely by a signal, with no trigger of its own. */
|
|
151
|
+
export function useDialog(initial = false): {
|
|
152
|
+
open: () => boolean;
|
|
153
|
+
show(): void;
|
|
154
|
+
hide(): void;
|
|
155
|
+
} {
|
|
156
|
+
const open = signal(initial);
|
|
157
|
+
return {
|
|
158
|
+
open: () => open(),
|
|
159
|
+
show: () => open(true),
|
|
160
|
+
hide: () => open(false),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drawer — a bottom sheet you can swipe away.
|
|
3
|
+
*
|
|
4
|
+
* A Sheet from the bottom edge, plus the one gesture that makes it feel native
|
|
5
|
+
* on a phone: drag the panel down to dismiss it.
|
|
6
|
+
*
|
|
7
|
+
* The drag rules are what separate this from a panel that merely follows the
|
|
8
|
+
* finger:
|
|
9
|
+
*
|
|
10
|
+
* - Downward only. Dragging up would peel the panel off its edge and leave a
|
|
11
|
+
* gap under it.
|
|
12
|
+
* - Released past a threshold, it closes; short of it, it springs back. A
|
|
13
|
+
* distance threshold alone punishes a fast flick, so velocity counts too —
|
|
14
|
+
* a quick short drag dismisses, which is what the gesture means.
|
|
15
|
+
* - The transition is disabled during the drag and restored on release.
|
|
16
|
+
* Leaving it on makes the panel lag the finger by its own duration, which
|
|
17
|
+
* reads as a broken gesture rather than a smooth one.
|
|
18
|
+
*
|
|
19
|
+
* The grab handle is decorative. Everything the drag does, the close button,
|
|
20
|
+
* Escape and the backdrop already do — a gesture must never be the only way
|
|
21
|
+
* out of a modal surface.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { component, html } from "@c9up/aurora";
|
|
25
|
+
import type { Child } from "../lib/children.js";
|
|
26
|
+
import { type Slot, slot } from "../lib/children.js";
|
|
27
|
+
import { cn } from "../lib/cn.js";
|
|
28
|
+
import { uid } from "../lib/id.js";
|
|
29
|
+
import { fadeInOut, slideFrom } from "../lib/motion.js";
|
|
30
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
31
|
+
import { controllable } from "../primitives/controllable.js";
|
|
32
|
+
import { modalSurface } from "../primitives/modalSurface.js";
|
|
33
|
+
import { dialogBackdropClasses } from "./Dialog.js";
|
|
34
|
+
|
|
35
|
+
/** Drag this far down and release to dismiss, in px. */
|
|
36
|
+
const DISMISS_DISTANCE = 120;
|
|
37
|
+
/** Or release faster than this, in px per millisecond. */
|
|
38
|
+
const DISMISS_VELOCITY = 0.5;
|
|
39
|
+
|
|
40
|
+
export interface DrawerProps {
|
|
41
|
+
trigger?: Slot;
|
|
42
|
+
title: Child;
|
|
43
|
+
description?: Child;
|
|
44
|
+
children?: Slot;
|
|
45
|
+
footer?: Slot;
|
|
46
|
+
open?: Reactive<boolean>;
|
|
47
|
+
defaultOpen?: boolean;
|
|
48
|
+
onOpenChange?: (open: boolean) => void;
|
|
49
|
+
/** Turn off drag-to-dismiss — for a drawer holding a scrollable list. */
|
|
50
|
+
disableDrag?: boolean;
|
|
51
|
+
triggerClass?: Reactive<string>;
|
|
52
|
+
contentClass?: Reactive<string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Drawer = component<DrawerProps>((props) => {
|
|
56
|
+
const triggerId = uid("drawer-trigger");
|
|
57
|
+
const panelId = uid("drawer-panel");
|
|
58
|
+
const titleId = uid("drawer-title");
|
|
59
|
+
const descriptionId = uid("drawer-description");
|
|
60
|
+
|
|
61
|
+
const state = controllable<boolean>({
|
|
62
|
+
value: props.open,
|
|
63
|
+
initial: props.defaultOpen ?? false,
|
|
64
|
+
onChange: props.onOpenChange,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function onPointerDown(event: PointerEvent): void {
|
|
68
|
+
if (props.disableDrag === true) return;
|
|
69
|
+
const panel = document.getElementById(panelId);
|
|
70
|
+
if (panel === null) return;
|
|
71
|
+
|
|
72
|
+
const startY = event.clientY;
|
|
73
|
+
const startedAt = performance.now();
|
|
74
|
+
let offset = 0;
|
|
75
|
+
panel.setPointerCapture(event.pointerId);
|
|
76
|
+
panel.style.transition = "none";
|
|
77
|
+
|
|
78
|
+
const onMove = (move: PointerEvent): void => {
|
|
79
|
+
offset = Math.max(0, move.clientY - startY);
|
|
80
|
+
panel.style.transform = `translateY(${offset}px)`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const onUp = (): void => {
|
|
84
|
+
panel.releasePointerCapture(event.pointerId);
|
|
85
|
+
panel.removeEventListener("pointermove", onMove);
|
|
86
|
+
panel.removeEventListener("pointerup", onUp);
|
|
87
|
+
panel.removeEventListener("pointercancel", onUp);
|
|
88
|
+
|
|
89
|
+
// Restore the transition before deciding, so both outcomes animate.
|
|
90
|
+
panel.style.transition = "";
|
|
91
|
+
const velocity = offset / Math.max(1, performance.now() - startedAt);
|
|
92
|
+
|
|
93
|
+
if (offset > DISMISS_DISTANCE || velocity > DISMISS_VELOCITY) {
|
|
94
|
+
panel.style.transform = "";
|
|
95
|
+
state.set(false);
|
|
96
|
+
} else {
|
|
97
|
+
panel.style.transform = "";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
panel.addEventListener("pointermove", onMove);
|
|
102
|
+
panel.addEventListener("pointerup", onUp);
|
|
103
|
+
panel.addEventListener("pointercancel", onUp);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
modalSurface({
|
|
107
|
+
open: () => state.current(),
|
|
108
|
+
onClose: () => state.set(false),
|
|
109
|
+
panel: (root) => root.querySelector(`#${CSS.escape(panelId)}`),
|
|
110
|
+
returnFocus: () => document.getElementById(triggerId),
|
|
111
|
+
content: () =>
|
|
112
|
+
html`<div data-slot="drawer-overlay" class="${cn("fixed inset-0 z-50", fadeInOut)}">
|
|
113
|
+
<div class="${dialogBackdropClasses}"></div>
|
|
114
|
+
<div
|
|
115
|
+
data-slot="drawer-content"
|
|
116
|
+
id="${panelId}"
|
|
117
|
+
role="dialog"
|
|
118
|
+
aria-modal="true"
|
|
119
|
+
aria-labelledby="${titleId}"
|
|
120
|
+
aria-describedby="${props.description === undefined ? undefined : descriptionId}"
|
|
121
|
+
tabindex="-1"
|
|
122
|
+
class="${cn(
|
|
123
|
+
"bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[80vh] touch-none flex-col gap-4 rounded-t-lg border-t p-6 shadow-lg outline-none",
|
|
124
|
+
slideFrom("bottom"),
|
|
125
|
+
read(props.contentClass),
|
|
126
|
+
)}"
|
|
127
|
+
@pointerdown="${onPointerDown}"
|
|
128
|
+
>
|
|
129
|
+
<div
|
|
130
|
+
data-slot="drawer-handle"
|
|
131
|
+
aria-hidden="true"
|
|
132
|
+
class="bg-muted mx-auto h-1.5 w-12 shrink-0 rounded-full"
|
|
133
|
+
></div>
|
|
134
|
+
<div class="flex flex-col gap-1.5 text-center sm:text-left">
|
|
135
|
+
<h2 id="${titleId}" class="text-foreground font-semibold">${props.title}</h2>
|
|
136
|
+
${
|
|
137
|
+
props.description === undefined
|
|
138
|
+
? null
|
|
139
|
+
: html`<p id="${descriptionId}" class="text-muted-foreground text-sm">
|
|
140
|
+
${props.description}
|
|
141
|
+
</p>`
|
|
142
|
+
}
|
|
143
|
+
</div>
|
|
144
|
+
<div class="flex-1 overflow-y-auto">${slot(props.children)}</div>
|
|
145
|
+
${
|
|
146
|
+
props.footer === undefined
|
|
147
|
+
? null
|
|
148
|
+
: html`<div class="mt-auto flex flex-col gap-2">${slot(props.footer)}</div>`
|
|
149
|
+
}
|
|
150
|
+
</div>
|
|
151
|
+
</div>`,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (props.trigger === undefined) {
|
|
155
|
+
return html`<span data-slot="drawer" hidden></span>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return html`<button
|
|
159
|
+
type="button"
|
|
160
|
+
data-slot="drawer-trigger"
|
|
161
|
+
id="${triggerId}"
|
|
162
|
+
aria-haspopup="dialog"
|
|
163
|
+
aria-expanded="${() => (state.current() ? "true" : "false")}"
|
|
164
|
+
class="${() => cn(read(props.triggerClass))}"
|
|
165
|
+
@click="${() => state.set(true)}"
|
|
166
|
+
>${slot(props.trigger)}</button>`;
|
|
167
|
+
});
|