# INFRA-F46 Pilot Implementation Plan — docs 全局 Vue↔React 框架开关（React islands）

> **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:** 在 Vue docs 外壳加全局 framework 开关，拨到 React 时 4 个 pilot 组件页（Button/PopupBox/Table/Chart）的 demo 区内联渲染 full-parity React 版本（React islands），验证 bridge + full-parity + parity-audit 端到端跑通，再 fan-out 剩余组件。

**Architecture:** Vue 页 demo 区用 `<FrameworkDemoRegion>` 包裹；framework==='vue' 渲染既有 Vue 示例 slot，==='react' 时 `<ReactIsland>` 懒挂载对应 React demo 模块（`react-pilot/src/demos/*.tsx`）。demo 模块是唯一 React demo 真源，被独立 react-pilot gallery（App.tsx）与 docs islands 双消费。主题/双语靠共享 `document` + props 下发；overlay teleport 到真 `document.body`。

**Tech Stack:** Vue 3.5 + Vite（docs 构建 `vite.playground.config.ts`）、React 19 + react-dom/client（懒加载 island）、`@vitejs/plugin-react`、TypeScript compiler API + `@vue/compiler-sfc`（确定性 audit）、vitest、playwright。

## Global Constraints

- **F45 audit 连续性（硬红线）**：`scripts/audit-demo-slot-boolean-coverage.mjs`（AGENTS 硬规则 #8，L4 blocking pre-commit gate）在任何 commit 前必须 `pnpm audit:demo-slot-boolean` exit 0。它当前把 `react-pilot/src/App.tsx` 当 React demo 面解析（`REACT_APP` 常量，TS AST）——拆 App.tsx 必须同步更新它并保 `tests/audit-demo-slot-boolean.test.ts` 7 测试绿。
- **React 只进 docs devDependencies**：react/react-dom/@vitejs/plugin-react 加到根 `package.json` 的 **devDependencies**；npm 发布包经 `src/index.ts` 走 `vite build`（主配置）产 Vue-only dist，不含 React。禁止让 React 进入发布产物。
- **React runtime 懒 chunk**：`import('react-dom/client')` 与 demo 模块必须动态 import，确保默认 Vue-only 访问零 React 开销。
- **一份源两消费者**：React demo 内容只写在 `react-pilot/src/demos/*.tsx`；App.tsx 与 docs islands 都消费它，禁止复制第三份。
- **确定性验证**：所有 audit 用 AST 解析（TS compiler API / @vue/compiler-sfc），禁脆弱 regex（对齐 FIGMA_AS_SOURCE_OF_TRUTH.md §验证方法 + F45 范式）。
- **双语**：React demo 标签走 `t(en, zh)`（同 `DualFrameworkPilotPage.vue` 范式）；parity audit 以英文 section 标题为比对锚。
- **主题**：island 接 `theme: 'dark'|'light'`，wrapper 内部按 `theme==='dark'?'on':'off'` 派生（同 App.tsx 现有写法）；single theme only（no side-by-side，memory `feedback_no-side-by-side-theme`）。
- **Commit 纪律**：executor 不自 commit（memory `feedback_executor-no-self-commit`）；plan owner 复审 diff + owner ack 后由 owner 直 commit master + 三端 push；React demo 视觉改动 commit 需 `VISUAL_COMMIT_APPROVED=1`；commit 用 `git commit -F`。
- **图标**：任何图标从 `dist/icons/svg/` 取或用既有 `<Icon>`，禁内联自画 SVG（memory `feedback_tvu-icons-mandatory`）。

---

## File Structure

**Create:**
- `playground/docs/useDocsFramework.ts` — framework 状态 composable（state + localStorage 持久化）。
- `playground/docs/components/ReactIsland.vue` — React demo 模块挂载桥。
- `playground/docs/components/FrameworkDemoRegion.vue` — demo 区框架切换包裹器（Vue slot ↔ ReactIsland）。
- `react-pilot/src/demos/types.ts` — 共享 `DemoProps` 接口。
- `react-pilot/src/demos/Button.tsx` / `PopupBox.tsx` / `Table.tsx` / `Chart.tsx` — 4 个 full-parity demo 模块。
- `scripts/audit-demo-framework-parity.mjs` — parity audit。
- `tests/useDocsFramework.test.ts` / `tests/react-island.test.ts` / `tests/framework-demo-region.test.ts` / `tests/audit-demo-framework-parity.test.ts` — 单测。

**Modify:**
- `package.json`（根）— React devDeps + `audit:demo-framework-parity` script。
- `vite.playground.config.ts` — `@vitejs/plugin-react` + `@demos` alias。
- `playground/docs/DocsShell.vue` — 用 composable、provide `docsFramework`、toolbar 开关、foundation 页禁用。
- `react-pilot/src/App.tsx` — import + 渲染 4 个 demo 模块。
- `scripts/audit-demo-slot-boolean-coverage.mjs` — React 面解析扩展到 `demos/*.tsx`（F45 连续性）。
- `tests/audit-demo-slot-boolean.test.ts` — 保绿 + 覆盖 demo 解析。
- `playground/docs/pages/{Button,PopupBox,Table,Chart}Page.vue` — demo 区包 `<FrameworkDemoRegion>`。

---

