How it works
src/components/ui/actions/icon-button.tsx
import type { ReactNode } from 'react'
import type { LucideIcon } from 'lucide-react'
/**
* A button with a leading icon and text label.
*
* @example <IconButton icon={Heart}>Like</IconButton>
* @example <IconButton icon={Settings} rounded>Settings</IconButton>
* @example <IconButton icon={Trash2} disabled>Delete</IconButton>
*/
export interface IconButtonProps {
children: ReactNode
icon: LucideIcon
rounded?: boolean
disabled?: boolean
}
/** A button with a leading icon and text label. */
export function IconButton({ children, icon: Icon, rounded, disabled }: IconButtonProps) {
return (
<button
type="button"
disabled={disabled}
className={`inline-flex items-center gap-2 px-3 h-9 bg-surface-raised
border border-border text-fg text-sm hover:bg-surface-overlay transition-colors
${rounded ? 'rounded-full' : 'rounded-md'}
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
<Icon size={16} />
<span>{children}</span>
</button>
)
}
Generated showcase
$ bunx jc extract
Scanning src/components/ui/**/*.tsx...
actions/command-button.tsx CommandButton 7 props 2 presets
actions/icon-button.tsx IconButton 3 props 3 presets
data-display/stat-card.tsx StatCard 6 props 3 presets
forms/text-field.tsx TextField 8 props 4 presets
feedback/callout.tsx Callout 4 props 4 presets
...
✓ 16 components extracted in 280ms
✓ Written to src/jc/generated/meta.json
✓ Written to src/jc/generated/registry.tsYour TypeScript interfaces are the source of truth. No .stories.ts, no boilerplate, no duplication.
Runs alongside your dev server. One command extracts metadata, your existing bundler serves the page.
Faker-powered prop values, icon pickers, light/dark themes, responsive viewports. Works out of the box.
Features
@example blocks become one-click presets. Wrapper components, default props, children — parsed from JSDoc you already write.
/**
* @example
* <Button variant="primary" size="lg">
* Save changes
* </Button>
*/Register icon libraries or custom components as fixtures. Matching props get a visual picker automatically.
const icons = defineFixtures({
name: 'lucide',
fixtures: [
{ key: 'star', label: 'Star',
render: () => <Star /> },
],
})Component and prop state is serialized into the URL. Share a link — it loads exactly as you left it.
/showcase?component=Button#s=eyJwIjp7
InZhcmlhbnQiOiJwcmltYXJ5In19Comparison
| Storybook | jc | |
|---|---|---|
| Config per component | stories.tsx + meta + argTypes | None |
| Separate build process | Yes (webpack/vite) | No |
| Type extraction | Manual argTypes | Automatic from TS |
| Icon/component pickers | Custom addons | Built-in fixtures |
| URL state sharing | Limited | Full prop serialization |
| Bundle size | ~2MB | ~80KB |
button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './button'
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['default', 'primary', 'ghost'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: { control: 'boolean' },
},
}
export default meta
type Story = StoryObj<typeof Button>
export const Primary: Story = {
args: { children: 'Click me', variant: 'primary' },
}with jc — nothing to write
$ bunx jc extract// showcase/page.tsx
import { createShowcasePage } from 'jc/next'
import meta from '@/jc/generated/meta.json'
import { registry } from '@/jc/generated/registry'
export default createShowcasePage({ meta, registry })Setup
1 — install
bun add jc2 — extract
bunx jc extract3 — render
import { createShowcasePage } from 'jc/next'
import meta from '@/jc/generated/meta.json'
import { registry } from '@/jc/generated/registry'
export default createShowcasePage({ meta, registry })FAQ
Get notified about releases