diff --git a/package-lock.json b/package-lock.json index 776567b8a4..dbc0ca596e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "foxact": "^0.2.41", "next": "15.0.3", "next-themes": "^0.4.3", "react": "19.0.0-rc-66855b96-20241106", @@ -11194,6 +11195,24 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/foxact": { + "version": "0.2.41", + "resolved": "https://registry.npmjs.org/foxact/-/foxact-0.2.41.tgz", + "integrity": "sha512-NgKsYRU6Dx4dHjZSlgO8AKrwsGuvrcCOqiPPLy+x6Nd9vBmRPq+JZjKWC5OcAVnISR1Bjn+Yg69UEdHEooJXDQ==", + "license": "MIT", + "dependencies": { + "client-only": "^0.0.1", + "server-only": "^0.0.1" + }, + "peerDependencies": { + "react": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -16980,6 +16999,12 @@ "node": ">= 0.8" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", diff --git a/package.json b/package.json index cc1b622752..f1f3c28f7d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "foxact": "^0.2.41", "next": "15.0.3", "next-themes": "^0.4.3", "react": "19.0.0-rc-66855b96-20241106", diff --git a/src/app/components/ui/Drawer.tsx b/src/app/components/ui/Drawer.tsx index 172bf19fc6..a0f847eb99 100644 --- a/src/app/components/ui/Drawer.tsx +++ b/src/app/components/ui/Drawer.tsx @@ -11,14 +11,10 @@ import { import { Drawer as DrawerPrimitive } from "vaul"; const Drawer = ({ - shouldScaleBackground = true, ...props -}: ComponentProps) => ( - -); +}: ComponentProps & { + shouldScaleBackground?: never; +}) => ; Drawer.displayName = "Drawer"; const DrawerTrigger = DrawerPrimitive.Trigger; diff --git a/src/app/components/ui/ResponsiveDialog.tsx b/src/app/components/ui/ResponsiveDialog.tsx new file mode 100644 index 0000000000..ab41a71868 --- /dev/null +++ b/src/app/components/ui/ResponsiveDialog.tsx @@ -0,0 +1,157 @@ +import { useMediaQuery } from "foxact/use-media-query"; +import { type ComponentProps, useCallback, useState } from "react"; +import { + Dialog, + DialogBody, + DialogCloseButton, + DialogContent, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} from "./Dialog"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + DrawerPortal, + DrawerTitle, + DrawerTrigger, +} from "./Drawer"; + +const useIsMobile = () => useMediaQuery("(max-width: 640px)", false); + +export const ResponsiveDialog = ({ + open: openProp, + onOpenChange: onOpenChangeProp, + ...props +}: ComponentProps & ComponentProps) => { + const [open, setOpen] = useState(openProp); + + const onOpenChange = useCallback( + (newOpen: boolean) => { + setOpen(newOpen); + if (typeof onOpenChangeProp === "function") onOpenChangeProp(newOpen); + }, + [onOpenChangeProp], + ); + + const isMobile = useIsMobile(); + + if (isMobile) + return ; + + return ; +}; +ResponsiveDialog.displayName = "ResponsiveDialog"; + +export const ResponsiveDialogTrigger = ( + props: ComponentProps & + ComponentProps, +) => { + const isMobile = useIsMobile(); + + if (isMobile) return ; + + return ; +}; +ResponsiveDialogTrigger.displayName = "ResponsiveDialogTrigger"; + +export const ResponsiveDialogPortal = ( + props: ComponentProps & + ComponentProps, +) => { + const isMobile = useIsMobile(); + + if (isMobile) return ; + + return ; +}; +ResponsiveDialogPortal.displayName = "ResponsiveDialogPortal"; + +export const ResponsiveDialogOverlay = ( + props: ComponentProps & + ComponentProps, +) => { + const isMobile = useIsMobile(); + + if (isMobile) return ; + + return ; +}; +ResponsiveDialogOverlay.displayName = "ResponsiveDialogOverlay"; + +export const ResponsiveDialogContent = ( + props: ComponentProps & + ComponentProps, +) => { + const isMobile = useIsMobile(); + + if (isMobile) return ; + + return ; +}; +ResponsiveDialogContent.displayName = "ResponsiveDialogContent"; + +export const ResponsiveDialogCloseButton = ({ + children, + asChild, + ...props +}: ComponentProps & + ComponentProps) => { + const isMobile = useIsMobile(); + + if (isMobile) + return ( + + {children} + + ); + + return ; +}; +ResponsiveDialogCloseButton.displayName = "ResponsiveDialogCloseButton"; + +export const ResponsiveDialogHeader = ( + props: ComponentProps & + ComponentProps, +) => { + const isMobile = useIsMobile(); + + if (isMobile) return ; + + return ; +}; +ResponsiveDialogHeader.displayName = "ResponsiveDialogHeader"; + +export const ResponsiveDialogTitle = ( + props: ComponentProps & + ComponentProps, +) => { + const isMobile = useIsMobile(); + + if (isMobile) return ; + + return ; +}; +ResponsiveDialogTitle.displayName = "ResponsiveDialogTitle"; + +export const ResponsiveDialogBody = DialogBody; +ResponsiveDialogBody.displayName = "ResponsiveDialogBody"; + +export const ResponsiveDialogFooter = ( + props: ComponentProps & + ComponentProps, +) => { + const isMobile = useIsMobile(); + + if (isMobile) return ; + + return ; +}; +ResponsiveDialogFooter.displayName = "ResponsiveDialogFooter"; diff --git a/src/stories/ResponsiveDialog.stories.tsx b/src/stories/ResponsiveDialog.stories.tsx new file mode 100644 index 0000000000..30cd919d60 --- /dev/null +++ b/src/stories/ResponsiveDialog.stories.tsx @@ -0,0 +1,128 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "app/components/ui/Button"; +import type { DialogContentProps } from "app/components/ui/Dialog"; +import { + ResponsiveDialog, + ResponsiveDialogBody, + ResponsiveDialogCloseButton, + ResponsiveDialogContent, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, + ResponsiveDialogTrigger, +} from "app/components/ui/ResponsiveDialog"; + +import type { ComponentProps } from "react"; + +const ResponsiveDialogExample = ({ + size, + longContent, + scrollBody, + showHeader = true, + showFooter, +}: { + size?: DialogContentProps["size"]; + longContent?: ComponentProps["longContent"]; + scrollBody?: boolean; + showHeader?: boolean; + showFooter?: boolean; +}) => ( + + Open responsive dialog + + {showHeader && ( + + Responsive dialog + + )} + + + + + + {showFooter && ( + Sneaky footer + )} + + + + + + +); + +const meta: Meta = { + title: "Design system/ResponsiveDialog", + component: ResponsiveDialogExample, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + longContent: false, + size: "md", + scrollBody: false, + showHeader: true, + showFooter: false, + }, + argTypes: { + size: { + control: { + type: "select", + }, + options: ["sm", "md", "lg", "xl", "2xl", "3xl", "4xl"], + }, + }, +}; + +const DynamicDialogContent = ({ longContent }: { longContent?: boolean }) => { + if (!longContent) return

