# Figma Plugin (`@cloudflare/kumo-figma`)

Generates production-quality Figma components from `component-registry.json`. Destructive sync: purges and recreates all components per run.

**Parent:** See [root AGENTS.md](../../AGENTS.md) for monorepo context.

## STRUCTURE

```
kumo-figma/
├── src/
│   ├── code.ts                    # Plugin entry: GENERATORS array, page management
│   ├── ui.html                    # Plugin UI
│   ├── manifest.json              # Figma manifest (main: "code.js")
│   ├── build.sh                   # Build script: codegen → esbuild (ES2017, IIFE)
│   ├── generators/                # → see src/generators/AGENTS.md
│   │   ├── shared.ts              # ALL constants + utilities (~1544 lines, critical)
│   │   ├── icon-utils.ts          # Icon creation, placeholder, color binding (308 lines)
│   │   ├── _test-utils.ts         # Shared test assertions
│   │   ├── drift-detection.test.ts  # Meta-test: registry ↔ generator sync (1733 lines)
│   │   └── {component}.ts         # 37 component generators
│   ├── parsers/
│   │   ├── tailwind-to-figma.ts   # Core: Tailwind classes → Figma values
│   │   ├── opacity-extractor.ts   # `bg-kumo-brand/70` → opacity data
│   │   ├── component-registry.ts  # Type-safe registry wrapper
│   │   └── tailwind-theme-parser.ts  # Parses tailwindcss/theme.css (test-only)
│   └── generated/                 # BUILD OUTPUT (gitignored): theme-data.json, etc.
├── scripts/
│   ├── sync-tokens-to-figma.ts    # CSS → Figma Variables API (unidirectional)
│   ├── figma-api.ts               # Low-level Figma REST client (723 lines)
│   ├── color-utils.ts             # oklch → sRGB conversion (uses culori)
│   └── maybe-sync.ts              # Conditional sync gate (skips if no FIGMA_TOKEN)
└── vitest.config.ts               # Node env (no DOM)
```

## WHERE TO LOOK

| Task                     | Location                                             | Notes                              |
| ------------------------ | ---------------------------------------------------- | ---------------------------------- |
| Add generator            | `src/generators/` + register in `code.ts` GENERATORS | Also update drift-detection        |
| Centralized constants    | `src/generators/shared.ts`                           | ALL magic numbers must live here   |
| Icon utilities           | `src/generators/icon-utils.ts`                       | createIconInstance, bindIconColor  |
| Tailwind → Figma parsing | `src/parsers/tailwind-to-figma.ts`                   | Scale lookups from theme-data.json |
| Token sync to Figma      | `scripts/sync-tokens-to-figma.ts`                    | Requires FIGMA_TOKEN               |
| Drift detection          | `src/generators/drift-detection.test.ts`             | Meta-test enforcing sync           |

## CONVENTIONS

### Build Pipeline (Sequential, Order Matters)

```bash
pnpm build =
  1. sync:maybe                        # Sync tokens if FIGMA_TOKEN present
  2. build:data =                      # 4 codegen steps:
     a. tsx build-theme-data.ts        → generated/theme-data.json
     b. tsx build-loader-data.ts       → generated/loader-data.json
     c. tsx build-phosphor-icons.ts    → generated/phosphor-icons.json
     d. tsx build-figma-variables.ts   → generated/figma-variables.json
  3. build:plugin =                    # esbuild → src/code.js (IIFE, ES2017)
```

### Testing Philosophy

- **Test structure, NOT values**: `"DO NOT test specific colors, sizes, or variant names"`
- Tests validate against registry as source of truth
- `_test-utils.ts`: `expectValidRegistryProp()`, `expectAllClassesParsable()`, `expectValidParsedTypes()`
- Drift detection enforces: every registry component has generator, no magic numbers

## ANTI-PATTERNS

| Pattern                                    | Why                     | Instead                              |
| ------------------------------------------ | ----------------------- | ------------------------------------ |
| Hardcoded `SECTION_PADDING`, `SECTION_GAP` | Drift detection fails   | Import from `shared.ts`              |
| Hardcoded shadow effects                   | Test enforcement        | Import `SHADOWS` from `shared.ts`    |
| Hardcoded `opacity = 0.5`                  | Test enforcement        | Use `OPACITY.disabled`               |
| Hardcoded RGB `{ r: 0.5, ... }`            | Test enforcement        | Use `COLORS` from `shared.ts`        |
| `.toBe(16)` for font sizes in tests        | Fragile assertions      | Use `FONT_SIZE.*` or registry values |
| `.toBe(600)` for font weights              | Fragile assertions      | Use `FALLBACK_VALUES.fontWeight.*`   |
| Redeclaring constants from shared.ts       | Drift detection catches | Always import                        |
| `??` operator                              | ES2017 doesn't support  | Use `if (x === undefined)` pattern   |

## NOTES

- **`generated/` is gitignored**: Run `pnpm build:data` after clone/branch switch. Tests fail without it.
- **ES2017 target**: Figma runtime constraint. Avoid `??`, output is IIFE not ESM.
- **`code.js` lives in `src/`**: Not `dist/`. Figma reads `manifest.json` which points to `code.js` in same dir.
- **Two color conversion paths**: `build-figma-variables.ts` (manual oklch→RGB), `scripts/color-utils.ts` (culori). Manual one is less accurate.
- **Opacity variables created at runtime**: `getOrCreateVariableWithOpacity("color-kumo-info/20")` alpha-blends against white/black on the fly.
- **Component name mapping** in drift-detection: `DropdownMenu→dropdown`, `Toasty→toast`, `Switch.Group→switch`
- **`VAR_NAMES`** in shared.ts has legacy aliases (both `color.surface` and `color.base` map to same variable)
- **Font**: Inter is required (default Figma font). `createTextNode()` handles async loading.
- **26 `@ts-ignore` instances**: All for `figma.combineAsVariants()` type mismatch at runtime
