add drawer component

This commit is contained in:
2026-02-24 08:52:14 +01:00
parent 117d44718e
commit dd44f053f9
5 changed files with 226 additions and 12 deletions

View File

@@ -10,7 +10,7 @@
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.0.0", "@base-ui/react": "^1.2.0",
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@fontsource/instrument-serif": "^5.2.8", "@fontsource/instrument-serif": "^5.2.8",
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",

View File

@@ -0,0 +1,148 @@
import { DrawerPreview as BaseDrawer } from "@base-ui/react/drawer";
import { tv } from "tailwind-variants";
const drawerStyles = tv({
slots: {
viewport: "fixed inset-y-0 right-0 pointer-events-none",
popup: [
"pointer-events-auto",
"fixed top-0 right-0 h-dvh w-[400px]",
"bg-card text-card-foreground border-l border-border shadow-lg",
"flex flex-col",
"transition-transform duration-100 ease-out",
],
title: "text-lg font-semibold",
description: "text-sm text-muted-foreground",
close: [
"focus-visible:ring-ring focus-visible:outline-none",
"focus-visible:ring-2 focus-visible:ring-offset-2",
],
content: "flex-1 overflow-y-auto",
},
});
const { viewport, popup, title, description, close, content } = drawerStyles();
type DrawerRootProps = React.ComponentProps<typeof BaseDrawer.Root>;
function DrawerRoot(props: DrawerRootProps) {
return <BaseDrawer.Root modal={false} swipeDirection="right" {...props} />;
}
type DrawerTriggerProps = React.ComponentProps<typeof BaseDrawer.Trigger>;
function DrawerTrigger({ className, ...props }: DrawerTriggerProps) {
return <BaseDrawer.Trigger className={className} {...props} />;
}
type DrawerPortalProps = React.ComponentProps<typeof BaseDrawer.Portal>;
function DrawerPortal(props: DrawerPortalProps) {
return <BaseDrawer.Portal {...props} />;
}
type DrawerViewportProps = React.ComponentProps<typeof BaseDrawer.Viewport>;
function DrawerViewport({ className, ...props }: DrawerViewportProps) {
return (
<BaseDrawer.Viewport
className={(state) =>
viewport({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type DrawerPopupProps = React.ComponentProps<typeof BaseDrawer.Popup>;
function DrawerPopup({ className, ...props }: DrawerPopupProps) {
return (
<BaseDrawer.Popup
data-drawer-popup=""
className={(state) =>
popup({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type DrawerTitleProps = React.ComponentProps<typeof BaseDrawer.Title>;
function DrawerTitle({ className, ...props }: DrawerTitleProps) {
return (
<BaseDrawer.Title
className={(state) =>
title({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type DrawerDescriptionProps = React.ComponentProps<
typeof BaseDrawer.Description
>;
function DrawerDescription({ className, ...props }: DrawerDescriptionProps) {
return (
<BaseDrawer.Description
className={(state) =>
description({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type DrawerCloseProps = React.ComponentProps<typeof BaseDrawer.Close>;
function DrawerClose({ className, ...props }: DrawerCloseProps) {
return (
<BaseDrawer.Close
className={(state) =>
close({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
type DrawerContentProps = React.ComponentProps<typeof BaseDrawer.Content>;
function DrawerContent({ className, ...props }: DrawerContentProps) {
return (
<BaseDrawer.Content
className={(state) =>
content({
class: typeof className === "function" ? className(state) : className,
})
}
{...props}
/>
);
}
export {
DrawerRoot,
DrawerTrigger,
DrawerPortal,
DrawerViewport,
DrawerPopup,
DrawerTitle,
DrawerDescription,
DrawerClose,
DrawerContent,
drawerStyles,
};

View File

@@ -129,3 +129,8 @@
@apply font-sans; @apply font-sans;
} }
} }
[data-drawer-popup][data-starting-style],
[data-drawer-popup][data-ending-style] {
translate: 100% 0;
}

View File

@@ -1,8 +1,19 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Heart, Search } from "lucide-react"; import { Heart, Search, X } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerPopup,
DrawerPortal,
DrawerRoot,
DrawerTitle,
DrawerTrigger,
DrawerViewport,
} from "@/components/ui/drawer";
import { import {
FieldControl, FieldControl,
FieldDescription, FieldDescription,
@@ -166,6 +177,57 @@ function DemoPage() {
</Button> </Button>
</div> </div>
</Section> </Section>
{/* Drawer */}
<Section title="Drawer">
<DrawerRoot>
<DrawerTrigger>
<Button variant="outline">Open Drawer</Button>
</DrawerTrigger>
<DrawerPortal>
<DrawerViewport>
<DrawerPopup>
<div className="flex items-center justify-between border-b border-border p-6">
<div className="space-y-1">
<DrawerTitle>Edit Item</DrawerTitle>
<DrawerDescription>
Update the details for this timeline item.
</DrawerDescription>
</div>
<DrawerClose>
<Button variant="outline" size="icon" aria-label="Close">
<X className="h-4 w-4" />
</Button>
</DrawerClose>
</div>
<DrawerContent>
<form
className="grid gap-4 p-6"
onSubmit={(e) => e.preventDefault()}
>
<FieldRoot>
<FieldLabel>Title</FieldLabel>
<FieldControl placeholder="Item title" />
</FieldRoot>
<FieldRoot>
<FieldLabel>Date</FieldLabel>
<FieldControl type="date" />
</FieldRoot>
<FieldRoot>
<FieldLabel>Description</FieldLabel>
<FieldControl placeholder="Describe this event" />
<FieldDescription>Optional</FieldDescription>
</FieldRoot>
<Button type="submit" className="w-fit">
Save
</Button>
</form>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</DrawerRoot>
</Section>
</div> </div>
); );
} }

19
pnpm-lock.yaml generated
View File

@@ -58,8 +58,8 @@ importers:
apps/web: apps/web:
dependencies: dependencies:
'@base-ui/react': '@base-ui/react':
specifier: ^1.0.0 specifier: ^1.2.0
version: 1.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 1.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@fontsource-variable/inter': '@fontsource-variable/inter':
specifier: ^5.2.8 specifier: ^5.2.8
version: 5.2.8 version: 5.2.8
@@ -468,8 +468,8 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@base-ui/react@1.1.0': '@base-ui/react@1.2.0':
resolution: {integrity: sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw==} resolution: {integrity: sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
'@types/react': ^17 || ^18 || ^19 '@types/react': ^17 || ^18 || ^19
@@ -479,8 +479,8 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@base-ui/utils@0.2.4': '@base-ui/utils@0.2.5':
resolution: {integrity: sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng==} resolution: {integrity: sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw==}
peerDependencies: peerDependencies:
'@types/react': ^17 || ^18 || ^19 '@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19 react: ^17 || ^18 || ^19
@@ -4821,21 +4821,20 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
'@base-ui/react@1.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': '@base-ui/react@1.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6
'@base-ui/utils': 0.2.4(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@base-ui/utils': 0.2.5(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@floating-ui/react-dom': 2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@floating-ui/react-dom': 2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@floating-ui/utils': 0.2.10 '@floating-ui/utils': 0.2.10
react: 19.2.3 react: 19.2.3
react-dom: 19.2.3(react@19.2.3) react-dom: 19.2.3(react@19.2.3)
reselect: 5.1.1
tabbable: 6.4.0 tabbable: 6.4.0
use-sync-external-store: 1.6.0(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3)
optionalDependencies: optionalDependencies:
'@types/react': 19.2.7 '@types/react': 19.2.7
'@base-ui/utils@0.2.4(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': '@base-ui/utils@0.2.5(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6
'@floating-ui/utils': 0.2.10 '@floating-ui/utils': 0.2.10