diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..f5bf784 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,23 @@ +import type { StorybookConfig } from 'storybook-react-rsbuild'; + +/* Dev-only playground. Never shipped — package `files` is ["dist"]. */ +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(ts|tsx)'], + addons: ['@storybook/addon-docs'], + framework: { + name: 'storybook-react-rsbuild', + options: {}, + }, + typescript: { + // Prop tables in autodocs come from the components' TS types. + reactDocgen: 'react-docgen-typescript', + reactDocgenTypescriptOptions: { + shouldExtractLiteralValuesFromEnum: true, + // Keep our own props; drop the noise inherited from node_modules. + propFilter: (prop) => + prop.parent ? !/node_modules/.test(prop.parent.fileName) : true, + }, + }, +}; + +export default config; diff --git a/.storybook/manager.ts b/.storybook/manager.ts new file mode 100644 index 0000000..248aba0 --- /dev/null +++ b/.storybook/manager.ts @@ -0,0 +1,4 @@ +import { addons } from 'storybook/manager-api'; +import theme from './theme'; + +addons.setConfig({ theme }); diff --git a/.storybook/preview.css b/.storybook/preview.css new file mode 100644 index 0000000..b481165 --- /dev/null +++ b/.storybook/preview.css @@ -0,0 +1,19 @@ +/* Calm dark canvas. Uses the felt's subtle radial glow (no heavy fixed + grain) so the story sits on-brand without flooding the frame. The + --bg-glow / --ink tokens come from the imported stylesheet and react + to the data-theme toggle automatically. */ +.sb-show-main { + background-color: var(--ink); + background-image: var(--bg-glow); + background-size: cover; + background-repeat: no-repeat; + background-attachment: fixed; +} + +/* Autodocs preview blocks: same dark surface as the canvas. */ +.docs-story, +.sbdocs-preview { + background-color: var(--ink) !important; + background-image: var(--bg-glow); + background-size: cover; +} diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..cc693e6 --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,60 @@ +import { useEffect, type ReactNode } from 'react'; +import type { Preview, Decorator } from 'storybook-react-rsbuild'; +import { Tooltip } from 'radix-ui'; + +/* The shipped library surface, exactly as a consumer would load it. */ +import '../src/styles/fonts.css'; +import '../src/styles/index.css'; +/* Storybook-only canvas styling (background, docs blocks). */ +import './preview.css'; +import theme from './theme'; + +/* Toolbar theme switch → drives `data-theme` on , same lever as the + library's ThemeProvider. The frame stays content-sized; the dark canvas + comes from preview.css, so stories never balloon to full-viewport. */ +function ThemeFrame({ theme, children }: { theme: string; children: ReactNode }) { + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme); + }, [theme]); + + return {children}; +} + +const withModernSk: Decorator = (Story, context) => ( + + + +); + +const preview: Preview = { + tags: ['autodocs'], + decorators: [withModernSk], + parameters: { + layout: 'centered', + backgrounds: { disable: true }, + docs: { theme }, + controls: { + matchers: { color: /(background|color)$/i, date: /Date$/i }, + }, + options: { + storySort: { order: ['Getting Started', 'Inputs', 'Selection', 'Feedback', 'Overlays', 'Data Display', 'Layout'] }, + }, + }, + globalTypes: { + theme: { + description: 'ModernSK theme', + defaultValue: 'dark', + toolbar: { + title: 'Theme', + icon: 'contrast', + items: [ + { value: 'dark', title: 'Dark', icon: 'moon' }, + { value: 'light', title: 'Light', icon: 'sun' }, + ], + dynamicTitle: true, + }, + }, + }, +}; + +export default preview; diff --git a/.storybook/theme.ts b/.storybook/theme.ts new file mode 100644 index 0000000..0a2b910 --- /dev/null +++ b/.storybook/theme.ts @@ -0,0 +1,39 @@ +import { create } from 'storybook/theming'; + +/* Dark, lime-accented chrome so the Storybook UI + autodocs match the + components instead of framing them in stock white. */ +export default create({ + base: 'dark', + + brandTitle: 'ModernSK', + colorPrimary: '#bef264', + colorSecondary: '#bef264', + + // App + appBg: '#0f100d', + appContentBg: '#0f100d', + appPreviewBg: '#0f100d', + appBorderColor: '#2a2c22', + appBorderRadius: 10, + + // Typography + fontBase: "'Onest', system-ui, -apple-system, 'Segoe UI', sans-serif", + fontCode: "'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace", + + // Text + textColor: '#f3f4ee', + textInverseColor: '#0f100d', + textMutedColor: '#9a9c8c', + + // Toolbar / sidebar bars + barBg: '#16170f', + barTextColor: '#9a9c8c', + barSelectedColor: '#bef264', + barHoverColor: '#bef264', + + // Inputs + inputBg: '#1c1d16', + inputBorder: '#2a2c22', + inputTextColor: '#f3f4ee', + inputBorderRadius: 8, +}); diff --git a/src/stories/Button.stories.tsx b/src/stories/Button.stories.tsx new file mode 100644 index 0000000..16c8cf9 --- /dev/null +++ b/src/stories/Button.stories.tsx @@ -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 ` + + + + + ), +}; + +export const Sizes: Story = { + render: () => ( +
+ + +
+ ), +}; + +export const Disabled: Story = { + args: { disabled: true, variant: 'primary', children: 'Disabled' }, +}; diff --git a/src/stories/DataDisplay.stories.tsx b/src/stories/DataDisplay.stories.tsx new file mode 100644 index 0000000..1ea8ca5 --- /dev/null +++ b/src/stories/DataDisplay.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const CardSurface: Story = { + render: () => ( + +

Storage

+

128 GB of 256 GB used.

+
+ ), +}; + +export const ListRows: Story = { + render: () => ( + + General + Appearance + Notifications + Privacy + + ), +}; + +export const DataTable: Story = { + render: () => ( + + + + + + + + + + + + + + + + + + + + +
DeviceStatusBattery
MacBook ProOnline82%
iPhone 16Idle54%
+ ), +}; diff --git a/src/stories/Feedback.stories.tsx b/src/stories/Feedback.stories.tsx new file mode 100644 index 0000000..97fbea5 --- /dev/null +++ b/src/stories/Feedback.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +function ProgressDemo() { + const [v, setV] = useState(40); + return ( +
+ +
+ + +
+
+ ); +} + +export const ProgressBar: Story = { render: () => }; + +export const Spinners: Story = { + render: () => ( +
+ + + +
+ ), +}; + +export const Callouts: Story = { + render: () => ( +
+ }>Sync runs in the background. + }>All changes saved. + }>Storage is almost full. + }>Failed to reach the server. +
+ ), +}; + +export const Badges: Story = { + render: () => ( +
+ Lime + Ember + Neutral + Outline + Online +
+ ), +}; + +export const Chips: Story = { + render: () => ( +
+ Design + {}}>Removable +
+ ), +}; diff --git a/src/stories/IconButton.stories.tsx b/src/stories/IconButton.stories.tsx new file mode 100644 index 0000000..8fd4ac1 --- /dev/null +++ b/src/stories/IconButton.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { variant: 'key', children: }, +}; + +export const Variants: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +export const Sizes: Story = { + render: () => ( +
+ + + +
+ ), +}; diff --git a/src/stories/Introduction.mdx b/src/stories/Introduction.mdx new file mode 100644 index 0000000..c13604f --- /dev/null +++ b/src/stories/Introduction.mdx @@ -0,0 +1,42 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# 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 ( + + + + + + ); +} +``` + +## 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 ``, 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. diff --git a/src/stories/Overlays.stories.tsx b/src/stories/Overlays.stories.tsx new file mode 100644 index 0000000..4511122 --- /dev/null +++ b/src/stories/Overlays.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const TooltipStory: Story = { + name: 'Tooltip', + render: () => ( + + + + ), +}; + +export const DropdownMenu: Story = { + render: () => ( + + + + + + } shortcut="⌘C">Copy + } shortcut="⌘X">Cut + } shortcut="⌘Z">Undo + + }>Delete + + + ), +}; + +export const ModalDialog: Story = { + render: () => ( + Open dialog} + title="Rename project" + description="Choose a new name for this project." + footer={} + /> + ), +}; + +export const Confirm: Story = { + render: () => ( + Delete…} + title="Delete this project?" + description="This action cannot be undone." + actionLabel="Delete" + destructive + onAction={() => {}} + /> + ), +}; diff --git a/src/stories/Select.stories.tsx b/src/stories/Select.stories.tsx new file mode 100644 index 0000000..9e3566f --- /dev/null +++ b/src/stories/Select.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +export const WithDefault: Story = { args: { defaultValue: 'sonoma' } }; diff --git a/src/stories/Selection.stories.tsx b/src/stories/Selection.stories.tsx new file mode 100644 index 0000000..fe77632 --- /dev/null +++ b/src/stories/Selection.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Switches: Story = { + render: () => ( +
+ + + }>Wi-Fi +
+ ), +}; + +export const Checkboxes: Story = { + render: () => ( +
+ }>Sync to iCloud + }>Share analytics +
+ ), +}; + +export const Radios: Story = { + render: () => ( + + }>Compact + }>Comfortable + }>Spacious + + ), +}; + +function SegmentedDemo() { + const [v, setV] = useState('day'); + return ( + + ); +} + +export const Segmented: Story = { render: () => }; diff --git a/src/stories/Slider.stories.tsx b/src/stories/Slider.stories.tsx new file mode 100644 index 0000000..5a4fe26 --- /dev/null +++ b/src/stories/Slider.stories.tsx @@ -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) =>
], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { args: { defaultValue: [60], max: 100, step: 1 } }; + +export const Stepped: Story = { args: { defaultValue: [40], max: 100, step: 10 } }; diff --git a/src/stories/Tabs.stories.tsx b/src/stories/Tabs.stories.tsx new file mode 100644 index 0000000..9004e4c --- /dev/null +++ b/src/stories/Tabs.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + render: () => ( + + + + Project at a glance. + + + Recent activity feed. + + + Preferences and configuration. + + + ), +}; diff --git a/src/stories/TextField.stories.tsx b/src/stories/TextField.stories.tsx new file mode 100644 index 0000000..aa6bdee --- /dev/null +++ b/src/stories/TextField.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +export const Disabled: Story = { args: { disabled: true, value: 'Read only' } }; + +export const Multiline: Story = { + render: () =>