refactor(base-drawer): migrate base drawer to @base-ui/react/drawer (#10430)

* refactor(base-drawer): rewrite base drawer wrapper for @base-ui/react/drawer

Replace vaul with @base-ui/react/drawer as the primitive behind the
base drawer wrapper.

Keep the public API aligned with the radix drawer shape while
rewriting DrawerContent to compose Backdrop, Viewport, Popup, and
Content internally.

Keep the registry dependency change in the same commit so the wrapper
rewrite lands as one source-level migration step.

* refactor(drawer): move shared drawer direction rules from stylesheets to primitives

Shared drawer styles previously owned direction-specific layout in CSS.
With base and radix now exposing different direction attributes,
keeping that logic in shared tokens would duplicate primitive-specific
branching in the stylesheets.

Move direction-specific layout into the base/radix drawer wrappers and
leave shared CSS responsible only for visual surface styling.

Also move handle visibility and header alignment into TSX, and unify
the shared drawer token names to cn-drawer-*.

* fix(base-drawer): migrate examples, blocks, and app consumers to render and swipeDirection

Update in-repo base drawer consumers to the new wrapper API.

Replace asChild usage with render, switch direction to swipeDirection,
and align examples, blocks, app consumers, and docs with the new
base drawer usage pattern.

* chore: update registries

* fix(drawer): update base marker example usage

* fix(drawer): clean up base style selectors

* docs: add migration docs

* wip

* fix: nested

* feat: drawer

* fix

* fix

* fix

* fix: pointer events

* fix: clean up radix drawers

* fix: position

---------

Co-authored-by: shadcn <m@shadcn.com>
This commit is contained in:
coi
2026-07-02 19:16:37 +09:00
committed by GitHub
parent dbf9c5ebc4
commit 34223a842d
81 changed files with 2784 additions and 941 deletions

View File

@@ -6129,6 +6129,24 @@ export const ExamplesComponents: Record<string, Record<string, any>> = {
) || "drawer-dialog"
return { default: mod.default || mod[exportName] }
}),
"drawer-nested": React.lazy(async () => {
const mod = await import("./base/drawer-nested")
const exportName =
Object.keys(mod).find(
(key) =>
typeof mod[key] === "function" || typeof mod[key] === "object"
) || "drawer-nested"
return { default: mod.default || mod[exportName] }
}),
"drawer-non-modal": React.lazy(async () => {
const mod = await import("./base/drawer-non-modal")
const exportName =
Object.keys(mod).find(
(key) =>
typeof mod[key] === "function" || typeof mod[key] === "object"
) || "drawer-non-modal"
return { default: mod.default || mod[exportName] }
}),
"drawer-rtl": React.lazy(async () => {
const mod = await import("./base/drawer-rtl")
const exportName =
@@ -6138,15 +6156,6 @@ export const ExamplesComponents: Record<string, Record<string, any>> = {
) || "drawer-rtl"
return { default: mod.default || mod[exportName] }
}),
"drawer-scrollable-content": React.lazy(async () => {
const mod = await import("./base/drawer-scrollable-content")
const exportName =
Object.keys(mod).find(
(key) =>
typeof mod[key] === "function" || typeof mod[key] === "object"
) || "drawer-scrollable-content"
return { default: mod.default || mod[exportName] }
}),
"drawer-sides": React.lazy(async () => {
const mod = await import("./base/drawer-sides")
const exportName =
@@ -6156,6 +6165,24 @@ export const ExamplesComponents: Record<string, Record<string, any>> = {
) || "drawer-sides"
return { default: mod.default || mod[exportName] }
}),
"drawer-snap-points": React.lazy(async () => {
const mod = await import("./base/drawer-snap-points")
const exportName =
Object.keys(mod).find(
(key) =>
typeof mod[key] === "function" || typeof mod[key] === "object"
) || "drawer-snap-points"
return { default: mod.default || mod[exportName] }
}),
"drawer-swipe-handle": React.lazy(async () => {
const mod = await import("./base/drawer-swipe-handle")
const exportName =
Object.keys(mod).find(
(key) =>
typeof mod[key] === "function" || typeof mod[key] === "object"
) || "drawer-swipe-handle"
return { default: mod.default || mod[exportName] }
}),
"dropdown-menu-avatar": React.lazy(async () => {
const mod = await import("./base/dropdown-menu-avatar")
const exportName =

View File

@@ -2727,18 +2727,30 @@ export const ExamplesIndex: Record<string, Record<string, any>> = {
name: "drawer-dialog",
filePath: "examples/base/drawer-dialog.tsx",
},
"drawer-nested": {
name: "drawer-nested",
filePath: "examples/base/drawer-nested.tsx",
},
"drawer-non-modal": {
name: "drawer-non-modal",
filePath: "examples/base/drawer-non-modal.tsx",
},
"drawer-rtl": {
name: "drawer-rtl",
filePath: "examples/base/drawer-rtl.tsx",
},
"drawer-scrollable-content": {
name: "drawer-scrollable-content",
filePath: "examples/base/drawer-scrollable-content.tsx",
},
"drawer-sides": {
name: "drawer-sides",
filePath: "examples/base/drawer-sides.tsx",
},
"drawer-snap-points": {
name: "drawer-snap-points",
filePath: "examples/base/drawer-snap-points.tsx",
},
"drawer-swipe-handle": {
name: "drawer-swipe-handle",
filePath: "examples/base/drawer-swipe-handle.tsx",
},
"dropdown-menu-avatar": {
name: "dropdown-menu-avatar",
filePath: "examples/base/dropdown-menu-avatar.tsx",

View File

@@ -1,10 +1,11 @@
"use client"
import * as React from "react"
import { Minus, Plus } from "lucide-react"
import { Bar, BarChart, ResponsiveContainer } from "recharts"
import { toast } from "sonner"
import { Button } from "@/styles/base-nova/ui/button"
import { useIsMobile } from "@/hooks/use-mobile"
import { Badge } from "@/styles/base-rhea/ui/badge"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Drawer,
DrawerClose,
@@ -14,121 +15,117 @@ import {
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-nova/ui/drawer"
} from "@/styles/base-rhea/ui/drawer"
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
FieldTitle,
} from "@/styles/base-rhea/ui/field"
import { RadioGroup, RadioGroupItem } from "@/styles/base-rhea/ui/radio-group"
const data = [
const deliveryTimes = [
{
goal: 400,
value: "asap",
id: "delivery-asap",
label: "Standard delivery",
description: "2535 min · Driver assigned now",
badge: "Fastest",
},
{
goal: 300,
value: "5-00",
id: "delivery-5-00",
label: "5:00 PM 5:15 PM",
description: "Prep starts at 4:45 PM",
},
{
goal: 200,
value: "5-30",
id: "delivery-5-30",
label: "5:30 PM 5:45 PM",
description: "Good if you're heading home",
},
{
goal: 300,
value: "6-00",
id: "delivery-6-00",
label: "6:00 PM 6:15 PM",
description: "Most popular · High demand",
},
{
goal: 200,
},
{
goal: 278,
},
{
goal: 189,
},
{
goal: 239,
},
{
goal: 300,
},
{
goal: 200,
},
{
goal: 278,
},
{
goal: 189,
},
{
goal: 349,
value: "6-30",
id: "delivery-6-30",
label: "6:30 PM 6:45 PM",
description: "Last slot before kitchen closes",
},
]
export function DrawerDemo() {
const [goal, setGoal] = React.useState(350)
const [open, setOpen] = React.useState(false)
const [deliveryTime, setDeliveryTime] = React.useState("asap")
const isMobile = useIsMobile()
function onClick(adjustment: number) {
setGoal(Math.max(200, Math.min(400, goal + adjustment)))
function handleConfirm() {
const selected = deliveryTimes.find((time) => time.value === deliveryTime)
if (!selected) {
return
}
setOpen(false)
toast("Delivery time confirmed", {
description: selected.label,
})
}
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline">Open Drawer</Button>
<Drawer
open={open}
onOpenChange={setOpen}
showSwipeHandle={isMobile}
swipeDirection={isMobile ? "down" : "right"}
>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<div className="mx-auto w-full max-w-sm">
<DrawerHeader>
<DrawerTitle>Move Goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<div className="p-4 pb-0">
<div className="flex items-center justify-center space-x-2">
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 rounded-full"
onClick={() => onClick(-10)}
disabled={goal <= 200}
>
<Minus />
<span className="sr-only">Decrease</span>
</Button>
<div className="flex-1 text-center">
<div className="text-7xl font-bold tracking-tighter">
{goal}
</div>
<div className="text-[0.70rem] text-muted-foreground uppercase">
Calories/day
</div>
</div>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 rounded-full"
onClick={() => onClick(10)}
disabled={goal >= 400}
>
<Plus />
<span className="sr-only">Increase</span>
</Button>
</div>
<div className="mt-3 h-[120px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data}>
<Bar
dataKey="goal"
style={
{
fill: "var(--chart-1)",
} as React.CSSProperties
}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
<DrawerHeader>
<DrawerTitle>Pick a delivery time</DrawerTitle>
<DrawerDescription>
We&apos;ll prepare your order as soon as possible.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 scroll-fade overflow-y-auto p-4">
<RadioGroup
value={deliveryTime}
onValueChange={setDeliveryTime}
className="gap-2"
>
{deliveryTimes.map((time) => (
<FieldLabel key={time.value} htmlFor={time.id}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle className="flex items-center gap-2">
{time.label}
{time.badge ? (
<Badge variant="secondary">{time.badge}</Badge>
) : null}
</FieldTitle>
<FieldDescription>{time.description}</FieldDescription>
</FieldContent>
<RadioGroupItem value={time.value} id={time.id} />
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
<DrawerFooter>
<Button onClick={handleConfirm} className="h-[34px]">
Confirm Delivery Time
</Button>
<DrawerClose render={<Button variant="outline" />}>
Cancel
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
import { useMediaQuery } from "@/hooks/use-media-query"
import { Button } from "@/styles/base-nova/ui/button"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Dialog,
DialogContent,
@@ -12,7 +12,7 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/styles/base-nova/ui/dialog"
} from "@/styles/base-rhea/ui/dialog"
import {
Drawer,
DrawerClose,
@@ -22,9 +22,9 @@ import {
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-nova/ui/drawer"
import { Input } from "@/styles/base-nova/ui/input"
import { Label } from "@/styles/base-nova/ui/label"
} from "@/styles/base-rhea/ui/drawer"
import { Input } from "@/styles/base-rhea/ui/input"
import { Label } from "@/styles/base-rhea/ui/label"
export function DrawerDialogDemo() {
const [open, setOpen] = React.useState(false)
@@ -52,8 +52,8 @@ export function DrawerDialogDemo() {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button variant="outline">Edit Profile</Button>
<DrawerTrigger render={<Button variant="outline" />}>
Edit Profile
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
@@ -62,12 +62,7 @@ export function DrawerDialogDemo() {
Make changes to your profile here. Click save when you&apos;re done.
</DrawerDescription>
</DrawerHeader>
<ProfileForm className="px-4" />
<DrawerFooter className="pt-2">
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
<ProfileForm className="p-4" />
</DrawerContent>
</Drawer>
)

View File

@@ -0,0 +1,111 @@
"use client"
import { useIsMobile } from "@/hooks/use-mobile"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-rhea/ui/drawer"
export function DrawerNested() {
const isMobile = useIsMobile()
const swipeDirection = isMobile ? "down" : "right"
return (
<Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection}>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer</DrawerTitle>
<DrawerDescription>
Open another drawer from the same direction.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection}>
<DrawerTrigger render={<Button variant="outline" />}>
Open Nested Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Nested Drawer</DrawerTitle>
<DrawerDescription>
The parent drawer stays mounted behind this one.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer
showSwipeHandle={isMobile}
swipeDirection={swipeDirection}
>
<DrawerTrigger render={<Button variant="outline" />}>
Open Third Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Third Drawer</DrawerTitle>
<DrawerDescription>
Two drawers are stacked behind this one.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer
showSwipeHandle={isMobile}
swipeDirection={swipeDirection}
>
<DrawerTrigger render={<Button variant="outline" />}>
Open Fourth Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Fourth Drawer</DrawerTitle>
<DrawerDescription>
This is the frontmost drawer in the stack.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button variant="outline" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="outline" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="outline" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}

View File

@@ -0,0 +1,31 @@
import { Button } from "@/styles/base-rhea/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-rhea/ui/drawer"
export function DrawerNonModal() {
return (
<Drawer modal={false} disablePointerDismissal swipeDirection="right">
<DrawerTrigger render={<Button variant="outline" />}>
Non Modal
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Non Modal Drawer</DrawerTitle>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:h-80 group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}

View File

@@ -1,13 +1,14 @@
"use client"
import * as React from "react"
import { Minus, Plus } from "lucide-react"
import { Bar, BarChart, ResponsiveContainer, XAxis } from "recharts"
import { toast } from "sonner"
import { useIsMobile } from "@/hooks/use-mobile"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import { Badge } from "@/styles/base-nova/ui-rtl/badge"
import { Button } from "@/styles/base-nova/ui-rtl/button"
import {
Drawer,
@@ -19,62 +20,40 @@ import {
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-nova/ui-rtl/drawer"
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
FieldTitle,
} from "@/styles/base-nova/ui-rtl/field"
import {
RadioGroup,
RadioGroupItem,
} from "@/styles/base-nova/ui-rtl/radio-group"
const data = [
{
goal: 400,
},
{
goal: 300,
},
{
goal: 200,
},
{
goal: 300,
},
{
goal: 200,
},
{
goal: 278,
},
{
goal: 189,
},
{
goal: 239,
},
{
goal: 300,
},
{
goal: 200,
},
{
goal: 278,
},
{
goal: 189,
},
{
goal: 349,
},
]
const translations: Translations = {
const translations = {
en: {
dir: "ltr",
locale: "en-US",
values: {
trigger: "Open Drawer",
title: "Move Goal",
description: "Set your daily activity goal.",
caloriesPerDay: "Calories/day",
decrease: "Decrease",
increase: "Increase",
submit: "Submit",
title: "Pick a delivery time",
description: "We'll prepare your order as soon as possible.",
confirm: "Confirm Delivery Time",
cancel: "Cancel",
toastTitle: "Delivery time confirmed",
asapLabel: "Standard delivery",
asapDescription: "2535 min · Driver assigned now",
asapBadge: "Fastest",
slot500Label: "5:00 PM 5:15 PM",
slot500Description: "Prep starts at 4:45 PM",
slot530Label: "5:30 PM 5:45 PM",
slot530Description: "Good if you're heading home",
slot600Label: "6:00 PM 6:15 PM",
slot600Description: "Most popular · High demand",
slot630Label: "6:30 PM 6:45 PM",
slot630Description: "Last slot before kitchen closes",
},
},
ar: {
@@ -82,13 +61,22 @@ const translations: Translations = {
locale: "ar-EG",
values: {
trigger: "فتح الدرج",
title: "نقل الهدف",
description: "حدد هدف نشاطك اليومي.",
caloriesPerDay: "سعرات حرارية/يوم",
decrease: "تقليل",
increase: "زيادة",
submit: "إرسال",
title: "اختر وقت التوصيل",
description: "سنجهز طلبك في أقرب وقت ممكن.",
confirm: "تأكيد وقت التوصيل",
cancel: "إلغاء",
toastTitle: "تم تأكيد وقت التوصيل",
asapLabel: "توصيل قياسي",
asapDescription: "2535 دقيقة · تم تعيين السائق الآن",
asapBadge: "الأسرع",
slot500Label: "5:00 م 5:15 م",
slot500Description: "يبدأ التحضير في 4:45 م",
slot530Label: "5:30 م 5:45 م",
slot530Description: "مناسب إذا كنت في الطريق إلى المنزل",
slot600Label: "6:00 م 6:15 م",
slot600Description: "الأكثر شيوعًا · طلب مرتفع",
slot630Label: "6:30 م 6:45 م",
slot630Description: "آخر موعد قبل إغلاق المطبخ",
},
},
he: {
@@ -96,97 +84,137 @@ const translations: Translations = {
locale: "he-IL",
values: {
trigger: "פתח מגירה",
title: "הזז מטרה",
description: "הגדר את יעד הפעילות היומי שלך.",
caloriesPerDay: "קלוריות/יום",
decrease: "הקטן",
increase: "הגדל",
submit: "שלח",
title: "בחר זמן משלוח",
description: "נכין את ההזמנה שלך בהקדם האפשרי.",
confirm: "אשר זמן משלוח",
cancel: "בטל",
toastTitle: "זמן המשלוח אושר",
asapLabel: "משלוח רגיל",
asapDescription: "2535 דק׳ · נהג הוקצה כעת",
asapBadge: "הכי מהיר",
slot500Label: "17:00 17:15",
slot500Description: "ההכנה מתחילה ב-16:45",
slot530Label: "17:30 17:45",
slot530Description: "מתאים אם אתה בדרך הביתה",
slot600Label: "18:00 18:15",
slot600Description: "הפופולרי ביותר · ביקוש גבוה",
slot630Label: "18:30 18:45",
slot630Description: "המשבצת האחרונה לפני סגירת המטבח",
},
},
}
} satisfies Translations
type TranslationKey = keyof typeof translations.en.values
const deliveryTimes: Array<{
value: string
id: string
labelKey: TranslationKey
descriptionKey: TranslationKey
badgeKey?: TranslationKey
}> = [
{
value: "asap",
id: "delivery-asap-rtl",
labelKey: "asapLabel",
descriptionKey: "asapDescription",
badgeKey: "asapBadge",
},
{
value: "5-00",
id: "delivery-5-00-rtl",
labelKey: "slot500Label",
descriptionKey: "slot500Description",
},
{
value: "5-30",
id: "delivery-5-30-rtl",
labelKey: "slot530Label",
descriptionKey: "slot530Description",
},
{
value: "6-00",
id: "delivery-6-00-rtl",
labelKey: "slot600Label",
descriptionKey: "slot600Description",
},
{
value: "6-30",
id: "delivery-6-30-rtl",
labelKey: "slot630Label",
descriptionKey: "slot630Description",
},
]
export function DrawerRtl() {
const { dir, locale, language, t } = useTranslation(translations, "ar")
const [goal, setGoal] = React.useState(350)
const { dir, language, t } = useTranslation(translations, "ar")
const [open, setOpen] = React.useState(false)
const [deliveryTime, setDeliveryTime] = React.useState("asap")
const isMobile = useIsMobile()
function onClick(adjustment: number) {
setGoal(Math.max(200, Math.min(400, goal + adjustment)))
function handleConfirm() {
const selected = deliveryTimes.find((time) => time.value === deliveryTime)
if (!selected) {
return
}
setOpen(false)
toast(t.toastTitle, {
description: t[selected.labelKey],
})
}
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline">{t.trigger}</Button>
<Drawer
open={open}
onOpenChange={setOpen}
showSwipeHandle={isMobile}
swipeDirection={isMobile ? "down" : "right"}
>
<DrawerTrigger render={<Button variant="secondary" />}>
{t.trigger}
</DrawerTrigger>
<DrawerContent dir={dir} data-lang={dir === "rtl" ? language : undefined}>
<div className="mx-auto w-full max-w-sm">
<DrawerHeader>
<DrawerTitle>{t.title}</DrawerTitle>
<DrawerDescription>{t.description}</DrawerDescription>
</DrawerHeader>
<div className="p-4 pb-0">
<div className="flex items-center justify-center space-x-2">
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 rounded-full"
onClick={() => onClick(-10)}
disabled={goal <= 200}
>
<Minus />
<span className="sr-only">{t.decrease}</span>
</Button>
<div className="flex-1 text-center">
<div className="text-7xl font-bold tracking-tighter">
{goal.toLocaleString(locale)}
</div>
<div className="text-[0.70rem] text-muted-foreground uppercase">
{t.caloriesPerDay}
</div>
</div>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 rounded-full"
onClick={() => onClick(10)}
disabled={goal >= 400}
>
<Plus />
<span className="sr-only">{t.increase}</span>
</Button>
</div>
<div className="mt-3 h-[120px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data}>
<XAxis
dataKey="goal"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.toLocaleString(locale)}
reversed={dir === "rtl"}
/>
<Bar
dataKey="goal"
style={
{
fill: "var(--chart-2)",
} as React.CSSProperties
}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
<DrawerFooter>
<Button>{t.submit}</Button>
<DrawerClose asChild>
<Button variant="outline">{t.cancel}</Button>
</DrawerClose>
</DrawerFooter>
<DrawerHeader>
<DrawerTitle>{t.title}</DrawerTitle>
<DrawerDescription>{t.description}</DrawerDescription>
</DrawerHeader>
<div className="flex-1 scroll-fade overflow-y-auto p-4">
<RadioGroup
value={deliveryTime}
onValueChange={setDeliveryTime}
className="gap-2"
dir={dir}
>
{deliveryTimes.map((time) => (
<FieldLabel key={time.value} htmlFor={time.id} dir={dir}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle className="flex items-center gap-2">
{t[time.labelKey]}
{time.badgeKey ? (
<Badge variant="secondary">{t[time.badgeKey]}</Badge>
) : null}
</FieldTitle>
<FieldDescription dir={dir}>
{t[time.descriptionKey]}
</FieldDescription>
</FieldContent>
<RadioGroupItem value={time.value} id={time.id} dir={dir} />
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
<DrawerFooter>
<Button onClick={handleConfirm} className="h-[34px]">
{t.confirm}
</Button>
<DrawerClose render={<Button variant="outline" />}>
{t.cancel}
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)

View File

@@ -1,46 +0,0 @@
import { Button } from "@/styles/base-nova/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-nova/ui/drawer"
export function DrawerScrollableContent() {
return (
<Drawer direction="right">
<DrawerTrigger asChild>
<Button variant="outline">Scrollable Content</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move Goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<div className="no-scrollbar overflow-y-auto px-4">
{Array.from({ length: 10 }).map((_, index) => (
<p key={index} className="mb-4 leading-normal">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
))}
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}

View File

@@ -1,4 +1,4 @@
import { Button } from "@/styles/base-nova/ui/button"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Drawer,
DrawerClose,
@@ -8,55 +8,26 @@ import {
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-nova/ui/drawer"
const DRAWER_SIDES = ["top", "right", "bottom", "left"] as const
} from "@/styles/base-rhea/ui/drawer"
export function DrawerWithSides() {
return (
<div className="flex flex-wrap gap-2">
{DRAWER_SIDES.map((side) => (
<Drawer
key={side}
direction={
side === "bottom" ? undefined : (side as "top" | "right" | "left")
}
>
<DrawerTrigger asChild>
<Button variant="outline" className="capitalize">
{side}
</Button>
</DrawerTrigger>
<DrawerContent className="data-[vaul-drawer-direction=bottom]:max-h-[50vh] data-[vaul-drawer-direction=top]:max-h-[50vh]">
<DrawerHeader>
<DrawerTitle>Move Goal</DrawerTitle>
<DrawerDescription>
Set your daily activity goal.
</DrawerDescription>
</DrawerHeader>
<div className="no-scrollbar overflow-y-auto px-4">
{Array.from({ length: 10 }).map((_, index) => (
<p key={index} className="mb-4 leading-normal">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute
irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
))}
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
))}
</div>
<Drawer swipeDirection="left">
<DrawerTrigger render={<Button variant="secondary" />}>
Open Left Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move Goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="size-full rounded-2xl bg-muted" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}

View File

@@ -0,0 +1,40 @@
"use client"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-rhea/ui/drawer"
const SNAP_POINTS = ["31rem", 1]
export function DrawerSnapPoints() {
return (
<Drawer snapPoints={SNAP_POINTS} showSwipeHandle>
<DrawerTrigger render={<Button variant="outline" />}>
Open Snap Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Snap points</DrawerTitle>
<DrawerDescription>
Drag the drawer to snap between a compact peek and a near
full-height view.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:h-80 group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}

View File

@@ -0,0 +1,35 @@
"use client"
import { Button } from "@/styles/base-rhea/ui/button"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/styles/base-rhea/ui/drawer"
export function DrawerSwipeHandle() {
return (
<Drawer showSwipeHandle>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer</DrawerTitle>
<DrawerDescription>Drawer with a swipe handle.</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:h-80 group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}