# Prompt — CANONICAL-011: Chart canonical 实现（6 variants · Chart.js）

> **角色**：executor
> **范围**：实现 `Chart` canonical 组件（pie / donut / line / bar / bar-horizontal / line-bar 6 variants），覆盖 Figma Chart set (4949:7176)。落地 12 色 chart palette token。
>
> ⚠️ **本任务需要 commit**——所有产出是 source of truth。
> ⚠️ **不扩范围**：只改/创建 §0.1 文件清单；不动 figma-data/raw/（AGENTS.md 硬规则 #1）/ 不动其他 canonical 组件 / 不写 visual baseline 截图（F20 hook 会自动跟）/ 不动 retrospection。
> ⚠️ §0 所有 API 设计 / token / 库选 / 颜色 plan owner 已定裁定（D1 完成），**不要自行重设计**。
> ⚠️ 完成后按 §4 格式回报。

---

## §0 — Plan owner 已定裁定（D1 + D2）

### 背景

Figma 已发布 `Chart` set (nodeId 4949:7176, page "— — Chart")，6 variants: type=pie / donut / line / bar / bar-horizontal / line-bar。Code 端 `src/canonical/Chart.vue` 不存在。本任务 D1（库选 Chart.js + vue-chartjs）+ D2（API + token）已定，本 prompt 就是 D3 实现。

### §0.1 — 文件清单（共 10 个新/改）

| # | 文件 | 操作 |
|---|---|---|
| 1 | `package.json` | 改：加 `chart.js` `vue-chartjs` 到 devDeps + peerDeps |
| 2 | `vite.config.ts` | 改：build.rollupOptions.external 加 `chart.js` `vue-chartjs` |
| 3 | `src/tokens/variables.css` | 改：dark + light 两段各加 12 个 `--chart-color-1..12` |
| 4 | `src/components/Chart/Chart.vue` | 新建：base 组件，包 Chart.js Canvas |
| 5 | `src/components/Chart/use-chart-tokens.ts` | 新建：CSS-var resolver helper |
| 6 | `src/canonical/Chart.vue` | 新建：canonical wrapper（thin layer，挂 figma 属性） |
| 7 | `src/index.ts` | 改：export Chart + global registration |
| 8 | `playground/docs/navigation.ts` | 改：加 `'chart'` 到 `CanonicalPageId` union + 加 nav item |
| 9 | `playground/docs/DocsShell.vue` | 改：加 ChartPage loader（L98 区域）+ component map（L130 区域） |
| 10 | `playground/docs/pages/ChartPage.vue` | 新建：docs page（6 variants live demo + API table） |
| 11 | `playground/docs/figmaFirstRegistry.ts` | 改：加 Chart record |

**不要新建**：`figma-data/normalized/docs-figma-members/chart.ts`（Chart 只有 type 单轴，不适用 FigmaMembersGrid axes × variants 范式；ChartPage 直接 inline 6 live demo）

### §0.2 — Package 依赖装配

```bash
pnpm add -D chart.js vue-chartjs
```

`package.json`：
- `devDependencies`: `"chart.js": "^4.4.0"`, `"vue-chartjs": "^5.3.0"`
- 新增 `peerDependencies` 段（如已存在则补，不存在则新加在 devDependencies 旁）：

```json
"peerDependencies": {
  "vue": "^3.4.0",
  "chart.js": "^4.4.0",
  "vue-chartjs": "^5.3.0"
}
```

**为什么 peerDep**：避免 consumer 项目装 TVU 后重复打包 Chart.js（typically ~50KB 多余）；consumer `pnpm add chart.js vue-chartjs` 显式声明依赖。

`vite.config.ts` `build.rollupOptions.external`（如已有 `vue` 在 external 数组，追加；如无 external 配置则新加）：

```ts
build: {
  rollupOptions: {
    external: ['vue', 'chart.js', 'vue-chartjs', /^chart\.js\//],
    output: {
      globals: { vue: 'Vue', 'chart.js': 'Chart', 'vue-chartjs': 'VueChartJS' },
    },
  },
}
```

### §0.3 — `src/tokens/variables.css` 加 12 色 chart palette

**Dark theme 段**（找到 `[data-theme="dark"]` 或顶层 `:root` block，在末尾加）：

