Skills

Agent skills for Claude Code that teach an AI agent to run Particles UI workflows correctly. Copy a skill's markdown into your project and your agent gains the knowledge automatically.

To install: copy the markdown below into .claude/skills/<name>/SKILL.md at your project root, then ask your agent to perform the task.

push-tokens

Push a local design-token file into Particles Studio with the CLI so tokens land with the correct type, tier, Figma scopes, and a usage description, per file format (DTCG/JSON, Tokens Studio, Style Dictionary, Particles JSON, CSS, SCSS, JS/TS). Use when asked to push/import/upload/migrate/bootstrap tokens into a Particles project or branch, or to sync a token file up to Studio.

.claude/skills/push-tokens/SKILL.md
---
name: push-tokens
description: Push a local design-token file into Particles Studio with the CLI so tokens land with the correct type, tier, Figma scopes, and a usage description, per file format (DTCG/JSON, Tokens Studio, Style Dictionary, Particles JSON, CSS, SCSS, JS/TS). Use when asked to push/import/upload/migrate/bootstrap tokens into a Particles project or branch, or to sync a token file up to Studio.
---

# Push tokens to Particles Studio

The `particles token-studio push` command imports a local token file into a project branch.
The command infers the format **from the file extension** and the backend normaliser then maps it
to Particles tokens. Getting the push "correct" means each token lands with the right **type**,
**tier**, **scopes**, and a **usage description** — so choose the format for the fidelity you need,
name and describe tokens well, and validate *before* you push, because the CLI dry-run reports
**counts only**, not per-token results, so several silent failures are invisible in its output.

## The one-line command

```bash
particles token-studio push --file <path> [--branch <name>] [--tier <tier>] [--dry-run]
```

- `--branch` defaults to `main`. It does **NOT** fall back to `particles-ui.json`'s `branch`. If you
  want a non-main branch you must pass `--branch` explicitly (and the branch must already exist —
  create it with `particles token-studio branch create <name>`).
- `--tier` is `primitive | semantic | composition`, default `primitive`. It is only a **fallback** for
  tokens whose format doesn't carry a tier (see the fidelity table).
- Import is **additive-only**: tokens are added, overwritten (value change), or skipped (unchanged).
  It **never deletes** tokens that aren't in the file.
- **Type, tier, groupPath, and scopes are set ONLY when a token is first inserted.** An overwrite
  updates `valueRaw` *only* — re-pushing the same token name will **not** correct a wrong type, tier,
  or scope. To fix those you must edit the token in Studio (or delete it and re-add). Get type +
  naming right *before* the first push.

## Workflow (follow in order)

1. **Check prerequisites.**
   ```bash
   particles auth status          # must be logged in
   ```
   - There must be a `particles-ui.json` with a `projectId` in the cwd (or an ancestor dir), or pass
     `--config <path>`. If missing, run `particles init` first.
   - The account needs the `tokens:write` permission on the project (Admin, or Designer/Developer
     with write access).

2. **Pick the right format for the source data.** See **Format → fidelity** below. The extension you
   give `--file` decides how it's parsed — rename or convert the file if its extension doesn't match
   its real format.