This is a simple dialog.

; + + return ( + <> +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin a luctus + lorem. Etiam aliquam vel est vel lacinia. Nulla vehicula tortor quis + erat pellentesque, et interdum nisl aliquet. Aenean ac malesuada mauris. + Aliquam scelerisque lorem ut metus interdum, ut venenatis orci egestas. + In semper mollis eros lobortis mollis. Praesent vestibulum convallis + ipsum at fermentum. Donec porta facilisis lacus eu dignissim. Proin + hendrerit orci gravida risus commodo fermentum. Donec finibus sapien eu + nibh mattis dictum. Praesent ac tempor odio, et lobortis ex. Donec nec + mauris et lorem facilisis auctor. Maecenas vitae convallis leo. +

+

+ Maecenas maximus felis scelerisque turpis euismod rutrum. Pellentesque a + dolor scelerisque, elementum sapien eu, porta libero. Donec volutpat + egestas tincidunt. Vivamus blandit eros mollis viverra aliquam. Mauris + eu turpis id est gravida finibus. In viverra, elit eget eleifend + sagittis, massa quam faucibus erat, sed mattis odio nunc vitae enim. + Donec lacus diam, lobortis at facilisis in, placerat ut diam. Maecenas + sed dui sit amet massa tristique vulputate non ac erat. Nullam orci + urna, finibus eu blandit et, pharetra ac enim. Donec magna augue, + interdum at sollicitudin id, fringilla nec sapien. In nisl quam, rhoncus + blandit ipsum in, volutpat aliquam nisi. Nam accumsan lobortis ante, at + tristique ante vehicula eget. Sed ornare varius velit, ut ultrices ante + auctor non. Cras bibendum, libero sed varius fringilla, quam lorem + fermentum nunc, vitae varius mauris libero sed tortor. +

+

+ Donec ut aliquam massa. Etiam congue turpis at purus tempor maximus. + Etiam semper libero non varius pellentesque. Ut egestas faucibus purus + non faucibus. Duis sed nisi consequat, laoreet nisi in, laoreet purus. + Integer aliquam mi ac metus interdum rhoncus. In ac iaculis quam. Sed eu + nibh lectus. Donec imperdiet vestibulum nisl in facilisis. Sed molestie + ipsum eu orci imperdiet cursus. Morbi sit amet quam mi. Etiam maximus + scelerisque orci, id gravida ligula sagittis in. Maecenas volutpat quam + elit, vel vehicula ex fringilla ac. Aenean risus lectus, pellentesque id + est eget, feugiat hendrerit ligula. Nulla tempor pulvinar lacus, ut + euismod tortor. +

+ + ); +};