# Dual-Framework (Vue + React) Route-A Pilot — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Prove (or disprove, per component) that TVU's existing Vue canonical components can be exposed to React via `defineCustomElement` + thin React wrappers, at Figma-verified fidelity, using Button / Input / FormItem as the pilot set.

**Architecture:** A new, isolated Web Component build entry compiles 3 canonical Vue components into native custom elements (reusing the existing Vue source and shared `tokens/variables.css`). An isolated `react-pilot/` workspace provides thin React wrappers that map props/events/v-model/slots to an idiomatic React API. A React render-harness reuses the existing render-verification manifest entries for those 3 components and compares `getComputedStyle` against the same Figma expected values the Vue side already passes. Nothing touches the published Vue package or its build.

**Tech Stack:** Vue 3.5.32 (`defineCustomElement`), Vite, React 19 + `@vitejs/plugin-react`, vitest + @testing-library/react, Playwright (existing render-verification harness), pnpm.

## Global Constraints

- **Figma is the absolute source of truth** ([`docs/FIGMA_AS_SOURCE_OF_TRUTH.md`](../../FIGMA_AS_SOURCE_OF_TRUTH.md)): React renders must 1:1 reproduce Figma; verification must be **deterministic (manifest-based getComputedStyle vs Figma expected)** — never heuristic, never AI free-styling. Tolerance = 5% (existing `audit:render-drift-gate`口径).
- **Do not modify the published Vue package or its build**: no changes to `package.json` `main`/`module`/`exports`/`files`, no changes to `vite.config.ts`, `src/index.ts`, or `src/canonical/*.vue` source. Pilot artifacts are additive and isolated.
- **Pilot scope only**: Button (`src/canonical/ButtonBridge.vue`), Input (`src/canonical/InputBoxLine.vue`), FormItem (`src/canonical/FormItem.vue`). No other components. No production React npm publish flow.
- **Shared, not copied**: tokens/CSS/icons are consumed from existing `src/tokens/variables.css` and `dist/icons/`; never duplicated.
- **Pilot PASS gate** (per spec §5, all 3 components must satisfy all 4): (1) global `--token` light/dark theming visibly applies in React; (2) React render passes existing render-verification for the same manifest nodes; (3) v-model/events/slots work through an idiomatic React API; (4) the React artifact is importable the way design-sync consumes it.
- **Commit discipline**: project owner commits directly to `master`; `commit` includes `push` to `origin` (Gitea + GitHub dual push URL). Each task ends with a commit. Co-author footer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
- **`.gitignore`**: `react-pilot/node_modules`, `dist-wc/`, and `react-pilot/dist/` are build/dep output — add to `.gitignore`, never commit.

---

## File Structure

| Path | Responsibility | Create/Modify |
|---|---|---|
| `vite.web-components.config.ts` | Isolated Vite build that compiles the 3 canonical SFCs into a custom-elements IIFE/ESM bundle → `dist-wc/` | Create |
| `src/web-components/register.ts` | `defineCustomElement` registration entry for `tvu-button` / `tvu-input` / `tvu-form-item` (+ token CSS import) | Create |
| `react-pilot/package.json` | Isolated workspace: React 19, @vitejs/plugin-react, vite, vitest, @testing-library/react, @types/react | Create |
| `react-pilot/vite.config.ts` | React build/dev config; aliases the WC bundle | Create |
| `react-pilot/src/wrappers/Button.tsx` | Thin React wrapper over `<tvu-button>` (props→attrs, click event) | Create |
| `react-pilot/src/wrappers/Input.tsx` | Wrapper over `<tvu-input>` (v-model→controlled value/onChange) | Create |
| `react-pilot/src/wrappers/FormItem.tsx` | Wrapper over `<tvu-form-item>` (slots→children) | Create |
| `react-pilot/src/wrappers/types.ts` | Shared prop union types mirrored from the canonical `.vue` defineProps | Create |
| `react-pilot/src/wrappers/*.test.tsx` | vitest + @testing-library/react smoke + interop tests per wrapper | Create |
| `react-pilot/harness/RenderHarness.tsx` | React render-harness that renders manifest entries for the 3 components | Create |
| `react-pilot/harness/index.html` + `harness/main.tsx` | Entry serving the React harness at a known route for Playwright | Create |
| `tests/render-verification-react/react-drift.spec.ts` | Playwright spec: load React harness, read getComputedStyle, compare to Figma expected (reuse existing expected loader) | Create |
| `playground/docs/pages/DualFrameworkPilotPage.vue` | Vue/React side-by-side showcase page for the 3 components (iframes the React demo) | Create |
| `docs/internal/retrospection/dual-framework-react-pilot-findings-2026-06-29.md` | Per-component go/fall-back-to-B decision + custom-element quirks + full-rollout estimate | Create |
| `.gitignore` | ignore `react-pilot/node_modules`, `dist-wc/`, `react-pilot/dist/` | Modify |