```css
/* Chart categorical palette (12 colors) — CANONICAL-011 D1
   4 reuse existing base hues; 8 new derived by HSL hue rotation
   maintaining S 65-78% / L 50-60% to match brand-level visual weight.
   Order matches Option C warm/cool alternating (1=cool brand → 12=warm). */
--chart-color-1: var(--brand);     /* 134° 绿 (brand) */
--chart-color-2: var(--red);       /* 6° 红 */
--chart-color-3: var(--blue);      /* 211° 蓝 */
--chart-color-4: var(--orange);    /* 31° 橙 */
--chart-color-5: #2dceae;          /* 169° 青绿 */
--chart-color-6: #d8c537;          /* 52° 黄 */
--chart-color-7: #9a4ee5;          /* 265° 紫 */
--chart-color-8: #e54eb4;          /* 320° 洋红 */
--chart-color-9: #4e6ee5;          /* 228° 靛蓝 */
--chart-color-10: #e57a4e;         /* 17° 珊瑚 */
--chart-color-11: #c47de8;         /* 293° lavender */
--chart-color-12: #a3d837;         /* 90° chartreuse */
```

**Light theme 段**（`[data-theme="light"]` block 末尾）：

```css
/* Chart palette · light theme variants */
--chart-color-1: var(--brand);     /* 299f45 light brand */
--chart-color-2: var(--red);       /* dc2717 */
--chart-color-3: var(--blue);      /* 0473d5 */
--chart-color-4: var(--orange);    /* e26601 */
--chart-color-5: #13a886;          /* 青绿 */
--chart-color-6: #b3a113;          /* 黄 */
--chart-color-7: #7a25c9;          /* 紫 */
--chart-color-8: #c52596;          /* 洋红 */
--chart-color-9: #2545c9;          /* 靛蓝 */
--chart-color-10: #c95425;         /* 珊瑚 */
--chart-color-11: #a83bd1;         /* lavender */
--chart-color-12: #7ab313;         /* chartreuse */
```

**重要**：
- 1-4 用 `var()` 引用现有 brand/red/blue/orange token（自动跟 theme switch）
- 5-12 是直接 hex 值（后续 BRIDGE-MOCKUP-005 设计师补完 figma 真源后从 sync pipeline 流回，code 端只换 `var()` 引用，consumer 0 改）

### §0.4 — `src/components/Chart/use-chart-tokens.ts`（CSS-var resolver）

```typescript
/**
 * Resolve --chart-color-1..12 from document.documentElement at runtime.
 * Returns hex array. Re-call on theme switch to get current theme values.
 */
export function resolveChartPalette(): string[] {
  if (typeof document === 'undefined') {
    // SSR-safe fallback (default dark theme hex values)
    return [
      '#2fb54e', '#ea4233', '#3892f3', '#f68512', '#2dceae', '#d8c537',
      '#9a4ee5', '#e54eb4', '#4e6ee5', '#e57a4e', '#c47de8', '#a3d837',
    ]
  }
  const style = getComputedStyle(document.documentElement)
  return Array.from({ length: 12 }, (_, i) =>
    style.getPropertyValue(`--chart-color-${i + 1}`).trim() || '#000000'
  )
}
```

### §0.5 — `src/components/Chart/Chart.vue`（base 组件）

Chart.js v4 + vue-chartjs v5 wrapper。完整文件：

