# Switch — Figma alignment fix spec (X.4.2 batch)

## Inputs (✓ all read)

- ✓ `figma-data/normalized/figma-mcp-cache/switch.tsx` (axes: darkTheme(boolean) × status(live/off/on) × enable(boolean) × loading(boolean))
- ✓ `src/canonical/Switch.vue` (wrapper, maps darkTheme/enable/loading to dark/yes/no enums)
- ✓ `src/components/Switch/Switch.vue` (impl)
- ✓ `figma-data/normalized/figma-styles.json`
- ✓ `src/design-system/translation/token-aliases.ts`
- ✓ `src/design-system/translation/prop-aliases.md` (Switch axis alias documented: darkTheme→theme, enable inverted→disabled, loading exact)
- ✓ `src/design-system/translation/icon-aliases.ts`
- ✓ `src/tokens/variables.css`

## Mismatches by dimension

### Spacing

| Location | Canonical (impl) | Figma 真源 | Fix |
|---|---|---|---|
| `.switch` `width: 40px; height: 20px` | raw | `h-[20px] w-[40px]` figma raw | non-token base sizing — propose `--switch-track-w: 40px`, `--switch-track-h: 20px` |
| `.switch` `padding: 2px` | raw | implicit (thumb at `left-[2px]` / `right-[2px]`) | propose `--switch-thumb-inset: 2px` |
| `.switch-thumb` `width/height: 16px` | raw | `size-[16px]` figma raw | `var(--sp-m)` (16) OR domain `--switch-thumb-size: 16px` (recommend domain since semantically a control size, not gap) |
| `.switch-thumb` `transform: translateX(20px)` (on state) | raw | implicit: thumb travels 40-2-2-16 = 20 | propose `--switch-thumb-travel: 20px` |
| Loading icon size | `:size="12"` (Icon prop) + `width:12px` (CSS) | `size-[12px]` figma raw | use domain `--switch-loading-icon-size: 12px` OR `var(--sp-s)` (12) |
| Focus outline offset | `outline-offset: 2px` raw | n/a (figma 无 focus state) | runtime addition; keep |
| Border-radius | `100px` raw | `rounded-[100px]` figma raw | replace with `var(--r-xxl)` (canonical `--r-xxl: 100px` ✓ exact match) |
| Thumb border-radius `50%` | raw | `rounded-[100px]` (full pill = 50%) | OK as 50% — semantic for a square; alternatively `var(--r-xxl)` |

### Color

