# 上手页做成 docs 站内真页面 — 设计

> **状态**：owner 2026-08-13 拍板「按这个走」（四个决策点 + 代价一次性批准）。
> **来源缺口**：[`STATUS.md`](../../STATUS.md) §一.0（owner 2026-08-12 提出：「还是不容易找到」）。
> **轨道**：B（文档站 / 对外消费面）。**适用 [`AGENTS.md`](../../../AGENTS.md) §docs 站内部 demo 快车道**
> —— 改动集 ⊆ `playground/**` + `docs/**` + `tests/**`，不碰 `src/**` ⇒ 免 changeset、视觉批准合批、
> 不为它新建闸（现成的 `audit:docs-overflow` / `audit:layout-tokens` / `vue-tsc` / `pnpm test` 照跑）。

---

## 1. 病象与目标

**病象（owner 原话）**：两页上手内容现在只从 Overview 页中部的一张卡片链出去，左侧常驻导航里一条都没有；
点进去还会**离开 SPA** —— 顶栏、侧栏消失，主题不跟随，站内搜索搜不到，段落也没有可分享的锚点。

**目标**：让它「找得到」。进 `navigationGroups` 成为站内页 + 左侧「上手」组，
顺带拿到站内搜索（名字过滤面）/ 段落锚点 / 主题跟随 / CONTENTS 栏。

**⛔ 明确排除的做法**：iframe 套静态页（滚动、锚点、主题三重打架）。

---

## 2. 已实测的输入（数字都来自实跑，不是估）

| 事实 | 值 | 取法 |
|---|---|---|
| `playground/public/onboarding.html` | 1258 行 = CSS 347 + 内联 JS 225 + 正文 ~660 | `wc -l` + `<style>`/`<script>` 行界 |
| `playground/public/for-developers.html` | 641 行 = CSS 310 + JS 25 + 正文 ~280 | 同上 |
| 身份筛选覆盖面 | 17 个 `data-aud`：**16 个 `all`**，专属只有 `install-clone`=`maintain` / `install-plugin`=`use`。13 个 section 里切身份**只隐藏 1 个** | `grep -o 'data-aud="[^"]*"' \| sort \| uniq -c` |
| `for-developers.html` 有没有 chip | **没有** —— `<div class="chips"></div>` 是空的，它 3 个 `data-aud="code"` 是残留、零消费方 | 读源码 |
| 正文区标签平衡 | 404–1027 行区间内 `p/li/td/tr/code/strong/b/div/section/ul/ol/table/h2/h3/pre/span/a/em/small` **零失衡**；`{{` 0 处、`v-` 0 处 | 逐标签 open/close 计数 |
| 唯一的 `<b>` 不平衡 | 在 **`<script>` 里的注释**（1116 行 `对 <b> 一样成立`），不在迁移范围内 | `grep -n '<b[ >]'` |
| void 元素 | 4 个（3 `<hr class="sep">` + 1 `<br>`），未自闭合 —— Vue 模板编译器认 HTML void 标签，无需改写 | `grep -n -E '<br\|<hr'` |
| 内容类 CSS | onboarding 37 个 class、for-developers 22 个，其中**壳级 16 个**（topbar/topbar-in/brand/chips/chip/theme-toggle/toc/toc-title/shell/wrap/totop/masthead/masthead-in/eyebrow/standfirst/filter-banner + `fb-*`）由 shell 提供、**全丢**；逐条去留见 §4.3 | 正文区 `class="…"` 全集 |

---

## 3. 四个决策（owner 已拍）

### ① chip 身份筛选：删掉隐藏式筛选，保留分诊为直达入口

**依据**：实测它的全部效果 = 换一句报头副标题 + 隐藏 1/13 段 + 给一条跳转链接。
而站内页有两个机制与「隐藏段」直接冲突：`DocsShell.collectContentSections()` 从 DOM 现读
`.docs-page .docs-section__title` 建 CONTENTS，段锚点也按同一批标题派生 ⇒ 隐藏段要么从 CONTENTS 消失
（同事收到的链接与他看到的目录不一致），要么留在目录里指向 `display:none`。

**落地形态**：两条安装段都常驻，各自在**段标题里点名读者**（见 §5 映射表的 `Clone` / `Plugin` 两行）；
`Triage` 段内保留两个直达链接（普通 `<a href="#section-clone">` 式站内跳转，零状态、零 JS、零 `?view=`）。

