npm package: @playground-ds/design-system
Description: Design Team Playground — shadcn/ui components with Tailwind CSS v4 (Trading Game design system)
Latest version: 1.1.0
Deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

# @playground-ds/design-system

A React component library and design token system for the Deriv trading game product. Built on [shadcn/ui](https://ui.shadcn.com/) (New York style) with a custom Figma-driven token architecture, Tailwind CSS v4, and React 19.

---

## Table of contents

1. [Installation](#installation)
2. [Setup](#setup)
3. [Template build process](#template-build-process)
4. [Phase 1 · Design preparation](#phase-1--design-preparation)
5. [Phase 2 · Component usage rules](#phase-2--component-usage-rules)
6. [Phase 3 · Styling rules](#phase-3--styling-rules)
7. [Phase 4 · Asset and icon sourcing](#phase-4--asset-and-icon-sourcing)
8. [Phase 5 · Pre-build checklist and linting](#phase-5--pre-build-checklist-and-linting)
9. [Phase 6 · Review and approval](#phase-6--review-and-approval)
10. [Forbidden patterns](#forbidden-patterns)
11. [Design tokens](#design-tokens)
12. [Components](#components)
13. [Template design guide](#template-design-guide)
14. [Default starter template](#default-starter-template) (within §13)
15. [Development](#development)
16. [Cursor instructions](#cursor-instructions)

---

## Installation

```bash
npm install @playground-ds/design-system --save-exact
```

### Peer dependencies

```bash
npm install react@19 react-dom@19 tailwindcss@4
```

---

## Setup

### 1. Import styles

In your global CSS file (e.g. `app/globals.css`):

```css
@import "@playground-ds/design-system/styles";
@import "tailwindcss";
@source "../node_modules/@playground-ds/design-system/dist";
```

> **Why `@source`?** Tailwind v4 skips `node_modules` by default. Without this line Tailwind won't generate utility classes for component class names like `bg-primary` or `text-on-prominent`.

### 2. Apply the dark theme

The design system is dark-first. Add the `dark` class to your app root:

```tsx
// app/layout.tsx (Next.js) or index.tsx (Vite)
export default function RootLayout({ children }) {
  return (
    <html lang="en" className="dark">
      <body>{children}</body>
    </html>
  )
}
```

### 3. Use components

```tsx
import { Button, Card, Badge } from "@playground-ds/design-system"

export default function App() {
  return (
    <Card>
      <Button variant="primary">Trade Now</Button>
      <Badge variant="fill-purple">Live</Badge>
    </Card>
  )
}
```

---

## Template build process

Every new layout template must follow this process from start to merge. Steps are mandatory — PRs that skip any phase will be rejected.

```
Start: new template task
        │
        ▼
Phase 1 · Design preparation
  Review Figma spec · map every UI element to a component
        │
        ▼
All components available in the package?
   ├── No  → Raise Component Request → wait for approval → loop back
   └── Yes ↓
        │
        ▼
Phase 2 · Component usage
  Import from @playground-ds/design-system only
  Use approved variants · wrap to extend, never fork
        │
        ▼
Phase 3 · Styling
  All values via Tailwind token utilities · no hardcoded values
  No inline styles · no arbitrary Tailwind values
        │
        ▼
Phase 4 · Asset and icon sourcing
  Icons from Lucide via package · images from @/assets/
  No external URLs · SVGs use currentColor · no custom font loading
        │
        ▼
Phase 5 · Pre-build checklist and linting
  npm run lint · npm run type-check · npm run design-lint · npm run build
        │
        ▼
Zero errors?
   ├── No  → Fix errors → re-run → loop back
   └── Yes ↓
        │
        ▼
Phase 6 · Review and approval
  Open PR · 3 approvals required
        │
        ▼
All 3 approved?
   ├── No  → Address comments → loop back
   └── Yes ↓
        │
        ▼
Merged + changelog updated → ships in next release
```

---

## Phase 1 · Design preparation

Complete all of the following **before writing any code**.

**Steps:**

1. Open the Figma file linked in the task ticket
2. List every distinct UI element in the design (buttons, cards, inputs, tables, etc.)
3. Find the matching component for each element in the package
4. If a component is missing, open a **Component Request** issue and stop — do not build a one-off replacement

**Component mapping reference:**

| Design element | Import from `@playground-ds/design-system` |
|---|---|
| Button, icon button | `Button`, `buttonVariants` |
| Text input | `Input` |
| Textarea | `Textarea` |
| Select / dropdown | `Select`, `SelectTrigger`, `SelectContent`, `SelectItem` |
| Modal / dialog | `Dialog`, `DialogTrigger`, `DialogContent` |
| Toast / notification | `Toaster` |
| Table | `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell` |
| Card | `Card`, `CardHeader`, `CardTitle`, `CardContent`, `CardFooter` |
| Tabs | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` |
| Badge / tag | `Badge`, `badgeVariants` |
| Checkbox | `Checkbox` |
| Radio group | `RadioGroup`, `RadioGroupItem` |
| Navigation | `NavigationMenu`, `NavigationMenuList`, `NavigationMenuTrigger` |
| Sidebar | `Sidebar`, `SidebarProvider`, `SidebarMenu` |
| Chart | `ChartContainer`, `ChartTooltip`, `ChartLegend` |
| Avatar | `Avatar`, `AvatarImage`, `AvatarFallback` |
| Progress bar | `Progress` |
| Loading | `Spinner`, `Skeleton` |
| Tooltip | `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider` |
| Popover | `Popover`, `PopoverTrigger`, `PopoverContent` |

> If no match exists → open a Component Request and **do not proceed** until it is resolved.

---

## Phase 2 · Component usage rules

### Import rule

Always import from the package. Never import from shadcn, Radix, or Lucide directly.

```tsx
// ✅ correct
import { Button } from "@playground-ds/design-system"
import { Card, CardHeader, CardContent } from "@playground-ds/design-system"
import { Input } from "@playground-ds/design-system"

// ❌ wrong
import { Button } from "shadcn/ui"
import { Button } from "@radix-ui/react-button"
import { ChevronRight } from "lucide-react"
```

### Composition rule

Wrap to extend — never edit the package source or fork a component.

```tsx
// ✅ correct — wrap the component
const DangerButton = (props: ButtonProps) => (
  <Button variant="destructive" {...props} />
)

// ❌ wrong — forking or modifying the package internals
```

### Variant rule

Use only the variants the component exposes. If a required variant is missing, raise a Component Request.

```tsx
// ✅ correct
<Button variant="primary">Trade Now</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="outline">View</Button>
<Button variant="ghost">Settings</Button>
<Button variant="destructive">Delete</Button>

// ❌ wrong — simulating variants with inline styles
<Button style={{ background: "#FF6600" }}>Custom</Button>
```

### Outline button colour rule

`variant="outline"` is the **Cyan — Secondary** button style. It always renders with cyan text and a cyan border using the `--primary` token (`#00FFFF`).

**The colour must never change — not for any reason.** This means:
- Do not override it because the action is destructive
- Do not override it because the surrounding context shows an error or loss state
- Do not override it to match `text-semantic-loss`, `text-destructive`, or any other semantic colour
- Do not add any `className` or `style` colour prop to an outline button

The outline button is a neutral secondary action trigger. Its appearance is always cyan regardless of what it does.

```tsx
// ✅ correct — always cyan, no matter the action
<Button variant="outline">Cancel</Button>
<Button variant="outline">Discard</Button>
<Button variant="outline">Close</Button>
<Button variant="outline">Remove</Button>   // still cyan even if action deletes something

// ❌ wrong — overriding to red because the action feels destructive
<Button variant="outline" className="text-destructive border-destructive">Remove</Button>

// ❌ wrong — overriding because the context shows a loss state
<Button variant="outline" className="text-semantic-loss">Close</Button>

// ❌ wrong — hardcoded red
<Button variant="outline" className="text-[#FF3355]">Cancel</Button>
<Button variant="outline" style={{ color: "red" }}>Cancel</Button>

// ❌ wrong — inferring button colour from surrounding error/loss context
// Even inside an error alert or loss summary, the outline button stays cyan:
<Alert variant="error">
  <Button variant="outline" className="text-destructive">Dismiss</Button>  {/* ❌ */}
  <Button variant="outline">Dismiss</Button>                               {/* ✅ */}
</Alert>
```

> If you need a button that is itself red (e.g. a standalone "Delete account" CTA), use `variant="destructive"` — that is a separate variant with its own distinct appearance. Never use `variant="outline"` with colour overrides as a substitute.

### Two-button pairing rule

Whenever two buttons appear together — in a `CardFooter`, `DialogFooter`, `DrawerFooter`, `AlertDialogFooter`, or any inline button group — the **right (confirm / primary action) button must always be `variant="primary"`** and the **left (cancel / secondary action) button must be `variant="secondary"` or `variant="outline"`**. This rule applies to every component and layout in the system without exception.

```tsx
// ✅ correct — primary action on the right
<CardFooter className="flex justify-end gap-3">
  <Button variant="outline">Cancel</Button>
  <Button variant="primary">Confirm</Button>
</CardFooter>

// ✅ correct — dialog footer
<DialogFooter>
  <Button variant="secondary">Discard</Button>
  <Button variant="primary">Save changes</Button>
</DialogFooter>

// ✅ correct — alert dialog footer
<AlertDialogFooter>
  <AlertDialogCancel asChild>
    <Button variant="outline">Cancel</Button>
  </AlertDialogCancel>
  <AlertDialogAction asChild>
    <Button variant="primary">Confirm</Button>
  </AlertDialogAction>
</AlertDialogFooter>

// ❌ wrong — primary on the left
<CardFooter className="flex gap-3">
  <Button variant="primary">Confirm</Button>
  <Button variant="outline">Cancel</Button>
</CardFooter>

// ❌ wrong — two primary buttons
<CardFooter className="flex gap-3">
  <Button variant="primary">Cancel</Button>
  <Button variant="primary">Confirm</Button>
</CardFooter>

// ❌ wrong — secondary variant used for the primary action
<DialogFooter>
  <Button variant="secondary">Save changes</Button>
  <Button variant="outline">Discard</Button>
</DialogFooter>
```

### Button sizes

```tsx
<Button size="lg">Large — 48px</Button>
<Button size="md">Medium — 40px</Button>
<Button size="sm">Small — 32px</Button>
<Button size="xs">XSmall — 24px</Button>

<Button size="icon-lg"><Icon /></Button>   // 48×48px
<Button size="icon-md"><Icon /></Button>   // 40×40px
<Button size="icon-sm"><Icon /></Button>   // 28×28px
<Button size="icon-xs"><Icon /></Button>   // 24×24px
```

### Badge variants and sizes

```tsx
<Badge>Default</Badge>                          // cyan filled
<Badge variant="fill">Cyan tint</Badge>
<Badge variant="fill-amber">Amber tint</Badge>
<Badge variant="fill-pink">Pink tint</Badge>
<Badge variant="fill-purple">Rank #1</Badge>    // violet — rank/badge accent
<Badge variant="ghost">Ghost</Badge>

<Badge size="default">20px</Badge>
<Badge size="md">32px</Badge>
<Badge size="lg">40px</Badge>
```

---

## Phase 3 · Styling rules

### The rule

> **Every colour, spacing, typography, radius, and shadow value must use a Tailwind utility backed by a design token. No hardcoded values. No exceptions.**

### Card border

Always apply `border border-border` to `<Card>` elements. Never use `border-white`, hardcoded border colours, or omit the border entirely when a border is visible in the Figma spec. The `--border` token (`#0F2830`) is the only permitted card border colour.

```tsx
// ✅ correct — token border
<Card className="border border-border bg-card rounded-lg p-4">
  …
</Card>

// ❌ wrong — white border
<Card className="border border-white bg-card rounded-lg p-4">

// ❌ wrong — hardcoded colour
<Card className="border border-[#FFFFFF] bg-card rounded-lg p-4">

// ❌ wrong — inline border style
<Card style={{ border: "1px solid white" }}>
```

### Use token-based Tailwind utilities

```tsx
// ✅ correct — Tailwind utilities backed by tokens
<div className="bg-background text-on-prominent rounded-lg p-4 border border-border">
  <p className="text-on-subtle body-md">Description</p>
  <span className="text-semantic-win">+$1,250.00</span>
  <span className="text-semantic-loss">-$340.00</span>
  <span className="text-semantic-warning">Margin call</span>
</div>

// ❌ wrong — hardcoded values
<div style={{ background: "#020B0A", color: "#D5EEE8", padding: "16px" }}>
<div className="bg-[#020B0A] text-[#D5EEE8] p-[16px]">
```

### Use typography utility classes

```tsx
// ✅ correct — Orbitron headings
<h1 className="heading-h1">LEADERBOARD</h1>
<h2 className="heading-h2">TOP TRADERS</h2>
<h3 className="heading-h3">WEEKLY RESULTS</h3>
<h4 className="heading-h4">SEASON STATS</h4>
<h5 className="heading-xs">ROUND 1</h5>

// ✅ correct — Barlow body
<p className="body-lg">Primary body copy</p>
<p className="body-md">Standard body copy</p>
<span className="body-sm">Caption text</span>
<span className="body-xs">Micro label</span>

// ❌ wrong — re-declaring font properties manually
<h1 style={{ fontFamily: "Orbitron", fontWeight: 600, fontSize: "72px", textTransform: "uppercase" }}>
```

### No inline styles

```tsx
// ✅ correct
<p className="text-on-subtle body-md">Description</p>

// ❌ wrong
<p style={{ color: "#7AA8AE", fontSize: "16px", fontWeight: 600 }}>Description</p>
```

### No Tailwind arbitrary values

```tsx
// ✅ correct — scale values only
<div className="p-4 gap-6 rounded-lg mt-8">

// ❌ wrong
<div className="p-[18px] gap-[26px] rounded-[10px] mt-[34px]">
```

### Colour token quick reference

| Tailwind class | Token | Hex | Usage |
|---|---|---|---|
| `bg-background` | `--background` | `#020B0A` | Page background |
| `text-on-prominent` | `--on-prominent` | `#D5EEE8` | Primary text |
| `text-on-subtle` | `--on-subtle` | `#7AA8AE` | Paragraphs, labels |
| `text-on-muted` | `--on-muted` | `#295B60` | Muted headers |
| `bg-card` | `--card` | `#06000F` | Card backgrounds |
| `bg-popover` | `--popover` | `#0A1A1E` | Elevated surfaces |
| `bg-primary` / `text-primary` | `--primary` | `#00FFFF` | Brand cyan — CTAs |
| `bg-primary-hover` | `--primary-hover` | `#00D4D4` | Cyan hover |
| `text-primary-foreground` | `--primary-foreground` | `#020B0A` | Text on cyan buttons |
| `bg-brand-accent` | `--brand-accent` | `#FF6600` | Accent orange |
| `bg-badge-rank` | `--badge-rank` | `#A040FF` | Rank/badge violet |
| `text-semantic-win` | `--semantic-win` | `#00FF9F` | Profit / positive |
| `text-semantic-loss` | `--semantic-loss` | `#FF3355` | Loss / negative |
| `text-semantic-warning` | `--semantic-warning` | `#FF8C00` | Warning |
| `bg-destructive` | `--destructive` | `#FF3355` | Errors |
| `border-border` | `--border` | `#0F2830` | Borders / dividers |
| `bg-hover` | `--hover` | cyan 12% | Hover tint |
| `bg-overlay` | `--overlay` | rgba black 50% | Modal scrim |

---

## Phase 4 · Asset and icon sourcing

### Icons

Use Lucide icons via the package's shadcn config — do not import Lucide directly.

```tsx
// ✅ correct — use Lucide via shadcn component wrappers in the package
import { ChevronRight } from "@playground-ds/design-system"

// ❌ wrong — direct Lucide import
import { ChevronRight } from "lucide-react"
```

### Images

Import from your local `@/assets/` folder. Never hardcode external URLs in source.

```tsx
// ✅ correct
import heroImage from "@/assets/images/hero-banner.png"
<img src={heroImage} alt="Hero banner" />

// ❌ wrong
<img src="https://cdn.example.com/image.png" alt="..." />
```

### SVGs

Always use `currentColor` for fill and stroke so the icon inherits its parent's token colour.

```tsx
// ✅ correct
<svg fill="currentColor" stroke="currentColor">

// ❌ wrong
<svg fill="#00FFFF">
```

### Fonts

Fonts (Barlow + Orbitron) are loaded automatically when you import the package styles. Do not add any `@import` or `<link>` for fonts.

```css
/* ❌ wrong — already loaded by the package */
@import url('https://fonts.googleapis.com/css2?family=Barlow...');
```

---

## Phase 5 · Pre-build checklist and linting

### Commands — run all four in order

```bash
# 1. ESLint — code quality and import violations
npm run lint

# 2. TypeScript — type errors
npm run type-check

# 3. Design token compliance — catches hardcoded values
npm run design-lint

# 4. Full build verification
npm run build
```

All four must exit with **zero errors** before opening a PR. Warnings must be resolved or explicitly documented in the PR description.

### Self-review checklist

Copy this into your PR description and check every item before requesting review.

```markdown
## Self-review checklist

### Components
- [ ] All components imported from `@playground-ds/design-system`
- [ ] No direct shadcn, Radix, or Lucide imports
- [ ] No third-party component libraries introduced
- [ ] No package source files edited or forked
- [ ] Two-button groups: right button is `variant="primary"`, left is `variant="secondary"` or `variant="outline"`

### Styling
- [ ] Zero hardcoded hex / rgb / oklch colour values
- [ ] Zero hardcoded px / rem spacing values
- [ ] Zero inline styles (`style={{}}`)
- [ ] Zero Tailwind arbitrary values (`[...]`)
- [ ] Typography uses heading-h1…heading-xs and body-lg…body-xs classes
- [ ] Trading states use text-semantic-win / text-semantic-loss / text-semantic-warning
- [ ] Cards use `border border-border` — never `border-white` or hardcoded border colours
- [ ] Outline buttons have no colour overrides — text and border must remain cyan (`--primary`)

### Assets
- [ ] No external image URLs in source
- [ ] All SVGs use `currentColor`
- [ ] No custom font loading

### Quality
- [ ] `npm run lint` → 0 errors
- [ ] `npm run type-check` → 0 errors
- [ ] `npm run design-lint` → 0 errors
- [ ] `npm run build` → successful
- [ ] Tested at mobile (`sm`), tablet (`md`), and desktop (`lg`/`xl`) breakpoints
- [ ] Dark theme verified (`html.dark`)
- [ ] No console errors in browser
```

---

## Phase 6 · Review and approval

### Opening a PR

- Use the **Template PR** template (auto-populated on PR creation)
- Title format: `feat(template): [template-name] — brief description`
- Link the Figma file and task ticket in the PR body
- Attach screenshots at mobile and desktop breakpoints
- Paste the completed self-review checklist

### Required approvals

All three approvals are required before merge. No exceptions.

| Approver | Checks |
|---|---|
| **Design Lead** | Fidelity to Figma spec, visual consistency, spacing, typography scale |
| **Senior Developer** | Code quality, correct component imports, performance |
| **Design System Owner** | Token compliance, semantic colour usage, no hardcoded values |

### Review SLA

| Stage | Turnaround |
|---|---|
| Design review | 2 business days |
| Code review | 2 business days |
| Token compliance | 1 business day |
| Addressing comments | 2 business days |

### Rejection criteria

A PR will be blocked from merging if any of the following are true:

- Hardcoded colour, spacing, or typography values present
- Components imported from outside the package
- External asset URLs in source code
- Inline styles or Tailwind arbitrary values present
- Typography not using heading/body utility classes
- Semantic colours not used for win/loss/warning states
- Card borders using `border-white` or any colour other than `border-border`
- Two-button groups where the primary action is not `variant="primary"` on the right
- Outline buttons with colour overrides (text or border) — must stay cyan (`--primary`)
- Lint, type-check, or design-lint errors present
- Self-review checklist incomplete
- Any one of the three required approvals is missing

### After all approvals

1. Squash and merge into `main`
2. Update `CHANGELOG.md` with the template name and PR link
3. Template ships in the next package release

---

## Forbidden patterns

```tsx
// ❌ hardcoded colour
style={{ color: "#D5EEE8" }}
className="text-[#7AA8AE]"
style={{ background: "oklch(0.138 0.018 188.4)" }}

// ❌ hardcoded spacing or size
style={{ padding: "16px", margin: "0 24px" }}
className="mt-[18px] p-[24px]"

// ❌ inline style
style={{ fontSize: "16px", fontWeight: 600 }}

// ❌ wrong import source
import { Button } from "shadcn/ui"
import { ChevronRight } from "lucide-react"
import { Button } from "@radix-ui/react-button"

// ❌ re-declaring typography manually
style={{ fontFamily: "Orbitron", fontWeight: 600, fontSize: "72px" }}

// ❌ Tailwind arbitrary values
className="rounded-[10px] text-[14px] w-[347px]"

// ❌ wrong semantic colour usage
<span style={{ color: "#00FF9F" }}>+12%</span>   // → use className="text-semantic-win"

// ❌ wrong card border colour
<Card className="border border-white …">         // → use border-border
<Card style={{ border: "1px solid white" }}>     // → use className="border border-border"

// ❌ wrong outline button colour — variant="outline" is always cyan, no exceptions
<Button variant="outline" className="text-destructive border-destructive">Remove</Button>
<Button variant="outline" className="text-semantic-loss">Close</Button>   // never override for loss/error context
<Button variant="outline" className="text-[#FF3355]">Cancel</Button>
<Button variant="outline" style={{ color: "red" }}>Cancel</Button>

// ❌ wrong two-button order or variant
<CardFooter>
  <Button variant="primary">Confirm</Button>     // primary must be on the RIGHT
  <Button variant="outline">Cancel</Button>
</CardFooter>

<CardFooter>
  <Button variant="primary">Cancel</Button>      // cancel must never be primary
  <Button variant="primary">Confirm</Button>     // only one primary allowed
</CardFooter>

// ❌ custom font loading (already loaded by the package)
@import url('https://fonts.googleapis.com/...')
```

---

## Design tokens

All tokens are CSS custom properties available after importing the package styles. In Tailwind v4 they are also available as utility classes.

### Colour tokens

| CSS variable | Tailwind utility | Hex | Usage |
|---|---|---|---|
| `--background` | `bg-background` | `#020B0A` | Page background |
| `--on-prominent` | `text-on-prominent` | `#D5EEE8` | Primary text |
| `--on-subtle` | `text-on-subtle` | `#7AA8AE` | Paragraphs, labels, descriptions |
| `--on-muted` | `text-on-muted` | `#295B60` | Muted headers, labels |
| `--card` | `bg-card` | `#06000F` | Card and panel backgrounds |
| `--popover` | `bg-popover` | `#0A1A1E` | Elevated surfaces, dropdowns |
| `--primary` | `bg-primary` / `text-primary` | `#00FFFF` | Brand cyan — primary CTAs |
| `--primary-hover` | `bg-primary-hover` | `#00D4D4` | Hover state for primary |
| `--primary-foreground` | `text-primary-foreground` | `#020B0A` | Text on primary buttons |
| `--secondary` | `bg-secondary` | `#FFB800` @ 4% | Amber tint background |
| `--secondary-hover` | `bg-secondary-hover` | `#FFB800` | Full amber on hover |
| `--brand-accent` | `bg-brand-accent` | `#FF6600` | Accent orange |
| `--brand-accent-hover` | `bg-brand-accent-hover` | `#D45200` | Accent orange hover |
| `--badge-rank` | `bg-badge-rank` | `#A040FF` | Violet — rank/badge accent |
| `--semantic-win` | `text-semantic-win` / `bg-semantic-win` | `#00FF9F` | Profit / positive |
| `--semantic-loss` | `text-semantic-loss` / `bg-semantic-loss` | `#FF3355` | Loss / negative |
| `--semantic-warning` | `text-semantic-warning` | `#FF8C00` | Warning state |
| `--destructive` | `bg-destructive` | `#FF3355` | Errors, destructive actions |
| `--border` | `border-border` | `#0F2830` | Default borders and dividers |
| `--hover` | `bg-hover` | cyan 12% | Hover background tint |
| `--overlay` | `bg-overlay` | rgba(0,0,0,0.5) | Modal scrim |

### Opacity colour tokens

| CSS variable | Value | Usage |
|---|---|---|
| `--color-opacity-cyan-4%` | cyan @ 4% | Subtle cyan tint backgrounds |
| `--color-opacity-cyan-40%` | cyan @ 40% | Medium cyan tint |
| `--color-opacity-amber-4%` | amber @ 4% | Subtle amber tint backgrounds |
| `--color-opacity-amber-30%` | amber @ 30% | Medium amber tint |

### Alert tokens

| Group | Variables |
|---|---|
| Info (blue `#53B9FF`) | `--alert-info-bg`, `--alert-info-text`, `--alert-info-border` |
| Error (red `#FF4D4D`) | `--alert-error-bg`, `--alert-error-text`, `--alert-error-border` |

### Typography

Fonts are loaded automatically. Do not import them yourself.

| CSS variable | Font | Role |
|---|---|---|
| `--font-barlow` / `--font-sans` / `--font-body` | Barlow | Body text, UI labels |
| `--font-orbitron` / `--font-display` | Orbitron | Headings, display text |

**Heading utility classes** — Orbitron, weight 600, letter-spacing 1.5px, uppercase

| Class | Size | Line height |
|---|---|---|
| `heading-h1` | 72px | 72px |
| `heading-h2` | 64px | 64px |
| `heading-h3` | 48px | 48px |
| `heading-h4` | 40px | 40px |
| `heading-xs` | 24px | 24px |

**Body utility classes** — Barlow, weight 600

| Class | Size | Line height |
|---|---|---|
| `body-lg` | 18px | 28px |
| `body-md` | 16px | 24px |
| `body-sm` | 12px | 16px |
| `body-xs` | 8px | 12px |

### Border radius

| Tailwind | Value |
|---|---|
| `rounded-2xs` | 2px |
| `rounded-xs` | 4px |
| `rounded-sm` | 6px |
| `rounded-md` | 8px |
| `rounded-lg` | 10px |
| `rounded-xl` | 14px |
| `rounded-2xl` | 18px |
| `rounded-3xl` | 22px |
| `rounded-4xl` | 26px |

### Breakpoints

| Name | Value |
|---|---|
| `sm` | 640px |
| `md` | 768px |
| `lg` | 1024px |
| `xl` | 1280px |
| `2xl` | 1536px |

---

## Components

### Full component list

| Component | Named exports |
|---|---|
| Accordion | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` |
| Alert | `Alert`, `AlertTitle`, `AlertDescription` |
| Alert Dialog | `AlertDialog`, `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogFooter`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogAction`, `AlertDialogCancel` |
| Avatar | `Avatar`, `AvatarImage`, `AvatarFallback`, `AvatarBadge`, `AvatarGroup` |
| Badge | `Badge`, `badgeVariants` |
| Breadcrumb | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator` |
| Button | `Button`, `buttonVariants` |
| Calendar | `Calendar`, `CalendarDayButton` |
| Card | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` |
| Carousel | `Carousel`, `CarouselContent`, `CarouselItem`, `CarouselPrevious`, `CarouselNext` |
| Chart | `ChartContainer`, `ChartTooltip`, `ChartLegend`, `ChartStyle` |
| Checkbox | `Checkbox` |
| Collapsible | `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` |
| Combobox | `Combobox`, `ComboboxInput`, `ComboboxContent`, `ComboboxItem`, `ComboboxEmpty` |
| Command | `Command`, `CommandDialog`, `CommandInput`, `CommandList`, `CommandEmpty`, `CommandGroup`, `CommandItem` |
| Context Menu | `ContextMenu`, `ContextMenuTrigger`, `ContextMenuContent`, `ContextMenuItem` |
| Dialog | `Dialog`, `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogFooter` |
| Drawer | `Drawer`, `DrawerTrigger`, `DrawerContent`, `DrawerHeader`, `DrawerTitle`, `DrawerDescription`, `DrawerFooter` |
| Dropdown Menu | `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuItem`, `DropdownMenuSeparator`, `DropdownMenuLabel` |
| Empty State | `Empty`, `EmptyHeader`, `EmptyTitle`, `EmptyDescription`, `EmptyContent` |
| Field | `Field`, `FieldLabel`, `FieldDescription`, `FieldError`, `FieldGroup` |
| Form | `Form`, `FormItem`, `FormLabel`, `FormControl`, `FormField`, `FormMessage` |
| Hover Card | `HoverCard`, `HoverCardTrigger`, `HoverCardContent` |
| Input | `Input` |
| Input Group | `InputGroup`, `InputGroupAddon`, `InputGroupButton`, `InputGroupText` |
| Input OTP | `InputOTP`, `InputOTPGroup`, `InputOTPSlot`, `InputOTPSeparator` |
| Item | `Item`, `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription` |
| Kbd | `Kbd`, `KbdGroup` |
| Label | `Label` |
| Menubar | `Menubar`, `MenubarMenu`, `MenubarTrigger`, `MenubarContent`, `MenubarItem` |
| Native Select | `NativeSelect`, `NativeSelectOptGroup`, `NativeSelectOption` |
| Navigation Menu | `NavigationMenu`, `NavigationMenuList`, `NavigationMenuTrigger`, `NavigationMenuContent`, `NavigationMenuLink` |
| Pagination | `Pagination`, `PaginationContent`, `PaginationLink`, `PaginationPrevious`, `PaginationNext` |
| Popover | `Popover`, `PopoverTrigger`, `PopoverContent`, `PopoverAnchor` |
| Progress | `Progress` |
| Radio Group | `RadioGroup`, `RadioGroupItem` |
| Resizable | `ResizableHandle`, `ResizablePanel`, `ResizablePanelGroup` |
| Scroll Area | `ScrollArea`, `ScrollBar` |
| Select | `Select`, `SelectTrigger`, `SelectContent`, `SelectItem`, `SelectValue`, `SelectGroup`, `SelectLabel` |
| Separator | `Separator` |
| Sheet | `Sheet`, `SheetTrigger`, `SheetContent`, `SheetHeader`, `SheetTitle`, `SheetDescription`, `SheetFooter` |
| Sidebar | `Sidebar`, `SidebarProvider`, `SidebarMenu`, `SidebarMenuItem`, `SidebarMenuButton`, `SidebarHeader`, `SidebarFooter`, `SidebarContent`, `SidebarGroup`, `SidebarTrigger` |
| Skeleton | `Skeleton` |
| Slider | `Slider` |
| Spinner | `Spinner` |
| Switch | `Switch` |
| Table | `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`, `TableCaption` |
| Tabs | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` |
| Textarea | `Textarea` |
| Toast | `Toaster` |
| Toggle | `Toggle`, `toggleVariants` |
| Toggle Group | `ToggleGroup`, `ToggleGroupItem` |
| Tooltip | `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider` |

---

## Template design guide

Every webpage or screen built with this design system must follow the rules below. These apply on top of all Phase 1–6 rules and must be respected by both human developers and AI code generation tools.

---

### Page structure

Every page must follow this top-level layout skeleton:

```tsx
<div className="min-h-screen bg-background text-on-prominent">
  {/* 1. Navigation / header */}
  <header>…</header>

  {/* 2. Main content area */}
  <main className="flex flex-col gap-8 px-4 py-8 md:px-8 lg:px-16">
    {/* sections go here */}
  </main>

  {/* 3. Footer (if required) */}
  <footer>…</footer>
</div>
```

- The root wrapper always carries `bg-background` and `text-on-prominent`
- Never set a background colour anywhere other than the root unless it is a card, popover, or modal surface
- The `<main>` uses `flex flex-col` with `gap-8` between sections minimum

---

### Layout grid

| Context | Rule |
|---|---|
| Full-width page | `max-w-screen-xl mx-auto px-4 md:px-8 lg:px-16` |
| Centred content | `max-w-2xl mx-auto` |
| Two-column split | `grid grid-cols-1 md:grid-cols-2 gap-6` |
| Three-column grid | `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6` |
| Sidebar + content | `grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-6` |

Never hardcode pixel widths. Never use `w-[Npx]` or `max-w-[Npx]`.

---

### Spacing rhythm

Use the Tailwind spacing scale consistently. Do not mix arbitrary values.

| Purpose | Class |
|---|---|
| Gap between page sections | `gap-8` or `gap-12` |
| Gap between cards in a grid | `gap-4` or `gap-6` |
| Padding inside a card | `p-4` or `p-6` |
| Gap between label and value | `gap-1` or `gap-2` |
| Gap between buttons | `gap-3` |
| Vertical stack inside a card | `flex flex-col gap-3` |

---

### Section anatomy

Every distinct content section on a page must follow this pattern:

```tsx
<section className="flex flex-col gap-4">
  {/* Section heading — always Orbitron via heading utility */}
  <h2 className="heading-xs text-on-prominent">SECTION TITLE</h2>

  {/* Section content — cards, tables, charts, etc. */}
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
    …
  </div>
</section>
```

- Section headings always use a `heading-*` utility class (Orbitron, uppercase)
- Never write a heading with `font-bold text-xl` or any manual font styling
- Section content lives inside a grid or flex container — never loose children

---

### Card composition

Cards are the primary content surface. Every card must follow this structure:

```tsx
<Card className="border border-border bg-card rounded-lg">
  <CardHeader>
    <CardTitle className="heading-xs text-on-prominent">CARD TITLE</CardTitle>
    {/* Optional */}
    <CardDescription className="body-sm text-on-subtle">Supporting text</CardDescription>
  </CardHeader>

  <CardContent className="flex flex-col gap-3">
    {/* Card body content */}
  </CardContent>

  {/* Only include CardFooter when there are actions */}
  <CardFooter className="flex justify-end gap-3">
    <Button variant="outline">Cancel</Button>
    <Button variant="primary">Confirm</Button>
  </CardFooter>
</Card>
```

Rules:
- Always `border border-border` — never `border-white` or no border
- Always `bg-card` — never a hardcoded background
- `CardFooter` is only added when the card has actions; do not add an empty footer
- When two actions exist in `CardFooter`, the right button is always `variant="primary"`

---

### Typography hierarchy

Every page must use exactly one `heading-h*` level per visual section. Do not skip or repeat levels.

| Element | Class | Notes |
|---|---|---|
| Page title | `heading-h2` or `heading-h3` | One per page |
| Section title | `heading-xs` | One per section |
| Card title | `heading-xs` | Inside `CardTitle` |
| Primary body | `body-md text-on-prominent` | Main readable content |
| Supporting text | `body-sm text-on-subtle` | Descriptions, subtitles |
| Micro labels | `body-xs text-on-muted` | Timestamps, metadata |
| Profit / gain | `body-md text-semantic-win` | Always use semantic token |
| Loss / negative | `body-md text-semantic-loss` | Always use semantic token |
| Warning | `body-md text-semantic-warning` | Always use semantic token |

Never combine `heading-*` and `body-*` on the same element. Never apply `text-semantic-*` to buttons or headings.

---

### Colour usage on a page

| Surface | Token class | Never use |
|---|---|---|
| Page background | `bg-background` | Any other bg on the root |
| Card / panel | `bg-card` | `bg-white`, `bg-gray-*`, hardcoded hex |
| Elevated surface (dropdown, popover) | `bg-popover` | Custom backgrounds |
| Primary CTA | `bg-primary` (via Button) | Inline `bg-[#00FFFF]` |
| Accent highlight | `bg-brand-accent` | `bg-orange-*` |
| Hover tint | `bg-hover` | Custom hover colours |
| Modal scrim | `bg-overlay` | `bg-black/50` or hardcoded rgba |

---

### Responsive behaviour

Every template must be tested and functional at three breakpoints. Build mobile-first.

| Breakpoint | Width | Expectation |
|---|---|---|
| Mobile (`default`) | < 640px | Single column, full-width cards, stacked nav |
| Tablet (`md`) | 768px+ | Two-column grids unlock, sidebar may collapse |
| Desktop (`lg` / `xl`) | 1024px+ | Full layout, three-column grids, sidebar visible |

```tsx
// ✅ correct — mobile-first responsive grid
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">

// ❌ wrong — desktop-only, no mobile consideration
<div className="grid grid-cols-3 gap-4">
```

---

### Dark theme

The design system is dark-first. The `dark` class must always be present on the `<html>` element.

- Never add `light` mode overrides unless explicitly specified in the Figma spec
- Never use `dark:` prefixed classes to toggle colours — all tokens are already dark-mode values
- Never add `bg-white`, `text-black`, or any light-mode colour class anywhere

---

### AI prompt checklist for webpage generation

When using AI (Cursor, Copilot, or similar) to generate a new page or template, the prompt must include or the output must satisfy all of the following:

```markdown
- [ ] Root wrapper has bg-background and text-on-prominent
- [ ] All components imported from @playground-ds/design-system
- [ ] Page uses max-w-screen-xl mx-auto for content width
- [ ] Sections use heading-xs (Orbitron) for titles — no manual font styling
- [ ] Body text uses body-md / body-sm / body-xs utility classes
- [ ] Cards have border border-border and bg-card
- [ ] Two-button groups: outline on left, primary on right
- [ ] Outline buttons have no colour overrides — always cyan
- [ ] Profit/loss/warning values use text-semantic-win / text-semantic-loss / text-semantic-warning
- [ ] Grid layouts are mobile-first (grid-cols-1 → md:grid-cols-2 → lg:grid-cols-3)
- [ ] No hardcoded colours, spacing, or font values anywhere
- [ ] No inline styles
- [ ] No Tailwind arbitrary values
- [ ] dark class is on the html element
```

### Default starter template

When a designer or developer prompts AI with **"set up a new project in React, I want to use this design system to build a webpage"**, the AI must output this exact starter template. It is pre-wired with all correct imports, token classes, typography, layout, and component patterns.

Copy this as `src/App.tsx` (Vite) or `app/page.tsx` (Next.js) and replace the placeholder content with the real design.

```tsx
import {
  Button,
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
  CardFooter,
  Badge,
} from "@playground-ds/design-system"

// ─── Page ────────────────────────────────────────────────────────────────────

export default function Page() {
  return (
    <div className="min-h-screen bg-background text-on-prominent">
      {/* ── Header ── */}
      <header className="border-b border-border px-4 py-4 md:px-8 lg:px-16">
        <div className="max-w-screen-xl mx-auto flex items-center justify-between">
          <h1 className="heading-xs">YOUR LOGO</h1>
          <nav className="flex items-center gap-3">
            <Button variant="ghost">Home</Button>
            <Button variant="outline">Sign in</Button>
            <Button variant="primary">Get started</Button>
          </nav>
        </div>
      </header>

      {/* ── Main ── */}
      <main className="max-w-screen-xl mx-auto flex flex-col gap-12 px-4 py-8 md:px-8 lg:px-16">

        {/* ── Hero section ── */}
        <section className="flex flex-col gap-4">
          <Badge variant="fill-purple">Live</Badge>
          <h2 className="heading-h2">PAGE TITLE</h2>
          <p className="body-lg text-on-subtle">
            Supporting description text goes here. Keep it concise and scannable.
          </p>
          <div className="flex items-center gap-3">
            <Button variant="outline">Learn more</Button>
            <Button variant="primary">Start now</Button>
          </div>
        </section>

        {/* ── Stats row ── */}
        <section className="flex flex-col gap-4">
          <h3 className="heading-xs">OVERVIEW</h3>
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">

            {/* Stat card — positive */}
            <Card className="border border-border bg-card rounded-lg">
              <CardHeader>
                <CardTitle className="heading-xs text-on-prominent">TOTAL PROFIT</CardTitle>
                <CardDescription className="body-sm text-on-subtle">All time</CardDescription>
              </CardHeader>
              <CardContent>
                <span className="heading-h4 text-semantic-win">+$1,250.00</span>
              </CardContent>
            </Card>

            {/* Stat card — negative */}
            <Card className="border border-border bg-card rounded-lg">
              <CardHeader>
                <CardTitle className="heading-xs text-on-prominent">TOTAL LOSS</CardTitle>
                <CardDescription className="body-sm text-on-subtle">All time</CardDescription>
              </CardHeader>
              <CardContent>
                <span className="heading-h4 text-semantic-loss">-$340.00</span>
              </CardContent>
            </Card>

            {/* Stat card — warning */}
            <Card className="border border-border bg-card rounded-lg">
              <CardHeader>
                <CardTitle className="heading-xs text-on-prominent">MARGIN STATUS</CardTitle>
                <CardDescription className="body-sm text-on-subtle">Current</CardDescription>
              </CardHeader>
              <CardContent>
                <span className="heading-h4 text-semantic-warning">Margin call</span>
              </CardContent>
            </Card>

          </div>
        </section>

        {/* ── Content section with action card ── */}
        <section className="flex flex-col gap-4">
          <h3 className="heading-xs">ACTIONS</h3>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">

            <Card className="border border-border bg-card rounded-lg">
              <CardHeader>
                <CardTitle className="heading-xs text-on-prominent">CARD TITLE</CardTitle>
                <CardDescription className="body-sm text-on-subtle">
                  Describe what this card does in one or two sentences.
                </CardDescription>
              </CardHeader>
              <CardContent className="flex flex-col gap-3">
                <p className="body-md text-on-prominent">Primary content goes here.</p>
                <p className="body-sm text-on-subtle">Supporting detail or metadata.</p>
              </CardContent>
              <CardFooter className="flex justify-end gap-3">
                {/* Left = outline (cyan), Right = primary (cyan filled) */}
                <Button variant="outline">Cancel</Button>
                <Button variant="primary">Confirm</Button>
              </CardFooter>
            </Card>

            <Card className="border border-border bg-card rounded-lg">
              <CardHeader>
                <CardTitle className="heading-xs text-on-prominent">ANOTHER CARD</CardTitle>
                <CardDescription className="body-sm text-on-subtle">
                  Another section of content or settings.
                </CardDescription>
              </CardHeader>
              <CardContent className="flex flex-col gap-3">
                <p className="body-md text-on-prominent">Content here.</p>
                <span className="body-xs text-on-muted">Last updated: just now</span>
              </CardContent>
            </Card>

          </div>
        </section>

      </main>

      {/* ── Footer ── */}
      <footer className="border-t border-border px-4 py-6 md:px-8 lg:px-16">
        <div className="max-w-screen-xl mx-auto flex items-center justify-between">
          <span className="body-sm text-on-muted">© 2025 Your Company</span>
          <span className="body-sm text-on-muted">All rights reserved</span>
        </div>
      </footer>
    </div>
  )
}
```

**What this template enforces out of the box:**

- `bg-background` + `text-on-prominent` on the root — no hardcoded colours
- `max-w-screen-xl mx-auto` content width with responsive horizontal padding
- `heading-h*` / `heading-xs` for all titles (Orbitron, uppercase) — no manual font styling
- `body-lg` / `body-md` / `body-sm` / `body-xs` for all body copy (Barlow)
- `text-semantic-win` / `text-semantic-loss` / `text-semantic-warning` for trading states
- `border border-border bg-card` on every `<Card>` — no `border-white` or hardcoded bg
- `variant="outline"` (cyan) on the left, `variant="primary"` on the right in every `CardFooter`
- `border-b border-border` / `border-t border-border` on header and footer dividers — token colour only
- Mobile-first grid: `grid-cols-1 → md:grid-cols-2 → lg:grid-cols-3`
- Zero inline styles · zero Tailwind arbitrary values · zero hardcoded hex values

---

---

## Development

```bash
git clone https://github.com/trading-game/design-team-playground.git
cd design-team-playground
npm install
npm run dev          # Next.js playground — http://localhost:3000
npm run build        # library build (ESM + CJS + types)
npm run build:next   # Next.js showcase build
```

### Updating design tokens

Tokens are managed in Figma across five variable collections: `1. TailwindCSS` (scale), `2. Theme` (semantic roles), `3. Mode` (light/dark overrides), `4. Custom` (responsive typography), `5. Icon Library`.

To update:
1. Export updated variables from Figma (TailwindCSS.json, Theme.json, Mode.json)
2. Update `app/globals.css` with the new CSS custom property values
3. Run `npm run build` to verify no breakages

### Tech stack

- **React 19** + **TypeScript**
- **Tailwind CSS v4** — CSS-first configuration via `@theme inline`
- **shadcn/ui** New York style — component primitives
- **Radix UI** — accessible headless primitives
- **Barlow** (body) + **Orbitron** (display) — Google Fonts, auto-loaded
- **Lucide** — icon library (`iconLibrary: "lucide"` in `components.json`)
- **Figma** — source of truth for all design tokens

---

## Cursor instructions

Add these to **Settings → Rules for AI** or a `.cursorrules` file at the repo root so Cursor follows the design system rules automatically when generating template code:

```
- Import all UI components from "@playground-ds/design-system" only
- Never import from shadcn/ui, @radix-ui, or lucide-react directly
- Use Tailwind utility classes for all styling — never write inline styles or style={{}} props
- Never use Tailwind arbitrary values like w-[...], p-[...], or text-[...]
- Use bg-background, text-on-prominent, text-on-subtle, text-on-muted for colour
- Use bg-primary, bg-brand-accent, bg-badge-rank, bg-destructive for brand colours
- Use text-semantic-win, text-semantic-loss, text-semantic-warning for trading states
- Use heading-h1, heading-h2, heading-h3, heading-h4, heading-xs for all headings (Orbitron)
- Use body-lg, body-md, body-sm, body-xs for all body text (Barlow)
- Use rounded-sm, rounded-md, rounded-lg etc — never rounded-[Npx]
- Place new templates under src/templates/[template-name]/index.tsx
- Sub-components go in src/templates/[template-name]/components/
- Apply the dark class to the html element — the system is dark-first
- Do not add any @import for fonts — they are loaded by the package
- Two-button groups: the right (confirm/primary) button must always be variant="primary"; the left (cancel/secondary) button must be variant="secondary" or variant="outline" — this applies to CardFooter, DialogFooter, DrawerFooter, AlertDialogFooter, and all inline button pairs
- Card borders must always use border-border (--border token, #0F2830) — never border-white, border-[#FFFFFF], or any hardcoded border colour
- Outline buttons (variant="outline") are the Cyan — Secondary style and must never have any colour overrides under any circumstance — not for destructive actions, not for error/loss contexts, not for any reason; text and border are always cyan from --primary; if a red button is needed use variant="destructive" instead
- For a new full page from scratch, follow the Default starter template in this README (Template design guide section) unless the task specifies otherwise
```

---

*Maintained by the Deriv Design System team · To propose changes open a PR with the label `guideline-update` and tag the Design System Owner.*