| Location | Canonical (impl) | Figma 真源 | Fix |
|---|---|---|---|
| `.switch` default bg (off, enabled) | `var(--line-border)` (theme-aware) | dark: `var(--ux\/grey\/grey-8-\#595959,#595959)` → `--color-grey-8`; light: `var(--ux\/grey\/grey-6-\#9e9e9e,#9e9e9e)` → `--color-grey-6` | **MISMATCH**: canonical uses single site-theme-aware `--line-border`; figma uses theme-stable per Switch's own theme axis. **Fix**: theme-axis-keyed domain tokens. |
| `.switch--on` bg | `var(--brand)` | `var(--ux\/brand\/brand,#2fb54e)` (dark) / `#299f45` (light) — figma cache shows light `#299f45` matches `--brand` light value | ✓ match (`--brand` is theme-aware, dark=#2fb54e, light=#299f45 — and figma also uses theme-aware brand) |
| `.switch--live.switch--on` bg | `var(--red)` | dark: `var(--ux\/red\/default,#ea4233)` light: `#dc2717` | ✓ match (`--red` theme-aware: dark=#ea4233, light=#dc2717) |
| `.switch--live` (off) bg | `var(--line-border)` | same as default off | (same fix as default off) |
| Disabled (off, !enable) bg | `opacity: 0.4` only (no bg change) | dark: `var(--ux\/grey\/grey-9-\#434343)` → `--color-grey-9`; light: `var(--ux\/grey\/grey-5-\#cccccc)` → `--color-grey-5` | **MISMATCH**: canonical uses opacity-only disabled treatment; figma uses distinct disabled track color. **Fix**: add per-theme disabled bg tokens; remove blanket opacity. |
| Disabled (on, !enable) bg | `opacity: 0.4` on enabled bg | dark: `var(--ux\/brand\/disable,#1a652c)` → `--brand-disable`; light: `var(--ux\/brand\/disable,#8fcc9d)` → `--brand-disable` | **MISMATCH**: canonical opacity, figma uses `--brand-disable` (theme-aware). **Fix**: bg = `var(--brand-disable)` for disabled-on. |
| Disabled (live, !enable) bg | `opacity: 0.4` only | figma cache shows `opacity-40` wrapping live disabled | ✓ figma actually does use opacity for live disabled — keep canonical opacity for live state only. |
| Thumb bg | `var(--color-white)` | `bg-white` figma | ✓ match (now); but fallback color in radial-gradient uses raw — actually no gradient on Switch thumb, just solid white. ✓ |
| Thumb on disabled-off (!darkTheme && off && !enable) | n/a (canonical does not render alt thumb) | figma: thumb image swapped to "Rectangle535" only in `!darkTheme && off && !enable` case (different shape) | review later — likely visual stub, may be rounded square thumb for disabled state. **Open**: probably negligible; keep canonical thumb as-is. |
| Loading spinner color | `rgba(0, 0, 0, 0.4)` raw | figma loading icon uses `imgUnion`/`imgUnion1`/`imgUnion2` (SVG assets, color baked in) | replace `rgba(0,0,0,0.4)` with token; closest is `color-mix(in srgb, var(--color-grey-14) 40%, transparent)` or reuse `--icon-disabled` semantically. **Fix**: use `var(--icon-disabled)` (semantic intent: "icon shown in disabled-like state"). |

### Typography

- No text in Switch. ✓

### Effect

- No shadows in figma source. Canonical has none. ✓

### Composition

- Figma renders thumb as `<img>` (with embedded SVG asset for filled circle) plus a separate loading SVG layer. Canonical uses CSS-styled `<span class="switch-thumb">` + Vue `<Icon name="loading">`. Visual parity preserved; markup divergence approved.
- Figma has 17 distinct variants (3 status × 2 darkTheme × 2 enable × 2 loading minus illegal combos). Canonical impl handles `on/off/live` + disabled/loading via class modifiers. ✓ structurally parametrized.
- **Mismatch**: figma has `darkTheme` axis, but canonical impl CSS does not branch on theme (it relies on `--line-border`/`--brand`/`--red` site-theme-aware tokens). Like Tooltip/Progress, Switch needs **theme-axis-stable** per-variant tokens. (See Color mismatches above.)

### Variant coverage

| Axis | Figma values | Canonical wrapper | Canonical impl | Status |
|---|---|---|---|---|
| darkTheme (figma boolean) | `true` / `false` | `'on'/'off'` enum | not consumed in CSS | **GAP** — impl ignores theme |
| status | `live`, `off`, `on` | `'live' \| 'off' \| 'on'` | wrapper computes `modelValue` (on/live = true), passes `live` flag → impl `.switch--on`, `.switch--live` | ✓ match (semantic) |
| enable (figma boolean) | `true` / `false` | `'yes' \| 'no'` enum, inverted → `disabled` | `disabled` boolean prop | ✓ match per `prop-aliases.md` inverted alias |
| loading (figma boolean) | `true` / `false` | `'yes' \| 'no'` enum → `loading` boolean | `loading` prop | ✓ match |

Default mismatch: figma `darkTheme: true, enable: true, loading: false, status: "on"`. Canonical wrapper defaults: `darkTheme: 'on', enable: 'yes', loading: 'no', status: 'off'`. Status default differs (figma=on, canonical=off) — **Open**: align?

## Token usage summary

After fix the impl file should reference only:

- Spacing: `--sp-s` (12 loading icon)
- Color: `--brand`, `--brand-disable`, `--red`, `--color-grey-5`, `--color-grey-6`, `--color-grey-8`, `--color-grey-9`, `--color-white`, `--icon-disabled`
- Radius: `--r-xxl`
- Typography: none
- Effect: none
- Domain (proposed): `--switch-track-w`, `--switch-track-h`, `--switch-thumb-size`, `--switch-thumb-inset`, `--switch-thumb-travel`, `--switch-track-bg-off-dark`, `--switch-track-bg-off-light`, `--switch-track-bg-disabled-off-dark`, `--switch-track-bg-disabled-off-light`

## Refactor scope

1. `src/components/Switch/Switch.vue` `<script setup>`:
   - Accept `theme: 'dark' \| 'light'` prop (currently inferred from wrapper but not consumed). Update class binding: add `switch--${theme}`.
   - Wrapper `Switch.vue` (canonical) — pass `:theme="darkTheme === 'on' ? 'dark' : 'light'"` to BaseSwitch.
2. `src/components/Switch/Switch.vue` `<style scoped>`:
   - Replace all raw widths/heights/radii with proposed domain tokens.
   - Replace `border-radius: 100px` with `var(--r-xxl)`.
   - Add per-theme `.switch--dark`/`.switch--light` blocks setting `--switch-bg-off`, `--switch-bg-disabled-off`.
   - Off enabled bg: `var(--switch-bg-off)` — dark=grey-8, light=grey-6.
   - Disabled off bg: `var(--switch-bg-disabled-off)` — dark=grey-9, light=grey-5.
   - Disabled on bg: `var(--brand-disable)`.
   - Remove `.switch--disabled { opacity: 0.4 }` blanket; instead apply opacity ONLY to `.switch--live.switch--disabled` (figma uses opacity for live disabled per cache).
   - Replace `rgba(0, 0, 0, 0.4)` thumb spinner color with `var(--icon-disabled)`.
3. `src/canonical/Switch.vue` — pass theme to BaseSwitch (1-line edit).
4. `src/tokens/variables.css` — add under "Derived aliases":
   ```
   /* Switch — has own theme axis, theme-stable per variant (figma 真源) */
   --switch-track-w: 40px;
   --switch-track-h: 20px;
   --switch-thumb-size: var(--sp-m);          /* 16px */
   --switch-thumb-inset: 2px;
   --switch-thumb-travel: 20px;
   --switch-loading-icon-size: var(--sp-s);   /* 12px */
   --switch-bg-off-dark: var(--color-grey-8);
   --switch-bg-off-light: var(--color-grey-6);
   --switch-bg-disabled-off-dark: var(--color-grey-9);
   --switch-bg-disabled-off-light: var(--color-grey-5);
   ```
5. `src/design-system/translation/prop-aliases.md`:
   - Switch entries already in Boolean Property Aliases table (theme axis alias, enable inverted, loading exact). ✓
   - `status` axis (`live/off/on`) not registered — add row: `Switch | status | status (live/off/on) | exact match | identity`.

## Blockers / open decisions

- **Default status**: figma `on`, canonical `off`. Recommend keep canonical `off` (safer default for forms; figma "on" is just preview state).
- **Disabled treatment**: canonical uses opacity, figma uses solid disabled bg colors (per state). Aligning to figma is the fix; live-disabled is the only state that figma DOES use opacity for (`opacity-40`). Keep that one.
- Canonical impl `--brand-disable` is theme-aware (dark=#1a652c, light=#8fcc9d) and figma per-theme uses same variable values — ✓ aligned, no separate domain token needed.
- Loading spinner SVG color is currently inline from Icon component (uses `currentColor`); CSS `color: var(--icon-disabled)` propagates via `currentColor`. Verify Icon component honors color cascade.
