# P15-B 设计:bootstrap 加载屏暗色 + 对比度守卫补钉 + 死令牌清理(crearte)
- **日期**:2026-10-03
- **仓**:`crearte`(前端;`package.json` 与全部源码在 **`src/`**,不是仓根)
- **分支**:`feat/p15b-bootstrap-dark-and-guard-pins`(AGENTS.md 只允许 `{feat|fix|docs|chore}/`;本批含用户可感知的暗色改动,故用 `feat/`)
- **base**:`f823195`(master,P13 交付后的 AGENTS.md 不变量 commit)
- **维度**:用户体验 / UI 交互 + 代码优雅(USER.md 六维度循环)
- **并行**:P15-A 在 `crearte-server` 仓,两仓各自单写者 → **可同时派发**(`sdd-parallel-dispatch` §1)
---
## §0 基线(已实测,2026-10-03 13:07)
**vitest**:`Test Files 79 passed (79)` · `Tests 688 passed (688)` · Duration 44.83s。
命令:`cd crearte/src && npx vitest run`。**与 P13 交付时报的 688/79 逐字相同** → P14 未触及前端,且此后无新增测试。这是本批的比对基线。
`bootstrap/host-origin.test.ts (3 tests)` 已在 79 个文件里 → **`bootstrap/` 目录已有测试先例**,新增测试文件不属破例。
**vitest 环境**:`src/vite.config.ts` 的 `test` 段只有 `exclude: [...configDefaults.exclude, 'e2e/**']`,**无 `environment` 键** → 默认 **node**。这是 `contrast.test.ts` / `noHardcodedColor.test.ts` / `themeBootstrap.test.ts` 能用 `fileURLToPath(new URL('../..', import.meta.url))` 读源码文件的前提(**P13 教训:happy-dom 下会抛 `ERR_INVALID_URL_SCHEME`**)。本批新增的守卫**必须沿用同款写法**,不得引入需要 DOM 的断言。
**e2e**:`src/e2e/`,`playwright.config.ts` / `.noauth.` / `.stack.` 三份配置都是 `testDir: './e2e'`。`dark.spec.ts` **10 腿**(P13 交付),其中 `:100` 是 **pre-paint 归因腿**:「Vue 包被拦截时 `data-theme` 仍为 dark(证明是内联脚本而非挂载后设置)」。
---
## §1 三项改动的取证
### 1.1 改动①:bootstrap 游戏加载屏对暗色用户「白闪」
**现象**:`src/bootstrap/index.html`(虚拟模式游戏的加载屏,`
游戏加载中…` + 进度条 + 状态行 + 错误区)把整套配色**硬编码为亮色值**,且 `localStorage` / `prefers-color-scheme` / `data-theme` / `@media` **四项全部零命中**。暗色用户启动虚拟游戏时,会在暗色页面里看到一块亮色矩形。
**它确实是用户面**(非死代码):
- `src/vite.config.ts:21` 把 `bootstrap/index.html` 注册为名为 `bootstrap` 的**第二个 Vite 构建入口**
- `runtime/sw/router.ts:1` `export const BOOTSTRAP_PATH = '/__bootstrap'`;`sw/index.ts:211,218` 有 `redirect-bootstrap` 与离线兜底
- `runtime/host/adapters.ts:38`:`targets.push({ mode: 'virtual', url: \`${origin}/__bootstrap#${hash.toString()}\`, origin })`
- `useGameFrame.test.ts`:virtual 模式 url 形如 `http://demo.localhost:4173/__bootstrap#v=1`
- 游戏跑在**独立子域**:`runtime/host/config.ts:18` `derivePlayOrigin → ${protocol}//${16hex}.${baseDomain}`,由 `GameHost.vue` 以 iframe 嵌入
**根因不是漏改,是 P13 的令牌化只覆盖了单入口**:P13 改的是 `src/index.html`(主应用入口)+ `app/styles/main.css`(12 令牌)+ `app/` 与 `runtime/` 的 `.vue`。`bootstrap/` 是**第二个 HTML 入口**,不在任何一处覆盖范围内。
→ **教训(已入 ROADMAP 挂账):换肤/令牌化类改动须先 `grep -n 'input' src/vite.config.ts` 枚举所有构建入口,逐个确认覆盖。单入口假设会漏掉多入口应用。**
**跨源约束(决定修法的上限)**:
- **`localStorage` 按源隔离** → bootstrap 跑在游戏子域,**读不到**主应用存在主域 `localStorage['crearte.theme.v1']` 的显式选择。主应用 `src/index.html:15-31` 的 pre-paint 脚本判定序是 `stored → prefers-color-scheme → light`(由 `themeBootstrap.test.ts` 7 腿钉住),**在子域上拿不到 `stored` 那一级**。
- **`prefers-color-scheme` 是浏览器级、不按源隔离** → 可用。
- **hash 通道已存在**:`bootstrap/main.ts:4` 就是 `const params = new URLSearchParams(location.hash.replace(/^#/, ''))`,且 `main.ts:97` 有 `fail('启动参数完整', location.href)` 说明它是必需参数集 → **主题可搭车现有 hash 通道传入,不必新建 postMessage 协议**(P15 账本 §7 原估「完整修法须 postMessage,成本更高」,**这条被证伪**)。
**🔑 架构陷阱(本批最重要的设计约束)**:`GameHost.vue:27-31` 的 `targets` 是 **`computed`**:
```js
const targets = computed(() => resolveRuntimeTargets(props.game, {
baseDomain: config.baseDomain, protocol: location.protocol, config
}))
```
而 `useTheme.ts:51` 是模块级响应式单例 `const theme: Ref = ref(effectiveTheme())`。
| 注入方式 | 响应式追踪 | 后果 |
|---|---|---|
| ❌ `theme: useTheme().theme.value` | **是**(读 `ref` 的 getter → computed 建立依赖) | 用户在游戏页切主题 → `targets` 重算 → **iframe `src` 变化 → 正在运行的游戏被重载**(存档/进度丢失) |
| ✅ `theme: effectiveTheme()` | **否**(`effectiveTheme()` 内部读 `localStorage` 与 `matchMedia`,都不是响应式源) | computed 不依赖主题;iframe 不重载。代价:切主题后已打开的加载屏保持旧主题,直到下次导航 |
→ **裁定用 `effectiveTheme()`**(`useTheme.ts:39` 已导出)。"游戏运行中不重载" 比 "加载屏实时跟随主题切换" 重要得多:加载屏只在**启动瞬间**可见,而重载会毁掉正在进行的游戏。**这条必须有测试钉住**(§5-T1 **腿 5**;⚠️ RE-PIN 2026-10-03:原写「腿 4」,而腿 4 是 IIFE/var、注入是**腿 5**——spec 内部自相矛盾,§5-T1 表自身与 §5 mutation (d)、§7 风险表都写腿 5),否则下一个贡献者"顺手改成响应式"就会引入静默的游戏重载缺陷。
**色值映射(实测;`bootstrap/index.html` 里 8 个硬编码 hex 的令牌归属)**:
| hex | 令牌归属 | 暗色对应值 |
|---|---|---|
| `#f7f2e7` | 亮 `paper` **且** 暗 `ink`(翻转对称点) | `#17140f`(暗 paper) |
| `#141414` | 亮 `ink` | `#f7f2e7` |
| `#ffffff` | 亮 `surface` | `#221e18` |
| `#5c584d` | 亮 `ink-soft` | `#cbc4b5` |
| `#e8552f` | 亮 `accent` | `#ff7a4d` |
| `#c03a1b` | 亮 `accent-ink` | `#ffb59a` |
| `#a3a3a3` | ❌ **不对应任何令牌** | 见下 |
| `#f87171` | ❌ **不对应任何令牌** | 见下 |
**🔴 同批发现两条 pre-existing WCAG 违规(inline `style` 覆盖了达标的样式表值)**:
| 元素 | 样式表值(`