---

## Task 1: Web Component build entry — compile 3 canonical SFCs to custom elements

**Files:**
- Create: `src/web-components/register.ts`
- Create: `vite.web-components.config.ts`
- Modify: `.gitignore` (add `dist-wc/`)
- Modify: `package.json` scripts (add `build:wc` — additive script only, does not touch main build)

**Interfaces:**
- Produces: a built bundle at `dist-wc/tvu-web-components.js` that, when loaded in a page, registers custom elements `tvu-button`, `tvu-input`, `tvu-form-item`. Each element accepts the canonical component's props as attributes/properties and emits its native events.

- [ ] **Step 1: Add the registration entry**

`src/web-components/register.ts`:
```ts
import { defineCustomElement } from 'vue'
// Shared design-token tier (:root CSS custom properties). CSS custom properties
// pierce shadow-DOM boundaries, so this is reachable even from shadow roots.
import tokenCss from '../tokens/variables.css?inline'
import ButtonBridge from '../canonical/ButtonBridge.vue'
import InputBoxLine from '../canonical/InputBoxLine.vue'
import FormItem from '../canonical/FormItem.vue'

// styles: [tokenCss] injects the global token tier into each element's root so
// tokens resolve even under shadow DOM. Component <style> is auto-collected by
// defineCustomElement from the SFC.
const opts = { styles: [tokenCss] }

export const TvuButton = defineCustomElement(ButtonBridge, opts)
export const TvuInput = defineCustomElement(InputBoxLine, opts)
export const TvuFormItem = defineCustomElement(FormItem, opts)

export function registerTvuElements() {
  if (!customElements.get('tvu-button')) customElements.define('tvu-button', TvuButton)
  if (!customElements.get('tvu-input')) customElements.define('tvu-input', TvuInput)
  if (!customElements.get('tvu-form-item')) customElements.define('tvu-form-item', TvuFormItem)
}

registerTvuElements()
```

- [ ] **Step 2: Add the isolated Vite build config**

`vite.web-components.config.ts`:
```ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  // customElement: true makes vue compile SFC <style> into the custom element
  plugins: [vue({ customElement: true })],
  resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
  build: {
    outDir: 'dist-wc',
    lib: {
      entry: fileURLToPath(new URL('./src/web-components/register.ts', import.meta.url)),
      name: 'TvuWebComponents',
      fileName: () => 'tvu-web-components.js',
      formats: ['es', 'umd'],
    },
  },
})
```

- [ ] **Step 3: Add the build script and gitignore entry**

In `package.json` `scripts`, add (do not modify existing scripts):
```json
"build:wc": "vite build --config vite.web-components.config.ts"
```
Append to `.gitignore`:
```
dist-wc/
```

- [ ] **Step 4: Run the build, verify it produces the bundle**

Run: `pnpm build:wc`
Expected: exit 0; `dist-wc/tvu-web-components.js` exists. Verify: `ls -la dist-wc/`.
If the build errors on `?inline` or `customElement`, this is the first spike decision point — record the exact error and the resolution (e.g. style handling mode) in the findings doc (Task 8) before continuing.

- [ ] **Step 5: Smoke-render the custom elements headlessly (theming probe)**

Create a throwaway probe `dist-wc/_probe.html` (gitignored dir) that loads the bundle and mounts one of each element in both light and dark theme, then assert via a short Playwright/node script that `getComputedStyle(button).backgroundColor` is non-empty and **differs between `data-figma-theme="light"` and `dark`** (proves global `--token` theming pierces into the element).

