Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .claude/skills/tailwind-design-system/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
name: tailwind-design-system
description: Tailwind CSS design system for this mobile-first wallet app. Covers theme system (USD/BTC/dark), CSS variables, CVA components, shadcn/ui patterns, custom animations, and layout conventions. Use when creating or modifying UI components.
---

# Tailwind Design System — Project Conventions

**Stack**: Tailwind CSS 4.1 + `@tailwindcss/vite` + shadcn/ui + CVA + tailwind-merge + tw-animate-css + Radix UI + vaul
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to merge tailwind upgrade pr for this to be true


**References** (load when needed):
- [component-patterns.md](references/component-patterns.md) — CVA examples, MoneyDisplay, Dialog/Drawer/Toast animations, view transitions, page layout, available UI components

## Quick Reference

| What | How |
|------|-----|
| Class merging | `cn()` from `~/lib/utils` (clsx + tailwind-merge) |
| Component variants | CVA (`class-variance-authority`) |
| Semantic colors | CSS variables: `bg-primary`, `text-foreground`, `border-border` |
| Amount fonts | `font-numeric` (Teko) |
| Primary font | `font-primary` (Kode Mono) |
| Full viewport | `h-dvh` (dynamic viewport height) |
| Mobile container | `mx-auto w-full sm:max-w-sm` |
| Hide scrollbar | `scrollbar-none` (custom `@utility`) |
| Dark mode | Class-based (`.dark` on root) |
| Currency theme | `.usd` or `.btc` class on root |

## Configuration

**Tailwind v4 uses CSS-first configuration** — no `tailwind.config.ts`. All config lives in `app/tailwind.css`.

| File | Purpose |
|------|---------|
| `app/tailwind.css` | All Tailwind config: `@theme`, `@utility`, CSS variables, base styles |
| `vite.config.ts` | `@tailwindcss/vite` plugin (first in plugins array) |
| `app/lib/utils.ts` | `cn()` utility (clsx + tailwind-merge) |
| `app/features/theme/theme-provider.tsx` | Theme context and switching |
| `app/features/theme/colors.ts` | Theme colors in TypeScript (**must stay in sync with CSS**) |
| `app/components/ui/` | shadcn/ui base components |
| `app/components/page.tsx` | Page layout components |
| `app/components/money-display.tsx` | MoneyDisplay / MoneyInputDisplay |
| `components.json` | shadcn/ui config (no `config` path — v4 uses CSS) |

## v4 CSS-First Config Structure

The `app/tailwind.css` file structure:

```css
@import "tailwindcss";
@plugin "tailwindcss-animate";

/* 1. CSS Variables — outside @layer (v4 requirement) */
:root { --background: hsl(0 0% 100%); /* ... */ }
.usd { --background: hsl(178 100% 15%); /* ... */ }
.btc { --background: hsl(217 68% 35%); /* ... */ }
.dark { --background: hsl(0 0% 3.9%); /* ... */ }

/* 2. @theme inline — registers CSS vars as Tailwind color tokens */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... maps all CSS vars to Tailwind tokens */
}

/* 3. @theme — custom tokens (fonts, sizes, animations) */
@theme {
--font-numeric: "Teko", sans-serif;
--font-primary: "Kode Mono", monospace;
--font-size-2xs: 0.625rem;
--animate-shake: shake 0.2s ease-in-out;
--animate-slam: slam 0.4s ease-out both;
--animate-slide-out-up: slide-out-up 300ms ease-out forwards;
}

/* 4. @keyframes — top-level, referenced by @theme animations */
@keyframes shake { /* ... */ }

/* 5. @utility — custom utilities (replaces v3 plugins) */
@utility scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}

/* 6. @layer base — global styles */
@layer base {
* { @apply border-border; }
body { @apply bg-background text-foreground; }
button:not(:disabled), [role="button"]:not(:disabled) {
cursor: pointer; /* v4 removed default cursor:pointer */
}
}
```

**Key v4 differences from v3:**
- `@import "tailwindcss"` replaces three `@tailwind` directives
- CSS variables use `hsl()` wrapper (v3 used bare values like `0 0% 100%`)
- `@theme inline` registers CSS vars as Tailwind tokens (replaces `theme.extend.colors` in JS)
- `@theme` defines custom tokens (replaces `theme.extend.*` in JS config)
- `@utility` creates custom utilities (replaces `plugin({ addUtilities })`)
- `@plugin` loads plugins (replaces `require()` in JS config)
- No `postcss.config.js` needed — `@tailwindcss/vite` handles everything

## v4 Class Name Changes

Use the v4 names. The v3 names no longer exist:

| v3 (removed) | v4 (use this) |
|--------------|---------------|
| `shadow-sm` | `shadow-xs` |
| `rounded-sm` | `rounded-xs` |
| `outline-none` | `outline-hidden` |

Also: default `ring` width changed from `3px` to `1px` (use `ring-3` for old behavior).

## Theme System

Two independent axes, both cookie-persisted for SSR:
1. **Currency theme**: `.usd` (teal) or `.btc` (blue) on `<html>`
2. **Color mode**: `light` / `dark` / `system` — applies `.dark` class

Access via `useTheme()` from `app/features/theme/use-theme.tsx`.

Always use semantic color classes (`bg-primary`, `text-foreground`, `border-border`). Never hardcode colors like `bg-blue-500`.

