npm package: @parasutcom/fds
Description: FDS React UI components.
Author: Mikrogrup
Latest version: 0.2.0

# @parasutcom/fds — Frame Design System

A React component library built with [Base UI](https://base-ui.com/), [Tailwind CSS v4](https://tailwindcss.com/), and [class-variance-authority](https://cva.style/).

## Quick Start

### In the monorepo (workspace)

```json
// package.json
{ "dependencies": { "@parasutcom/fds": "workspace:*" } }
```

```css
/* globals.css */
@import 'tailwindcss';
@import 'tw-animate-css';
@import '@parasutcom/fds/theme.css';

@source '../../../packages/shared/ui/src/**/*.{ts,tsx}';
@source '../**/*.{ts,tsx}';
```

```tsx
import { Button } from '@parasutcom/fds'

export default function Page() {
  return <Button variant="outline">Click me</Button>
}
```

### From npm (external project)

```bash
npm install @parasutcom/fds tailwindcss@^4 @tailwindcss/postcss lucide-react
```

```css
/* globals.css */
@import 'tailwindcss';
@import '@parasutcom/fds/styles.css';
```

```tsx
import { Button } from '@parasutcom/fds'
```

---

## Development

```bash
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 npm
```

The `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.tsx
```

Every component follows the same pattern:

```tsx
'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 `cva` for variants (not conditional classnames)
- Use `cn()` from `@fds/lib/utils` for merging classnames
- Use `@fds/` path alias for internal imports
- Accept `className` prop and merge it with `cn()`
- 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:

```tsx
'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`:

```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 showcase
```

**Demo file** (`demos/basic.tsx`):

```tsx
import { MyComponent } from '@parasutcom/fds'

export function BasicDemo() {
    return <MyComponent variant="outline">Hello</MyComponent>
}
```

**Page file** (`my-component-page.tsx`):

```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`:

```ts
{ title: 'MyComponent', href: '/components/my-component' },
```

Add to `src/docs/lib/routes.tsx`:

```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:

```tsx
import { Button, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@parasutcom/fds'
```

### Icons

FDS uses [Lucide React](https://lucide.dev/) for icons (peer dependency):

```tsx
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:

```tsx
<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:

```tsx
<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:

```tsx
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/fds
```

## CSS 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.