Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-cats-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shipfox/react-ui": minor
---

Add Sheet component, update Select component, minor updates
1 change: 1 addition & 0 deletions libs/react/ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 2 additions & 5 deletions libs/react/ui/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -203,11 +204,7 @@ function ModalHeader({
<div className="flex-1">{children}</div>
)}
<div className="flex items-center gap-8">
{isDesktop && showEscIndicator && (
<kbd className="flex items-center justify-center rounded-8 border border-border-neutral-base shadow-button-neutral bg-background-field-base text-xs text-foreground-neutral-subtle px-4">
esc
</kbd>
)}
{isDesktop && showEscIndicator && <Kbd>Esc</Kbd>}
{showClose && (
<ModalClose asChild>
<Button
Expand Down
5 changes: 3 additions & 2 deletions libs/react/ui/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,11 @@ function SelectContent({
position = 'popper',
sideOffset = 4,
align = 'center',
container,
...props
}: ComponentProps<typeof SelectPrimitive.Content>) {
}: ComponentProps<typeof SelectPrimitive.Content> & {container?: HTMLElement | null}) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Portal container={container}>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
Expand Down
1 change: 1 addition & 0 deletions libs/react/ui/src/components/sheet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './sheet';
277 changes: 277 additions & 0 deletions libs/react/ui/src/components/sheet/sheet.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import {argosScreenshot} from '@argos-ci/storybook/vitest';
import type {Meta, StoryObj} from '@storybook/react';
import {screen, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {Button} from 'components/button';
import {Input} from 'components/input';
import {Label} from 'components/label';
import {Text} from 'components/typography';
import {useState} from 'react';
import {
Sheet,
SheetBody,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from './sheet';

const OPEN_SHEET_REGEX = /open sheet/i;
const SETTINGS_REGEX = /settings/i;

async function openSheetAndScreenshot(
ctx: Parameters<NonNullable<StoryObj<typeof meta>['play']>>[0],
triggerRegex: RegExp,
screenshotName: string,
): Promise<void> {
const {canvasElement, step} = ctx;
const canvas = within(canvasElement);
const user = userEvent.setup();

await step('Open the sheet', async () => {
const triggerButton = canvas.getByRole('button', {name: triggerRegex});
await user.click(triggerButton);
});

await step('Wait for sheet to appear and render', async () => {
await screen.findByRole('dialog');
await new Promise((resolve) => setTimeout(resolve, 100));
});

await argosScreenshot(ctx, screenshotName);
}

const meta = {
title: 'Components/Sheet',
component: Sheet,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof Sheet>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
play: (ctx) => openSheetAndScreenshot(ctx, OPEN_SHEET_REGEX, 'Default Sheet Open'),
render: () => {
const [open, setOpen] = useState(false);

return (
<div className="flex h-[calc(100vh/2)] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button>Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Sheet Title</SheetTitle>
</SheetHeader>
<SheetBody>
<SheetDescription>
This is a description of the sheet content. Sheets are useful for displaying
supplementary information or actions.
</SheetDescription>
<Text size="sm" className="text-foreground-neutral-subtle w-full">
This is the body content of the sheet. You can add any content here, including
forms, lists, or other components.
</Text>
</SheetBody>
<SheetFooter>
<Button variant="transparent" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant="primary" onClick={() => setOpen(false)}>
Save
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
);
},
};

export const LeftSide: Story = {
play: (ctx) => openSheetAndScreenshot(ctx, OPEN_SHEET_REGEX, 'Left Side Sheet'),
render: () => {
const [open, setOpen] = useState(false);

return (
<div className="flex h-[calc(100vh/2)] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button>Open Sheet</Button>
</SheetTrigger>
<SheetContent side="left">
<SheetHeader>
<SheetTitle>Left Side Sheet</SheetTitle>
</SheetHeader>
<SheetBody>
<SheetDescription>
This sheet slides in from the left side of the screen.
</SheetDescription>
<Text size="sm" className="text-foreground-neutral-subtle w-full">
Left side sheets are often used for navigation menus or sidebar content.
</Text>
</SheetBody>
<SheetFooter>
<Button variant="transparent" onClick={() => setOpen(false)}>
Close
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
);
},
};

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 (
<div className="flex h-[calc(100vh/2)] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button>Settings</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Account Settings</SheetTitle>
</SheetHeader>
<SheetBody className="gap-20">
<SheetDescription>
Update your account information and preferences here.
</SheetDescription>
<div className="flex flex-col gap-20 w-full">
<div className="flex flex-col gap-8 w-full">
<Label htmlFor="name">Name</Label>
<Input
id="name"
placeholder="John Doe"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="flex flex-col gap-8 w-full">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="john@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
</div>
</SheetBody>
<SheetFooter>
<Button variant="transparent" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant="primary" onClick={() => setOpen(false)}>
Save Changes
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
);
},
};

export const WithoutCloseButton: Story = {
play: (ctx) => openSheetAndScreenshot(ctx, OPEN_SHEET_REGEX, 'Sheet Without Close Button'),
render: () => {
const [open, setOpen] = useState(false);

return (
<div className="flex h-[calc(100vh/2)] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button>Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader showClose={false}>
<SheetTitle>Sheet Without Close Button</SheetTitle>
</SheetHeader>
<SheetBody>
<SheetDescription>
This sheet doesn't show a close button. Users can still close it by pressing Esc or
clicking outside.
</SheetDescription>
<Text size="sm" className="text-foreground-neutral-subtle w-full">
The close button can be hidden by setting the showClose prop to false.
</Text>
</SheetBody>
<SheetFooter>
<Button variant="primary" onClick={() => setOpen(false)}>
Close
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
);
},
};

export const LongContent: Story = {
play: (ctx) => openSheetAndScreenshot(ctx, OPEN_SHEET_REGEX, 'Sheet With Long Content'),
render: () => {
const [open, setOpen] = useState(false);

return (
<div className="flex h-[calc(100vh/2)] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button>Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Long Content Example</SheetTitle>
</SheetHeader>
<SheetBody>
<SheetDescription>
This sheet demonstrates how it handles long scrollable content.
</SheetDescription>
<div className="flex flex-col gap-16 w-full">
{Array.from({length: 20}, (_, i) => {
const sectionId = `section-${i + 1}`;
return (
<div key={sectionId} className="flex flex-col gap-8">
<Text size="sm" className="font-medium text-foreground-neutral-base">
Section {i + 1}
</Text>
<Text size="sm" className="text-foreground-neutral-subtle">
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.
</Text>
</div>
);
})}
</div>
</SheetBody>
<SheetFooter>
<Button variant="transparent" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant="primary" onClick={() => setOpen(false)}>
Save
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
);
},
};
Loading
Loading