## Task 1: 构建基建 — React 进 docs 构建

**Files:**
- Modify: `package.json`（根，devDependencies + scripts）
- Modify: `vite.playground.config.ts`

**Interfaces:**
- Produces: docs 构建可解析 TSX + 经 `@demos` alias import `react-pilot/src/demos/*`；根有 `react`/`react-dom`/`@vitejs/plugin-react`。

- [ ] **Step 1: 加 React devDeps**

在根 `package.json` 的 `devDependencies` 加（版本对齐 react-pilot）：

```json
"@vitejs/plugin-react": "^4.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
```

- [ ] **Step 2: 安装**

Run: `pnpm install`
Expected: 安装成功，`node_modules/.pnpm` 出现 react@19。

- [ ] **Step 3: vite.playground.config.ts 加 react plugin + alias**

改 `vite.playground.config.ts`：顶部加 `import react from '@vitejs/plugin-react'`；plugins 数组加 `react()`（放 `vue()` 之后）；`resolve.alias` 加：

```ts
'@demos': fileURLToPath(new URL('./react-pilot/src/demos', import.meta.url)),
'@tvu/wc': fileURLToPath(new URL('./dist-wc/tvu-web-components.js', import.meta.url)),
```

> `@vitejs/plugin-react` 只对 `.tsx`/`.jsx` 生效；`.vue` 仍走 `vue()`。两插件共存无冲突。

- [ ] **Step 4: 确保 dist-wc 存在（demo 模块运行前置）**

Run: `pnpm build:wc`
Expected: 生成 `dist-wc/tvu-web-components.js`（后续 island demo 的 CE 来源）。

- [ ] **Step 5: 验证 docs 构建不回归**

Run: `pnpm exec vue-tsc --noEmit && pnpm build:playground`
Expected: 两者成功（此时无 demo 模块，plugin-react 空转无害）。

- [ ] **Step 6: 交 plan owner 复审（不自 commit）**

报告 `git diff --stat` + 上述命令 stdout。

---

## Task 2: useDocsFramework composable

**Files:**
- Create: `playground/docs/useDocsFramework.ts`
- Test: `tests/useDocsFramework.test.ts`

**Interfaces:**
- Produces:
  - `export type DocsFramework = 'vue' | 'react'`
  - `export function useDocsFramework(): { framework: Ref<DocsFramework>; setFramework(f: DocsFramework): void; toggleFramework(): void }`
  - localStorage key `'tvu-docs-framework'`；默认 `'vue'`。

- [ ] **Step 1: 写失败测试**

```ts
// tests/useDocsFramework.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useDocsFramework } from '../playground/docs/useDocsFramework'

describe('useDocsFramework', () => {
  beforeEach(() => { window.localStorage.clear() })

  it('defaults to vue when storage empty', () => {
    const { framework } = useDocsFramework()
    expect(framework.value).toBe('vue')
  })

  it('reads persisted framework', () => {
    window.localStorage.setItem('tvu-docs-framework', 'react')
    const { framework } = useDocsFramework()
    expect(framework.value).toBe('react')
  })

  it('setFramework persists', () => {
    const { framework, setFramework } = useDocsFramework()
    setFramework('react')
    expect(framework.value).toBe('react')
    expect(window.localStorage.getItem('tvu-docs-framework')).toBe('react')
  })

  it('toggleFramework flips vue<->react', () => {
    const { framework, toggleFramework } = useDocsFramework()
    toggleFramework()
    expect(framework.value).toBe('react')
    toggleFramework()
    expect(framework.value).toBe('vue')
  })
})
```

- [ ] **Step 2: 跑测试确认失败**

Run: `pnpm vitest run tests/useDocsFramework.test.ts`
Expected: FAIL（模块不存在）。

- [ ] **Step 3: 实现**

```ts
// playground/docs/useDocsFramework.ts
import { ref, type Ref } from 'vue'

export type DocsFramework = 'vue' | 'react'
const STORAGE_KEY = 'tvu-docs-framework'

function readInitial(): DocsFramework {
  if (typeof window === 'undefined') return 'vue'
  return window.localStorage.getItem(STORAGE_KEY) === 'react' ? 'react' : 'vue'
}

export function useDocsFramework(): {
  framework: Ref<DocsFramework>
  setFramework: (f: DocsFramework) => void
  toggleFramework: () => void
} {
  const framework = ref<DocsFramework>(readInitial())
  function setFramework(f: DocsFramework) {
    framework.value = f
    if (typeof window !== 'undefined') window.localStorage.setItem(STORAGE_KEY, f)
  }
  function toggleFramework() {
    setFramework(framework.value === 'vue' ? 'react' : 'vue')
  }
  return { framework, setFramework, toggleFramework }
}
```

- [ ] **Step 4: 跑测试确认通过**

Run: `pnpm vitest run tests/useDocsFramework.test.ts`
Expected: PASS（4 测试）。

- [ ] **Step 5: 交 plan owner 复审**

---

## Task 3: ReactIsland.vue 挂载桥

**Files:**
- Create: `playground/docs/components/ReactIsland.vue`
- Test: `tests/react-island.test.ts`