**Keep `app/tailwind.css` and `app/features/theme/colors.ts` in sync** — the TypeScript file duplicates CSS variable values for programmatic access.

## Layout Conventions

- **Full viewport**: `h-dvh` (not `h-screen` — handles mobile browser chrome)
- **Safe viewport**: `h-[90svh]` for drawers/modals
- **Mobile container**: `w-full sm:max-w-sm` (full on mobile, 448px max on larger)
- **Centered layout**: `mx-auto sm:items-center`
- **Scrollable content**: `flex-1 overflow-y-auto scrollbar-none min-h-0`
- **Numpad**: `sm:hidden` (keyboard input on larger screens)
- **Mobile-first**: Base styles target mobile; `sm:` adjusts for larger screens

## Rules

| Do | Don't |
|----|-------|
| Semantic color classes (`bg-primary`) | Hardcoded colors (`bg-blue-500`) |
| `font-numeric` for all monetary amounts | Arbitrary font values |
| `h-dvh` for full-viewport layouts | `h-screen` (broken on mobile) |
| `cn()` for all className composition | Manual string concatenation |
| CVA for component variants | Inline conditional classes |
| Check `app/components/ui/` before creating new components | Duplicate existing shadcn components |
| Keep CSS vars and `colors.ts` in sync | Change one without the other |
| Add new keyframes/animations in `@theme`/`@keyframes` in CSS | Create animations outside `app/tailwind.css` |
| Use cookies for theme persistence | `localStorage` (breaks SSR) |
| `forwardRef` on components wrapping HTML elements | Skip ref forwarding |
152 changes: 152 additions & 0 deletions .claude/skills/tailwind-design-system/references/component-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Component Patterns

## CVA Component Pattern

All UI components use CVA for type-safe variants with `cn()` for className merging:

```typescript
// app/components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/lib/utils';

const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: { variant: 'default', size: 'default' },
},
);
```

**Button features**: `loading` prop with spinner, `asChild` via Radix `Slot`, `forwardRef`, `cn()` for overrides.

## MoneyDisplay Component

**Always use `MoneyDisplay` or `MoneyInputDisplay` for monetary amounts.**

File: `app/components/money-display.tsx`

```typescript
const valueVariants = cva('font-numeric', {
variants: {
size: {
xs: 'pt-0.5 text-xl',
sm: 'pt-1 text-2xl',
md: 'pt-1.5 text-5xl',
lg: 'pt-2 text-6xl',
},
},
});
```

Usage:
```tsx
<MoneyDisplay money={balance} size="lg" variant="default" />
<MoneyInputDisplay inputValue="1.5" currency="USD" unit="usd" />
```

## Available UI Components

Check `app/components/ui/` before creating new ones:

Badge, Button, Card (compound), Carousel, Checkbox, Dialog, Drawer (vaul), Dropdown Menu, Hover Card, Input, Label, Radio Group, Scroll Area, Select, Separator, Skeleton, Tabs, Toast, Toaster

All follow: CVA variants, `forwardRef`, `cn()`, Radix primitives.

## Compound Component Pattern (Card)

```typescript
const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-lg border bg-card text-card-foreground shadow-xs', className)}
{...props}
/>
),
);
// Plus: CardHeader, CardTitle, CardDescription, CardContent, CardFooter
```

## Dialog/Drawer/Toast Animations

**Dialog** uses `tailwindcss-animate` data-attribute animations:
```tsx
// Overlay
'data-[state=open]:animate-in data-[state=closed]:animate-out'
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0'

// Content
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95'
'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]'
'data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]'
```

**Drawer** (vaul):
```tsx
className="bg-gradient-to-b from-transparent via-black/70 to-black/80" // overlay
className="fixed inset-x-0 bottom-0 rounded-t-[10px]" // content
className="h-[90svh] font-primary sm:h-[75vh]" // responsive height
```

**Toast** exit uses custom animation:
```tsx
'data-[state=closed]:animate-slide-out-up'
```

## View Transitions

File: `app/lib/transitions/view-transition.tsx` + `transitions.css`

```tsx
<LinkWithViewTransition to="/page" transition="slideLeft" applyTo="oldView">
Go Forward
</LinkWithViewTransition>

const navigate = useNavigateWithViewTransition();
navigate('/page', { transition: 'slideRight', applyTo: 'bothViews' });
```

Types: `slideLeft`, `slideRight`, `slideUp`, `slideDown`, `fade`
Duration: 180ms (synced between CSS and TS — keep in sync!)
Apply modes: `newView`, `oldView`, `bothViews`

## Page Layout Components

File: `app/components/page.tsx`

```tsx
// Main page wrapper
<div className="mx-auto flex h-dvh w-full flex-col p-4 font-primary sm:items-center sm:px-6 lg:px-8">

// Content area
<div className="flex flex-grow flex-col gap-2 p-2 sm:w-full sm:max-w-sm">

// Footer
<div className="flex w-full flex-col items-center gap-2 p-2 sm:max-w-sm">

// Header — centered title with absolute positioning
<div className="-translate-x-1/2 absolute left-1/2 transform">
```

## Icons

Using **Lucide React**:
- Standard: `size-4` or `size-5`
- Large: `size-6`
- In buttons: `[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`
Loading