# Cross-Platform Design Tokens

> One canonical JSON source generates CSS, TypeScript, Kotlin, and Swift tokens. Change a color once, update everywhere.

## When to Use

- Building apps across web + iOS + Android
- Maintaining a design system with consistent colors, spacing, typography
- When designers change the palette and you need to update 4 platforms
- When AI agents create UI code and need to use the right tokens

## The Pattern

```
bytelyst.tokens.json          ← CANONICAL SOURCE (one file)
       │
       ├── generate.ts        ← Token generator script
       │
       ├── tokens.css         → Web (CSS custom properties)
       ├── tokens.ts          → Web (TypeScript constants)
       ├── Tokens.kt          → Android (Kotlin Color objects)
       └── Theme.swift        → iOS (SwiftUI Color extension)
```

## Step-by-Step

### 1. Define canonical tokens

`tokens/design-tokens.json`:

```json
{
  "color": {
    "bg": {
      "canvas": { "value": "#06070A", "description": "Page background" },
      "elevated": { "value": "#0E1118", "description": "Elevated surfaces" }
    },
    "surface": {
      "card": { "value": "#121725", "description": "Cards, panels" },
      "muted": { "value": "#1A2335", "description": "Muted backgrounds" }
    },
    "text": {
      "primary": { "value": "#EFF4FF", "description": "Main text" },
      "secondary": { "value": "#A5B1C7", "description": "Descriptions" },
      "tertiary": { "value": "#6C7C98", "description": "Hints" }
    },
    "accent": {
      "primary": { "value": "#5A8CFF", "description": "Primary actions" },
      "secondary": { "value": "#2EE6D6", "description": "Secondary accent" }
    },
    "status": {
      "success": { "value": "#34D399" },
      "warning": { "value": "#F59E0B" },
      "danger": { "value": "#FF6E6E" }
    }
  },
  "spacing": {
    "xs": { "value": "4px" },
    "sm": { "value": "8px" },
    "md": { "value": "16px" },
    "lg": { "value": "24px" },
    "xl": { "value": "32px" }
  },
  "radius": {
    "sm": { "value": "6px" },
    "md": { "value": "12px" },
    "lg": { "value": "16px" },
    "full": { "value": "9999px" }
  },
  "font": {
    "family": {
      "display": { "value": "Space Grotesk" },
      "body": { "value": "DM Sans" },
      "mono": { "value": "IBM Plex Mono" }
    },
    "size": {
      "xs": { "value": "12px" },
      "sm": { "value": "14px" },
      "base": { "value": "16px" },
      "lg": { "value": "20px" },
      "xl": { "value": "24px" },
      "2xl": { "value": "32px" }
    }
  }
}
```

### 2. Write the generator

`scripts/generate-tokens.ts`:

```typescript
import { readFileSync, writeFileSync, mkdirSync } from 'fs';

const tokens = JSON.parse(readFileSync('tokens/design-tokens.json', 'utf-8'));

function flattenTokens(
  obj: any,
  prefix = ''
): Array<{ path: string; value: string; desc?: string }> {
  const result: Array<{ path: string; value: string; desc?: string }> = [];
  for (const [key, val] of Object.entries(obj)) {
    const path = prefix ? `${prefix}-${key}` : key;
    if ((val as any).value !== undefined) {
      result.push({ path, value: (val as any).value, desc: (val as any).description });
    } else {
      result.push(...flattenTokens(val, path));
    }
  }
  return result;
}

const flat = flattenTokens(tokens);

// ── CSS ──────────────────────────────────────────
const css = `:root {\n${flat
  .map(t => `  --${t.path}: ${t.value};${t.desc ? ` /* ${t.desc} */` : ''}`)
  .join('\n')}\n}\n`;
writeFileSync('generated/tokens.css', css);

// ── TypeScript ───────────────────────────────────
const ts =
  flat
    .map(t => {
      const name = t.path.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
      return `export const ${name} = '${t.value}';${t.desc ? ` // ${t.desc}` : ''}`;
    })
    .join('\n') + '\n';
writeFileSync('generated/tokens.ts', ts);

// ── Kotlin ───────────────────────────────────────
const ktColors = flat
  .filter(t => t.value.startsWith('#'))
  .map(t => {
    const name = t.path.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
    const hex = t.value.replace('#', '');
    return `    val ${name} = Color(0xFF${hex})`;
  })
  .join('\n');

const kt = `package com.example.theme\n\nimport androidx.compose.ui.graphics.Color\n\nobject AppTokens {\n${ktColors}\n}\n`;
writeFileSync('generated/AppTokens.kt', kt);

// ── Swift ────────────────────────────────────────
const swiftColors = flat
  .filter(t => t.value.startsWith('#'))
  .map(t => {
    const name = t.path.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
    const hex = t.value.replace('#', '');
    const r = parseInt(hex.substring(0, 2), 16) / 255;
    const g = parseInt(hex.substring(2, 4), 16) / 255;
    const b = parseInt(hex.substring(4, 6), 16) / 255;
    return `    static let ${name} = Color(red: ${r.toFixed(3)}, green: ${g.toFixed(3)}, blue: ${b.toFixed(3)})`;
  })
  .join('\n');

const swift = `import SwiftUI\n\nextension Color {\n${swiftColors}\n}\n`;
writeFileSync('generated/AppTheme.swift', swift);

console.log(`Generated ${flat.length} tokens across 4 platforms`);
```

### 3. Run the generator

```bash
npx tsx scripts/generate-tokens.ts
```

### 4. Consume in each platform

**Web (CSS):**

```css
@import './tokens.css';

.card {
  background: var(--surface-card);
  color: var(--text-primary);
  border-radius: var(--radius-md);
  padding: var(--spacing-md);
}
```

**Web (TypeScript):**

```typescript
import { accentPrimary, textPrimary } from './tokens';
```

**Android (Kotlin):**

```kotlin
import com.example.theme.AppTokens

Text(
  text = "Hello",
  color = AppTokens.textPrimary,
)
```

**iOS (Swift):**

```swift
Text("Hello")
  .foregroundColor(.textPrimary)
```

## AGENTS.md Rule

Add this to your AGENTS.md so agents never hardcode colors:

```markdown
## Conventions

- NEVER hardcode colors — use theme tokens
- Web: `var(--accent-primary)` or import from tokens.ts
- iOS: `Color.accentPrimary` (from AppTheme.swift)
- Android: `AppTokens.accentPrimary` (from AppTokens.kt)
```

## Anti-Patterns

- **Hardcoded hex values in components** — Always use token references
- **Different colors on different platforms** — Generate from one source
- **Manual token updates** — Run the generator, don't hand-edit generated files
- **No description field** — Descriptions help agents pick the right token
- **Tokens for everything** — Tokens are for design decisions, not one-off values

## Related Skills

- [01 — AGENTS.md Pattern](./01-agents-md-pattern.md)
- [14 — Shared Package Extraction](./14-shared-packages.md)