Probe script `scripts/_wc-theme-probe.mjs` (gitignored or deleted after):
```js
import { chromium } from 'playwright'
import { fileURLToPath } from 'node:url'
const url = 'file://' + fileURLToPath(new URL('../dist-wc/_probe.html', import.meta.url))
const b = await chromium.launch(); const p = await b.newPage(); await p.goto(url)
const light = await p.$eval('tvu-button.light', el => getComputedStyle(el.shadowRoot?.querySelector('button') ?? el).backgroundColor)
const dark = await p.$eval('tvu-button.dark', el => getComputedStyle(el.shadowRoot?.querySelector('button') ?? el).backgroundColor)
console.log({ light, dark, themingWorks: !!light && light !== 'rgba(0, 0, 0, 0)' })
await b.close()
```
Run: `node scripts/_wc-theme-probe.mjs`
Expected: `themingWorks: true` and a sensible non-transparent color.
**Decision gate:** if theming does NOT apply, record it as the primary route-A risk realized (findings doc) — do not silently work around it; surface to owner.

- [ ] **Step 6: Commit**

```bash
git add src/web-components/register.ts vite.web-components.config.ts package.json .gitignore
git commit -m "feat(wc): pilot — defineCustomElement entry for Button/Input/FormItem

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push origin master
```

---

## Task 2: React pilot workspace scaffold + raw custom-element render

**Files:**
- Create: `react-pilot/package.json`, `react-pilot/vite.config.ts`, `react-pilot/tsconfig.json`
- Create: `react-pilot/src/App.tsx`, `react-pilot/index.html`, `react-pilot/src/main.tsx`
- Modify: `.gitignore` (add `react-pilot/node_modules`, `react-pilot/dist`)

**Interfaces:**
- Consumes: `dist-wc/tvu-web-components.js` (Task 1).
- Produces: a runnable React app (`pnpm --dir react-pilot dev`) that imports `registerTvuElements` and renders a raw `<tvu-button>` in JSX. Establishes that React 19 can mount the custom element at all.

- [ ] **Step 1: Scaffold the isolated workspace**

`react-pilot/package.json`:
```json
{
  "name": "tvu-react-pilot",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest run"
  },
  "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" },
  "devDependencies": {
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "@vitejs/plugin-react": "^4.3.0",
    "@testing-library/react": "^16.0.0",
    "@testing-library/user-event": "^14.5.0",
    "jsdom": "^24.0.0",
    "typescript": "^5.4.0",
    "vite": "^5.0.0",
    "vitest": "^1.6.0"
  }
}
```

- [ ] **Step 2: Add the gitignore entries and install**

Append to `.gitignore`:
```
react-pilot/node_modules
react-pilot/dist
```
Run: `cd react-pilot && pnpm install` (isolated; not a workspace member — it has its own lockfile). Expected: exit 0.

- [ ] **Step 3: Add vite config aliasing the WC bundle**

`react-pilot/vite.config.ts`:
```ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: { '@tvu/wc': fileURLToPath(new URL('../dist-wc/tvu-web-components.js', import.meta.url)) },
  },
  test: { environment: 'jsdom', globals: true },
})
```

- [ ] **Step 4: Add a minimal app that renders the raw element**

`react-pilot/src/App.tsx`:
```tsx
import '@tvu/wc' // side-effect: registerTvuElements() runs

export default function App() {
  return (
    <div>
      {/* @ts-expect-error custom element not yet in JSX.IntrinsicElements */}
      <tvu-button data-figma-color="green" data-figma-size="M">Click me</tvu-button>
    </div>
  )
}
```
`react-pilot/src/main.tsx`:
```tsx
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
```
`react-pilot/index.html`: standard Vite React index pointing at `/src/main.tsx`.

- [ ] **Step 5: Run dev and confirm it renders**

Run: `cd react-pilot && pnpm dev` then load the page; confirm a styled TVU button appears (manual or a quick Playwright screenshot). Expected: a real styled button, not an empty/unstyled box.
**Decision gate:** if the element renders but is unstyled, theming-in-React is the realized risk — record in findings.

- [ ] **Step 6: Commit**

