mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-08 06:28:37 +00:00
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:
@@ -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: "25–35 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'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>
|
||||
)
|
||||
|
||||
@@ -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'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>
|
||||
)
|
||||
|
||||
111
apps/v4/examples/base/drawer-nested.tsx
Normal file
111
apps/v4/examples/base/drawer-nested.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
31
apps/v4/examples/base/drawer-non-modal.tsx
Normal file
31
apps/v4/examples/base/drawer-non-modal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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: "25–35 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: "25–35 دقيقة · تم تعيين السائق الآن",
|
||||
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: "25–35 דק׳ · נהג הוקצה כעת",
|
||||
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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
40
apps/v4/examples/base/drawer-snap-points.tsx
Normal file
40
apps/v4/examples/base/drawer-snap-points.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
35
apps/v4/examples/base/drawer-swipe-handle.tsx
Normal file
35
apps/v4/examples/base/drawer-swipe-handle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user