Skip to content

Commit

Permalink
Refine tabs design and update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
almsh committed Jan 24, 2025
1 parent 0b84d84 commit be1a715
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 108 deletions.
2 changes: 1 addition & 1 deletion ui/src/components/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-primary data-[state=active]:text-white bg-card-muted text-accent-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',
className,
)}
{...props}
Expand Down
342 changes: 235 additions & 107 deletions ui/src/stories/tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,155 +1,283 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/tabs';
import { ChatBubbleLeftIcon, Cog6ToothIcon } from '@heroicons/react/24/outline';

const meta = {
title: 'Components/Tabs',
component: Tabs,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: `
A fully accessible tab component built on Radix UI primitives.
Component Structure:
- TabsList: Container for tab triggers, manages tab organization
- TabsTrigger: Interactive button that activates its associated content panel
- TabsContent: Content panel that's shown when its associated trigger is active
`,
},
},
controls: {
exclude: ['dir', 'asChild', 'orientation']
}
},
argTypes: {
defaultValue: {
control: 'text',
description: 'The value of the tab that should be active when initially rendered',
type: { name: 'string', required: true },
},
value: {
control: 'text',
description: 'The controlled value of the tab to activate',
},
onValueChange: {
description: 'Event handler called when the value changes',
action: 'changed',
table: {
disable: true,
}
},
className: {
control: 'text',
description: 'Optional CSS class to add to the component',
table: {
category: 'Styling',
},
},
dir: {
table: {
disable: true
}
}
},
} satisfies Meta<typeof Tabs>;

export default meta;
type Story = StoryObj<typeof Tabs>;
// First, define an interface for the additional props
interface BasicStoryProps extends React.ComponentProps<typeof Tabs> {
tab1Label?: string;
tab2Label?: string;
tab1Content?: string;
tab2Content?: string;
}