**Interfaces:**
- Consumes: `react-dom/client`（动态 import）。
- Produces: 组件 props `{ loader: () => Promise<{ default: DemoComponent }>; theme: 'dark'|'light'; locale: 'en-US'|'zh-CN' }`；`DemoComponent = (p:{theme,locale}) => any`（React element）。渲染进内部 host `<div class="react-island">`。

- [ ] **Step 1: 写失败测试**

```ts
// tests/react-island.test.ts
import { describe, it, expect } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createElement } from 'react'
import ReactIsland from '../playground/docs/components/ReactIsland.vue'

const loader = () => Promise.resolve({
  default: (p: { theme: string; locale: string }) =>
    createElement('span', { 'data-testid': 'demo' }, `${p.theme}/${p.locale}`),
})

describe('ReactIsland', () => {
  it('mounts a react demo module into the host and passes theme/locale', async () => {
    const wrapper = mount(ReactIsland, { props: { loader, theme: 'dark', locale: 'en-US' } })
    await flushPromises()
    await new Promise((r) => setTimeout(r, 0))
    expect(wrapper.find('.react-island').exists()).toBe(true)
    expect(wrapper.element.textContent).toContain('dark/en-US')
  })

  it('re-renders when theme prop changes', async () => {
    const wrapper = mount(ReactIsland, { props: { loader, theme: 'dark', locale: 'en-US' } })
    await flushPromises(); await new Promise((r) => setTimeout(r, 0))
    await wrapper.setProps({ theme: 'light' })
    await new Promise((r) => setTimeout(r, 0))
    expect(wrapper.element.textContent).toContain('light/en-US')
  })
})
```

- [ ] **Step 2: 跑测试确认失败**

Run: `pnpm vitest run tests/react-island.test.ts`
Expected: FAIL（组件不存在）。

- [ ] **Step 3: 实现 ReactIsland.vue**

```vue
<!-- playground/docs/components/ReactIsland.vue -->
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'

type DemoComponent = (p: { theme: 'dark' | 'light'; locale: 'en-US' | 'zh-CN' }) => unknown
const props = defineProps<{
  loader: () => Promise<{ default: DemoComponent }>
  theme: 'dark' | 'light'
  locale: 'en-US' | 'zh-CN'
}>()

const host = ref<HTMLElement | null>(null)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let root: any = null
let Demo: DemoComponent | null = null
let createElement: typeof import('react')['createElement'] | null = null

async function ensureMounted() {
  if (!host.value) return
  if (!root) {
    const [{ createRoot }, react, mod] = await Promise.all([
      import('react-dom/client'),
      import('react'),
      props.loader(),
    ])
    createElement = react.createElement
    Demo = mod.default
    root = createRoot(host.value)
  }
  render()
}

function render() {
  if (root && Demo && createElement) {
    root.render(createElement(Demo, { theme: props.theme, locale: props.locale }))
  }
}

onMounted(ensureMounted)
watch(() => [props.theme, props.locale], render)
onBeforeUnmount(() => { if (root) { root.unmount(); root = null } })
</script>

<template>
  <div ref="host" class="react-island" />
</template>
```

- [ ] **Step 4: 跑测试确认通过**

Run: `pnpm vitest run tests/react-island.test.ts`
Expected: PASS（2 测试）。若 `@vue/test-utils` 未装，用 `pnpm add -D @vue/test-utils` 后重跑（先 `grep '"@vue/test-utils"' package.json` 确认，多数已装）。

- [ ] **Step 5: 交 plan owner 复审**

---

## Task 4: FrameworkDemoRegion.vue 切换包裹器

**Files:**
- Create: `playground/docs/components/FrameworkDemoRegion.vue`
- Test: `tests/framework-demo-region.test.ts`

**Interfaces:**
- Consumes: `inject('docsFramework')`（Task 5 provide）、`inject('docsTheme')`、`ReactIsland`。
- Produces: props `{ loader: () => Promise<{ default: any }> }`；framework==='vue' 渲染默认 slot；==='react' 渲染 `<ReactIsland :loader :theme :locale>`。locale 经 prop `locale` 传入。

- [ ] **Step 1: 写失败测试**

```ts
// tests/framework-demo-region.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { ref, readonly, h } from 'vue'
import FrameworkDemoRegion from '../playground/docs/components/FrameworkDemoRegion.vue'

function mountWith(framework: 'vue' | 'react') {
  return mount(FrameworkDemoRegion, {
    props: { loader: () => Promise.resolve({ default: () => null }), locale: 'en-US' as const },
    slots: { default: () => h('div', { class: 'vue-demo' }, 'vue side') },
    global: {
      provide: {
        docsFramework: readonly(ref(framework)),
        docsTheme: readonly(ref('dark')),
      },
      stubs: { ReactIsland: { template: '<div class="react-island-stub" />' } },
    },
  })
}

describe('FrameworkDemoRegion', () => {
  it('renders the Vue slot when framework is vue', () => {
    const w = mountWith('vue')
    expect(w.find('.vue-demo').exists()).toBe(true)
    expect(w.find('.react-island-stub').exists()).toBe(false)
  })
  it('renders ReactIsland when framework is react', () => {
    const w = mountWith('react')
    expect(w.find('.vue-demo').exists()).toBe(false)
    expect(w.find('.react-island-stub').exists()).toBe(true)
  })
})
```

- [ ] **Step 2: 跑测试确认失败**