```vue
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import {
  Chart as ChartJS,
  ArcElement,
  BarElement,
  LineElement,
  PointElement,
  CategoryScale,
  LinearScale,
  Tooltip,
  Legend,
} from 'chart.js'
import type { ChartOptions, ChartData } from 'chart.js'
import { Bar, Doughnut, Line, Pie } from 'vue-chartjs'
import { resolveChartPalette } from './use-chart-tokens'

ChartJS.register(
  ArcElement, BarElement, LineElement, PointElement,
  CategoryScale, LinearScale, Tooltip, Legend,
)

export type ChartType = 'pie' | 'donut' | 'line' | 'bar' | 'bar-horizontal' | 'line-bar'

export interface ChartDatasetInput {
  label: string
  data: number[]
  type?: 'line' | 'bar'   // for mixed (line-bar) chart per-dataset override
  color?: string          // optional explicit color; default = --chart-color-(index+1)
}

const props = withDefaults(defineProps<{
  type: ChartType
  datasets: ChartDatasetInput[]
  labels?: string[]
  height?: number
  width?: number
}>(), {
  labels: () => [],
  height: 200,
  width: 320,
})

const palette = ref<string[]>(resolveChartPalette())

// Re-resolve on theme switch
let observer: MutationObserver | null = null
onMounted(() => {
  observer = new MutationObserver(() => { palette.value = resolveChartPalette() })
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
})
onUnmounted(() => { observer?.disconnect() })

const resolvedDatasets = computed(() =>
  props.datasets.map((ds, i) => {
    const color = ds.color || palette.value[i % 12]
    const base = {
      label: ds.label,
      data: ds.data,
      backgroundColor: color,
      borderColor: color,
      borderWidth: 2,
    }
    if (ds.type) return { ...base, type: ds.type }
    return base
  })
)

// For pie/donut: backgroundColor must be an array (one per slice)
const pieDatasets = computed(() =>
  props.datasets.map((ds) => ({
    label: ds.label,
    data: ds.data,
    backgroundColor: ds.data.map((_, i) => ds.color || palette.value[i % 12]),
    borderColor: 'transparent',
    borderWidth: 0,
  }))
)

const chartData = computed<ChartData>(() => {
  if (props.type === 'pie' || props.type === 'donut') {
    return { labels: props.labels, datasets: pieDatasets.value as any }
  }
  return { labels: props.labels, datasets: resolvedDatasets.value as any }
})

const chartOptions = computed<ChartOptions>(() => ({
  responsive: false,
  maintainAspectRatio: false,
  plugins: { legend: { position: 'bottom' } },
  ...(props.type === 'bar-horizontal' ? { indexAxis: 'y' as const } : {}),
}))
</script>

<template>
  <div class="tvu-chart" :style="{ width: `${width}px`, height: `${height}px` }">
    <Pie v-if="type === 'pie'" :data="chartData" :options="chartOptions" />
    <Doughnut v-else-if="type === 'donut'" :data="chartData" :options="chartOptions" />
    <Line v-else-if="type === 'line'" :data="chartData" :options="chartOptions" />
    <Bar v-else :data="chartData" :options="chartOptions" />
  </div>
</template>

<style scoped>
.tvu-chart {
  position: relative;
  display: inline-block;
}
</style>
```

**注意**：
- Chart.js 全部 `register()` 在文件顶层（仅在 base 组件被 import 时执行；canonical wrapper 通过 `defineAsyncComponent` 包裹实现 lazy load）
- `type='bar-horizontal'` 用 Chart.js 原生 `<Bar>` + `options.indexAxis='y'`
- `type='line-bar'` 用 `<Bar>` 作 base，datasets[i].type 可个别覆盖为 `'line'`
- pie/donut 的 backgroundColor 必须是数组（每 slice 一个色），其他类型是单值

### §0.6 — `src/canonical/Chart.vue`（canonical wrapper）

```vue
<script setup lang="ts">
import { computed, defineAsyncComponent } from 'vue'
import type { ChartType, ChartDatasetInput } from '../components/Chart/Chart.vue'

const BaseChart = defineAsyncComponent(() => import('../components/Chart/Chart.vue'))

const props = withDefaults(defineProps<{
  type: ChartType
  datasets: ChartDatasetInput[]
  labels?: string[]
  height?: number
  width?: number
}>(), {
  labels: () => [],
  height: 200,
  width: 320,
})

const figmaAttrs = computed(() => ({
  'data-figma-component': 'Chart',
  'data-figma-type': props.type,
}))
</script>

<template>
  <BaseChart
    v-bind="figmaAttrs"
    :type="type"
    :datasets="datasets"
    :labels="labels"
    :height="height"
    :width="width"
  />
</template>
```

### §0.7 — 6 variants 映射表

| 我们 prop type | Chart.js 原生 | options 特殊 |
|---|---|---|
| `pie` | `pie` | — |
| `donut` | `doughnut` | — |
| `line` | `line` | — |
| `bar` | `bar` | — |
| `bar-horizontal` | `bar` | `indexAxis: 'y'` |
| `line-bar` | `bar` (base) | datasets[i].type='line' 覆盖部分 |

### §0.8 — `src/index.ts` export 加 Chart

