Files
aetherbound-guild/tools/build_design_gallery.py
T

349 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Build the durable Aetherbound UI design/status gallery."""
from __future__ import annotations
from datetime import datetime, timezone
import json
from pathlib import Path
import re
import shutil
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
OUTPUT_DIR = ROOT / "docs/design-authority/gallery"
OUTPUT = OUTPUT_DIR / "index.html"
ASSET_DIR = OUTPUT_DIR / "assets"
DESIGN_INDEX = ROOT / "docs/design-authority/design-index.json"
PAGE_PLAN = ROOT / "docs/03_PAGES_AND_UX.md"
STAGES = [
"SPEC_READY",
"AI_DRAFT",
"DRAFT_REVIEW",
"APPROVED_FOR_RECONSTRUCTION",
"DECONSTRUCTION_IN_PROGRESS",
"DECONSTRUCTION_REVIEW",
"ASSET_GENERATION_IN_PROGRESS",
"ASSET_REVIEW",
"RECONSTRUCTION_READY",
"IMPLEMENTATION_IN_PROGRESS",
"IMPLEMENTED_AWAITING_OWNER_RUNTIME_REVIEW",
"VERIFIED_AWAITING_OWNER_ACCEPTANCE",
"COMPLETE",
]
STAGE_DEFINITIONS = [
("SPEC_READY", "规格", "页面合同、状态矩阵、响应式与文案约束"),
("AI_DRAFT", "AI 设计稿", "提示词、生成记录、原始稿与来源"),
("DRAFT_REVIEW", "AI 稿评审", "Owner 确认构图、层级、主体和文字槽位"),
("APPROVED_FOR_RECONSTRUCTION", "批准解构", "冻结视觉方向,允许进入解构,不等于允许编码"),
("DECONSTRUCTION_IN_PROGRESS", "解构中", "图层、焦点、引擎文字、响应式锚点与交互映射"),
("DECONSTRUCTION_REVIEW", "解构评审", "Owner 检查解构是否忠实且可实现"),
("ASSET_GENERATION_IN_PROGRESS", "素材生成中", "背景清理、角色、道具、图标和组件素材"),
("ASSET_REVIEW", "素材评审", "检查来源、裁切、比例、透明度、无烘焙文字"),
("RECONSTRUCTION_READY", "重建包就绪", "runtime spec、素材清单、命中区域和状态绑定"),
("IMPLEMENTATION_IN_PROGRESS", "编码中", "Godot 页面、状态、输入与本地化实现"),
("IMPLEMENTED_AWAITING_OWNER_RUNTIME_REVIEW", "运行候选", "行为完成,等待真实截图审查"),
("VERIFIED_AWAITING_OWNER_ACCEPTANCE", "证据待验收", "触控、响应式、截图和诊断证据齐全"),
("COMPLETE", "完成", "Owner runtime acceptance 已记录"),
]
STATUS_LABELS = {
"COMPLETE": ("已完成", "complete"),
"SPEC_READY": ("规格已就绪", "active"),
"AI_DRAFT": ("AI 稿生成中", "active"),
"DRAFT_REVIEW": ("AI 稿待评审", "active"),
"APPROVED_FOR_RECONSTRUCTION": ("已批准解构", "active"),
"DECONSTRUCTION_IN_PROGRESS": ("解构中", "active"),
"DECONSTRUCTION_REVIEW": ("解构待评审", "active"),
"ASSET_GENERATION_IN_PROGRESS": ("素材生成中", "active"),
"ASSET_REVIEW": ("素材待评审", "active"),
"RECONSTRUCTION_READY": ("重建包就绪", "active"),
"IMPLEMENTATION_IN_PROGRESS": ("编码中", "active"),
"IMPLEMENTED_AWAITING_OWNER_RUNTIME_REVIEW": ("运行候选待验收", "active"),
"VERIFIED_AWAITING_OWNER_ACCEPTANCE": ("证据完成待验收", "active"),
"NOT_STARTED": ("未开始", "not-started"),
}
RUNTIME_PREVIEWS = {
"SYS-001": "runtime/assets/entry/boot/background.png",
"SYS-002": "runtime/assets/entry/first_launch/background.png",
"SYS-003": "runtime/assets/backgrounds/title_master_v002.png",
"SYS-005": "runtime/assets/backgrounds/p9_3_guild_hall.png",
"HUB-001": "runtime/assets/backgrounds/guild_master_b_composite.png",
}
DECONSTRUCTION_BOARDS = {
"HUB-001": "docs/design-authority/pages/HUB-001-guild-desk/reconstruction/v001/deconstruction-board.png",
"PTY-001": "docs/design-authority/pages/PTY-001-party-line/reconstruction/v001/deconstruction-board.png",
"PTY-002": "docs/design-authority/pages/PTY-002-readiness-review/reconstruction/v001/deconstruction-board.png",
"RSK-002": "docs/design-authority/pages/RSK-002-risk-commitment/reconstruction/v001/deconstruction-board.png",
"BAT-001": "docs/design-authority/pages/BAT-001-battle-opening/reconstruction/v001/deconstruction-board.png",
"BAT-002": "docs/design-authority/pages/BAT-002-live-battle/reconstruction/v001/deconstruction-board.png",
}
ASSET_CONTACT_SHEETS = {
"HUB-001": "docs/design-authority/pages/HUB-001-guild-desk/reconstruction/v001/asset-contact-sheet.png",
"PTY-001": "docs/design-authority/pages/PTY-001-party-line/reconstruction/v001/asset-contact-sheet.png",
"PTY-002": "docs/design-authority/pages/PTY-002-readiness-review/reconstruction/v001/asset-contact-sheet.png",
"RSK-002": "docs/design-authority/pages/RSK-002-risk-commitment/reconstruction/v001/asset-contact-sheet.png",
"BAT-001": "docs/design-authority/pages/BAT-001-battle-opening/reconstruction/v001/asset-contact-sheet.png",
"BAT-002": "docs/design-authority/pages/BAT-002-live-battle/reconstruction/v001/asset-contact-sheet.png",
}
DRAFTS = {
"SYS-003": "docs/design-authority/pages/SYS-003-title/drafts/v002/title-v002-raw.png",
"PTY-002": "docs/design-authority/pages/PTY-002-readiness-review/drafts/v001/readiness-v001-raw.png",
}
GROUPS = {
"SYS-001": "入口模块",
"SYS-002": "入口模块",
"SYS-003": "入口模块",
"SYS-005": "入口模块",
"SYS-008": "入口模块",
}
def relative_url(path: Path) -> str | None:
if not path.is_file():
return None
return Path(__import__("os").path.relpath(path, OUTPUT_DIR)).as_posix()
def copy_gallery_asset(source: Path, key: str) -> str | None:
if not source.is_file():
return None
ASSET_DIR.mkdir(parents=True, exist_ok=True)
target = ASSET_DIR / f"{key}{source.suffix.lower()}"
shutil.copy2(source, target)
return (Path("assets") / target.name).as_posix()
def page_rows() -> list[tuple[str, str]]:
pattern = re.compile(r"^\|\s*`([A-Z]{3}-\d{3})`\s*\|\s*([^|]+?)\s*\|", re.MULTILINE)
rows: list[tuple[str, str]] = []
seen: set[str] = set()
for page_id, name in pattern.findall(PAGE_PLAN.read_text(encoding="utf-8")):
if page_id not in seen:
rows.append((page_id, name.strip()))
seen.add(page_id)
return rows
def image_field(entry: dict[str, Any], field: str, page_id: str) -> str | None:
value = entry.get(field)
if not value:
return None
candidate = ROOT / "docs/design-authority" / str(value)
return copy_gallery_asset(candidate, f"{page_id.lower()}-{field}")
def build_page(page_id: str, name: str, entry: dict[str, Any]) -> dict[str, Any]:
status = str(entry.get("status", "NOT_STARTED"))
if not entry:
status = "NOT_STARTED"
approved = image_field(entry, "approved", page_id) or image_field(entry, "master", page_id)
draft = image_field(entry, "draft", page_id) or image_field(entry, "next_draft", page_id)
if page_id in DRAFTS:
draft = copy_gallery_asset(ROOT / DRAFTS[page_id], f"{page_id.lower()}-draft")
runtime = copy_gallery_asset(ROOT / RUNTIME_PREVIEWS[page_id], f"{page_id.lower()}-runtime") if page_id in RUNTIME_PREVIEWS else None
if entry.get("runtime_preview"):
runtime = image_field(entry, "runtime_preview", page_id)
runtime_captures = []
for index, capture in enumerate(entry.get("runtime_captures", [])):
copied = copy_gallery_asset(ROOT / "docs/design-authority" / str(capture), f"{page_id.lower()}-runtime-capture-{index + 1}")
if copied:
runtime_captures.append({"src": copied, "path": str(capture)})
deconstruction = copy_gallery_asset(ROOT / DECONSTRUCTION_BOARDS[page_id], f"{page_id.lower()}-deconstruction") if page_id in DECONSTRUCTION_BOARDS else None
deconstruction_detail = image_field(entry, "deconstruction_detail", page_id)
assets = copy_gallery_asset(ROOT / ASSET_CONTACT_SHEETS[page_id], f"{page_id.lower()}-assets") if page_id in ASSET_CONTACT_SHEETS else None
label, tone = STATUS_LABELS.get(status, (status, "not-started"))
stage = STAGES.index(status) + 1 if status in STAGES else 0
if status == "COMPLETE":
missing = "无,已完成设计、实现、验证和 Owner 验收。"
elif status == "SPEC_READY":
missing = "生成并评审个人 AI 设计稿,完成重建素材和 runtime 设计包。"
elif status == "DRAFT_REVIEW":
missing = "等待 Owner 评审 AI 设计稿;通过后才能进入批准解构。"
elif status == "APPROVED_FOR_RECONSTRUCTION":
missing = "视觉方向已批准;下一步先完成图层/文字/交互解构与 Owner 解构评审。"
elif status == "DECONSTRUCTION_IN_PROGRESS":
missing = "完成图像取证、图层归属、引擎文字槽位、响应式锚点和命中区域。"
elif status == "DECONSTRUCTION_REVIEW":
missing = "等待 Owner 检查解构映射;通过后才能生成和处理素材。"
elif status == "ASSET_GENERATION_IN_PROGRESS":
missing = "按已审查的解构清单生成背景、角色、道具、图标和组件素材。"
elif status == "ASSET_REVIEW":
missing = "等待 Owner 检查素材来源、比例、裁切、透明度和无烘焙文字;通过后才能进入重建包。"
elif status == "RECONSTRUCTION_READY":
missing = "重建包已就绪,下一步才允许进入 Godot 编码。"
elif status == "IMPLEMENTATION_IN_PROGRESS":
missing = "先通过批准稿与 Runtime 的视觉保真检查,再继续页面状态、输入、响应式和本地化验收。"
elif status == "DECONSTRUCTION_REVIEW":
missing = "等待 Owner 检查图层归属、引擎文字槽位、响应式锚点、命中区域和素材 provenance;通过后才能进入素材/重建就绪。"
elif status == "NOT_STARTED":
missing = "尚未进入当前页面生产流程。"
else:
missing = "继续当前状态的页面生产门,不能跳过 Owner 评审。"
stage_details = []
current_index = stage - 1
for index, (stage_id, stage_label, deliverable) in enumerate(STAGE_DEFINITIONS):
if status == "COMPLETE" or index < current_index:
phase = "done"
elif index == current_index and status != "NOT_STARTED":
phase = "current"
else:
phase = "pending"
stage_details.append({
"id": stage_id,
"label": stage_label,
"deliverable": deliverable,
"phase": phase,
})
group = GROUPS.get(page_id)
if group is None:
prefix = page_id.split("-", 1)[0]
group = {
"SET": "设置与数据",
"HUB": "公会与经营",
"SHP": "市场与招募",
"PTY": "队伍准备",
"RSK": "风险选择",
"BAT": "战斗循环",
"OUT": "结算与恢复",
"PRO": "成长与进度",
"PST": "结局与后期",
}.get(prefix, "未分类页面")
references = []
for key in ("contract", "state_matrix", "review", "approved", "approved_manifest", "visual_reference", "scale_reference", "rejected_draft", "rejected_draft_manifest", "rejected_draft_review", "deconstruction", "deconstruction_detail", "deconstruction_plan", "deconstruction_review", "deconstruction_text_slots", "deconstruction_hit_areas", "top_rail_decomposition", "asset_plan", "asset_manifest", "asset_contact_sheet", "asset_review", "rail_slice_manifest", "rail_fit_preview", "reconstruction_spec", "runtime_asset_manifest", "runtime_hit_areas", "runtime", "preflight", "verification", "visual_fidelity", "owner_acceptance", "provenance"):
if entry.get(key):
path = str(entry[key])
prefix = "../" if path.startswith(("pages/", "modules/")) else "../../../"
references.append({"label": key, "path": path, "url": prefix + path})
return {
"id": page_id,
"name": name,
"group": group,
"status": status,
"statusLabel": label,
"tone": tone,
"stage": stage,
"stageTotal": len(STAGES),
"stageDetails": stage_details,
"approved": approved,
"draft": draft,
"deconstruction": deconstruction,
"deconstructionDetail": deconstruction_detail,
"assets": assets,
"runtime": runtime,
"runtimeCaptures": runtime_captures,
"missing": missing,
"references": references,
"search": f"{page_id} {name} {group} {label} {missing}".lower(),
}
def gallery_data() -> dict[str, Any]:
index = json.loads(DESIGN_INDEX.read_text(encoding="utf-8"))
entries = index.get("pages", {})
pages = [build_page(page_id, name, entries.get(page_id, {})) for page_id, name in page_rows()]
counts = {
"total": len(pages),
"complete": sum(page["status"] == "COMPLETE" for page in pages),
"active": sum(page["status"] != "NOT_STARTED" and page["status"] != "COMPLETE" for page in pages),
"notStarted": sum(page["status"] == "NOT_STARTED" for page in pages),
"withDesign": sum(bool(page["approved"] or page["draft"]) for page in pages),
"withRuntime": sum(bool(page["runtime"]) for page in pages),
}
return {
"generatedAt": datetime.now(timezone.utc).astimezone().isoformat(timespec="minutes"),
"stages": STAGES,
"pages": pages,
"counts": counts,
}
HTML = r'''<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#182127">
<title>Aetherbound Guild | UI Design Ledger</title>
<style>
:root{--ink:#182127;--ink2:#2d3c43;--muted:#66767b;--paper:#f6f7f2;--white:#fff;--line:#d8dfdc;--gold:#d6a72e;--gold-soft:#fff4ce;--jade:#27845e;--blue:#3969a8;--teal:#13818a;--coral:#c94e52;--slate:#87959a;--shadow:0 16px 34px rgba(24,33,39,.12)}
*{box-sizing:border-box}html{background:var(--paper);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}body{margin:0;letter-spacing:0}button,input{font:inherit;letter-spacing:0}button{cursor:pointer}button:focus-visible,input:focus-visible{outline:3px solid var(--blue);outline-offset:2px}img{display:block;max-width:100%}
.app{min-height:100vh;display:grid;grid-template-columns:250px minmax(0,1fr)}.side{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;color:#eef4f0;background:var(--ink);border-right:1px solid #3a4c53}.brand{padding:22px 20px;border-bottom:1px solid rgba(255,255,255,.13)}.brand-mark{display:inline-grid;width:34px;height:34px;place-items:center;margin-bottom:12px;color:var(--ink);font-weight:900;background:var(--gold);border:2px solid #f0d980;border-radius:3px}.brand h1{margin:0;font-size:15px}.brand p{margin:6px 0 0;color:#adbec1;font-size:11px;line-height:1.45}.side-nav{display:grid;gap:3px;padding:16px 10px}.side-nav button{min-height:44px;display:grid;grid-template-columns:22px 1fr auto;align-items:center;gap:8px;padding:8px 10px;color:#b9c6c9;text-align:left;background:transparent;border:1px solid transparent;border-radius:3px}.side-nav button:hover{color:#fff;background:#26363d}.side-nav button.is-active{color:#fff;background:#304047;border-color:#52676e;box-shadow:inset 4px 0 var(--gold)}.side-nav b{font-size:12px}.side-nav span{color:var(--gold);font-size:10px;font-weight:900}.side-nav em{min-width:24px;padding:3px 5px;color:#c8d3d5;font-size:10px;font-style:normal;text-align:center;border:1px solid #52646a;border-radius:2px}.side-foot{margin-top:auto;padding:16px 20px;color:#aebdc1;font-size:10px;line-height:1.45;border-top:1px solid rgba(255,255,255,.13)}.side-foot strong{display:block;color:#fff;font-size:11px}.side-foot code{color:var(--gold);font-size:10px}
.main{min-width:0}.top{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:64px;padding:12px 28px;background:rgba(255,255,255,.96);border-bottom:1px solid var(--line)}.crumb{min-width:0;display:flex;gap:9px;align-items:center;font-size:12px}.crumb span{color:var(--muted)}.crumb strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.crumb b{color:#a4b0b2}.top-actions{display:flex;align-items:center;gap:8px;flex:0 0 auto}.top-actions button{min-height:34px;padding:5px 10px;color:var(--ink2);background:#fff;border:1px solid var(--line);border-radius:3px}.top-actions button:hover{border-color:var(--ink2)}.top-actions .menu{display:none}
main{max-width:1700px;margin:0 auto;padding:30px}.eyebrow{display:block;color:var(--teal);font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.hero{display:flex;justify-content:space-between;gap:28px;align-items:end;margin-bottom:22px}.hero h2{margin:5px 0 8px;font-size:30px;line-height:1.15}.hero p{max-width:780px;margin:0;color:var(--muted);font-size:13px;line-height:1.55;overflow-wrap:anywhere}.generated{color:var(--muted);font-size:11px;white-space:nowrap}.metrics{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:1px;margin-bottom:18px;background:var(--line);border:1px solid var(--line)}.metric{min-height:88px;padding:15px;background:#fff}.metric strong{display:block;font-size:26px;line-height:1}.metric span{display:block;margin-top:8px;color:var(--muted);font-size:11px}.metric.complete strong{color:var(--jade)}.metric.active strong{color:var(--gold)}.metric.not strong{color:var(--slate)}.progress-band{display:grid;grid-template-columns:minmax(170px,250px) 1fr auto;align-items:center;gap:16px;margin-bottom:26px;padding:12px 15px;background:#fff;border:1px solid var(--line)}.progress-label{font-size:11px;font-weight:800}.progress-track{height:10px;overflow:hidden;background:#e6ece9;border-radius:2px}.progress-fill{height:100%;background:var(--jade)}.progress-note{color:var(--muted);font-size:11px;white-space:nowrap}
.toolbar{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-bottom:14px}.filters{display:flex;flex-wrap:wrap;gap:5px}.filters button{min-height:34px;padding:6px 10px;color:var(--ink2);background:#fff;border:1px solid var(--line);border-radius:3px}.filters button.is-active{color:#fff;background:var(--ink);border-color:var(--ink)}.search{width:min(300px,100%);min-height:36px;padding:7px 10px;background:#fff;border:1px solid var(--line);border-radius:3px}.group{margin-top:27px}.group-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px;border-bottom:2px solid var(--ink)}.group-heading h3{margin:0 0 7px;font-size:16px}.group-heading span{color:var(--muted);font-size:11px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.card{min-width:0;display:grid;grid-template-rows:auto auto 1fr auto;padding:13px;background:#fff;border:1px solid var(--line);box-shadow:0 3px 0 rgba(24,33,39,.04)}.card:hover{border-color:#8fa0a4;box-shadow:var(--shadow);transform:translateY(-1px)}.card-head{display:flex;align-items:start;justify-content:space-between;gap:10px}.card-id{color:var(--teal);font-size:10px;font-weight:900;letter-spacing:.05em}.card h4{margin:4px 0 0;font-size:16px;line-height:1.2}.pill{flex:0 0 auto;padding:5px 7px;font-size:10px;font-weight:800;border:1px solid}.pill.complete{color:var(--jade);background:#e8f5ee;border-color:#a6d5ba}.pill.active{color:#8a6312;background:var(--gold-soft);border-color:#e2c55c}.pill.not-started{color:#66767b;background:#eef1f0;border-color:#cbd4d2}.stage{display:flex;gap:3px;margin:13px 0 11px}.stage i{height:5px;flex:1;background:#e3e8e6}.stage i.done{background:var(--jade)}.stage i.current{background:var(--gold)}.art-row{display:grid;grid-template-columns:1fr 1fr;gap:6px;min-height:100px;margin-bottom:12px}.art{position:relative;min-width:0;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#eef2ef;border:1px solid var(--line)}.art img{width:100%;height:100%;object-fit:cover}.art.design img{object-fit:cover}.art.runtime img{object-fit:cover}.art small{position:absolute;left:5px;top:5px;padding:3px 5px;color:var(--ink2);font-size:9px;background:rgba(255,255,255,.9)}.art.empty{min-height:100px;color:#9aa7a9;font-size:11px;text-align:center}.card-meta{display:grid;gap:5px;color:var(--muted);font-size:11px;line-height:1.4}.card-meta strong{color:var(--ink2);font-size:10px}.card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:13px;padding-top:10px;border-top:1px solid var(--line)}.card-foot button{min-height:32px;padding:5px 9px;color:#fff;background:var(--ink);border:1px solid var(--ink);border-radius:3px}.card-foot button:hover{background:var(--ink2)}.card-foot span{color:var(--muted);font-size:10px}
.spotlight{display:grid;grid-template-columns:minmax(260px,1fr) minmax(260px,1.2fr);gap:18px;align-items:center;margin:0 0 26px;padding:16px 18px;background:#fff;border:1px solid var(--gold);box-shadow:0 5px 0 rgba(214,167,46,.14)}.spotlight-copy{min-width:0}.spotlight-kicker{color:#8a6312;font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.spotlight h3{margin:5px 0 6px;font-size:20px}.spotlight p{margin:0;color:var(--muted);font-size:12px;line-height:1.5}.spotlight-meta{display:flex;flex-wrap:wrap;gap:6px;margin-top:11px}.spotlight-meta span{padding:4px 6px;color:var(--ink2);font-size:10px;background:var(--gold-soft);border:1px solid #ead58d}.spotlight button{margin-top:13px;min-height:34px;padding:6px 10px;color:#fff;background:var(--ink);border:1px solid var(--ink);border-radius:3px}.spotlight button:hover{background:var(--ink2)}.spotlight-image{min-width:0;overflow:hidden;background:#eef2ef;border:1px solid var(--line)}.spotlight-image img{width:100%;max-height:230px;object-fit:cover}
.table{display:none;background:#fff;border:1px solid var(--line)}.table-row{display:grid;grid-template-columns:100px 1.3fr 170px 1fr 110px;gap:10px;align-items:center;min-height:48px;padding:8px 12px;border-bottom:1px solid var(--line);font-size:11px}.table-row:last-child{border-bottom:0}.table-row.header{color:var(--muted);font-size:10px;font-weight:900;background:#f2f5f3}.table-row .id{color:var(--teal);font-weight:900}.table-row .missing{color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.table-row button{justify-self:end;min-height:30px;padding:5px 8px;color:#fff;background:var(--ink);border:0;border-radius:3px}
.empty{padding:34px;color:var(--muted);text-align:center;background:#fff;border:1px dashed #b8c5c3}.drawer{position:fixed;z-index:30;inset:0;display:none;background:rgba(24,33,39,.38)}.drawer.is-open{display:block}.drawer-panel{position:absolute;right:0;top:0;width:min(560px,100%);height:100%;overflow:auto;padding:25px;background:var(--paper);box-shadow:-18px 0 40px rgba(0,0,0,.2)}.drawer-close{float:right;width:34px;height:34px;color:var(--ink);background:#fff;border:1px solid var(--line);border-radius:3px}.drawer-panel h2{margin:34px 0 4px;font-size:25px}.drawer-panel .lead{margin:0 0 18px;color:var(--muted);font-size:12px;line-height:1.5}.detail-status{display:inline-block;margin-bottom:16px}.detail-images{display:grid;gap:10px}.detail-images figure{margin:0;background:#fff;border:1px solid var(--line)}.detail-images img{width:100%;max-height:280px;object-fit:cover}.detail-images figcaption{padding:8px 10px;color:var(--muted);font-size:10px;border-top:1px solid var(--line)}.detail-block{margin-top:18px;padding-top:15px;border-top:1px solid var(--line)}.detail-block h3{margin:0 0 8px;font-size:13px}.detail-block p{margin:0;color:var(--muted);font-size:12px;line-height:1.55}.refs{display:grid;gap:5px;margin:0;padding:0;list-style:none}.refs a{color:var(--blue);font-size:11px;word-break:break-word}.workflow{display:grid;gap:6px;margin:0;padding:0;list-style:none}.workflow li{display:grid;grid-template-columns:18px 1fr;gap:8px;align-items:start;padding:7px 8px;background:#fff;border:1px solid var(--line);font-size:11px}.workflow li::before{content:' ';width:8px;height:8px;margin-top:3px;background:var(--slate)}.workflow li.done::before{background:var(--jade)}.workflow li.current{border-color:var(--gold);background:var(--gold-soft)}.workflow li.current::before{background:var(--gold)}.workflow b{display:block;color:var(--ink2);font-size:11px}.workflow span{display:block;margin-top:2px;color:var(--muted);font-size:10px;line-height:1.4}.legend{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px;color:var(--muted);font-size:10px}.legend span{display:inline-flex;align-items:center;gap:5px}.legend i{width:9px;height:9px;display:inline-block}.legend .g{background:var(--jade)}.legend .y{background:var(--gold)}.legend .s{background:var(--slate)}
@media(max-width:1120px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}.metric:nth-child(n+4){border-top:1px solid var(--line)}}
.capture-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.capture-grid figure{margin:0;background:#fff;border:1px solid var(--line)}.capture-grid img{width:100%;height:108px;object-fit:cover}.capture-grid figcaption{padding:6px 7px;color:var(--muted);font-size:9px;line-height:1.35;border-top:1px solid var(--line);overflow-wrap:anywhere}.nav-toggle{display:none}
@media(max-width:800px){.app{display:block}.side{position:fixed;inset:0 auto 0 0;width:min(290px,calc(100vw - 48px));height:100vh;z-index:50;transform:translateX(-105%);transition:transform .18s ease;box-shadow:18px 0 36px rgba(0,0,0,.25)}.side.is-open{transform:translateX(0)}.side-nav{grid-template-columns:1fr}.top{padding:10px 16px;z-index:40}.nav-toggle{display:block!important}.top-actions #toggle-table{display:none}.top-actions #reset-filter{display:none}main{padding:20px 15px;overflow:hidden}.hero{display:block}.hero h2{font-size:26px;overflow-wrap:anywhere}.generated{display:block;margin-top:12px}.toolbar{align-items:stretch;flex-direction:column}.search{width:100%}.grid{grid-template-columns:1fr}.progress-band{grid-template-columns:1fr;gap:8px}.progress-note{white-space:normal}.spotlight{grid-template-columns:1fr;gap:12px}.spotlight-image{order:-1}.table-row{grid-template-columns:75px 1fr 100px}.table-row .missing{display:none}.table-row.header{display:none}}
@media(max-width:500px){.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.metric{min-height:76px}.metric strong{font-size:22px}.art-row{min-height:120px}}
</style>
</head>
<body>
<div class="app">
<aside class="side">
<div class="brand"><span class="brand-mark">AG</span><h1>Aetherbound Guild</h1><p>UI Design Ledger<br>设计稿 · 解构 · 素材 · Runtime</p></div>
<nav class="side-nav" aria-label="Gallery navigation">
<button class="is-active" data-filter="all"><span>01</span><b>全部页面</b><em id="nav-all">50</em></button>
<button data-filter="complete"><span>02</span><b>已完成</b><em id="nav-complete">0</em></button>
<button data-filter="active"><span>03</span><b>进行中</b><em id="nav-active">0</em></button>
<button data-filter="not-started"><span>04</span><b>未开始</b><em id="nav-not">0</em></button>
<button data-filter="has-art"><span>05</span><b>已有视觉稿</b><em id="nav-art">0</em></button>
</nav>
<div class="side-foot"><strong>当前边界</strong><span id="side-boundary">HUB-001 Guild desk deconstruction review</span><br><code>one page at a time</code></div>
</aside>
<div class="main">
<header class="top"><button class="nav-toggle" id="toggle-side" aria-expanded="false">导航</button><div class="crumb"><span>Aetherbound Guild</span><b>/</b><strong>UI Design Ledger</strong></div><div class="top-actions"><button id="toggle-table">列表视图</button><button id="reset-filter">重置筛选</button></div></header>
<main>
<section class="hero"><div><span class="eyebrow">长期设计总览</span><h2>页面从设计稿到可玩页面的进度</h2><p>这里汇总每一个玩家页面的 AI 稿、解构板、素材清单、运行时预览、当前生产门和剩余缺口。解构评审与素材评审独立于编码,避免错误理解直接进入 Godot。</p></div><span class="generated" id="generated">生成时间</span></section>
<section class="metrics" aria-label="Progress summary"><div class="metric complete"><strong id="metric-complete">0</strong><span>已完成并验收</span></div><div class="metric active"><strong id="metric-active">0</strong><span>正在生产</span></div><div class="metric not"><strong id="metric-not">0</strong><span>尚未开始</span></div><div class="metric"><strong id="metric-art">0</strong><span>已有设计或 AI 稿</span></div><div class="metric"><strong id="metric-runtime">0</strong><span>已有 runtime 预览</span></div></section>
<section class="progress-band"><span class="progress-label">总体完成度 <b id="progress-value">0%</b></span><div class="progress-track"><div class="progress-fill" id="progress-fill" style="width:0%"></div></div><span class="progress-note" id="progress-note">还差 50 个页面</span></section>
<section class="spotlight" id="spotlight" aria-live="polite"></section>
<div class="toolbar"><div class="filters" role="toolbar" aria-label="Filter pages"><button class="is-active" data-filter="all">全部</button><button data-filter="complete">已完成</button><button data-filter="active">进行中</button><button data-filter="not-started">未开始</button><button data-filter="has-art">有视觉稿</button></div><input class="search" id="search" type="search" placeholder="搜索页面 ID、名称或缺口…" aria-label="搜索页面"></div>
<div id="groups"></div><div class="table" id="table"></div>
<div class="legend"><span><i class="g"></i>已完成</span><span><i class="y"></i>进行中</span><span><i class="s"></i>未开始</span></div>
</main>
</div>
</div>
<div class="drawer" id="drawer" aria-hidden="true"><section class="drawer-panel" role="dialog" aria-modal="true" aria-labelledby="detail-title"><button class="drawer-close" id="drawer-close" aria-label="关闭">×</button><div id="detail"></div></section></div>
<script id="gallery-data" type="application/json">__DATA__</script>
<script>
const DATA=JSON.parse(document.getElementById('gallery-data').textContent);let filter='all',query='',listMode=false;
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const statusClass=p=>p.tone;const visible=()=>DATA.pages.filter(p=>{const ok=filter==='all'||filter==='complete'&&p.status==='COMPLETE'||filter==='active'&&p.status!=='COMPLETE'&&p.status!=='NOT_STARTED'||filter==='not-started'&&p.status==='NOT_STARTED'||filter==='has-art'&&(p.approved||p.draft);return ok&&(!query||p.search.includes(query.toLowerCase()))});
function imageBox(src,label,kind){return src?`<figure class="art ${kind}"><img src="${src}" alt="${label}" loading="lazy"><small>${label}</small></figure>`:`<div class="art empty"><span>${label}<br>尚未提供</span></div>`}
function card(p){const bars=Array.from({length:p.stageTotal},(_,i)=>`<i class="${i<p.stage?'done':i===p.stage?'current':''}"></i>`).join('');const current=p.stageDetails.find(x=>x.phase==='current');const showRuntime=Boolean(p.runtime)&&(p.status!=='NOT_STARTED');const second=showRuntime?p.runtime:(p.assets||p.deconstruction||p.approved);const secondLabel=showRuntime?(p.status==='COMPLETE'?'Runtime / 已验收':'Runtime / 运行候选'):p.assets?'素材清单':p.deconstruction?'解构板':'批准设计稿';const secondKind=showRuntime?'runtime':(p.assets||p.deconstruction?'design':'runtime');return `<article class="card"><div class="card-head"><div><div class="card-id">${p.id}</div><h4>${p.name}</h4></div><span class="pill ${statusClass(p)}">${p.statusLabel}</span></div><div class="stage" title="生产门 ${p.stage}/${p.stageTotal}">${bars}</div><div><div class="art-row">${imageBox(p.draft,'AI / 设计稿','design')}${imageBox(second,secondLabel,secondKind)}</div><div class="card-meta"><div><strong>当前阶段:</strong>${current?current.label:(p.status==='COMPLETE'?'全部阶段已完成':'尚未开始')}</div><div><strong>还差:</strong>${p.missing}</div><div><strong>生产门:</strong>${p.stage}/${p.stageTotal}</div></div></div><div class="card-foot"><span>${p.group}</span><button data-open="${p.id}">查看详情</button></div></article>`}
function renderSpotlight(){const candidates=DATA.pages.filter(p=>p.status!=='NOT_STARTED'&&p.status!=='COMPLETE'&&p.runtime);if(!candidates.length){$('#spotlight').style.display='none';return}const p=candidates[0];$('#spotlight').style.display='grid';$('#spotlight').innerHTML=`<div class="spotlight-copy"><span class="spotlight-kicker">当前运行候选 · ${p.statusLabel}</span><h3>${p.id} · ${p.name}</h3><p>这个页面已经完成 AI 稿、解构、素材、重建和 Godot 编码。下面直接展示真实 Runtime 截图;点击详情可以查看完整制作链路和所有证据。</p><div class="spotlight-meta"><span>生产门 ${p.stage}/${p.stageTotal}</span><span>${p.group}</span><span>Owner runtime review 待确认</span></div><button data-open="${p.id}">查看 ${p.id} 完整页面</button></div><figure class="spotlight-image"><img src="${p.runtime}" alt="${p.id} runtime preview"><figcaption class="sr-only">${p.id} runtime preview</figcaption></figure>`;$('#spotlight').querySelector('button').onclick=()=>openDetail(p.id)}
function render(){const pages=visible();renderSpotlight();const groups=pages.reduce((m,p)=>{(m[p.group]??=[]).push(p);return m},{});$('#groups').innerHTML=pages.length?Object.entries(groups).map(([g,items])=>`<section class="group"><div class="group-heading"><h3>${g}</h3><span>${items.length} 个页面</span></div><div class="grid">${items.map(card).join('')}</div></section>`).join(''):`<div class="empty">没有匹配页面。清空筛选或换一个关键词。</div>`;$('#table').innerHTML=`<div class="table-row header"><span>ID</span><span>页面</span><span>状态</span><span>当前缺口</span><span></span></div>${pages.map(p=>`<div class="table-row"><span class="id">${p.id}</span><strong>${p.name}</strong><span class="pill ${statusClass(p)}">${p.statusLabel}</span><span class="missing">${p.missing}</span><button data-open="${p.id}">详情</button></div>`).join('')}`;$('#groups').style.display=listMode?'none':'';$('#table').style.display=listMode?'block':'none';$$('[data-open]').forEach(b=>b.onclick=()=>openDetail(b.dataset.open));}
function openDetail(id){const p=DATA.pages.find(x=>x.id===id);if(!p)return;const runtimeLabel=p.status==='COMPLETE'?'Runtime / 已验收':'Runtime / 历史候选';const workflow=p.stageDetails.map(s=>`<li class="${s.phase}"><div><b>${s.label} <small>${s.id}</small></b><span>${s.deliverable}</span></div></li>`).join('');const images=[p.draft?`<figure><img src="${p.draft}" alt="${p.id} AI design"><figcaption>AI / 设计稿</figcaption></figure>`:'',p.deconstruction?`<figure><img src="${p.deconstruction}" alt="${p.id} deconstruction board"><figcaption>解构板 / 图层、文字与交互映射</figcaption></figure>`:'',p.deconstructionDetail?`<figure><img src="${p.deconstructionDetail}" alt="${p.id} detailed deconstruction"><figcaption>解构细图 / 顶部状态栏子部件</figcaption></figure>`:'',p.assets?`<figure><img src="${p.assets}" alt="${p.id} asset contact sheet"><figcaption>素材清单 / 生成与处理结果</figcaption></figure>`:'',(p.runtime||p.approved)?`<figure><img src="${p.runtime||p.approved}" alt="${p.id} runtime"><figcaption>${p.runtime?runtimeLabel:'批准设计稿'}</figcaption></figure>`:''].join('');$('#detail').innerHTML=`<span class="pill ${statusClass(p)} detail-status">${p.statusLabel}</span><h2 id="detail-title">${p.id} · ${p.name}</h2><p class="lead">${p.group} · 生产门 ${p.stage}/${p.stageTotal}</p><div class="detail-images">${images}</div><div class="detail-block"><h3>制作流程</h3><ol class="workflow">${workflow}</ol></div><div class="detail-block"><h3>当前缺口</h3><p>${p.missing}</p></div><div class="detail-block"><h3>页面资料(仓库路径)</h3><ul class="refs">${p.references.length?p.references.map(r=>`<li><span>${r.label} · ${r.path}</span></li>`).join(''):'<li>尚无个人页面资料包。</li>'}</ul></div>`;$('#drawer').classList.add('is-open');$('#drawer').setAttribute('aria-hidden','false');}
function openDetail(id){const p=DATA.pages.find(x=>x.id===id);if(!p)return;const runtimeLabel=p.status==='COMPLETE'?'Runtime / 已验收':'Runtime / 运行候选';const workflow=p.stageDetails.map(s=>`<li class="${s.phase}"><div><b>${s.label} <small>${s.id}</small></b><span>${s.deliverable}</span></div></li>`).join('');const images=[p.draft?`<figure><img src="${p.draft}" alt="${p.id} AI design"><figcaption>AI / 设计稿</figcaption></figure>`:'',p.deconstruction?`<figure><img src="${p.deconstruction}" alt="${p.id} deconstruction board"><figcaption>解构板 / 图层、文字与交互映射</figcaption></figure>`:'',p.assets?`<figure><img src="${p.assets}" alt="${p.id} asset contact sheet"><figcaption>素材清单 / 生成与处理结果</figcaption></figure>`:'',(p.runtime||p.approved)?`<figure><img src="${p.runtime||p.approved}" alt="${p.id} runtime"><figcaption>${p.runtime?runtimeLabel:'批准设计稿'}</figcaption></figure>`:''].join('');const captures=p.runtimeCaptures?.length?`<div class="detail-block"><h3>Runtime 验证截图(${p.runtimeCaptures.length} 张)</h3><div class="capture-grid">${p.runtimeCaptures.map((c,i)=>`<figure><img src="${c.src}" alt="${p.id} runtime capture ${i+1}" loading="lazy"><figcaption>${c.path.split('/').pop()}</figcaption></figure>`).join('')}</div></div>`:'';$('#detail').innerHTML=`<span class="pill ${statusClass(p)} detail-status">${p.statusLabel}</span><h2 id="detail-title">${p.id} · ${p.name}</h2><p class="lead">${p.group} · 生产门 ${p.stage}/${p.stageTotal}</p><div class="detail-images">${images}</div>${captures}<div class="detail-block"><h3>制作流程</h3><ol class="workflow">${workflow}</ol></div><div class="detail-block"><h3>当前缺口</h3><p>${p.missing}</p></div><div class="detail-block"><h3>页面资料(仓库路径)</h3><ul class="refs">${p.references.length?p.references.map(r=>`<li><span>${r.label} · ${r.path}</span></li>`).join(''):'<li>尚无个人页面资料包。</li>'}</ul></div>`;$('#drawer').classList.add('is-open');$('#drawer').setAttribute('aria-hidden','false');}
function setFilter(value){filter=value;$$('[data-filter]').forEach(b=>b.classList.toggle('is-active',b.dataset.filter===value));render()}
function updateMetrics(){const c=DATA.counts;$('#metric-complete').textContent=c.complete;$('#metric-active').textContent=c.active;$('#metric-not').textContent=c.notStarted;$('#metric-art').textContent=c.withDesign;$('#metric-runtime').textContent=c.withRuntime;$('#nav-all').textContent=c.total;$('#nav-complete').textContent=c.complete;$('#nav-active').textContent=c.active;$('#nav-not').textContent=c.notStarted;$('#nav-art').textContent=c.withDesign;const pct=Math.round(c.complete/c.total*100);$('#progress-value').textContent=`${pct}%`;$('#progress-fill').style.width=`${pct}%`;$('#progress-note').textContent=`还差 ${c.total-c.complete} 个页面`}
$$('[data-filter]').forEach(b=>b.onclick=()=>{setFilter(b.dataset.filter);if(window.innerWidth<=800)$('#toggle-side').click()});$('#search').oninput=e=>{query=e.target.value.trim();render()};$('#reset-filter').onclick=()=>{$('#search').value='';query='';setFilter('all')};$('#toggle-table').onclick=()=>{listMode=!listMode;$('#toggle-table').textContent=listMode?'看板视图':'列表视图';render()};$('#toggle-side').onclick=()=>{const open=$('.side').classList.toggle('is-open');$('#toggle-side').setAttribute('aria-expanded',String(open))};$('#drawer-close').onclick=()=>{$('#drawer').classList.remove('is-open');$('#drawer').setAttribute('aria-hidden','true')};$('#drawer').onclick=e=>{if(e.target.id==='drawer')$('#drawer-close').click()};document.addEventListener('keydown',e=>{if(e.key==='Escape')$('#drawer-close').click()});$('#generated').textContent=`数据生成于 ${DATA.generatedAt}`;updateMetrics();render();const initialPage=decodeURIComponent(location.hash.slice(1));if(initialPage)openDetail(initialPage);
</script>
</body>
</html>
'''
def main() -> None:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
payload = json.dumps(gallery_data(), ensure_ascii=False, separators=(",", ":"))
OUTPUT.write_text(HTML.replace("__DATA__", payload), encoding="utf-8")
print(f"ABG_DESIGN_GALLERY_OK pages={len(json.loads(payload)['pages'])} output={OUTPUT}")
if __name__ == "__main__":
main()