@parasutcom/fds
v0.2.0
Published
FDS React UI components.
Maintainers
Readme
@parasutcom/fds — Frame Design System
A React component library built with Base UI, Tailwind CSS v4, and class-variance-authority.
Quick Start
In the monorepo (workspace)
// package.json
{ "dependencies": { "@parasutcom/fds": "workspace:*" } }/* globals.css */
@import 'tailwindcss';
@import 'tw-animate-css';
@import '@parasutcom/fds/theme.css';
@source '../../../packages/shared/ui/src/**/*.{ts,tsx}';
@source '../**/*.{ts,tsx}';import { Button } from '@parasutcom/fds'
export default function Page() {
return <Button variant="outline">Click me</Button>
}From npm (external project)
npm install @parasutcom/fds tailwindcss@^4 @tailwindcss/postcss lucide-react/* globals.css */
@import 'tailwindcss';
@import '@parasutcom/fds/styles.css';import { Button } from '@parasutcom/fds'Development
pnpm dev # Start docs site (Vite dev server)
pnpm build # Build library to dist/
pnpm storybook # Start Storybook on port 6006
pnpm release # Build + publish to npmThe pnpm dev command starts a documentation site at http://localhost:5173 with live demos for every component.
Creating a New Component
1. Create the component file
src/components/ui/my-component.tsxEvery component follows the same pattern:
'use client'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@fds/lib/utils'
const myComponentVariants = cva(
'base-classes-here',
{
variants: {
variant: {
default: 'default-style-classes',
outline: 'outline-style-classes',
},
size: {
sm: 'h-8 text-sm',
default: 'h-10 text-base',
lg: 'h-12 text-lg',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function MyComponent({
className,
variant,
size,
...props
}: React.ComponentProps<'div'> &
VariantProps<typeof myComponentVariants>) {
return (
<div
className={cn(myComponentVariants({ variant, size }), className)}
{...props}
/>
)
}
export { MyComponent, myComponentVariants }Key rules:
- Always start with
'use client' - Use
cvafor variants (not conditional classnames) - Use
cn()from@fds/lib/utilsfor merging classnames - Use
@fds/path alias for internal imports - Accept
classNameprop and merge it withcn() - Use
React.ComponentProps<'element'>for typing (extends native HTML) - Export both the component and variants
2. For compound components (with subcomponents)
Use Base UI primitives as the headless foundation:
'use client'
import * as React from 'react'
import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@fds/lib/utils'
// Root component
function MyDialog({ children, ...props }: React.ComponentProps<'div'>) {
return <div {...props}>{children}</div>
}
// Subcomponents
function MyDialogTitle({ className, ...props }: React.ComponentProps<'h2'>) {
return <h2 className={cn('text-lg font-semibold', className)} {...props} />
}
function MyDialogContent({ className, ...props }: React.ComponentProps<'div'>) {
return <div className={cn('p-6', className)} {...props} />
}
export { MyDialog, MyDialogTitle, MyDialogContent }3. Export from the library
Add the export to src/index.ts:
export * from './components/ui/my-component'4. Add documentation
Create a doc page at src/docs/pages/my-component/:
src/docs/pages/my-component/
my-component-page.tsx # Documentation page
demos/
basic.tsx # Basic usage demo
variants.tsx # Variants showcaseDemo file (demos/basic.tsx):
import { MyComponent } from '@parasutcom/fds'
export function BasicDemo() {
return <MyComponent variant="outline">Hello</MyComponent>
}Page file (my-component-page.tsx):
import { DemoCard } from '../../components/demo-card'
import { DemoGrid } from '../../components/demo-grid'
import { ApiTable, type PropDefinition } from '../../components/api-table'
import { BasicDemo } from './demos/basic'
import basicSource from './demos/basic.tsx?raw'
const apiProps: PropDefinition[] = [
{
name: 'variant',
description: 'Visual style',
type: '"default" | "outline"',
default: '"default"',
},
]
export function MyComponentPage() {
return (
<div className="space-y-10">
<div>
<h1 id="my-component" className="text-3xl font-bold">MyComponent</h1>
<p className="mt-2 text-lg text-muted-foreground">
Short description.
</p>
</div>
<section>
<h2 id="when-to-use" className="text-xl font-semibold mb-3">When To Use</h2>
<p className="text-muted-foreground">Guidance text.</p>
</section>
<section>
<h2 id="examples" className="text-xl font-semibold mb-6">Examples</h2>
<DemoGrid>
<DemoCard title="Basic" sourceCode={basicSource} id="demo-basic">
<BasicDemo />
</DemoCard>
</DemoGrid>
</section>
<section>
<h2 id="api" className="text-xl font-semibold mb-6">API</h2>
<ApiTable title="MyComponent" props={apiProps} />
</section>
</div>
)
}5. Register the route
Add to src/docs/lib/nav-data.ts:
{ title: 'MyComponent', href: '/components/my-component' },Add to src/docs/lib/routes.tsx:
import { MyComponentPage } from '../pages/my-component/my-component-page'
// ...
{ path: 'components/my-component', element: <MyComponentPage /> },Using Components
Import
All components are exported from the package root:
import { Button, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@parasutcom/fds'Icons
FDS uses Lucide React for icons (peer dependency):
import { Loader2Icon, MailIcon } from 'lucide-react'
<Button disabled>
<Loader2Icon className="mr-2 size-4 animate-spin" />
Loading...
</Button>Variants & Sizes
Most components accept variant and size props via CVA:
<Button variant="destructive" size="lg">Delete</Button>
<Badge variant="outline">Status</Badge>
<Input size="sm" variant="outline" />Compound Components
Complex components use the compound component pattern:
<Select>
<SelectTrigger>
<SelectValue placeholder="Pick one" />
</SelectTrigger>
<SelectContent>
<SelectItem value="a">Option A</SelectItem>
<SelectItem value="b">Option B</SelectItem>
</SelectContent>
</Select>Render Prop (polymorphic elements)
Some components support Base UI's render prop to change the underlying element:
import { Link } from 'react-router-dom'
<SidebarMenuButton render={<Link to="/about" />}>
About
</SidebarMenuButton>Server Components
All FDS components include 'use client'. In Next.js Server Component pages, either:
- Import them directly (Next.js auto-creates the client boundary)
- Or wrap in a Client Component for explicit control
Project Structure
packages/shared/ui/
├── src/
│ ├── index.ts # Library entry — all exports
│ ├── index.css # Tailwind + theme (bundled as styles.css)
│ ├── theme.css # Theme tokens only (for workspace consumers)
│ ├── components/
│ │ ├── ui/ # Base components (Button, Select, Dialog, etc.)
│ │ └── customized/ # Higher-level components (TextField, DataTable, etc.)
│ ├── hooks/ # Shared hooks (use-mobile)
│ ├── lib/ # Utilities (cn, dictionary)
│ ├── styles/ # Extended theme tokens (fds-theme.css, dev-fonts.css)
│ └── docs/ # Documentation site (dev-only, excluded from build)
│ ├── app.tsx # Router root
│ ├── lib/ # Routes, nav data, hooks
│ ├── layout/ # Sidebar, header, TOC
│ ├── components/ # DemoCard, ApiTable, CodeBlock
│ └── pages/ # One folder per component
├── public/ # Static assets for docs site
├── scripts/
│ └── prepare-publish.mjs # Rewrites package.json for npm publish
├── vite.config.ts # Build config (lib mode + dev server)
└── package.json # @parasutcom/fdsCSS Architecture
| File | Purpose | Who imports it |
|------|---------|----------------|
| styles.css | Full bundle (Tailwind + theme + component styles) | npm consumers |
| theme.css | Theme tokens only (colors, fonts, radii) | Workspace apps via globals.css |
| index.css | Source CSS (builds into styles.css) | Internal only |
Workspace apps use theme.css because they run their own Tailwind pipeline and only need the design tokens. npm consumers use styles.css which includes everything pre-built.
