From 34223a842df71bc87e4a8bce80115fed1301437a Mon Sep 17 00:00:00 2001 From: coi <102783842+lunaxislu@users.noreply.github.com> Date: Thu, 2 Jul 2026 19:16:37 +0900 Subject: [PATCH] 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 --- .../(app)/create/components/open-preset.tsx | 19 +- apps/v4/app/globals.css | 1 + apps/v4/components/directory-add-button.tsx | 4 +- .../content/docs/components/base/drawer.mdx | 235 ++++++++- apps/v4/examples/__components__.tsx | 45 +- apps/v4/examples/__index__.tsx | 20 +- apps/v4/examples/base/drawer-demo.tsx | 191 ++++--- apps/v4/examples/base/drawer-dialog.tsx | 21 +- apps/v4/examples/base/drawer-nested.tsx | 111 ++++ apps/v4/examples/base/drawer-non-modal.tsx | 31 ++ apps/v4/examples/base/drawer-rtl.tsx | 300 ++++++----- .../base/drawer-scrollable-content.tsx | 46 -- apps/v4/examples/base/drawer-sides.tsx | 67 +-- apps/v4/examples/base/drawer-snap-points.tsx | 40 ++ apps/v4/examples/base/drawer-swipe-handle.tsx | 35 ++ apps/v4/public/r/index.json | 2 +- .../r/styles/base-luma/dashboard-01.json | 2 +- .../r/styles/base-luma/drawer-example.json | 2 +- apps/v4/public/r/styles/base-luma/drawer.json | 6 +- .../r/styles/base-luma/marker-example.json | 2 +- .../public/r/styles/base-luma/registry.json | 4 +- .../r/styles/base-lyra/dashboard-01.json | 2 +- .../r/styles/base-lyra/drawer-example.json | 2 +- apps/v4/public/r/styles/base-lyra/drawer.json | 6 +- .../r/styles/base-lyra/marker-example.json | 2 +- .../public/r/styles/base-lyra/registry.json | 4 +- .../r/styles/base-maia/dashboard-01.json | 2 +- .../r/styles/base-maia/drawer-example.json | 2 +- apps/v4/public/r/styles/base-maia/drawer.json | 6 +- .../r/styles/base-maia/marker-example.json | 2 +- .../public/r/styles/base-maia/registry.json | 4 +- .../r/styles/base-mira/dashboard-01.json | 2 +- .../r/styles/base-mira/drawer-example.json | 2 +- apps/v4/public/r/styles/base-mira/drawer.json | 6 +- .../r/styles/base-mira/marker-example.json | 2 +- .../public/r/styles/base-mira/registry.json | 4 +- .../r/styles/base-nova/dashboard-01.json | 2 +- .../r/styles/base-nova/drawer-example.json | 2 +- apps/v4/public/r/styles/base-nova/drawer.json | 6 +- .../r/styles/base-nova/marker-example.json | 2 +- .../public/r/styles/base-nova/registry.json | 4 +- .../r/styles/base-rhea/dashboard-01.json | 2 +- .../r/styles/base-rhea/drawer-example.json | 2 +- apps/v4/public/r/styles/base-rhea/drawer.json | 6 +- .../r/styles/base-rhea/marker-example.json | 2 +- .../public/r/styles/base-rhea/registry.json | 4 +- .../r/styles/base-sera/dashboard-01.json | 2 +- .../r/styles/base-sera/drawer-example.json | 2 +- apps/v4/public/r/styles/base-sera/drawer.json | 6 +- .../r/styles/base-sera/marker-example.json | 2 +- .../public/r/styles/base-sera/registry.json | 4 +- .../r/styles/base-vega/dashboard-01.json | 2 +- .../r/styles/base-vega/drawer-example.json | 2 +- apps/v4/public/r/styles/base-vega/drawer.json | 6 +- .../r/styles/base-vega/marker-example.json | 2 +- .../public/r/styles/base-vega/registry.json | 4 +- apps/v4/registry/__index__.tsx | 16 +- apps/v4/registry/bases/__index__.tsx | 2 +- .../dashboard-01/components/data-table.tsx | 19 +- .../bases/base/examples/drawer-example.tsx | 480 +++++++++++++++--- .../bases/base/examples/marker-example.tsx | 28 +- apps/v4/registry/bases/base/ui/_registry.ts | 4 +- apps/v4/registry/bases/base/ui/drawer.tsx | 175 +++++-- apps/v4/registry/bases/radix/ui/drawer.tsx | 12 +- apps/v4/registry/styles/style-luma.css | 24 +- apps/v4/registry/styles/style-lyra.css | 24 +- apps/v4/registry/styles/style-maia.css | 25 +- apps/v4/registry/styles/style-mira.css | 24 +- apps/v4/registry/styles/style-nova.css | 24 +- apps/v4/registry/styles/style-rhea.css | 24 +- apps/v4/registry/styles/style-sera.css | 24 +- apps/v4/registry/styles/style-vega.css | 24 +- apps/v4/styles/base-luma/ui/drawer.tsx | 166 ++++-- apps/v4/styles/base-lyra/ui/drawer.tsx | 169 ++++-- apps/v4/styles/base-maia/ui/drawer.tsx | 166 ++++-- apps/v4/styles/base-mira/ui/drawer.tsx | 169 ++++-- apps/v4/styles/base-nova/ui-rtl/drawer.tsx | 166 ++++-- apps/v4/styles/base-nova/ui/drawer.tsx | 166 ++++-- apps/v4/styles/base-rhea/ui/drawer.tsx | 166 ++++-- apps/v4/styles/base-sera/ui/drawer.tsx | 166 ++++-- apps/v4/styles/base-vega/ui/drawer.tsx | 166 ++++-- 81 files changed, 2784 insertions(+), 941 deletions(-) create mode 100644 apps/v4/examples/base/drawer-nested.tsx create mode 100644 apps/v4/examples/base/drawer-non-modal.tsx delete mode 100644 apps/v4/examples/base/drawer-scrollable-content.tsx create mode 100644 apps/v4/examples/base/drawer-snap-points.tsx create mode 100644 apps/v4/examples/base/drawer-swipe-handle.tsx diff --git a/apps/v4/app/(app)/create/components/open-preset.tsx b/apps/v4/app/(app)/create/components/open-preset.tsx index 11767a516d..e6c53c1244 100644 --- a/apps/v4/app/(app)/create/components/open-preset.tsx +++ b/apps/v4/app/(app)/create/components/open-preset.tsx @@ -111,11 +111,12 @@ export function OpenPreset({ if (isMobile) { return ( - - + } + > + {label} + {PRESET_TITLE} @@ -127,10 +128,12 @@ export function OpenPreset({ - - + + } + > + Cancel diff --git a/apps/v4/app/globals.css b/apps/v4/app/globals.css index 78f40c70b1..281dbc7ef1 100644 --- a/apps/v4/app/globals.css +++ b/apps/v4/app/globals.css @@ -170,6 +170,7 @@ @apply overscroll-y-none; } body { + position: relative; font-synthesis-weight: none; text-rendering: optimizeLegibility; } diff --git a/apps/v4/components/directory-add-button.tsx b/apps/v4/components/directory-add-button.tsx index e3497bf9ec..95bc50daee 100644 --- a/apps/v4/components/directory-add-button.tsx +++ b/apps/v4/components/directory-add-button.tsx @@ -185,9 +185,7 @@ export function DirectoryAddProvider({
{Content}
- - - + }>Done
diff --git a/apps/v4/content/docs/components/base/drawer.mdx b/apps/v4/content/docs/components/base/drawer.mdx index 0b46bcbb00..7ffe85e812 100644 --- a/apps/v4/content/docs/components/base/drawer.mdx +++ b/apps/v4/content/docs/components/base/drawer.mdx @@ -4,14 +4,11 @@ description: A drawer component for React. base: base component: true links: - doc: https://vaul.emilkowal.ski/getting-started + doc: https://base-ui.com/react/components/drawer + api: https://base-ui.com/react/components/drawer#api-reference --- - - -## About - -Drawer is built on top of [Vaul](https://github.com/emilkowalski/vaul) by [emilkowalski](https://twitter.com/emilkowalski). + ## Installation @@ -36,7 +33,7 @@ npx shadcn@latest add drawer Install the following dependencies: ```bash -npm install vaul +npm install @base-ui/react ``` Copy and paste the following code into your project. @@ -44,7 +41,7 @@ npm install vaul Update the import paths to match your project setup. @@ -55,6 +52,14 @@ npm install vaul +Add the following to your global styles. On iOS Safari, the drawer overlay is absolutely positioned and requires a positioned `body` to cover the viewport after the page is scrolled. See the [Base UI docs](https://base-ui.com/react/overview/quick-start#ios-26-safari) for details. + +```css +body { + position: relative; +} +``` + ## Usage ```tsx showLineNumbers @@ -72,17 +77,16 @@ import { ```tsx showLineNumbers - Open + }>Open Are you absolutely sure? This action cannot be undone. +
{/* Content here */}
- - - + }>Cancel
@@ -102,32 +106,215 @@ Drawer └── DrawerFooter ``` +`DrawerContent` composes the portal, overlay, viewport, and popup from Base UI. For lower-level control, `DrawerPortal`, `DrawerOverlay`, and `DrawerSwipeHandle` are also exported. + +## Custom Sizes + +A vertical drawer sizes itself to its content and is capped at `calc(100dvh - 6rem)` by default. A side drawer spans `75%` of the viewport width, or `24rem` on larger screens. + +To customize the height of a vertical drawer, use the `h-*` and `max-h-*` utilities on `DrawerContent`. + +```tsx + +``` + +To customize the width of a side drawer, use the `w-*` and `max-w-*` utilities on `DrawerContent`. + +```tsx + +``` + +When the same component renders in multiple directions, scope an override to one axis using the `data-[swipe-axis=*]` variants. + +```tsx + +``` + +To make a region of the drawer scrollable, make the scroll container a flex item. Avoid `h-full`, which does not resolve inside a content-sized drawer. + +```tsx + + ... +
{/* Scrollable content */}
+ ... +
+``` + +## Styling + +The drawer exposes CSS variables for style-level customization. Set the sizing variables on `DrawerContent`. Set the overlay variable on `[data-slot=drawer-overlay]` in your CSS. + +| Variable | Default | Description | +| ------------------------------ | ---------------------- | ----------------------------------------------------------------------- | +| `--drawer-inset` | `0px` | Floats the drawer from the viewport edges. | +| `--drawer-bleed-background` | `var(--color-popover)` | Fills the gap behind the drawer on swipe overshoot. | +| `--drawer-overlay-min-opacity` | `0` | Minimum overlay opacity. Defaults to `0.5` when snap points are active. | + +The drawer also sets data attributes you can target with variants such as `data-[swipe-direction=down]:` on `DrawerContent`, or `group-data-[swipe-axis=y]/drawer-popup:` on its descendants. + +| Attribute | Values | Set when | +| ------------------------- | ----------------------------- | ------------------------------------- | +| `data-swipe-direction` | `up`, `right`, `down`, `left` | Always. | +| `data-swipe-axis` | `x`, `y` | Always. | +| `data-snap-points` | Present | The drawer has snap points. | +| `data-expanded` | Present | The drawer is at the full snap point. | +| `data-swiping` | Present | A swipe is in progress. | +| `data-nested-drawer-open` | Present | A nested drawer is open on top. | + ## Examples -### Scrollable Content +### Position -Keep actions visible while the content scrolls. +Use the `swipeDirection` prop to set the side of the drawer. - +Available options are `up`, `right`, `down`, and `left`. -### Sides + -Use the `direction` prop to set the side of the drawer. Available options are `top`, `right`, `bottom`, and `left`. +### Swipe Handle - +Use `showSwipeHandle` on `Drawer` to render a swipe handle. -### Responsive Dialog + + +### Nested + +Open drawers from inside another drawer. Parent drawers stay mounted and stack behind the frontmost drawer. + + + +### Non Modal + +Set `modal={false}` to allow interaction with the rest of the page while the drawer is open. Combine with `disablePointerDismissal` to prevent the drawer from closing on outside presses. Use `modal="trap-focus"` to keep focus inside the drawer while leaving scroll and pointer interaction unrestricted. + + + +### Snap Points + +Use `snapPoints` to snap a drawer to preset heights. Numbers between `0` and `1` represent fractions of the viewport. Numbers greater than `1` are treated as pixel values. String values support `px` and `rem` units. Snap points apply to vertical drawers. + +Track the active snap point with the controlled `snapPoint` and `onSnapPointChange` props. At the full snap point, the drawer gets a `data-expanded` attribute you can style with the `data-expanded:` variant. + + + +### Responsive You can combine the `Dialog` and `Drawer` components to create a responsive dialog. This renders a `Dialog` component on desktop and a `Drawer` on mobile. - + -## RTL +## Migrating from Vaul -To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl). +The base drawer now uses [Base UI](https://base-ui.com/react/components/drawer) +instead of Vaul. If you installed the previous base drawer, update your usage +to the Base UI API. - + + +Update the dependency. + +```diff +- npm install vaul ++ npm install @base-ui/react +``` + +Replace `direction` with `swipeDirection`. + +Use `down` instead of `bottom`, and `up` instead of `top`. `left` and `right` +stay the same. + +```diff +- ++ +``` + +Replace `asChild` with `render`. + +For `DrawerTrigger`, pass the trigger element to the `render` prop. + +```diff +- +- +- ++ }> ++ Open ++ +``` + +For `DrawerClose`, pass the close element to the `render` prop. + +```diff +- +- +- ++ }> ++ Cancel ++ +``` + +Update snap point props. + +If you use snap points, rename the controlled snap point props and the sequential +snap point prop. + +```diff + +``` + +Update animation and focus props. + +```diff +- setDone(open)}> ++ setDone(open)}> +``` + +```diff +- event.preventDefault()}> ++ +``` + +Review Vaul-only props. + +Vaul props like `handleOnly`, `repositionInputs`, and +`shouldScaleBackground` do not have one-to-one replacements in the base drawer +API. Use Base UI props such as `disablePointerDismissal`, `modal`, `snapPoints`, +or controlled `open` state for the behavior you need. + +```diff +- ++ +``` + +```diff +- ++ +``` + +Update custom data attribute selectors. + +Replace Vaul's `data-vaul-drawer-direction` selectors with Base UI's +`data-swipe-direction` selectors. + +```diff +- ++ +``` + +Base UI also exposes attributes like `data-swiping`, `data-starting-style`, and +`data-ending-style` for swipe and transition states. Descendants inside +`DrawerContent` can use `group-data-[swipe-axis=x]/drawer-popup` and +`group-data-[swipe-axis=y]/drawer-popup` for axis-specific styling. + + ## API Reference -See the [Vaul documentation](https://vaul.emilkowal.ski/getting-started) for the full API reference. +See the [Base UI documentation](https://base-ui.com/react/components/drawer) for the full API reference. diff --git a/apps/v4/examples/__components__.tsx b/apps/v4/examples/__components__.tsx index 6724ed46a7..5e1b28c430 100644 --- a/apps/v4/examples/__components__.tsx +++ b/apps/v4/examples/__components__.tsx @@ -6129,6 +6129,24 @@ export const ExamplesComponents: Record> = { ) || "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> = { ) || "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> = { ) || "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 = diff --git a/apps/v4/examples/__index__.tsx b/apps/v4/examples/__index__.tsx index 5b27127735..338196b2c6 100644 --- a/apps/v4/examples/__index__.tsx +++ b/apps/v4/examples/__index__.tsx @@ -2727,18 +2727,30 @@ export const ExamplesIndex: Record> = { 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", diff --git a/apps/v4/examples/base/drawer-demo.tsx b/apps/v4/examples/base/drawer-demo.tsx index ab8c129adc..14502278ac 100644 --- a/apps/v4/examples/base/drawer-demo.tsx +++ b/apps/v4/examples/base/drawer-demo.tsx @@ -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 ( - - - + + }> + Open Drawer -
- - Move Goal - Set your daily activity goal. - -
-
- -
-
- {goal} -
-
- Calories/day -
-
- -
-
- - - - - -
-
- - - - - - + + Pick a delivery time + + We'll prepare your order as soon as possible. + + +
+ + {deliveryTimes.map((time) => ( + + + + + {time.label} + {time.badge ? ( + {time.badge} + ) : null} + + {time.description} + + + + + ))} +
+ + + }> + Cancel + + ) diff --git a/apps/v4/examples/base/drawer-dialog.tsx b/apps/v4/examples/base/drawer-dialog.tsx index 9512ddd647..a4c0224393 100644 --- a/apps/v4/examples/base/drawer-dialog.tsx +++ b/apps/v4/examples/base/drawer-dialog.tsx @@ -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 ( - - + }> + Edit Profile @@ -62,12 +62,7 @@ export function DrawerDialogDemo() { Make changes to your profile here. Click save when you're done. - - - - - - + ) diff --git a/apps/v4/examples/base/drawer-nested.tsx b/apps/v4/examples/base/drawer-nested.tsx new file mode 100644 index 0000000000..034497de03 --- /dev/null +++ b/apps/v4/examples/base/drawer-nested.tsx @@ -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 ( + + }> + Open Drawer + + + + Drawer + + Open another drawer from the same direction. + + +
+
+
+ + + }> + Open Nested Drawer + + + + Nested Drawer + + The parent drawer stays mounted behind this one. + + +
+
+
+ + + }> + Open Third Drawer + + + + Third Drawer + + Two drawers are stacked behind this one. + + +
+
+
+ + + }> + Open Fourth Drawer + + + + Fourth Drawer + + This is the frontmost drawer in the stack. + + +
+
+
+ + }> + Close + + + + + }> + Close + + + + + }> + Close + + + + + }>Close + + + + ) +} diff --git a/apps/v4/examples/base/drawer-non-modal.tsx b/apps/v4/examples/base/drawer-non-modal.tsx new file mode 100644 index 0000000000..e1fb00c200 --- /dev/null +++ b/apps/v4/examples/base/drawer-non-modal.tsx @@ -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 ( + + }> + Non Modal + + + + Non Modal Drawer + +
+
+
+ + }>Close + + + + ) +} diff --git a/apps/v4/examples/base/drawer-rtl.tsx b/apps/v4/examples/base/drawer-rtl.tsx index 23a3a3cc37..5495f923c7 100644 --- a/apps/v4/examples/base/drawer-rtl.tsx +++ b/apps/v4/examples/base/drawer-rtl.tsx @@ -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 ( - - - + + }> + {t.trigger} -
- - {t.title} - {t.description} - -
-
- -
-
- {goal.toLocaleString(locale)} -
-
- {t.caloriesPerDay} -
-
- -
-
- - - value.toLocaleString(locale)} - reversed={dir === "rtl"} - /> - - - -
-
- - - - - - + + {t.title} + {t.description} + +
+ + {deliveryTimes.map((time) => ( + + + + + {t[time.labelKey]} + {time.badgeKey ? ( + {t[time.badgeKey]} + ) : null} + + + {t[time.descriptionKey]} + + + + + + ))} +
+ + + }> + {t.cancel} + + ) diff --git a/apps/v4/examples/base/drawer-scrollable-content.tsx b/apps/v4/examples/base/drawer-scrollable-content.tsx deleted file mode 100644 index e005c83ac1..0000000000 --- a/apps/v4/examples/base/drawer-scrollable-content.tsx +++ /dev/null @@ -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 ( - - - - - - - Move Goal - Set your daily activity goal. - -
- {Array.from({ length: 10 }).map((_, index) => ( -

- 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. -

- ))} -
- - - - - - -
-
- ) -} diff --git a/apps/v4/examples/base/drawer-sides.tsx b/apps/v4/examples/base/drawer-sides.tsx index 6b26bd27f9..7e53743c67 100644 --- a/apps/v4/examples/base/drawer-sides.tsx +++ b/apps/v4/examples/base/drawer-sides.tsx @@ -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 ( -
- {DRAWER_SIDES.map((side) => ( - - - - - - - Move Goal - - Set your daily activity goal. - - -
- {Array.from({ length: 10 }).map((_, index) => ( -

- 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. -

- ))} -
- - - - - - -
-
- ))} -
+ + }> + Open Left Drawer + + + + Move Goal + Set your daily activity goal. + +
+
+
+ + }>Close + + + ) } diff --git a/apps/v4/examples/base/drawer-snap-points.tsx b/apps/v4/examples/base/drawer-snap-points.tsx new file mode 100644 index 0000000000..7e2ce41d9d --- /dev/null +++ b/apps/v4/examples/base/drawer-snap-points.tsx @@ -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 ( + + }> + Open Snap Drawer + + + + Snap points + + Drag the drawer to snap between a compact peek and a near + full-height view. + + +
+
+
+ + }>Close + + + + ) +} diff --git a/apps/v4/examples/base/drawer-swipe-handle.tsx b/apps/v4/examples/base/drawer-swipe-handle.tsx new file mode 100644 index 0000000000..1047bd0ef3 --- /dev/null +++ b/apps/v4/examples/base/drawer-swipe-handle.tsx @@ -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 ( + + }> + Open Drawer + + + + Drawer + Drawer with a swipe handle. + +
+
+
+ + }>Close + + + + ) +} diff --git a/apps/v4/public/r/index.json b/apps/v4/public/r/index.json index bcbe3fcc1e..35f433dace 100644 --- a/apps/v4/public/r/index.json +++ b/apps/v4/public/r/index.json @@ -515,7 +515,7 @@ "base": { "docs": "https://ui.shadcn.com/docs/components/base/drawer", "examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/drawer-example.tsx", - "api": "https://vaul.emilkowal.ski/getting-started" + "api": "https://base-ui.com/react/components/drawer.md" } } } diff --git a/apps/v4/public/r/styles/base-luma/dashboard-01.json b/apps/v4/public/r/styles/base-luma/dashboard-01.json index 210b6a6fec..25e5bda52c 100644 --- a/apps/v4/public/r/styles/base-luma/dashboard-01.json +++ b/apps/v4/public/r/styles/base-luma/dashboard-01.json @@ -57,7 +57,7 @@ }, { "path": "registry/base-luma/blocks/dashboard-01/components/data-table.tsx", - "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n closestCenter,\n DndContext,\n KeyboardSensor,\n MouseSensor,\n TouchSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n type UniqueIdentifier,\n} from \"@dnd-kit/core\"\nimport { restrictToVerticalAxis } from \"@dnd-kit/modifiers\"\nimport {\n arrayMove,\n SortableContext,\n useSortable,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport {\n flexRender,\n getCoreRowModel,\n getFacetedRowModel,\n getFacetedUniqueValues,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n useReactTable,\n type ColumnDef,\n type ColumnFiltersState,\n type Row,\n type SortingState,\n type VisibilityState,\n} from \"@tanstack/react-table\"\nimport { Area, AreaChart, CartesianGrid, XAxis } from \"recharts\"\nimport { toast } from \"sonner\"\nimport { z } from \"zod\"\n\nimport { useIsMobile } from \"@/registry/base-luma/hooks/use-mobile\"\nimport { Badge } from \"@/registry/base-luma/ui/badge\"\nimport { Button } from \"@/registry/base-luma/ui/button\"\nimport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n type ChartConfig,\n} from \"@/registry/base-luma/ui/chart\"\nimport { Checkbox } from \"@/registry/base-luma/ui/checkbox\"\nimport {\n Drawer,\n DrawerClose,\n DrawerContent,\n DrawerDescription,\n DrawerFooter,\n DrawerHeader,\n DrawerTitle,\n DrawerTrigger,\n} from \"@/registry/base-luma/ui/drawer\"\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/registry/base-luma/ui/dropdown-menu\"\nimport { Input } from \"@/registry/base-luma/ui/input\"\nimport { Label } from \"@/registry/base-luma/ui/label\"\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/base-luma/ui/select\"\nimport { Separator } from \"@/registry/base-luma/ui/separator\"\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/registry/base-luma/ui/table\"\nimport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from \"@/registry/base-luma/ui/tabs\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\nexport const schema = z.object({\n id: z.number(),\n header: z.string(),\n type: z.string(),\n status: z.string(),\n target: z.string(),\n limit: z.string(),\n reviewer: z.string(),\n})\n\n// Create a separate component for the drag handle\nfunction DragHandle({ id }: { id: number }) {\n const { attributes, listeners } = useSortable({\n id,\n })\n return (\n \n \n Drag to reorder\n \n )\n}\nconst columns: ColumnDef>[] = [\n {\n id: \"drag\",\n header: () => null,\n cell: ({ row }) => ,\n },\n {\n id: \"select\",\n header: ({ table }) => (\n
\n table.toggleAllPageRowsSelected(!!value)}\n aria-label=\"Select all\"\n />\n
\n ),\n cell: ({ row }) => (\n
\n row.toggleSelected(!!value)}\n aria-label=\"Select row\"\n />\n
\n ),\n enableSorting: false,\n enableHiding: false,\n },\n {\n accessorKey: \"header\",\n header: \"Header\",\n cell: ({ row }) => {\n return \n },\n enableHiding: false,\n },\n {\n accessorKey: \"type\",\n header: \"Section Type\",\n cell: ({ row }) => (\n
\n \n {row.original.type}\n \n
\n ),\n },\n {\n accessorKey: \"status\",\n header: \"Status\",\n cell: ({ row }) => (\n \n {row.original.status === \"Done\" ? (\n \n ) : (\n \n )}\n {row.original.status}\n \n ),\n },\n {\n accessorKey: \"target\",\n header: () =>
Target
,\n cell: ({ row }) => (\n {\n e.preventDefault()\n toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {\n loading: `Saving ${row.original.header}`,\n success: \"Done\",\n error: \"Error\",\n })\n }}\n >\n \n \n \n ),\n },\n {\n accessorKey: \"limit\",\n header: () =>
Limit
,\n cell: ({ row }) => (\n {\n e.preventDefault()\n toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {\n loading: `Saving ${row.original.header}`,\n success: \"Done\",\n error: \"Error\",\n })\n }}\n >\n \n \n \n ),\n },\n {\n accessorKey: \"reviewer\",\n header: \"Reviewer\",\n cell: ({ row }) => {\n const isAssigned = row.original.reviewer !== \"Assign reviewer\"\n if (isAssigned) {\n return row.original.reviewer\n }\n return (\n <>\n \n \n \n \n \n \n \n Eddie Lake\n \n Jamik Tashpulatov\n \n \n \n \n \n )\n },\n },\n {\n id: \"actions\",\n cell: () => (\n \n \n }\n >\n \n Open menu\n \n \n Edit\n Make a copy\n Favorite\n \n Delete\n \n \n ),\n },\n]\nfunction DraggableRow({ row }: { row: Row> }) {\n const { transform, transition, setNodeRef, isDragging } = useSortable({\n id: row.original.id,\n })\n return (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ))}\n \n )\n}\nexport function DataTable({\n data: initialData,\n}: {\n data: z.infer[]\n}) {\n const [data, setData] = React.useState(() => initialData)\n const [rowSelection, setRowSelection] = React.useState({})\n const [columnVisibility, setColumnVisibility] =\n React.useState({})\n const [columnFilters, setColumnFilters] = React.useState(\n []\n )\n const [sorting, setSorting] = React.useState([])\n const [pagination, setPagination] = React.useState({\n pageIndex: 0,\n pageSize: 10,\n })\n const sortableId = React.useId()\n const sensors = useSensors(\n useSensor(MouseSensor, {}),\n useSensor(TouchSensor, {}),\n useSensor(KeyboardSensor, {})\n )\n const dataIds = React.useMemo(\n () => data?.map(({ id }) => id) || [],\n [data]\n )\n const table = useReactTable({\n data,\n columns,\n state: {\n sorting,\n columnVisibility,\n rowSelection,\n columnFilters,\n pagination,\n },\n getRowId: (row) => row.id.toString(),\n enableRowSelection: true,\n onRowSelectionChange: setRowSelection,\n onSortingChange: setSorting,\n onColumnFiltersChange: setColumnFilters,\n onColumnVisibilityChange: setColumnVisibility,\n onPaginationChange: setPagination,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFacetedRowModel: getFacetedRowModel(),\n getFacetedUniqueValues: getFacetedUniqueValues(),\n })\n function handleDragEnd(event: DragEndEvent) {\n const { active, over } = event\n if (active && over && active.id !== over.id) {\n setData((data) => {\n const oldIndex = dataIds.indexOf(active.id)\n const newIndex = dataIds.indexOf(over.id)\n return arrayMove(data, oldIndex, newIndex)\n })\n }\n }\n return (\n \n
\n \n \n \n \n \n \n \n Outline\n Past Performance\n Key Personnel\n Focus Documents\n \n \n \n \n Outline\n \n Past Performance 3\n \n \n Key Personnel 2\n \n Focus Documents\n \n
\n \n }\n >\n \n Columns\n \n \n \n {table\n .getAllColumns()\n .filter(\n (column) =>\n typeof column.accessorFn !== \"undefined\" &&\n column.getCanHide()\n )\n .map((column) => {\n return (\n \n column.toggleVisibility(!!value)\n }\n >\n {column.id}\n \n )\n })}\n \n \n \n
\n
\n \n
\n \n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => {\n return (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n \n )\n })}\n \n ))}\n \n \n {table.getRowModel().rows?.length ? (\n \n {table.getRowModel().rows.map((row) => (\n \n ))}\n \n ) : (\n \n \n No results.\n \n \n )}\n \n
\n \n
\n
\n
\n {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n {table.getFilteredRowModel().rows.length} row(s) selected.\n
\n
\n
\n \n {\n table.setPageSize(Number(value))\n }}\n items={[10, 20, 30, 40, 50].map((pageSize) => ({\n label: `${pageSize}`,\n value: `${pageSize}`,\n }))}\n >\n \n \n \n \n \n {[10, 20, 30, 40, 50].map((pageSize) => (\n \n {pageSize}\n \n ))}\n \n \n \n
\n
\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {table.getPageCount()}\n
\n
\n table.setPageIndex(0)}\n disabled={!table.getCanPreviousPage()}\n >\n Go to first page\n \n \n table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n >\n Go to previous page\n \n \n table.nextPage()}\n disabled={!table.getCanNextPage()}\n >\n Go to next page\n \n \n table.setPageIndex(table.getPageCount() - 1)}\n disabled={!table.getCanNextPage()}\n >\n Go to last page\n \n \n
\n
\n
\n \n \n
\n \n \n
\n
\n \n
\n \n \n )\n}\nconst chartData = [\n {\n month: \"January\",\n desktop: 186,\n mobile: 80,\n },\n {\n month: \"February\",\n desktop: 305,\n mobile: 200,\n },\n {\n month: \"March\",\n desktop: 237,\n mobile: 120,\n },\n {\n month: \"April\",\n desktop: 73,\n mobile: 190,\n },\n {\n month: \"May\",\n desktop: 209,\n mobile: 130,\n },\n {\n month: \"June\",\n desktop: 214,\n mobile: 140,\n },\n]\nconst chartConfig = {\n desktop: {\n label: \"Desktop\",\n color: \"var(--primary)\",\n },\n mobile: {\n label: \"Mobile\",\n color: \"var(--primary)\",\n },\n} satisfies ChartConfig\nfunction TableCellViewer({ item }: { item: z.infer }) {\n const isMobile = useIsMobile()\n return (\n \n \n \n \n \n \n {item.header}\n \n Showing total visitors for the last 6 months\n \n \n
\n {!isMobile && (\n <>\n \n \n \n value.slice(0, 3)}\n hide\n />\n }\n />\n \n \n \n \n \n
\n
\n Trending up by 5.2% this month{\" \"}\n \n
\n
\n Showing total visitors for the last 6 months. This is just\n some random text to test the layout. It spans multiple lines\n and should wrap around.\n
\n
\n \n \n )}\n
\n
\n \n \n
\n
\n
\n \n \n \n \n \n \n \n \n Table of Contents\n \n \n Executive Summary\n \n \n Technical Approach\n \n Design\n Capabilities\n \n Focus Documents\n \n Narrative\n Cover Page\n \n \n \n
\n
\n \n \n \n \n \n \n \n Done\n In Progress\n Not Started\n \n \n \n
\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n \n \n \n \n \n \n \n Eddie Lake\n \n Jamik Tashpulatov\n \n Emily Whalen\n \n \n \n
\n
\n
\n \n \n \n \n )\n}\nconst columns: ColumnDef>[] = [\n {\n id: \"drag\",\n header: () => null,\n cell: ({ row }) => ,\n },\n {\n id: \"select\",\n header: ({ table }) => (\n
\n table.toggleAllPageRowsSelected(!!value)}\n aria-label=\"Select all\"\n />\n
\n ),\n cell: ({ row }) => (\n
\n row.toggleSelected(!!value)}\n aria-label=\"Select row\"\n />\n
\n ),\n enableSorting: false,\n enableHiding: false,\n },\n {\n accessorKey: \"header\",\n header: \"Header\",\n cell: ({ row }) => {\n return \n },\n enableHiding: false,\n },\n {\n accessorKey: \"type\",\n header: \"Section Type\",\n cell: ({ row }) => (\n
\n \n {row.original.type}\n \n
\n ),\n },\n {\n accessorKey: \"status\",\n header: \"Status\",\n cell: ({ row }) => (\n \n {row.original.status === \"Done\" ? (\n \n ) : (\n \n )}\n {row.original.status}\n \n ),\n },\n {\n accessorKey: \"target\",\n header: () =>
Target
,\n cell: ({ row }) => (\n {\n e.preventDefault()\n toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {\n loading: `Saving ${row.original.header}`,\n success: \"Done\",\n error: \"Error\",\n })\n }}\n >\n \n \n \n ),\n },\n {\n accessorKey: \"limit\",\n header: () =>
Limit
,\n cell: ({ row }) => (\n {\n e.preventDefault()\n toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {\n loading: `Saving ${row.original.header}`,\n success: \"Done\",\n error: \"Error\",\n })\n }}\n >\n \n \n \n ),\n },\n {\n accessorKey: \"reviewer\",\n header: \"Reviewer\",\n cell: ({ row }) => {\n const isAssigned = row.original.reviewer !== \"Assign reviewer\"\n if (isAssigned) {\n return row.original.reviewer\n }\n return (\n <>\n \n \n \n \n \n \n \n Eddie Lake\n \n Jamik Tashpulatov\n \n \n \n \n \n )\n },\n },\n {\n id: \"actions\",\n cell: () => (\n \n \n }\n >\n \n Open menu\n \n \n Edit\n Make a copy\n Favorite\n \n Delete\n \n \n ),\n },\n]\nfunction DraggableRow({ row }: { row: Row> }) {\n const { transform, transition, setNodeRef, isDragging } = useSortable({\n id: row.original.id,\n })\n return (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ))}\n \n )\n}\nexport function DataTable({\n data: initialData,\n}: {\n data: z.infer[]\n}) {\n const [data, setData] = React.useState(() => initialData)\n const [rowSelection, setRowSelection] = React.useState({})\n const [columnVisibility, setColumnVisibility] =\n React.useState({})\n const [columnFilters, setColumnFilters] = React.useState(\n []\n )\n const [sorting, setSorting] = React.useState([])\n const [pagination, setPagination] = React.useState({\n pageIndex: 0,\n pageSize: 10,\n })\n const sortableId = React.useId()\n const sensors = useSensors(\n useSensor(MouseSensor, {}),\n useSensor(TouchSensor, {}),\n useSensor(KeyboardSensor, {})\n )\n const dataIds = React.useMemo(\n () => data?.map(({ id }) => id) || [],\n [data]\n )\n const table = useReactTable({\n data,\n columns,\n state: {\n sorting,\n columnVisibility,\n rowSelection,\n columnFilters,\n pagination,\n },\n getRowId: (row) => row.id.toString(),\n enableRowSelection: true,\n onRowSelectionChange: setRowSelection,\n onSortingChange: setSorting,\n onColumnFiltersChange: setColumnFilters,\n onColumnVisibilityChange: setColumnVisibility,\n onPaginationChange: setPagination,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFacetedRowModel: getFacetedRowModel(),\n getFacetedUniqueValues: getFacetedUniqueValues(),\n })\n function handleDragEnd(event: DragEndEvent) {\n const { active, over } = event\n if (active && over && active.id !== over.id) {\n setData((data) => {\n const oldIndex = dataIds.indexOf(active.id)\n const newIndex = dataIds.indexOf(over.id)\n return arrayMove(data, oldIndex, newIndex)\n })\n }\n }\n return (\n \n
\n \n \n \n \n \n \n \n Outline\n Past Performance\n Key Personnel\n Focus Documents\n \n \n \n \n Outline\n \n Past Performance 3\n \n \n Key Personnel 2\n \n Focus Documents\n \n
\n \n }\n >\n \n Columns\n \n \n \n {table\n .getAllColumns()\n .filter(\n (column) =>\n typeof column.accessorFn !== \"undefined\" &&\n column.getCanHide()\n )\n .map((column) => {\n return (\n \n column.toggleVisibility(!!value)\n }\n >\n {column.id}\n \n )\n })}\n \n \n \n
\n
\n \n
\n \n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => {\n return (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n \n )\n })}\n \n ))}\n \n \n {table.getRowModel().rows?.length ? (\n \n {table.getRowModel().rows.map((row) => (\n \n ))}\n \n ) : (\n \n \n No results.\n \n \n )}\n \n
\n \n
\n
\n
\n {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n {table.getFilteredRowModel().rows.length} row(s) selected.\n
\n
\n
\n \n {\n table.setPageSize(Number(value))\n }}\n items={[10, 20, 30, 40, 50].map((pageSize) => ({\n label: `${pageSize}`,\n value: `${pageSize}`,\n }))}\n >\n \n \n \n \n \n {[10, 20, 30, 40, 50].map((pageSize) => (\n \n {pageSize}\n \n ))}\n \n \n \n
\n
\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {table.getPageCount()}\n
\n
\n table.setPageIndex(0)}\n disabled={!table.getCanPreviousPage()}\n >\n Go to first page\n \n \n table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n >\n Go to previous page\n \n \n table.nextPage()}\n disabled={!table.getCanNextPage()}\n >\n Go to next page\n \n \n table.setPageIndex(table.getPageCount() - 1)}\n disabled={!table.getCanNextPage()}\n >\n Go to last page\n \n \n
\n
\n
\n \n \n
\n \n \n
\n
\n \n
\n \n \n )\n}\nconst chartData = [\n {\n month: \"January\",\n desktop: 186,\n mobile: 80,\n },\n {\n month: \"February\",\n desktop: 305,\n mobile: 200,\n },\n {\n month: \"March\",\n desktop: 237,\n mobile: 120,\n },\n {\n month: \"April\",\n desktop: 73,\n mobile: 190,\n },\n {\n month: \"May\",\n desktop: 209,\n mobile: 130,\n },\n {\n month: \"June\",\n desktop: 214,\n mobile: 140,\n },\n]\nconst chartConfig = {\n desktop: {\n label: \"Desktop\",\n color: \"var(--primary)\",\n },\n mobile: {\n label: \"Mobile\",\n color: \"var(--primary)\",\n },\n} satisfies ChartConfig\nfunction TableCellViewer({ item }: { item: z.infer }) {\n const isMobile = useIsMobile()\n return (\n \n \n }\n >\n {item.header}\n \n \n \n {item.header}\n \n Showing total visitors for the last 6 months\n \n \n
\n {!isMobile && (\n <>\n \n \n \n value.slice(0, 3)}\n hide\n />\n }\n />\n \n \n \n \n \n
\n
\n Trending up by 5.2% this month{\" \"}\n \n
\n
\n Showing total visitors for the last 6 months. This is just\n some random text to test the layout. It spans multiple lines\n and should wrap around.\n
\n
\n \n \n )}\n
\n
\n \n \n
\n
\n
\n \n \n \n \n \n \n \n \n Table of Contents\n \n \n Executive Summary\n \n \n Technical Approach\n \n Design\n Capabilities\n \n Focus Documents\n \n Narrative\n Cover Page\n \n \n \n
\n
\n \n \n \n \n \n \n \n Done\n In Progress\n Not Started\n \n \n \n
\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n \n \n \n \n \n \n \n Eddie Lake\n \n Jamik Tashpulatov\n \n Emily Whalen\n \n \n \n
\n
\n
\n \n \n }>Done\n \n
\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/v4/public/r/styles/base-luma/drawer-example.json b/apps/v4/public/r/styles/base-luma/drawer-example.json index 3902cd4efb..1c4b4e1622 100644 --- a/apps/v4/public/r/styles/base-luma/drawer-example.json +++ b/apps/v4/public/r/styles/base-luma/drawer-example.json @@ -9,7 +9,7 @@ "files": [ { "path": "registry/base-luma/examples/drawer-example.tsx", - "content": "\"use client\"\n\nimport {\n Example,\n ExampleWrapper,\n} from \"@/registry/base-luma/components/example\"\nimport { Button } from \"@/registry/base-luma/ui/button\"\nimport {\n Drawer,\n DrawerClose,\n DrawerContent,\n DrawerDescription,\n DrawerFooter,\n DrawerHeader,\n DrawerTitle,\n DrawerTrigger,\n} from \"@/registry/base-luma/ui/drawer\"\n\nexport default function DrawerExample() {\n return (\n \n \n \n \n )\n}\n\nconst DRAWER_SIDES = [\"top\", \"right\", \"bottom\", \"left\"] as const\n\nfunction DrawerWithSides() {\n return (\n \n
\n {DRAWER_SIDES.map((side) => (\n \n \n \n \n \n \n Move Goal\n \n Set your daily activity goal.\n \n \n
\n {Array.from({ length: 10 }).map((_, index) => (\n \n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed\n do eiusmod tempor incididunt ut labore et dolore magna\n aliqua. Ut enim ad minim veniam, quis nostrud exercitation\n ullamco laboris nisi ut aliquip ex ea commodo consequat.\n Duis aute irure dolor in reprehenderit in voluptate velit\n esse cillum dolore eu fugiat nulla pariatur. Excepteur sint\n occaecat cupidatat non proident, sunt in culpa qui officia\n deserunt mollit anim id est laborum.\n

\n ))}\n
\n \n \n \n \n \n \n
\n \n ))}\n
\n
\n )\n}\n\nfunction DrawerScrollableContent() {\n return (\n \n \n \n \n \n \n \n Move Goal\n Set your daily activity goal.\n \n
\n {Array.from({ length: 10 }).map((_, index) => (\n \n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do\n eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut\n enim ad minim veniam, quis nostrud exercitation ullamco laboris\n nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor\n in reprehenderit in voluptate velit esse cillum dolore eu fugiat\n nulla pariatur. Excepteur sint occaecat cupidatat non proident,\n sunt in culpa qui officia deserunt mollit anim id est laborum.\n

\n ))}\n
\n \n \n \n \n \n \n
\n
\n
\n )\n}\n", + "content": "\"use client\"\n\nimport {\n Example,\n ExampleWrapper,\n} from \"@/registry/base-luma/components/example\"\nimport { Button } from \"@/registry/base-luma/ui/button\"\nimport {\n Drawer,\n DrawerClose,\n DrawerContent,\n DrawerDescription,\n DrawerFooter,\n DrawerHeader,\n DrawerTitle,\n DrawerTrigger,\n} from \"@/registry/base-luma/ui/drawer\"\n\nconst PARAGRAPHS = [\n \"Your changes are saved automatically as you type.\",\n \"Delivery usually takes three to five business days, depending on your location and the shipping method you selected at checkout.\",\n \"We use your email address for account notifications and order updates. You can change this anytime from your profile settings.\",\n \"Two-factor authentication adds an extra layer of security to your account. When enabled, you'll need to enter a code from your authenticator app in addition to your password. We recommend enabling it if you store payment methods or sensitive information.\",\n \"By continuing, you agree to our Terms of Service and Privacy Policy. We collect usage data to improve the product, personalize your experience, and troubleshoot issues. You can export or delete your data at any time from the account settings page. Third-party integrations may have their own policies, and you should review them before connecting external services.\",\n \"Refunds are processed within five to ten business days after we receive your return. Items must be unused and in their original packaging to qualify. Shipping costs are non-refundable unless the return is due to our error or a defective product. Once approved, the refund is issued to your original payment method; credit card refunds may take an additional billing cycle to appear on your statement. If you paid with store credit, the balance is restored to your account immediately. Contact support if you haven't received your refund after two weeks.\",\n \"Last updated March 12, 2026.\",\n \"Upgrade to Pro for unlimited projects, priority support, and advanced analytics. Cancel anytime from billing settings.\",\n \"You haven't verified your email yet. Check your inbox for a confirmation link—we sent it when you signed up. The link expires after 24 hours, but you can request a new one below.\",\n \"API requests are rate-limited to 1,000 calls per hour on the free plan. Exceeding the limit returns a 429 response with a Retry-After header. Upgrade to a paid plan for higher limits and dedicated support. Webhook deliveries are retried up to three times with exponential backoff if your endpoint returns a non-2xx status.\",\n \"No payment method on file.\",\n \"Our design team rebuilt the checkout flow last quarter after interviews with forty-two customers. The biggest friction point was surprise fees at the final step, so we moved shipping and tax estimates earlier in the process. Early tests show a twelve percent drop in cart abandonment. We're still rolling out the update region by region, and you may see the old flow until your account is migrated.\",\n]\n\nexport default function DrawerExample() {\n return (\n \n \n \n \n \n \n \n \n \n \n )\n}\n\nfunction DrawerDemo() {\n return (\n \n
\n \n }>\n Open Drawer\n \n \n
\n
\n
\n \n \n \n }>\n Header\n \n \n \n Edit profile\n \n Make changes to your profile here. Click save when you're\n done.\n \n \n
\n
\n
\n \n \n \n }>\n Footer\n \n \n
\n
\n
\n \n \n }>\n Cancel\n \n \n \n \n \n }>\n Header and Footer\n \n \n \n Edit profile\n \n Make changes to your profile here. Click save when you're\n done.\n \n \n
\n
\n
\n \n \n }>\n Cancel\n \n \n \n \n \n }>\n Edge to Edge\n \n \n
\n \n \n
\n \n )\n}\n\nfunction DrawerSwipeHandleExample() {\n return (\n \n
\n {DRAWER_SIDES.map((side) => (\n \n }\n >\n {side}\n \n \n \n Drawer\n \n Drawer with a swipe handle.\n \n \n
\n
\n
\n \n \n ))}\n
\n \n )\n}\n\nfunction DrawerCustomWidthAndHeight() {\n return (\n \n
\n \n }>\n Down\n \n \n \n Down drawer\n \n Drawer with a custom height.\n \n \n
\n {Array.from({ length: 10 }).map((_, index) => (\n \n {PARAGRAPHS[index % PARAGRAPHS.length]}\n

\n ))}\n
\n \n }>\n Close\n \n \n
\n
\n \n }>\n Up\n \n \n \n Up drawer\n \n Drawer with a custom height.\n \n \n
\n {Array.from({ length: 10 }).map((_, index) => (\n \n {PARAGRAPHS[index % PARAGRAPHS.length]}\n

\n ))}\n
\n \n }>\n Close\n \n \n
\n
\n \n }>\n Left\n \n \n \n Left drawer\n Drawer with a custom width.\n \n
\n {Array.from({ length: 10 }).map((_, index) => (\n \n {PARAGRAPHS[index % PARAGRAPHS.length]}\n

\n ))}\n
\n \n }>\n Close\n \n \n
\n
\n \n }>\n Right\n \n \n \n Right drawer\n Drawer with a custom width.\n \n
\n {Array.from({ length: 10 }).map((_, index) => (\n \n {PARAGRAPHS[index % PARAGRAPHS.length]}\n

\n ))}\n
\n \n }>\n Close\n \n \n
\n
\n
\n
\n )\n}\n\nconst DRAWER_SIDES = [\"up\", \"right\", \"down\", \"left\"] as const\n\nfunction DrawerPosition() {\n return (\n \n
\n {DRAWER_SIDES.map((side) => (\n \n }\n >\n {side}\n \n \n \n Move Goal\n \n Set your daily activity goal.\n \n \n
\n
\n
\n \n \n }>\n Cancel\n \n \n \n \n ))}\n
\n \n )\n}\n\nfunction DrawerScrollable() {\n return (\n \n
\n {DRAWER_SIDES.map((side) => (\n \n }\n >\n {side}\n \n \n \n Move Goal\n \n Set your daily activity goal.\n \n \n
\n {Array.from({ length: 20 }).map((_, index) => (\n \n {PARAGRAPHS[index % PARAGRAPHS.length]}\n

\n ))}\n
\n \n \n }>\n Cancel\n \n \n
\n
\n ))}\n
\n
\n )\n}\n\nconst SNAP_POINTS = [\"31rem\", 1]\n\nfunction DrawerSnapPoints() {\n return (\n \n \n }>\n Open Snap Drawer\n \n \n \n Snap points\n \n Drag the drawer to snap between a compact peek and a near\n full-height view.\n \n \n
\n {Array.from({ length: 16 }).map((_, index) => (\n
\n ))}\n
\n \n \n \n )\n}\n\nfunction DrawerNested() {\n return (\n \n
\n {DRAWER_SIDES.map((side) => (\n \n }\n >\n {side}\n \n \n \n {side} drawer\n \n Open another drawer from the same direction.\n \n \n
\n
\n
\n \n \n }>\n Open nested drawer\n \n \n \n Nested drawer\n \n The parent drawer stays mounted behind this one.\n \n \n
\n
\n
\n \n \n }>\n Open third drawer\n \n \n \n Third drawer\n \n This is the frontmost drawer in the stack.\n \n \n
\n
\n
\n \n }>\n Close\n \n \n \n \n }>\n Close\n \n \n \n \n }>\n Close\n \n \n \n \n ))}\n
\n \n )\n}\n\nfunction DrawerNonModal() {\n return (\n \n \n }>\n Non Modal\n \n \n \n Non Modal Drawer\n \n
\n
\n
\n \n }>\n Close\n \n \n \n \n \n )\n}\n", "type": "registry:example" } ], diff --git a/apps/v4/public/r/styles/base-luma/drawer.json b/apps/v4/public/r/styles/base-luma/drawer.json index e286adaeca..4c7f2cc3d2 100644 --- a/apps/v4/public/r/styles/base-luma/drawer.json +++ b/apps/v4/public/r/styles/base-luma/drawer.json @@ -2,12 +2,12 @@ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "drawer", "dependencies": [ - "vaul" + "@base-ui/react" ], "files": [ { "path": "registry/base-luma/ui/drawer.tsx", - "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Drawer as DrawerPrimitive } from \"vaul\"\n\nimport { cn } from \"@/registry/base-luma/lib/utils\"\n\nfunction Drawer({\n ...props\n}: React.ComponentProps) {\n return \n}\n\nfunction DrawerTrigger({\n ...props\n}: React.ComponentProps) {\n return \n}\n\nfunction DrawerPortal({\n ...props\n}: React.ComponentProps) {\n return \n}\n\nfunction DrawerClose({\n ...props\n}: React.ComponentProps) {\n return \n}\n\nfunction DrawerOverlay({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n )\n}\n\nfunction DrawerContent({\n className,\n children,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n