Extract Base UI Drawer into the v2 kit

Promote the portal burger shell into a reusable
Drawer matching Dialog (thin Base UI + tv slots).
TopBarMobileNav now composes the kit parts.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-20 16:47:47 +02:00
parent ea8377a0cc
commit d047f1b323
14 changed files with 683 additions and 136 deletions

View File

@@ -0,0 +1,124 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useState } from "react";
import { Button } from "../Button/Button";
import { Text } from "../typography/Text";
import { Drawer } from "./Drawer";
import { DrawerBody } from "./DrawerBody";
import { DrawerClose } from "./DrawerClose";
import { DrawerDescription } from "./DrawerDescription";
import { DrawerFooter } from "./DrawerFooter";
import { DrawerHeader } from "./DrawerHeader";
import { DrawerPopup } from "./DrawerPopup";
import { DrawerSkeleton } from "./DrawerSkeleton";
import { DrawerTitle } from "./DrawerTitle";
import { DrawerTrigger } from "./DrawerTrigger";
export default {
title: "v2/Drawer",
component: Drawer,
};
export function Default() {
return (
<Drawer swipeDirection="right">
<DrawerTrigger render={<Button variant="solid" color="neutral" highContrast>Open drawer</Button>} />
<DrawerPopup side="right">
<DrawerHeader>
<DrawerTitle>Menu</DrawerTitle>
<DrawerClose render={<Button variant="soft" color="neutral" highContrast>Close</Button>} />
</DrawerHeader>
<DrawerBody>
<Text size={2} color="neutral">
Drawer body content navigation links, filters, or any other slot
the surrounding page composes.
</Text>
</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button variant="solid" color="neutral" highContrast className="w-full">Done</Button>} />
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
export function BottomSheet() {
return (
<Drawer swipeDirection="down">
<DrawerTrigger render={<Button variant="soft" color="neutral" highContrast>Open bottom sheet</Button>} />
<DrawerPopup side="bottom">
<DrawerHeader>
<DrawerTitle>Options</DrawerTitle>
<DrawerClose render={<Button variant="soft" color="neutral" highContrast>Close</Button>} />
</DrawerHeader>
<DrawerBody>
<DrawerDescription>
Swipe down to dismiss.
</DrawerDescription>
</DrawerBody>
<DrawerFooter>
<DrawerClose render={<Button variant="solid" color="neutral" highContrast className="w-full">Confirm</Button>} />
</DrawerFooter>
</DrawerPopup>
</Drawer>
);
}
export function Controlled() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-start gap-3">
<Button variant="soft" color="neutral" highContrast onClick={() => setOpen(true)}>
Open controlled drawer
</Button>
<Text size={1} color="faint">{open ? "open" : "closed"}</Text>
<Drawer open={open} onOpenChange={setOpen} swipeDirection="right">
<DrawerPopup side="right">
<DrawerHeader>
<DrawerTitle>Account</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<Text size={2} color="neutral">
Controlled the same way as Base UI `open` / `onOpenChange`.
</Text>
</DrawerBody>
<DrawerFooter>
<Button variant="solid" color="neutral" highContrast className="w-full" onClick={() => setOpen(false)}>
Close
</Button>
</DrawerFooter>
</DrawerPopup>
</Drawer>
</div>
);
}
export function Skeleton() {
return (
<div className="flex h-80 justify-end border border-sand-6 bg-sand-2">
<DrawerSkeleton />
</div>
);
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Drawer as BaseDrawer } from "@base-ui/react/drawer";
import type { ComponentProps } from "react";
// Root of the edge drawer. Controlled the same way as Base UI's Drawer:
// `open` / `onOpenChange`, plus `swipeDirection` for dismiss gestures. Pair
// `swipeDirection` with `DrawerPopup`'s `side` (right↔right, left↔left,
// bottom↔down, top↔up). See contrib/claude/ui.md.
export type DrawerProps = ComponentProps<typeof BaseDrawer.Root>;
export function Drawer(props: DrawerProps) {
return <BaseDrawer.Root {...props} />;
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { ComponentProps } from "react";
import { drawer } from "./variants";
// Main content region between the header and footer.
export function DrawerBody(props: ComponentProps<"div">) {
const { className, ...rest } = props;
const { body } = drawer();
return <div className={body({ className })} {...rest} />;
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Drawer as BaseDrawer } from "@base-ui/react/drawer";
import type { ComponentProps } from "react";
// Closes the drawer. Pass `render` to reuse an existing control (e.g. an
// IconButton) as the close action, per Base UI's polymorphism.
export type DrawerCloseProps = ComponentProps<typeof BaseDrawer.Close>;
export function DrawerClose(props: DrawerCloseProps) {
return <BaseDrawer.Close {...props} />;
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Drawer as BaseDrawer } from "@base-ui/react/drawer";
import type { ComponentProps } from "react";
import { drawer } from "./variants";
// Accessible drawer description (wires `aria-describedby` via Base UI).
export type DrawerDescriptionProps
= Omit<ComponentProps<typeof BaseDrawer.Description>, "className"> & {
className?: string;
};
export function DrawerDescription(props: DrawerDescriptionProps) {
const { className, ...rest } = props;
const { description } = drawer();
return <BaseDrawer.Description className={description({ className })} {...rest} />;
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { ComponentProps } from "react";
import { drawer } from "./variants";
// Footer region pinned to the bottom of the drawer for primary actions.
export function DrawerFooter(props: ComponentProps<"div">) {
const { className, ...rest } = props;
const { footer } = drawer();
return <div className={footer({ className })} {...rest} />;
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { ComponentProps } from "react";
import { drawer } from "./variants";
// Header region grouping the title (and optional close control).
export function DrawerHeader(props: ComponentProps<"div">) {
const { className, ...rest } = props;
const { header } = drawer();
return <div className={header({ className })} {...rest} />;
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Drawer as BaseDrawer } from "@base-ui/react/drawer";
import type { ComponentProps } from "react";
import type { VariantProps } from "tailwind-variants/lite";
import { drawer } from "./variants";
export type DrawerPopupProps
= & Omit<ComponentProps<typeof BaseDrawer.Popup>, "className">
& VariantProps<typeof drawer>
& {
className?: string;
};
// Portal + dimmed backdrop + edge-aligned popup. Children compose the header /
// body / footer regions inside Base UI's Content (swipe-safe text selection).
export function DrawerPopup(props: DrawerPopupProps) {
const { className, children, side, ...popupProps } = props;
const slots = drawer({ side });
return (
<BaseDrawer.Portal>
<BaseDrawer.Backdrop className={slots.backdrop()} />
<BaseDrawer.Viewport className={slots.viewport()}>
<BaseDrawer.Popup className={slots.popup({ className })} {...popupProps}>
<BaseDrawer.Content className={slots.content()}>
{children}
</BaseDrawer.Content>
</BaseDrawer.Popup>
</BaseDrawer.Viewport>
</BaseDrawer.Portal>
);
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { TextSkeleton } from "../typography/TextSkeleton";
import { drawer, drawerSkeleton } from "./variants";
// Loading placeholder matching the drawer frame (header + body + footer),
// importing only variants and skeleton primitives — never Base UI.
export function DrawerSkeleton() {
const { header, body, footer } = drawer();
return (
<div className={drawerSkeleton()} aria-hidden>
<div className={header()}>
<TextSkeleton size={4} className="w-24" />
<TextSkeleton size={2} className="w-8" />
</div>
<div className={body()}>
<TextSkeleton size={2} className="w-full" />
<TextSkeleton size={2} className="w-3/4" />
<TextSkeleton size={2} className="w-5/6" />
</div>
<div className={footer()}>
<TextSkeleton size={2} className="w-full" />
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Drawer as BaseDrawer } from "@base-ui/react/drawer";
import type { ComponentProps } from "react";
import { drawer } from "./variants";
// Accessible drawer title (wires `aria-labelledby` via Base UI).
export type DrawerTitleProps = Omit<ComponentProps<typeof BaseDrawer.Title>, "className"> & {
className?: string;
};
export function DrawerTitle(props: DrawerTitleProps) {
const { className, ...rest } = props;
const { title } = drawer();
return <BaseDrawer.Title className={title({ className })} {...rest} />;
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Drawer as BaseDrawer } from "@base-ui/react/drawer";
import type { ComponentProps } from "react";
// Opens the drawer. Pass `render` to use an existing control (e.g. an
// IconButton) as the trigger, per Base UI's polymorphism.
export type DrawerTriggerProps = ComponentProps<typeof BaseDrawer.Trigger>;
export function DrawerTrigger(props: DrawerTriggerProps) {
return <BaseDrawer.Trigger {...props} />;
}

View File

@@ -0,0 +1,91 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { tv } from "tailwind-variants/lite";
// Edge drawer over Base UI. Positioning lives in CSS (`side`); pass the matching
// `swipeDirection` on `Drawer` (right↔right, left↔left, bottom↔down, top↔up).
export const drawer = tv({
slots: {
backdrop: [
"fixed inset-0 z-50 bg-sand-12/40",
"transition-opacity duration-200",
"data-starting-style:opacity-0 data-ending-style:opacity-0",
],
viewport: "fixed inset-0 z-50 flex",
popup: [
"relative flex flex-col bg-sand-1 shadow-6 outline-none",
"transition-transform duration-200",
],
content: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4",
header: "flex items-center justify-between gap-3",
title: "text-4 font-medium text-sand-12",
description: "text-2 text-sand-11",
body: "flex min-h-0 flex-1 flex-col gap-1",
footer: "mt-auto flex flex-col gap-2 border-t border-sand-a3 pt-4",
},
variants: {
side: {
right: {
viewport: "justify-end",
popup: [
"h-full w-[min(20rem,100%)]",
"transform-[translateX(var(--drawer-swipe-movement-x,0px))]",
"data-starting-style:translate-x-full data-ending-style:translate-x-full",
],
},
left: {
viewport: "justify-start",
popup: [
"h-full w-[min(20rem,100%)]",
"transform-[translateX(var(--drawer-swipe-movement-x,0px))]",
"data-starting-style:-translate-x-full data-ending-style:-translate-x-full",
],
},
bottom: {
viewport: "items-end justify-center",
popup: [
"w-full max-h-[min(90dvh,100%)] rounded-t-5",
"transform-[translateY(var(--drawer-swipe-movement-y,0px))]",
"data-starting-style:translate-y-full data-ending-style:translate-y-full",
],
},
top: {
viewport: "items-start justify-center",
popup: [
"w-full max-h-[min(90dvh,100%)] rounded-b-5",
"transform-[translateY(var(--drawer-swipe-movement-y,0px))]",
"data-starting-style:-translate-y-full data-ending-style:-translate-y-full",
],
},
},
},
defaultVariants: {
side: "right",
},
});
// Static frame matching the drawer popup, without Base UI positioning, so a
// placeholder can render before the interactive drawer loads.
export const drawerSkeleton = tv({
base: [
"flex h-full w-[min(20rem,100%)] flex-col gap-4 bg-sand-1 p-4 shadow-6",
],
});