3. **Pre-validate types AND naming (critical — the CLI will not warn you, and you can't fix it later).**
   - **Type:** every token's declared type runs through `normaliseTokenType`, which **silently coerces
     any unrecognised type to `color`**. Read the file and confirm every `$type` / `type` is in the
     supported set (or a known alias) below. A wrong type also produces wrong **scopes** (scopes are
     derived from type), so this one mistake corrupts two fields at once.
   - **Scopes follow from type + name.** Particles auto-infers each token's Figma `scopes` from its
     **type** plus keywords in its **name/groupPath** — the file never carries scopes. For `color` and
     `spacing` tokens the *name* decides the scope, so name them with the right keyword (see
     **Types & scopes** below). Tokens whose names lack a keyword fall back to a broad default
     (`ALL_FILLS` / `GAP`), which is usually not what you want for borders, text, icons, or sizes.
   - **Describe every token.** Give every token a `$description` (DTCG / Tokens Studio) or
     `description` (Particles JSON) saying **what it's for / when to use it** — not what its value is.
     Good: `"Primary action button background; use for the main CTA on a page."` Bad: `"Blue 500."`
     The description is imported and shown in Studio and the Figma plugin to guide consumers. Like
     type and scopes it is set **only on first insert** — an overwrite won't add or change it — and
     **CSS / SCSS / JS / TS / CSV carry no descriptions at all** (see **Descriptions** below).

4. **Dry-run and read the counts.**
   ```bash
   particles token-studio push --file ./tokens.json --dry-run
   ```
   Output is **counts only**:
   ```
     Added:       12
     Overwritten: 3
     Skipped:     1
     Skipped (non-primitive): 4   # only printed when > 0
   ```
   What the counts catch / miss:
   - **Caught:** fewer `Added` than tokens in the file (silently dropped); `Skipped (non-primitive) > 0`
     (the project is a Foundation/Brand "primitives-only" project and dropped your semantic/composition
     tokens); unexpected `Overwritten` (you're clobbering existing values).
   - **NOT caught:** type coercion to `color`, tier collapse, composition flattening. These never show
     up in counts — that's why step 3 matters.

5. **Apply** once the dry-run counts match your expectation (drop `--dry-run`):
   ```bash
   particles token-studio push --file ./tokens.json --branch main
   ```

6. **Verify** what actually landed (counts can't confirm types/tiers/scopes). Round-trip export and
   diff:
   ```bash
   particles token-studio export --format json --branch main --output /tmp/after.json
   ```
   The `json` format includes `type` and `tier` per token — compare them against your source.
   **Scopes are not included in any export format** — confirm those in Particles Studio
   (Project → Tokens → token detail) if scope correctness is critical.

## Format → fidelity

| Extension | Detected as | Tier carried? | Composition? | Notes |
|---|---|---|---|---|
| `.json` (array w/ `valueRaw`) | **Particles JSON** | ✅ per-token `tier` | ✅ (string value) | Native export. Best for full-fidelity round-trips. Foreign `parentId`s are silently dropped (IDOR guard). |
| `.json` (`$value`+`$type`, has `$schema`) | **W3C / DTCG** | ❌ → all get `--tier` | ✅ object `$value` preserved | Best interoperable format; preserves composition. |
| `.json` (`$value`/`value`, `$metadata`/`$values`, or bare nested) | **Tokens Studio** | ❌ → all get `--tier` | partial | Default fallback for unrecognised JSON objects. |
| `.json` (Style Dictionary nested `{value,type}`) | via Tokens Studio walk | ❌ → all get `--tier` | partial | `value` (no `$`) is read. |
| `.css` | **CSS custom properties** | ✅ inferred | ✅ `.class{}` / `@utility` blocks | `:root{}` / `@theme{}` vars → primitive; `var(--…)` refs or `semantic` in the name → semantic. |
| `.scss` | **SCSS variables** | ✅ inferred | ✅ | `$name: value;` lines reuse the CSS path; the `$tokens:()` map is skipped. |
| `.js` / `.ts` | **JS/TS theme object** | ❌ → all get `--tier` | ❌ **flattened to leaf tokens (lossy)** | Must `export const theme = { … }`. Evaluated in a 5s VM sandbox; `as const` / `export type` stripped. |

### Choosing correctly

- **Mixed primitive + semantic + composition design system?** It **cannot** round-trip through a single
  DTCG / Tokens-Studio / JS / TS push — those formats carry no tier, so every token collapses to the one
  `--tier` value. Use one of:
  - **Particles JSON** (per-token `tier`), or
  - **CSS / SCSS** (tier inferred from `var()` references and `semantic` naming), or
  - **Split into separate pushes**, one per tier: `--tier primitive`, then `--tier semantic`, etc.
- **Composition tokens matter?** Use **DTCG** (object `$value` preserved) or **CSS** (`.class{}` /
  `@utility` blocks). Avoid JS/TS — composition objects flatten to individual leaf tokens.
- **Just bootstrapping primitives?** Any format works with the default `--tier primitive`.

## Supported token types (for step 3 pre-validation)

Canonical types (used as-is, case-insensitive):

```
color, typography, spacing, radii, shadow, elevation, opacity, border,
duration, motion, grid, lineHeight, letterSpacing, fontWeight, breakpoint, zIndex
```

Recognised aliases (auto-mapped):

```
text|fontSizes|fontWeights|fontFamilies|lineHeights|letterSpacing|paragraphSpacing → typography
sizing → spacing      borderRadius → radii      borderWidth|borders → border
boxShadow|shadows → shadow      time|animation → duration
```

**Anything not in either list is silently coerced to `color`.** Flag such types to the user and fix the
source before pushing.

## Types & scopes

Scopes are the Figma `VariableScope`s a token is allowed to bind to (e.g. only fills, only corner
radius). Particles **infers them automatically at import** from the token's `type` plus keywords in
its full name (`groupPath.name`). The file never supplies scopes — so the only levers you have are
**getting the type right** and **naming tokens with the right keyword**.

**`color` — scope is chosen by the first matching name keyword:**

| Name contains (case-insensitive) | Inferred scopes |
|---|---|
| `background`, `bg`, `surface`, `backdrop`, `canvas`, `fill` | `FRAME_FILL`, `SHAPE_FILL` |
| `text`, `foreground`, `fg`, `label`, `caption`, `content`, `on-`, `oncolor` | `TEXT_FILL` |
| `border`, `stroke`, `outline`, `ring`, `divider`, `separator`, `line` | `STROKE_COLOR` |
| `shadow`, `effect`, `scrim`, `overlay` | `EFFECT_COLOR` |
| `icon`, `glyph`, `symbol` | `SHAPE_FILL` |
| _(no keyword matched)_ | `ALL_FILLS` (default) |

**`spacing` — scope is chosen by the first matching name keyword:**

| Name contains | Inferred scopes |
|---|---|
| `gap`, `gutter`, `spacing`, `space`, `padding`, `margin`, `inset` | `GAP` |
| `width`, `height`, `size`, `dimension`, `min`, `max` | `WIDTH_HEIGHT` |
| _(no keyword matched)_ | `GAP` (default) |

**All other types get a fixed scope from their type** (name is ignored):
`radii → CORNER_RADIUS`, `opacity → OPACITY`, `lineHeight → LINE_HEIGHT`,
`letterSpacing → LETTER_SPACING`, `fontWeight → FONT_WEIGHT`, and
`duration / zIndex / elevation / grid / motion / breakpoint → ALL_SCOPES`.
Types with no mapping (`typography`, `shadow`, `border`) get **no scopes** (stored as null) — that is
expected; bind them in Figma manually if needed.

**Naming guidance to get correct scopes** (matters most for `color` and `spacing`):

- A border color named `color.brand` infers `ALL_FILLS` — wrong. Name it `color.border.default` or
  `border-strong` so it infers `STROKE_COLOR`.
- A text color should include `text`/`fg`/`on-` (e.g. `text.primary`, `on-surface`) → `TEXT_FILL`.
- A surface/background color should include `background`/`surface`/`bg` → `FRAME_FILL` + `SHAPE_FILL`.
- A fixed dimension named `spacing.4` infers `GAP`; if it's really a width/height, name it
  `size.icon.md` or include `width`/`height` → `WIDTH_HEIGHT`.
- `groupPath` counts toward the match, so `border` as a group (`border.default`) works as well as a
  name keyword.

## Descriptions

Every token should carry a description of **what it's used for** (its intent / when to reach for it),
which is imported and surfaced in Studio and the Figma plugin. A token's value already says what it
*is* — the description must say what it's *for*.

- **Write intent, not the value.** ✅ `"Default page surface; sits behind cards and content."`
  ❌ `"#ffffff"` / `"Gray 50."`
- **Set it before the first push** — descriptions are insert-only, so an overwrite never adds or
  updates one. Backfilling later means editing in Studio.

**Which formats carry a description, and how to write it:**

| Format | Field | Example |
|---|---|---|
| DTCG / W3C `.json` | `$description` | `{ "$value": "#3b82f6", "$type": "color", "$description": "Primary brand color; main CTA fills and active states." }` |
| Tokens Studio `.json` | `$description` | same `$description` key as DTCG |
| Particles JSON `.json` | `description` | `{ "name": "primary", "type": "color", "valueRaw": "#3b82f6", "description": "Primary brand color; main CTA fills." }` |
| CSS / SCSS / JS / TS / CSV | _(none)_ | These formats **cannot** carry descriptions — tokens import with `description = null`. Use DTCG if descriptions matter. |

**Recommended default format for AI pushes: DTCG (`.json`)** — it's the only widely-interoperable
format that carries `$type`, `$description`, and object composition values together. Reach for
Particles JSON when you also need per-token tiers.

## Errors

| Message | Cause / fix |
|---|---|
| `Not logged in. Run 'particles auth login'.` | Run `particles auth login`. |
| `No project configured. Run 'particles init' first.` | No `particles-ui.json` with `projectId`; run `particles init` or pass `--config`. |
| `Cannot read file: <path>` | `--file` path is wrong. |
| `File is not valid JSON` | A non-css/scss/js/ts file failed `JSON.parse`. |
| `Could not evaluate JS/TS theme file` / `must export a 'theme' object` | The `.js`/`.ts` file doesn't `export const theme = {…}`, or it threw / timed out in the sandbox. |
| `Permission denied: tokens:write is required` | Account lacks write access; ask a project admin. |
| `You are not a member of this project's organization.` | Wrong account / project. |

## Quick recipes

```bash
# DTCG/W3C JSON — safest interop, preserves composition (all one tier)
particles token-studio push --file ./tokens.json --dry-run
particles token-studio push --file ./tokens.json

# Full-fidelity Particles JSON (per-token tiers) to a feature branch
particles token-studio branch create import-2026
particles token-studio push --file ./particles-export.json --branch import-2026

# Mixed tiers via DTCG split into two pushes
particles token-studio push --file ./primitives.json --tier primitive
particles token-studio push --file ./semantic.json   --tier semantic

# CSS with var() semantics — tier inferred automatically
particles token-studio push --file ./globals.css
```