```bash
git add react-pilot/package.json react-pilot/pnpm-lock.yaml react-pilot/vite.config.ts react-pilot/tsconfig.json react-pilot/index.html react-pilot/src/main.tsx react-pilot/src/App.tsx .gitignore
git commit -m "feat(react-pilot): isolated React 19 workspace renders raw tvu-button

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push origin master
```

---

## Task 3: React wrapper — Button (props + click event)

**Files:**
- Create: `react-pilot/src/wrappers/types.ts`
- Create: `react-pilot/src/wrappers/Button.tsx`
- Create: `react-pilot/src/wrappers/Button.test.tsx`

**Interfaces:**
- Consumes: `<tvu-button>` custom element; canonical Button props (from `src/canonical/ButtonBridge.vue`): `color, fixedWidth, icon, radius, size, status, style, theme` (all optional string unions) and a `click` event.
- Produces: `export function Button(props: ButtonProps)` rendering `<tvu-button>` with props mapped to the `data-figma-*` attributes the canonical component reads, and `onClick` wired to the element's `click`.

- [ ] **Step 1: Mirror the prop union types**

`react-pilot/src/wrappers/types.ts`:
```ts
// Mirrored verbatim from src/canonical/ButtonBridge.vue defineProps unions.
export type ButtonColor = 'gray 1' | 'gray 2' | 'green' | 'red' | 'orange' // confirm full set from source at impl time
export interface ButtonProps {
  color?: ButtonColor
  size?: 'S' | 'M' | 'L'
  status?: 'default' | 'hover' | 'disable'
  style?: 'filling' | 'stroke'
  radius?: 'square' | 'round'
  theme?: 'light' | 'dark'
  children?: React.ReactNode
  onClick?: (e: Event) => void
}
```
NOTE at impl time: open `src/canonical/ButtonBridge.vue` and the `Color`/`Size`/etc. type aliases it imports, and copy the EXACT union members — do not guess. This is a mechanical mirror, not invention.

- [ ] **Step 2: Write the failing test**

`react-pilot/src/wrappers/Button.test.tsx`:
```tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import '@tvu/wc'
import { Button } from './Button'

test('renders tvu-button with mapped attrs and fires onClick', async () => {
  const onClick = vi.fn()
  render(<Button color="green" size="M" onClick={onClick}>Go</Button>)
  const el = document.querySelector('tvu-button')!
  expect(el).toBeTruthy()
  expect(el.getAttribute('data-figma-color')).toBe('green')
  await userEvent.click(el as HTMLElement)
  expect(onClick).toHaveBeenCalledTimes(1)
})
```

- [ ] **Step 3: Run test to verify it fails**

Run: `cd react-pilot && pnpm test -- Button`
Expected: FAIL (`Button` not found / no module `./Button`).

- [ ] **Step 4: Write the wrapper**

`react-pilot/src/wrappers/Button.tsx`:
```tsx
import { useEffect, useRef } from 'react'
import type { ButtonProps } from './types'

export function Button({ color, size, status, style, radius, theme, onClick, children }: ButtonProps) {
  const ref = useRef<HTMLElement>(null)
  useEffect(() => {
    const el = ref.current
    if (!el || !onClick) return
    el.addEventListener('click', onClick)
    return () => el.removeEventListener('click', onClick)
  }, [onClick])
  return (
    // @ts-expect-error custom element
    <tvu-button
      ref={ref}
      data-figma-color={color}
      data-figma-size={size}
      data-figma-status={status}
      data-figma-style={style}
      data-figma-radius={radius}
      data-figma-theme={theme}
    >
      {children}
    {/* @ts-expect-error custom element */}
    </tvu-button>
  )
}
```
NOTE at impl time: confirm whether the canonical component reads `data-figma-*` attributes or expects real props (the SFC uses `defineProps` + emits `figmaAttrs` as host attributes). If the element reads PROPERTIES not attributes, set them imperatively in the effect (`el.color = color`) instead of as JSX attributes. Adjust and record which path worked.

- [ ] **Step 5: Run test to verify it passes**

