From dd44f053f9e378d095c64232a17742022718edf4 Mon Sep 17 00:00:00 2001 From: Jonatan Granqvist Date: Tue, 24 Feb 2026 08:52:14 +0100 Subject: [PATCH] add drawer component --- apps/web/package.json | 2 +- apps/web/src/components/ui/drawer.tsx | 148 ++++++++++++++++++++++++++ apps/web/src/index.css | 5 + apps/web/src/routes/demo.tsx | 64 ++++++++++- pnpm-lock.yaml | 19 ++-- 5 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/components/ui/drawer.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 826ef18..239bba6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,7 +10,7 @@ "lint": "eslint ." }, "dependencies": { - "@base-ui/react": "^1.0.0", + "@base-ui/react": "^1.2.0", "@fontsource-variable/inter": "^5.2.8", "@fontsource/instrument-serif": "^5.2.8", "@tailwindcss/vite": "^4.1.8", diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx new file mode 100644 index 0000000..370144f --- /dev/null +++ b/apps/web/src/components/ui/drawer.tsx @@ -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; + +function DrawerRoot(props: DrawerRootProps) { + return ; +} + +type DrawerTriggerProps = React.ComponentProps; + +function DrawerTrigger({ className, ...props }: DrawerTriggerProps) { + return ; +} + +type DrawerPortalProps = React.ComponentProps; + +function DrawerPortal(props: DrawerPortalProps) { + return ; +} + +type DrawerViewportProps = React.ComponentProps; + +function DrawerViewport({ className, ...props }: DrawerViewportProps) { + return ( + + viewport({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type DrawerPopupProps = React.ComponentProps; + +function DrawerPopup({ className, ...props }: DrawerPopupProps) { + return ( + + popup({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type DrawerTitleProps = React.ComponentProps; + +function DrawerTitle({ className, ...props }: DrawerTitleProps) { + return ( + + title({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type DrawerDescriptionProps = React.ComponentProps< + typeof BaseDrawer.Description +>; + +function DrawerDescription({ className, ...props }: DrawerDescriptionProps) { + return ( + + description({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type DrawerCloseProps = React.ComponentProps; + +function DrawerClose({ className, ...props }: DrawerCloseProps) { + return ( + + close({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +type DrawerContentProps = React.ComponentProps; + +function DrawerContent({ className, ...props }: DrawerContentProps) { + return ( + + content({ + class: typeof className === "function" ? className(state) : className, + }) + } + {...props} + /> + ); +} + +export { + DrawerRoot, + DrawerTrigger, + DrawerPortal, + DrawerViewport, + DrawerPopup, + DrawerTitle, + DrawerDescription, + DrawerClose, + DrawerContent, + drawerStyles, +}; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 4bebe75..cbb196d 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -129,3 +129,8 @@ @apply font-sans; } } + +[data-drawer-popup][data-starting-style], +[data-drawer-popup][data-ending-style] { + translate: 100% 0; +} diff --git a/apps/web/src/routes/demo.tsx b/apps/web/src/routes/demo.tsx index b373b26..e4f6df6 100644 --- a/apps/web/src/routes/demo.tsx +++ b/apps/web/src/routes/demo.tsx @@ -1,8 +1,19 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Heart, Search } from "lucide-react"; +import { Heart, Search, X } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; +import { + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerPopup, + DrawerPortal, + DrawerRoot, + DrawerTitle, + DrawerTrigger, + DrawerViewport, +} from "@/components/ui/drawer"; import { FieldControl, FieldDescription, @@ -166,6 +177,57 @@ function DemoPage() { + + {/* Drawer */} +
+ + + + + + + +
+
+ Edit Item + + Update the details for this timeline item. + +
+ + + +
+ +
e.preventDefault()} + > + + Title + + + + Date + + + + Description + + Optional + + +
+
+
+
+
+
+
); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a888590..3e85b6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,8 +58,8 @@ importers: apps/web: dependencies: '@base-ui/react': - specifier: ^1.0.0 - version: 1.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.2.0 + 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': specifier: ^5.2.8 version: 5.2.8 @@ -468,8 +468,8 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@base-ui/react@1.1.0': - resolution: {integrity: sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw==} + '@base-ui/react@1.2.0': + resolution: {integrity: sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17 || ^18 || ^19 @@ -479,8 +479,8 @@ packages: '@types/react': optional: true - '@base-ui/utils@0.2.4': - resolution: {integrity: sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng==} + '@base-ui/utils@0.2.5': + resolution: {integrity: sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw==} peerDependencies: '@types/react': ^17 || ^18 || ^19 react: ^17 || ^18 || ^19 @@ -4821,21 +4821,20 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@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: '@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/utils': 0.2.10 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - reselect: 5.1.1 tabbable: 6.4.0 use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: '@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: '@babel/runtime': 7.28.6 '@floating-ui/utils': 0.2.10