Skip to content

Vibecoder UI Guide

This guide is for vibecoders — contributors who want to improve the look and feel of Cosella’s UI without touching business logic, API calls, state management, or any functionality. If you follow this guide, you can safely make visual improvements, add new components, tweak styles, and enhance the user experience — all without breaking anything.

AreaSafe to ModifyExample
UI componentspackages/ui/src/Button styles, Card layouts, Modal animations
Design tokenspackages/tokens/src/theme.cssColors, spacing, typography, shadows
Component CSSpackages/tokens/src/components.css.cs-btn-*, .cs-panel, .cs-badge-*
Storybook storiespackages/ui/src/**/*.stories.tsxAdding stories for existing components
App page layoutsapps/*/src/features/*/components/Layout, spacing, alignment
IconsLucide React icons in componentsSwapping or adding icons
AreaWhyFiles to Avoid
API clientBusiness logic, data fetchingpackages/api-client/
Domain typesShared data contractspackages/domain/
Auth / PermissionsSecurity-criticalpackages/auth/, packages/permissions/
RealtimeWebSocket connectionspackages/realtime/
State managementZustand stores, React Query hooks**/hooks/, **/store.ts
Route definitionsNavigation logic**/route.ts, routeTree.gen.ts
Test files for logicDon’t modify existing tests*.test.ts (unless yours)
Build configTurborepo, Vite, TypeScriptturbo.json, vite.config.ts, tsconfig.json

If it imports from @cosella/domain, @cosella/auth, @cosella/realtime, or @cosella/api-client — don’t touch it.

If it only imports from @cosella/ui, @cosella/tokens, or uses Tailwind classes — you’re safe.

Terminal window
# Clone and install
git clone https://github.com/YOUR_USERNAME/cosella.git
cd cosella
pnpm install
# Verify everything works
pnpm typecheck
Terminal window
pnpm --filter @cosella/storybook dev

This opens the component library at http://localhost:6006. Browse all existing components here.

Pick one of these safe tasks:

  • Change a button color in packages/ui/src/button/Button.tsx
  • Adjust spacing in packages/ui/src/card/Card.tsx
  • Add a new badge variant in packages/ui/src/badge/Badge.tsx
  • Improve a Storybook story in any *.stories.tsx file
Terminal window
# Type check passes
pnpm typecheck
# Lint passes
pnpm lint
# Stories still render
pnpm --filter @cosella/storybook dev

Every component in packages/ui follows this structure:

packages/ui/src/
└── button/ # kebab-case directory name
├── Button.tsx # Component implementation
├── Button.stories.tsx # Storybook stories
└── Button.test.tsx # Vitest tests (optional for UI)

Here’s the pattern every component follows:

// 1. Imports — use cn() for class merging, cva for variants
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from '@radix-ui/react-slot';
import { cn } from '../lib/utils.js';
// 2. Variant definitions — CVA handles variant + default
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-lg font-medium',
{
variants: {
variant: {
primary: 'bg-primary text-white',
outline: 'border border-border bg-transparent',
},
size: {
sm: 'h-9 px-3 text-sm',
default: 'h-10 px-4 text-sm',
},
},
defaultVariants: {
variant: 'primary',
size: 'default',
},
},
);
// 3. Props interface — extends HTML attributes + CVA variants
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
// 4. Component — uses cn() for className merging
export function Button({
className,
variant,
size,
asChild = false,
children,
ref,
...props
}: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
>
{children}
</Comp>
);
}
  1. Always use cn() for className merging (never string concatenation)
  2. Always use cva() for variant-based styling
  3. Always accept className prop for consumer overrides
  4. Always export the props interface alongside the component
  5. Always use ref as prop (React 19 — no forwardRef)
  6. Always import from ../lib/utils.js for cn()

All colors, spacing, and typography are defined as CSS custom properties in packages/tokens/src/theme.css.

Use these Tailwind classes (they map to the CSS variables):

// Backgrounds
<div className="bg-bg-base" /> {/* Page background */}
<div className="bg-bg-raised" /> {/* Card / panel */}
<div className="bg-bg-subtle" /> {/* Muted surface */}
// Text
<p className="text-text-primary" /> {/* Main text */}
<p className="text-text-secondary" /> {/* Secondary text */}
<p className="text-text-muted" /> {/* Muted text */}
// Brand
<Button className="bg-primary" /> {/* Primary brand color */}
<div className="border-border" /> {/* Standard border */}
// Status
<Alert className="bg-success/10 border-success/25 text-success" />
<Alert className="bg-warning/10 border-warning/25 text-warning" />
<Alert className="bg-danger/10 border-danger/25 text-danger" />
<Alert className="bg-info/10 border-info/25 text-info" />
<h1 className="text-2xl font-bold text-text-primary" /> {/* Page title */}
<h2 className="text-lg font-bold text-text-primary" /> {/* Section title */}
<h3 className="text-base font-bold text-text-primary" /> {/* Card title */}
<p className="text-sm text-text-muted" /> {/* Body text */}
<label className="text-sm font-medium text-text-primary" /> {/* Label */}
{/* Standard page layout */}
<div className="cs-page-shell">
<div className="cs-page-stack">
<div className="cs-page-header">...</div>
<div className="cs-panel">...</div>
</div>
</div>
{/* Card with header, content, footer */}
<Card>
<CardHeader><CardTitle>Title</CardTitle></CardHeader>
<CardContent>Content</CardContent>
<CardFooter>Actions</CardFooter>
</Card>
Terminal window
pnpm gen component