Run: `cd react-pilot && pnpm test -- Button`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add react-pilot/src/wrappers/types.ts react-pilot/src/wrappers/Button.tsx react-pilot/src/wrappers/Button.test.tsx
git commit -m "feat(react-pilot): Button wrapper — props + click interop

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push origin master
```

---

## Task 4: React wrapper — Input (v-model → controlled value/onChange)

**Files:**
- Create: `react-pilot/src/wrappers/Input.tsx`
- Create: `react-pilot/src/wrappers/Input.test.tsx`
- Modify: `react-pilot/src/wrappers/types.ts` (add `InputProps`)

**Interfaces:**
- Consumes: `<tvu-input>`; canonical Input props (from `src/canonical/InputBoxLine.vue`): `modelValue, placeholder, darkTheme, status, enable, ux, size, feature, readonly`; emits `update:modelValue` (payload `string`).
- Produces: `export function Input(props: InputProps)` exposing a React-idiomatic controlled API: `value: string`, `onChange: (value: string) => void`, plus the remaining props.

- [ ] **Step 1: Add InputProps to types.ts**

```ts
export interface InputProps {
  value?: string
  onChange?: (value: string) => void
  placeholder?: string
  size?: 'M' | 'L' | 'XL'
  ux?: 'default' | 'error' | 'click' | 'hover'
  darkTheme?: 'on' | 'off'
  readonly?: boolean
}
```

- [ ] **Step 2: Write the failing test**

`react-pilot/src/wrappers/Input.test.tsx`:
```tsx
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import '@tvu/wc'
import { Input } from './Input'

test('maps value down and update:modelValue up to onChange', async () => {
  const onChange = vi.fn()
  render(<Input value="hello" onChange={onChange} placeholder="type" />)
  const el = document.querySelector('tvu-input') as any
  expect(el).toBeTruthy()
  // simulate the Vue element emitting its update event
  el.dispatchEvent(new CustomEvent('update:modelValue', { detail: ['world'] }))
  expect(onChange).toHaveBeenCalledWith('world')
})
```
NOTE at impl time: Vue custom elements dispatch emits as native CustomEvents whose name is the emit name; the payload arrives in `event.detail` (array of emit args). Confirm the exact event name (`update:modelValue` vs lowercased) and detail shape from a live probe, and align both wrapper and test to reality.

- [ ] **Step 3: Run test to verify it fails**

Run: `cd react-pilot && pnpm test -- Input`
Expected: FAIL (no `./Input`).

- [ ] **Step 4: Write the wrapper**

`react-pilot/src/wrappers/Input.tsx`:
```tsx
import { useEffect, useRef } from 'react'
import type { InputProps } from './types'

export function Input({ value = '', onChange, placeholder, size, ux, darkTheme, readonly }: InputProps) {
  const ref = useRef<HTMLElement>(null)
  // Push controlled value down as a property (objects/strings → property, not attr)
  useEffect(() => { if (ref.current) (ref.current as any).modelValue = value }, [value])
  // Listen for the Vue emit and surface it as onChange
  useEffect(() => {
    const el = ref.current
    if (!el || !onChange) return
    const handler = (e: Event) => onChange((e as CustomEvent).detail?.[0] ?? '')
    el.addEventListener('update:modelValue', handler)
    return () => el.removeEventListener('update:modelValue', handler)
  }, [onChange])
  return (
    // @ts-expect-error custom element
    <tvu-input
      ref={ref}
      placeholder={placeholder}
      data-figma-size={size}
      data-figma-ux={ux}
      data-figma-dark-theme={darkTheme}
      {...(readonly ? { readonly: '' } : {})}
    />
  )
}
```
NOTE at impl time: whether `size`/`ux` are read as `data-figma-*` attrs or as element properties depends on Task 3's finding — use the same mechanism that worked for Button.

- [ ] **Step 5: Run test to verify it passes**

Run: `cd react-pilot && pnpm test -- Input`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add react-pilot/src/wrappers/Input.tsx react-pilot/src/wrappers/Input.test.tsx react-pilot/src/wrappers/types.ts
git commit -m "feat(react-pilot): Input wrapper — v-model <-> controlled value/onChange

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push origin master
```

---

## Task 5: React wrapper — FormItem (slots → children/composition)

**Files:**
- Create: `react-pilot/src/wrappers/FormItem.tsx`
- Create: `react-pilot/src/wrappers/FormItem.test.tsx`
- Modify: `react-pilot/src/wrappers/types.ts` (add `FormItemProps`)

