feat: storybook

This commit is contained in:
2026-05-31 17:11:42 +03:00
parent 2f937e94b1
commit 67993ae3ec
17 changed files with 764 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Button } from '../components/ui';
const meta = {
title: 'Inputs/Button',
component: Button,
parameters: {
docs: {
description: {
component:
'Tactile push button. Four variants and an optional small size; everything else is a native `<button>`, so all standard button props pass through.',
},
},
},
argTypes: {
variant: {
control: 'inline-radio',
options: ['key', 'primary', 'ember', 'ghost'],
description: 'Visual emphasis. `key` is the default neutral button.',
},
size: {
control: 'inline-radio',
options: [undefined, 'sm'],
description: 'Omit for default; `sm` for the compact size.',
},
iconOnly: { control: 'boolean', description: 'Square padding for a single glyph.' },
disabled: { control: 'boolean' },
children: { control: 'text' },
},
args: { children: 'Button', variant: 'key' },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Variants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<Button variant="key">Key</Button>
<Button variant="primary">Primary</Button>
<Button variant="ember">Ember</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Button variant="primary">Default</Button>
<Button variant="primary" size="sm">
Small
</Button>
</div>
),
};
export const Disabled: Story = {
args: { disabled: true, variant: 'primary', children: 'Disabled' },
};
+74
View File
@@ -0,0 +1,74 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import {
Card,
List,
Row,
Table,
THead,
TBody,
Tr,
Th,
Td,
Badge,
} from '../components/ui';
const meta = {
title: 'Data Display/Surfaces',
component: Card,
parameters: {
docs: {
description: {
component: 'Cards, selectable lists/rows, and the bordered table.',
},
},
},
} satisfies Meta<typeof Card>;
export default meta;
type Story = StoryObj<typeof meta>;
export const CardSurface: Story = {
render: () => (
<Card style={{ maxWidth: 320, padding: 20 }}>
<h3 className="modern-sk-h3">Storage</h3>
<p className="modern-sk-body">128 GB of 256 GB used.</p>
</Card>
),
};
export const ListRows: Story = {
render: () => (
<List style={{ width: 320 }}>
<Row selected>General</Row>
<Row>Appearance</Row>
<Row>Notifications</Row>
<Row>Privacy</Row>
</List>
),
};
export const DataTable: Story = {
render: () => (
<Table>
<THead>
<Tr>
<Th>Device</Th>
<Th>Status</Th>
<Th>Battery</Th>
</Tr>
</THead>
<TBody>
<Tr selected>
<Td>MacBook Pro</Td>
<Td><Badge variant="lime" dot>Online</Badge></Td>
<Td>82%</Td>
</Tr>
<Tr>
<Td>iPhone 16</Td>
<Td><Badge variant="neutral">Idle</Badge></Td>
<Td>54%</Td>
</Tr>
</TBody>
</Table>
),
};
+77
View File
@@ -0,0 +1,77 @@
import { useState } from 'react';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Info, CheckCircle, Warning, XCircle } from '@phosphor-icons/react';
import { Progress, Spinner, Callout, Badge, Chip, Button } from '../components/ui';
const meta = {
title: 'Feedback/Status',
component: Progress,
parameters: {
docs: {
description: {
component:
'Progress bar, spinner, callouts, badges and chips — the status + signalling family.',
},
},
},
} satisfies Meta<typeof Progress>;
export default meta;
type Story = StoryObj<typeof meta>;
function ProgressDemo() {
const [v, setV] = useState(40);
return (
<div style={{ width: 320, display: 'flex', flexDirection: 'column', gap: 12 }}>
<Progress value={v} />
<div style={{ display: 'flex', gap: 8 }}>
<Button size="sm" onClick={() => setV((x) => Math.max(0, x - 10))}>10</Button>
<Button size="sm" variant="primary" onClick={() => setV((x) => Math.min(100, x + 10))}>+10</Button>
</div>
</div>
);
}
export const ProgressBar: Story = { render: () => <ProgressDemo /> };
export const Spinners: Story = {
render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Spinner size="sm" />
<Spinner />
<Spinner size="lg" />
</div>
),
};
export const Callouts: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 460 }}>
<Callout variant="info" icon={<Info size={18} />}>Sync runs in the background.</Callout>
<Callout variant="success" icon={<CheckCircle size={18} />}>All changes saved.</Callout>
<Callout variant="warning" icon={<Warning size={18} />}>Storage is almost full.</Callout>
<Callout variant="danger" icon={<XCircle size={18} />}>Failed to reach the server.</Callout>
</div>
),
};
export const Badges: Story = {
render: () => (
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
<Badge variant="lime">Lime</Badge>
<Badge variant="ember">Ember</Badge>
<Badge variant="neutral">Neutral</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="lime" dot>Online</Badge>
</div>
),
};
export const Chips: Story = {
render: () => (
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<Chip>Design</Chip>
<Chip onRemove={() => {}}>Removable</Chip>
</div>
),
};
+48
View File
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Gear, Plus, Trash } from '@phosphor-icons/react';
import { IconButton } from '../components/ui';
const meta = {
title: 'Inputs/IconButton',
component: IconButton,
parameters: {
docs: {
description: {
component:
'Square button for a single icon. Shares the Button variants; sizes are `sm` / default / `lg`.',
},
},
},
argTypes: {
variant: { control: 'inline-radio', options: ['key', 'primary', 'ember', 'ghost'] },
size: { control: 'inline-radio', options: ['sm', undefined, 'lg'] },
},
} satisfies Meta<typeof IconButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
args: { variant: 'key', children: <Gear size={18} /> },
};
export const Variants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12 }}>
<IconButton variant="key" aria-label="settings"><Gear size={18} /></IconButton>
<IconButton variant="primary" aria-label="add"><Plus size={18} weight="bold" /></IconButton>
<IconButton variant="ember" aria-label="delete"><Trash size={18} /></IconButton>
<IconButton variant="ghost" aria-label="settings"><Gear size={18} /></IconButton>
</div>
),
};
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<IconButton size="sm" variant="primary" aria-label="add"><Plus size={14} weight="bold" /></IconButton>
<IconButton variant="primary" aria-label="add"><Plus size={18} weight="bold" /></IconButton>
<IconButton size="lg" variant="primary" aria-label="add"><Plus size={22} weight="bold" /></IconButton>
</div>
),
};
+42
View File
@@ -0,0 +1,42 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Getting Started/Introduction" />
# ModernSK
Tactile, dark-first React components built on [Radix](https://www.radix-ui.com/) primitives.
Old-iOS skeuomorphism × macOS Sequoia neatness × Ubuntu warmth.
This Storybook is the **development playground** — it is never published with the
package. Use it to browse every component, read its prop table (the **Docs** tab on
each story), and try props live in the **Controls** panel.
## Using the library in an app
```tsx
import 'modern-sk/styles.css'; // required — tokens + components
import 'modern-sk/fonts.css'; // optional — branded faces
import { ThemeProvider, TooltipProvider, Button } from 'modern-sk';
export function App() {
return (
<ThemeProvider>
<TooltipProvider delayDuration={200}>
<Button variant="primary">Click</Button>
</TooltipProvider>
</ThemeProvider>
);
}
```
## Theme
Use the **Theme** toggle in the toolbar above to flip every story between dark and
light. In an app the same lever is `data-theme` on `<html>`, managed by
`ThemeProvider` / `useTheme()`.
## Fonts
Components read the `--font-display`, `--font-sans` and `--font-mono` tokens. Import
`modern-sk/fonts.css` for the branded faces, or override those tokens to map the
components onto fonts your app already loads.
+79
View File
@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Copy, Scissors, Trash, ArrowCounterClockwise } from '@phosphor-icons/react';
import {
Tooltip,
Menu,
MenuTrigger,
MenuContent,
MenuItem,
MenuSeparator,
Dialog,
AlertDialog,
Button,
} from '../components/ui';
const meta = {
title: 'Overlays/Floating',
component: Tooltip,
parameters: {
docs: {
description: {
component:
'Floating surfaces — Tooltip, dropdown Menu, Dialog and AlertDialog. All are Radix-backed and portal-rendered.',
},
},
},
} satisfies Meta<typeof Tooltip>;
export default meta;
type Story = StoryObj<typeof meta>;
export const TooltipStory: Story = {
name: 'Tooltip',
render: () => (
<Tooltip content="Saved to iCloud">
<Button variant="ghost">Hover me</Button>
</Tooltip>
),
};
export const DropdownMenu: Story = {
render: () => (
<Menu>
<MenuTrigger asChild>
<Button>Open menu</Button>
</MenuTrigger>
<MenuContent>
<MenuItem icon={<Copy size={16} />} shortcut="⌘C">Copy</MenuItem>
<MenuItem icon={<Scissors size={16} />} shortcut="⌘X">Cut</MenuItem>
<MenuItem icon={<ArrowCounterClockwise size={16} />} shortcut="⌘Z">Undo</MenuItem>
<MenuSeparator />
<MenuItem icon={<Trash size={16} />}>Delete</MenuItem>
</MenuContent>
</Menu>
),
};
export const ModalDialog: Story = {
render: () => (
<Dialog
trigger={<Button variant="primary">Open dialog</Button>}
title="Rename project"
description="Choose a new name for this project."
footer={<Button variant="primary">Save</Button>}
/>
),
};
export const Confirm: Story = {
render: () => (
<AlertDialog
trigger={<Button variant="ember">Delete</Button>}
title="Delete this project?"
description="This action cannot be undone."
actionLabel="Delete"
destructive
onAction={() => {}}
/>
),
};
+30
View File
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Select } from '../components/ui';
const items = [
{ value: 'sequoia', label: 'Sequoia' },
{ value: 'sonoma', label: 'Sonoma' },
{ value: 'ventura', label: 'Ventura' },
{ value: 'monterey', label: 'Monterey' },
];
const meta = {
title: 'Inputs/Select',
component: Select,
parameters: {
docs: {
description: {
component:
'Radix Select in the ModernSK skin. Pass `items` plus an optional `placeholder`; control it with `value` / `onValueChange` or leave it uncontrolled with `defaultValue`.',
},
},
},
args: { items, placeholder: 'Pick a release…', 'aria-label': 'macOS release' },
} satisfies Meta<typeof Select>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const WithDefault: Story = { args: { defaultValue: 'sonoma' } };
+74
View File
@@ -0,0 +1,74 @@
import { useState } from 'react';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import {
Switch,
Checkbox,
RadioGroup,
RadioItem,
SegmentedControl,
Control,
} from '../components/ui';
/* Grouped selection controls. Anchored on Switch for the docgen table;
the stories below showcase each control. */
const meta = {
title: 'Selection/Controls',
component: Switch,
parameters: {
docs: {
description: {
component:
'Toggle, checkbox, radio group and segmented control. The `Control` helper pairs any of them with a clickable label.',
},
},
},
} satisfies Meta<typeof Switch>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Switches: Story = {
render: () => (
<div style={{ display: 'flex', gap: 20, alignItems: 'center' }}>
<Switch defaultChecked />
<Switch />
<Control control={<Switch defaultChecked />}>Wi-Fi</Control>
</div>
),
};
export const Checkboxes: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Control control={<Checkbox defaultChecked />}>Sync to iCloud</Control>
<Control control={<Checkbox />}>Share analytics</Control>
</div>
),
};
export const Radios: Story = {
render: () => (
<RadioGroup defaultValue="comfortable" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Control control={<RadioItem value="compact" />}>Compact</Control>
<Control control={<RadioItem value="comfortable" />}>Comfortable</Control>
<Control control={<RadioItem value="spacious" />}>Spacious</Control>
</RadioGroup>
),
};
function SegmentedDemo() {
const [v, setV] = useState('day');
return (
<SegmentedControl
value={v}
onValueChange={setV}
items={[
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
]}
/>
);
}
export const Segmented: Story = { render: () => <SegmentedDemo /> };
+23
View File
@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Slider } from '../components/ui';
const meta = {
title: 'Inputs/Slider',
component: Slider,
parameters: {
docs: {
description: {
component:
'Radix Slider in the carved-track skin. All Radix Slider props pass through (`defaultValue`, `min`, `max`, `step`, `onValueChange`).',
},
},
},
decorators: [(Story) => <div style={{ width: 280 }}><Story /></div>],
} satisfies Meta<typeof Slider>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = { args: { defaultValue: [60], max: 100, step: 1 } };
export const Stepped: Story = { args: { defaultValue: [40], max: 100, step: 10 } };
+41
View File
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Tabs, TabsList, TabsContent } from '../components/ui';
const meta = {
title: 'Navigation/Tabs',
component: Tabs,
parameters: {
docs: {
description: {
component:
'Radix Tabs. `Tabs` is the root, `TabsList` takes `items`, and `TabsContent` matches each `value`.',
},
},
},
} satisfies Meta<typeof Tabs>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<Tabs defaultValue="overview" style={{ width: 420 }}>
<TabsList
items={[
{ value: 'overview', label: 'Overview' },
{ value: 'activity', label: 'Activity' },
{ value: 'settings', label: 'Settings' },
]}
/>
<TabsContent value="overview" style={{ paddingTop: 16 }} className="modern-sk-body">
Project at a glance.
</TabsContent>
<TabsContent value="activity" style={{ paddingTop: 16 }} className="modern-sk-body">
Recent activity feed.
</TabsContent>
<TabsContent value="settings" style={{ paddingTop: 16 }} className="modern-sk-body">
Preferences and configuration.
</TabsContent>
</Tabs>
),
};
+38
View File
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { MagnifyingGlass } from '@phosphor-icons/react';
import { TextField, TextArea, SearchField } from '../components/ui';
const meta = {
title: 'Inputs/TextField',
component: TextField,
parameters: {
docs: {
description: {
component:
'Sunken text input. `TextField`, `TextArea`, and `SearchField` (icon + input) share the `modern-sk-field` look and forward all native props.',
},
},
},
args: { placeholder: 'Type here…' },
} satisfies Meta<typeof TextField>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {};
export const Disabled: Story = { args: { disabled: true, value: 'Read only' } };
export const Multiline: Story = {
render: () => <TextArea rows={4} placeholder="Multiple lines…" style={{ width: 320 }} />,
};
export const Search: Story = {
render: () => (
<SearchField
icon={<MagnifyingGlass size={16} />}
placeholder="Search…"
style={{ width: 280 }}
/>
),
};
+31
View File
@@ -0,0 +1,31 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { Window, Badge, List, Row } from '../components/ui';
const meta = {
title: 'Layout/Window',
component: Window,
parameters: {
docs: {
description: {
component:
'macOS-style window chrome: traffic lights, a title, an optional `badge` slot, and arbitrary children for the body.',
},
},
},
args: { title: 'Finder' },
} satisfies Meta<typeof Window>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: (args) => (
<Window {...args} style={{ width: 380 }} badge={<Badge variant="lime" dot>Synced</Badge>}>
<List style={{ padding: 12 }}>
<Row selected>Documents</Row>
<Row>Downloads</Row>
<Row>Pictures</Row>
</List>
</Window>
),
};