**随之消失的**：`?view=` URL 参数 · `.filter-banner` 及 `fb-*` · 报头副标题按身份切换（`STANDFIRST` 表）·
`data-aud` 属性本身（无消费方后删除，不留死属性）。

### ② 路由前缀新加 `guide`，不复用 `component`

`getPagePath()` 现在对非隐藏页一律返回 `/component/<id>`，`#/component/onboarding` 会主动误导。

**要动的三处（都在 `playground/docs/navigation.ts`）**：
1. `getPagePath()` — 新分支：`GUIDE_PAGE_IDS` 命中 → `/guide/<id>`；
2. `getPageIdFromLocation()` — 加 `guide/<page>` 与 `guide/<page>/<section-slug>` 两段匹配（**顺序同 component：先两段后单段**，理由见该函数内注释）；
3. `SECTION_ANCHOR_PREFIXES` 加 `'guide'` —— **不加就拿不到段锚点**，而段锚点是本项「顺带免费拿到」里唯一不免费的一项。

### ③ 段标题两个 locale 用同一个字符串（英文 token 在前 + 中文），正文保持中文

**依据（实测逼出来的，不是偏好）**：`slugifySectionTitle()` 把非 `[a-z0-9]` 全替换，
纯中文标题 → **空串**，`DocsShell.vue:603` 兜底成位置式 `section-1/2/3…`。
位置 slug 一插段就整体位移、已发出的链接指错段 —— 正是 F90 注释里点名要避免的那件事。

**收益**：两 locale 同字符串 ⇒ **这一页的段链接跨 locale 也解析**（现有 33 页都做不到，见 F90 已声明的降级）。

**EN locale 的处理**：正文中文不变；页面首屏加一行英文说明（`This guide is Chinese-only for now.`），
仅在 `locale === 'en-US'` 时渲染。导航 `label` / `title` / `summary` 双语（走既有 `text(en, zh)`）。

### ④ 两页同批做

两页页脚互相引用。只搬一页 ⇒ 读者点一下就掉回静态页，主题/侧栏/搜索全丢，比现状更别扭。

---

## 4. 架构

### 4.1 文件清单

| 动作 | 文件 |
|---|---|
| 新增 | `playground/docs/pages/OnboardingPage.vue`（上手 · 跟 Claude 怎么说） |
| 新增 | `playground/docs/pages/ForDevelopersPage.vue`（写代码） |
| 改 | `playground/docs/navigation.ts` — `CanonicalPageId` union +2 · 新组「上手 / Getting started」置于 `Foundation` **之前** · `GUIDE_PAGE_IDS` · `getPagePath` · `getPageIdFromLocation` · `SECTION_ANCHOR_PREFIXES` |
| 改 | `playground/docs/DocsShell.vue` — lazy import 表 +2 行 |
| 改 | `playground/docs/pages/OverviewPage.vue` — 中部那张卡片改指站内路由（不再指 `.html`） |
| 重写为壳 | `playground/public/onboarding.html` · `playground/public/for-developers.html`（见 §6） |
| 改 | `tests/docs-section-anchor.test.ts` — 覆盖 `guide` 前缀 + 本页 16 个 slug 的期望值 |

### 4.2 页面组件形态（照现有页范式，不发明新的）

```vue
<script setup lang="ts">
const props = withDefaults(defineProps<{ locale?: string }>(), { locale: 'en-US' })
const isEn = computed(() => props.locale === 'en-US')
</script>
<template>
  <div class="docs-page">
    <p v-if="isEn" class="guide-en-note">This guide is Chinese-only for now.</p>
    <section class="docs-section">
      <h2 class="docs-section__title">Method · 这套系统怎么用（一句话）</h2>
      …正文近逐字搬运…
    </section>
  </div>
</template>
<style scoped>…只留内容语义类…</style>
```

**关键点**：`.docs-page` + `.docs-section` + `h2.docs-section__title` 是 shell 认的三个约定 ——
满足即自动拿到 CONTENTS 栏、段落锚点、`scrollMarginTop`、IntersectionObserver 高亮。
标题**不走 `t(en, zh)`**（决策 ③：两 locale 同字符串），正文段落同理不做双语分支。

### 4.3 CSS 移植边界

