diff --git a/.gitignore b/.gitignore
index f2e10a1..4c5993c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,5 +9,6 @@
/docs/design/assets/a01-vnext/candidates/
/docs/design/assets/a01-vnext/review/
/.superpowers/
+/runtime/
**/__pycache__/
*.py[cod]
diff --git a/components/AppButton.vue b/components/AppButton.vue
new file mode 100644
index 0000000..89db12e
--- /dev/null
+++ b/components/AppButton.vue
@@ -0,0 +1,82 @@
+
+
+
+ {{ label }}
+
+
+
+
+
+
diff --git a/components/AppDialog.vue b/components/AppDialog.vue
new file mode 100644
index 0000000..eb27517
--- /dev/null
+++ b/components/AppDialog.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+ {{ eyebrow }}
+ {{ title }}
+ {{ message }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/AppLoading.vue b/components/AppLoading.vue
new file mode 100644
index 0000000..ca7e440
--- /dev/null
+++ b/components/AppLoading.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+ {{ text }}
+ {{
+ description
+ }}
+
+
+
+
+
+
diff --git a/components/AppTabbar.vue b/components/AppTabbar.vue
index 9aeb8e1..c26e67c 100644
--- a/components/AppTabbar.vue
+++ b/components/AppTabbar.vue
@@ -1,25 +1,55 @@
-
-
- {{ item.label }}
+
+
+ {{
+ item.label
+ }}
diff --git a/components/AppToast.vue b/components/AppToast.vue
new file mode 100644
index 0000000..6fe8a3f
--- /dev/null
+++ b/components/AppToast.vue
@@ -0,0 +1,46 @@
+
+
+ {{ message }}
+
+
+
+
+
+
diff --git a/components/GenealogyCard.vue b/components/GenealogyCard.vue
index 9b8f417..4d4a088 100644
--- a/components/GenealogyCard.vue
+++ b/components/GenealogyCard.vue
@@ -34,6 +34,9 @@
+
+ 更新于 {{ genealogy.updatedAt }}
+
{{ genealogy.memberCount }} 位成员
-
- 更新于 {{ genealogy.updatedAt }}
-
@@ -73,7 +73,7 @@ defineEmits(["select"]);
.genealogy-card {
position: relative;
display: flex;
- min-height: 156rpx;
+ min-height: 178rpx;
align-items: center;
padding: 18rpx 24rpx;
box-sizing: border-box;
@@ -177,25 +177,34 @@ defineEmits(["select"]);
.card-detail-row {
display: flex;
min-width: 0;
+ flex-wrap: wrap;
align-items: center;
justify-content: space-between;
- margin-top: 12rpx;
+ margin-top: 8rpx;
}
.card-metas {
display: flex;
min-width: 0;
+ width: 100%;
align-items: center;
+ margin-top: 2rpx;
}
.card-meta-item {
display: flex;
min-width: 0;
align-items: center;
- margin-right: 14rpx;
+ margin-right: 0;
+}
+.card-meta-item + .card-meta-item {
+ margin-left: 18rpx;
}
.card-meta-icon {
- width: 28rpx;
- height: 28rpx;
+ width: 46rpx;
+ height: 46rpx;
+ flex: 0 0 auto;
margin-right: 6rpx;
+ opacity: 1;
+ filter: saturate(1.35) brightness(0.82) contrast(1.15);
}
.card-meta {
color: #62584c;
@@ -204,9 +213,12 @@ defineEmits(["select"]);
}
.card-updated {
display: flex;
+ width: 100%;
flex: 0 0 auto;
align-items: center;
- margin-left: 10rpx;
+ justify-content: flex-end;
+ margin-top: 0;
+ margin-left: 0;
color: #62584c;
font-size: 24rpx;
font-weight: 500;
diff --git a/components/GenealogyPageBackground.vue b/components/GenealogyPageBackground.vue
new file mode 100644
index 0000000..8cc5d78
--- /dev/null
+++ b/components/GenealogyPageBackground.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/components/ModulePageBackground.vue b/components/ModulePageBackground.vue
new file mode 100644
index 0000000..76ca72e
--- /dev/null
+++ b/components/ModulePageBackground.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
diff --git a/components/PageHeader.vue b/components/PageHeader.vue
index 3f6e6a6..ac02058 100644
--- a/components/PageHeader.vue
+++ b/components/PageHeader.vue
@@ -2,25 +2,58 @@
@@ -28,12 +61,12 @@
diff --git a/components/tree/TreeMemberForm.vue b/components/tree/TreeMemberForm.vue
index c5967cc..dbd230e 100644
--- a/components/tree/TreeMemberForm.vue
+++ b/components/tree/TreeMemberForm.vue
@@ -1,28 +1,51 @@
-
+
-
+
-
+
{{ config.eyebrow }}
{{ config.title }}
{{ config.copy }}
-
-
+
+
{{ field.label }}
-
+
{{ config.note }}
- {{ config.action }}
+ {{ config.action }}
@@ -32,14 +55,23 @@
{{ resultCopy.copy }}
- 返回核对
+ 返回核对
- 查看冲突
+ 查看冲突
- {{ formState === 'success' ? '继续完善' : '重新填写' }}
+ {{ formState === "success" ? "继续完善" : "重新填写" }}
@@ -56,93 +88,268 @@
diff --git a/design-pipeline/manifests/module-page-backgrounds.json b/design-pipeline/manifests/module-page-backgrounds.json
new file mode 100644
index 0000000..7e37741
--- /dev/null
+++ b/design-pipeline/manifests/module-page-backgrounds.json
@@ -0,0 +1,32 @@
+{
+ "version": 1,
+ "width": 1440,
+ "height": 3600,
+ "backgrounds": [
+ {
+ "module": "tree",
+ "source": "docs/design/assets/module-backgrounds/masters/tree-page-background-imagegen-source.png",
+ "output": "static/assets/modules/tree/opaque/tree-page-background-long.png"
+ },
+ {
+ "module": "family",
+ "source": "docs/design/assets/module-backgrounds/masters/family-page-background-imagegen-source.png",
+ "output": "static/assets/modules/family/opaque/family-page-background-long.png"
+ },
+ {
+ "module": "records",
+ "source": "docs/design/assets/module-backgrounds/masters/records-page-background-imagegen-source.png",
+ "output": "static/assets/modules/records/opaque/records-page-background-long.png"
+ },
+ {
+ "module": "notification",
+ "source": "docs/design/assets/module-backgrounds/masters/notification-page-background-imagegen-source.png",
+ "output": "static/assets/modules/notification/opaque/notification-page-background-long.png"
+ },
+ {
+ "module": "profile",
+ "source": "docs/design/assets/module-backgrounds/masters/profile-page-background-imagegen-source.png",
+ "output": "static/assets/modules/profile/opaque/profile-page-background-long.png"
+ }
+ ]
+}
diff --git a/design-pipeline/package.json b/design-pipeline/package.json
index b29503e..53d7371 100644
--- a/design-pipeline/package.json
+++ b/design-pipeline/package.json
@@ -11,7 +11,8 @@
"verify:assets": "node scripts/verify-assets.mjs",
"build:a01-scroll-skins": "node scripts/build-a01-scroll-skins.mjs",
"verify:a01-scroll-skins": "node scripts/verify-a01-scroll-skins.mjs",
- "build:g01-background-candidates": "node scripts/build-g01-backgrounds.mjs"
+ "build:g01-background-candidates": "node scripts/build-g01-backgrounds.mjs",
+ "build:g01-empty-frame": "node scripts/build-g01-empty-frame.mjs"
},
"dependencies": {
"sharp": "0.34.5"
diff --git a/design-pipeline/scripts/build-g01-empty-frame.mjs b/design-pipeline/scripts/build-g01-empty-frame.mjs
new file mode 100644
index 0000000..ab5d424
--- /dev/null
+++ b/design-pipeline/scripts/build-g01-empty-frame.mjs
@@ -0,0 +1,51 @@
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import sharp from 'sharp'
+
+const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
+const projectRoot = path.resolve(scriptDirectory, '..', '..')
+const sourcePath = path.join(projectRoot, 'static', 'assets', 'modules', 'genealogy', 'opaque', 'g01-empty-panel.png')
+const outputPath = path.join(projectRoot, 'static', 'assets', 'modules', 'genealogy', 'transparent', 'g01-empty-panel-frame.png')
+const borderBand = 110
+
+const { data, info } = await sharp(sourcePath)
+ .ensureAlpha()
+ .raw()
+ .toBuffer({ resolveWithObject: true })
+
+for (let y = 0; y < info.height; y += 1) {
+ for (let x = 0; x < info.width; x += 1) {
+ const offset = (y * info.width + x) * info.channels
+ const red = data[offset]
+ const green = data[offset + 1]
+ const blue = data[offset + 2]
+ const alpha = data[offset + 3]
+ const distanceToEdge = Math.min(x, y, info.width - 1 - x, info.height - 1 - y)
+ const isWarmGold = red > green
+ && green > blue
+ && red - green >= 15
+ && green - blue >= 12
+ && red - blue >= 35
+ && red < 245
+ && blue < 180
+
+ if (distanceToEdge >= borderBand || !isWarmGold) {
+ data[offset + 3] = 0
+ continue
+ }
+
+ const edgeAlpha = Math.max(0, Math.min(255, (red - blue - 25) * 6))
+ data[offset + 3] = Math.min(alpha, edgeAlpha)
+ }
+}
+
+await sharp(data, {
+ raw: {
+ width: info.width,
+ height: info.height,
+ channels: info.channels
+ }
+}).png().toFile(outputPath)
+
+process.stdout.write(`BUILT ${path.relative(projectRoot, outputPath)}\n`)
+
diff --git a/design-pipeline/scripts/build_module_page_backgrounds.py b/design-pipeline/scripts/build_module_page_backgrounds.py
new file mode 100644
index 0000000..3f16772
--- /dev/null
+++ b/design-pipeline/scripts/build_module_page_backgrounds.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+import hashlib
+import json
+from pathlib import Path
+
+from PIL import Image
+
+
+ROOT = Path(__file__).resolve().parents[2]
+MANIFEST = ROOT / "design-pipeline/manifests/module-page-backgrounds.json"
+
+
+def sha256(path: Path) -> str:
+ digest = hashlib.sha256()
+ with path.open("rb") as stream:
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
+ digest.update(chunk)
+ return digest.hexdigest()
+
+
+def normalize(source: Path, output: Path, width: int, height: int) -> None:
+ with Image.open(source) as image:
+ image = image.convert("RGB")
+ scale = max(width / image.width, height / image.height)
+ resized = image.resize(
+ (round(image.width * scale), round(image.height * scale)),
+ Image.Resampling.LANCZOS,
+ )
+ left = max(0, (resized.width - width) // 2)
+ top = max(0, (resized.height - height) // 2)
+ normalized = resized.crop((left, top, left + width, top + height))
+ output.parent.mkdir(parents=True, exist_ok=True)
+ normalized.save(output, format="PNG", optimize=True)
+
+
+def main() -> None:
+ manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
+ width = int(manifest["width"])
+ height = int(manifest["height"])
+ report = []
+ for item in manifest["backgrounds"]:
+ source = ROOT / item["source"]
+ output = ROOT / item["output"]
+ if not source.is_file():
+ raise FileNotFoundError(source)
+ normalize(source, output, width, height)
+ report.append(
+ {
+ "module": item["module"],
+ "source": item["source"],
+ "output": item["output"],
+ "size": [width, height],
+ "sha256": sha256(output),
+ }
+ )
+ print(json.dumps(report, ensure_ascii=False, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/design/全模块长背景与A系列控件交接.md b/docs/design/全模块长背景与A系列控件交接.md
new file mode 100644
index 0000000..4e1f6ad
--- /dev/null
+++ b/docs/design/全模块长背景与A系列控件交接.md
@@ -0,0 +1,46 @@
+# 全模块长背景与 A 系列控件交接
+
+## 当前决定
+
+- A 系列现有按钮、弹窗、Toast 和对话框视觉已经由用户确认,作为全局唯一标准。
+- G01 是 G 系列风格母体;9 个活动 G 页面继续共用 `GenealogyPageBackground.vue` 与既定 G01 长背景。
+- T、F、R、N、M 各自保留模块性格,但共享浅色国风、楷体、宣纸、朱砂、古金与 A 系列控件。
+- 本轮按用户要求不生成新的运行截图;页面仍需用户逐页审核和 Android/HBuilderX 复核。
+
+## 公共组件
+
+- `components/AppButton.vue`:A 系列主/次卷轴按钮。
+- `components/AppDialog.vue`:A 系列单/双按钮卷轴对话框。
+- `components/AppToast.vue`:A 系列卷轴轻提示。
+- `components/AppLoading.vue`:项目自定义水墨加载。
+- `components/PageHeader.vue`:普通页使用真实返回箭头,根页保留品牌头部。
+- `components/ModulePageBackground.vue`:T、F、R、N、M 背景唯一映射入口。
+
+## 可恢复输入与运行资产
+
+| 模块 | ImageGen 原始输入 | APP 运行资产 | SHA-256 |
+| --- | --- | --- | --- |
+| T | `tree-page-background-imagegen-source.png` | `static/assets/modules/tree/opaque/tree-page-background-long.png` | `b4322e6d8294c1e30d03baa005aa77122dd49257504da2462db74bfe79e44b37` |
+| F | `family-page-background-imagegen-source.png` | `static/assets/modules/family/opaque/family-page-background-long.png` | `9c65aa0526d392e6d03e6c0216e9f5f9f9f917aa539e15c045283195882b5fec` |
+| R | `records-page-background-imagegen-source.png` | `static/assets/modules/records/opaque/records-page-background-long.png` | `a420ac3791acc13e25ae68f68782e07754c0040b7f9fbde037bf1588f2ff7007` |
+| N | `notification-page-background-imagegen-source.png` | `static/assets/modules/notification/opaque/notification-page-background-long.png` | `7f707153f6ecf53183fc360e6a34b4312975babeb5ccb3f36e048f1f64c65ca5` |
+| M | `profile-page-background-imagegen-source.png` | `static/assets/modules/profile/opaque/profile-page-background-long.png` | `c5adb199e5773582775f9f0bab1fef62e0948755c9f22aca647d236f8d4ef8b8` |
+
+所有原始输入位于 `docs/design/assets/module-backgrounds/masters/`;运行资产统一为 `1440×3600` RGB PNG。
+
+## 换机重建
+
+```powershell
+design-pipeline/.venv/Scripts/python.exe design-pipeline/scripts/build_module_page_backgrounds.py
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/global-heritage-visual-system-contract.ps1
+```
+
+清单:`design-pipeline/manifests/module-page-backgrounds.json`。
+
+## 验证边界
+
+- 当前静态合同和无截图 H5 运行 smoke 已覆盖主要页面状态、按钮事件、导航与自定义弹层。
+- 尚未完成 Android/HBuilderX 字体、状态栏、返回手势、软键盘与不同面板亮度复核。
+- 五张 `1440×3600` 图片在 4GB Android 上的解码、切页与回收仍需专项检查。
+- 当前仅为全线基础样式候选,不表示 52 页已由用户正式验收或冻结。
+
diff --git a/docs/superpowers/plans/2026-07-16-g01-secondary-text-legibility.md b/docs/superpowers/plans/2026-07-16-g01-secondary-text-legibility.md
new file mode 100644
index 0000000..e8342b0
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-16-g01-secondary-text-legibility.md
@@ -0,0 +1,143 @@
+# G01 次要文字可读性优化实施计划
+
+> **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:** 提高 G01“加入申请”卡片说明与状态文字在浅色山水背景上的可读性,同时保持原有信息层级、卡片尺寸和滚动结构。
+
+**Architecture:** 样式所有权继续留在 `pages/genealogy/g01-my-genealogies.vue`,不修改共享 `GenealogyCard.vue`。静态视觉契约锁定四类文字的字号、颜色和字重;现有 G01 运行 smoke 与五档截图验证没有重叠、裁切和滚动回归。
+
+**Tech Stack:** uni-app、Vue 3、SCSS、PowerShell 静态契约、Node.js Chrome DevTools Protocol 运行 smoke。
+
+## Global Constraints
+
+- 只修改 G01 申请卡片说明与状态样式,不修改共享家谱卡、28% 公共背景或其他页面。
+- 申请说明固定为 `28rpx / 500 / #62584c / 1.4`;状态固定为 `27rpx / 600`,审核中 `#7f4f16`,被拒绝 `#a7160c`,已退出 `#62584c`。
+- 卡片高度、内边距、相邻 `12rpx` 间距、箭头、文案和点击行为保持不变。
+- H5 证据不等于 Android/HBuilderX 已通过,也不等于 G01 整页已验收。
+- 不使用多代理或 worktree,不执行 `git add`、`commit`、`push`、`reset` 或 `checkout`。
+
+---
+
+### Task 1: 用视觉契约锁定可读性样式
+
+**Files:**
+- Modify: `tests/g01-visual-contract.ps1`
+- Test: `tests/g01-visual-contract.ps1`
+
+**Interfaces:**
+- Consumes: G01 的 `.application-record__copy`、`.application-record__status` 及两个状态修饰类。
+- Produces: 已确认字号、颜色、行高和字重的静态合同。
+
+- [ ] **Step 1: 写入失败断言**
+
+在申请卡片间距断言之后加入:
+
+```powershell
+if ($page -notmatch '(?s)\.application-record__copy\s*\{[^}]*color:\s*#62584c;[^}]*font-size:\s*28rpx;[^}]*font-weight:\s*500;[^}]*line-height:\s*1\.4;') { throw 'G-01 application descriptions do not use the approved readable style.' }
+if ($page -notmatch '(?s)\.application-record__status\s*\{[^}]*color:\s*#7f4f16;[^}]*font-size:\s*27rpx;[^}]*font-weight:\s*600;') { throw 'G-01 application statuses do not use the approved readable base style.' }
+if ($page -notmatch '(?s)\.application-record__status--rejected\s*\{[^}]*color:\s*#a7160c;') { throw 'G-01 rejected status must preserve the approved semantic red.' }
+if ($page -notmatch '(?s)\.application-record__status--muted\s*\{[^}]*color:\s*#62584c;') { throw 'G-01 muted status does not use the approved readable color.' }
+```
+
+- [ ] **Step 2: 运行契约并确认 RED**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
+```
+
+Expected: FAIL,错误包含 `application descriptions do not use the approved readable style`。
+
+---
+
+### Task 2: 实施最小样式修改
+
+**Files:**
+- Modify: `pages/genealogy/g01-my-genealogies.vue`
+- Test: `tests/g01-visual-contract.ps1`
+
+**Interfaces:**
+- Consumes: Task 1 的四组视觉契约。
+- Produces: 仅 G01 申请卡片生效的增强可读性样式。
+
+- [ ] **Step 1: 替换申请文字样式**
+
+```scss
+.application-record__status {
+ flex: 0 0 auto;
+ margin-left: 16rpx;
+ color: #7f4f16;
+ font-size: 27rpx;
+ font-weight: 600;
+}
+.application-record__status--rejected { color: #a7160c; }
+.application-record__status--muted { color: #62584c; }
+.application-record__copy {
+ display: block;
+ margin-top: 10rpx;
+ color: #62584c;
+ font-size: 28rpx;
+ font-weight: 500;
+ line-height: 1.4;
+}
+```
+
+- [ ] **Step 2: 运行契约并确认 GREEN**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
+```
+
+Expected: `PASS G-01 visual contract`。
+
+- [ ] **Step 3: 证明契约可捕获回归**
+
+临时把说明字号恢复为首轮 `26rpx`,运行契约确认失败;随后恢复 `28rpx` 并再次确认通过。临时变化不得保留。
+
+---
+
+### Task 3: 运行、截图和状态更新
+
+**Files:**
+- Modify: `docs/交接记录.md`
+- Modify: `docs/验收规划.md`
+- Modify: `design-qa.md`
+- Evidence: `docs/design/screens/runtime/2026-07-16/g01-text-legibility-audit/`
+
+**Interfaces:**
+- Consumes: Task 2 已通过静态契约的 G01 页面。
+- Produces: 五档响应式证据、修改前后 412×915 对比和准确的交接状态。
+
+- [ ] **Step 1: 运行 G01 静态与运行回归**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
+node tests/g01-empty-state-runtime-smoke.js http://localhost:5173
+```
+
+Expected: 三组均输出 `PASS`。
+
+- [ ] **Step 2: 生成五档截图并检查布局**
+
+覆盖 `320×568`、`360×640`、`360×800`、`412×915`、`412×1000`;确认长说明不与状态或箭头重叠,卡片高度和 12rpx 间距不变,底栏与独立滚动正常。
+
+- [ ] **Step 3: 更新状态文档**
+
+写明用户确认并实施 G01 申请次要文字增强;H5 只形成内部候选,Android/HBuilderX 与 G01 整页验收仍未完成。
+
+- [ ] **Step 4: 最终检查**
+
+Run:
+
+```powershell
+git diff --check
+git status --short
+```
+
+Expected: `git diff --check` 退出码为 0;不覆盖、删除或清理任何已有工作区内容。
diff --git a/docs/superpowers/plans/2026-07-16-genealogy-background-opacity.md b/docs/superpowers/plans/2026-07-16-genealogy-background-opacity.md
new file mode 100644
index 0000000..4acba0d
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-16-genealogy-background-opacity.md
@@ -0,0 +1,144 @@
+# G 模块公共背景 28% 透明度实施计划
+
+> **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:** 让 9 个活动 G 页面通过共享背景组件统一使用 28% 不透明度的连续长背景画层,同时保持宣纸底色、内容层和现有布局不变。
+
+**Architecture:** `components/GenealogyPageBackground.vue` 继续作为背景显示规则的唯一所有者,只在图片画层增加 `opacity: 0.28`。`tests/genealogy-shared-background-contract.ps1` 锁定该值及单一所有权,页面级运行 smoke 和截图负责验证布局与可读性没有回归。
+
+**Tech Stack:** uni-app、Vue 3、SCSS/CSS、PowerShell 契约测试、Node.js Chrome DevTools Protocol 运行 smoke。
+
+## Global Constraints
+
+- 只修改共享背景图片画层,不修改背景 PNG、母版、裁切、贴底方式或页面滚动结构。
+- G01、G03、G05、G06、G08、G09、G10、G11、G12 统一生效;各页面不得重复定义透明度。
+- H5 截图仅作为内部候选;Android/HBuilderX 真机或模拟器仍待复核。
+- 不使用多代理、worktree,不执行 `git add`、`commit`、`push`、`reset` 或 `checkout`。
+- 保留所有已有修改、未跟踪文件、测试、文档和证据。
+
+---
+
+### Task 1: 用共享背景契约锁定 28% 不透明度
+
+**Files:**
+- Modify: `tests/genealogy-shared-background-contract.ps1`
+- Test: `tests/genealogy-shared-background-contract.ps1`
+
+**Interfaces:**
+- Consumes: `.genealogy-page-background__art` 作为共享长背景图片画层。
+- Produces: 背景透明度唯一值 `opacity: 0.28` 的仓库契约。
+
+- [ ] **Step 1: 写入会失败的透明度与单一所有权断言**
+
+在组件画层断言之后加入:
+
+```powershell
+Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background__art\s*\{[^}]*opacity:\s*0\.28;') 'shared artwork must use the approved 28% opacity'
+```
+
+在每个 G 页面循环中加入:
+
+```powershell
+Assert-Contract ($pageSource -notmatch '(?s)\.genealogy-page-background__art\s*\{[^}]*opacity:') "$route must not override the shared background opacity"
+```
+
+- [ ] **Step 2: 运行契约并确认按预期失败**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1
+```
+
+Expected: FAIL,错误包含 `shared artwork must use the approved 28% opacity`。
+
+---
+
+### Task 2: 在共享组件实现 28% 画层不透明度
+
+**Files:**
+- Modify: `components/GenealogyPageBackground.vue`
+- Test: `tests/genealogy-shared-background-contract.ps1`
+
+**Interfaces:**
+- Consumes: Task 1 的 `opacity: 0.28` 契约。
+- Produces: 9 个活动 G 页面共用的淡化背景,无页面级覆盖。
+
+- [ ] **Step 1: 最小修改共享画层样式**
+
+将画层规则改为:
+
+```css
+.genealogy-page-background__art {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ display: block;
+ width: 100%;
+ opacity: 0.28;
+}
+```
+
+- [ ] **Step 2: 运行契约并确认通过**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1
+```
+
+Expected: `PASS G genealogy shared background contract`。
+
+- [ ] **Step 3: 证明契约能捕获回归**
+
+临时把 `opacity: 0.28` 改回无透明度,运行同一契约并确认失败;随后恢复 `opacity: 0.28`,再次运行并确认通过。临时变化不得保留。
+
+---
+
+### Task 3: 运行回归、截图并更新内部状态
+
+**Files:**
+- Modify: `docs/交接记录.md`
+- Modify: `docs/验收规划.md`
+- Modify: `docs/design/G01_列表背景候选与换机重建.md`
+- Modify: `design-qa.md`
+- Evidence: `docs/design/screens/runtime/2026-07-16/`
+
+**Interfaces:**
+- Consumes: Task 2 已通过契约的共享组件。
+- Produces: 五档 G01 H5 内部候选证据、其余 G 页面运行回归结果,以及不夸大验收层级的交接记录。
+
+- [ ] **Step 1: 顺序运行 G 页面 smoke,避免调试端口竞争**
+
+Run:
+
+```powershell
+node tests/g01-empty-state-runtime-smoke.js http://localhost:5173
+node tests/g03-create-flow-runtime-smoke.js http://localhost:5173
+node tests/g05-overview-runtime-smoke.js http://localhost:5173
+node tests/g06-search-flow-runtime-smoke.js http://localhost:5173
+node tests/g08-g10-application-flow-runtime-smoke.js http://localhost:5173
+node tests/g11-g12-settings-poems-runtime-smoke.js http://localhost:5173
+```
+
+Expected: 六组脚本全部输出 `PASS`,无浏览器异常或 `console.error`。
+
+- [ ] **Step 2: 生成并检查 G01 五档截图**
+
+尺寸为 `320×568`、`360×640`、`360×800`、`412×915`、`412×1000`。截图必须确认:背景比原候选明显变淡;标题、卡片、文字、按钮和底栏未同步变淡;左右不裁剪;内部列表滚动后底栏仍固定。
+
+- [ ] **Step 3: 更新权威状态文档**
+
+文档统一写明:用户确认共享背景画层为 28% 不透明度;当前只有 H5 内部候选证据;G01 及其余 G 页面没有因此获得整页验收;Android/HBuilderX 与 4GB Android 长图性能仍待复核。
+
+- [ ] **Step 4: 最终一致性检查**
+
+Run:
+
+```powershell
+git diff --check
+git status --short
+```
+
+Expected: `git diff --check` 退出码为 0;只报告已有换行符提示,不覆盖或清理工作区中的任何既有修改。
diff --git a/docs/superpowers/plans/2026-07-16-genealogy-bottom-aligned-background.md b/docs/superpowers/plans/2026-07-16-genealogy-bottom-aligned-background.md
new file mode 100644
index 0000000..6050e92
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-16-genealogy-bottom-aligned-background.md
@@ -0,0 +1,83 @@
+# G 类型页面 C 背景贴底自适应 Implementation Plan
+
+> **For agentic workers:** 本计划由当前主代理在同一会话内执行;用户明确禁止多代理、worktree、git add、commit、push、reset 和 checkout。
+
+**Goal:** 让现有 C 背景保持完整比例并贴底显示,把山水视觉集中到 G 页面下半区,减少加入申请区域的大块纯色空白。
+
+**Architecture:** 保持 `GenealogyPageBackground.vue` 为 9 个 G 页面的唯一背景入口,不增加图片资产。只把现有图片从 `top: 0` 改为 `bottom: 0`,继续使用 `widthFix` 和满屏宣纸底色。
+
+**Tech Stack:** uni-app、Vue 单文件组件、CSS、PowerShell 契约测试、Chrome CDP 截图。
+
+## Global Constraints
+
+- 只使用现有 `genealogy-page-background.png`,不生成或重做图片。
+- 背景完整显示、左右不裁剪、不拉伸。
+- 9 个活动 G 页面继续共用一个公共组件。
+- 不改变 G01 内容、卡片、交互、路由或固定区/滚动区结构。
+- 不使用多代理或 worktree,不执行任何 Git 写操作。
+
+---
+
+### Task 1: 背景贴底契约与最小实现
+
+**Files:**
+- Modify: `tests/genealogy-shared-background-contract.ps1`
+- Modify: `components/GenealogyPageBackground.vue`
+
+**Interfaces:**
+- Consumes: 现有公共背景组件和 C 运行资产。
+- Produces: `bottom: 0`、全宽、`widthFix` 的固定背景图层。
+
+- [ ] **Step 1: 写失败契约**
+
+在公共背景契约中要求 `.genealogy-page-background__art` 包含 `bottom: 0`,并禁止 `top: 0`。
+
+- [ ] **Step 2: 运行契约确认失败**
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
+
+Expected: 因当前组件仍使用 `top: 0` 而失败。
+
+- [ ] **Step 3: 最小修改公共组件**
+
+将图片定位从 `top: 0` 改为 `bottom: 0`,不修改资产、宽度、模式、层级或 9 个页面。
+
+- [ ] **Step 4: 运行契约确认通过**
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
+
+Expected: `PASS G genealogy shared background contract`。
+
+### Task 2: 运行截图与回归
+
+**Files:**
+- Modify: `design-qa.md`
+- Modify: `docs/交接记录.md`
+- Modify: `docs/验收规划.md`
+- Runtime evidence: `docs/design/screens/runtime/2026-07-16/`
+
+**Interfaces:**
+- Consumes: 贴底公共背景组件。
+- Produces: 四档 G01 H5 内部候选截图和未验收状态记录。
+
+- [ ] **Step 1: 四档真实截图**
+
+在 320×568、360×640、360×800、412×915 下截取 G01,确认完整画面贴底、上方宣纸区被主要内容覆盖、下方申请区域不再大块空白。
+
+- [ ] **Step 2: 源图与实现同轮对照**
+
+同一比较输入内打开 C 源图与 412×915 实现截图,确认无裁剪、无拉伸、无横向溢出和文字对比度问题。
+
+- [ ] **Step 3: 回归验证**
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1`
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/full-page-visual-contract.ps1`
+
+Run: G01、G03、G05、G06、G08-G10、G11-G12 现有运行 smoke。
+
+Run: `git diff --check`
+
+Expected: 所有命令退出码为 0;仍明确缺少 Android/HBuilderX 真机或模拟器复核。
diff --git a/docs/superpowers/plans/2026-07-16-genealogy-flagship-long-background-candidate.md b/docs/superpowers/plans/2026-07-16-genealogy-flagship-long-background-candidate.md
new file mode 100644
index 0000000..b3477fa
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-16-genealogy-flagship-long-background-candidate.md
@@ -0,0 +1,70 @@
+# G 模块旗舰级连续长背景候选 Implementation Plan
+
+> **For agentic workers:** 当前主代理内联执行;用户明确禁止多代理、worktree、git add、commit、push、reset 和 checkout。
+
+**Goal:** 基于现有 C 方向生成连续长背景候选;用户确认后,将其固化为 1536×3840 母版、生成 1440×3600 运行图并接入共享组件。
+
+**Architecture:** 使用内置 ImageGen 编辑现有 C 母版,生成完整不透明长图。候选先存入 `design-pipeline/generated/g01-background/`;用户确认后保留原稿,以锁定 Pillow/Lanczos 归一化母版并生成版本化运行图,由 `GenealogyPageBackground.vue` 单一接入。
+
+**Tech Stack:** Built-in ImageGen、Pillow 12.3.0、现有 G01 资产流水线。
+
+## Global Constraints
+
+- 候选确认前不替换运行资产或页面引用;用户确认后只切换共享组件,不改 9 个页面业务结构。
+- 目标母版 1536×3840,运行方向 1440×3600。
+- 无文字、Logo、UI、卡片、红色、接缝和重复图案。
+- 不删除、覆盖或清理现有母版、候选、截图和未跟踪文件。
+
+---
+
+### Task 1: 生成长图候选
+
+**Files:**
+- Read: `docs/design/assets/g01-background/masters/g01-list-background-direction-c-paper-master.png`
+- Create: `design-pipeline/generated/g01-background/genealogy-page-background-long-flagship-candidate.png`
+
+**Interfaces:**
+- Consumes: C 母版的宣纸色、竹影、水墨山水、亭台和中央低对比构图。
+- Produces: 一张未接入页面的长图候选 PNG。
+
+- [x] **Step 1: 查看 C 母版并作为 ImageGen 编辑目标**
+
+确认输入图无文字、无 UI,保持其暖宣纸和灰金水墨语言。
+
+- [x] **Step 2: 使用内置 ImageGen 生成 2.5:1 候选**
+
+提示中明确 2.5:1 连续纵向构图、顶部安全留白、中部低对比、下部山水、无接缝和禁止元素。
+
+- [x] **Step 3: 将生成结果复制到被忽略的候选路径**
+
+不覆盖现有 A/B/C 母版或 `genealogy-page-background.png`。
+
+### Task 2: 候选验证与展示
+
+**Files:**
+- Inspect: `design-pipeline/generated/g01-background/genealogy-page-background-long-flagship-candidate.png`
+
+**Interfaces:**
+- Consumes: ImageGen 候选。
+- Produces: 实际尺寸、文件字节数、SHA-256 和视觉检查结果。
+
+- [x] **Step 1: 用 Pillow 检查尺寸、模式和边缘连续性**
+
+确认不透明 RGB/RGBA、实际像素尺寸及没有异常透明边。
+
+- [x] **Step 2: 打开候选检查视觉**
+
+检查文字/Logo 污染、明显横向色带、重复竹枝、中央过密和亭台位置。
+
+- [x] **Step 3: 向用户展示候选**
+
+只报告候选事实,不宣称已接入、已验收或已完成 Android 验证。
+
+### Task 3: 用户确认后的正式接入
+
+- [x] 保留 793×1983 ImageGen 原稿,并生成 1536×3840 sRGB 归一化母版。
+- [x] 先新增失败契约,再扩展 manifest、Python 构建器和 Node 包装器为四候选。
+- [x] 生成 1440×3600 版本化运行图,更新共享背景组件;旧 C 文件不覆盖、不删除。
+- [x] 完成 G01 五档 H5 截图、G 页面运行 smoke、Python/Node/PowerShell 契约和内部设计 QA。
+- [ ] Android/HBuilderX 与 4GB Android 图片解码、切页、回收复核。
+- [ ] G01 固定区/独立滚动结构继续与用户讨论,不在本次背景接入中擅自实施。
diff --git a/docs/superpowers/plans/2026-07-16-genealogy-shared-background.md b/docs/superpowers/plans/2026-07-16-genealogy-shared-background.md
new file mode 100644
index 0000000..78f1bcb
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-16-genealogy-shared-background.md
@@ -0,0 +1,154 @@
+# G 类型页面共用 C 背景 Implementation Plan
+
+> **For agentic workers:** 本计划由当前主代理在同一会话内执行;用户明确禁止多代理、worktree、git add、commit、push、reset 和 checkout。
+
+**Goal:** 让 9 个活动 G 页面通过一个公共组件完整显示 C 背景,左右不裁剪、不变形,并用宣纸底色延展剩余页面。
+
+**Architecture:** `components/GenealogyPageBackground.vue` 是背景资产路径和显示规则的唯一所有者。各 G 页面移除旧背景图片与私有背景样式,只保留一个公共组件实例;测试从 `pages.json` 推导路由,防止页面清单漂移。
+
+**Tech Stack:** uni-app、Vue 单文件组件、SCSS、PowerShell 契约测试、Node.js 运行冒烟测试。
+
+## Global Constraints
+
+- 仅修改 G 类型页面背景层,不改变页面内容、结构、交互和接口。
+- C 背景按宽度等比显示,顶部对齐,左右不裁剪、不变形。
+- 高屏和长页面未覆盖区域使用同色宣纸底色延展。
+- 不实施 G01 固定区/列表独立滚动。
+- 不使用多代理或 worktree,不执行任何 Git 写操作。
+- H5 截图仅作为内部候选,不能替代 Android/HBuilderX 复核。
+
+---
+
+### Task 1: 建立公共背景契约
+
+**Files:**
+- Create: `tests/genealogy-shared-background-contract.ps1`
+- Read: `pages.json`
+
+**Interfaces:**
+- Consumes: `pages.json` 中所有 `pages/genealogy/g*.vue` 活动路由。
+- Produces: 每个 G 页面必须渲染一次 `` 的仓库契约。
+
+- [ ] **Step 1: 写失败测试**
+
+测试必须断言:公共组件存在;组件唯一引用 `/static/assets/modules/genealogy/opaque/genealogy-page-background.png`;使用 `mode="widthFix"`;包含固定满屏宣纸底层和宽度 `100%` 的图片;每个实际 G 页面恰好使用一次公共组件;页面不得直接引用旧纸纹、旧页脚或公共背景文件。
+
+- [ ] **Step 2: 运行测试确认失败**
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
+
+Expected: 因公共组件和公共资产尚不存在而失败。
+
+### Task 2: 实现单一公共背景入口
+
+**Files:**
+- Create: `components/GenealogyPageBackground.vue`
+- Create: `static/assets/modules/genealogy/opaque/genealogy-page-background.png`
+- Modify: `design-pipeline/manifests/g01-background-candidates.json`
+- Modify: `design-pipeline/tests/g01-background-candidates.test.mjs`
+
+**Interfaces:**
+- Consumes: C 母版和可重建生成结果。
+- Produces: 无属性、无事件的 `` 视觉组件。
+
+- [ ] **Step 1: 将当前 C 生成结果复制为 G 模块公共运行资产**
+
+源文件为 `design-pipeline/generated/g01-background/g01-list-background-direction-c.png`;目标文件为 `static/assets/modules/genealogy/opaque/genealogy-page-background.png`。复制后验证 SHA-256 仍为 `f14aeed81046d2d5e7ae78b1e62f572689c38261a2cc043142bef9f70200dc5b`。
+
+- [ ] **Step 2: 创建最小公共组件**
+
+组件使用固定满屏 `view` 提供宣纸底色,并在顶部放置 `width: 100%`、`mode="widthFix"` 的 C 背景图片;组件必须 `pointer-events: none` 且位于页面内容下方。
+
+- [ ] **Step 3: 更新流水线清单测试和运行输出所有权**
+
+将 manifest 的 `runtimeOutput` 改为公共资产路径,并让 Node 测试断言新路径,避免 G01 私有资产继续成为正式入口。
+
+### Task 3: 迁移 9 个 G 页面
+
+**Files:**
+- Modify: `pages/genealogy/g01-my-genealogies.vue`
+- Modify: `pages/genealogy/g03-create-genealogy.vue`
+- Modify: `pages/genealogy/g05-genealogy-overview.vue`
+- Modify: `pages/genealogy/g06-search-genealogies.vue`
+- Modify: `pages/genealogy/g08-join-application.vue`
+- Modify: `pages/genealogy/g09-my-applications.vue`
+- Modify: `pages/genealogy/g10-application-review.vue`
+- Modify: `pages/genealogy/g11-genealogy-settings.vue`
+- Modify: `pages/genealogy/g12-generation-poems.vue`
+
+**Interfaces:**
+- Consumes: ``。
+- Produces: 每个 G 页面一个公共背景实例,页面内容和交互保持原样。
+
+- [ ] **Step 1: 替换模板背景层**
+
+删除各页开头的旧纸纹和页脚山水图片,或 G01 的私有 C 背景图片,原位置统一放置 ``。
+
+- [ ] **Step 2: 删除仅服务于旧背景的样式**
+
+删除 9 个页面中对应 `__paper`、`__footer`、`page-paper-texture`、`page-footer-landscape` 或 `page-background` 规则;不改相邻内容样式。
+
+- [ ] **Step 3: 运行公共背景契约确认通过**
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
+
+Expected: `PASS G genealogy shared background contract`。
+
+### Task 4: 更新既有契约与权威资料
+
+**Files:**
+- Modify: `tests/g01-visual-contract.ps1`
+- Modify: `docs/交接记录.md`
+- Modify: `docs/验收规划.md`
+- Modify: `docs/design/P00_页面结构与资产清单.md`
+- Modify: `docs/design/设计资产生产流水线规范.md`
+- Modify: `docs/design/G01_列表背景候选与换机重建.md`
+- Modify: `design-qa.md`
+
+**Interfaces:**
+- Consumes: 公共组件、公共资产路径和用户本轮决定。
+- Produces: 文档与实际代码一致的未验收状态说明。
+
+- [ ] **Step 1: 更新 G01 视觉契约**
+
+契约改为检查公共组件和公共资产 SHA-256,不再要求 G01 私有资产路径。
+
+- [ ] **Step 2: 更新权威资料**
+
+记录 C 已成为 9 个 G 页面公共背景;明确完整显示和宣纸延展规则,同时保留“G01 未整页验收、其他 G 页面未逐页截图复核、Android 复核缺失”。
+
+### Task 5: 验证和截图审核
+
+**Files:**
+- Runtime evidence only: `docs/design/screens/runtime/2026-07-16/`
+
+**Interfaces:**
+- Consumes: H5 运行页和四个目标视口。
+- Produces: 仅供内部审核的运行截图与测试证据。
+
+- [ ] **Step 1: 运行聚焦验证**
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1`
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1`
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1`
+
+Run: `node --test design-pipeline/tests/g01-background-candidates.test.mjs`
+
+Run: `design-pipeline/.venv/Scripts/python.exe -m unittest design-pipeline/tests/test_build_g01_backgrounds.py -v`
+
+Run: `node tests/g01-empty-state-runtime-smoke.js`
+
+Run: `git diff --check`
+
+Expected: 所有测试退出码为 0;`git diff --check` 无空白错误。
+
+- [ ] **Step 2: 四档 H5 截图复核 G01**
+
+在 320×568、360×640、360×800、412×915 下确认完整左右边缘、无纵向拉伸、无横向溢出,图片下方自然过渡到宣纸底色。
+
+- [ ] **Step 3: 报告验证边界**
+
+报告 H5 内部候选结果,不宣称 G01 或其他 G 页面正式验收;保留 Android/HBuilderX 真机或模拟器复核缺口。
+
diff --git a/docs/superpowers/plans/2026-07-16-global-heritage-visual-system.md b/docs/superpowers/plans/2026-07-16-global-heritage-visual-system.md
new file mode 100644
index 0000000..cf9e357
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-16-global-heritage-visual-system.md
@@ -0,0 +1,106 @@
+# 全模块国风视觉系统 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 以 A 系列已确认控件为全局唯一标准,完成 G、T、F、R、N、M 活动页面的按钮、对话框、提示、返回导航和模块背景统一。
+
+**Architecture:** 新建公共视觉组件持有 A 系列资产,G 页面继续消费现有共享背景,T/F/R/N/M 通过模块背景组件与 `ModulePage` 统一壳消费各自长背景。页面只保留业务状态和模块内容,不再自行定义全局控件外观。
+
+**Tech Stack:** uni-app、Vue 3 `
+```
+
+样式按 Global Constraints 的两档精确尺寸实现,动画继续由 `app-loading-breathe` 唯一维护。
+
+- [ ] **Step 4: 更新 G01 消费方式**
+
+```vue
+
+```
+
+删除 G01 的 `.state-copy--loading` 独立说明节点。
+
+- [ ] **Step 5: 验证通过**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
+```
+
+Expected: 全部 PASS。
+
+---
+
+### Task 2: G 系列接入
+
+**Files:**
+- Create: `tests/g-series-app-loading-contract.ps1`
+- Modify: `pages/genealogy/g05-genealogy-overview.vue`
+- Modify: `pages/genealogy/g06-search-genealogies.vue`
+- Modify: `pages/genealogy/g09-my-applications.vue`
+- Modify: `pages/genealogy/g10-application-review.vue`
+- Modify: `pages/genealogy/g11-genealogy-settings.vue`
+- Modify: `pages/genealogy/g12-generation-poems.vue`
+
+**Interfaces:**
+- Consumes: Task 1 的 `AppLoading` 属性合同。
+- Produces: G05/G09/G10/G11/G12 页面级 Loading 与 G06 局部 Loading。
+
+- [ ] **Step 1: 写入失败契约**
+
+每页必须导入 `AppLoading`;G05、G09、G10、G11、G12 在自身 `loading` 分支渲染页面级组件;G06 搜索区渲染 `variant="section"`。
+
+- [ ] **Step 2: 验证失败**
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1`
+
+Expected: FAIL,指出首个尚未接入的 G 页面。
+
+- [ ] **Step 3: 接入页面级分支**
+
+各页在现有成功/空/失败分支之前加入:
+
+```vue
+
+```
+
+其中 `pageState` 分别为 `overviewState`、`applicationState`、`reviewState`、`settingsState`、`poemState`;保留各页其他分支和操作。
+
+- [ ] **Step 4: 接入 G06 局部加载**
+
+```vue
+
+```
+
+搜索框、模式切换、地区筛选和分隔线继续可见。
+
+- [ ] **Step 5: 验证通过**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
+node tests/module-page-runtime-smoke.js http://localhost:5173
+```
+
+Expected: 全部 PASS。
+
+---
+
+### Task 3: T/F/N 页面级接入
+
+**Files:**
+- Create: `tests/module-app-loading-contract.ps1`
+- Modify: `pages/tree/t01-tree-overview.vue`
+- Modify: `pages/tree/t03-member-profile.vue`
+- Modify: `pages/tree/t07-member-directory.vue`
+- Modify: `pages/family/f01-family-feed.vue`
+- Modify: `pages/notification/n01-message-center.vue`
+
+**Interfaces:**
+- Consumes: Task 1 的页面级 `AppLoading`。
+- Produces: 五个页面独立且不会误落入错误卡的 `loading` 分支。
+
+- [ ] **Step 1: 写入失败契约**
+
+锁定五页导入组件并在现有列表/详情/树/错误判断之前单独处理 `loading`;Loading 期间不显示错误操作或发布/审核按钮。
+
+- [ ] **Step 2: 验证失败**
+
+Run: `powershell -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1`
+
+Expected: FAIL,指出首个仍把 `loading` 当成错误/空状态的页面。
+
+- [ ] **Step 3: 实施五页分支**
+
+统一结构:
+
+```vue
+
+…
+…原空/错误状态…
+```
+
+只调整状态分支,不改变数据、入口、卡片和按钮行为。
+
+- [ ] **Step 4: 验证通过**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
+node tests/t01-tree-state-runtime-smoke.js http://localhost:5173
+node tests/t03-t08-member-flow-runtime-smoke.js http://localhost:5173
+node tests/root-pages-runtime-smoke.js http://localhost:5173
+```
+
+Expected: 全部 PASS。
+
+---
+
+### Task 4: 全量验证与视觉检查
+
+**Files:**
+- Verify only: all modified files.
+
+**Interfaces:**
+- Consumes: Tasks 1–3 的加载组件和页面状态分支。
+- Produces: H5 内部候选证据,不改变验收状态。
+
+- [ ] **Step 1: 静态与编译验证**
+
+Run:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
+powershell -NoProfile -ExecutionPolicy Bypass -File tests/compile-audit.ps1
+```
+
+- [ ] **Step 2: 运行态验证**
+
+Run G01、G 系列、T/F/N 现有 runtime smoke,任何失败均先定位状态分支,不放宽测试。
+
+- [ ] **Step 3: 代表性截图**
+
+捕获 G01 页面级与 G06 局部级在 320×568、360×640、360×800、412×915 的真实 H5 画面,检查无横向溢出、无错误态闪现、固定底栏不遮挡。
+
+- [ ] **Step 4: 工作区检查**
+
+Run: `git diff --check`
+
+Expected: 无空白错误;现有 LF/CRLF 提示单独报告。
diff --git a/docs/superpowers/plans/2026-07-19-app-loading-essential-motion.md b/docs/superpowers/plans/2026-07-19-app-loading-essential-motion.md
new file mode 100644
index 0000000..e26c0ef
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-19-app-loading-essential-motion.md
@@ -0,0 +1,160 @@
+# AppLoading 必要加载动效实施计划
+
+> **执行要求:** 使用 `superpowers:executing-plans` 在当前会话内逐步执行。用户明确禁止多代理、worktree、`git add`、commit、push、reset 和 checkout。
+
+**目标:** 修复 G01 在 `prefers-reduced-motion: reduce` 下加载态完全静止的问题,使印牌和如意结保留清晰的透明度呼吸,同时不产生缩放、旋转或位移。
+
+**架构:** 继续由 `components/AppLoading.vue` 单一维护加载动效,不在 G01 复制样式。正常模式保持现有动画;减少动态效果模式改用独立的透明度关键帧,并强制 `transform: none`。测试先锁定旧行为必须失败,再验证静态契约和当前 Chrome 运行时证据。
+
+**技术栈:** Vue 3、uni-app、SCSS、PowerShell 契约测试、Chrome DevTools Protocol、Node.js。
+
+## 全局约束
+
+- 只修改 G01 加载态直接依赖的公共 `AppLoading`、聚焦测试和当轮内部证据。
+- 不改变 `variant`、`text`、`description` 组件契约。
+- 不改变 G01 状态逻辑、布局、背景、文案、标题栏或底部导航。
+- 不新增图片、CSS 图形、JavaScript 定时器或业务接口。
+- `prefers-reduced-motion: reduce` 下只允许透明度变化,`transform` 必须为 `none`。
+- H5 截图只是内部候选;Android/HBuilderX 仍未验证。
+- 不执行任何 Git 写操作。
+
+---
+
+### Task 1:修复并验证减少动态效果模式的必要加载反馈
+
+**文件:**
+
+- 修改:`tests/app-loading-contract.ps1`
+- 修改:`components/AppLoading.vue`
+- 验证:`tests/g01-loading-state-contract.ps1`
+- 验证:`tests/g-series-app-loading-contract.ps1`
+- 验证:`tests/module-app-loading-contract.ps1`
+- 内部证据:`docs/design/screens/runtime/2026-07-19/g01-approval/03-loading-essential-motion-412x915.png`
+
+**接口:**
+
+- 消费:`AppLoading` 现有 `variant`、`text`、`description` 属性。
+- 产出:正常模式继续使用 `app-loading-seal-breathe`/`app-loading-knot-breathe`;减少动态效果模式使用 `app-loading-seal-essential-pulse`/`app-loading-knot-essential-pulse`。
+- 不增加新的组件属性、事件或页面状态。
+
+- [ ] **Step 1:先补充会失败的公共契约**
+
+在 `tests/app-loading-contract.ps1` 的现有动画断言之后加入:
+
+```powershell
+Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-seal-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential seal opacity pulse'
+Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-knot-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential knot opacity pulse'
+Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__seal\s*\{[^}]*animation:\s*app-loading-seal-essential-pulse\s+1\.2s\s+ease-in-out\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion seal must pulse opacity without transform motion'
+Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__knot\s*\{[^}]*animation:\s*app-loading-knot-essential-pulse\s+1\.2s\s+ease-in-out\s+\.2s\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion knot must pulse opacity without transform motion'
+if ($component -match '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?animation:\s*none') {
+ throw 'Reduced-motion AppLoading must not become completely static'
+}
+```
+
+- [ ] **Step 2:运行聚焦契约并确认 RED**
+
+运行:
+
+```powershell
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
+```
+
+预期:退出码非 0,错误首先指向缺少 `app-loading-seal-essential-pulse`,证明测试捕获的是当前完全静止行为,而不是语法或路径错误。
+
+- [ ] **Step 3:实现最小降动效透明度脉冲**
+
+在 `components/AppLoading.vue` 中保留现有正常模式关键帧,并把当前媒体查询替换为:
+
+```scss
+@keyframes app-loading-seal-essential-pulse {
+ 0%, 100% { opacity: .58; }
+ 50% { opacity: 1; }
+}
+
+@keyframes app-loading-knot-essential-pulse {
+ 0%, 100% { opacity: .28; }
+ 50% { opacity: .86; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .app-loading__seal {
+ animation: app-loading-seal-essential-pulse 1.2s ease-in-out infinite;
+ transform: none;
+ }
+
+ .app-loading__knot {
+ animation: app-loading-knot-essential-pulse 1.2s ease-in-out .2s infinite;
+ transform: none;
+ }
+}
+```
+
+不得修改模板、属性、尺寸、文案或正常模式关键帧。
+
+- [ ] **Step 4:运行聚焦契约并确认 GREEN**
+
+运行:
+
+```powershell
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
+```
+
+预期:分别输出 `APP-LOADING-CONTRACT PASS` 和 `G01-LOADING-STATE-CONTRACT PASS`,退出码均为 0。
+
+- [ ] **Step 5:在当前唯一 Chrome 标签页验证真实动画**
+
+保持 G01 `state=loading` 和 412×915 视口。通过 9222 CDP 在同一次检查中读取:
+
+```js
+const seal = document.querySelector('.app-loading__seal')
+const knot = document.querySelector('.app-loading__knot')
+const sealStyle = getComputedStyle(seal)
+const knotStyle = getComputedStyle(knot)
+({
+ reduced: matchMedia('(prefers-reduced-motion: reduce)').matches,
+ sealAnimation: sealStyle.animationName,
+ sealTransform: sealStyle.transform,
+ sealOpacity: sealStyle.opacity,
+ sealAnimations: seal.getAnimations().length,
+ knotAnimation: knotStyle.animationName,
+ knotTransform: knotStyle.transform,
+ knotOpacity: knotStyle.opacity,
+ knotAnimations: knot.getAnimations().length
+})
+```
+
+等待约 420ms 后再次读取。预期:
+
+- `reduced` 为 `true`;
+- 两个 `animationName` 分别以新的 essential pulse 名称开头;Vue scoped CSS 可在运行时追加哈希后缀;
+- 两个 `getAnimations().length` 均大于 0;
+- 两次读取的透明度不同;
+- 两个 `transform` 均为 `none`;
+- 页面仍为 G01 加载态。
+
+- [ ] **Step 6:捕获并人工检查当前候选帧**
+
+使用当前唯一标签页保存:
+
+```text
+docs/design/screens/runtime/2026-07-19/g01-approval/03-loading-essential-motion-412x915.png
+```
+
+人工检查:标题栏、背景、印牌、如意结、两行文案和底部导航均完整;没有列表、失败态或原生 Loading 串入。静态截图不作为动画运行证明,动画结论以 Step 5 的多时点数据为准。
+
+- [ ] **Step 7:运行受影响合同与空白检查**
+
+运行:
+
+```powershell
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1
+git diff --check -- components/AppLoading.vue tests/app-loading-contract.ps1 docs/superpowers/specs/2026-07-19-app-loading-essential-motion-design.md docs/superpowers/plans/2026-07-19-app-loading-essential-motion.md
+```
+
+预期:两个合同均输出各自的 `PASS`,`git diff --check` 退出码为 0。不得把 Android/HBuilderX 写成已验证。
+
+- [ ] **Step 8:回到同一加载态等待用户复核**
+
+保持浏览器停留在修正后的 G01 加载态,不自动切换到失败态。只有用户明确说加载态“通过”后,才继续 G01 下一状态;在此之前 G01 保持 `[!]`。
diff --git a/docs/superpowers/plans/2026-07-19-f01-family-feed-baseline-redesign.md b/docs/superpowers/plans/2026-07-19-f01-family-feed-baseline-redesign.md
new file mode 100644
index 0000000..33c327c
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-19-f01-family-feed-baseline-redesign.md
@@ -0,0 +1,200 @@
+# F01 Family Feed Baseline Redesign Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. This project explicitly forbids subagents, worktrees, staging, and commits.
+
+**Goal:** Replace F01's application-status cards with a dedicated transparent family-letter content card and make all four shortcuts usable at every supported viewport.
+
+**Architecture:** Keep F01 state and navigation logic unchanged. Add one F01-owned raster card skin under the family module, update only F01 markup/styles, and protect the asset, hierarchy, touch-target, and state contracts with a focused PowerShell test plus existing runtime coverage.
+
+**Tech Stack:** uni-app, Vue 3 `
diff --git a/pages/family/f02-publish-feed.vue b/pages/family/f02-publish-feed.vue
index d89b17b..51a67d3 100644
--- a/pages/family/f02-publish-feed.vue
+++ b/pages/family/f02-publish-feed.vue
@@ -1,4 +1,168 @@
-记录此刻分享通知、活动、家族故事或一段共同记忆。{{publishState==='success'?'动态已发布':'动态未发布'}}{{publishState==='success'?'家人现在可以在家族圈看到这条记录。':'请检查内容后重新发布,当前文字仍保留在页面中。'}}
-
-
+
+ 记录此刻分享通知、活动、家族故事或一段共同记忆。{{
+ publishState === "success" ? "动态已发布" : "动态未发布"
+ }}{{
+ publishState === "success"
+ ? "家人现在可以在家族圈看到这条记录。"
+ : "请检查内容后重新发布,当前文字仍保留在页面中。"
+ }}
+
+
+
diff --git a/pages/family/f03-feed-detail.vue b/pages/family/f03-feed-detail.vue
index fc24b03..9930eac 100644
--- a/pages/family/f03-feed-detail.vue
+++ b/pages/family/f03-feed-detail.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/family/f04-article-list.vue b/pages/family/f04-article-list.vue
index da8c03d..b3cb585 100644
--- a/pages/family/f04-article-list.vue
+++ b/pages/family/f04-article-list.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/family/f05-article-detail.vue b/pages/family/f05-article-detail.vue
index dee77fd..fe083e9 100644
--- a/pages/family/f05-article-detail.vue
+++ b/pages/family/f05-article-detail.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/family/f06-article-editor.vue b/pages/family/f06-article-editor.vue
index d582099..33ea3fb 100644
--- a/pages/family/f06-article-editor.vue
+++ b/pages/family/f06-article-editor.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/family/f07-album-list.vue b/pages/family/f07-album-list.vue
index 9fc63e2..48dcdf6 100644
--- a/pages/family/f07-album-list.vue
+++ b/pages/family/f07-album-list.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/family/f08-album-detail.vue b/pages/family/f08-album-detail.vue
index 01af8d3..00b4946 100644
--- a/pages/family/f08-album-detail.vue
+++ b/pages/family/f08-album-detail.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/family/f09-media-upload.vue b/pages/family/f09-media-upload.vue
index 3330ff4..648e7a2 100644
--- a/pages/family/f09-media-upload.vue
+++ b/pages/family/f09-media-upload.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/family/f10-video-list.vue b/pages/family/f10-video-list.vue
index 8a95d9e..b70ec89 100644
--- a/pages/family/f10-video-list.vue
+++ b/pages/family/f10-video-list.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/genealogy/g08-join-application.vue b/pages/genealogy/g08-join-application.vue
index b28a19c..1bc4e33 100644
--- a/pages/genealogy/g08-join-application.vue
+++ b/pages/genealogy/g08-join-application.vue
@@ -2,50 +2,117 @@
-
+
-
-
+
+
{{ sourceContract.eyebrow }}
- {{ sourceContract.formTitle }} {{ genealogyName }}
+ {{ sourceContract.formTitle }} {{ genealogyName }}
{{ sourceContract.formCopy }}
-
- 真实姓名
+
+ 真实姓名
- {{ fieldErrors.realName }}
+ {{
+ fieldErrors.realName
+ }}
-
- 与家谱关系
+
+ 与家谱关系
- {{ fieldErrors.relation }}
- 请以家谱中一位已知长辈为参照,例如:汤正华堂侄
+ {{
+ fieldErrors.relation
+ }}
+ 请以家谱中一位已知长辈为参照,例如:汤正华堂侄
-
- {{ sourceContract.thirdFieldLabel }}
+
+ {{ sourceContract.thirdFieldLabel }}
{{ sourceContract.note }}
-
- {{ isSubmitting ? sourceContract.submittingLabel : sourceContract.submitLabel }}
+
+ {{
+ isSubmitting
+ ? sourceContract.submittingLabel
+ : sourceContract.submitLabel
+ }}
- {{ joinState === 'success' ? sourceContract.successEyebrow : sourceContract.errorEyebrow }}
- {{ joinState === 'success' ? sourceContract.successTitle : sourceContract.errorTitle }}
+ {{
+ joinState === "success"
+ ? sourceContract.successEyebrow
+ : sourceContract.errorEyebrow
+ }}
+ {{
+ joinState === "success"
+ ? sourceContract.successTitle
+ : sourceContract.errorTitle
+ }}
{{ resultCopy }}
-
-
- {{ joinState === 'success' ? sourceContract.nextLabel : '重新填写' }}
+
+
+ {{
+ joinState === "success" ? sourceContract.nextLabel : "重新填写"
+ }}
@@ -53,121 +120,280 @@
diff --git a/pages/genealogy/g09-my-applications.vue b/pages/genealogy/g09-my-applications.vue
index 93d942f..9d84839 100644
--- a/pages/genealogy/g09-my-applications.vue
+++ b/pages/genealogy/g09-my-applications.vue
@@ -2,47 +2,94 @@
-
+
-
+
入谱申请进度审核结果会同步到消息中心
-
-
+
+
{{ item.genealogyName }}
{{ item.appliedAt }}
{{ item.relation }}
- {{ statusLabel(item.status) }}
- {{ statusHint(item.status) }}
- {{ actionFor(item) }}
+ {{ statusLabel(item.status) }}
+ {{
+ statusHint(item.status)
+ }}
+ {{ actionFor(item) }}
+
-
+
{{ stateTitle }}
{{ stateCopy }}
-
-
- {{ applicationState === 'error' ? '重新查看' : '查找公开家谱' }}
+
+
+ {{
+ applicationState === "error" ? "重新查看" : "查找公开家谱"
+ }}
diff --git a/pages/notification/n01-message-center.vue b/pages/notification/n01-message-center.vue
index 9cfbf95..9468f78 100644
--- a/pages/notification/n01-message-center.vue
+++ b/pages/notification/n01-message-center.vue
@@ -1,4 +1,289 @@
-{{item.unread?'未读提醒':'已读'}} · {{item.time}}{{item.title}}{{item.content}}{{noticeState==='empty'?'暂时没有新消息':'消息中心暂不可用'}}{{noticeState==='empty'?'家谱申请、审核结果和家族提醒会留在这里。':'请稍后重新进入,已读状态不会受到影响。'}}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.unread ? "未读提醒" : "已读" }} · {{ item.time }}
+ {{ item.title }}
+ {{ item.content }}
+
+
+
+
+
+
+
+
+
+ {{
+ noticeState === "empty" ? "暂时没有新消息" : "消息中心暂不可用"
+ }}
+ {{
+ noticeState === "empty"
+ ? "家谱申请、审核结果和家族提醒会留在这里。"
+ : "请稍后重新进入,已读状态不会受到影响。"
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/notification/n02-message-detail.vue b/pages/notification/n02-message-detail.vue
index a4edcb6..23e1cd4 100644
--- a/pages/notification/n02-message-detail.vue
+++ b/pages/notification/n02-message-detail.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m02-edit-profile.vue b/pages/profile/m02-edit-profile.vue
index 27f0a46..3ffe033 100644
--- a/pages/profile/m02-edit-profile.vue
+++ b/pages/profile/m02-edit-profile.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m03-security-settings.vue b/pages/profile/m03-security-settings.vue
index 68390cb..ea16d31 100644
--- a/pages/profile/m03-security-settings.vue
+++ b/pages/profile/m03-security-settings.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m04-change-password.vue b/pages/profile/m04-change-password.vue
index 3457359..d9ada7f 100644
--- a/pages/profile/m04-change-password.vue
+++ b/pages/profile/m04-change-password.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m05-change-phone.vue b/pages/profile/m05-change-phone.vue
index d229477..7dc4762 100644
--- a/pages/profile/m05-change-phone.vue
+++ b/pages/profile/m05-change-phone.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m06-help-center.vue b/pages/profile/m06-help-center.vue
index 16832e1..de5fd14 100644
--- a/pages/profile/m06-help-center.vue
+++ b/pages/profile/m06-help-center.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m07-feedback.vue b/pages/profile/m07-feedback.vue
index 1f1f779..f508348 100644
--- a/pages/profile/m07-feedback.vue
+++ b/pages/profile/m07-feedback.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m08-promotion.vue b/pages/profile/m08-promotion.vue
index e468f03..7dc905f 100644
--- a/pages/profile/m08-promotion.vue
+++ b/pages/profile/m08-promotion.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m09-vip-orders.vue b/pages/profile/m09-vip-orders.vue
index f1c8b36..1f4d7c8 100644
--- a/pages/profile/m09-vip-orders.vue
+++ b/pages/profile/m09-vip-orders.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/profile/m10-about-settings.vue b/pages/profile/m10-about-settings.vue
index c49ded8..8957935 100644
--- a/pages/profile/m10-about-settings.vue
+++ b/pages/profile/m10-about-settings.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r01-people-list.vue b/pages/records/r01-people-list.vue
index 51d19f5..c353561 100644
--- a/pages/records/r01-people-list.vue
+++ b/pages/records/r01-people-list.vue
@@ -1,3 +1,344 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ keyword ? "重置" : "搜索" }}
+
+
+
+
+
+
+ {{ person.name }}
+ {{ person.role }} · 第 {{ person.generation }} 世
+ 查看人物档案
+
+
+
+
+
+
+
+ 没有找到相关人物请更换姓名、身份或世代关键词后再试。
+
+
+
+
+
+
+
+ {{
+ peopleState === "empty" ? "还没有人物记录" : "人物录暂不可用"
+ }}
+ {{
+ peopleState === "empty"
+ ? "从第一位值得铭记的家人开始建立人物录。"
+ : "请稍后重新进入,已有档案不会受到影响。"
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/records/r02-person-detail.vue b/pages/records/r02-person-detail.vue
index de8f6d8..ff16647 100644
--- a/pages/records/r02-person-detail.vue
+++ b/pages/records/r02-person-detail.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r03-gift-list.vue b/pages/records/r03-gift-list.vue
index 96703cb..3fdb2f1 100644
--- a/pages/records/r03-gift-list.vue
+++ b/pages/records/r03-gift-list.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r04-gift-editor.vue b/pages/records/r04-gift-editor.vue
index 878cb78..f3ae63b 100644
--- a/pages/records/r04-gift-editor.vue
+++ b/pages/records/r04-gift-editor.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r05-ritual-list.vue b/pages/records/r05-ritual-list.vue
index 79a3c98..5e401e3 100644
--- a/pages/records/r05-ritual-list.vue
+++ b/pages/records/r05-ritual-list.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r06-ritual-detail.vue b/pages/records/r06-ritual-detail.vue
index 5289ea8..a6d4b70 100644
--- a/pages/records/r06-ritual-detail.vue
+++ b/pages/records/r06-ritual-detail.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r07-ritual-editor.vue b/pages/records/r07-ritual-editor.vue
index 391ee83..d91238a 100644
--- a/pages/records/r07-ritual-editor.vue
+++ b/pages/records/r07-ritual-editor.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r08-growth-journal.vue b/pages/records/r08-growth-journal.vue
index fa35dea..a61d778 100644
--- a/pages/records/r08-growth-journal.vue
+++ b/pages/records/r08-growth-journal.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r09-life-events.vue b/pages/records/r09-life-events.vue
index 5f01169..7426ee7 100644
--- a/pages/records/r09-life-events.vue
+++ b/pages/records/r09-life-events.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r10-memo-list.vue b/pages/records/r10-memo-list.vue
index fd99146..175aed2 100644
--- a/pages/records/r10-memo-list.vue
+++ b/pages/records/r10-memo-list.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/records/r11-merit-records.vue b/pages/records/r11-merit-records.vue
index d95ac7b..03be5c2 100644
--- a/pages/records/r11-merit-records.vue
+++ b/pages/records/r11-merit-records.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/tree/t03-member-profile.vue b/pages/tree/t03-member-profile.vue
index f1bcc98..bebd848 100644
--- a/pages/tree/t03-member-profile.vue
+++ b/pages/tree/t03-member-profile.vue
@@ -1,86 +1,284 @@
-
+
-
+
-
-
+
+
+
- {{ member.name.slice(0, 1) }}
- {{ member.name }}第 {{ member.generation }} 世 · {{ member.relation }} · {{ member.branch }}
+ {{ member.name.slice(0, 1) }}
+ {{ member.name }}第 {{ member.generation }} 世 · {{ member.relation }} ·
+ {{ member.branch }}
基本资料
- {{ item.label }}{{ item.value }}
+ {{ item.label }}{{ item.value }}
- 亲属关系
+ 亲属关系
长子 · 汤正国次子 · 汤正华
- 完善成员档案
+ 完善成员档案
- 成员档案暂不可用请从世系树重新选择成员,当前没有可展示的个人资料。
- 返回世系树
+ 成员档案暂不可用请从世系树重新选择成员,当前没有可展示的个人资料。
+ 返回世系树
diff --git a/pages/tree/t04-add-relative.vue b/pages/tree/t04-add-relative.vue
index b46972a..827acc8 100644
--- a/pages/tree/t04-add-relative.vue
+++ b/pages/tree/t04-add-relative.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/tree/t05-edit-member.vue b/pages/tree/t05-edit-member.vue
index be05abd..00cae2b 100644
--- a/pages/tree/t05-edit-member.vue
+++ b/pages/tree/t05-edit-member.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/tree/t06-edit-relationship.vue b/pages/tree/t06-edit-relationship.vue
index 338557f..ab4f83d 100644
--- a/pages/tree/t06-edit-relationship.vue
+++ b/pages/tree/t06-edit-relationship.vue
@@ -1,3 +1,5 @@
-
+
diff --git a/pages/tree/t07-member-directory.vue b/pages/tree/t07-member-directory.vue
index b8b7ff2..1dffb04 100644
--- a/pages/tree/t07-member-directory.vue
+++ b/pages/tree/t07-member-directory.vue
@@ -1,72 +1,368 @@
-
+
-
-
-
- 查找
+
+ 汤氏家谱
+ 主支 · {{ members.length }} 位成员
+
+
+
+
+ 查找
-
- 家谱成员{{ filteredMembers.length }} 人 · 按世代排列
-
-
- {{ item.name }}第 {{ item.generation }} 世 · {{ item.generationName }} · {{ item.branch }}{{ item.note }}
+
+
+ 家谱成员{{ filteredMembers.length }} 人 · 按世代排列
+
+
+
+ {{ item.name }}
+ 第 {{ item.generation }} 世 · {{ item.generationName }} ·
+ {{ item.branch }}
+ {{ item.note }}
+
-
- {{ directoryState === 'empty' ? '没有找到相关成员' : '成员目录暂不可用' }}{{ directoryState === 'empty' ? '换一个姓名、字辈或支系关键词再试。' : '请从当前家谱重新进入,成员资料不会受到影响。' }}
+
+ {{
+ directoryState === "empty" ? "没有找到相关成员" : "成员目录暂不可用"
+ }}{{
+ directoryState === "empty"
+ ? "换一个姓名、字辈或支系关键词再试。"
+ : "请从当前家谱重新进入,成员资料不会受到影响。"
+ }}
+
diff --git a/static/assets/modules/tree/transparent/t01-member-node.png b/static/assets/modules/tree/transparent/t01-member-node.png
new file mode 100644
index 0000000..508b467
Binary files /dev/null and b/static/assets/modules/tree/transparent/t01-member-node.png differ
diff --git a/tests/app-loading-contract.ps1 b/tests/app-loading-contract.ps1
new file mode 100644
index 0000000..d192626
--- /dev/null
+++ b/tests/app-loading-contract.ps1
@@ -0,0 +1,38 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$component = Get-Content -LiteralPath (Join-Path $root 'components/AppLoading.vue') -Raw -Encoding utf8
+
+function Assert-Match {
+ param([string]$Content, [string]$Pattern, [string]$Message)
+ if ($Content -notmatch $Pattern) { throw $Message }
+}
+
+Assert-Match -Content $component -Pattern ':class="`app-loading--\$\{variant\}`"' -Message 'AppLoading must expose page and section modifier classes'
+Assert-Match -Content $component -Pattern 'variant:\s*\{\s*type:\s*String,\s*default:\s*''page'',\s*validator:' -Message 'AppLoading must own a validated page/section variant prop'
+Assert-Match -Content $component -Pattern 'description:\s*\{\s*type:\s*String,\s*default:\s*''''\s*\}' -Message 'AppLoading must expose an optional description prop'
+Assert-Match -Content $component -Pattern 'v-if="description" class="app-loading__description"' -Message 'AppLoading must render its optional description'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading\s*\{[^}]*position:\s*relative;[^}]*z-index:\s*1;' -Message 'AppLoading must stay above page panel skins without page-specific overrides'
+Assert-Match -Content $component -Pattern 'class="app-loading__seal"\s+src="/static/assets/foundation/transparent/brand-seal\.png"' -Message 'AppLoading must render the approved real red-gold seal asset'
+Assert-Match -Content $component -Pattern 'class="app-loading__knot"\s+src="/static/assets/foundation/transparent/auth-divider-knot\.png"' -Message 'AppLoading must render the approved real gold knot asset'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s*\{[^}]*min-height:\s*320rpx;' -Message 'AppLoading page variant must use the approved minimum height'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s+\.app-loading__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' -Message 'AppLoading page seal must use the approved size'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s+\.app-loading__copy\s*\{[^}]*font-size:\s*30rpx;' -Message 'AppLoading page copy must use the approved size'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s+\.app-loading__description\s*\{[^}]*font-size:\s*24rpx;' -Message 'AppLoading page description must use the approved size'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s*\{[^}]*min-height:\s*180rpx;' -Message 'AppLoading section variant must use the approved minimum height'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s+\.app-loading__seal\s*\{[^}]*width:\s*88rpx;[^}]*height:\s*90rpx;' -Message 'AppLoading section seal must use the approved size'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s+\.app-loading__copy\s*\{[^}]*font-size:\s*24rpx;' -Message 'AppLoading section copy must use the approved size'
+Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s+\.app-loading__description\s*\{[^}]*font-size:\s*22rpx;' -Message 'AppLoading section description must use the approved size'
+Assert-Match -Content $component -Pattern 'app-loading-seal-breathe\s+1\.6s' -Message 'AppLoading seal must use the approved restrained breathing rhythm'
+Assert-Match -Content $component -Pattern '@media\s*\(prefers-reduced-motion:\s*reduce\)' -Message 'AppLoading must respect reduced-motion preferences'
+Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-seal-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential seal opacity pulse'
+Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-knot-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential knot opacity pulse'
+Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__seal\s*\{[^}]*animation:\s*app-loading-seal-essential-pulse\s+1\.2s\s+ease-in-out\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion seal must pulse opacity without transform motion'
+Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__knot\s*\{[^}]*animation:\s*app-loading-knot-essential-pulse\s+1\.2s\s+ease-in-out\s+\.2s\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion knot must pulse opacity without transform motion'
+if ($component -match '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?animation:\s*none') {
+ throw 'Reduced-motion AppLoading must not become completely static'
+}
+
+if ($component -match 'app-loading__mark|border:\s*4rpx\s+double') { throw 'AppLoading must not retain the legacy CSS box mark' }
+
+Write-Output 'APP-LOADING-CONTRACT PASS'
diff --git a/tests/capture-chrome-page-contract.ps1 b/tests/capture-chrome-page-contract.ps1
index 30b8b4b..cb683d4 100644
--- a/tests/capture-chrome-page-contract.ps1
+++ b/tests/capture-chrome-page-contract.ps1
@@ -12,8 +12,8 @@ $captureScript = Get-Content -LiteralPath $captureScriptPath -Raw -Encoding utf8
if ($captureScript -notmatch "const renderSettleMilliseconds = 900") {
throw 'Chrome capture must define a 900ms render-settle delay after the target selector appears'
}
-if ($captureScript -notmatch 'await sleep\(renderSettleMilliseconds\)') {
- throw 'Chrome capture must wait for the render-settle delay before taking a screenshot'
+if ($captureScript -notmatch "await sleep\(action\?\.endsWith\('-submitting'\) \? 80 : renderSettleMilliseconds\)") {
+ throw 'Chrome capture must wait for the standard render-settle delay except for verified transient submitting states'
}
if ($captureScript -match 'await sleep\(300\)') {
throw 'Chrome capture must not retain the unstable 300ms post-selector delay'
@@ -27,10 +27,10 @@ if ($captureScript -notmatch 'Array\.from\(document\.images\)\.every') {
if ($captureScript -notmatch 'await waitForLoadedImages\(send\)') {
throw 'Chrome capture must wait for document images after the target selector renders'
}
-if ($captureScript -notmatch "await send\('Page.reload'\)") {
- throw 'Chrome capture must reload after navigation so route screenshots do not inherit a prior Vue state'
+if ($captureScript -notmatch "await waitForPageLoad\(socket, \(\) => send\('Page.reload'\)\)") {
+ throw 'Chrome capture must reload and await Page.loadEventFired so route screenshots do not inherit a prior Vue state'
}
-if ($captureScript.IndexOf("await send('Page.navigate'") -ge $captureScript.IndexOf("await send('Page.reload')")) {
+if ($captureScript.IndexOf("await send('Page.navigate'") -ge $captureScript.IndexOf("await waitForPageLoad(socket, () => send('Page.reload'))")) {
throw 'Chrome capture must reload only after it has navigated to the requested URL'
}
if ($captureScript -notmatch 'const waitForFreshDocument = async \(send, documentTimeOrigin\) =>') {
@@ -45,7 +45,7 @@ if ($captureScript -notmatch 'await waitForFreshDocument\(send, documentTimeOrig
if ($captureScript -notmatch "action !== 'fresh-navigation'") {
throw 'Chrome capture must support fresh-navigation for root pages whose tab lifecycle closes a reloaded CDP target'
}
-foreach ($g06Action in @('g06-results', 'g06-empty', 'g06-invite')) {
+foreach ($g06Action in @('g06-results', 'g06-empty', 'g06-error', 'g06-invite', 'g06-invite-invalid')) {
if ($captureScript -notmatch [regex]::Escape($g06Action)) {
throw "Chrome capture must support the $g06Action visual state"
}
diff --git a/tests/capture-f-series-states-contract.ps1 b/tests/capture-f-series-states-contract.ps1
new file mode 100644
index 0000000..2898c29
--- /dev/null
+++ b/tests/capture-f-series-states-contract.ps1
@@ -0,0 +1,11 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
+
+foreach ($required in @('prepareFSeriesState', 'f02-validation', 'module-preview-toast', 'waitForPageLoad')) {
+ if ($capture -notmatch [regex]::Escape($required)) { throw "F series capture state missing: $required" }
+}
+if ($capture -notmatch 'await prepareFSeriesState\(send, action\)') { throw 'Capture runner does not prepare F series states' }
+if ($capture -notmatch "waitForPageLoad\(socket, \(\) => send\('Page\.reload'\)\)") { throw 'Capture runner must wait for the reload load event before DOM inspection' }
+
+Write-Output 'CAPTURE-F-SERIES-STATES-CONTRACT PASS'
diff --git a/tests/capture-g03-states-contract.ps1 b/tests/capture-g03-states-contract.ps1
new file mode 100644
index 0000000..7c98652
--- /dev/null
+++ b/tests/capture-g03-states-contract.ps1
@@ -0,0 +1,23 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
+
+foreach ($required in @(
+ 'prepareG03State',
+ 'g03-create-validation',
+ 'g03-duplicate-reminder',
+ 'g03-create-submitting',
+ 'g03-create-error',
+ 'g03-ancestor-validation',
+ 'g03-ancestor-submitting',
+ 'g03-ancestor-error',
+ 'g03-ancestor-success'
+)) {
+ if ($capture -notmatch [regex]::Escape($required)) { throw "G03 capture state missing: $required" }
+}
+
+if ($capture -notmatch 'await prepareG03State\(send, action\)') { throw 'Capture runner does not prepare G03 states' }
+if ($capture -notmatch 'action\?\.endsWith\(''-submitting''\)') { throw 'Capture runner does not shorten settle time for transient submitting states' }
+
+Write-Output 'CAPTURE-G03-STATES-CONTRACT PASS'
diff --git a/tests/capture-g08-states-contract.ps1 b/tests/capture-g08-states-contract.ps1
new file mode 100644
index 0000000..3e00242
--- /dev/null
+++ b/tests/capture-g08-states-contract.ps1
@@ -0,0 +1,8 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
+foreach ($required in @('prepareG08State', 'g08-validation', 'g08-submitting', 'g08-error', 'g08-success')) {
+ if ($capture -notmatch [regex]::Escape($required)) { throw "G08 capture state missing: $required" }
+}
+if ($capture -notmatch 'await prepareG08State\(send, action\)') { throw 'Capture runner does not prepare G08 states' }
+Write-Output 'CAPTURE-G08-STATES-CONTRACT PASS'
diff --git a/tests/capture-g10-states-contract.ps1 b/tests/capture-g10-states-contract.ps1
new file mode 100644
index 0000000..5435fed
--- /dev/null
+++ b/tests/capture-g10-states-contract.ps1
@@ -0,0 +1,17 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
+
+foreach ($required in @(
+ 'prepareG10State',
+ 'g10-help',
+ 'g10-approve-dialog',
+ 'g10-approved',
+ 'g10-reject-dialog',
+ 'g10-rejected'
+)) {
+ if ($capture -notmatch [regex]::Escape($required)) { throw "G10 capture state missing: $required" }
+}
+if ($capture -notmatch 'await prepareG10State\(send, action\)') { throw 'Capture runner does not prepare G10 states' }
+
+Write-Output 'CAPTURE-G10-STATES-CONTRACT PASS'
diff --git a/tests/capture-g11-g12-states-contract.ps1 b/tests/capture-g11-g12-states-contract.ps1
new file mode 100644
index 0000000..82391af
--- /dev/null
+++ b/tests/capture-g11-g12-states-contract.ps1
@@ -0,0 +1,14 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
+
+foreach ($required in @(
+ 'prepareG11State', 'g11-validation', 'g11-saved',
+ 'prepareG12State', 'g12-validation', 'g12-save-error', 'g12-saved'
+)) {
+ if ($capture -notmatch [regex]::Escape($required)) { throw "G11/G12 capture state missing: $required" }
+}
+if ($capture -notmatch 'await prepareG11State\(send, action\)') { throw 'Capture runner does not prepare G11 states' }
+if ($capture -notmatch 'await prepareG12State\(send, action\)') { throw 'Capture runner does not prepare G12 states' }
+
+Write-Output 'CAPTURE-G11-G12-STATES-CONTRACT PASS'
diff --git a/tests/capture-t03-t06-states-contract.ps1 b/tests/capture-t03-t06-states-contract.ps1
new file mode 100644
index 0000000..183eb97
--- /dev/null
+++ b/tests/capture-t03-t06-states-contract.ps1
@@ -0,0 +1,10 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
+
+foreach ($required in @('prepareT03T06State', 't06-conflict-help')) {
+ if ($capture -notmatch [regex]::Escape($required)) { throw "T03-T06 capture state missing: $required" }
+}
+if ($capture -notmatch 'await prepareT03T06State\(send, action\)') { throw 'Capture runner does not prepare T03-T06 states' }
+
+Write-Output 'CAPTURE-T03-T06-STATES-CONTRACT PASS'
diff --git a/tests/core-flow-contract.ps1 b/tests/core-flow-contract.ps1
index 023db84..03db6bc 100644
--- a/tests/core-flow-contract.ps1
+++ b/tests/core-flow-contract.ps1
@@ -53,11 +53,12 @@ if ($pages -match 'pages/genealogy/g04-first-ancestor') {
throw 'G04 first-person route must be merged into G03'
}
$createFlow = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue')
-foreach ($token in @('step=ancestor', 'createPerson', 'genealogyContext')) {
+foreach ($token in @('step=ancestor', 'submitAncestor', 'genealogyId')) {
if ($createFlow -notmatch [regex]::Escape($token)) {
throw "Missing G03 first-person flow: $token"
}
}
+if ($createFlow -match 'createPerson') { throw 'G03 must not retain the removed createPerson entrypoint' }
foreach ($route in @('pages/family/f04-article-list', 'pages/family/f02-publish-feed')) {
if ($pages -notmatch [regex]::Escape($route)) {
throw "Missing content page route: $route"
@@ -69,7 +70,11 @@ foreach ($pageFile in @('pages/genealogy/g05-genealogy-overview.vue', 'pages/tre
if ($page -match "id=1001|id=3") {
throw "Hard-coded route ID remains in $pageFile"
}
- if ($page -notmatch 'genealogyContext') {
+ if ($pageFile -eq 'pages/genealogy/g05-genealogy-overview.vue') {
+ if ($page -notmatch "const genealogyId = ref\(''\)" -or $page -notmatch 'query\.genealogyId') {
+ throw 'G05 must own its explicit genealogyId route context'
+ }
+ } elseif ($page -notmatch 'genealogyContext') {
throw "Missing genealogy context in $pageFile"
}
}
diff --git a/tests/f-series-all-states-visual-contract.ps1 b/tests/f-series-all-states-visual-contract.ps1
new file mode 100644
index 0000000..f8c277d
--- /dev/null
+++ b/tests/f-series-all-states-visual-contract.ps1
@@ -0,0 +1,27 @@
+$ErrorActionPreference = 'Stop'
+
+function Read-Utf8([string]$Path) { [System.IO.File]::ReadAllText((Join-Path (Join-Path $PSScriptRoot '..') $Path), [System.Text.Encoding]::UTF8) }
+function Assert-Contains([string]$Content, [string]$Expected, [string]$Message) { if (-not $Content.Contains($Expected)) { throw $Message } }
+
+$f01 = Read-Utf8 'pages/family/f01-family-feed.vue'
+$f02 = Read-Utf8 'pages/family/f02-publish-feed.vue'
+$module = Read-Utf8 'components/ModulePage.vue'
+
+foreach ($expected in @('font-size:24rpx','font-size:23rpx','font-size:22rpx')) {
+ Assert-Contains $f01 $expected "F01 readability token missing: $expected"
+}
+foreach ($expected in @('font-size:24rpx','font-size:23rpx')) {
+ Assert-Contains $f02 $expected "F02 readability token missing: $expected"
+}
+foreach ($expected in @(
+ '.module-lead { position: relative; height: 56rpx; margin: 0 12rpx 18rpx; color: $ink-muted; font-size: 23rpx;',
+ '.form-row > text { position: absolute; top: 28rpx; left: 26rpx; z-index: 1; color: $ink; font-size: 24rpx;',
+ '.form-row input { position: absolute; top: 0; right: 22rpx; bottom: 0; left: 178rpx; z-index: 1; height: 84rpx; color: $ink; font-size: 23rpx;',
+ '.list-card__copy text:nth-child(2) { margin-top: 10rpx; color: $ink-muted; font-size: 23rpx;',
+ '.detail-card text:last-child { margin-top: 10rpx; color: $ink; font-size: 24rpx;',
+ '.status-card__note { display: block; margin-top: 17rpx; color: $ink-muted; font-size: 24rpx;'
+)) {
+ Assert-Contains $module $expected "ModulePage readability contract missing: $expected"
+}
+
+Write-Output 'F series all-states visual contract passed.'
diff --git a/tests/f01-module-baseline-contract.ps1 b/tests/f01-module-baseline-contract.ps1
new file mode 100644
index 0000000..7cd7808
--- /dev/null
+++ b/tests/f01-module-baseline-contract.ps1
@@ -0,0 +1,19 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$page = Get-Content -LiteralPath (Join-Path $root 'pages/family/f01-family-feed.vue') -Raw -Encoding utf8
+
+function Assert-Match([string]$Pattern, [string]$Message) {
+ if ($page -notmatch $Pattern) { throw $Message }
+}
+
+Assert-Match 'modules/family/transparent/f01-family-letter-card\.png' 'F01 must use its dedicated transparent family-letter card.'
+if ($page -match 'application-status-card\.png') { throw 'F01 must not reuse the application status card.' }
+Assert-Match 'class="feed-card__title"' 'F01 must expose an explicit feed title hierarchy.'
+Assert-Match 'class="feed-card__meta"' 'F01 must expose category and time as secondary metadata.'
+Assert-Match 'class="feed-card__summary"' 'F01 must expose feed summary copy.'
+Assert-Match 'class="feed-card__author"' 'F01 must expose the author as tertiary information.'
+Assert-Match '(?s)\.feed-shortcut\s*\{[^}]*min-height:\s*44px;' 'F01 shortcuts must preserve a 44 CSS px touch height.'
+Assert-Match 'class="feed-shortcuts__skin"' 'F01 shortcuts must share one visible navigation skin.'
+Assert-Match '(?s)\.feed-shortcuts\s*\{[^}]*grid-template-columns:\s*repeat\(4,minmax\(0,1fr\)\);' 'F01 shortcuts must use one four-column equal-width navigation strip.'
+
+Write-Output 'F01-MODULE-BASELINE-CONTRACT PASS'
diff --git a/tests/g-series-app-loading-contract.ps1 b/tests/g-series-app-loading-contract.ps1
new file mode 100644
index 0000000..607e360
--- /dev/null
+++ b/tests/g-series-app-loading-contract.ps1
@@ -0,0 +1,40 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+
+function Read-Page([string]$Path) {
+ Get-Content -LiteralPath (Join-Path $root $Path) -Raw -Encoding utf8
+}
+
+function Assert-Match {
+ param([string]$Content, [string]$Pattern, [string]$Message)
+ if ($Content -notmatch $Pattern) { throw $Message }
+}
+
+$pages = @{
+ G05 = Read-Page 'pages/genealogy/g05-genealogy-overview.vue'
+ G06 = Read-Page 'pages/genealogy/g06-search-genealogies.vue'
+ G09 = Read-Page 'pages/genealogy/g09-my-applications.vue'
+ G10 = Read-Page 'pages/genealogy/g10-application-review.vue'
+ G11 = Read-Page 'pages/genealogy/g11-genealogy-settings.vue'
+ G12 = Read-Page 'pages/genealogy/g12-generation-poems.vue'
+}
+
+foreach ($entry in $pages.GetEnumerator()) {
+ Assert-Match -Content $entry.Value -Pattern 'import AppLoading from [''"]@/components/AppLoading\.vue[''"]' -Message "$($entry.Key) must import AppLoading"
+}
+
+Assert-Match -Content $pages.G05 -Pattern '(?s).*?]+description="[^"]+"\s*/>.*?' -Message 'G05 must render AppLoading inside its transparent framed loading state'
+Assert-Match -Content $pages.G06 -Pattern ']+description="[^"]+"\s*/>' -Message 'G06 search results must render the section AppLoading variant'
+Assert-Match -Content $pages.G06 -Pattern 'requestedState === ''loading''' -Message 'G06 must expose its section loading state for visual review'
+Assert-Match -Content $pages.G09 -Pattern ']+description="[^"]+"\s*/>' -Message 'G09 must render a page AppLoading branch'
+Assert-Match -Content $pages.G10 -Pattern ']+description="[^"]+"\s*/>' -Message 'G10 must render a page AppLoading branch'
+Assert-Match -Content $pages.G11 -Pattern ']+description="[^"]+"\s*/>' -Message 'G11 must render a page AppLoading branch'
+Assert-Match -Content $pages.G12 -Pattern ']+description="[^"]+"\s*/>' -Message 'G12 must render a page AppLoading branch'
+Assert-Match -Content $pages.G10 -Pattern 'if \(query\.state === ''loading''\) \{ reviewState\.value = ''loading''; return \}' -Message 'G10 must preserve its loading presentation state'
+Assert-Match -Content $pages.G11 -Pattern 'query\.state === ''loading'' \? ''loading''' -Message 'G11 must preserve its loading presentation state'
+Assert-Match -Content $pages.G12 -Pattern 'query\.state === ''loading'' \? ''loading''' -Message 'G12 must preserve its loading presentation state'
+
+if ($pages.G06 -match 'class="search-status search-loading"') { throw 'G06 must not retain its duplicated text-only search loader' }
+
+Write-Output 'G-SERIES-APP-LOADING-CONTRACT PASS'
diff --git a/tests/g01-empty-state-contract.ps1 b/tests/g01-empty-state-contract.ps1
index 1689019..fc71a97 100644
--- a/tests/g01-empty-state-contract.ps1
+++ b/tests/g01-empty-state-contract.ps1
@@ -21,6 +21,9 @@ $paths = @($pages.pages.path)
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
$runtimeSmoke = Get-Content -LiteralPath (Join-Path $root 'tests/g01-empty-state-runtime-smoke.js') -Raw -Encoding utf8
$panelPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g01-empty-panel.png'
+$framePath = Join-Path $root 'static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png'
+$pipelinePackage = Get-Content -LiteralPath (Join-Path $root 'design-pipeline/package.json') -Raw -Encoding utf8
+$frameBuilderPath = Join-Path $root 'design-pipeline/scripts/build-g01-empty-frame.mjs'
if (-not ($paths -contains 'pages/genealogy/g01-my-genealogies')) { throw 'G01 route missing' }
if ($paths -contains 'pages/genealogy/g02-empty-genealogies') { throw 'G02 route must be removed; empty state belongs to G01' }
@@ -28,11 +31,12 @@ if ($paths -contains 'pages/genealogy/g02-empty-genealogies') { throw 'G02 route
foreach ($required in @(
'forceEmptyState',
'query.get("state") || "default"',
- 'g01-empty-panel.png',
'empty-create-action',
'empty-search-action',
'empty-invite-action',
'empty-create-note',
+ 'empty-panel__frame',
+ 'g01-empty-panel-frame.png',
'const createGenealogy = () =>',
'/pages/genealogy/g03-create-genealogy',
'/pages/genealogy/g06-search-genealogies',
@@ -56,12 +60,32 @@ foreach ($required in @(
Assert-Match -Content $g01 -Pattern '(?s).*class="current-slip".*class="shortcut-grid".*class="section-divider".*\s*]*class="genealogy-list-scroll"[^>]*>.*class="genealogy-lower".*' -Message 'G01 fixed and scrolling regions are not separated correctly'
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-index--split\s*\{[^}]*height:\s*100vh;[^}]*padding-bottom:\s*calc\(112rpx\s*\+\s*env\(safe-area-inset-bottom\)\)' -Message 'G01 split layout must reserve the fixed tabbar and safe area'
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-list-scroll\s*\{[^}]*height:\s*0;[^}]*flex:\s*1;' -Message 'G01 list scroll region must consume the remaining computed height'
+Assert-Match -Content $g01 -Pattern '(?s).*?\s*' -Message 'G01 empty state must render the exact extracted transparent frame without an opaque panel skin'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel__frame\s*\{[^}]*position:\s*absolute;[^}]*inset:\s*0;[^}]*width:\s*100%;[^}]*height:\s*100%;[^}]*pointer-events:\s*none;' -Message 'G01 empty state transparent frame does not cover the approved panel slot'
+Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-empty-state\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 empty state does not reserve the approved visibly fuller height'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*height:\s*1120rpx;' -Message 'G01 empty panel does not use the approved visibly fuller height'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel__content\s*\{[^}]*justify-content:\s*center;[^}]*padding:\s*50rpx 28rpx 46rpx;' -Message 'G01 empty panel content is not centered with the approved responsive padding'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-seal\s*\{[^}]*width:\s*120rpx;[^}]*height:\s*184rpx;' -Message 'G01 empty seal does not use the approved prominent size'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-title\s*\{[^}]*margin-top:\s*30rpx;[^}]*font-size:\s*48rpx;' -Message 'G01 empty title does not use the approved prominent size and spacing'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-copy\s*\{[^}]*min-height:\s*84rpx;[^}]*margin-top:\s*18rpx;[^}]*font-size:\s*28rpx;' -Message 'G01 empty guidance does not use the approved prominent size and spacing'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action,\s*\.empty-invite-action\s*\{[^}]*width:\s*560rpx;[^}]*height:\s*124rpx;' -Message 'G01 empty actions do not use the approved prominent size'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action__copy,\s*\.empty-invite-action__copy\s*\{[^}]*font-size:\s*34rpx;' -Message 'G01 empty action labels do not use the approved prominent size'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action\s*\{[^}]*min-height:\s*76rpx;[^}]*margin-top:\s*14rpx;' -Message 'G01 empty create action does not use the approved prominent hit area and spacing'
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action__copy\s*\{[^}]*font-size:\s*31rpx;' -Message 'G01 empty create action does not use the approved prominent size'
+if ($g01 -match '(?s)\.empty-create-action__copy\s*\{[^}]*text-decoration:\s*underline') {
+ throw 'G01 empty create action must not render a text underline'
+}
+Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-note\s*\{[^}]*font-size:\s*26rpx;[^}]*line-height:\s*38rpx;' -Message 'G01 empty-state create note does not use the approved readable size'
+if ($g01 -match '(?s).*?]+g01-empty-panel\.png') { throw 'G01 empty state must not retain the opaque panel background' }
foreach ($className in @('empty-panel', 'empty-create-action', 'empty-search-action')) {
Assert-NoCssSurface -Content $g01 -ClassName $className
}
if (-not (Test-Path -LiteralPath $panelPath)) { throw 'G01 requires its final raster empty-panel asset' }
+if (-not (Test-Path -LiteralPath $framePath)) { throw 'G01 requires the extracted transparent empty-panel frame asset' }
+if (-not (Test-Path -LiteralPath $frameBuilderPath)) { throw 'G01 transparent empty-panel frame must remain reproducible from its builder' }
+Assert-Match -Content $pipelinePackage -Pattern '"build:g01-empty-frame"\s*:\s*"node scripts/build-g01-empty-frame\.mjs"' -Message 'G01 transparent frame builder is not exposed by the design pipeline'
Assert-Match -Content $runtimeSmoke -Pattern '\{\s*width:\s*412,\s*height:\s*1000\s*\}' -Message 'G01 runtime smoke must include the approved 412x1000 tall-screen stress viewport'
Add-Type -AssemblyName System.Drawing
$panel = [System.Drawing.Bitmap]::FromFile($panelPath)
@@ -78,4 +102,21 @@ try {
$panel.Dispose()
}
+$frame = [System.Drawing.Bitmap]::FromFile($framePath)
+try {
+ if ($frame.Width -ne 1122 -or $frame.Height -ne 1402) { throw 'G01 transparent empty-panel frame must remain 1122 x 1402px' }
+ foreach ($point in @(@(0, 0), @(1121, 0), @(0, 1401), @(1121, 1401), @(561, 701))) {
+ if ($frame.GetPixel($point[0], $point[1]).A -ne 0) { throw 'G01 transparent empty-panel frame retains an opaque background pixel' }
+ }
+ $visible = 0
+ for ($y = 0; $y -lt $frame.Height; $y += 4) {
+ for ($x = 0; $x -lt $frame.Width; $x += 4) {
+ if ($frame.GetPixel($x, $y).A -gt 0) { $visible++ }
+ }
+ }
+ if ($visible -lt 1200 -or $visible -gt 12000) { throw "G01 transparent frame alpha coverage is implausible: $visible sampled pixels" }
+} finally {
+ $frame.Dispose()
+}
+
Write-Output 'G01-EMPTY-STATE-CONTRACT PASS'
diff --git a/tests/g01-error-state-contract.ps1 b/tests/g01-error-state-contract.ps1
new file mode 100644
index 0000000..c651f72
--- /dev/null
+++ b/tests/g01-error-state-contract.ps1
@@ -0,0 +1,24 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
+
+function Assert-Match {
+ param([string]$Content, [string]$Pattern, [string]$Message)
+ if ($Content -notmatch $Pattern) { throw $Message }
+}
+
+$errorBlock = [regex]::Match($g01, '(?s).*?').Value
+if (-not $errorBlock) { throw 'G01 error state block missing' }
+foreach ($required in @('error-panel__frame', 'g01-empty-panel-frame.png', 'error-panel__seal', 'brand-seal.png', 'error-panel__divider', 'section-divider.png', 'state-retry', 'a01-scroll-primary-v3.png', '@click="retryLoad"')) {
+ if ($errorBlock -notmatch [regex]::Escape($required)) { throw "G01 error state missing approved contract: $required" }
+}
+Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*height:\s*1120rpx;' -Message 'G01 error state must use the approved full-height frame slot'
+Assert-Match -Content $g01 -Pattern '(?s)\.error-panel__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' -Message 'G01 error state seal must use the approved prominent size'
+Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-title\s*\{[^}]*font-size:\s*42rpx;' -Message 'G01 error title must use the approved hierarchy'
+Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-copy\s*\{[^}]*font-size:\s*27rpx;' -Message 'G01 error guidance must use the approved readable size'
+Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-retry\s*\{[^}]*width:\s*560rpx;[^}]*height:\s*124rpx;' -Message 'G01 retry action must match the approved primary action size'
+
+if ($errorBlock -match 'modules/genealogy/opaque/g01-empty-panel\.png') { throw 'G01 error state must not retain the legacy opaque panel' }
+
+Write-Output 'G01-ERROR-STATE-CONTRACT PASS'
diff --git a/tests/g01-loading-state-contract.ps1 b/tests/g01-loading-state-contract.ps1
new file mode 100644
index 0000000..050d1bc
--- /dev/null
+++ b/tests/g01-loading-state-contract.ps1
@@ -0,0 +1,20 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
+$appLoading = Get-Content -LiteralPath (Join-Path $root 'components/AppLoading.vue') -Raw -Encoding utf8
+
+function Assert-Match {
+ param([string]$Content, [string]$Pattern, [string]$Message)
+ if ($Content -notmatch $Pattern) { throw $Message }
+}
+
+Assert-Match -Content $g01 -Pattern 'import AppLoading from "@/components/AppLoading\.vue";' -Message 'G01 loading state must import the global AppLoading component'
+Assert-Match -Content $g01 -Pattern '(?s)\s*\s*' -Message 'G01 loading state must pass its approved title and supporting copy directly to AppLoading'
+Assert-Match -Content $appLoading -Pattern 'class="app-loading__seal"\s+src="/static/assets/foundation/transparent/brand-seal\.png"' -Message 'Global AppLoading must retain its approved red-gold seal asset'
+Assert-Match -Content $appLoading -Pattern 'app-loading-seal-breathe' -Message 'Global AppLoading must retain its restrained seal breathing motion'
+
+if ($g01 -match 'loading-seal') { throw 'G01 loading state must not retain the legacy hollow loading seal' }
+if ($g01 -match 'state-copy--loading') { throw 'G01 loading state must not duplicate AppLoading description layout' }
+
+Write-Output 'G01-LOADING-STATE-CONTRACT PASS'
diff --git a/tests/g03-create-flow-contract.ps1 b/tests/g03-create-flow-contract.ps1
index 9aabdb1..18bffd2 100644
--- a/tests/g03-create-flow-contract.ps1
+++ b/tests/g03-create-flow-contract.ps1
@@ -18,7 +18,7 @@ $pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding
$paths = @($pages.pages.path)
$g03 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue') -Raw -Encoding utf8
$g04 = Join-Path $root 'pages/genealogy/g04-first-ancestor.vue'
-$panelPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g03-create-flow-panel.png'
+$panelPath = Join-Path $root 'static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png'
if (-not ($paths -contains 'pages/genealogy/g03-create-genealogy')) { throw 'G03 route missing' }
if ($paths -contains 'pages/genealogy/g04-first-ancestor') { throw 'G04 route must be removed; first-ancestor is a G03 state' }
@@ -36,7 +36,7 @@ foreach ($required in @(
'class="duplicate-reminder-layer"',
'if (isSubmitting.value) return;',
'/pages/genealogy/g05-genealogy-overview?genealogyId=',
- 'g03-create-flow-panel.png',
+ 'g01-empty-panel-frame.png',
'root-header-cinnabar.jpg',
'a01-scroll-primary-v3.png',
'create-flow-panel',
@@ -53,18 +53,18 @@ foreach ($className in @('create-flow-panel', 'flow-primary-action')) {
Assert-NoCssSurface -Content $g03 -ClassName $className
}
-if (-not (Test-Path -LiteralPath $panelPath)) { throw 'G03 requires a final opaque flow-panel bitmap' }
+if (-not (Test-Path -LiteralPath $panelPath)) { throw 'G03 requires the accepted genealogy paper panel bitmap' }
Add-Type -AssemblyName System.Drawing
$panel = [System.Drawing.Bitmap]::FromFile($panelPath)
try {
- if ($panel.Width -ne 1122 -or $panel.Height -ne 1500) { throw 'G03 flow-panel asset must be 1122 x 1500px' }
+ if ($panel.Width -ne 1122 -or $panel.Height -ne 1402) { throw 'G03 paper panel asset must be 1122 x 1402px' }
$corners = @(
$panel.GetPixel(0, 0).A,
$panel.GetPixel(($panel.Width - 1), 0).A,
$panel.GetPixel(0, ($panel.Height - 1)).A,
$panel.GetPixel(($panel.Width - 1), ($panel.Height - 1)).A
)
- if (($corners | Where-Object { $_ -ne 255 }).Count -ne 0) { throw 'G03 flow-panel outer corners must be opaque' }
+ if (($corners | Where-Object { $_ -ne 0 }).Count -ne 0) { throw 'G03 paper panel outer corners must remain transparent' }
} finally {
$panel.Dispose()
}
diff --git a/tests/g03-visual-states-contract.ps1 b/tests/g03-visual-states-contract.ps1
new file mode 100644
index 0000000..5342acb
--- /dev/null
+++ b/tests/g03-visual-states-contract.ps1
@@ -0,0 +1,23 @@
+$ErrorActionPreference = 'Stop'
+
+$root = Split-Path -Parent $PSScriptRoot
+$g03 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue') -Raw -Encoding utf8
+
+function Assert-Match {
+ param([string]$Content, [string]$Pattern, [string]$Message)
+ if ($Content -notmatch $Pattern) { throw $Message }
+}
+
+if ($g03 -match '返回') { throw 'G03 header must use only the approved image back arrow' }
+Assert-Match -Content $g03 -Pattern '(?s)\.flow-header__back,\s*\.flow-header__side\s*\{[^}]*width:\s*72rpx;' -Message 'G03 header side slots must preserve centered title after removing back text'
+Assert-Match -Content $g03 -Pattern '(?s)\.flow-header__back-icon\s*\{[^}]*width:\s*34rpx;[^}]*height:\s*34rpx;' -Message 'G03 back arrow must use the approved visible size'
+Assert-Match -Content $g03 -Pattern '(?s)\.flow-note,\s*\.flow-rule__note\s*\{[^}]*font-size:\s*25rpx;' -Message 'G03 guidance copy must use the approved readable size'
+Assert-Match -Content $g03 -Pattern '(?s)\.flow-rule__option\s*\{[^}]*font-size:\s*23rpx;' -Message 'G03 visibility options must use the approved readable size'
+Assert-Match -Content $g03 -Pattern '(?s)\.field-error\s*\{[^}]*font-size:\s*24rpx;[^}]*line-height:\s*34rpx;' -Message 'G03 field errors must use the approved readable size'
+Assert-Match -Content $g03 -Pattern '(?s)\.flow-error\s*\{[^}]*font-size:\s*25rpx;[^}]*line-height:\s*36rpx;' -Message 'G03 submit errors must use the approved readable size'
+
+foreach ($asset in @('a01-scroll-dialog-v3.png', 'a01-scroll-primary-v3.png', 'a01-scroll-secondary-v3.png')) {
+ if ($g03 -notmatch [regex]::Escape($asset)) { throw "G03 must retain approved shared asset: $asset" }
+}
+
+Write-Output 'G03-VISUAL-STATES-CONTRACT PASS'
diff --git a/tests/g05-all-states-visual-contract.ps1 b/tests/g05-all-states-visual-contract.ps1
new file mode 100644
index 0000000..6b91b07
--- /dev/null
+++ b/tests/g05-all-states-visual-contract.ps1
@@ -0,0 +1,19 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$g05 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g05-genealogy-overview.vue') -Raw -Encoding utf8
+
+function Assert-Match { param([string]$Pattern,[string]$Message); if ($g05 -notmatch $Pattern) { throw $Message } }
+
+Assert-Match 'overview-surface--state' 'G05 system states must own an independent surface'
+Assert-Match 'class="overview-state__frame".*g01-empty-panel-frame\.png' 'G05 system states must reuse the transparent gold frame'
+Assert-Match 'class="overview-state__seal".*brand-seal\.png' 'G05 non-loading states must use the approved red-gold seal'
+Assert-Match '(?s)\.overview-surface--state\s+\.overview-surface__skin\s*\{[^}]*display:\s*none;' 'G05 system states must hide the legacy function-grid skin'
+Assert-Match '(?s)\.overview-state__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' 'G05 state seal must use the approved size'
+Assert-Match 'overview-action-lock' 'G05 member mode must explain owner-only actions instead of leaving blank cells'
+Assert-Match '(?s)\.overview-actions--member\s*\{[^}]*grid-template-rows:\s*1fr 1fr;' 'G05 member actions must retain the full two-row surface'
+Assert-Match '(?s)\.overview-public__details > view:nth-child\(5\)\s*\{[^}]*grid-column:\s*1 / -1;' 'G05 public fifth identity detail must span the full row'
+Assert-Match '(?s)\.overview-public__details > view:nth-child\(5\)\s*\{[^}]*position:\s*absolute;[^}]*top:\s*250rpx;' 'G05 public parent genealogy must occupy the dedicated identity row'
+Assert-Match '(?s)\s*.*publicDescription.*\s* {
}
throw new Error(message)
}
-const open = async (send, query, selector) => {
+const waitForPageLoad = async (socket, trigger) => new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ socket.removeEventListener('message', onMessage)
+ reject(new Error('G05 reload did not emit Page.loadEventFired'))
+ }, 4000)
+ const onMessage = (event) => {
+ const message = JSON.parse(event.data)
+ if (message.method !== 'Page.loadEventFired') return
+ clearTimeout(timeout)
+ socket.removeEventListener('message', onMessage)
+ resolve()
+ }
+ socket.addEventListener('message', onMessage)
+ Promise.resolve().then(trigger).catch((error) => {
+ clearTimeout(timeout)
+ socket.removeEventListener('message', onMessage)
+ reject(error)
+ })
+})
+const open = async (socket, send, query, selector) => {
const url = `${origin}/#${route}${query}`
await send('Page.navigate', { url })
await waitFor(send, `location.href === ${JSON.stringify(url)}`, 'G05 navigation failed')
const documentTimeOrigin = await valueOf(send, 'performance.timeOrigin')
- await send('Page.reload')
+ await waitForPageLoad(socket, () => send('Page.reload'))
await waitFor(send, `performance.timeOrigin !== ${JSON.stringify(documentTimeOrigin)}`, `G05 ${query} did not reload into a fresh document`)
await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `G05 ${query} did not render`)
}
@@ -57,27 +76,27 @@ const run = async () => {
try {
await send('Page.enable')
await send('Runtime.enable')
- await open(send, '?state=empty', '.overview-state--empty')
- await open(send, '?state=error&genealogyId=1001', '.overview-state--error')
- await open(send, '?state=no-permission&genealogyId=1001', '.overview-state--no-permission')
- await open(send, '?mode=public&genealogyId=2001', '.overview-public')
+ await open(socket, send, '?state=empty', '.overview-state--empty')
+ await open(socket, send, '?state=error&genealogyId=1001', '.overview-state--error')
+ await open(socket, send, '?state=no-permission&genealogyId=1001', '.overview-state--no-permission')
+ await open(socket, send, '?mode=public&genealogyId=2001', '.overview-public')
if ((await valueOf(send, "document.querySelectorAll('.overview-public__details > uni-view').length")) !== 5) throw new Error('G05 public preview is missing identity details')
if (await valueOf(send, "Boolean(document.querySelector('.overview-actions'))")) throw new Error('G05 public preview exposed member actions')
await valueOf(send, "document.querySelector('.overview-public__action').click()")
await waitFor(send, "location.href.includes('/pages/genealogy/g08-join-application?source=search&genealogyId=2001')", 'G05 public apply action did not open G08 search source')
- await open(send, '?role=member&genealogyId=1001', '.overview-ready')
+ await open(socket, send, '?role=member&genealogyId=1001', '.overview-ready')
if ((await valueOf(send, "document.querySelectorAll('.overview-action').length")) !== 2) throw new Error('G05 member overview exposed owner actions')
if (await valueOf(send, "document.querySelector('.header-action')?.textContent.trim().length > 0")) throw new Error('G05 member overview exposed management action')
- await open(send, '?genealogyId=1001', '.overview-ready')
+ await open(socket, send, '?genealogyId=1001', '.overview-ready')
if ((await valueOf(send, "document.querySelectorAll('.overview-action').length")) !== 4) throw new Error('G05 owner overview is missing management actions')
await valueOf(send, "document.querySelector('.overview-action--ancestor')?.click()")
await waitFor(send, "location.href.includes('/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=1001')", 'G05 ancestor action did not open the G03 ancestor step')
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true })
- await open(send, '?mode=public&genealogyId=2001', '.overview-public')
+ await open(socket, send, '?mode=public&genealogyId=2001', '.overview-public')
const width = await valueOf(send, 'document.documentElement.scrollWidth')
if (width > size.width + 1) throw new Error(`G05 has horizontal overflow at ${size.width}x${size.height}`)
}
diff --git a/tests/g06-search-flow-contract.ps1 b/tests/g06-search-flow-contract.ps1
index 241241d..dbb4a24 100644
--- a/tests/g06-search-flow-contract.ps1
+++ b/tests/g06-search-flow-contract.ps1
@@ -78,6 +78,11 @@ foreach ($className in @('search-title-strip', 'search-field', 'search-action'))
if ($g06 -notmatch '(?s)\.search-controls\s*\{[^}]*display:\s*flex[^}]*align-items:\s*center[^}]*gap:\s*20rpx') {
throw 'G06 search input and action must form one compact control row'
}
+Assert-Contains -Content $g06 -Expected 'class="status-retry"' -Message 'G06 error retry action is missing'
+Assert-Contains -Content $g06 -Expected 'src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"' -Message 'G06 error retry must use the approved A-series primary scroll asset'
+if ($g06 -notmatch '(?s)\.status-retry\s*\{[^}]*width:\s*300rpx;[^}]*height:\s*76rpx;') {
+ throw 'G06 error retry must use the approved compact action size'
+}
Add-Type -AssemblyName System.Drawing
foreach ($asset in @(
diff --git a/tests/g09-all-states-visual-contract.ps1 b/tests/g09-all-states-visual-contract.ps1
new file mode 100644
index 0000000..8e406db
--- /dev/null
+++ b/tests/g09-all-states-visual-contract.ps1
@@ -0,0 +1,18 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$page = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g09-my-applications.vue') -Raw -Encoding utf8
+
+$rules = @(
+ '(?s)\.application-intro text:last-child\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.application-card__time\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.application-card__relation\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.application-card__status\s*\{[^}]*font-size:\s*25rpx;',
+ '(?s)\.application-card__hint\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.application-card__action\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.application-state-card__copy text:last-child\s*\{[^}]*font-size:\s*24rpx;'
+)
+foreach ($rule in $rules) {
+ if ($page -notmatch $rule) { throw "G09 readable visual rule missing: $rule" }
+}
+
+Write-Output 'G09-ALL-STATES-VISUAL-CONTRACT PASS'
diff --git a/tests/g10-all-states-visual-contract.ps1 b/tests/g10-all-states-visual-contract.ps1
new file mode 100644
index 0000000..bb789e7
--- /dev/null
+++ b/tests/g10-all-states-visual-contract.ps1
@@ -0,0 +1,31 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$page = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g10-application-review.vue') -Raw -Encoding utf8
+
+foreach ($required in @(
+ 'class="review-result"',
+ 'review-result--approved',
+ 'review-result--rejected',
+ 'a01-secondary-button-v2.png',
+ 'a01-primary-button-v2.png',
+ 'a01-scroll-secondary-v3.png',
+ 'a01-scroll-primary-v3.png'
+)) {
+ if ($page -notmatch [regex]::Escape($required)) { throw "G10 result visual missing: $required" }
+}
+
+$rules = @(
+ '(?s)\.review-intro text:nth-child\(2\)\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.application-card__phone\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.application-card__time\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.application-card__relation\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.review-action\s*\{[^}]*width:\s*148rpx;[^}]*height:\s*68rpx;',
+ '(?s)\.review-action text\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.review-result\s*\{[^}]*width:\s*308rpx;[^}]*height:\s*68rpx;',
+ '(?s)\.review-state-card__copy text:last-child\s*\{[^}]*font-size:\s*24rpx;'
+)
+foreach ($rule in $rules) {
+ if ($page -notmatch $rule) { throw "G10 readable visual rule missing: $rule" }
+}
+
+Write-Output 'G10-ALL-STATES-VISUAL-CONTRACT PASS'
diff --git a/tests/g11-g12-all-states-visual-contract.ps1 b/tests/g11-g12-all-states-visual-contract.ps1
new file mode 100644
index 0000000..9065822
--- /dev/null
+++ b/tests/g11-g12-all-states-visual-contract.ps1
@@ -0,0 +1,26 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$g11 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g11-genealogy-settings.vue') -Raw -Encoding utf8
+$g12 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g12-generation-poems.vue') -Raw -Encoding utf8
+
+foreach ($rule in @(
+ '(?s)\.settings-form__copy, \.settings-result__copy\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.settings-field-error\s*\{[^}]*font-size:\s*24rpx;[^}]*line-height:\s*34rpx;',
+ '(?s)\.visibility-block__hint\s*\{[^}]*font-size:\s*22rpx;',
+ '(?s)\.visibility-option text\s*\{[^}]*font-size:\s*24rpx;'
+)) {
+ if ($g11 -notmatch $rule) { throw "G11 readable visual rule missing: $rule" }
+}
+
+foreach ($rule in @(
+ '(?s)\.poem-list__copy\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.poem-row__number\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.poem-row__status\s*\{[^}]*font-size:\s*22rpx;',
+ '(?s)\.poem-field-error\s*\{[^}]*font-size:\s*24rpx;[^}]*line-height:\s*34rpx;',
+ '(?s)\.poem-policy > text\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.poem-preview\s*\{[^}]*font-size:\s*23rpx;'
+)) {
+ if ($g12 -notmatch $rule) { throw "G12 readable visual rule missing: $rule" }
+}
+
+Write-Output 'G11-G12-ALL-STATES-VISUAL-CONTRACT PASS'
diff --git a/tests/g11-g12-runtime-navigation-contract.ps1 b/tests/g11-g12-runtime-navigation-contract.ps1
new file mode 100644
index 0000000..bb1958f
--- /dev/null
+++ b/tests/g11-g12-runtime-navigation-contract.ps1
@@ -0,0 +1,13 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$runtime = Get-Content -LiteralPath (Join-Path $root 'tests/g11-g12-settings-poems-runtime-smoke.js') -Raw -Encoding utf8
+
+foreach ($required in @(
+ 'const waitForPageLoad = async (socket, trigger)',
+ "message.method === 'Page.loadEventFired'",
+ "await waitForPageLoad(socket, () => send('Page.reload'))"
+)) {
+ if ($runtime -notmatch [regex]::Escape($required)) { throw "G11/G12 runtime navigation synchronization missing: $required" }
+}
+
+Write-Output 'G11-G12-RUNTIME-NAVIGATION-CONTRACT PASS'
diff --git a/tests/g11-g12-settings-poems-contract.ps1 b/tests/g11-g12-settings-poems-contract.ps1
index 0a8183b..dbdbc48 100644
--- a/tests/g11-g12-settings-poems-contract.ps1
+++ b/tests/g11-g12-settings-poems-contract.ps1
@@ -22,7 +22,7 @@ $catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Ra
foreach ($page in @($g11, $g12)) {
if ($page -match '' 'G11-G12 must use the accepted shared genealogy background'
- Assert-Contains $page 'g03-create-flow-panel.png' 'G11-G12 forms must reuse the complete genealogy paper panel'
+ Assert-Contains $page 'g01-empty-panel-frame.png' 'G11-G12 forms must reuse the complete genealogy paper panel'
if ($page -match "@/utils/api\.js|\bappApi\b") { throw 'G11-G12 design phase must not connect the API layer' }
}
@@ -34,7 +34,7 @@ foreach ($required in @(
'settings-state--no-permission',
'const nameError = ref',
'class="settings-feedback"',
- 'g06-search-input-wide.png',
+ 'g-form-field-frame.png',
'a01-scroll-primary-v3.png',
'const genealogyDraft',
'query.genealogyId',
@@ -51,7 +51,7 @@ foreach ($required in @(
'poem-state--no-permission',
'const poemError = ref',
'class="poem-feedback"',
- 'g06-search-input-wide.png',
+ 'g-form-field-frame.png',
'a01-scroll-primary-v3.png',
'a01-scroll-secondary-v3.png',
'const poemRows',
diff --git a/tests/g11-g12-settings-poems-runtime-smoke.js b/tests/g11-g12-settings-poems-runtime-smoke.js
index cd7b304..337c987 100644
--- a/tests/g11-g12-settings-poems-runtime-smoke.js
+++ b/tests/g11-g12-settings-poems-runtime-smoke.js
@@ -35,11 +35,31 @@ const waitFor = async (send, expression, message) => {
}
throw new Error(message)
}
-const open = async (send, route, query, selector) => {
+const waitForPageLoad = async (socket, trigger) => new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ socket.removeEventListener('message', onMessage)
+ reject(new Error('Page reload did not emit Page.loadEventFired'))
+ }, 4000)
+ const onMessage = (event) => {
+ const message = JSON.parse(event.data)
+ if (message.method === 'Page.loadEventFired') {
+ clearTimeout(timeout)
+ socket.removeEventListener('message', onMessage)
+ resolve()
+ }
+ }
+ socket.addEventListener('message', onMessage)
+ Promise.resolve().then(trigger).catch((error) => {
+ clearTimeout(timeout)
+ socket.removeEventListener('message', onMessage)
+ reject(error)
+ })
+})
+const open = async (socket, send, route, query, selector) => {
const url = `${origin}/#${route}${query}`
await send('Page.navigate', { url })
await waitFor(send, `location.href === ${JSON.stringify(url)}`, `${route} navigation failed`)
- await send('Page.reload')
+ await waitForPageLoad(socket, () => send('Page.reload'))
await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `${route}${query} did not render`)
}
@@ -48,11 +68,11 @@ const run = async () => {
try {
await send('Page.enable')
await send('Runtime.enable')
- await open(send, '/pages/genealogy/g11-genealogy-settings', '?genealogyId=1001', '.settings-state--form')
- await open(send, '/pages/genealogy/g11-genealogy-settings', '?state=success&genealogyId=1001', '.settings-state--success')
- await open(send, '/pages/genealogy/g12-generation-poems', '?genealogyId=1001', '.poem-state--list')
- await open(send, '/pages/genealogy/g12-generation-poems', '?state=empty&genealogyId=1001', '.poem-state--empty')
- await open(send, '/pages/genealogy/g12-generation-poems', '?state=edit&genealogyId=1001', '.poem-state--edit')
+ await open(socket, send, '/pages/genealogy/g11-genealogy-settings', '?genealogyId=1001', '.settings-state--form')
+ await open(socket, send, '/pages/genealogy/g11-genealogy-settings', '?state=success&genealogyId=1001', '.settings-state--success')
+ await open(socket, send, '/pages/genealogy/g12-generation-poems', '?genealogyId=1001', '.poem-state--list')
+ await open(socket, send, '/pages/genealogy/g12-generation-poems', '?state=empty&genealogyId=1001', '.poem-state--empty')
+ await open(socket, send, '/pages/genealogy/g12-generation-poems', '?state=edit&genealogyId=1001', '.poem-state--edit')
process.stdout.write('G11-G12-SETTINGS-POEMS-RUNTIME-SMOKE PASS\n')
} finally {
socket.close()
diff --git a/tests/genealogy-shared-background-contract.ps1 b/tests/genealogy-shared-background-contract.ps1
new file mode 100644
index 0000000..0e74332
--- /dev/null
+++ b/tests/genealogy-shared-background-contract.ps1
@@ -0,0 +1,66 @@
+$ErrorActionPreference = 'Stop'
+Add-Type -AssemblyName System.Drawing
+
+$repoRoot = Split-Path -Parent $PSScriptRoot
+$pagesJsonPath = Join-Path $repoRoot 'pages.json'
+$componentPath = Join-Path $repoRoot 'components/GenealogyPageBackground.vue'
+$assetRelativePath = 'static/assets/modules/genealogy/opaque/genealogy-page-background-long.png'
+$assetPath = Join-Path $repoRoot $assetRelativePath
+$expectedHash = '9aa9ebdd231eddbc151e238ba340a00f8603f23143117ad8c93009947dcca2e2'
+
+function Assert-Contract {
+ param(
+ [bool]$Condition,
+ [string]$Message
+ )
+
+ if (-not $Condition) {
+ throw "G shared background contract failed: $Message"
+ }
+}
+
+$pagesConfig = Get-Content -Raw -Encoding UTF8 $pagesJsonPath | ConvertFrom-Json
+$genealogyRoutes = @(
+ $pagesConfig.pages |
+ Where-Object { $_.path -match '^pages/genealogy/g\d{2}-' } |
+ ForEach-Object { $_.path }
+)
+
+Assert-Contract ($genealogyRoutes.Count -eq 9) "expected 9 active G routes from pages.json, got $($genealogyRoutes.Count)"
+Assert-Contract (Test-Path -LiteralPath $componentPath) 'missing components/GenealogyPageBackground.vue'
+Assert-Contract (Test-Path -LiteralPath $assetPath) "missing $assetRelativePath"
+
+$componentSource = Get-Content -Raw -Encoding UTF8 $componentPath
+Assert-Contract ($componentSource -match [regex]::Escape('/static/assets/modules/genealogy/opaque/genealogy-page-background-long.png')) 'component must own the approved long runtime asset path'
+Assert-Contract ($componentSource -match 'mode="widthFix"') 'shared artwork must use widthFix'
+Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background\s*\{.*position:\s*fixed;.*inset:\s*0;.*background:\s*#[0-9a-fA-F]{6};.*pointer-events:\s*none;') 'shared background layer must be fixed, full-screen, paper-colored, and non-interactive'
+Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background__art\s*\{.*width:\s*100%;') 'shared artwork must render at full page width'
+Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background__art\s*\{.*bottom:\s*0;') 'shared artwork must stay bottom-aligned on tall viewports'
+Assert-Contract ($componentSource -notmatch '(?s)\.genealogy-page-background__art\s*\{[^}]*top:\s*0;') 'shared artwork must not remain top-aligned'
+Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background__art\s*\{[^}]*opacity:\s*0\.28;') 'shared artwork must use the approved 28% opacity'
+
+$image = [System.Drawing.Image]::FromFile($assetPath)
+try {
+ Assert-Contract ($image.Width -eq 1440 -and $image.Height -eq 3600) "expected 1440x3600 runtime background, got $($image.Width)x$($image.Height)"
+}
+finally {
+ $image.Dispose()
+}
+$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $assetPath).Hash.ToLowerInvariant()
+Assert-Contract ($actualHash -eq $expectedHash) "shared runtime asset SHA-256 mismatch: $actualHash"
+
+foreach ($route in $genealogyRoutes) {
+ $pagePath = Join-Path $repoRoot ($route + '.vue')
+ Assert-Contract (Test-Path -LiteralPath $pagePath) "missing route file $route.vue"
+
+ $pageSource = Get-Content -Raw -Encoding UTF8 $pagePath
+ Assert-Contract ($pageSource -match 'import\s+GenealogyPageBackground\s+from\s+["'']@/components/GenealogyPageBackground\.vue["''];?') "$route must explicitly import GenealogyPageBackground"
+ $componentCount = ([regex]::Matches($pageSource, '')).Count
+ Assert-Contract ($componentCount -eq 1) "$route must render exactly one GenealogyPageBackground, got $componentCount"
+ Assert-Contract ($pageSource -notmatch 'page-paper\.jpg') "$route still references page-paper.jpg"
+ Assert-Contract ($pageSource -notmatch 'footer-mountain-bamboo\.png') "$route still references footer-mountain-bamboo.png"
+ Assert-Contract ($pageSource -notmatch [regex]::Escape('/static/assets/modules/genealogy/opaque/genealogy-page-background-long.png')) "$route must consume the component instead of the asset directly"
+ Assert-Contract ($pageSource -notmatch '(?s)\.genealogy-page-background__art\s*\{[^}]*opacity:') "$route must not override the shared background opacity"
+}
+
+Write-Output 'PASS G genealogy shared background contract'
diff --git a/tests/global-heritage-visual-system-contract.ps1 b/tests/global-heritage-visual-system-contract.ps1
new file mode 100644
index 0000000..967729f
--- /dev/null
+++ b/tests/global-heritage-visual-system-contract.ps1
@@ -0,0 +1,86 @@
+$ErrorActionPreference = 'Stop'
+Add-Type -AssemblyName System.Drawing
+
+$root = Split-Path -Parent $PSScriptRoot
+$componentsRoot = Join-Path $root 'components'
+$pagesRoot = Join-Path $root 'pages'
+
+function Read-Utf8([string]$Path) {
+ return Get-Content -Raw -Encoding UTF8 $Path
+}
+
+foreach ($component in @('AppButton.vue', 'AppDialog.vue', 'AppToast.vue', 'AppLoading.vue', 'ModulePageBackground.vue')) {
+ $path = Join-Path $componentsRoot $component
+ if (-not (Test-Path -LiteralPath $path)) { throw "Missing global visual component: $component" }
+}
+
+$button = Read-Utf8 (Join-Path $componentsRoot 'AppButton.vue')
+if ($button -notmatch 'a01-scroll-primary-v3\.png') { throw 'AppButton does not own the approved A-series primary scroll asset.' }
+if ($button -notmatch 'a01-scroll-secondary-v3\.png') { throw 'AppButton does not own the approved A-series secondary scroll asset.' }
+
+$dialog = Read-Utf8 (Join-Path $componentsRoot 'AppDialog.vue')
+if ($dialog -notmatch 'a01-scroll-dialog-v3\.png') { throw 'AppDialog does not own the approved A-series dialog asset.' }
+
+$toast = Read-Utf8 (Join-Path $componentsRoot 'AppToast.vue')
+if ($toast -notmatch 'a01-scroll-toast-v3\.png') { throw 'AppToast does not own the approved A-series toast asset.' }
+
+$header = Read-Utf8 (Join-Path $componentsRoot 'PageHeader.vue')
+if ($header -notmatch 'chevron-right\.png') { throw 'PageHeader does not use the approved image arrow.' }
+if ($header -match '\{\{\s*backLabel\s*\}\}') { throw 'PageHeader still renders a text back label.' }
+if ($header -notmatch 'scaleX\(-1\)') { throw 'PageHeader back arrow is not oriented toward the previous page.' }
+
+$activeSources = @(
+ Get-ChildItem -LiteralPath $componentsRoot -Recurse -File -Filter '*.vue'
+ Get-ChildItem -LiteralPath $pagesRoot -Recurse -File -Filter '*.vue' | Where-Object { $_.Name -ne 'a06-auth-status.vue' }
+)
+foreach ($source in $activeSources) {
+ $content = Read-Utf8 $source.FullName
+ foreach ($forbidden in @(
+ 'uni.showToast',
+ 'uni.showModal',
+ 'uni.showLoading',
+ 'uni.showActionSheet',
+ '/static/assets/foundation/opaque/a01-primary-button.png',
+ '/static/assets/foundation/opaque/a01-secondary-button.png'
+ )) {
+ if ($content.Contains($forbidden)) { throw "$($source.FullName) still contains forbidden global UI path: $forbidden" }
+ }
+}
+
+$background = Read-Utf8 (Join-Path $componentsRoot 'ModulePageBackground.vue')
+$moduleAssets = @{
+ tree = 'tree-page-background-long.png'
+ family = 'family-page-background-long.png'
+ records = 'records-page-background-long.png'
+ notification = 'notification-page-background-long.png'
+ profile = 'profile-page-background-long.png'
+}
+foreach ($entry in $moduleAssets.GetEnumerator()) {
+ if ($background -notmatch [regex]::Escape($entry.Value)) { throw "ModulePageBackground is missing $($entry.Key) mapping." }
+ $asset = Get-ChildItem -LiteralPath (Join-Path $root 'static/assets/modules') -Recurse -File -Filter $entry.Value
+ if (-not $asset) { throw "Missing module background asset: $($entry.Value)" }
+ $image = [System.Drawing.Image]::FromFile($asset.FullName)
+ try {
+ if ($image.Width -ne 1440 -or $image.Height -ne 3600) {
+ throw "$($entry.Value) must be 1440x3600, got $($image.Width)x$($image.Height)."
+ }
+ }
+ finally {
+ $image.Dispose()
+ }
+}
+
+$modulePage = Read-Utf8 (Join-Path $componentsRoot 'ModulePage.vue')
+foreach ($token in @(']+description="[^"]+"\s*/>' -Message 'T01 must render AppLoading before its tree and state cards'
+Assert-Match -Content $pages.T03 -Pattern ']+description="[^"]+"\s*/>' -Message 'T03 must render AppLoading before detail and error states'
+Assert-Match -Content $pages.T07 -Pattern ']+description="[^"]+"\s*/>' -Message 'T07 must render AppLoading before list and state cards'
+Assert-Match -Content $pages.F01 -Pattern ']+description="[^"]+"\s*/>' -Message 'F01 must render AppLoading before feed content'
+Assert-Match -Content $pages.N01 -Pattern ']+description="[^"]+"\s*/>' -Message 'N01 must render AppLoading before message content'
+
+Assert-Match -Content $pages.T01 -Pattern 'query\.state === ''loading''' -Message 'T01 must preserve its loading presentation state'
+Assert-Match -Content $pages.T03 -Pattern 'query\.state\s*===\s*''loading''\s*\?\s*''loading''' -Message 'T03 must preserve its loading presentation state'
+Assert-Match -Content $pages.T07 -Pattern 'query\.state\s*===\s*''loading''\s*\?\s*''loading''' -Message 'T07 must preserve its loading presentation state'
+Assert-Match -Content $pages.F01 -Pattern 'query\.state\s*===\s*''loading''\s*\?\s*''loading''' -Message 'F01 must preserve its loading presentation state'
+Assert-Match -Content $pages.N01 -Pattern 'query\.state\s*===\s*''loading''\s*\?\s*''loading''' -Message 'N01 must preserve its loading presentation state'
+
+Write-Output 'MODULE-APP-LOADING-CONTRACT PASS'
diff --git a/tests/module-baseline-promotion-contract.ps1 b/tests/module-baseline-promotion-contract.ps1
new file mode 100644
index 0000000..af56005
--- /dev/null
+++ b/tests/module-baseline-promotion-contract.ps1
@@ -0,0 +1,33 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+
+function Read-Utf8([string]$Path) {
+ [System.IO.File]::ReadAllText((Join-Path $root $Path), [System.Text.Encoding]::UTF8)
+}
+
+$targets = @(
+ 'pages/family/f02-publish-feed.vue',
+ 'pages/tree/t03-member-profile.vue',
+ 'pages/tree/t08-member-states.vue',
+ 'pages/genealogy/g03-create-genealogy.vue',
+ 'pages/genealogy/g08-join-application.vue',
+ 'pages/genealogy/g09-my-applications.vue',
+ 'pages/genealogy/g10-application-review.vue',
+ 'pages/genealogy/g11-genealogy-settings.vue',
+ 'pages/genealogy/g12-generation-poems.vue'
+)
+
+foreach ($target in $targets) {
+ $content = Read-Utf8 $target
+ foreach ($forbidden in @('g03-create-flow-panel.png', 'g06-search-input-wide.png', 'application-status-card.png')) {
+ if ($content.Contains($forbidden)) { throw "$target still uses obsolete opaque cross-module surface: $forbidden" }
+ }
+}
+
+$modulePage = Read-Utf8 'components/ModulePage.vue'
+if ($modulePage.Contains('class="page-intro"')) { throw 'ModulePage must remove repeated numbered intro blocks' }
+foreach ($required in @('module-content-frame.png', 'module-field-frame.png', ' new Promise((resolve) => setTimeout(resolve, ms))
+const origin = process.argv[2] || 'http://localhost:5173'
+const sizes = [
+ { width: 320, height: 568 },
+ { width: 360, height: 640 },
+ { width: 360, height: 800 },
+ { width: 412, height: 915 }
+]
+const routes = [
+ ['/pages/family/f01-family-feed', '.family-page'],
+ ['/pages/family/f02-publish-feed', '.publish-page'],
+ ['/pages/family/f04-article-list', '.module-page--list'],
+ ['/pages/family/f06-article-editor', '.module-page--form'],
+ ['/pages/family/f03-feed-detail', '.module-page--detail'],
+ ['/pages/records/r08-growth-journal', '.module-page--timeline'],
+ ['/pages/profile/m03-security-settings', '.module-page--settings'],
+ ['/pages/family/f10-video-list', '.module-page--status'],
+ ['/pages/notification/n01-message-center', '.notice-page'],
+ ['/pages/profile/m01-profile-home', '.profile-page']
+]
+
+const connect = async () => {
+ const pages = await (await fetch('http://127.0.0.1:9222/json/list')).json()
+ const page = pages.find((item) => item.type === 'page' && item.url.startsWith(`${origin}/`))
+ if (!page) throw new Error(`Chrome debugging has no ${origin} page`)
+ const socket = new WebSocket(page.webSocketDebuggerUrl)
+ await new Promise((resolve, reject) => {
+ socket.addEventListener('open', resolve, { once: true })
+ socket.addEventListener('error', reject, { once: true })
+ })
+ let id = 0
+ const pending = new Map()
+ socket.addEventListener('message', (event) => {
+ const message = JSON.parse(event.data)
+ const request = pending.get(message.id)
+ if (!request) return
+ pending.delete(message.id)
+ message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result)
+ })
+ const send = (method, params = {}) => new Promise((resolve, reject) => {
+ id += 1
+ pending.set(id, { resolve, reject })
+ socket.send(JSON.stringify({ id, method, params }))
+ })
+ return { socket, send }
+}
+
+const valueOf = async (send, expression) => (await send('Runtime.evaluate', { expression, returnByValue: true })).result?.value
+const waitFor = async (send, expression, message) => {
+ for (let attempt = 0; attempt < 50; attempt += 1) {
+ if (await valueOf(send, expression)) return
+ await sleep(80)
+ }
+ throw new Error(message)
+}
+
+const run = async () => {
+ const { socket, send } = await connect()
+ let auditId = 0
+ try {
+ await send('Page.enable')
+ await send('Runtime.enable')
+ for (const size of sizes) {
+ await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true, screenWidth: size.width, screenHeight: size.height })
+ for (const [route, selector] of routes) {
+ auditId += 1
+ const url = `${origin}/?responsiveAudit=${auditId}#${route}`
+ await send('Page.navigate', { url })
+ await waitFor(send, `location.href === ${JSON.stringify(url)}`, `Navigation failed: ${route} at ${size.width}x${size.height}`)
+ await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `State missing: ${route} at ${size.width}x${size.height}`)
+ const metrics = await valueOf(send, `(() => {
+ const root = document.querySelector(${JSON.stringify(selector)})
+ const rect = root.getBoundingClientRect()
+ const visible = Array.from(document.querySelectorAll('.app-button,.list-card,.detail-card,.form-row,.settings-row,.notice-card,.profile-menu'))
+ .filter((node) => node.getClientRects().length)
+ .map((node) => node.getBoundingClientRect())
+ return {
+ documentWidth: document.documentElement.scrollWidth,
+ rootLeft: rect.left,
+ rootRight: rect.right,
+ childOverflow: visible.some((item) => item.left < -1 || item.right > innerWidth + 1)
+ }
+ })()`)
+ assert(metrics.documentWidth <= size.width + 1, `${route} has horizontal document overflow at ${size.width}x${size.height}`)
+ assert(metrics.rootLeft >= -1 && metrics.rootRight <= size.width + 1, `${route} root exceeds viewport at ${size.width}x${size.height}`)
+ assert.strictEqual(metrics.childOverflow, false, `${route} has a visible control outside the viewport at ${size.width}x${size.height}`)
+ }
+ }
+ process.stdout.write('MODULE-SERIES-RESPONSIVE-RUNTIME-SMOKE PASS\n')
+ } finally {
+ try { await send('Emulation.clearDeviceMetricsOverride') } catch (_) {}
+ socket.close()
+ }
+}
+
+run().catch((error) => {
+ process.stderr.write(`${error.stack || error.message}\n`)
+ process.exit(1)
+})
diff --git a/tests/n-series-all-states-visual-contract.ps1 b/tests/n-series-all-states-visual-contract.ps1
new file mode 100644
index 0000000..89cee8c
--- /dev/null
+++ b/tests/n-series-all-states-visual-contract.ps1
@@ -0,0 +1,14 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$n01 = Get-Content -LiteralPath (Join-Path $root 'pages/notification/n01-message-center.vue') -Raw -Encoding utf8
+$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
+
+foreach ($required in @('font-size:\s*21rpx','font-size:\s*23rpx','class="notice-review-action" block',')") { throw 'R01 must own its page instead of delegating to ModulePage.' }
+if ($page -match 'application-status-card\.png') { throw 'R01 must not reuse the application status card.' }
+if ($page -match 'people-intro|brand-seal\.png') { throw 'R01 must not repeat a numbered intro block below the page header.' }
+Assert-Match 'modules/records/transparent/r01-person-name-card\.png' 'R01 must use its dedicated transparent person name card.'
+Assert-Match 'class="people-search"' 'R01 must expose its search region.'
+Assert-Match 'people-state--empty' 'R01 must expose an empty review state.'
+Assert-Match 'people-state--error' 'R01 must expose an error review state.'
+Assert-Match 'AppButton' 'R01 must use the project button component.'
+Assert-Match 'AppToast' 'R01 must use the project Toast component.'
+Assert-Match '/pages/records/r02-person-detail' 'R01 person cards must navigate to R02.'
+Assert-Match '(?s)\.people-search__action\s*\{[^}]*min-height:\s*44px;' 'R01 search action must preserve a 44 CSS px touch height.'
+
+Write-Output 'R01-MODULE-BASELINE-CONTRACT PASS'
diff --git a/tests/root-pages-visual-contract.ps1 b/tests/root-pages-visual-contract.ps1
index 1636544..f710488 100644
--- a/tests/root-pages-visual-contract.ps1
+++ b/tests/root-pages-visual-contract.ps1
@@ -4,8 +4,10 @@ $root=Split-Path -Parent $PSScriptRoot
$files=@('pages/family/f01-family-feed.vue','pages/family/f02-publish-feed.vue','pages/notification/n01-message-center.vue','pages/profile/m01-profile-home.vue')
$pages=@{}
foreach($file in $files){$content=Get-Content -LiteralPath(Join-Path $root $file)-Raw -Encoding utf8;if($content-match"@/utils/api\.js|\bappApi\b"){ throw "$file must not connect the API layer" };Assert-Contains $content 'ModulePageBackground' "$file must use its module background";$pages[$file]=$content}
-foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','application-status-card.png','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
-foreach($required in @('publish-state--form','publish-state--success','publish-state--error','g03-create-flow-panel.png','g06-search-input-wide.png','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
-foreach($required in @('notice-state--list','notice-state--empty','notice-state--error','application-status-card.png','AppButton','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
-foreach($required in @('profile-state--ready','profile-state--error','g03-create-flow-panel.png','g06-search-input-wide.png','application-status-card.png','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
+foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','f01-family-letter-card.png','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
+foreach($required in @('publish-state--form','publish-state--success','publish-state--error','module-content-frame.png','module-field-frame.png','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
+foreach($required in @('notice-state--loading','notice-state--list','notice-state--empty','notice-state--error','n01-notice-card.png','AppButton','AppLoading','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
+if($pages['pages/notification/n01-message-center.vue']-match'application-status-card\.png'){throw 'N01 must not reuse the genealogy application card'}
+foreach($required in @('profile-state--ready','profile-state--error','m01-profile-summary-card.png','brand-seal.png','auth-divider-knot.png','chevron-right.png','AppButton','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
+foreach($forbidden in @('m01-profile-notice-card.png','m01-profile-menu-frame.png','g03-create-flow-panel.png','g06-search-input-wide.png','application-status-card.png')){if($pages['pages/profile/m01-profile-home.vue']-match[regex]::Escape($forbidden)){throw "M01 must not reuse repeated or opaque business asset: $forbidden"}}
Write-Output 'ROOT-PAGES-VISUAL-CONTRACT PASS'
diff --git a/tests/t01-all-states-visual-contract.ps1 b/tests/t01-all-states-visual-contract.ps1
new file mode 100644
index 0000000..9941f9a
--- /dev/null
+++ b/tests/t01-all-states-visual-contract.ps1
@@ -0,0 +1,31 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t01-tree-overview.vue') -Raw -Encoding utf8
+
+foreach ($required in @(
+ 'a01-primary-button-v2.png',
+ 'a01-secondary-button-v2.png',
+ 't01-member-node-standard.png',
+ 't01-member-node-selected.png',
+ 't01-state-panel.png',
+ 't01-member-drawer.png',
+ 'lineage-connector--root',
+ 'lineage-pan-cue'
+)) {
+ if ($page -notmatch [regex]::Escape($required)) { throw "T01 member action asset missing: $required" }
+}
+
+foreach ($rule in @(
+ '(?s)\.tree-toolbar__title text:last-child\s*\{[^}]*font-size:\s*22rpx;',
+ '(?s)\.tree-toolbar__actions\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.generation-band\s*\{[^}]*font-size:\s*22rpx;',
+ '(?s)\.node-relation\s*\{[^}]*font-size:\s*21rpx;',
+ '(?s)\.node-years\s*\{[^}]*font-size:\s*20rpx;',
+ '(?s)\.tree-state-card__copy\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.sheet-meta\s*\{[^}]*font-size:\s*22rpx;',
+ '(?s)\.sheet-action text\s*\{[^}]*font-family:\s*''Microsoft YaHei''[^}]*font-size:\s*22rpx;'
+)) {
+ if ($page -notmatch $rule) { throw "T01 readable visual rule missing: $rule" }
+}
+
+Write-Output 'T01-ALL-STATES-VISUAL-CONTRACT PASS'
diff --git a/tests/t01-tree-state-contract.ps1 b/tests/t01-tree-state-contract.ps1
index 7563294..dffb694 100644
--- a/tests/t01-tree-state-contract.ps1
+++ b/tests/t01-tree-state-contract.ps1
@@ -33,9 +33,14 @@ foreach ($required in @(
'tree-state--empty',
'tree-state--error',
'tree-canvas--state',
- 'g03-create-flow-panel.png',
- 'g06-search-input-wide.png',
- 'application-status-card.png',
+ 'lineage-connector',
+ 'generation-band--twelve',
+ 'generation-band--thirteen',
+ 'generation-band--fourteen',
+ 't01-member-node-standard.png',
+ 't01-member-node-selected.png',
+ 't01-state-panel.png',
+ 't01-member-drawer.png',
'/pages/tree/t03-member-profile',
'/pages/tree/t04-add-relative',
'/pages/tree/t06-edit-relationship',
@@ -43,6 +48,14 @@ foreach ($required in @(
'query.genealogyId'
)) { Assert-Contains $page $required "Missing T01 contract: $required" }
+foreach ($forbidden in @(
+ 'g03-create-flow-panel.png',
+ 'g06-search-input-wide.png',
+ 'application-status-card.png'
+)) {
+ if ($page -match [regex]::Escape($forbidden)) { throw "T01 must not keep opaque surface asset: $forbidden" }
+}
+
foreach ($className in @('tree-canvas', 'member-node', 'member-sheet', 'tree-state-card')) {
Assert-NoCssSurface $page $className
}
diff --git a/tests/t03-t06-all-states-visual-contract.ps1 b/tests/t03-t06-all-states-visual-contract.ps1
new file mode 100644
index 0000000..d698b46
--- /dev/null
+++ b/tests/t03-t06-all-states-visual-contract.ps1
@@ -0,0 +1,25 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$profile = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t03-member-profile.vue') -Raw -Encoding utf8
+$form = Get-Content -LiteralPath (Join-Path $root 'components/tree/TreeMemberForm.vue') -Raw -Encoding utf8
+
+foreach ($rule in @(
+ '(?s)\.member-heading > view:last-child text:last-child\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.member-section-title\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.member-info-row text\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.member-relatives\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.member-error > text:nth-child\(2\)\s*\{[^}]*font-size:\s*24rpx;'
+)) {
+ if ($profile -notmatch $rule) { throw "T03 readable visual rule missing: $rule" }
+}
+
+foreach ($rule in @(
+ '(?s)\.member-form__copy\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.member-field > text\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.member-field input\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.member-form__note\s*\{[^}]*font-size:\s*22rpx;'
+)) {
+ if ($form -notmatch $rule) { throw "T04-T06 readable visual rule missing: $rule" }
+}
+
+Write-Output 'T03-T06-ALL-STATES-VISUAL-CONTRACT PASS'
diff --git a/tests/t03-t08-member-flow-contract.ps1 b/tests/t03-t08-member-flow-contract.ps1
index 5f4e889..575c8b7 100644
--- a/tests/t03-t08-member-flow-contract.ps1
+++ b/tests/t03-t08-member-flow-contract.ps1
@@ -27,7 +27,7 @@ foreach ($path in @('pages/tree/t03-member-profile.vue', 'pages/tree/t07-member-
}
$t03 = $pages['pages/tree/t03-member-profile.vue']
-foreach ($required in @('member-state--detail', 'member-state--error', 'g03-create-flow-panel.png', 'g06-search-input-wide.png', '/pages/tree/t05-edit-member', 'query.personId')) {
+foreach ($required in @('member-state--detail', 'member-state--error', 't01-state-panel.png', 't07-search-input-frame.png', '/pages/tree/t05-edit-member', 'query.personId')) {
Assert-Contains $t03 $required "Missing T03 contract: $required"
}
@@ -38,7 +38,7 @@ if ($form -match "@/utils/api\.js|\bappApi\b") { throw 'TreeMemberForm must not
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
if ($form -match [regex]::Escape($nativeUi)) { throw "TreeMemberForm must use project-owned feedback instead of $nativeUi" }
}
-foreach ($required in @('form-state--form', 'form-state--success', 'form-state--conflict', 'form-state--error', 'g03-create-flow-panel.png', 'g06-search-input-wide.png', 'a01-scroll-primary-v3.png', 'a01-scroll-secondary-v3.png', 'AppDialog', 'ModulePageBackground')) {
+foreach ($required in @('form-state--form', 'form-state--success', 'form-state--conflict', 'form-state--error', 't01-state-panel.png', 't07-search-input-frame.png', 'a01-scroll-primary-v3.png', 'a01-scroll-secondary-v3.png', 'AppDialog', 'ModulePageBackground')) {
Assert-Contains $form $required "Missing shared form contract: $required"
}
foreach ($route in @('t04-add-relative.vue', 't05-edit-member.vue', 't06-edit-relationship.vue')) {
@@ -46,12 +46,12 @@ foreach ($route in @('t04-add-relative.vue', 't05-edit-member.vue', 't06-edit-re
}
$t07 = $pages['pages/tree/t07-member-directory.vue']
-foreach ($required in @('directory-state--list', 'directory-state--empty', 'directory-state--error', 'application-status-card.png', 'g06-search-input-wide.png', '/pages/tree/t03-member-profile')) {
+foreach ($required in @('directory-state--list', 'directory-state--empty', 'directory-state--error', 'list-slip-frame.png', 'directory-card__status', '/pages/tree/t03-member-profile')) {
Assert-Contains $t07 $required "Missing T07 contract: $required"
}
$t08 = $pages['pages/tree/t08-member-states.vue']
-foreach ($required in @('member-status--privacy', 'member-status--deceased', 'member-status--forbidden', 'application-status-card.png', 'query.state')) {
+foreach ($required in @('member-status--privacy', 'member-status--deceased', 'member-status--forbidden', 't01-state-panel.png', 'query.state')) {
Assert-Contains $t08 $required "Missing T08 contract: $required"
}
diff --git a/tests/t07-module-baseline-contract.ps1 b/tests/t07-module-baseline-contract.ps1
new file mode 100644
index 0000000..0d65a93
--- /dev/null
+++ b/tests/t07-module-baseline-contract.ps1
@@ -0,0 +1,33 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t07-member-directory.vue') -Raw -Encoding utf8
+
+function Assert-Match([string]$Pattern, [string]$Message) {
+ if ($page -notmatch $Pattern) { throw $Message }
+}
+
+Assert-Match 'class="directory-context"' 'T07 must show the current genealogy context.'
+Assert-Match '\u6c64\u6c0f\u5bb6\u8c31' 'T07 baseline must identify the current genealogy in the H5 mock state.'
+Assert-Match '''directory-state--loading'': directoryState === ''loading''' 'T07 loading state must have an explicit root class.'
+Assert-Match 'v-if="directoryState === ''list'' \|\| directoryState === ''empty''"' 'T07 search must remain available for list and empty states only.'
+Assert-Match 'class="directory-search__action"[^>]*role="button"[^>]*aria-label="\u67e5\u627e\u6210\u5458"' 'T07 search action must expose button semantics and an accessible label.'
+Assert-Match ']+type="secondary"[^>]+label="\u91cd\u65b0\u67e5\u770b"[^>]+@click="retryDirectory"' 'T07 error state must provide a custom recovery action.'
+Assert-Match 'const retryDirectory = \(\) => \{ directoryState\.value = ''list'' \}' 'T07 retry action must return to the list state.'
+Assert-Match '(?s)\.directory-page\s*\{[^}]*overflow-x:\s*hidden;[^}]*overflow-y:\s*auto;' 'T07 must allow natural vertical scrolling without horizontal overflow.'
+Assert-Match '(?s)\.directory-search\s*\{[^}]*height:\s*44px;' 'T07 search field must preserve a 44 CSS px touch height at every viewport.'
+Assert-Match '(?s)\.directory-search__action\s*\{[^}]*min-width:\s*88rpx;[^}]*min-height:\s*44px;' 'T07 search action must preserve a 44 CSS px touch target.'
+Assert-Match 'modules/tree/transparent/t07-search-input-frame\.png' 'T07 search must use its dedicated transparent input frame.'
+Assert-Match 'modules/genealogy/transparent/list-slip-frame\.png' 'T07 member cards must use the approved transparent list frame.'
+if ($page -match 'g06-search-input-wide\.png|application-status-card\.png') {
+ throw 'T07 must not reuse opaque search or application-status skins.'
+}
+if ($page -match 'row-seal-frame\.png|directory-card__seal') {
+ throw 'T07 member cards must not repeat generation information in decorative seals.'
+}
+Assert-Match 'class="directory-card__status"' 'T07 member identity or profile state must use a dedicated status label.'
+
+foreach ($state in @('loading', 'list', 'empty', 'error')) {
+ Assert-Match $state "T07 must preserve the $state state."
+}
+
+Write-Output 'T07-MODULE-BASELINE-CONTRACT PASS'
diff --git a/tests/t07-module-baseline-runtime-smoke.js b/tests/t07-module-baseline-runtime-smoke.js
new file mode 100644
index 0000000..18aab5c
--- /dev/null
+++ b/tests/t07-module-baseline-runtime-smoke.js
@@ -0,0 +1,181 @@
+const fs = require('fs')
+const path = require('path')
+
+const origin = 'http://localhost:5173'
+const outputDirectory = path.resolve('docs/design/screens/runtime/2026-07-19/t07-baseline')
+const sizes = [
+ { width: 320, height: 568 },
+ { width: 360, height: 640 },
+ { width: 360, height: 800 },
+ { width: 412, height: 915 }
+]
+const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
+
+const connect = async () => {
+ const pages = await (await fetch('http://127.0.0.1:9222/json/list')).json()
+ const projectPages = pages.filter((page) => page.type === 'page' && page.url.startsWith(origin))
+ if (projectPages.length !== 1) throw new Error(`Expected one project page, found ${projectPages.length}`)
+ const socket = new WebSocket(projectPages[0].webSocketDebuggerUrl)
+ await new Promise((resolve, reject) => {
+ socket.addEventListener('open', resolve, { once: true })
+ socket.addEventListener('error', reject, { once: true })
+ })
+ let id = 0
+ const pending = new Map()
+ socket.addEventListener('message', (event) => {
+ const message = JSON.parse(event.data)
+ const request = pending.get(message.id)
+ if (!request) return
+ pending.delete(message.id)
+ message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result)
+ })
+ const send = (method, params = {}) => new Promise((resolve, reject) => {
+ id += 1
+ pending.set(id, { resolve, reject })
+ socket.send(JSON.stringify({ id, method, params }))
+ })
+ return { projectPageCount: projectPages.length, socket, send }
+}
+
+const valueOf = async (send, expression) => {
+ const result = await send('Runtime.evaluate', { expression, returnByValue: true })
+ if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Runtime evaluation failed')
+ return result.result?.value
+}
+
+const waitFor = async (send, expression, message) => {
+ for (let attempt = 0; attempt < 60; attempt += 1) {
+ try {
+ if (await valueOf(send, expression)) return
+ } catch (error) {
+ if (!String(error.message).includes('Inspected target navigated or closed')) throw error
+ }
+ await sleep(100)
+ }
+ throw new Error(message)
+}
+
+const setSize = (send, size) => send('Emulation.setDeviceMetricsOverride', {
+ ...size,
+ deviceScaleFactor: 1,
+ mobile: true,
+ screenWidth: size.width,
+ screenHeight: size.height
+})
+
+let navigationId = 0
+const openState = async (send, query = '') => {
+ navigationId += 1
+ const url = `${origin}/?t07Baseline=${navigationId}#/pages/tree/t07-member-directory${query}`
+ await send('Page.navigate', { url })
+ await waitFor(send, `location.href === ${JSON.stringify(url)} && Boolean(document.querySelector('.directory-page'))`, `T07 did not render ${query || 'list'}`)
+ await sleep(250)
+}
+
+const capture = async (send, filename) => {
+ const screenshot = await send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false })
+ fs.mkdirSync(outputDirectory, { recursive: true })
+ fs.writeFileSync(path.join(outputDirectory, filename), Buffer.from(screenshot.data, 'base64'))
+}
+
+const metrics = (send) => valueOf(send, `(() => {
+ const rect = (selector) => {
+ const node = document.querySelector(selector)
+ if (!node) return null
+ const box = node.getBoundingClientRect()
+ return { top: box.top, right: box.right, bottom: box.bottom, left: box.left, width: box.width, height: box.height }
+ }
+ return {
+ viewport: { width: innerWidth, height: innerHeight },
+ context: rect('.directory-context'),
+ search: rect('.directory-search'),
+ firstCard: rect('.directory-card'),
+ lastCard: rect('.directory-card:last-child'),
+ cardCount: document.querySelectorAll('.directory-card').length,
+ state: document.querySelector('.directory-state--loading') ? 'loading' : document.querySelector('.directory-state--empty') ? 'empty' : document.querySelector('.directory-state--error') ? 'error' : 'list',
+ horizontalOverflow: document.documentElement.scrollWidth > innerWidth || document.body.scrollWidth > innerWidth,
+ documentScrollHeight: document.documentElement.scrollHeight
+ }
+})()`)
+
+const assert = (condition, message) => { if (!condition) throw new Error(message) }
+
+const run = async () => {
+ const { projectPageCount, socket, send } = await connect()
+ const runtimeErrors = []
+ const resourceErrors = []
+ socket.addEventListener('message', (event) => {
+ const message = JSON.parse(event.data)
+ if (message.method === 'Runtime.exceptionThrown') runtimeErrors.push(message.params.exceptionDetails?.text || 'runtime exception')
+ if (message.method === 'Network.responseReceived' && message.params.response.status >= 400) resourceErrors.push(`${message.params.response.status} ${message.params.response.url}`)
+ })
+
+ try {
+ await send('Page.enable')
+ await send('Runtime.enable')
+ await send('Network.enable')
+ const responsive = []
+ for (const size of sizes) {
+ await setSize(send, size)
+ await openState(send, '?genealogyId=1001')
+ const current = await metrics(send)
+ assert(!current.horizontalOverflow, `${size.width}x${size.height}: horizontal overflow`)
+ assert(current.context && current.search && current.firstCard, `${size.width}x${size.height}: missing baseline content`)
+ assert(current.cardCount === 3, `${size.width}x${size.height}: expected three members`)
+ await valueOf(send, 'scrollTo(0, document.documentElement.scrollHeight)')
+ await sleep(50)
+ const lastBottom = await valueOf(send, "document.querySelector('.directory-card:last-child').getBoundingClientRect().bottom")
+ assert(lastBottom <= size.height + 1, `${size.width}x${size.height}: last member is not reachable`)
+ await valueOf(send, 'scrollTo(0, 0)')
+ responsive.push(current)
+ await capture(send, `responsive-${size.width}x${size.height}.png`)
+ }
+
+ await setSize(send, { width: 412, height: 915 })
+ await openState(send, '?state=loading&genealogyId=1001')
+ assert((await metrics(send)).state === 'loading', 'Loading state did not render')
+ await capture(send, '02-loading-412x915.png')
+
+ await openState(send, '?state=empty&genealogyId=1001')
+ assert((await metrics(send)).state === 'empty', 'Empty state did not render')
+ await capture(send, '03-empty-412x915.png')
+
+ await openState(send, '?state=error&genealogyId=1001')
+ assert((await metrics(send)).state === 'error', 'Error state did not render')
+ assert(!await valueOf(send, "Boolean(document.querySelector('.directory-search'))"), 'Error state must not expose search')
+ await capture(send, '04-error-412x915.png')
+ await valueOf(send, "document.querySelector('.directory-content .app-button').click()")
+ await waitFor(send, "document.querySelectorAll('.directory-card').length === 3 && Boolean(document.querySelector('.directory-search'))", 'Error retry did not restore the list')
+
+ const inputSearch = async (value) => {
+ await valueOf(send, `(() => {
+ const input = document.querySelector('.directory-search input')
+ input.value = ${JSON.stringify(value)}
+ input.dispatchEvent(new Event('input', { bubbles: true }))
+ return true
+ })()`)
+ await sleep(50)
+ await valueOf(send, "document.querySelector('.directory-search__action').click()")
+ }
+ await inputSearch('不存在')
+ await waitFor(send, "Boolean(document.querySelector('.directory-state--empty'))", 'No-result search did not render the empty state')
+ await inputSearch('')
+ await waitFor(send, "document.querySelectorAll('.directory-card').length === 3", 'Cleared search did not restore all members')
+
+ await openState(send, '?genealogyId=1001')
+ await capture(send, '01-list-412x915.png')
+ const final = await metrics(send)
+ assert(final.viewport.width === 412 && final.viewport.height === 915 && final.state === 'list', 'Final approval state is invalid')
+ assert(runtimeErrors.length === 0, `Runtime errors: ${runtimeErrors.join(' | ')}`)
+ assert(resourceErrors.length === 0, `Resource errors: ${resourceErrors.join(' | ')}`)
+ process.stdout.write(`${JSON.stringify({ projectPageCount, responsive, final, runtimeErrors, resourceErrors }, null, 2)}\n`)
+ process.stdout.write('PASS T07 module baseline runtime smoke\n')
+ } finally {
+ socket.close()
+ }
+}
+
+run().catch((error) => {
+ process.stderr.write(`${error.stack || error.message}\n`)
+ process.exit(1)
+})
diff --git a/tests/t07-t08-all-states-visual-contract.ps1 b/tests/t07-t08-all-states-visual-contract.ps1
new file mode 100644
index 0000000..7243b4f
--- /dev/null
+++ b/tests/t07-t08-all-states-visual-contract.ps1
@@ -0,0 +1,26 @@
+$ErrorActionPreference = 'Stop'
+$root = Split-Path -Parent $PSScriptRoot
+$t07 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t07-member-directory.vue') -Raw -Encoding utf8
+$t08 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t08-member-states.vue') -Raw -Encoding utf8
+
+foreach ($rule in @(
+ '(?s)\.directory-search input\s*\{[^}]*font-size:\s*26rpx;',
+ '(?s)\.directory-summary text:last-child\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.directory-card__meta\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.directory-card__status\s*\{[^}]*font-size:\s*22rpx;',
+ '(?s)\.directory-state-card text:last-child\s*\{[^}]*font-size:\s*24rpx;'
+)) {
+ if ($t07 -notmatch $rule) { throw "T07 readable visual rule missing: $rule" }
+}
+
+foreach ($rule in @(
+ '(?s)\.status-tab text\s*\{[^}]*font-size:\s*23rpx;',
+ '(?s)\.status-card__copy text:first-child\s*\{[^}]*font-size:\s*22rpx;',
+ '(?s)\.status-card__copy text:nth-child\(2\)\s*\{[^}]*font-size:\s*32rpx;',
+ '(?s)\.status-card__copy text:last-child\s*\{[^}]*font-size:\s*24rpx;',
+ '(?s)\.status-guidance text\s*\{[^}]*font-size:\s*24rpx;'
+)) {
+ if ($t08 -notmatch $rule) { throw "T08 readable visual rule missing: $rule" }
+}
+
+Write-Output 'T07-T08-ALL-STATES-VISUAL-CONTRACT PASS'