diff --git a/DESIGN-TOKENS.md b/DESIGN-TOKENS.md index f806881..d418306 100644 --- a/DESIGN-TOKENS.md +++ b/DESIGN-TOKENS.md @@ -1,6 +1,8 @@ # Design Tokens -This file maps the shipped dark visual system to its implementation source in `web/styles.css`. `DESIGN.md` is the portable design contract; the `:root` custom properties in `web/styles.css` are the runtime source of truth. +**HEL-342 定稿:** 运行时视觉来源是 `web/design-system.css` 的霜曜日间 + 黑金夜间变量(`.v-fusion` / `[data-theme="night"]`),依据 `jinniu-fusion-design`。禁止退回旧青绿 token 或叠第二套覆盖层。主题偏好只存 `localStorage.jinniu-theme`(`day` | `night`),不跟随系统、不恢复业务数据。 + +下文是历史暗色合同存档,不再作为施工依据。 ## Architecture @@ -8,7 +10,6 @@ This file maps the shipped dark visual system to its implementation source in `w - Keep shared primitives in `:root`; keep component behavior in its existing semantic rule group. - Business-component rules must not use `!important`. - Only accessibility utilities (`.sr-only`, `[hidden]`) and `prefers-reduced-motion` enforcement may force priority. -- Do not restore warm-paper, white-card, cobalt-action, cream, or light-theme aliases. New code must consume the dark semantic tokens directly. - Keep administrator and cashier portals as independent route-level surfaces even when they share tokens and component primitives. ## Typography diff --git a/scripts/hel342_shot.py b/scripts/hel342_shot.py new file mode 100644 index 0000000..51be419 --- /dev/null +++ b/scripts/hel342_shot.py @@ -0,0 +1,126 @@ +"""HEL-342: static visual shots + computed-style probe (no business data writes).""" +from __future__ import annotations + +import json +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from threading import Thread + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" +OUT = ROOT.parent / "hel342-shots" + +PROBES = [ + (".sidebar", ["width", "backgroundColor", "backdropFilter"]), + (".brand-logo-day", ["width", "height"]), + (".btn-primary", ["backgroundColor", "height", "borderRadius", "color"]), + (".stat-card.gold .stat-value, .stat-card .stat-value", ["color", "fontSize", "fontVariantNumeric"]), + (".page-head h1", ["fontSize", "fontWeight"]), + (".ds-table th", ["fontSize", "color"]), +] + + +def main() -> None: + from playwright.sync_api import sync_playwright + + OUT.mkdir(parents=True, exist_ok=True) + handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) + httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) + Thread(target=httpd.serve_forever, daemon=True).start() + base = f"http://127.0.0.1:{httpd.server_address[1]}" + report = [] + + pages = [ + ("index", "index.html", None, None), + ("login-admin", "login-admin.html", None, None), + ("login-company", "login-company.html", None, None), + ("admin-dashboard", "admin.html", "dashboard", None), + ("admin-flows", "admin.html", "flows", None), + ("admin-settings", "admin.html", "settings", None), + ("admin-audit", "admin.html", "audit", None), + ("admin-reminders", "admin.html", "reminders", None), + ("admin-period-audit", "admin.html", "period-audit", None), + ("admin-companies", "admin.html", "companies", None), + ("admin-pair", "admin.html", "pair", None), + ("company-workspace", "company.html", "workspace", None), + ("company-transfers", "company.html", "transfers", None), + ("company-reconcile", "company.html", "reconcile", None), + ("company-upload", "company.html", "upload", None), + ] + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) + for theme in ("day", "night"): + for width, height, tag in ((1440, 900, "1440"), (820, 900, "820"), (390, 844, "390")): + if tag != "1440" and theme == "night" and width != 390: + continue + page = browser.new_page(viewport={"width": width, "height": height}) + page.add_init_script( + f"() => {{ try {{ localStorage.setItem('jinniu-theme', '{theme}'); }} catch (e) {{}} }}" + ) + for name, html, view, _ in pages: + if tag != "1440" and name not in ("login-admin", "admin-flows", "company-transfers", "admin-dashboard"): + continue + page.route("**/app.js**", lambda route: route.abort()) + page.goto(f"{base}/{html}", wait_until="domcontentloaded") + page.evaluate( + """(args) => { + document.documentElement.classList.add('v-fusion'); + document.documentElement.setAttribute('data-theme', args.theme); + if (args.view) { + document.querySelectorAll('.app-view').forEach((el) => { + el.classList.toggle('is-active', el.dataset.page === args.view); + }); + } + }""", + {"theme": theme, "view": view}, + ) + page.wait_for_timeout(80) + shot = OUT / f"{name}-{theme}-{tag}.png" + page.screenshot(path=str(shot), full_page=False) + if tag == "1440" and name in ("login-admin", "admin-dashboard", "admin-flows"): + probe = page.evaluate( + """(sels) => sels.map(([sel, props]) => { + const el = document.querySelector(sel); + if (!el) return { sel, missing: true }; + const cs = getComputedStyle(el); + const out = { sel }; + props.forEach((p) => { out[p] = cs[p]; }); + const box = el.getBoundingClientRect(); + out.box = { w: Math.round(box.width), h: Math.round(box.height) }; + return out; + })""", + PROBES, + ) + overflow = page.evaluate( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" + ) + report.append({"page": name, "theme": theme, "overflow": overflow, "probe": probe}) + page.close() + + # 390 drawer + page = browser.new_page(viewport={"width": 390, "height": 844}) + page.add_init_script("() => { try { localStorage.setItem('jinniu-theme', 'day'); } catch (e) {} }") + page.route("**/app.js**", lambda route: route.abort()) + page.goto(f"{base}/company.html", wait_until="domcontentloaded") + page.evaluate( + """() => { + document.documentElement.classList.add('v-fusion'); + document.querySelectorAll('.app-view').forEach((el) => { + el.classList.toggle('is-active', el.dataset.page === 'transfers'); + }); + const d = document.getElementById('transferEvidenceDrawer'); + if (d) d.classList.add('is-open'); + }""" + ) + page.screenshot(path=str(OUT / "company-transfers-drawer-day-390.png"), full_page=False) + page.close() + browser.close() + httpd.shutdown() + (OUT / "probe.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") + print(f"wrote {OUT} ({len(list(OUT.glob('*.png')))} png)") + + +if __name__ == "__main__": + main() diff --git a/tests/test_company_confirm_status_color.py b/tests/test_company_confirm_status_color.py index f09bd1a..6253336 100644 --- a/tests/test_company_confirm_status_color.py +++ b/tests/test_company_confirm_status_color.py @@ -50,7 +50,7 @@ class ConfirmStatusSourceContractTests(unittest.TestCase): self.assertIn('id="workspacePendingStatus"', html) self.assertIn('id="workspaceFlowSub"', html) self.assertIn('data-view-link="reconcile"', html) - self.assertIn("app.js?v=15", html) + self.assertIn("app.js?v=16", html) # 静态初值仍为进行中(黄),由 JS 在 pending=0 时切 done self.assertRegex(html, r'class="flow-step doing"[^>]*data-view-link="reconcile"') diff --git a/tests/test_company_transfers_page.py b/tests/test_company_transfers_page.py index 123b254..91493a3 100644 --- a/tests/test_company_transfers_page.py +++ b/tests/test_company_transfers_page.py @@ -31,8 +31,8 @@ class TransfersPageSourceContractTests(unittest.TestCase): self.assertIn('id="transferEvidenceDrawer"', html) self.assertIn("期间净变动", html) self.assertNotIn("本公司往来合计", html) - self.assertIn("design-system.css?v=7", html) - self.assertIn("app.js?v=15", html) + self.assertIn("design-system.css?v=11", html) + self.assertIn("app.js?v=16", html) # 侧栏顺序:流水管理 → 转账往来 → 往来确认 flows = html.index('data-view="flows"') transfers = html.index('data-view="transfers"') diff --git a/tests/test_reminders_page.py b/tests/test_reminders_page.py index 26b30d6..5858360 100644 --- a/tests/test_reminders_page.py +++ b/tests/test_reminders_page.py @@ -37,8 +37,8 @@ class RemindersPageSourceContractTests(unittest.TestCase): self.assertIn('id="reminder-tbody"', html) self.assertIn('id="reminder-tabs"', html) self.assertIn('id="reminder-detail-drawer"', html) - self.assertIn("design-system.css?v=10", html) - self.assertIn("app.js?v=15", html) + self.assertIn("design-system.css?v=11", html) + self.assertIn("app.js?v=16", html) pending = html.index('id="pending-reminders-card"') history = html.index('id="reminder-history-card"') send = html.index('id="send-reminder-card"') @@ -135,7 +135,7 @@ class RemindersPageLayoutSmokeTests(unittest.TestCase): def test_send_flow_columns_and_no_page_overflow(self) -> None: html = (WEB / "admin.html").read_text(encoding="utf-8") - self.assertIn("app.js?v=15", html) + self.assertIn("app.js?v=16", html) with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() diff --git a/web/admin.html b/web/admin.html index c935ce6..cb5a916 100644 --- a/web/admin.html +++ b/web/admin.html @@ -5,16 +5,26 @@