**留（内容语义，进 scoped style）**：`good` `note` `hint` `dim` `small` `say` `cmd` `cmt` `lbl` `rule` `rules`
`tree` `tscroll` `you` `you-tag` `path-eyebrow` `sep` `bigsteps`。
**丢（壳级，shell 已提供）**：`topbar` `topbar-in` `brand` `chips` `chip` `theme-toggle` `toc` `toc-title`
`shell` `wrap` `totop` `masthead` `masthead-in` `eyebrow` `standfirst` `filter-banner` `fb-*`。

⚠️ 移植时**照抄现有声明、不重新设计视觉** —— 本项是搬家，不是重设计。
`.tscroll`（表格横滚容器）尤其要原样留：它是窄档不溢出的现役手段。

---

## 5. 段落映射表（旧 id → 新段标题 → slug）

slug 均由 `slugifySectionTitle` 实跑得出，**16 个全唯一、无空串**。

### 5.1 `#/guide/onboarding`

| 旧 id | 新段标题（两 locale 同字符串） | slug |
|---|---|---|
| `method` | `Method · 这套系统怎么用（一句话）` | `method` |
| `triage` | `Triage · 你走哪条（两个问题）` | `triage` |
| `install-cc` | `Install · 先装 Claude Code` | `install-claude-code` |
| `install-clone` | `Clone · 装一份源码（维护设计系统的人看这段）` | `clone` |
| `install-plugin` | `Plugin · 两行命令装插件（只用它、不维护的人看这段）` | `plugin` |
| `design-browser` | `Claude Design · 用浏览器出稿` | `claude-design` |
| `first-words` | `First words · 第一次开口说什么` | `first-words` |
| `say-clearly` | `Say clearly · 需求怎么说清楚` | `say-clearly` |
| `correct-it` | `Correct · 它答偏了怎么纠` | `correct` |
| `accept-it` | `Accept · 怎么验收它给你的东西` | `accept` |
| `wrap-up` | `Wrap up · 收工怎么说` | `wrap-up` |
| `stuck` | `Stuck · 卡住了照贴这句` | `stuck` |
| `appendix-browse` | `Browse docs · 附一 · 想自己翻文档站` | `browse-docs` |

### 5.2 `#/guide/for-developers`

| 旧 id | 新段标题 | slug |
|---|---|---|
| `install-npm` | `Install npm · 装一个能 import 的组件库` | `install-npm-import` |
| `develop` | `Develop · 在产品里写代码用这些组件` | `develop` |
| `appendix` | `Appendix · 为什么是这种装法` | `appendix` |

> `install-npm-import` 略显冗余（`import` 在英文 token 与中文半句里各出现一次）。
> **刻意不为了 slug 好看去改 owner 的正文措辞** —— 那是 tail-wagging-dog。

### 5.3 h3 级旧链接的**如实降级**

原两页共 ~25 个带 id 的 h3（`figma-step0` `b-token` `b-verify` `browse-first` …）。
CONTENTS 与段锚点只认 h2 ⇒ **旧的 h3 级链接落到所属段的段首，不落到 h3 本身**。
h3 的 `id` 属性照留在模板里（无害，且留出以后手工深链的余地）。
这是有意接受的降级：最坏结果是多滚一屏，不是链接坏掉。

---

## 6. 静态页改成重定向壳

两个 `.html` **原地保留**（URL 不变，老链接不 404），各缩成 ~50 行：

1. `<meta http-equiv="refresh">` + 一句可见的手动链接（JS 关掉时仍可走）；
2. `<script>`：读 `location.hash`，查表得新 URL，`location.replace()`；
3. **`LEGACY`（**20** 条同页改名）+ `MOVED`（16 条跨页搬移）两张表留在壳里**，值改成
   `./#/guide/<page>/<slug>`（相对 `./` 解析到 `playground-dist/` 根 = SPA 入口）。

**为什么表留在壳里、不搬进 SPA**：它们是历史链接兼容层，36 条包袱进 SPA 只会让新页背着它跑。
壳是它们天然的归属地 —— 老 URL 命中的正是壳。

⚠️ 相对路径 `./#/…` 的解析结果**必须本地实测**（预期 `/playground-dist/onboarding.html` → `/playground-dist/#/guide/onboarding`）。

---

## 7. 验证（机械核验，不靠"看起来对"）

