@shadow-js/ui 0.0.0-stage → 1.0.1
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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2281 -0
- package/dist/dev/cli/schema.d.ts +71 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +139 -0
- package/dist/dev/locales/ar.js +127 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +139 -0
- package/dist/dev/locales/en.js +127 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +27 -0
- package/dist/dev/registry/utils/utils.js +26 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +185 -0
- package/dist/dev/theme/theme-logic.js +183 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +88 -0
- package/dist/dev/theme/theme-provider.js +149 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { onCleanup, splitProps, useSignal } from "@shadow-js/core";
|
|
2
|
+
import {
|
|
3
|
+
createPartContext,
|
|
4
|
+
type DialogCloseProps,
|
|
5
|
+
type DialogContentProps,
|
|
6
|
+
type DialogDescriptionProps,
|
|
7
|
+
type DialogOverlayProps,
|
|
8
|
+
type DialogPortalProps,
|
|
9
|
+
Dialog as DialogPrimitive,
|
|
10
|
+
type DialogTitleProps,
|
|
11
|
+
type DialogTriggerProps,
|
|
12
|
+
type PolymorphicTarget,
|
|
13
|
+
useControllableState,
|
|
14
|
+
} from "@shadow-js/primitives";
|
|
15
|
+
import type { ComponentProps, JSX } from "@shadow-js/web";
|
|
16
|
+
import { useUiStrings } from "@/lib/ui-strings";
|
|
17
|
+
import { cn } from "@/lib/utils";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The rest positions of a {@link Drawer}, as visible fractions of its height in ascending order:
|
|
21
|
+
* `[0.5, 1]` is a half-open position and a fully open one. After a drag the drawer rests at the nearest
|
|
22
|
+
* position.
|
|
23
|
+
* @see {@link Drawer}
|
|
24
|
+
* @since 1.0.0
|
|
25
|
+
*/
|
|
26
|
+
export type DrawerSnapPoints = readonly number[];
|
|
27
|
+
|
|
28
|
+
/** The nearest snap index to a visible fraction. @internal */
|
|
29
|
+
function nearestSnap(points: DrawerSnapPoints, visible: number): number {
|
|
30
|
+
let best = 0;
|
|
31
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
32
|
+
const candidate = points[index] ?? 0;
|
|
33
|
+
const current = points[best] ?? 0;
|
|
34
|
+
if (Math.abs(candidate - visible) < Math.abs(current - visible)) best = index;
|
|
35
|
+
}
|
|
36
|
+
return best;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The edge a {@link Drawer} slides in from. `bottom` is the default; `start` and `end` follow the
|
|
41
|
+
* reading direction, so they swap sides in a right-to-left page (there is no physical `left` or
|
|
42
|
+
* `right`, on purpose).
|
|
43
|
+
* @since 1.0.0
|
|
44
|
+
*/
|
|
45
|
+
export type DrawerDirection = "bottom" | "top" | "start" | "end";
|
|
46
|
+
|
|
47
|
+
/** The positioning, border, rounding and slide animation of each direction. @internal */
|
|
48
|
+
function directionClasses(direction: DrawerDirection): string {
|
|
49
|
+
switch (direction) {
|
|
50
|
+
case "top":
|
|
51
|
+
return "inset-x-0 top-0 max-h-[96%] rounded-b-xl border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top";
|
|
52
|
+
case "start":
|
|
53
|
+
return "inset-y-0 start-0 h-full w-3/4 max-w-sm rounded-e-xl border-e rtl:[--drawer-dir:-1] data-[state=closed]:ltr:slide-out-to-left data-[state=closed]:rtl:slide-out-to-right data-[state=open]:ltr:slide-in-from-left data-[state=open]:rtl:slide-in-from-right";
|
|
54
|
+
case "end":
|
|
55
|
+
return "inset-y-0 end-0 h-full w-3/4 max-w-sm rounded-s-xl border-s rtl:[--drawer-dir:-1] data-[state=closed]:ltr:slide-out-to-right data-[state=closed]:rtl:slide-out-to-left data-[state=open]:ltr:slide-in-from-right data-[state=open]:rtl:slide-in-from-left";
|
|
56
|
+
default:
|
|
57
|
+
return "inset-x-0 bottom-0 max-h-[96%] rounded-t-xl border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True for the directions that move along the horizontal axis. @internal */
|
|
62
|
+
const isHorizontal = (direction: DrawerDirection): boolean =>
|
|
63
|
+
direction === "start" || direction === "end";
|
|
64
|
+
|
|
65
|
+
/** True when the element is laid out right to left. @internal */
|
|
66
|
+
function isRtl(element: Element | null): boolean {
|
|
67
|
+
if (element === null) return false;
|
|
68
|
+
const computed = element.ownerDocument.defaultView?.getComputedStyle(element).direction;
|
|
69
|
+
return computed === "rtl" || element.closest("[dir]")?.getAttribute("dir") === "rtl";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The sign that turns a pointer movement into "pushed away from the open edge": positive means the
|
|
74
|
+
* sheet is being dismissed. @internal
|
|
75
|
+
*/
|
|
76
|
+
function dismissSign(direction: DrawerDirection, rtl: boolean): number {
|
|
77
|
+
if (direction === "bottom") return 1;
|
|
78
|
+
if (direction === "top") return -1;
|
|
79
|
+
// end: the right edge in ltr, so toward the right dismisses; start is the mirror.
|
|
80
|
+
const toEnd = direction === "end" ? 1 : -1;
|
|
81
|
+
return rtl ? -toEnd : toEnd;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What the drawer shares with its parts. @internal */
|
|
85
|
+
interface DrawerContextValue {
|
|
86
|
+
readonly direction: () => DrawerDirection;
|
|
87
|
+
readonly isOpen: () => boolean;
|
|
88
|
+
readonly close: () => void;
|
|
89
|
+
readonly snapPoints: () => DrawerSnapPoints;
|
|
90
|
+
readonly snap: () => number;
|
|
91
|
+
readonly setSnap: (index: number) => void;
|
|
92
|
+
readonly dismissible: () => boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const drawerContext = createPartContext<DrawerContextValue>("Drawer");
|
|
96
|
+
|
|
97
|
+
/** What the content shares with its grip. @internal */
|
|
98
|
+
interface DrawerSheetContextValue {
|
|
99
|
+
readonly offset: () => number;
|
|
100
|
+
readonly dragging: () => boolean;
|
|
101
|
+
readonly startDrag: (event: PointerEvent, size: number) => void;
|
|
102
|
+
/** Moves the snap point by `step` (positive opens further). */
|
|
103
|
+
readonly nudge: (step: number) => void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const drawerSheetContext = createPartContext<DrawerSheetContextValue>("DrawerContent");
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Props of {@link Drawer}: the open state of the dialog, and the snap points and dismissal of the sheet.
|
|
110
|
+
* @see {@link Drawer}
|
|
111
|
+
* @since 1.0.0
|
|
112
|
+
*/
|
|
113
|
+
export interface DrawerProps {
|
|
114
|
+
/**
|
|
115
|
+
* Controlled open state. Leave it unset to let the drawer manage its own state.
|
|
116
|
+
* @default none
|
|
117
|
+
*/
|
|
118
|
+
readonly open?: boolean | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* Initial open state when uncontrolled.
|
|
121
|
+
* @default false
|
|
122
|
+
*/
|
|
123
|
+
readonly defaultOpen?: boolean | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Called with the requested open state when the drawer opens or closes.
|
|
126
|
+
* @default none
|
|
127
|
+
*/
|
|
128
|
+
readonly onOpenChange?: ((open: boolean) => void) | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* Whether the drawer is modal: focus is trapped, the page behind is inert and scroll is locked.
|
|
131
|
+
* `false` leaves the page usable beside the sheet and sets no `aria-modal`.
|
|
132
|
+
* @default true
|
|
133
|
+
*/
|
|
134
|
+
readonly modal?: boolean | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* The edge the drawer slides in from: `"bottom"`, `"top"`, `"start"` or `"end"` (the last two follow
|
|
137
|
+
* the reading direction). Dragging the grip toward that edge dismisses the sheet.
|
|
138
|
+
* @default "bottom"
|
|
139
|
+
*/
|
|
140
|
+
readonly direction?: DrawerDirection | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* Rest positions as visible fractions of the drawer's size along its axis (height for `bottom` and
|
|
143
|
+
* `top`, width for `start` and `end`). Without them the drawer is one size.
|
|
144
|
+
* @default none
|
|
145
|
+
*/
|
|
146
|
+
readonly snapPoints?: DrawerSnapPoints | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* Index of the snap point the sheet rests at. Setting it controls the snap point.
|
|
149
|
+
* @default none
|
|
150
|
+
*/
|
|
151
|
+
readonly snap?: number | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Index of the snap point the drawer opens at when uncontrolled.
|
|
154
|
+
* @default 0
|
|
155
|
+
*/
|
|
156
|
+
readonly defaultSnap?: number | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* Called with the new snap index when a drag or the arrow keys change it.
|
|
159
|
+
* @default none
|
|
160
|
+
*/
|
|
161
|
+
readonly onSnapChange?: ((index: number) => void) | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* Whether dragging the sheet down far enough closes it. Without snap points, a drag of more than a
|
|
164
|
+
* quarter of the sheet's height closes it. With snap points, a drag that leaves less than half of the
|
|
165
|
+
* lowest snap point visible closes it.
|
|
166
|
+
* @default true
|
|
167
|
+
*/
|
|
168
|
+
readonly dismissible?: boolean | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* The {@link DrawerTrigger}, {@link DrawerContent} and any other parts.
|
|
171
|
+
* @default none
|
|
172
|
+
*/
|
|
173
|
+
readonly children?: JSX.Element | readonly JSX.Element[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A panel that slides up from the bottom of the screen: a mobile sheet with a grip. Use it for a
|
|
178
|
+
* secondary task on a phone, such as filters or a short form. It is a modal dialog: focus is trapped,
|
|
179
|
+
* the page behind is inert and scroll-locked, and Escape closes it. Drag the grip down to dismiss the
|
|
180
|
+
* sheet, or to move it between snap points when `snapPoints` is set.
|
|
181
|
+
*
|
|
182
|
+
* Keyboard and ARIA: Enter or Space on the trigger opens the drawer, and Escape or a press on the
|
|
183
|
+
* overlay closes it, which returns focus to the trigger. The grip is a focusable `role="separator"`
|
|
184
|
+
* labeled "Drag to resize". With snap points it has `aria-valuemin`, `aria-valuemax` and
|
|
185
|
+
* `aria-valuenow` (the snap index), and ArrowUp and ArrowDown move the sheet to a taller or a shorter
|
|
186
|
+
* snap point. Without snap points the grip only announces that it is a separator, and the arrow keys
|
|
187
|
+
* do nothing. The content is named by {@link DrawerTitle} and described by {@link DrawerDescription}.
|
|
188
|
+
*
|
|
189
|
+
* shadcn/ui differences: the drawer is a dialog with snap points built in (`snapPoints`, `snap`,
|
|
190
|
+
* `defaultSnap`, `onSnapChange`), not a separate sheet library. `as` replaces `asChild` on the trigger
|
|
191
|
+
* and the close button. The prop is `class`, not `className`, and every part has a `data-slot`.
|
|
192
|
+
* `direction` (`bottom`, `top`, `start`, `end`) replaces vaul's `direction` with logical sides, `modal={false}`
|
|
193
|
+
* keeps the page usable, `dismissible={false}` is the way to stop a swipe or press outside from closing it, and
|
|
194
|
+
* the content takes the dialog's `onOpenAutoFocus` to choose the first focus.
|
|
195
|
+
*
|
|
196
|
+
* @param props The drawer props: see {@link DrawerProps}.
|
|
197
|
+
* @returns The drawer. It renders no element of its own.
|
|
198
|
+
* @example
|
|
199
|
+
* import {
|
|
200
|
+
* Drawer,
|
|
201
|
+
* DrawerContent,
|
|
202
|
+
* DrawerDescription,
|
|
203
|
+
* DrawerHandle,
|
|
204
|
+
* DrawerHeader,
|
|
205
|
+
* DrawerTitle,
|
|
206
|
+
* DrawerTrigger,
|
|
207
|
+
* } from "@/components/ui/drawer";
|
|
208
|
+
*
|
|
209
|
+
* export function Filters() {
|
|
210
|
+
* return (
|
|
211
|
+
* <Drawer snapPoints={[0.5, 1]}>
|
|
212
|
+
* <DrawerTrigger>Filters</DrawerTrigger>
|
|
213
|
+
* <DrawerContent>
|
|
214
|
+
* <DrawerHandle />
|
|
215
|
+
* <DrawerHeader>
|
|
216
|
+
* <DrawerTitle>Filters</DrawerTitle>
|
|
217
|
+
* <DrawerDescription>Narrow the list.</DrawerDescription>
|
|
218
|
+
* </DrawerHeader>
|
|
219
|
+
* </DrawerContent>
|
|
220
|
+
* </Drawer>
|
|
221
|
+
* );
|
|
222
|
+
* }
|
|
223
|
+
* @see {@link DrawerContent}
|
|
224
|
+
* @see {@link DrawerHandle}
|
|
225
|
+
* @since 1.0.0
|
|
226
|
+
*/
|
|
227
|
+
function Drawer(props: DrawerProps): JSX.Element {
|
|
228
|
+
const [local, rest] = splitProps(props, [
|
|
229
|
+
"open",
|
|
230
|
+
"defaultOpen",
|
|
231
|
+
"onOpenChange",
|
|
232
|
+
"modal",
|
|
233
|
+
"direction",
|
|
234
|
+
"snapPoints",
|
|
235
|
+
"snap",
|
|
236
|
+
"defaultSnap",
|
|
237
|
+
"onSnapChange",
|
|
238
|
+
"dismissible",
|
|
239
|
+
"children",
|
|
240
|
+
]);
|
|
241
|
+
const [open, setOpen] = useControllableState<boolean>({
|
|
242
|
+
value: () => local.open,
|
|
243
|
+
defaultValue: () => local.defaultOpen === true,
|
|
244
|
+
onChange: () => {
|
|
245
|
+
const callback = local.onOpenChange;
|
|
246
|
+
return callback === undefined ? undefined : (next) => callback(next === true);
|
|
247
|
+
},
|
|
248
|
+
name: "Drawer open",
|
|
249
|
+
});
|
|
250
|
+
const [snap, setSnap] = useControllableState<number>({
|
|
251
|
+
value: () => local.snap,
|
|
252
|
+
defaultValue: () => local.defaultSnap ?? 0,
|
|
253
|
+
onChange: () => {
|
|
254
|
+
const callback = local.onSnapChange;
|
|
255
|
+
return callback === undefined ? undefined : (next) => callback(next ?? 0);
|
|
256
|
+
},
|
|
257
|
+
name: "Drawer snap",
|
|
258
|
+
});
|
|
259
|
+
const context: DrawerContextValue = {
|
|
260
|
+
direction: () => local.direction ?? "bottom",
|
|
261
|
+
isOpen: () => open() === true,
|
|
262
|
+
close: () => setOpen(false),
|
|
263
|
+
snapPoints: () => local.snapPoints ?? [],
|
|
264
|
+
snap: () => snap() ?? 0,
|
|
265
|
+
setSnap: (index) => setSnap(index),
|
|
266
|
+
dismissible: () => local.dismissible !== false,
|
|
267
|
+
};
|
|
268
|
+
return (
|
|
269
|
+
<drawerContext.Provider value={context}>
|
|
270
|
+
<DialogPrimitive
|
|
271
|
+
open={open()}
|
|
272
|
+
onOpenChange={(next: boolean) => setOpen(next)}
|
|
273
|
+
modal={local.modal}
|
|
274
|
+
{...rest}
|
|
275
|
+
>
|
|
276
|
+
{local.children as JSX.Element}
|
|
277
|
+
</DialogPrimitive>
|
|
278
|
+
</drawerContext.Provider>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The button that opens the drawer. It is a native button; `as` renders another element.
|
|
284
|
+
*
|
|
285
|
+
* @param props The trigger props: the attributes of the rendered element, and `as`.
|
|
286
|
+
* @returns The trigger.
|
|
287
|
+
* @throws Error when rendered outside a {@link Drawer}.
|
|
288
|
+
* @example
|
|
289
|
+
* import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
|
290
|
+
*
|
|
291
|
+
* export function OpenSheet() {
|
|
292
|
+
* return (
|
|
293
|
+
* <Drawer>
|
|
294
|
+
* <DrawerTrigger>Open</DrawerTrigger>
|
|
295
|
+
* <DrawerContent>Sheet body.</DrawerContent>
|
|
296
|
+
* </Drawer>
|
|
297
|
+
* );
|
|
298
|
+
* }
|
|
299
|
+
* @see {@link Drawer}
|
|
300
|
+
* @since 1.0.0
|
|
301
|
+
*/
|
|
302
|
+
function DrawerTrigger<T extends PolymorphicTarget = "button">(
|
|
303
|
+
props: DialogTriggerProps<T>,
|
|
304
|
+
): JSX.Element {
|
|
305
|
+
return <DialogPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* A button that closes the drawer. It is a native button; `as` renders another element.
|
|
310
|
+
*
|
|
311
|
+
* @param props The close props: the attributes of the rendered element, and `as`.
|
|
312
|
+
* @returns The close button.
|
|
313
|
+
* @throws Error when rendered outside a {@link Drawer}.
|
|
314
|
+
* @example
|
|
315
|
+
* import { Drawer, DrawerClose, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
|
316
|
+
*
|
|
317
|
+
* export function CancellableSheet() {
|
|
318
|
+
* return (
|
|
319
|
+
* <Drawer>
|
|
320
|
+
* <DrawerTrigger>Open</DrawerTrigger>
|
|
321
|
+
* <DrawerContent>
|
|
322
|
+
* <DrawerClose>Cancel</DrawerClose>
|
|
323
|
+
* </DrawerContent>
|
|
324
|
+
* </Drawer>
|
|
325
|
+
* );
|
|
326
|
+
* }
|
|
327
|
+
* @see {@link Drawer}
|
|
328
|
+
* @since 1.0.0
|
|
329
|
+
*/
|
|
330
|
+
function DrawerClose<T extends PolymorphicTarget = "button">(
|
|
331
|
+
props: DialogCloseProps<T>,
|
|
332
|
+
): JSX.Element {
|
|
333
|
+
return <DialogPrimitive.Close data-slot="drawer-close" {...props} />;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Renders the overlay and the sheet into `document.body`, or into the `container` you pass, while the
|
|
338
|
+
* drawer is open. {@link DrawerContent} already portals itself, so use this only to choose another
|
|
339
|
+
* container.
|
|
340
|
+
*
|
|
341
|
+
* @param props The portal props: `container` and `forceMount`.
|
|
342
|
+
* @returns The portal, or nothing while the drawer is closed.
|
|
343
|
+
* @throws Error when rendered outside a {@link Drawer}.
|
|
344
|
+
* @example
|
|
345
|
+
* import { Drawer, DrawerContent, DrawerOverlay, DrawerPortal, DrawerTrigger } from "@/components/ui/drawer";
|
|
346
|
+
*
|
|
347
|
+
* export function PortaledSheet() {
|
|
348
|
+
* return (
|
|
349
|
+
* <Drawer>
|
|
350
|
+
* <DrawerTrigger>Open</DrawerTrigger>
|
|
351
|
+
* <DrawerPortal>
|
|
352
|
+
* <DrawerOverlay />
|
|
353
|
+
* <DrawerContent>Sheet body.</DrawerContent>
|
|
354
|
+
* </DrawerPortal>
|
|
355
|
+
* </Drawer>
|
|
356
|
+
* );
|
|
357
|
+
* }
|
|
358
|
+
* @see {@link DrawerContent}
|
|
359
|
+
* @since 1.0.0
|
|
360
|
+
*/
|
|
361
|
+
function DrawerPortal(props: DialogPortalProps): JSX.Element {
|
|
362
|
+
return <DialogPrimitive.Portal {...props} />;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The dimmed backdrop behind the drawer. A press on it closes the drawer. {@link DrawerContent} renders
|
|
367
|
+
* one already.
|
|
368
|
+
*
|
|
369
|
+
* @param props The overlay props: `class`, which is merged with the backdrop classes.
|
|
370
|
+
* @returns The overlay.
|
|
371
|
+
* @throws Error when rendered outside a {@link Drawer}.
|
|
372
|
+
* @example
|
|
373
|
+
* import { Drawer, DrawerContent, DrawerOverlay, DrawerPortal, DrawerTrigger } from "@/components/ui/drawer";
|
|
374
|
+
*
|
|
375
|
+
* export function DimmedSheet() {
|
|
376
|
+
* return (
|
|
377
|
+
* <Drawer>
|
|
378
|
+
* <DrawerTrigger>Open</DrawerTrigger>
|
|
379
|
+
* <DrawerPortal>
|
|
380
|
+
* <DrawerOverlay class="bg-black/70" />
|
|
381
|
+
* <DrawerContent>Sheet body.</DrawerContent>
|
|
382
|
+
* </DrawerPortal>
|
|
383
|
+
* </Drawer>
|
|
384
|
+
* );
|
|
385
|
+
* }
|
|
386
|
+
* @see {@link DrawerContent}
|
|
387
|
+
* @since 1.0.0
|
|
388
|
+
*/
|
|
389
|
+
function DrawerOverlay(props: DialogOverlayProps): JSX.Element {
|
|
390
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
391
|
+
return (
|
|
392
|
+
<DialogPrimitive.Overlay
|
|
393
|
+
data-slot="drawer-overlay"
|
|
394
|
+
class={cn(
|
|
395
|
+
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 motion-reduce:animate-none",
|
|
396
|
+
local.class,
|
|
397
|
+
)}
|
|
398
|
+
{...rest}
|
|
399
|
+
/>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The drawer panel: a sheet at the bottom of the screen, with the overlay. While the grip is dragged the
|
|
405
|
+
* sheet follows the pointer; on release it rests at the nearest snap point, or closes when it was
|
|
406
|
+
* dragged far enough (see {@link DrawerProps.dismissible}). `class` is merged with its own classes.
|
|
407
|
+
*
|
|
408
|
+
* @param props The panel props: the attributes of the rendered `div`, and `class`.
|
|
409
|
+
* @returns The panel, rendered in a portal while the drawer is open.
|
|
410
|
+
* @throws Error when rendered outside a {@link Drawer}.
|
|
411
|
+
* @example
|
|
412
|
+
* import { Drawer, DrawerContent, DrawerHandle, DrawerTrigger } from "@/components/ui/drawer";
|
|
413
|
+
*
|
|
414
|
+
* export function Sheet() {
|
|
415
|
+
* return (
|
|
416
|
+
* <Drawer>
|
|
417
|
+
* <DrawerTrigger>Open</DrawerTrigger>
|
|
418
|
+
* <DrawerContent class="max-h-[80vh]">
|
|
419
|
+
* <DrawerHandle />
|
|
420
|
+
* <p>Sheet body.</p>
|
|
421
|
+
* </DrawerContent>
|
|
422
|
+
* </Drawer>
|
|
423
|
+
* );
|
|
424
|
+
* }
|
|
425
|
+
* @see {@link Drawer}
|
|
426
|
+
* @see {@link DrawerHandle}
|
|
427
|
+
* @since 1.0.0
|
|
428
|
+
*/
|
|
429
|
+
function DrawerContent(props: DialogContentProps): JSX.Element {
|
|
430
|
+
const drawer = drawerContext.use("DrawerContent");
|
|
431
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
432
|
+
// The offset is how far the sheet is pushed away from its edge, in pixels: 0 is the snap point it rests at.
|
|
433
|
+
const [dragOffset, setDragOffset] = useSignal<number | undefined>(undefined);
|
|
434
|
+
let stopDrag: (() => void) | undefined;
|
|
435
|
+
onCleanup(() => stopDrag?.());
|
|
436
|
+
|
|
437
|
+
const restOffset = (size: number): number => {
|
|
438
|
+
const points = drawer.snapPoints();
|
|
439
|
+
if (points.length === 0) return 0;
|
|
440
|
+
return (1 - (points[drawer.snap()] ?? 1)) * size;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Decides where a release leaves the sheet: a snap point, or closed when dragged far enough.
|
|
444
|
+
const settle = (offset: number, size: number): void => {
|
|
445
|
+
const points = drawer.snapPoints();
|
|
446
|
+
const visible = size > 0 ? 1 - offset / size : 1;
|
|
447
|
+
setDragOffset(undefined);
|
|
448
|
+
if (points.length === 0) {
|
|
449
|
+
if (drawer.dismissible() && offset > size * 0.25) drawer.close();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const lowest = points[0] ?? 0;
|
|
453
|
+
if (drawer.dismissible() && visible < lowest * 0.5) {
|
|
454
|
+
drawer.close();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
drawer.setSnap(nearestSnap(points, visible));
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Where the sheet is while dragged: pushed away from its edge by `offset` pixels, in screen terms.
|
|
461
|
+
const dragTransform = (offset: number, rtl: boolean): string => {
|
|
462
|
+
const direction = drawer.direction();
|
|
463
|
+
const signed = offset * dismissSign(direction, rtl);
|
|
464
|
+
return isHorizontal(direction)
|
|
465
|
+
? `translate3d(${signed}px, 0, 0)`
|
|
466
|
+
: `translate3d(0, ${signed}px, 0)`;
|
|
467
|
+
};
|
|
468
|
+
// The reading direction found when the drag started; the sheet's own classes mirror the rest.
|
|
469
|
+
let dragRtl = false;
|
|
470
|
+
|
|
471
|
+
const sheet: DrawerSheetContextValue = {
|
|
472
|
+
offset: () => dragOffset() ?? 0,
|
|
473
|
+
dragging: () => dragOffset() !== undefined,
|
|
474
|
+
startDrag(event: PointerEvent, size: number): void {
|
|
475
|
+
if (event.button !== 0) return;
|
|
476
|
+
stopDrag?.();
|
|
477
|
+
event.preventDefault();
|
|
478
|
+
const direction = drawer.direction();
|
|
479
|
+
const horizontal = isHorizontal(direction);
|
|
480
|
+
dragRtl = isRtl(
|
|
481
|
+
(event.currentTarget as Element | null)?.closest('[data-slot="drawer-content"]') ?? null,
|
|
482
|
+
);
|
|
483
|
+
const sign = dismissSign(direction, dragRtl);
|
|
484
|
+
const origin = horizontal ? event.clientX : event.clientY;
|
|
485
|
+
const base = dragOffset() ?? restOffset(size);
|
|
486
|
+
let current = base;
|
|
487
|
+
setDragOffset(base);
|
|
488
|
+
// Only the pointer that pressed the grip moves the sheet; another finger is not part of this drag.
|
|
489
|
+
const pointer = event.pointerId;
|
|
490
|
+
const move = (moveEvent: PointerEvent): void => {
|
|
491
|
+
if (moveEvent.pointerId !== pointer) return;
|
|
492
|
+
const at = horizontal ? moveEvent.clientX : moveEvent.clientY;
|
|
493
|
+
current = Math.max(0, base + (at - origin) * sign);
|
|
494
|
+
setDragOffset(current);
|
|
495
|
+
};
|
|
496
|
+
const end = (finished: PointerEvent): void => {
|
|
497
|
+
if (finished.pointerId !== pointer) return;
|
|
498
|
+
stopDrag?.();
|
|
499
|
+
settle(current, size);
|
|
500
|
+
};
|
|
501
|
+
window.addEventListener("pointermove", move);
|
|
502
|
+
window.addEventListener("pointerup", end);
|
|
503
|
+
window.addEventListener("pointercancel", end);
|
|
504
|
+
stopDrag = () => {
|
|
505
|
+
window.removeEventListener("pointermove", move);
|
|
506
|
+
window.removeEventListener("pointerup", end);
|
|
507
|
+
window.removeEventListener("pointercancel", end);
|
|
508
|
+
stopDrag = undefined;
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
nudge(step: number): void {
|
|
512
|
+
const points = drawer.snapPoints();
|
|
513
|
+
if (points.length === 0) return;
|
|
514
|
+
const next = Math.min(points.length - 1, Math.max(0, drawer.snap() + step));
|
|
515
|
+
drawer.setSnap(next);
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// The rest position is a percentage of the sheet's own size along its axis, so it needs no
|
|
520
|
+
// measurement: a half-open sheet is pushed away by half its size and the fully open one is not
|
|
521
|
+
// moved. Horizontal sheets mirror it with `--drawer-dir` (1, or -1 in a right-to-left page).
|
|
522
|
+
const restPercent = (): number => (1 - (drawer.snapPoints()[drawer.snap()] ?? 1)) * 100;
|
|
523
|
+
const restTransform = (): string => {
|
|
524
|
+
const direction = drawer.direction();
|
|
525
|
+
const percent = restPercent();
|
|
526
|
+
if (direction === "bottom") return `translate3d(0, ${percent}%, 0)`;
|
|
527
|
+
if (direction === "top") return `translate3d(0, ${-percent}%, 0)`;
|
|
528
|
+
const toEnd = direction === "end" ? percent : -percent;
|
|
529
|
+
return `translate3d(calc(var(--drawer-dir, 1) * ${toEnd}%), 0, 0)`;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// Direct attributes (not a spread): the style and the class follow the drag as it happens. At
|
|
533
|
+
// rest with snap points the sheet sits at its snap point, and the release animates it there.
|
|
534
|
+
return (
|
|
535
|
+
<DrawerPortal>
|
|
536
|
+
<DrawerOverlay />
|
|
537
|
+
<DialogPrimitive.Content
|
|
538
|
+
data-slot="drawer-content"
|
|
539
|
+
class={cn(
|
|
540
|
+
"fixed z-50 flex flex-col bg-background shadow-lg outline-none transition-transform duration-300 ease-out motion-reduce:transition-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
541
|
+
directionClasses(drawer.direction()),
|
|
542
|
+
local.class,
|
|
543
|
+
)}
|
|
544
|
+
style={
|
|
545
|
+
dragOffset() !== undefined
|
|
546
|
+
? { transform: dragTransform(dragOffset() ?? 0, dragRtl), transition: "none" }
|
|
547
|
+
: drawer.snapPoints().length > 0
|
|
548
|
+
? { transform: restTransform() }
|
|
549
|
+
: undefined
|
|
550
|
+
}
|
|
551
|
+
{...rest}
|
|
552
|
+
>
|
|
553
|
+
<drawerSheetContext.Provider value={sheet}>
|
|
554
|
+
{local.children as JSX.Element}
|
|
555
|
+
</drawerSheetContext.Provider>
|
|
556
|
+
</DialogPrimitive.Content>
|
|
557
|
+
</DrawerPortal>
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The grip at the top of the drawer. Drag it to move the sheet. With snap points, ArrowUp moves to a
|
|
563
|
+
* taller snap point and ArrowDown to a shorter one. It is a focusable `role="separator"` labeled "Drag
|
|
564
|
+
* to resize", and with snap points it announces the snap index as `aria-valuenow`.
|
|
565
|
+
*
|
|
566
|
+
* @param props The grip props: the attributes of the rendered `div`, and `class`.
|
|
567
|
+
* @returns The grip.
|
|
568
|
+
* @throws Error when rendered outside a {@link Drawer}.
|
|
569
|
+
* @example
|
|
570
|
+
* import { Drawer, DrawerContent, DrawerHandle, DrawerTrigger } from "@/components/ui/drawer";
|
|
571
|
+
*
|
|
572
|
+
* export function Grip() {
|
|
573
|
+
* return (
|
|
574
|
+
* <Drawer snapPoints={[0.5, 1]}>
|
|
575
|
+
* <DrawerTrigger>Open</DrawerTrigger>
|
|
576
|
+
* <DrawerContent>
|
|
577
|
+
* <DrawerHandle />
|
|
578
|
+
* </DrawerContent>
|
|
579
|
+
* </Drawer>
|
|
580
|
+
* );
|
|
581
|
+
* }
|
|
582
|
+
* @see {@link DrawerContent}
|
|
583
|
+
* @since 1.0.0
|
|
584
|
+
*/
|
|
585
|
+
function DrawerHandle(props: ComponentProps<"div">): JSX.Element {
|
|
586
|
+
const drawer = drawerContext.use("DrawerHandle");
|
|
587
|
+
const sheet = drawerSheetContext.use("DrawerHandle");
|
|
588
|
+
const t = useUiStrings();
|
|
589
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
590
|
+
const points = (): DrawerSnapPoints => drawer.snapPoints();
|
|
591
|
+
const horizontal = (): boolean => isHorizontal(drawer.direction());
|
|
592
|
+
return (
|
|
593
|
+
<div
|
|
594
|
+
role="separator"
|
|
595
|
+
aria-orientation={horizontal() ? "vertical" : "horizontal"}
|
|
596
|
+
aria-label={t("drawer.handle", "Drag to resize")}
|
|
597
|
+
aria-valuemin={points().length > 0 ? 0 : undefined}
|
|
598
|
+
aria-valuemax={points().length > 0 ? points().length - 1 : undefined}
|
|
599
|
+
aria-valuenow={points().length > 0 ? drawer.snap() : undefined}
|
|
600
|
+
tabindex={0}
|
|
601
|
+
data-slot="drawer-handle"
|
|
602
|
+
data-dragging={sheet.dragging() ? "" : undefined}
|
|
603
|
+
class={cn(
|
|
604
|
+
"shrink-0 cursor-grab touch-none rounded-full bg-muted outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 forced-colors:bg-[CanvasText] active:cursor-grabbing focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent",
|
|
605
|
+
horizontal() ? "mx-2 my-auto h-12 w-1.5" : "mx-auto mt-4 mb-2 h-1.5 w-12",
|
|
606
|
+
local.class,
|
|
607
|
+
)}
|
|
608
|
+
onPointerDown={(event: PointerEvent) => {
|
|
609
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
610
|
+
const content = target?.closest('[data-slot="drawer-content"]');
|
|
611
|
+
const box = content instanceof HTMLElement ? content.getBoundingClientRect() : undefined;
|
|
612
|
+
sheet.startDrag(event, (horizontal() ? box?.width : box?.height) ?? 0);
|
|
613
|
+
}}
|
|
614
|
+
onKeyDown={(event: KeyboardEvent) => {
|
|
615
|
+
// Toward the open edge's far side opens further: up for bottom, down for top, away from the
|
|
616
|
+
// docked edge for start and end (mirrored in a right-to-left page).
|
|
617
|
+
const direction = drawer.direction();
|
|
618
|
+
const rtl = isRtl(event.currentTarget as Element | null);
|
|
619
|
+
const open =
|
|
620
|
+
direction === "bottom"
|
|
621
|
+
? "ArrowUp"
|
|
622
|
+
: direction === "top"
|
|
623
|
+
? "ArrowDown"
|
|
624
|
+
: (direction === "end") !== rtl
|
|
625
|
+
? "ArrowLeft"
|
|
626
|
+
: "ArrowRight";
|
|
627
|
+
const close =
|
|
628
|
+
direction === "bottom"
|
|
629
|
+
? "ArrowDown"
|
|
630
|
+
: direction === "top"
|
|
631
|
+
? "ArrowUp"
|
|
632
|
+
: open === "ArrowLeft"
|
|
633
|
+
? "ArrowRight"
|
|
634
|
+
: "ArrowLeft";
|
|
635
|
+
if (event.key === open) {
|
|
636
|
+
event.preventDefault();
|
|
637
|
+
sheet.nudge(1);
|
|
638
|
+
} else if (event.key === close) {
|
|
639
|
+
event.preventDefault();
|
|
640
|
+
sheet.nudge(-1);
|
|
641
|
+
}
|
|
642
|
+
}}
|
|
643
|
+
{...rest}
|
|
644
|
+
/>
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* The header of the drawer, with its title and description. It is a plain `div` with no role.
|
|
650
|
+
*
|
|
651
|
+
* @param props The header props: the attributes of the rendered `div`.
|
|
652
|
+
* @returns The header.
|
|
653
|
+
* @example
|
|
654
|
+
* import { DrawerDescription, DrawerHeader, DrawerTitle } from "@/components/ui/drawer";
|
|
655
|
+
*
|
|
656
|
+
* export function Heading() {
|
|
657
|
+
* return (
|
|
658
|
+
* <DrawerHeader>
|
|
659
|
+
* <DrawerTitle>Filters</DrawerTitle>
|
|
660
|
+
* <DrawerDescription>Narrow the list.</DrawerDescription>
|
|
661
|
+
* </DrawerHeader>
|
|
662
|
+
* );
|
|
663
|
+
* }
|
|
664
|
+
* @see {@link DrawerTitle}
|
|
665
|
+
* @since 1.0.0
|
|
666
|
+
*/
|
|
667
|
+
function DrawerHeader(props: ComponentProps<"div">): JSX.Element {
|
|
668
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
669
|
+
return (
|
|
670
|
+
<div
|
|
671
|
+
data-slot="drawer-header"
|
|
672
|
+
class={cn("flex flex-col gap-1.5 p-4 text-center md:text-start", local.class)}
|
|
673
|
+
{...rest}
|
|
674
|
+
/>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* The footer of the drawer, with its actions at the bottom of the sheet. It is a plain `div` with no role.
|
|
680
|
+
*
|
|
681
|
+
* @param props The footer props: the attributes of the rendered `div`.
|
|
682
|
+
* @returns The footer.
|
|
683
|
+
* @example
|
|
684
|
+
* import { Button } from "@/components/ui/button";
|
|
685
|
+
* import { DrawerFooter } from "@/components/ui/drawer";
|
|
686
|
+
*
|
|
687
|
+
* export function Actions() {
|
|
688
|
+
* return (
|
|
689
|
+
* <DrawerFooter>
|
|
690
|
+
* <Button>Apply</Button>
|
|
691
|
+
* </DrawerFooter>
|
|
692
|
+
* );
|
|
693
|
+
* }
|
|
694
|
+
* @see {@link DrawerContent}
|
|
695
|
+
* @since 1.0.0
|
|
696
|
+
*/
|
|
697
|
+
function DrawerFooter(props: ComponentProps<"div">): JSX.Element {
|
|
698
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
699
|
+
return (
|
|
700
|
+
<div
|
|
701
|
+
data-slot="drawer-footer"
|
|
702
|
+
class={cn("mt-auto flex flex-col gap-2 p-4", local.class)}
|
|
703
|
+
{...rest}
|
|
704
|
+
/>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* The accessible name of the drawer, rendered as its visible heading.
|
|
710
|
+
*
|
|
711
|
+
* @param props The title props: the attributes of the rendered `h2`.
|
|
712
|
+
* @returns The heading.
|
|
713
|
+
* @throws Error when rendered outside a {@link Drawer}.
|
|
714
|
+
* @example
|
|
715
|
+
* import { Drawer, DrawerContent, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer";
|
|
716
|
+
*
|
|
717
|
+
* export function Titled() {
|
|
718
|
+
* return (
|
|
719
|
+
* <Drawer>
|
|
720
|
+
* <DrawerTrigger>Filters</DrawerTrigger>
|
|
721
|
+
* <DrawerContent>
|
|
722
|
+
* <DrawerTitle>Filters</DrawerTitle>
|
|
723
|
+
* </DrawerContent>
|
|
724
|
+
* </Drawer>
|
|
725
|
+
* );
|
|
726
|
+
* }
|
|
727
|
+
* @see {@link DrawerDescription}
|
|
728
|
+
* @since 1.0.0
|
|
729
|
+
*/
|
|
730
|
+
function DrawerTitle(props: DialogTitleProps): JSX.Element {
|
|
731
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
732
|
+
return (
|
|
733
|
+
<DialogPrimitive.Title
|
|
734
|
+
data-slot="drawer-title"
|
|
735
|
+
class={cn("text-lg leading-none font-semibold", local.class)}
|
|
736
|
+
{...rest}
|
|
737
|
+
/>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* The accessible description of the drawer: the text a screen reader reads after its name.
|
|
743
|
+
*
|
|
744
|
+
* @param props The description props: the attributes of the rendered `p`.
|
|
745
|
+
* @returns The paragraph.
|
|
746
|
+
* @throws Error when rendered outside a {@link Drawer}.
|
|
747
|
+
* @example
|
|
748
|
+
* import { Drawer, DrawerContent, DrawerDescription, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer";
|
|
749
|
+
*
|
|
750
|
+
* export function Described() {
|
|
751
|
+
* return (
|
|
752
|
+
* <Drawer>
|
|
753
|
+
* <DrawerTrigger>Filters</DrawerTrigger>
|
|
754
|
+
* <DrawerContent>
|
|
755
|
+
* <DrawerTitle>Filters</DrawerTitle>
|
|
756
|
+
* <DrawerDescription>Narrow the list.</DrawerDescription>
|
|
757
|
+
* </DrawerContent>
|
|
758
|
+
* </Drawer>
|
|
759
|
+
* );
|
|
760
|
+
* }
|
|
761
|
+
* @see {@link DrawerTitle}
|
|
762
|
+
* @since 1.0.0
|
|
763
|
+
*/
|
|
764
|
+
function DrawerDescription(props: DialogDescriptionProps): JSX.Element {
|
|
765
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
766
|
+
return (
|
|
767
|
+
<DialogPrimitive.Description
|
|
768
|
+
data-slot="drawer-description"
|
|
769
|
+
class={cn("text-sm text-muted-foreground", local.class)}
|
|
770
|
+
{...rest}
|
|
771
|
+
/>
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export {
|
|
776
|
+
Drawer,
|
|
777
|
+
DrawerClose,
|
|
778
|
+
DrawerContent,
|
|
779
|
+
DrawerDescription,
|
|
780
|
+
DrawerFooter,
|
|
781
|
+
DrawerHandle,
|
|
782
|
+
DrawerHeader,
|
|
783
|
+
DrawerOverlay,
|
|
784
|
+
DrawerPortal,
|
|
785
|
+
DrawerTitle,
|
|
786
|
+
DrawerTrigger,
|
|
787
|
+
};
|