From d047f1b32391e1818a6a557139e9d095f4b4abc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 20 Jul 2026 16:47:47 +0200 Subject: [PATCH] Extract Base UI Drawer into the v2 kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- .../src/components/TopBar/TopBarMobileNav.tsx | 223 +++++++++--------- .../src/components/TopBar/variants.ts | 25 +- packages/ui/src/v2/Drawer/Drawer.stories.tsx | 124 ++++++++++ packages/ui/src/v2/Drawer/Drawer.tsx | 32 +++ packages/ui/src/v2/Drawer/DrawerBody.tsx | 31 +++ packages/ui/src/v2/Drawer/DrawerClose.tsx | 30 +++ .../ui/src/v2/Drawer/DrawerDescription.tsx | 37 +++ packages/ui/src/v2/Drawer/DrawerFooter.tsx | 31 +++ packages/ui/src/v2/Drawer/DrawerHeader.tsx | 31 +++ packages/ui/src/v2/Drawer/DrawerPopup.tsx | 52 ++++ packages/ui/src/v2/Drawer/DrawerSkeleton.tsx | 46 ++++ packages/ui/src/v2/Drawer/DrawerTitle.tsx | 36 +++ packages/ui/src/v2/Drawer/DrawerTrigger.tsx | 30 +++ packages/ui/src/v2/Drawer/variants.ts | 91 +++++++ 14 files changed, 683 insertions(+), 136 deletions(-) create mode 100644 packages/ui/src/v2/Drawer/Drawer.stories.tsx create mode 100644 packages/ui/src/v2/Drawer/Drawer.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerBody.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerClose.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerDescription.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerFooter.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerHeader.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerPopup.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerSkeleton.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerTitle.tsx create mode 100644 packages/ui/src/v2/Drawer/DrawerTrigger.tsx create mode 100644 packages/ui/src/v2/Drawer/variants.ts diff --git a/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx b/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx index 8c74783ea..da8f4fec2 100644 --- a/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx +++ b/apps/compliance-portal/src/components/TopBar/TopBarMobileNav.tsx @@ -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 ( - +
-
- - - - - -
- - {t("topBar.menuTitle")} - - - - - )} - /> -
+ + + {t("topBar.menuTitle")} + + + + )} + /> + - - -
- {identity == null - ? ( - - ) - : ( - <> - {displayName - ? ( -
- - {displayName} - - - {identity.email} - -
- ) - : null} - - - - )} -
-
-
-
-
-
+ {isSubscribed ? t("userMenu.subscribed") : t("userMenu.subscribe")} + + + + )} + + + ); } diff --git a/apps/compliance-portal/src/components/TopBar/variants.ts b/apps/compliance-portal/src/components/TopBar/variants.ts index 45c796216..61b86dd76 100644 --- a/apps/compliance-portal/src/components/TopBar/variants.ts +++ b/apps/compliance-portal/src/components/TopBar/variants.ts @@ -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", - }, -}); diff --git a/packages/ui/src/v2/Drawer/Drawer.stories.tsx b/packages/ui/src/v2/Drawer/Drawer.stories.tsx new file mode 100644 index 000000000..03792f976 --- /dev/null +++ b/packages/ui/src/v2/Drawer/Drawer.stories.tsx @@ -0,0 +1,124 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 ( + + Open drawer} /> + + + Menu + Close} /> + + + + Drawer body content — navigation links, filters, or any other slot + the surrounding page composes. + + + + Done} /> + + + + ); +} + +export function BottomSheet() { + return ( + + Open bottom sheet} /> + + + Options + Close} /> + + + + Swipe down to dismiss. + + + + Confirm} /> + + + + ); +} + +export function Controlled() { + const [open, setOpen] = useState(false); + + return ( +
+ + {open ? "open" : "closed"} + + + + + Account + + + + Controlled the same way as Base UI — `open` / `onOpenChange`. + + + + + + + +
+ ); +} + +export function Skeleton() { + return ( +
+ +
+ ); +} diff --git a/packages/ui/src/v2/Drawer/Drawer.tsx b/packages/ui/src/v2/Drawer/Drawer.tsx new file mode 100644 index 000000000..591dbab07 --- /dev/null +++ b/packages/ui/src/v2/Drawer/Drawer.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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; + +export function Drawer(props: DrawerProps) { + return ; +} diff --git a/packages/ui/src/v2/Drawer/DrawerBody.tsx b/packages/ui/src/v2/Drawer/DrawerBody.tsx new file mode 100644 index 000000000..f7fcb676a --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerBody.tsx @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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
; +} diff --git a/packages/ui/src/v2/Drawer/DrawerClose.tsx b/packages/ui/src/v2/Drawer/DrawerClose.tsx new file mode 100644 index 000000000..877f11915 --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerClose.tsx @@ -0,0 +1,30 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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; + +export function DrawerClose(props: DrawerCloseProps) { + return ; +} diff --git a/packages/ui/src/v2/Drawer/DrawerDescription.tsx b/packages/ui/src/v2/Drawer/DrawerDescription.tsx new file mode 100644 index 000000000..f53b2ee71 --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerDescription.tsx @@ -0,0 +1,37 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "className"> & { + className?: string; + }; + +export function DrawerDescription(props: DrawerDescriptionProps) { + const { className, ...rest } = props; + const { description } = drawer(); + + return ; +} diff --git a/packages/ui/src/v2/Drawer/DrawerFooter.tsx b/packages/ui/src/v2/Drawer/DrawerFooter.tsx new file mode 100644 index 000000000..a3d9284ea --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerFooter.tsx @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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
; +} diff --git a/packages/ui/src/v2/Drawer/DrawerHeader.tsx b/packages/ui/src/v2/Drawer/DrawerHeader.tsx new file mode 100644 index 000000000..d650e1ad1 --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerHeader.tsx @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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
; +} diff --git a/packages/ui/src/v2/Drawer/DrawerPopup.tsx b/packages/ui/src/v2/Drawer/DrawerPopup.tsx new file mode 100644 index 000000000..76858979e --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerPopup.tsx @@ -0,0 +1,52 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "className"> + & VariantProps + & { + 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 ( + + + + + + {children} + + + + + ); +} diff --git a/packages/ui/src/v2/Drawer/DrawerSkeleton.tsx b/packages/ui/src/v2/Drawer/DrawerSkeleton.tsx new file mode 100644 index 000000000..ea95bc325 --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerSkeleton.tsx @@ -0,0 +1,46 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 ( +
+
+ + +
+
+ + + +
+
+ +
+
+ ); +} diff --git a/packages/ui/src/v2/Drawer/DrawerTitle.tsx b/packages/ui/src/v2/Drawer/DrawerTitle.tsx new file mode 100644 index 000000000..e6f49a66f --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerTitle.tsx @@ -0,0 +1,36 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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, "className"> & { + className?: string; +}; + +export function DrawerTitle(props: DrawerTitleProps) { + const { className, ...rest } = props; + const { title } = drawer(); + + return ; +} diff --git a/packages/ui/src/v2/Drawer/DrawerTrigger.tsx b/packages/ui/src/v2/Drawer/DrawerTrigger.tsx new file mode 100644 index 000000000..c192e7fdc --- /dev/null +++ b/packages/ui/src/v2/Drawer/DrawerTrigger.tsx @@ -0,0 +1,30 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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; + +export function DrawerTrigger(props: DrawerTriggerProps) { + return ; +} diff --git a/packages/ui/src/v2/Drawer/variants.ts b/packages/ui/src/v2/Drawer/variants.ts new file mode 100644 index 000000000..1633c5beb --- /dev/null +++ b/packages/ui/src/v2/Drawer/variants.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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", + ], +});