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:
@@ -18,7 +18,6 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Drawer } from "@base-ui/react/drawer";
|
||||
import {
|
||||
BellIcon,
|
||||
BellRingingIcon,
|
||||
@@ -29,6 +28,14 @@ import {
|
||||
} from "@phosphor-icons/react";
|
||||
import { Button } from "@probo/ui/src/v2/Button/Button";
|
||||
import { Link } from "@probo/ui/src/v2/Button/Link";
|
||||
import { Drawer } from "@probo/ui/src/v2/Drawer/Drawer";
|
||||
import { DrawerBody } from "@probo/ui/src/v2/Drawer/DrawerBody";
|
||||
import { DrawerClose } from "@probo/ui/src/v2/Drawer/DrawerClose";
|
||||
import { DrawerFooter } from "@probo/ui/src/v2/Drawer/DrawerFooter";
|
||||
import { DrawerHeader } from "@probo/ui/src/v2/Drawer/DrawerHeader";
|
||||
import { DrawerPopup } from "@probo/ui/src/v2/Drawer/DrawerPopup";
|
||||
import { DrawerTitle } from "@probo/ui/src/v2/Drawer/DrawerTitle";
|
||||
import { DrawerTrigger } from "@probo/ui/src/v2/Drawer/DrawerTrigger";
|
||||
import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton";
|
||||
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import { useState } from "react";
|
||||
@@ -43,7 +50,7 @@ import { useSubscribeDialog } from "#/lib/mailingList/subscribeDialogContext";
|
||||
|
||||
import type { TopBarMobileNav_identity$key } from "./__generated__/TopBarMobileNav_identity.graphql";
|
||||
import { TOP_BAR_NAV_ITEMS } from "./navItems";
|
||||
import { topBar, topBarMobileNav } from "./variants";
|
||||
import { topBar } from "./variants";
|
||||
|
||||
const topBarMobileNavFragment = graphql`
|
||||
fragment TopBarMobileNav_identity on Identity {
|
||||
@@ -72,7 +79,6 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
|
||||
const identity = useFragment(topBarMobileNavFragment, identityKey);
|
||||
|
||||
const barSlots = topBar();
|
||||
const slots = topBarMobileNav();
|
||||
|
||||
const displayName = identity
|
||||
? (identity.fullName.trim() || identity.email)
|
||||
@@ -81,9 +87,9 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
|
||||
const close = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<Drawer.Root open={open} onOpenChange={setOpen} swipeDirection="right">
|
||||
<Drawer open={open} onOpenChange={setOpen} swipeDirection="right">
|
||||
<div className={barSlots.menuTrigger()}>
|
||||
<Drawer.Trigger
|
||||
<DrawerTrigger
|
||||
render={(
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
@@ -96,117 +102,110 @@ export function TopBarMobileNav({ identityKey }: TopBarMobileNavProps) {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Backdrop className={slots.backdrop()} />
|
||||
<Drawer.Viewport className={slots.viewport()}>
|
||||
<Drawer.Popup className={slots.popup()}>
|
||||
<Drawer.Content className={slots.content()}>
|
||||
<div className={slots.header()}>
|
||||
<Drawer.Title className={slots.title()}>
|
||||
{t("topBar.menuTitle")}
|
||||
</Drawer.Title>
|
||||
<Drawer.Close
|
||||
render={(
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size={2}
|
||||
aria-label={t("topBar.closeMenu")}
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DrawerPopup side="right">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{t("topBar.menuTitle")}</DrawerTitle>
|
||||
<DrawerClose
|
||||
render={(
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size={2}
|
||||
aria-label={t("topBar.closeMenu")}
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
/>
|
||||
</DrawerHeader>
|
||||
|
||||
<nav className={slots.nav()}>
|
||||
{TOP_BAR_NAV_ITEMS.map(item => (
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
<DrawerBody>
|
||||
<nav className="contents">
|
||||
{TOP_BAR_NAV_ITEMS.map(item => (
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size={3}
|
||||
active={isActive(pathname, item.to)}
|
||||
className="w-full justify-start"
|
||||
onClick={close}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</DrawerBody>
|
||||
|
||||
<DrawerFooter>
|
||||
{identity == null
|
||||
? (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="neutral"
|
||||
highContrast
|
||||
size={3}
|
||||
className="w-full"
|
||||
iconStart={<LockSimpleIcon />}
|
||||
onClick={() => {
|
||||
close();
|
||||
openSignIn({ continueTo: buildRequestAllContinueUrl() });
|
||||
}}
|
||||
>
|
||||
{t("topBar.getAccess")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{displayName
|
||||
? (
|
||||
<div className="flex flex-col gap-0.5 px-1 pb-2">
|
||||
<Text size={2} weight="medium" color="neutral" highContrast className="truncate">
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text size={1} color="faint" className="truncate">
|
||||
{identity.email}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
color={isSubscribed ? "green" : "gold"}
|
||||
size={3}
|
||||
active={isActive(pathname, item.to)}
|
||||
className="w-full justify-start"
|
||||
onClick={close}
|
||||
iconStart={isSubscribed ? <BellRingingIcon /> : <BellIcon />}
|
||||
disabled={isUnsubscribing}
|
||||
onClick={() => {
|
||||
close();
|
||||
if (isSubscribed) {
|
||||
void unsubscribe();
|
||||
return;
|
||||
}
|
||||
openSubscribe();
|
||||
}}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className={slots.actions()}>
|
||||
{identity == null
|
||||
? (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="neutral"
|
||||
highContrast
|
||||
size={3}
|
||||
className="w-full"
|
||||
iconStart={<LockSimpleIcon />}
|
||||
onClick={() => {
|
||||
close();
|
||||
openSignIn({ continueTo: buildRequestAllContinueUrl() });
|
||||
}}
|
||||
>
|
||||
{t("topBar.getAccess")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{displayName
|
||||
? (
|
||||
<div className="flex flex-col gap-0.5 px-1 pb-2">
|
||||
<Text size={2} weight="medium" color="neutral" highContrast className="truncate">
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text size={1} color="faint" className="truncate">
|
||||
{identity.email}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
color={isSubscribed ? "green" : "gold"}
|
||||
size={3}
|
||||
className="w-full justify-start"
|
||||
iconStart={isSubscribed ? <BellRingingIcon /> : <BellIcon />}
|
||||
disabled={isUnsubscribing}
|
||||
onClick={() => {
|
||||
close();
|
||||
if (isSubscribed) {
|
||||
void unsubscribe();
|
||||
return;
|
||||
}
|
||||
openSubscribe();
|
||||
}}
|
||||
>
|
||||
{isSubscribed ? t("userMenu.subscribed") : t("userMenu.subscribe")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="red"
|
||||
size={3}
|
||||
className="w-full justify-start"
|
||||
iconStart={<SignOutIcon />}
|
||||
disabled={isSigningOut}
|
||||
onClick={() => {
|
||||
close();
|
||||
void signOut();
|
||||
}}
|
||||
>
|
||||
{t("userMenu.signOut")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Drawer.Content>
|
||||
</Drawer.Popup>
|
||||
</Drawer.Viewport>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
{isSubscribed ? t("userMenu.subscribed") : t("userMenu.subscribe")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="red"
|
||||
size={3}
|
||||
className="w-full justify-start"
|
||||
iconStart={<SignOutIcon />}
|
||||
disabled={isSigningOut}
|
||||
onClick={() => {
|
||||
close();
|
||||
void signOut();
|
||||
}}
|
||||
>
|
||||
{t("userMenu.signOut")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</DrawerFooter>
|
||||
</DrawerPopup>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,30 +43,7 @@ export const topBarUserMenuTrigger = tv({
|
||||
base: [
|
||||
"flex h-8 items-center gap-2 rounded-full py-1 pr-2.5 pl-1",
|
||||
"cursor-pointer outline-none transition-colors select-none",
|
||||
"hover:bg-sand-3 data-[popup-open]:bg-sand-3",
|
||||
"hover:bg-sand-3 data-popup-open:bg-sand-3",
|
||||
"focus-visible:ring-2 focus-visible:ring-sand-8 focus-visible:ring-offset-1 focus-visible:ring-offset-sand-1",
|
||||
],
|
||||
});
|
||||
|
||||
// Right-edge mobile navigation drawer (Base UI Drawer).
|
||||
export const topBarMobileNav = 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 justify-end",
|
||||
popup: [
|
||||
"relative flex h-full w-[min(20rem,100%)] flex-col bg-sand-1 shadow-6 outline-none",
|
||||
"[transform:translateX(var(--drawer-swipe-movement-x,0px))]",
|
||||
"transition-transform duration-200",
|
||||
"data-starting-style:translate-x-full data-ending-style:translate-x-full",
|
||||
],
|
||||
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",
|
||||
nav: "flex flex-col gap-1",
|
||||
actions: "mt-auto flex flex-col gap-2 border-t border-sand-a3 pt-4",
|
||||
},
|
||||
});
|
||||
|
||||
124
packages/ui/src/v2/Drawer/Drawer.stories.tsx
Normal file
124
packages/ui/src/v2/Drawer/Drawer.stories.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
packages/ui/src/v2/Drawer/Drawer.tsx
Normal file
32
packages/ui/src/v2/Drawer/Drawer.tsx
Normal 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} />;
|
||||
}
|
||||
31
packages/ui/src/v2/Drawer/DrawerBody.tsx
Normal file
31
packages/ui/src/v2/Drawer/DrawerBody.tsx
Normal 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} />;
|
||||
}
|
||||
30
packages/ui/src/v2/Drawer/DrawerClose.tsx
Normal file
30
packages/ui/src/v2/Drawer/DrawerClose.tsx
Normal 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} />;
|
||||
}
|
||||
37
packages/ui/src/v2/Drawer/DrawerDescription.tsx
Normal file
37
packages/ui/src/v2/Drawer/DrawerDescription.tsx
Normal 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} />;
|
||||
}
|
||||
31
packages/ui/src/v2/Drawer/DrawerFooter.tsx
Normal file
31
packages/ui/src/v2/Drawer/DrawerFooter.tsx
Normal 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} />;
|
||||
}
|
||||
31
packages/ui/src/v2/Drawer/DrawerHeader.tsx
Normal file
31
packages/ui/src/v2/Drawer/DrawerHeader.tsx
Normal 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} />;
|
||||
}
|
||||
52
packages/ui/src/v2/Drawer/DrawerPopup.tsx
Normal file
52
packages/ui/src/v2/Drawer/DrawerPopup.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
packages/ui/src/v2/Drawer/DrawerSkeleton.tsx
Normal file
46
packages/ui/src/v2/Drawer/DrawerSkeleton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
36
packages/ui/src/v2/Drawer/DrawerTitle.tsx
Normal file
36
packages/ui/src/v2/Drawer/DrawerTitle.tsx
Normal 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} />;
|
||||
}
|
||||
30
packages/ui/src/v2/Drawer/DrawerTrigger.tsx
Normal file
30
packages/ui/src/v2/Drawer/DrawerTrigger.tsx
Normal 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} />;
|
||||
}
|
||||
91
packages/ui/src/v2/Drawer/variants.ts
Normal file
91
packages/ui/src/v2/Drawer/variants.ts
Normal 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",
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user