| # | 判据 | 手段 |
|---|---|---|
| 1 | 两页在左侧导航「上手」组可见、可点进 | 浏览器实测 |
| 2 | 16 个段 slug 与 §5 表逐字相同、无空串无重复 | 扩 `tests/docs-section-anchor.test.ts`（既有闸，非新建 —— 与 §8「免新建闸」不冲突）+ **一个不带修复的致败探针**证它非空过。理由：可分享链接整个建在这张 slug 表上，而一条空过的断言恰好什么都不守 |
| 3 | 段链接可分享：`#/guide/onboarding/clone` 直接落到该段 | 每 case **全新 page**（同文档改 hash 不重跑脚本）+ 「连续 3 次 `scrollY` 不变」判滚动落位 |
| 4 | 旧链接全活：`onboarding.html#install-source` / `#b-token` 等 **36** 条各落到预期段（13 SECTION + 20 LEGACY + 16 MOVED 去重后的目标面）| 壳表逐条实测 |
| 5 | 主题跟随、站内搜索能搜到这两页 | 浏览器实测（搜索面 = 名字过滤，符合 F111 ②a 已 ship 的范围） |
| 6 | 窄档不新增溢出：390 / 768 / 960 / 1280 四档 `.docs-main` 页级溢出与改动前**逐字段比对** | `scratch/f112/probe.mjs` 同一套判据 |
| 7 | 现成闸照跑 | `audit:docs-overflow` · `audit:layout-tokens` · `vue-tsc --noEmit` · `pnpm test` |
| 8 | 产物真含新页 | 重建后 `verify-dist.mjs`；⚠️ 提交产物目录**先 `git add -- playground-dist`**，判据 = 提交后 `git status --short playground-dist` 为空 |

**⛔ 别用 `Δ(scrollWidth - clientWidth)` 当"显示乱了"的判据**（上一轮据此把 0.5px 报成 191.5px）。
判「乱了」四类同时量：叶子控件两两求交 / 内容坐标落在可滚区间 / 矩形在视口内 / 计算样式。

**⛔ 本地预览必须让 `./tvu-tokens.css` 能解析**（只由 build 拷进 `playground-dist/`），否则整页 `var()` 落空
= 假缺陷；用 `scratch/f112/serve-utf8.py <port> <dir>`（`python3 -m http.server` **不送 charset**，
中文按 Latin-1 解码成乱码，会量出不存在的布局缺陷）。

---

## 8. 不做（YAGNI / 已排除）

- **iframe 套静态页** —— owner 已排除。
- **正文英文版** —— owner 2026-08-11 裁定「先只出中文，英文等真有人要再说」。
- **内容级搜索（F111 ②b）** —— 需构建期抽索引，独立一件事，未立项。
- **h3 级段锚点** —— 见 §5.3，如实降级。
- **为本项新建 audit 闸** —— 快车道条 3；现成的照跑（§7 #7）。
- **改 owner 的正文措辞去凑 slug** —— 见 §5.2 注。

---

## 9. 交付后要更新的指针（快车道条 3 = 不写复盘、不建 entry；但**指错地方的指针必须改**）

**状态类（2 处）**：
- `docs/STATUS.md` §一.0 → 该条关闭（本项 ship 后它不再是「等 owner 拍板」）；
- `docs/internal/_plans/next-session-pickup-2026-08-11-*.md` §1「交付面」那句 → 交付面从
  `playground/public/onboarding.html` 改成站内页 + 壳（**它是每 session mandatory 读物，指错地方代价高**）。

**"源文件在哪"类（3 处 —— 2026-08-13 补录，初版 spec 漏了）**：下列三份都逐字写着
「仓库内的源文件 = `playground/public/onboarding.html`」，搬家后这句**变成假的**
（那个路径届时只是个 50 行重定向壳）：
- `docs/DESIGNING_WITH_TVU.md:6`
- `docs/CONSUMER_ONBOARDING.md:6`
- `docs/ONBOARDING_NEW_MACHINE.md:6`

⇒ 三处改为指向 `playground/docs/pages/OnboardingPage.vue`（正文真源）+ 站上 URL。
这不是"顺手扩范围"：它们是**指针失效**，与本项同因同时发生，属本项的完成条件。

⛔ 仍然不写复盘文档、不建新 backlog entry（本项是 §一.0 的落地，不是新缺陷）。

⛔ 不写复盘文档、不建新 backlog entry（本项是 §一.0 的落地，不是新缺陷）。