找到 `import Progress from './canonical/Progress.vue'` 附近，照样加：
```ts
import Chart from './canonical/Chart.vue'
```
找到 `app.component('Progress', Progress)` 附近，加：
```ts
app.component('Chart', Chart)
```
找到 `export { Progress }` 段，加：
```ts
export { Chart }
```

### §0.9 — `playground/docs/navigation.ts` 加 chart

第 1-30 行 `CanonicalPageId` union 加 `| 'chart'`（建议加在 `'pagination'` 之后，与 figma "Chart" set 在分组里相邻）。

`navigationGroups` 数组找一个合适分组（"Data Display" 或 "Feedback" 都行；如果没明显分组，加到 "Data Display" 或最后一个含 Pagination/Progress 的 group），加 entry：

```ts
{
  id: 'chart',
  label: text('Chart', '图表'),
  title: text('Chart', '图表'),
  summary: text(
    '6 Figma variants (pie / donut / line / bar / bar-horizontal / line-bar) powered by Chart.js with TVU 12-color palette.',
    '6 个 Figma variant（pie / donut / line / bar / bar-horizontal / line-bar），基于 Chart.js + TVU 12 色 palette。',
  ),
},
```

### §0.10 — `playground/docs/DocsShell.vue` 注册

L98 区域 `pageLoaders` 对象加：
```ts
chart: () => import('./pages/ChartPage.vue'),
```

L130 区域 component map 加：
```ts
chart: createPageComponent(pageLoaders.chart),
```

（具体行号以当前文件为准，找到 `'popup-box'` 或 `progress` 的位置照样加）

### §0.11 — `playground/docs/figmaFirstRegistry.ts` 加 Chart record

```ts
{
  pageId: 'chart',
  exportNames: ['Chart'],
  pageFile: 'ChartPage.vue',
  requiredSourceMarkers: [],  // unused since F25.4 prep; kept for type compatibility
},
```

### §0.12 — `playground/docs/pages/ChartPage.vue` 结构

按 ProgressPage 的样式骨架，但 Chart 不用 FigmaMembersGrid。结构：

1. **Figma Coverage 段** — 文字描述 + figma 元数据（source=Chart · nodeId=4949:7176 · variant count=6）
2. **6 variants live demo 段** — 6 个 article card，每个跑一个 `<Chart type="..." :datasets="..." :labels="..." />` live demo + 配套 code snippet 显示
3. **Interactive playground 段**（可选）— 让 user 用 select 切 type，看同 dataset 在不同 variant 下渲染
4. **Token-driven palette 段** — 文字介绍 12 色 palette + 说明 `--chart-color-1..12` 命名规则 + 颜色 swatch grid (12 chips with hex labels)
5. **Development Usage 段** — `import Chart from '@/src/canonical/Chart.vue'` + 4-6 个 code snippet（每 variant 一个）
6. **Chart API 段** — props 表（type / datasets / labels / height / width） + Slots 表（无）+ Events 表（无）

完整 page 双语支持 `t(en, zh)` 同 ProgressPage 范式。

Sample datasets for demos（用真实有意义的 series 而不是 [1,2,3,4,5]，参考下面）：

```ts
const demoSalesByQuarter = {
  labels: ['Q1', 'Q2', 'Q3', 'Q4'],
  datasets: [{ label: 'Revenue', data: [120, 150, 180, 165] }],
}

const demoTrafficByChannel = {
  labels: ['Direct', 'Organic', 'Paid', 'Social', 'Referral'],
  datasets: [{ label: 'Visits', data: [30, 45, 15, 25, 10] }],
}

const demoMultiSeries = {
  labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
  datasets: [
    { label: 'Product A', data: [40, 55, 60, 50, 70, 80] },
    { label: 'Product B', data: [30, 40, 35, 45, 50, 60] },
    { label: 'Product C', data: [20, 25, 30, 35, 40, 45] },
  ],
}

const demoMixed = {
  labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
  datasets: [
    { label: 'Revenue', data: [100, 120, 90, 150, 130], type: 'bar' as const },
    { label: 'Trend', data: [105, 115, 108, 125, 130], type: 'line' as const },
  ],
}
```