Enter a PascalCase name (e.g., ProgressBar). This creates:

  • packages/ui/src/progress-bar/ProgressBar.tsx
  • packages/ui/src/progress-bar/ProgressBar.test.tsx
  • Export added to packages/ui/src/index.ts

Replace the template code with your implementation:

import { cn } from '../lib/utils.js';
export interface ProgressBarProps extends React.HTMLAttributes<HTMLDivElement> {
value: number;
max?: number;
variant?: 'primary' | 'success' | 'warning' | 'danger';
}
export function ProgressBar({
className,
value,
max = 100,
variant = 'primary',
...props
}: ProgressBarProps) {
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
const variantStyles = {
primary: 'bg-primary',
success: 'bg-success',
warning: 'bg-warning',
danger: 'bg-danger',
};
return (
<div
className={cn('h-2 w-full rounded-full bg-bg-subtle', className)}
role="progressbar"
aria-valuenow={value}
aria-valuemin={0}
aria-valuemax={max}
{...props}
>
<div
className={cn('h-full rounded-full transition-all', variantStyles[variant])}
style={{ width: `${percentage}%` }}
/>
</div>
);
}

Create ProgressBar.stories.tsx:

import type { Meta, StoryObj } from '@storybook/react';
import { ProgressBar } from './ProgressBar.js';
const meta = {
title: 'Primitives/ProgressBar',
component: ProgressBar,
tags: ['autodocs'],
argTypes: {
value: { control: { type: 'range', min: 0, max: 100 } },
variant: { control: 'select', options: ['primary', 'success', 'warning', 'danger'] },
},
} satisfies Meta<typeof ProgressBar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { args: { value: 60 } };
export const Complete: Story = { args: { value: 100, variant: 'success' } };
export const Warning: Story = { args: { value: 30, variant: 'warning' } };
export const Danger: Story = { args: { value: 10, variant: 'danger' } };
Terminal window
# Storybook renders your new component
pnpm --filter @cosella/storybook dev
# Type check passes
pnpm typecheck
# Lint passes
pnpm lint

File: packages/ui/src/button/Button.tsx

// Before
primary: 'bg-primary text-white hover:opacity-90',
// After — use a different token
primary: 'bg-brand-500 text-white hover:bg-brand-600',

File: packages/ui/src/card/Card.tsx

// Before
<div className={cn('px-5 py-4', className)} {...props} />
// After — more padding
<div className={cn('px-6 py-5', className)} {...props} />

File: packages/ui/src/badge/Badge.tsx

Add to the variant styles:

const badgeVariants = cva('...', {
variants: {
variant: {
// ... existing variants
teal: 'bg-badge-teal-bg text-badge-teal-text', // NEW
},
},
});

Then add the token in packages/tokens/src/theme.css:

--color-badge-teal-bg: oklch(97% 0.04 170);
--color-badge-teal-text: oklch(44% 0.13 170);

File: Any *.stories.tsx

Add interaction tests:

export const InteractionClick: Story = {
args: { children: 'Click me', onClick: fn() },
play: async ({ args, canvasElement, step }) => {
const canvas = within(canvasElement);
const btn = canvas.getByRole('button', { name: /click me/i });
await step('button is clickable', async () => {
await userEvent.click(btn);
await expect(args.onClick).toHaveBeenCalledTimes(1);
});
},
};

File: packages/ui/src/skeleton/Skeleton.tsx

Add a new skeleton variant:

export function SkeletonChart({ className }: SkeletonProps) {
return (
<div className={cn('h-48 w-full rounded-lg bg-bg-subtle animate-pulse', className)} />
);
}

Before submitting your PR, run these commands:

Terminal window
# 1. Type check — must pass
pnpm typecheck
# 2. Lint — must pass
pnpm lint
# 3. Storybook — your component renders correctly
pnpm --filter @cosella/storybook dev
# 4. Visual check — compare with existing components
# Open Storybook and check your component looks consistent

Follow Conventional Commits:

Terminal window
# Good
git commit -m "feat(ui): add ProgressBar component"
git commit -m "style(ui): adjust Button hover states"
git commit -m "docs(storybook): add interaction tests for Alert"
# Bad
git commit -m "update button"
git commit -m "fixed styles"
  1. Check Storybookpnpm --filter @cosella/storybook dev
  2. Check existing componentspackages/ui/src/ for patterns
  3. Check design tokenspackages/tokens/src/theme.css for available colors
  4. Check the cn() utilitypackages/ui/src/lib/utils.ts
  • Don’t import from @cosella/domain, @cosella/auth, @cosella/realtime, or @cosella/api-client
  • Don’t modify route definitions or routeTree.gen.ts
  • Don’t change Zustand stores or React Query hooks
  • Don’t modify existing test files (.test.ts) unless you wrote them
  • Don’t edit turbo.json, vite.config.ts, or tsconfig.json
  • Don’t use as casts or any types
  • Don’t use forwardRef — React 19 uses ref as prop
WhatWhereSafe?
Component stylespackages/ui/src/*/Yes
Design tokenspackages/tokens/src/theme.cssYes
Storybook storiespackages/ui/src/**/*.stories.tsxYes
App page layoutsapps/*/src/features/*/components/Yes
IconsLucide ReactYes
API callspackages/api-client/No
Domain typespackages/domain/No
Auth logicpackages/auth/No
State storesZustand/React QueryNo
Route definitionsTanStack Router filesNo