**Interfaces:**
- Consumes: `<tvu-form-item>`; canonical FormItem props (from `src/canonical/FormItem.vue`): `label, required, error, hint, labelWidth, layout, status, theme, type`; named slot `label`; default slot (composition — wraps a real field component).
- Produces: `export function FormItem(props: FormItemProps)` accepting `children` (default slot) and optional `label` content; the React `<Input>` wrapper must compose inside it.

- [ ] **Step 1: Add FormItemProps to types.ts**

```ts
export interface FormItemProps {
  label?: string
  required?: boolean
  error?: string
  hint?: string
  labelWidth?: '120 px' | '200 px' | 'Dynamic'
  layout?: '1 line' | '1 line & Right' | '2 lines'
  status?: 'Error' | 'Normal'
  theme?: 'Dark' | 'Light'
  type?: 'Label & Input' | 'Label & Selector' | 'Label & Radio' | 'Label & checkbox' | 'Label & Switch' | 'Label & Textarea'
  children?: React.ReactNode
}
```

- [ ] **Step 2: Write the failing test (default slot via light-DOM child)**

`react-pilot/src/wrappers/FormItem.test.tsx`:
```tsx
import { render } from '@testing-library/react'
import '@tvu/wc'
import { FormItem } from './FormItem'
import { Input } from './Input'

test('renders label and composes a child Input into the default slot', () => {
  render(
    <FormItem label="Email" status="Normal" theme="Light">
      <Input value="a@b.c" placeholder="email" />
    </FormItem>
  )
  const fi = document.querySelector('tvu-form-item')!
  expect(fi.getAttribute('data-figma-label')).toBe('Email')
  // the composed Input must exist as a slotted child
  expect(fi.querySelector('tvu-input')).toBeTruthy()
})
```
NOTE at impl time: Vue custom elements render shadow DOM with `<slot>`; light-DOM children projected into the default slot work only if the SFC's `<slot>` is preserved under `customElement: true`. Confirm projection works; if the SFC's default slot has a fallback (it does — FormItem renders a default field when no slot content), verify an explicit child overrides it. If projection fails, this is FormItem's route-A blocker — record and flag for route-B fallback.

- [ ] **Step 3: Run test to verify it fails**

Run: `cd react-pilot && pnpm test -- FormItem`
Expected: FAIL (no `./FormItem`).

- [ ] **Step 4: Write the wrapper**

`react-pilot/src/wrappers/FormItem.tsx`:
```tsx
import type { FormItemProps } from './types'

export function FormItem({ label, required, error, hint, labelWidth, layout, status, theme, type, children }: FormItemProps) {
  return (
    // @ts-expect-error custom element
    <tvu-form-item
      data-figma-label={label}
      data-figma-label-width={labelWidth}
      data-figma-layout={layout}
      data-figma-status={status}
      data-figma-theme={theme}
      data-figma-type={type}
      {...(required ? { required: '' } : {})}
    >
      {children}
    {/* @ts-expect-error custom element */}
    </tvu-form-item>
  )
}
```
NOTE at impl time: `label`/`hint`/`error` are string props the SFC reads — confirm attribute vs property path (same finding as Task 3). The `children` go into the default slot.

- [ ] **Step 5: Run test to verify it passes**