Run: `pnpm vitest run tests/framework-demo-region.test.ts`
Expected: FAIL（组件不存在）。

- [ ] **Step 3: 实现**

```vue
<!-- playground/docs/components/FrameworkDemoRegion.vue -->
<script setup lang="ts">
import { computed, inject, ref, type Ref } from 'vue'
import ReactIsland from './ReactIsland.vue'
import type { DocsFramework } from '../useDocsFramework'

const props = defineProps<{
  loader: () => Promise<{ default: any }>
  locale: 'en-US' | 'zh-CN'
}>()

const framework = inject<Readonly<Ref<DocsFramework>>>('docsFramework', ref('vue'))
const docsTheme = inject<Readonly<Ref<'dark' | 'light'>>>('docsTheme', ref('dark'))
const isReact = computed(() => framework.value === 'react')
</script>

<template>
  <ReactIsland v-if="isReact" :loader="props.loader" :theme="docsTheme" :locale="props.locale" />
  <slot v-else />
</template>
```

- [ ] **Step 4: 跑测试确认通过**

Run: `pnpm vitest run tests/framework-demo-region.test.ts`
Expected: PASS（2 测试）。

- [ ] **Step 5: 交 plan owner 复审**

---

## Task 5: DocsShell — framework 开关 + provide + foundation 禁用

**Files:**
- Modify: `playground/docs/DocsShell.vue`

**Interfaces:**
- Consumes: `useDocsFramework`（Task 2）。
- Produces: `provide('docsFramework', readonly(framework))`；toolbar 一个 framework 开关按钮；`isFoundationPage` computed 控制开关禁用。

- [ ] **Step 1: script 引入 composable + provide**

在 `<script setup>` import 区加 `import { useDocsFramework } from './useDocsFramework'`；在 `provide('docsTheme', ...)`（L26）附近加：

```ts
const { framework, toggleFramework } = useDocsFramework()
provide('docsFramework', readonly(framework))
const isFoundationPage = computed(() => foundationPages.has(currentPage.value))
const frameworkToggleLabel = computed(() =>
  framework.value === 'vue' ? 'React' : 'Vue'
)
```

> `foundationPages` 已在 L152 定义；`readonly`/`computed` 已 import（L2）。

- [ ] **Step 2: watch framework 同步 data 属性（可选调试锚）**

在 `watch(isDark, ...)`（L502）附近加：

```ts
watch(framework, (value) => {
  if (typeof document !== 'undefined') {
    document.documentElement.setAttribute('data-framework', value)
  }
}, { immediate: true })
```

- [ ] **Step 3: template 加 framework 开关（并列 theme toggle）**

在 `<button class="canonical-toggle" ... @click="toggleTheme">`（L618）**之前**加：

```html
<button
  class="canonical-toggle framework-toggle"
  type="button"
  :disabled="isFoundationPage"
  :title="isFoundationPage ? 'Foundation pages are framework-agnostic' : `Switch demos to ${frameworkToggleLabel}`"
  @click="toggleFramework"
>
  <Icon name="code" :size="14" aria-hidden="true" />
  {{ framework === 'vue' ? 'Vue' : 'React' }}
</button>
```

> `Icon` 已 import（L4）。若 `code` 图标不在 registry，先 `ls dist/icons/svg/ | grep -iE 'code|bracket'` 选真实存在的名（memory `feedback_tvu-icons-mandatory`）。

- [ ] **Step 4: 加禁用态样式**

在 `<style>` 段（若 scoped 在此文件）或对应 css 加：`.framework-toggle:disabled { opacity: 0.4; cursor: not-allowed; }`。若 DocsShell 样式在 `docs.css`，加到那里。

- [ ] **Step 5: 验证构建 + 既有页测试不回归**

Run: `pnpm exec vue-tsc --noEmit && pnpm vitest run tests/ButtonPage.test.ts && pnpm build:playground`
Expected: 全绿。

- [ ] **Step 6: 交 plan owner 复审**（此时开关可拨，但 pilot 页尚未接 island → 拨 react 无变化，正常）

---

## Task 6: Button demo 模块 + App.tsx 重构 + F45 audit 连续性

**Files:**
- Create: `react-pilot/src/demos/types.ts`
- Create: `react-pilot/src/demos/Button.tsx`
- Modify: `react-pilot/src/App.tsx`
- Modify: `scripts/audit-demo-slot-boolean-coverage.mjs`
- Modify: `tests/audit-demo-slot-boolean.test.ts`

**Interfaces:**
- Produces: `export interface DemoProps { theme: 'dark'|'light'; locale: 'en-US'|'zh-CN' }`；`react-pilot/src/demos/Button.tsx` default export `(p: DemoProps) => JSX.Element`，渲染镜像 `ButtonPage.vue` 的示例 section（用 `<h2 className="docs-section__title">` 标题）。
- Consumes（audit）：`REACT_DEMOS_DIR = react-pilot/src/demos`。

- [ ] **Step 1: 共享类型**

```tsx
// react-pilot/src/demos/types.ts
export interface DemoProps {
  theme: 'dark' | 'light'
  locale: 'en-US' | 'zh-CN'
}
export function makeT(locale: 'en-US' | 'zh-CN') {
  return (en: string, zh: string) => (locale === 'zh-CN' ? zh : en)
}
```