每个 variant demo 用配套数据：
- `pie` / `donut`: demoTrafficByChannel（5 categories，pie 经典场景）
- `line` / `bar`: demoMultiSeries（3 series × 6 月，line/bar 经典）
- `bar-horizontal`: demoTrafficByChannel（横柱通常 ≤5 category）
- `line-bar`: demoMixed（line 趋势 + bar 实数，combo 经典）

---

## §1 — 必读输入

1. [`figma-data/raw/components/chart__4949_7176.json`](../../../figma-data/raw/components/chart__4949_7176.json) — Figma Chart set 真源（6 variants 名 + dimensions）
2. [`src/canonical/Progress.vue`](../../../src/canonical/Progress.vue) — canonical wrapper 范式参考（figma-attrs + 直接 v-bind base 组件）
3. [`playground/docs/pages/ProgressPage.vue`](../../../playground/docs/pages/ProgressPage.vue) — docs page 双语 + Section 结构范式（除 FigmaMembersGrid 段外都参考）
4. [`playground/docs/navigation.ts`](../../../playground/docs/navigation.ts) — `CanonicalPageId` union + `navigationGroups` 加 entry 位置
5. [`playground/docs/DocsShell.vue`](../../../playground/docs/DocsShell.vue) — `pageLoaders` + component map 范式
6. [`src/tokens/variables.css`](../../../src/tokens/variables.css) — token 文件 dark/light 两 block 结构（L264 是 `[data-theme="light"]` 起始行）
7. [`vite.config.ts`](../../../vite.config.ts) — build config 当前 external 设置
8. [`src/index.ts`](../../../src/index.ts) — export + install 范式
9. [`playground/docs/figmaFirstRegistry.ts`](../../../playground/docs/figmaFirstRegistry.ts) — registry 现有 entries（按 pageId 字母序还是按 figma 分组都行，跟现有顺序一致即可）

---

## §2 — 任务清单（按顺序）

### 任务 2.1 — 装依赖

```bash
pnpm add -D chart.js vue-chartjs
```

确认 `package.json` 出现这两条 devDeps；按 §0.2 加 `peerDependencies` 段（或追加 vue 同段）。

### 任务 2.2 — 改 vite.config.ts

按 §0.2 加 external。如 vite config 已 `external: ['vue']`，扩成 `['vue', 'chart.js', 'vue-chartjs', /^chart\.js\//]`。

### 任务 2.3 — 改 variables.css 加 24 个 chart-color token

按 §0.3 在 dark + light 两段各加 12 行。位置：每段已有 token 列表末尾追加（保持 alphabetical 或 logical 段落 — 现有规则不严格，照 §0.3 段落格式贴进去即可）。

### 任务 2.4 — 写 use-chart-tokens.ts

按 §0.4 全文创建。

### 任务 2.5 — 写 src/components/Chart/Chart.vue

按 §0.5 全文创建（base 组件）。

### 任务 2.6 — 写 src/canonical/Chart.vue

按 §0.6 全文创建（canonical wrapper）。

### 任务 2.7 — 改 src/index.ts

按 §0.8 加 import + register + export。

### 任务 2.8 — 改 navigation.ts

按 §0.9 改 CanonicalPageId union + navigationGroups。

### 任务 2.9 — 改 DocsShell.vue

按 §0.10 在两处加 chart。

### 任务 2.10 — 改 figmaFirstRegistry.ts

按 §0.11 加 record。

### 任务 2.11 — 写 ChartPage.vue

按 §0.12 全文创建，结构 6 段。每 variant demo 用配套样本 data。

### 任务 2.12 — 跑回归

```bash
pnpm test
```
预期：105 passed + 1 skipped。如新加测试需要这里也加。

```bash
pnpm build
```
预期：build 通过（chart.js/vue-chartjs 在 external，不进 dist bundle）。

```bash
pnpm dev
```
预期：开浏览器 `http://localhost:5173/chart` 看 6 variants live demo 渲染正常；切 dark/light theme，颜色跟随。

### 任务 2.13 — 视觉 baseline 更新

```bash
pnpm test:visual:update
```

ChartPage 是新 page，F20 baseline 需 capture。生成的 `chart-dark.png` + `chart-light.png` 加入 commit。

### 任务 2.14 — commit

