diff --git a/.changeset/clean-cats-knock.md b/.changeset/clean-cats-knock.md new file mode 100644 index 00000000..6337ca24 --- /dev/null +++ b/.changeset/clean-cats-knock.md @@ -0,0 +1,5 @@ +--- +"@shipfox/react-ui": minor +--- + +Add Sheet component, update Select component, minor updates diff --git a/libs/react/ui/src/components/index.ts b/libs/react/ui/src/components/index.ts index 7a3f5277..f553a6a6 100644 --- a/libs/react/ui/src/components/index.ts +++ b/libs/react/ui/src/components/index.ts @@ -25,6 +25,7 @@ export * from './moving-border'; export * from './popover'; export * from './search'; export * from './select'; +export * from './sheet'; export * from './shiny-text'; export * from './skeleton'; export * from './table'; diff --git a/libs/react/ui/src/components/modal/modal.tsx b/libs/react/ui/src/components/modal/modal.tsx index 06dbc168..9913f864 100644 --- a/libs/react/ui/src/components/modal/modal.tsx +++ b/libs/react/ui/src/components/modal/modal.tsx @@ -2,6 +2,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'; import {cva} from 'class-variance-authority'; import {Button} from 'components/button'; import {Icon} from 'components/icon'; +import {Kbd} from 'components/kbd'; import {Text} from 'components/typography'; import {motion, type Transition} from 'framer-motion'; import {useMediaQuery} from 'hooks/useMediaQuery'; @@ -203,11 +204,7 @@ function ModalHeader({
{children}
)}
- {isDesktop && showEscIndicator && ( - - esc - - )} + {isDesktop && showEscIndicator && Esc} {showClose && ( + + + + Sheet Title + + + + This is a description of the sheet content. Sheets are useful for displaying + supplementary information or actions. + + + This is the body content of the sheet. You can add any content here, including + forms, lists, or other components. + + + + + + + + +
+ ); + }, +}; + +export const LeftSide: Story = { + play: (ctx) => openSheetAndScreenshot(ctx, OPEN_SHEET_REGEX, 'Left Side Sheet'), + render: () => { + const [open, setOpen] = useState(false); + + return ( +
+ + + + + + + Left Side Sheet + + + + This sheet slides in from the left side of the screen. + + + Left side sheets are often used for navigation menus or sidebar content. + + + + + + + +
+ ); + }, +}; + +export const SettingsForm: Story = { + play: (ctx) => openSheetAndScreenshot(ctx, SETTINGS_REGEX, 'Settings Form Sheet'), + render: () => { + const [open, setOpen] = useState(false); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + + return ( +
+ + + + + + + Account Settings + + + + Update your account information and preferences here. + +
+
+ + setName(e.target.value)} + /> +
+
+ + setEmail(e.target.value)} + /> +
+
+
+ + + + +
+
+
+ ); + }, +}; + +export const WithoutCloseButton: Story = { + play: (ctx) => openSheetAndScreenshot(ctx, OPEN_SHEET_REGEX, 'Sheet Without Close Button'), + render: () => { + const [open, setOpen] = useState(false); + + return ( +
+ + + + + + + Sheet Without Close Button + + + + This sheet doesn't show a close button. Users can still close it by pressing Esc or + clicking outside. + + + The close button can be hidden by setting the showClose prop to false. + + + + + + + +
+ ); + }, +}; + +export const LongContent: Story = { + play: (ctx) => openSheetAndScreenshot(ctx, OPEN_SHEET_REGEX, 'Sheet With Long Content'), + render: () => { + const [open, setOpen] = useState(false); + + return ( +
+ + + + + + + Long Content Example + + + + This sheet demonstrates how it handles long scrollable content. + +
+ {Array.from({length: 20}, (_, i) => { + const sectionId = `section-${i + 1}`; + return ( +
+ + Section {i + 1} + + + This is paragraph {i + 1} of the long content. The sheet body is scrollable, + so you can scroll through all the content while the header and footer remain + fixed. + +
+ ); + })} +
+
+ + + + +
+
+
+ ); + }, +}; diff --git a/libs/react/ui/src/components/sheet/sheet.tsx b/libs/react/ui/src/components/sheet/sheet.tsx new file mode 100644 index 00000000..26a4ea52 --- /dev/null +++ b/libs/react/ui/src/components/sheet/sheet.tsx @@ -0,0 +1,242 @@ +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import {cva, type VariantProps} from 'class-variance-authority'; +import {Button} from 'components/button'; +import {Icon} from 'components/icon'; +import {Kbd} from 'components/kbd'; +import {useMediaQuery} from 'hooks/useMediaQuery'; +import type {ComponentProps} from 'react'; +import {cn} from 'utils/cn'; + +const sheetOverlayVariants = cva( + 'fixed inset-0 z-40 bg-background-backdrop-backdrop data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', +); + +const sheetContentVariants = cva( + [ + 'fixed z-50 gap-4 bg-background-neutral-base shadow-tooltip transition ease-in-out', + 'data-[state=open]:animate-in data-[state=closed]:animate-out', + 'data-[state=closed]:duration-300 data-[state=open]:duration-500', + 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', + ], + { + variants: { + side: { + top: [ + 'inset-x-0 top-0 border-b border-border-neutral-strong', + 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + ], + bottom: [ + 'inset-x-0 bottom-0 border-t border-border-neutral-strong', + 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + ], + left: [ + 'inset-y-0 left-0 h-full w-3/4 border-r border-border-neutral-strong sm:max-w-sm', + 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left', + ], + right: [ + 'inset-y-0 right-0 h-full w-3/4 border-l border-border-neutral-strong sm:max-w-sm', + 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right', + ], + }, + }, + defaultVariants: { + side: 'right', + }, + }, +); + +function Sheet({...props}: ComponentProps) { + return ; +} + +function SheetTrigger({className, ...props}: ComponentProps) { + return ( + + ); +} + +function SheetPortal({...props}: ComponentProps) { + return ; +} + +function SheetClose({className, ...props}: ComponentProps) { + return ( + + ); +} + +type SheetOverlayProps = ComponentProps; + +function SheetOverlay({className, ...props}: SheetOverlayProps) { + return ( + + ); +} + +type SheetContentProps = ComponentProps & + VariantProps & { + container?: HTMLElement | null; + }; + +function SheetContent({ + side = 'right', + className, + children, + container, + ...props +}: SheetContentProps) { + return ( + + + +
+
+ {children} +
+ + + ); +} + +type SheetHeaderProps = ComponentProps<'div'> & { + showEscIndicator?: boolean; + showClose?: boolean; +}; + +function SheetHeader({ + className, + showEscIndicator = true, + showClose = true, + children, + ...props +}: SheetHeaderProps) { + const isDesktop = useMediaQuery('(min-width: 768px)'); + + return ( +
+
+
{children}
+
+ {isDesktop && showEscIndicator && Esc} + {showClose && ( + + + + )} +
+
+
+ ); +} + +type SheetFooterProps = ComponentProps<'div'>; + +function SheetFooter({className, ...props}: SheetFooterProps) { + return ( +
+ ); +} + +type SheetTitleProps = ComponentProps; + +function SheetTitle({className, ...props}: SheetTitleProps) { + return ( + + ); +} + +type SheetDescriptionProps = ComponentProps; + +function SheetDescription({className, ...props}: SheetDescriptionProps) { + return ( + + ); +} + +type SheetBodyProps = ComponentProps<'div'>; + +function SheetBody({className, children, ...props}: SheetBodyProps) { + return ( +
+ {children} +
+ ); +} + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, + SheetBody, + sheetContentVariants, + sheetOverlayVariants, +}; + +export type { + SheetContentProps, + SheetHeaderProps, + SheetFooterProps, + SheetTitleProps, + SheetDescriptionProps, + SheetBodyProps, + SheetOverlayProps, +};