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",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user