578 lines
18 KiB
TypeScript
578 lines
18 KiB
TypeScript
import { useState } from 'react';
|
||
import {
|
||
ArrowRight,
|
||
Trash,
|
||
Plus,
|
||
DotsThree,
|
||
MagnifyingGlass,
|
||
Folder,
|
||
FolderOpen,
|
||
PencilSimple,
|
||
Copy,
|
||
Tag,
|
||
Info,
|
||
FilePdf,
|
||
FileText,
|
||
CheckCircle,
|
||
Warning,
|
||
WarningOctagon,
|
||
} from '@phosphor-icons/react';
|
||
import { useTheme } from './components/theme';
|
||
import {
|
||
AlertDialog,
|
||
Badge,
|
||
Button,
|
||
Callout,
|
||
Card,
|
||
Checkbox,
|
||
Chip,
|
||
Control,
|
||
Dialog,
|
||
DialogClose,
|
||
IconButton,
|
||
Knob,
|
||
List,
|
||
MenuRow,
|
||
MenuSeparator,
|
||
MenuSurface,
|
||
Progress,
|
||
RadioGroup,
|
||
RadioItem,
|
||
Row,
|
||
ScrollArea,
|
||
SearchField,
|
||
SegmentedControl,
|
||
Select,
|
||
Slider,
|
||
Spinner,
|
||
Stepper,
|
||
Switch,
|
||
TBody,
|
||
THead,
|
||
Table,
|
||
Tabs,
|
||
TabsContent,
|
||
TabsList,
|
||
Td,
|
||
TextArea,
|
||
TextField,
|
||
Th,
|
||
Tooltip,
|
||
Tr,
|
||
Window,
|
||
} from './components/ui';
|
||
|
||
const Section = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||
<section>
|
||
<div className="sec-label">{label}</div>
|
||
{children}
|
||
</section>
|
||
);
|
||
|
||
const App = () => {
|
||
const { theme, setTheme } = useTheme();
|
||
const [seg, setSeg] = useState('list');
|
||
const [tab, setTab] = useState('general');
|
||
const [count, setCount] = useState(4);
|
||
const [chips, setChips] = useState(['design', '2026', 'invoices']);
|
||
|
||
return (
|
||
<div className="modern-sk-felt">
|
||
<div className="wrap">
|
||
<header>
|
||
<div className="topbar">
|
||
<div>
|
||
<div className="word">
|
||
MODERN<b>SK</b> · KITCHEN SINK
|
||
</div>
|
||
<p className="sub">
|
||
Every component, live and interactive, built on Radix Primitives
|
||
styled from the global tokens in{' '}
|
||
<span className="modern-sk-mono">tokens.css</span> +{' '}
|
||
<span className="modern-sk-mono">components.css</span>. Click, toggle,
|
||
focus — it all responds.
|
||
</p>
|
||
</div>
|
||
<div style={{ flexShrink: 0 }}>
|
||
<SegmentedControl
|
||
value={theme}
|
||
onValueChange={(v) => setTheme(v as 'dark' | 'light')}
|
||
items={[
|
||
{ value: 'dark', label: 'Dark' },
|
||
{ value: 'light', label: 'Light' },
|
||
]}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
{/* BUTTONS */}
|
||
<Section label="Buttons">
|
||
<div className="stack">
|
||
<div>
|
||
<div className="cap">Variants</div>
|
||
<div className="cluster">
|
||
<Button variant="primary">
|
||
<ArrowRight size={16} />
|
||
Primary
|
||
</Button>
|
||
<Button>Push button</Button>
|
||
<Button variant="ember">
|
||
<Trash size={16} />
|
||
Delete
|
||
</Button>
|
||
<Button variant="ghost">Cancel</Button>
|
||
<Button disabled>Disabled</Button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="cap">Sizes & icon</div>
|
||
<div className="cluster">
|
||
<Button variant="primary" size="sm">
|
||
Small
|
||
</Button>
|
||
<Button size="sm">Small</Button>
|
||
<Button iconOnly aria-label="Add">
|
||
<Plus size={16} />
|
||
</Button>
|
||
<Button iconOnly aria-label="More">
|
||
<DotsThree size={16} weight="bold" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* FIELDS */}
|
||
<Section label="Text fields & selects">
|
||
<div className="two">
|
||
<div className="stack">
|
||
<div>
|
||
<div className="lab">Name</div>
|
||
<TextField defaultValue="Quarterly Report" />
|
||
</div>
|
||
<div>
|
||
<div className="lab">Location</div>
|
||
<TextField placeholder="Choose a folder…" />
|
||
</div>
|
||
<div>
|
||
<div className="lab">Search</div>
|
||
<SearchField
|
||
icon={<MagnifyingGlass size={16} />}
|
||
placeholder="Search files…"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="stack">
|
||
<div>
|
||
<div className="lab">View</div>
|
||
<Select
|
||
defaultValue="name"
|
||
aria-label="Sort order"
|
||
items={[
|
||
{ value: 'name', label: 'Sort by name' },
|
||
{ value: 'date', label: 'Sort by date' },
|
||
{ value: 'size', label: 'Sort by size' },
|
||
]}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<div className="lab">Notes</div>
|
||
<TextArea
|
||
placeholder="Add a note…"
|
||
defaultValue="Everything right at your hands."
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* TOGGLES */}
|
||
<Section label="Switches · checkboxes · radios">
|
||
<div className="three">
|
||
<div>
|
||
<div className="cap">Switch</div>
|
||
<div className="stack">
|
||
<Control control={<Switch defaultChecked />}>
|
||
Sync across devices
|
||
</Control>
|
||
<Control control={<Switch />}>Show hidden files</Control>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="cap">Checkbox</div>
|
||
<div className="stack">
|
||
<Control control={<Checkbox defaultChecked />}>
|
||
Include subfolders
|
||
</Control>
|
||
<Control control={<Checkbox />}>Follow symlinks</Control>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="cap">Radio</div>
|
||
<RadioGroup defaultValue="list" className="stack">
|
||
<Control control={<RadioItem value="list" />}>List view</Control>
|
||
<Control control={<RadioItem value="grid" />}>Grid view</Control>
|
||
</RadioGroup>
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* CONTROLS */}
|
||
<Section label="Segmented · slider · stepper · tabs · progress">
|
||
<div className="two">
|
||
<div className="stack">
|
||
<div>
|
||
<div className="cap">Segmented</div>
|
||
<SegmentedControl
|
||
value={seg}
|
||
onValueChange={setSeg}
|
||
items={[
|
||
{ value: 'icons', label: 'Icons' },
|
||
{ value: 'list', label: 'List' },
|
||
{ value: 'columns', label: 'Columns' },
|
||
{ value: 'gallery', label: 'Gallery' },
|
||
]}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<div className="cap">Slider & stepper</div>
|
||
<div className="cluster">
|
||
<Slider defaultValue={[62]} max={100} step={1} />
|
||
<Slider defaultValue={[40]} max={100} step={20} marks />
|
||
<Stepper
|
||
onDecrement={() => setCount((n) => Math.max(0, n - 1))}
|
||
onIncrement={() => setCount((n) => n + 1)}
|
||
/>
|
||
<span className="modern-sk-mono" style={{ color: 'var(--fg-2)' }}>
|
||
{count}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="cap">Knobs — circular sliders</div>
|
||
<div className="cluster" style={{ gap: 40, alignItems: 'flex-start' }}>
|
||
<Knob defaultValue={62} aria-label="Volume" />
|
||
<Knob defaultValue={3} min={1} max={5} step={1} aria-label="Quality" />
|
||
<Knob defaultValue={40} accent="ember" aria-label="Warmth" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="stack">
|
||
<div>
|
||
<div className="cap">Tabs</div>
|
||
<Tabs value={tab} onValueChange={setTab}>
|
||
<TabsList
|
||
items={[
|
||
{ value: 'general', label: 'General' },
|
||
{ value: 'sharing', label: 'Sharing' },
|
||
{ value: 'tags', label: 'Tags' },
|
||
]}
|
||
/>
|
||
<TabsContent value={tab} />
|
||
</Tabs>
|
||
</div>
|
||
<div>
|
||
<div className="cap">Progress</div>
|
||
<div style={{ marginTop: 6 }}>
|
||
<Progress value={64} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* BADGES */}
|
||
<Section label="Badges · chips · tags">
|
||
<div className="stack">
|
||
<div className="cluster">
|
||
<Badge variant="lime">Synced</Badge>
|
||
<Badge variant="ember">3 conflicts</Badge>
|
||
<Badge variant="neutral">Draft</Badge>
|
||
<Badge variant="outline">v2.4</Badge>
|
||
<Badge variant="neutral" dot style={{ color: 'var(--lime)' }}>
|
||
Online
|
||
</Badge>
|
||
</div>
|
||
<div className="cluster">
|
||
{chips.map((c) => (
|
||
<Chip
|
||
key={c}
|
||
onRemove={() => setChips((cs) => cs.filter((x) => x !== c))}
|
||
>
|
||
{c}
|
||
</Chip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* SURFACES */}
|
||
<Section label="Cards · list rows · menu">
|
||
<div className="three">
|
||
<Card>
|
||
<div style={{ fontSize: 24, color: 'var(--lime)' }}>
|
||
<Folder weight="fill" />
|
||
</div>
|
||
<div style={{ fontWeight: 600, marginTop: 10 }}>Projects</div>
|
||
<div
|
||
style={{ color: 'var(--fg-3)', fontSize: 13, marginTop: 2 }}
|
||
>
|
||
24 items · 1.2 GB
|
||
</div>
|
||
<div style={{ marginTop: 14 }}>
|
||
<Progress value={48} />
|
||
</div>
|
||
</Card>
|
||
|
||
<List>
|
||
<Row selected>
|
||
<Folder weight="fill" size={22} color="#bef264" />
|
||
<span className="nm">Projects</span>
|
||
<span className="meta">24</span>
|
||
</Row>
|
||
<Row>
|
||
<Folder weight="fill" size={22} color="#e9572b" />
|
||
<span className="nm">Invoices</span>
|
||
<span className="meta">8</span>
|
||
</Row>
|
||
<Row>
|
||
<FilePdf weight="fill" size={22} color="var(--fg-2)" />
|
||
<span className="nm">Contract.pdf</span>
|
||
<span className="meta">2.4 MB</span>
|
||
</Row>
|
||
<Row>
|
||
<FileText weight="fill" size={22} color="var(--fg-2)" />
|
||
<span className="nm">notes.md</span>
|
||
<span className="meta">12 KB</span>
|
||
</Row>
|
||
</List>
|
||
|
||
<MenuSurface>
|
||
<MenuRow icon={<FolderOpen size={16} />} shortcut="⌘O">
|
||
Open
|
||
</MenuRow>
|
||
<MenuRow icon={<PencilSimple size={16} />} shortcut="⏎">
|
||
Rename
|
||
</MenuRow>
|
||
<MenuRow icon={<Copy size={16} />} shortcut="⌘D">
|
||
Duplicate
|
||
</MenuRow>
|
||
<MenuSeparator />
|
||
<MenuRow icon={<Tag size={16} />}>Add Tag…</MenuRow>
|
||
<MenuRow icon={<Info size={16} />} shortcut="⌘I">
|
||
Get Info
|
||
</MenuRow>
|
||
</MenuSurface>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* WINDOW */}
|
||
<Section label="Window — components composed">
|
||
<Window
|
||
title="New Bookmark"
|
||
style={{ maxWidth: 600 }}
|
||
badge={
|
||
<Badge variant="neutral" dot style={{ color: 'var(--lime)' }}>
|
||
Saved
|
||
</Badge>
|
||
}
|
||
>
|
||
<div className="panel">
|
||
<div className="stack">
|
||
<div>
|
||
<div className="lab">Name</div>
|
||
<TextField defaultValue="Projects" />
|
||
</div>
|
||
<div>
|
||
<div className="lab">Location</div>
|
||
<SearchField
|
||
icon={<Folder size={16} />}
|
||
defaultValue="~/Documents/2026"
|
||
/>
|
||
</div>
|
||
<div style={{ marginTop: 2 }}>
|
||
<Control control={<Switch defaultChecked />}>
|
||
Sync across devices
|
||
</Control>
|
||
</div>
|
||
<div
|
||
className="cluster"
|
||
style={{
|
||
justifyContent: 'flex-end',
|
||
marginTop: 6,
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<Button variant="ghost">Cancel</Button>
|
||
<Button variant="primary">Add Bookmark</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Window>
|
||
<div style={{ marginTop: 14 }}>
|
||
<Tooltip
|
||
content={
|
||
<>
|
||
<Info
|
||
size={14}
|
||
style={{ marginRight: 5, color: 'var(--lime)' }}
|
||
/>
|
||
Tooltip — appears over floating layers
|
||
</>
|
||
}
|
||
>
|
||
<Button variant="ghost" size="sm">
|
||
Hover for tooltip
|
||
</Button>
|
||
</Tooltip>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* ICON BUTTONS · SPINNER */}
|
||
<Section label="Icon buttons · spinner">
|
||
<div className="cluster">
|
||
<IconButton variant="primary" aria-label="Add">
|
||
<Plus size={16} weight="bold" />
|
||
</IconButton>
|
||
<IconButton aria-label="Edit">
|
||
<PencilSimple size={15} />
|
||
</IconButton>
|
||
<IconButton variant="ember" aria-label="Delete">
|
||
<Trash size={15} />
|
||
</IconButton>
|
||
<IconButton variant="ghost" aria-label="More">
|
||
<DotsThree size={18} weight="bold" />
|
||
</IconButton>
|
||
<IconButton size="lg" aria-label="Open">
|
||
<FolderOpen size={18} />
|
||
</IconButton>
|
||
<span style={{ width: 16 }} />
|
||
<Spinner size="sm" />
|
||
<Spinner />
|
||
<Spinner size="lg" />
|
||
</div>
|
||
</Section>
|
||
|
||
{/* CALLOUTS */}
|
||
<Section label="Callouts">
|
||
<div className="stack">
|
||
<Callout variant="info" icon={<Info size={17} weight="fill" />}>
|
||
<strong>Heads up.</strong> Files sync automatically when you’re
|
||
online — no manual save needed.
|
||
</Callout>
|
||
<Callout variant="success" icon={<CheckCircle size={17} weight="fill" />}>
|
||
<strong>All set.</strong> Your 24 projects are backed up and
|
||
encrypted.
|
||
</Callout>
|
||
<Callout variant="warning" icon={<Warning size={17} weight="fill" />}>
|
||
<strong>Low space.</strong> 1.2 GB left on this device.
|
||
</Callout>
|
||
<Callout variant="danger" icon={<WarningOctagon size={17} weight="fill" />}>
|
||
<strong>3 conflicts.</strong> Some files changed in two places at
|
||
once.
|
||
</Callout>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* TABLE */}
|
||
<Section label="Table">
|
||
<Table>
|
||
<THead>
|
||
<Tr>
|
||
<Th>Name</Th>
|
||
<Th>Owner</Th>
|
||
<Th>Status</Th>
|
||
<Th style={{ textAlign: 'right' }}>Size</Th>
|
||
</Tr>
|
||
</THead>
|
||
<TBody>
|
||
<Tr selected>
|
||
<Td>Quarterly Report.pdf</Td>
|
||
<Td className="muted">You</Td>
|
||
<Td>
|
||
<Badge variant="lime">Synced</Badge>
|
||
</Td>
|
||
<Td className="num">2.4 MB</Td>
|
||
</Tr>
|
||
<Tr>
|
||
<Td>Invoices</Td>
|
||
<Td className="muted">Mara K.</Td>
|
||
<Td>
|
||
<Badge variant="ember">3 conflicts</Badge>
|
||
</Td>
|
||
<Td className="num">812 KB</Td>
|
||
</Tr>
|
||
<Tr>
|
||
<Td>notes.md</Td>
|
||
<Td className="muted">You</Td>
|
||
<Td>
|
||
<Badge variant="neutral">Draft</Badge>
|
||
</Td>
|
||
<Td className="num">12 KB</Td>
|
||
</Tr>
|
||
</TBody>
|
||
</Table>
|
||
</Section>
|
||
|
||
{/* SCROLL AREA */}
|
||
<Section label="Scroll area">
|
||
<Card style={{ padding: 0, maxWidth: 320 }}>
|
||
<ScrollArea style={{ height: 160 }}>
|
||
<List style={{ border: 'none', boxShadow: 'none', borderRadius: 0 }}>
|
||
{Array.from({ length: 12 }).map((_, i) => (
|
||
<Row key={i}>
|
||
<FileText weight="fill" size={20} color="var(--fg-2)" />
|
||
<span className="nm">document-{i + 1}.txt</span>
|
||
<span className="meta">{(i + 1) * 7} KB</span>
|
||
</Row>
|
||
))}
|
||
</List>
|
||
</ScrollArea>
|
||
</Card>
|
||
</Section>
|
||
|
||
{/* DIALOGS */}
|
||
<Section label="Dialog · alert dialog">
|
||
<div className="cluster">
|
||
<Dialog
|
||
title="Rename file"
|
||
description="Choose a new name for this item."
|
||
trigger={<Button variant="primary">Open dialog</Button>}
|
||
footer={
|
||
<>
|
||
<DialogClose asChild>
|
||
<Button variant="ghost">Cancel</Button>
|
||
</DialogClose>
|
||
<DialogClose asChild>
|
||
<Button variant="primary">Save</Button>
|
||
</DialogClose>
|
||
</>
|
||
}
|
||
>
|
||
<div className="lab">Name</div>
|
||
<TextField defaultValue="Quarterly Report.pdf" autoFocus />
|
||
</Dialog>
|
||
|
||
<AlertDialog
|
||
title="Delete 3 files?"
|
||
description="This permanently removes the selected files. This action cannot be undone."
|
||
cancelLabel="Keep files"
|
||
actionLabel="Delete"
|
||
destructive
|
||
trigger={
|
||
<Button variant="ember">
|
||
<Trash size={16} />
|
||
Delete…
|
||
</Button>
|
||
}
|
||
/>
|
||
</div>
|
||
</Section>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default App;
|