- [ ] **Step 2: Button demo 模块（full parity 镜像 ButtonPage.vue）**

创建 `react-pilot/src/demos/Button.tsx`。**parity 契约**：镜像 `playground/docs/pages/ButtonPage.vue` 的**示例 section**（用同英文 section 标题）：
- `Figma M Coverage`（[ButtonPage.vue:542](../../../playground/docs/pages/ButtonPage.vue#L542)）— 颜色×variant×radius×size 变体网格。
- `Development Usage`（[:574](../../../playground/docs/pages/ButtonPage.vue#L574)）— 复制粘贴用法示例。
- loading review 各 row 标题（[:617](../../../playground/docs/pages/ButtonPage.vue#L617)）— loading 状态组。
> **不镜像** `Button API`（[:726](../../../playground/docs/pages/ButtonPage.vue#L726)）——API 表是 shell 级 Vue、框架无关，留在 FrameworkDemoRegion 之外（§7.3 增强项不在 pilot）。

模块骨架（section 标题必须与 Vue 页英文一致，供 parity audit 比对）：

```tsx
// react-pilot/src/demos/Button.tsx
import '@tvu/wc' // side-effect: register CE
import { Button } from '../wrappers/Button'
import type { DemoProps } from './types'
import { makeT } from './types'

export default function ButtonDemo({ theme, locale }: DemoProps) {
  const t = makeT(locale)
  return (
    <div className="docs-page">
      <section className="docs-section">
        <h2 className="docs-section__title">{t('Figma M Coverage', 'Figma M Coverage Figma M 变体覆盖')}</h2>
        {/* 镜像 ButtonPage 的颜色×variant×radius×size 变体网格 */}
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
          <Button color="green" variant="filling" size="M" radius="round" status="default">Save</Button>
          {/* … 逐一映射 ButtonPage 变体，port 全部 M 覆盖组合 … */}
        </div>
      </section>
      <section className="docs-section">
        <h2 className="docs-section__title">{t('Development Usage', 'Development Usage 开发引用')}</h2>
        {/* 镜像 ButtonPage Development Usage 示例 */}
      </section>
      <section className="docs-section">
        <h2 className="docs-section__title">{t('Loading States', 'Loading States 加载态')}</h2>
        {/* 镜像 loading review 组 */}
      </section>
    </div>
  )
}
```

> **实现要求**：逐一 port ButtonPage 示例 section 里的每个 Button 实例（保 color/variant/size/radius/status 组合一致），使两侧视觉可对比。section 标题英文文本必须与 Vue 页 `t()` 第一参一致（Task 7 audit 据此比对）。

- [ ] **Step 3: App.tsx 消费 demo 模块（保 F45 面）**

改 `react-pilot/src/App.tsx`：把 `Form controls` Section 里的 Button 卡片替换为渲染 `<ButtonDemo theme={theme} locale="en-US" />`，顶部 `import ButtonDemo from './demos/Button'`。**关键**：Button 的 slot 投影 + boolean live 切换（F45 要求）必须存在于 `demos/Button.tsx`（因 audit 将解析 demos）。Button 无 boolean prop（见 F45 spec §2.1 表），只需 slot 投影（`<Button>真内容</Button>`）——Button demo 已满足。

- [ ] **Step 4: 更新 F45 audit 解析 demos/**

改 `scripts/audit-demo-slot-boolean-coverage.mjs`：React 面来源从"仅 App.tsx"扩展为"App.tsx + `react-pilot/src/demos/*.tsx` 合并解析"。加常量与合并逻辑：

```js
import { readdirSync } from 'node:fs' // 已 import（L7）
const REACT_DEMOS_DIR = resolve(__dirname, '..', 'react-pilot', 'src', 'demos')
function reactSources() {
  const files = [REACT_APP]
  if (existsSync(REACT_DEMOS_DIR)) {
    for (const f of readdirSync(REACT_DEMOS_DIR)) {
      if (f.endsWith('.tsx')) files.push(resolve(REACT_DEMOS_DIR, f))
    }
  }
  return files
}
```

把原来解析单个 `REACT_APP` 的逻辑改为遍历 `reactSources()`，对每个文件建 SourceFile 并**合并** used-tags / slot / boolean-live 判定（"任一 React 源满足即 React 侧达标"）。保持既有判定语义不变，只是数据源从 1 个文件变多个。

- [ ] **Step 5: 保 F45 测试绿 + 覆盖 demo 解析**

改 `tests/audit-demo-slot-boolean.test.ts`：现有 7 测试保绿；加 1 测试断言"解析 React 面包含 demos/*.tsx"（如：Button slot 投影从 `demos/Button.tsx` 而非 App.tsx 也判为达标）。

Run: `pnpm vitest run tests/audit-demo-slot-boolean.test.ts`
Expected: PASS（8 测试）。

- [ ] **Step 6: 跑 F45 blocking audit（硬红线）**

Run: `pnpm audit:demo-slot-boolean`
Expected: exit 0（全 PASS）。**若非 0，STOP 修到绿再继续——这是不可回归的 L4 gate。**

- [ ] **Step 7: react-pilot 独立构建不破**

Run: `cd react-pilot && pnpm build && cd ..`
Expected: 成功，gallery 仍含 Button demo。

- [ ] **Step 8: 交 plan owner 复审**（report diff + audit stdout + build 结果）

---

## Task 7: parity audit 脚本（report-only）

**Files:**
- Create: `scripts/audit-demo-framework-parity.mjs`
- Create: `tests/audit-demo-framework-parity.test.ts`
- Modify: `package.json`（script）

**Interfaces:**
- Produces:
  - `export function collectVueRegionTitles(sfcSource: string): string[]` — 提取 `<FrameworkDemoRegion>` 内所有 `.docs-section__title` 的英文文本（`t('en','zh')` 取 en；纯文本取原文）。
  - `export function collectReactTitles(tsxSource: string): string[]` — 提取 demo 模块内 `<h2 className="docs-section__title">` 的英文文本（`t('en','zh')` 取 en）。
  - `main()` — 逐 pilot 组件比对两侧 title 集，report-only（打印 PASS/FAIL 表，exit 0）。

- [ ] **Step 1: 写失败测试**

```ts
// tests/audit-demo-framework-parity.test.ts
import { describe, it, expect } from 'vitest'
import { collectVueRegionTitles, collectReactTitles } from '../scripts/audit-demo-framework-parity.mjs'

const vue = `
<template>
  <FrameworkDemoRegion :loader="l" locale="en-US">
    <section><h2 class="docs-section__title">{{ t('Alpha', '甲') }}</h2></section>
    <section><h2 class="docs-section__title">{{ t('Beta', '乙') }}</h2></section>
  </FrameworkDemoRegion>
  <section><h2 class="docs-section__title">{{ t('API', 'API') }}</h2></section>
</template>`

const react = `
export default function D() {
  return (<div>
    <h2 className="docs-section__title">{t('Alpha','甲')}</h2>
    <h2 className="docs-section__title">{t('Beta','乙')}</h2>
  </div>)
}`

describe('demo-framework-parity extractors', () => {
  it('vue extractor takes only titles inside FrameworkDemoRegion (excludes API)', () => {
    expect(collectVueRegionTitles(vue).sort()).toEqual(['Alpha', 'Beta'])
  })
  it('react extractor takes docs-section__title english text', () => {
    expect(collectReactTitles(react).sort()).toEqual(['Alpha', 'Beta'])
  })
})
```

- [ ] **Step 2: 跑测试确认失败**

Run: `pnpm vitest run tests/audit-demo-framework-parity.test.ts`
Expected: FAIL（脚本不存在）。

- [ ] **Step 3: 实现 audit（AST 解析）**

创建 `scripts/audit-demo-framework-parity.mjs`：
- Vue：`@vue/compiler-sfc` parse → 遍历 template AST，定位 `FrameworkDemoRegion` 元素节点，收集其**子树内** class 含 `docs-section__title` 的元素文本；文本为 `t('en','zh')` 调用时取第一参字符串字面量，否则取静态文本 trim。
- React：`typescript` parse TSX → 遍历 JSX，收集 `className="docs-section__title"` 的 `<h2>` 文本（`t('en','zh')` 取第一参）。
- `main()`：pilot 映射表 `{ button: {vue:'pages/ButtonPage.vue', react:'demos/Button.tsx'}, ... }`，逐组件比对 set，打印逐组件 PASS/FAIL + 缺口（`react-missing:<title>` / `vue-missing:<title>`）。**report-only：始终 exit 0**（顶部 `const REPORT_ONLY = true`）。
- 复用 F45 audit 的 `@vue/compiler-sfc` / `typescript` import 范式。

- [ ] **Step 4: 跑测试确认通过**

Run: `pnpm vitest run tests/audit-demo-framework-parity.test.ts`
Expected: PASS（2 测试）。

- [ ] **Step 5: package.json 加 script**

在 `scripts` 加 `"audit:demo-framework-parity": "node scripts/audit-demo-framework-parity.mjs"`。

- [ ] **Step 6: 交 plan owner 复审**

---

## Task 8: ButtonPage island 接入 + 首个垂直切片验证

**Files:**
- Modify: `playground/docs/pages/ButtonPage.vue`

**Interfaces:**
- Consumes: `FrameworkDemoRegion`（Task 4）、`@demos/Button`（Task 6）。

- [ ] **Step 1: 引入 FrameworkDemoRegion 包 demo 区**

在 `ButtonPage.vue` `<script setup>` import `FrameworkDemoRegion from '../components/FrameworkDemoRegion.vue'`；template 里把 3 个示例 section（`Figma M Coverage` / `Development Usage` / loading review，即 [:542](../../../playground/docs/pages/ButtonPage.vue#L542)~loading 段）用 `<FrameworkDemoRegion :loader="() => import('@demos/Button')" :locale="locale">…</FrameworkDemoRegion>` 包裹。**API section（[:726](../../../playground/docs/pages/ButtonPage.vue#L726)）留在包裹器外**。

> `locale` 是页 prop（DocsShell 传 `:locale`，见 [DocsShell.vue:653](../../../playground/docs/DocsShell.vue#L653)）。

- [ ] **Step 2: vue-tsc + 构建**

Run: `pnpm exec vue-tsc --noEmit && pnpm build:playground`
Expected: 成功。

- [ ] **Step 3: 跑 parity audit（Button 应 PASS）**

Run: `pnpm audit:demo-framework-parity`
Expected: Button 行 PASS（两侧 section 标题集一致）。若 FAIL，按缺口补齐 `demos/Button.tsx` 或核对标题文本，回到 Task 6 Step 2。

- [ ] **Step 4: playwright 截图核实（双框架 × 双主题）**

写临时 playwright 脚本放项目内（memory `reference_local-env-tool-quirks`：@playwright/test 脚本放项目内），`pnpm build:playground` 后 `pnpm exec vite preview --outDir playground-dist` 起站，截 ButtonPage：`#/component/button` × {Vue开关, React开关} × {dark, light} 共 4 张。
Expected: React 态 demo 区渲染出 Button 变体网格；主题跟随；无空白/报错。用 `ls -la` 核实 4 张 png 真实存在（memory `feedback_verify-subagent-artifacts-exist`）。

- [ ] **Step 5: 交 plan owner 复审**（附 4 张截图路径 + audit stdout）

---

## Task 9: PopupBox（demo 模块 + App.tsx + 页接入 + overlay 证据）

> **islands 胜 iframe 的关键证据点**：验证 island 内 PopupBox teleport 到真 `document.body`、模态居中整个 docs 视口（非困在小盒）。

**Files:**
- Create: `react-pilot/src/demos/PopupBox.tsx`
- Modify: `react-pilot/src/App.tsx`、`playground/docs/pages/PopupBoxPage.vue`

**Interfaces:**
- Consumes: `../wrappers/PopupBox`、`DemoProps`。PopupBox boolean toggle = `closable`（F45 spec §2.1），slot = default + `footer`。

- [ ] **Step 1: 创建 `demos/PopupBox.tsx`**

镜像 `PopupBoxPage.vue` 的示例 section（英文标题一致）。复用 App.tsx 现有 PopupBox 写法（[App.tsx:335-372](../../../react-pilot/src/App.tsx#L335-L372)）——含 `closable` 的 boolean live 切换（CheckBox 驱动 useState）+ default slot 投影 FormItem 表单 + footer。必须含 F45 要求的 slot 投影 + boolean live。

- [ ] **Step 2: App.tsx 消费**

`import PopupBoxDemo from './demos/PopupBox'`；把 Overlay Section 的 PopupBox 卡片 + 底部 `<PopupBox>` 替换为 `<PopupBoxDemo theme={theme} locale="en-US" />`。

- [ ] **Step 3: F45 audit 硬红线**

Run: `pnpm audit:demo-slot-boolean`
Expected: exit 0（PopupBox 的 slot + boolean-live 现从 `demos/PopupBox.tsx` 判定）。非 0 则 STOP 修。

- [ ] **Step 4: PopupBoxPage.vue 接入**

demo 区包 `<FrameworkDemoRegion :loader="() => import('@demos/PopupBox')" :locale="locale">`，API 段外置。

- [ ] **Step 5: parity + 构建 + playwright（overlay 证据）**

Run: `pnpm audit:demo-framework-parity && pnpm exec vue-tsc --noEmit && pnpm build:playground`
截图：React 态点开 PopupBox，核实模态**居中整个视口 + backdrop 覆盖全页**（非局促小盒）。这是 islands 架构的验收证据。

- [ ] **Step 6: 交 plan owner 复审**

---

## Task 10: Table（demo 模块 + App.tsx + 页接入）

**Files:**
- Create: `react-pilot/src/demos/Table.tsx`
- Modify: `react-pilot/src/App.tsx`、`playground/docs/pages/TablePage.vue`

**Interfaces:**
- Consumes: `../wrappers/Table`、`DemoProps`。boolean toggle = `striped`（F45 spec §2.1）。

- [ ] **Step 1: `demos/Table.tsx`** — 镜像 `TablePage.vue` 示例 section（英文标题一致）；含 `striped` boolean live 切换（复用 [App.tsx:320-324](../../../react-pilot/src/App.tsx#L320-L324) 写法）+ columns/data。
- [ ] **Step 2: App.tsx** — `import TableDemo`，替换 Data Section 的 Table 卡片为 `<TableDemo theme locale/>`。
- [ ] **Step 3: F45 硬红线** — `pnpm audit:demo-slot-boolean` exit 0。
- [ ] **Step 4: TablePage.vue** — demo 区包 `<FrameworkDemoRegion :loader="() => import('@demos/Table')" :locale>`，API 外置。
- [ ] **Step 5: parity + 构建 + playwright** — `pnpm audit:demo-framework-parity && pnpm build:playground`；截 Table 双框架×双主题。
- [ ] **Step 6: 交 plan owner 复审**

---

## Task 11: Chart（demo 模块 + App.tsx + 页接入）

**Files:**
- Create: `react-pilot/src/demos/Chart.tsx`
- Modify: `react-pilot/src/App.tsx`、`playground/docs/pages/ChartPage.vue`

**Interfaces:**
- Consumes: `../wrappers/Chart`、`DemoProps`。Chart 无 slot/无 boolean（F45 spec §2.1 排除表含 Chart）→ 无 F45 义务，只需 full-parity 示例。

- [ ] **Step 1: `demos/Chart.tsx`** — 镜像 `ChartPage.vue` 示例 section（英文标题一致）；port 6 chart type（pie/donut/line/bar/bar-horizontal/line-bar，复用 [App.tsx:326-331](../../../react-pilot/src/App.tsx#L326-L331) + 数据 [:82-92](../../../react-pilot/src/App.tsx#L82-L92)）。
- [ ] **Step 2: App.tsx** — `import ChartDemo`，替换 Data Section 的 Chart 卡片。
- [ ] **Step 3: F45 硬红线**（Chart 非 in-scope，确认不误纳）— `pnpm audit:demo-slot-boolean` exit 0。
- [ ] **Step 4: ChartPage.vue** — demo 区包 FrameworkDemoRegion（`@demos/Chart`）。
- [ ] **Step 5: parity + 构建 + playwright** — 尤其核实 ECharts SVG 在 island 内正常渲染（无 canvas/SVG 尺寸塌陷）。
- [ ] **Step 6: 交 plan owner 复审**

---

## Task 12: Foundation no-op 确认 + 最终验证 + STATUS/backlog + 交付

**Files:**
- Modify: `docs/STATUS.md`、`docs/internal/backlog.md`（INFRA-F46 记 pilot shipped、fan-out 待做）

- [ ] **Step 1: Foundation no-op 确认**

起 preview，进 `#/component/color`，核实 framework 开关**禁用**（Task 5 Step 3 的 `:disabled`）、拨不动、Color 页内容不变。截 1 图存证。

- [ ] **Step 2: parity audit 转 blocking（pilot 4 组件全绿后）**

确认 4 组件 parity 全 PASS 后，把 `audit-demo-framework-parity.mjs` 的 `REPORT_ONLY` 改 false（FAIL→exit 1），并挂 pre-commit 条件 gate（staged 命中 `playground/docs/pages/*.vue` / `react-pilot/src/demos/*.tsx` / audit 自身时触发，照 F45 hook 范式）。

Run: `pnpm audit:demo-framework-parity`
Expected: exit 0（4 组件全 PASS）。

- [ ] **Step 3: 全 gates 扫一遍**

Run: 
```
pnpm exec vue-tsc --noEmit
pnpm vitest run
pnpm audit:demo-slot-boolean
pnpm audit:demo-framework-parity
pnpm build:playground
cd react-pilot && pnpm build && cd ..
```
Expected: 全绿（vitest 含所有新测试；F45 硬红线绿；react-pilot gallery 不破）。

- [ ] **Step 4: sprint 收尾 self-audit D/E/F/G**

Run: `pnpm audit:self-audit-phase2`（含 canonical-compliance / render-coverage / figma-library-vs-canonical / hard-rule-compliance）
Expected: exit 0。

- [ ] **Step 5: 更新 STATUS.md + backlog INFRA-F46**

- STATUS：Last updated 改今天 + 摘要（F46 pilot shipped：bridge/toggle/build 基建 + Button/PopupBox/Table/Chart 4 组件 full-parity React islands + parity audit blocking；fan-out 剩 26 组件为下一步）。旧摘要 prepend 进 STATUS-CHANGELOG.md。
- backlog：INFRA-F46 更新为"pilot 已 shipped，fan-out 剩余 ~26 组件（另开 Plan v2，派并行 subagent）"。

- [ ] **Step 6: 交 owner 验收（不自 commit）**

汇总：全 gates stdout + 所有 playwright 截图路径（ls 核实存在）+ diff stat。**owner ack 后**由 owner commit（`git commit -F` + 三端 push；React demo 视觉改动 `VISUAL_COMMIT_APPROVED=1`）。

- [ ] **Step 7: 给下一步（Plan v2 fan-out）起手 prompt**（memory `feedback_give-startup-prompt-directly`）

---

## Self-Review（plan owner 已跑）

**Spec coverage**：spec §3 bridge→Task 3；§3.3 一份源两消费者→Task 6 Step 3；§3.4 TOC 复用（同 markup）→Task 6 Step 2 契约 + Task 8;§4 toggle→Task 5;§5 parity audit→Task 7/12;§6 F45 连续性→Task 6 Step 4-6（硬红线）;§7.1 build→Task 1;§7.2 foundation no-op→Task 5+Task 12 Step 1;§7.3 API 表切换 pilot 不做→已在 Task 6/8 显式排除;§8 pilot 4 组件→Task 6/8/9/10/11;§9 gates→Task 12。无 spec 缺口。

**Placeholder scan**：demo 内容任务（6/9/10/11）以"镜像既有 Vue 页 + 指向精确行号 + 复用 App.tsx 现有写法"表达——是"port 指定既有 section"非 TBD；骨架代码真实可跑。audit/bridge/composite/config 均给完整代码。

**Type consistency**：`DemoProps {theme,locale}`（Task 6）贯穿 Task 3 ReactIsland props / Task 4 region / demo 模块;`DocsFramework`（Task 2）用于 Task 4/5;`collectVueRegionTitles`/`collectReactTitles`（Task 7）在测试与 main 一致;framework 值域 `'vue'|'react'` 全程一致。

**并行性**：Task 9/10/11 相互独立（不同组件、不同页），subagent-driven 可并行派（各自跑 F45 硬红线 + parity）；主线串行复审 diff + 合并前统一跑 Task 12 全 gates。