// Basic
export const Basic: Story = {
render: () => (
<Tabs defaultValue="account">
export const Basic: StoryObj<BasicStoryProps> = {
args: {
defaultValue: 'tab1',
tab1Label: 'Tab 1',
tab2Label: 'Tab 2',
},
parameters: {
docs: {
description: {
story: 'Basic usage of the Tabs component demonstrating core functionality.',
},
},
},
argTypes: {
tab1Label: {
control: 'text',
description: 'Label for the first tab',
} as const,
tab2Label: {
control: 'text',
description: 'Label for the second tab',
} as const,
tab1Content: {
control: 'text',
description: 'Content for the first tab',
} as const,
tab2Content: {
control: 'text',
description: 'Content for the second tab',
} as const,
},
render: ({ tab1Label = 'Tab 1', tab2Label = 'Tab 2', tab1Content = 'Content for Tab 1', tab2Content = 'Content for Tab 2', ...args }) => (
<Tabs {...args}>
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="tab1">{tab1Label}</TabsTrigger>
<TabsTrigger value="tab2">{tab2Label}</TabsTrigger>
</TabsList>
<TabsContent value="account">Make changes to your account here.</TabsContent>
<TabsContent value="password">Change your password here.</TabsContent>
<TabsContent value="tab1">{tab1Content}</TabsContent>
<TabsContent value="tab2">{tab2Content}</TabsContent>
</Tabs>
),
};

// Multiple Tabs
export const MultipleTabs: Story = {
render: () => (
<Tabs defaultValue="tab1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
<TabsTrigger value="tab3">Tab 3</TabsTrigger>
<TabsTrigger value="tab4">Tab 4</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content of tab 1</TabsContent>
<TabsContent value="tab2">Content of tab 2</TabsContent>
<TabsContent value="tab3">Content of tab 3</TabsContent>
<TabsContent value="tab4">Content of tab 4</TabsContent>
</Tabs>
),
};
interface StyledStoryProps extends React.ComponentProps<typeof Tabs> {
listClassName?: string;
triggerClassName?: string;
contentClassName?: string;
}

// With Disabled
export const WithDisabled: Story = {
render: () => (
<Tabs defaultValue="active">
<TabsList>
<TabsTrigger value="active">Active</TabsTrigger>
<TabsTrigger value="disabled" disabled>
Disabled
</TabsTrigger>
<TabsTrigger value="active2">Active 2</TabsTrigger>

export const Styled: StoryObj<StyledStoryProps>
= {
args: {
defaultValue: 'tab1',
listClassName: 'bg-gray-100 p-2 rounded-lg',
triggerClassName: 'px-4 py-2',
contentClassName: 'p-4 bg-white rounded-lg mt-2',
},
parameters: {
docs: {
description: {
story: 'Example demonstrating styling options and disabled state.',
},
},
},
argTypes: {
listClassName: {
control: 'text',
description: 'Custom class for TabsList',
},
triggerClassName: {
control: 'text',
description: 'Custom class for TabsTrigger',
},
contentClassName: {
control: 'text',
description: 'Custom class for TabsContent',
},
},
render: ({ listClassName, triggerClassName, contentClassName, ...args }) => (
<Tabs {...args}>
<TabsList className={listClassName}>
<TabsTrigger value="tab1" className={triggerClassName}>Active</TabsTrigger>
<TabsTrigger value="tab2" className={triggerClassName}>Settings</TabsTrigger>
<TabsTrigger value="tab3" className={triggerClassName} disabled>Disabled</TabsTrigger>
</TabsList>
<TabsContent value="active">This tab is active</TabsContent>
<TabsContent value="disabled">This tab is disabled</TabsContent>
<TabsContent value="active2">This is another active tab</TabsContent>
<TabsContent value="tab1" className={contentClassName}>
Active tab content with custom styling
</TabsContent>
<TabsContent value="tab2" className={contentClassName}>
Settings tab content with custom styling
</TabsContent>
<TabsContent value="tab3" className={contentClassName}>
Disabled tab content
</TabsContent>
</Tabs>
),
};

// Full Width
export const FullWidth: Story = {
render: () => (
<Tabs defaultValue="tab1" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="tab1">Messages</TabsTrigger>
<TabsTrigger value="tab2">Notifications</TabsTrigger>
<TabsTrigger value="tab3">Settings</TabsTrigger>
interface FullWidthStoryProps extends React.ComponentProps<typeof Tabs> {
gap?: number;
}

export const FullWidth: StoryObj<FullWidthStoryProps> = {
args: {
defaultValue: 'tab1',
className: 'w-full',
gap: 2,
},
parameters: {
docs: {
description: {
story: 'A full-width layout with evenly distributed tabs.',
},
},
},
argTypes: {
gap: {
control: { type: 'range', min: 0, max: 8 },
description: 'Gap between tabs (in Tailwind spacing units)',
},
},
render: ({ gap = 2, ...args }) => (
<Tabs {...args}>
<TabsList className={`w-full flex justify-between gap-${gap}`}>
<TabsTrigger value="tab1" className="flex-1">Messages</TabsTrigger>
<TabsTrigger value="tab2" className="flex-1">Notifications</TabsTrigger>
<TabsTrigger value="tab3" className="flex-1">Settings</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Messages content</TabsContent>
<TabsContent value="tab2">Notifications content</TabsContent>
<TabsContent value="tab3">Settings content</TabsContent>
<TabsContent value="tab1">Messages panel content</TabsContent>
<TabsContent value="tab2">Notifications panel content</TabsContent>
<TabsContent value="tab3">Settings panel content</TabsContent>
</Tabs>
),
};

// With Icons
export const WithIcons: Story = {
render: () => (
<Tabs defaultValue="messages">
interface WithIconsStoryProps extends React.ComponentProps<typeof Tabs> {
iconSize?: number;
}

export const WithIcons: StoryObj<WithIconsStoryProps> = {
args: {
defaultValue: 'messages',
iconSize: 16,
},
parameters: {
docs: {
description: {
story: 'Example of tabs with HeroIcons integration.',
},
},
},
argTypes: {
iconSize: {
control: { type: 'range', min: 12, max: 24 },
description: 'Size of the icons in pixels',
},
},
render: ({ iconSize = 16, ...args }) => (
<Tabs {...args}>
<TabsList>
<TabsTrigger value="messages" className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<ChatBubbleLeftIcon style={{ width: iconSize, height: iconSize }} />
Messages
</TabsTrigger>
<TabsTrigger value="settings" className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />
</svg>
<Cog6ToothIcon style={{ width: iconSize, height: iconSize }} />
Settings
</TabsTrigger>
</TabsList>
<TabsContent value="messages">Messages content</TabsContent>
<TabsContent value="settings">Settings content</TabsContent>
<TabsContent value="messages">Messages panel with icon example</TabsContent>
<TabsContent value="settings">Settings panel with icon example</TabsContent>
</Tabs>
),
};

// With Rich Content

export const WithRichContent: Story = {
render: () => (
<Tabs defaultValue="tab1">
args: {
defaultValue: 'profile',
},
parameters: {
docs: {
description: {
story: 'Example of tabs with rich content and configurable spacing.',
},
},
},
render: (args) => (
<Tabs {...args}>
<TabsList>
<TabsTrigger value="tab1">Account</TabsTrigger>
<TabsTrigger value="tab2">Preferences</TabsTrigger>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
</TabsList>
<TabsContent value="tab1" className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-medium">Account Settings</h3>
<p className="text-muted-foreground text-sm">Manage your account settings and preferences.</p>
</div>
<div className="grid gap-2">
<div className="flex items-center space-x-2">
<input type="email" placeholder="Email" className="flex h-10 w-full rounded-md border px-3" />
</div>
</div>
<TabsContent value="profile" className={`space-y-4`}>
<h3 className="text-lg font-medium">Profile Settings</h3>
<p className="text-muted-foreground text-sm">
Update your profile information and preferences.
</p>
<input
type="text"
placeholder="Display Name"
className="flex h-10 w-full rounded-md border px-3"
/>
</TabsContent>
<TabsContent value="tab2" className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-medium">Preferences</h3>
<p className="text-muted-foreground text-sm">Customize your application preferences.</p>
</div>
<div className="grid gap-2">
<label className="flex items-center space-x-2">
<input type="checkbox" />
<span>Enable notifications</span>
</label>
<TabsContent value="notifications" className={`space-y-4`}>
<h3 className="text-lg font-medium">Notification Settings</h3>
<p className="text-muted-foreground text-sm">
Choose how you want to be notified.
</p>
<div className="flex items-center space-x-2">
<input type="checkbox" id="emailNotifications" />
<label htmlFor="emailNotifications">Email notifications</label>
</div>
</TabsContent>
</Tabs>
Expand Down

0 comments on commit be1a715

Please sign in to comment.