Run: `cd react-pilot && pnpm test -- FormItem`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add react-pilot/src/wrappers/FormItem.tsx react-pilot/src/wrappers/FormItem.test.tsx react-pilot/src/wrappers/types.ts
git commit -m "feat(react-pilot): FormItem wrapper — slot/composition interop

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push origin master
```

---

## Task 6: React render-verification — getComputedStyle vs Figma expected

**Files:**
- Create: `react-pilot/harness/main.tsx`, `react-pilot/harness/RenderHarness.tsx`, `react-pilot/harness/index.html`
- Create: `tests/render-verification-react/react-drift.spec.ts`
- Create: `playwright.render-verification-react.config.ts`

**Interfaces:**
- Consumes: the existing `figma-data/render-verification-manifest.json` (filtered to `codeComponent ∈ {Button, InputBoxLine, FormItem}`), the existing Figma expected-value loader (the same one `tests/visual-verify/manifest-verifier.spec.ts` / `audit-render-drift-gate.mjs` use), and the React wrappers (Tasks 3-5).
- Produces: a per-variant PASS/FAIL report for the React renders, on the same 5% tolerance and same selectors (`rootTargetSelector`) the Vue side uses.

- [ ] **Step 1: Identify the manifest subset and the existing expected-value comparison**

Run: `node -e "const m=require('./figma-data/render-verification-manifest.json'); const a=Object.values(m); console.log(a.filter(e=>['Button','InputBoxLine','FormItem'].includes(e.codeComponent)).map(e=>e.manifestId))"`
Expected: a non-empty list of manifestIds for the 3 components. Read `tests/visual-verify/manifest-verifier.spec.ts` and `scripts/audit-render-drift-gate.mjs` to reuse their expected-value loading + comparison logic verbatim (do not reinvent the tolerance math — Global Constraint: deterministic, same口径).

- [ ] **Step 2: Build the React harness that renders a manifest entry by id**

`react-pilot/harness/RenderHarness.tsx` reads `?manifestId=` from the URL, maps `codeComponent`+`codeProps` to the matching React wrapper (Button/Input/FormItem), applies `harnessStyle`, and mounts a single isolated instance with `rootTargetSelector` reachable. Mirror the structure of `playground/docs/pages/RenderHarnessPage.vue` (Vue side) so selectors line up.

- [ ] **Step 3: Write the Playwright spec (failing — harness not served yet)**

`tests/render-verification-react/react-drift.spec.ts`: for each manifestId in the subset, navigate to the served React harness, read `getComputedStyle` of `rootTargetSelector` (and `textTargetSelector`), and compare against the Figma expected values using the reused comparator. Assert all within tolerance.

- [ ] **Step 4: Run it to confirm it fails (harness/route not wired)**

Run: `cd react-pilot && pnpm build` then `pnpm exec playwright test --config=../playwright.render-verification-react.config.ts` from repo root.
Expected: FAIL (cannot reach harness / mismatch).

- [ ] **Step 5: Wire the static-served harness and make the spec pass**

Configure `playwright.render-verification-react.config.ts` `webServer` to serve the built React harness (`react-pilot/dist`). Iterate wrapper/harness until each of the 3 components' variants is within tolerance.
**Decision gate:** any component that cannot reach tolerance after fixing genuine wrapper bugs (not by loosening tolerance — forbidden) is a route-A fidelity failure → record per-variant actual-vs-expected in findings, mark that component for route-B.

- [ ] **Step 6: Run to verify pass**

Run: `pnpm exec playwright test --config=playwright.render-verification-react.config.ts`
Expected: PASS for all in-scope variants (or documented, owner-surfaced failures routed to B).

- [ ] **Step 7: Commit**

```bash
git add react-pilot/harness tests/render-verification-react playwright.render-verification-react.config.ts
git commit -m "test(react-pilot): render-verification — React getComputedStyle vs Figma expected

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push origin master
```

---

## Task 7: Dual-framework showcase page (Vue / React side-by-side)

**Files:**
- Create: `playground/docs/pages/DualFrameworkPilotPage.vue`
- Modify: `playground/docs/navigation.ts` (add a nav entry for the pilot page)

**Interfaces:**
- Consumes: existing canonical Vue components (left column) and the built React demo (`react-pilot/dist`, right column via `<iframe>`).
- Produces: a `/dual-framework-pilot` (or nav-registered) docs page showing Button/Input/FormItem rendered in both frameworks for visual comparison.

- [ ] **Step 1: Build a React demo page**

In `react-pilot/src/App.tsx`, render the 3 wrappers with representative props (one per the manifest's primary variant), so the built `react-pilot/dist/index.html` is a self-contained demo.

- [ ] **Step 2: Add the Vue showcase page that iframes it**

`playground/docs/pages/DualFrameworkPilotPage.vue`: left column renders the canonical Vue `<Button>`/`<Input>`/`<FormItem>` with the same props; right column is an `<iframe src="/react-pilot-demo/">` (served from `react-pilot/dist`, wired in `vite.playground.config.ts` static-copy or a dev proxy — additive, do not alter existing config behavior).

- [ ] **Step 3: Register nav and verify it loads**

Add the page to `playground/docs/navigation.ts`. Run: `pnpm build:playground` (existing script) and confirm exit 0 and the page builds. Manually open and confirm both columns render the 3 components.

- [ ] **Step 4: Commit**

```bash
git add playground/docs/pages/DualFrameworkPilotPage.vue playground/docs/navigation.ts react-pilot/src/App.tsx
git commit -m "feat(playground): dual-framework pilot showcase (Vue | React)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push origin master
```

---

## Task 8: Findings report + per-component decision + status update

**Files:**
- Create: `docs/internal/retrospection/dual-framework-react-pilot-findings-2026-06-29.md`
- Modify: `docs/STATUS.md` (Wrap-up entry + Last updated), `docs/superpowers/specs/2026-06-29-dual-framework-react-pilot-design.md` (status → executed)

**Interfaces:**
- Consumes: the build/test/render-verification outcomes from Tasks 1-7.
- Produces: an owner-facing decision document.

- [ ] **Step 1: Write the findings report**

Cover, per component (Button / Input / FormItem): did it pass all 4 gate conditions? Which custom-element quirks surfaced (attr-vs-property, shadow-DOM theming reality, slot projection, v-model event shape)? Concrete render-verification numbers. Then the headline: **route A confirmed / route A with caveats / specific components → route B**. Include a corrected effort estimate for the full 26-component rollout (validating the spec's premise per the project's "baseline-before-plan" discipline).

- [ ] **Step 2: Verify every claim in the report against actual output**

For each "PASS"/"FAIL" assertion, point to the actual test run or render-verification number — no claim without evidence (project discipline: review result vs rationale; verification-before-completion).

- [ ] **Step 3: Update STATUS.md and the spec status**

Add a Wrap-up entry summarizing the pilot outcome; set `Last updated` to today; flip the spec's status line to executed with a pointer to the findings doc.

- [ ] **Step 4: Commit**

```bash
git add docs/internal/retrospection/dual-framework-react-pilot-findings-2026-06-29.md docs/STATUS.md docs/superpowers/specs/2026-06-29-dual-framework-react-pilot-design.md
git commit -m "docs(pilot): dual-framework React pilot findings + per-component route decision

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push origin master
```

---

## Self-Review

**1. Spec coverage:**
- Spec §2 (route A chosen, B fallback) → Tasks 1-6 build A; Tasks 5/6 decision gates route to B. ✓
- Spec §3.1 (3 components) → Tasks 3/4/5 (one each), chosen to hit the 3 risk axes. ✓
- Spec §3.2 (not-doing: no other components, no publish flow, no Figma/Vue-package changes) → Global Constraints + isolated `react-pilot/` + additive scripts. ✓
- Spec §4.1 units (WC entry / React wrappers / render-verification ext / showcase) → Tasks 1 / 3-5 / 6 / 7. ✓
- Spec §4.2 data flow (shared tokens, same gate both ends) → Task 1 token import, Task 6 reuses existing manifest+comparator. ✓
- Spec §5 gate (4 conditions) → Task 1 Step 5 (theming), Task 6 (fidelity), Tasks 3-5 (interop), Task 2/7 (importable/consumable). ✓
- Spec §6 deliverables (workspace / verification / showcase / findings) → Tasks 2-5 / 6 / 7 / 8. ✓
- Spec §7 risks → each has a Decision-gate step that surfaces rather than works around. ✓

**2. Placeholder scan:** No "TBD/implement later". Several "NOTE at impl time" blocks are explicit spike-resolution instructions with concrete probes/decisions (attr-vs-property, event shape, slot projection) — legitimate for a pilot, not vague hand-waving. The one genuine unknown (exact union members of Button's Color type) is handled by a mechanical "copy verbatim from source" instruction, not a guess baked into the plan.

**3. Type consistency:** `ButtonProps`/`InputProps`/`FormItemProps` defined in `types.ts` (Tasks 3/4/5), consumed by the matching wrappers; `registerTvuElements`/element names (`tvu-button`/`tvu-input`/`tvu-form-item`) consistent across Tasks 1-7; manifest `codeComponent` names (`Button`/`InputBoxLine`/`FormItem`) consistent in Task 6.