```bash
git add package.json pnpm-lock.yaml vite.config.ts \
        src/tokens/variables.css \
        src/components/Chart/ \
        src/canonical/Chart.vue \
        src/index.ts \
        playground/docs/ \
        tests/visual/__screenshots__/chart-*.png
git commit -m "feat(CANONICAL-011): Chart canonical (6 variants · Chart.js + 12-color palette)"
```

**注意**：commit 自身会触发 F21 hook（vue-tsc + vitest + figma-data block）。如 hook fail 修真问题再 commit，不要 --no-verify。

---

## §3 — 验收清单

- [ ] `chart.js@^4.4.0` + `vue-chartjs@^5.3.0` 在 devDeps + peerDeps
- [ ] vite external 含 `chart.js` `vue-chartjs`
- [ ] `--chart-color-1..12` 在 variables.css dark + light 段各 12 行（4 reuse var 引用 + 8 直接 hex）
- [ ] `src/components/Chart/Chart.vue` 实现 6 variant 渲染（pie/donut/line/bar/bar-horizontal/line-bar）
- [ ] `src/canonical/Chart.vue` 用 `defineAsyncComponent` lazy load base
- [ ] CSS-var 切 theme 自动 reload palette（MutationObserver 监听 data-theme attr）
- [ ] ChartPage 6 section（Figma Coverage / 6 variants demo / token palette / dev usage / API）
- [ ] DocsShell 注册 chart 路由
- [ ] navigationGroups 出现 chart entry
- [ ] figmaFirstRegistry 出现 chart record
- [ ] src/index.ts export + register Chart
- [ ] `pnpm test` 105+ passed
- [ ] `pnpm build` ✅ 无错
- [ ] `pnpm dev` 浏览器 `/chart` 6 variants 都 live 渲染正常
- [ ] dark↔light 切换，chart 颜色自动跟换
- [ ] `pnpm test:visual:update` 生成 chart-dark.png + chart-light.png baseline
- [ ] commit 自身通过 F21 hook（vue-tsc + vitest + 无 figma-data write）
- [ ] **不动**：figma-data/raw/ / figma-data/normalized/ / 其他 canonical 组件 / retrospection / backlog.md

---

## §4 — 完成报告

```
## CANONICAL-011 Chart 实现 完成报告

### 依赖
- chart.js: [version]
- vue-chartjs: [version]
- peerDeps 段：✅ / ❌
- vite external：✅ chart.js + vue-chartjs

### Token
- variables.css dark 段：+12 行 (--chart-color-1..12) ✅
- variables.css light 段：+12 行 ✅
- 4 reuse var()：✅（brand/red/blue/orange）
- 8 直接 hex：✅

### 组件
- src/components/Chart/Chart.vue：6 variant 支持 ✅
- src/components/Chart/use-chart-tokens.ts：✅
- src/canonical/Chart.vue：defineAsyncComponent lazy ✅
- 6 variant → Chart.js 类型映射：pie/donut/line/bar/bar-h(indexAxis y)/line-bar(mixed) ✅

### Theme 跟随
- MutationObserver on data-theme：✅
- dark↔light palette 切换实测：✅

### Docs
- ChartPage.vue 6 section：✅
- 双语 t(en, zh)：✅
- 6 variant live demo 样本数据：[列样本]
- 12 色 token swatch grid：✅

### Wiring
- src/index.ts：import + register + export ✅
- navigation.ts CanonicalPageId + group entry：✅
- DocsShell.vue pageLoaders + component map：✅
- figmaFirstRegistry.ts entry：✅

### 回归
- pnpm test：[X passed / Y failed]
- pnpm build：✅ / 失败
- pnpm dev `/chart` 6 variants 渲染：✅
- pnpm test:visual:update：chart-dark.png + chart-light.png 已生成 ✅

### commit hash
[hash] (本 commit 自动触发 F21 hook：[pass / 卡哪步])

### 未解决项 / blocker
- [无 / 描述]

### 范围澄清确认
- 没动 figma-data/raw/：✅
- 没动 其他 canonical 组件：✅
- 没动 retrospection / backlog.md：✅

DONE — CANONICAL-011 落地。BRIDGE-MOCKUP-004 tracker 子项 2/2 完成（PopupBox + Chart 都 ✅）；audit:published-vs-code 应可升 strict（待 plan owner 单独操作）。
```
