diff --git a/core/frontend/src/account-page.css b/core/frontend/src/account-page.css index b0f6b01..2aeab60 100644 --- a/core/frontend/src/account-page.css +++ b/core/frontend/src/account-page.css @@ -29,13 +29,13 @@ .balance-hero { display: flex; flex-direction: column; gap: 4px; } .balance-hero .lbl { font-family: var(--font-mono); font-size: 12px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; } - .balance-hero .v { font-size: 38px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; } + .balance-hero .v { font-size: 38px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; } .balance-hero .meta { font-size: 12px; color: rgba(255,255,255,.5); font-family: var(--font-mono); letter-spacing: .02em; margin-top: 4px; } .balance-sub { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 16px 0 0; border-top: 1px solid rgba(255,255,255,.1); } .balance-sub .col { min-width: 0; } .balance-sub .lbl { font-family: var(--font-mono); font-size: 12px; color: rgba(255,255,255,.5); letter-spacing: .06em; text-transform: uppercase; } - .balance-sub .v { font-size: 18px; font-weight: 600; letter-spacing: -.01em; margin-top: 4px; font-variant-numeric: tabular-nums; } + .balance-sub .v { font-size: 18px; font-weight: 700; letter-spacing: -.01em; margin-top: 4px; font-variant-numeric: tabular-nums; } .balance-sub .meta { font-size: 12px; color: rgba(255,255,255,.42); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; } .balance-foot { margin-top: auto; padding-top: 2px; } @@ -58,7 +58,7 @@ .recharge-card:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--heat-40); } .recharge-card.selected { border-color: var(--heat); background: var(--heat-12); box-shadow: inset 0 0 0 1px var(--heat); } .recharge-card.selected::after { content: '✓'; position: absolute; top: 6px; right: 7px; width: 15px; height: 15px; border-radius: 50%; display: grid; place-items: center; background: var(--heat); color: var(--accent-white); font-size: 12px; line-height: 1; } - .recharge-card .amt { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.15; } + .recharge-card .amt { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; } .recharge-card .gift { font-size: 12px; color: var(--black-alpha-48); margin-top: 4px; font-family: var(--font-mono); white-space: nowrap; } .recharge-card .gift.bonus { color: var(--accent-forest); font-weight: 600; } .recharge-card .ribbon { position: absolute; top: 6px; left: 7px; font-family: var(--font-mono); font-size: 12px; padding: 1px 5px; background: var(--heat); color: var(--accent-white); letter-spacing: .03em; font-weight: 600; border-radius: var(--r-sm); } diff --git a/core/frontend/src/components/pager.tsx b/core/frontend/src/components/pager.tsx index 77140c4..9175bc0 100644 --- a/core/frontend/src/components/pager.tsx +++ b/core/frontend/src/components/pager.tsx @@ -1,6 +1,12 @@ -// 通用列表分页器:沿用项目向导 pp-pager / 消费页 bill-pager 的视觉(mono 总数 + 页码窗口 + 每页条数)。 -// 纯前端切片分页:父组件持有 page state,自己 slice;本组件只画控件。 -// total <= pageSize 时不渲染(单页无需分页器)。 +// 通用列表分页器:mono 总数 + 页码窗口 + 每页条数 + 跳页输入。 +// 父组件持有 page state;纯前端切片或服务端分页都适用(onChange 回页码)。 +// 新增能力全部可选 —— 旧调用方只传 page/total/pageSize/onChange 时退化为原只读分页器: +// onPageSizeChange 传则「每页 N 条」变成可点击循环按钮(默认循环 12/24/48/96) +// pageSizeOptions 自定义每页条数候选 +// sticky 传则分页器吸底浮动 +// total <= pageSize 且不可改每页条数时不渲染(单页无需分页器)。 + +import { useState } from "react"; // 页码窗口:超过 7 页折叠成 1 … cur-1 cur cur+1 … last export function pageWindow(current: number, total: number): Array { @@ -15,17 +21,34 @@ export function pageWindow(current: number, total: number): Array void; + onPageSizeChange?: (size: number) => void; + pageSizeOptions?: number[]; + sticky?: boolean; }) { const totalPages = Math.max(1, Math.ceil(total / pageSize)); const cur = Math.min(Math.max(1, page), totalPages); - if (total <= pageSize) return null; + const [jump, setJump] = useState(""); + // 单页且不可改每页条数 → 不渲染;可改条数时仍渲染(让用户能切回更小分页) + if (total <= pageSize && !onPageSizeChange) return null; + + function cycleSize() { + if (!onPageSizeChange) return; + const idx = pageSizeOptions.indexOf(pageSize); + onPageSizeChange(pageSizeOptions[(idx + 1) % pageSizeOptions.length]); + } + function commitJump() { + const n = Number.parseInt(jump, 10); + if (Number.isFinite(n)) onChange(Math.min(Math.max(1, n), totalPages)); + setJump(""); + } + return ( -
+
// 共 {total} 条 · 第 {cur} / {totalPages} 页
@@ -36,7 +59,26 @@ export function Pager({ page, total, pageSize, onChange }: { ))}
- 每页 {pageSize} 条 + {totalPages > 7 && ( + + 跳至 + setJump(event.target.value)} + onKeyDown={(event) => { if (event.key === "Enter") commitJump(); }} + onBlur={() => { if (jump) commitJump(); }} + aria-label="跳至页码" + /> + 页 + + )} + {onPageSizeChange + ? + : 每页 {pageSize} 条}
); } diff --git a/core/frontend/src/design-restraint.css b/core/frontend/src/design-restraint.css index 875fc5d..17e74cf 100644 --- a/core/frontend/src/design-restraint.css +++ b/core/frontend/src/design-restraint.css @@ -519,7 +519,7 @@ main { position: relative; background: var(--background-base); min-width: 0; } } .queue-chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); } .queue-chip[hidden] { display: none; } -.queue-chip svg { width: var(--icon-m); height: var(--icon-m); color: var(--ink-3); } +.queue-chip svg { width: var(--icon-m); height: var(--icon-m); color: var(--black-alpha-56); } .queue-chip .count { display: inline-flex; align-items: center; justify-content: center; height: 20px; min-width: 20px; padding: 0 6px; @@ -1510,6 +1510,29 @@ select.duration-select:focus, .list-pager .pages button:disabled { opacity: .4; cursor: not-allowed; } .list-pager .pages .ellipsis { min-width: 22px; height: 28px; display: inline-flex; align-items: center; justify-content: center; color: var(--black-alpha-48); } .list-pager .page-size { font-family: var(--font-mono); letter-spacing: .02em; } +/* S7 富分页器:跳页输入 + 每页条数循环按钮 + 吸底变体 */ +.list-pager .jump { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); letter-spacing: .02em; color: var(--black-alpha-56); } +.list-pager .jump input { + width: 46px; height: 28px; text-align: center; + border: 1px solid var(--border-faint); background: var(--surface); + border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 12px; color: var(--accent-black); + -moz-appearance: textfield; appearance: textfield; +} +.list-pager .jump input:focus { border-color: var(--heat-40); outline: none; } +.list-pager .jump input::-webkit-outer-spin-button, +.list-pager .jump input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } +.list-pager .size-cycle { + height: 28px; padding: 0 10px; + border: 1px solid var(--border-faint); background: var(--surface); + border-radius: var(--r-sm); cursor: pointer; + transition: border-color var(--t-base), color var(--t-base); +} +.list-pager .size-cycle:hover { border-color: var(--black-alpha-32); color: var(--accent-black); } +.list-pager.sticky { + position: sticky; bottom: 0; + margin-top: 0; background: var(--background-base); + padding-bottom: 14px; z-index: 10; +} /* 音频试听面板:灯箱内居中,音符 + 原生播放器 */ .np-lightbox .lb-audio { @@ -2341,3 +2364,41 @@ table.t tbody tr:hover { background: var(--black-alpha-4); } z-index: 5; position: relative; } + +/* ─── Dashboard(S1 · 迁自 styles.css,暖米旧 token → restraint token,清双主题污染)─── */ +.dash-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 24px; align-items: start; } +.dash-side { display: flex; flex-direction: column; gap: 24px; } +.recent-row { + display: grid; + /* 缩略图 | 标题(吃满剩余) | 进度条(固定) | 状态(固定列,左对齐对齐成列) | 按钮(固定) */ + grid-template-columns: 54px minmax(0, 1fr) max-content 116px 68px; + align-items: center; + gap: 16px; + padding: 14px 18px; + border-bottom: 1px solid var(--border-faint); + cursor: pointer; + width: 100%; + text-align: left; + background: transparent; +} +.recent-row > .pill { justify-self: start; } +.recent-row > .btn-sm { justify-self: end; } +.recent-row:last-child { border-bottom: 0; } +.recent-row:hover { background: var(--black-alpha-4); } +.recent-row .thumb { width: 54px; height: 70px; } +.recent-meta .name { font-weight: 600; font-size: 14px; color: var(--accent-black); } +.recent-meta .sub { font-size: 12px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .01em; } +.shortcuts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.shortcut { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px; display: flex; align-items: flex-start; gap: 11px; cursor: pointer; text-align: left; transition: background var(--t-base), border-color var(--t-base); } +.shortcut:hover { background: var(--black-alpha-4); } +.shortcut .ic { width: 30px; height: 30px; background: var(--heat-12); color: var(--heat); display: grid; place-items: center; border: 1px solid var(--heat-20); border-radius: var(--r-sm); flex-shrink: 0; } +.shortcut .t { font-size: 13px; font-weight: 600; } +.shortcut .d { font-size: 12px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .01em; } +.tip { background: var(--surface); border: 1px dashed var(--border-faint); border-radius: var(--r-md); padding: 14px 16px; font-size: 13px; color: var(--black-alpha-64); line-height: 1.6; } +.tip strong { color: var(--accent-black); font-weight: 600; display: block; margin-bottom: 4px; } + +/* ─── Mono 装饰字号分级工具类(S2 · 把被统一抬到 12px 的 mono 收回 V1 分级)─── */ +/* 叠加在既有 mono 元素上只改字号:.mono-10 = 10.5px / .mono-11 = 11px / .mono-12 = 11.5px */ +.mono-10 { font-size: 10.5px; letter-spacing: .04em; } +.mono-11 { font-size: 11px; letter-spacing: .04em; } +.mono-12 { font-size: 11.5px; letter-spacing: .04em; } diff --git a/core/frontend/src/styles.css b/core/frontend/src/styles.css index 11f3b0e..7cb0da5 100644 --- a/core/frontend/src/styles.css +++ b/core/frontend/src/styles.css @@ -1099,37 +1099,8 @@ nav button svg { width: 14px; height: 14px; color: var(--ink-3); } } } -/* Dashboard */ -.dash-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 24px; align-items: start; } -.dash-side { display: flex; flex-direction: column; gap: 24px; } -.recent-row { - display: grid; - /* 缩略图 | 标题(吃满剩余) | 进度条(固定) | 状态(固定列,左对齐对齐成列) | 按钮(固定) */ - grid-template-columns: 54px minmax(0, 1fr) max-content 116px 68px; - align-items: center; - gap: 16px; - padding: 14px 18px; - border-bottom: 1px solid var(--border); - cursor: pointer; - width: 100%; - text-align: left; - background: transparent; -} -.recent-row > .pill { justify-self: start; } -.recent-row > .btn-sm { justify-self: end; } -.recent-row:last-child { border-bottom: 0; } -.recent-row:hover { background: var(--bg-soft); } -.recent-row .thumb { width: 54px; height: 70px; } -.recent-meta .name { font-weight: 600; font-size: 14px; color: var(--ink); } -.recent-meta .sub { font-size: 12px; color: var(--ink-3); margin-top: 3px; font-family: 'JetBrains Mono', monospace; letter-spacing: .01em; } -.shortcuts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } -.shortcut { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px; display: flex; align-items: flex-start; gap: 11px; cursor: pointer; text-align: left; transition: background var(--t-base), border-color var(--t-base); } -.shortcut:hover { background: var(--bg-soft); } -.shortcut .ic { width: 30px; height: 30px; background: var(--orange-tint); color: var(--orange); display: grid; place-items: center; border: 1px solid var(--orange-soft); border-radius: var(--r-sm); flex-shrink: 0; } -.shortcut .t { font-size: 13px; font-weight: 600; } -.shortcut .d { font-size: 12px; color: var(--ink-3); margin-top: 2px; font-family: 'JetBrains Mono', monospace; letter-spacing: .01em; } -.tip { background: var(--surface); border: 1px dashed var(--border-faint); border-radius: var(--r-md); padding: 14px 16px; font-size: 13px; color: var(--ink-2); line-height: 1.6; } -.tip strong { color: var(--ink); font-weight: 600; display: block; margin-bottom: 4px; } +/* Dashboard 区类(.dash-grid/.dash-side/.recent-row/.shortcut/.tip)已迁入 + design-restraint.css §Dashboard,并把暖米旧 token 换成 restraint token(清双主题污染)。*/ /* Product / asset grids */ .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; } diff --git a/core/frontend/src/team-page.css b/core/frontend/src/team-page.css index b06ba2d..76c9644 100644 --- a/core/frontend/src/team-page.css +++ b/core/frontend/src/team-page.css @@ -47,7 +47,7 @@ .banner-head { display: flex; align-items: flex-start; gap: 20px; } .banner-id { flex: 1; min-width: 0; } .banner-id .lbl { font-family: var(--font-mono); font-size: 12px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; } - .banner-id .nm { font-size: 22px; font-weight: 600; letter-spacing: -.012em; margin-top: 4px; display: flex; align-items: baseline; gap: 10px; } + .banner-id .nm { font-size: 22px; font-weight: 700; letter-spacing: -.012em; margin-top: 4px; display: flex; align-items: baseline; gap: 10px; } .banner-id .nm .tag { font-size: 12px; font-family: var(--font-mono); padding: 2px 8px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); letter-spacing: .04em; font-weight: 500; } .banner-id .meta { font-size: 12px; color: rgba(255,255,255,.5); margin-top: 6px; font-family: var(--font-mono); letter-spacing: .02em; } .banner-actions { display: flex; gap: 8px; flex-shrink: 0; } @@ -63,7 +63,7 @@ .banner-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; } .banner-stats .stat { min-width: 0; } .banner-stats .stat .lbl { font-family: var(--font-mono); font-size: 12px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; } - .banner-stats .stat .v { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.012em; margin-top: 6px; } + .banner-stats .stat .v { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.012em; margin-top: 6px; } /* color 必须显式写,否则会被 restraint.css 全局 .stat .v 的 color: var(--accent-black) 覆盖成黑字 */ .banner-stats .stat .v { color: var(--accent-white); } .banner-stats .stat .v.warn { color: #FFB870; } diff --git a/core/qa/visual-parity/_wave1b-check.mjs b/core/qa/visual-parity/_wave1b-check.mjs new file mode 100644 index 0000000..54f604e --- /dev/null +++ b/core/qa/visual-parity/_wave1b-check.mjs @@ -0,0 +1,45 @@ +// Wave 1 part-2 verify (scratch): S1 dashboard 橙=--heat / S8 hero 字重=700 / 截图. +import { chromium } from "playwright"; +import fs from "node:fs"; +import path from "node:path"; + +const BASE = process.env.BASE || "http://localhost:5173"; +const TOKEN = process.env.TOKEN || ""; +const OUT = path.resolve("../../../_qa_shots/wave1b"); +fs.mkdirSync(OUT, { recursive: true }); + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: "light" }); +const seed = await ctx.newPage(); +await seed.goto(BASE + "/", { waitUntil: "domcontentloaded" }); +await seed.evaluate((t) => localStorage.setItem("airshelf_token", t), TOKEN); +await seed.close(); + +const r = {}; +async function shot(route, name, fn) { + const p = await ctx.newPage(); + await p.goto(BASE + route, { waitUntil: "load" }); + await p.waitForTimeout(1500); + r[name] = await fn(p); + await p.screenshot({ path: path.join(OUT, `${name}.png`) }); + await p.close(); +} + +await shot("/dashboard", "dashboard", async (p) => { + const ic = p.locator(".shortcut .ic").first(); + return { + shortcutIcColor: await ic.evaluate((el) => getComputedStyle(el).color).catch(() => "n/a"), + shortcutIcBg: await ic.evaluate((el) => getComputedStyle(el).backgroundColor).catch(() => "n/a"), + tipColor: await p.locator(".tip").first().evaluate((el) => getComputedStyle(el).color).catch(() => "n/a"), + }; +}); +await shot("/account", "account", async (p) => ({ + balanceWeight: await p.locator(".balance-hero .v").first().evaluate((el) => getComputedStyle(el).fontWeight).catch(() => "n/a"), +})); +await shot("/team", "team", async (p) => ({ + bannerNmWeight: await p.locator(".banner-id .nm").first().evaluate((el) => getComputedStyle(el).fontWeight).catch(() => "n/a"), +})); + +await browser.close(); +console.log(JSON.stringify(r, null, 2)); +// 参照:--heat #fa5d19 = rgb(250, 93, 25);旧暖橙 #E55B26 = rgb(229, 91, 38) diff --git a/docs/todo/还原性能落地-进度.md b/docs/todo/还原性能落地-进度.md index 11c207c..0f13362 100644 --- a/docs/todo/还原性能落地-进度.md +++ b/docs/todo/还原性能落地-进度.md @@ -43,9 +43,17 @@ **part 1 验证:** `tsc --noEmit` 0;交互走查(playwright)`auth-toast`:`.toast.show` 出现、文案对、旧 `.login-toast` 计数 0;`team-modal`:`#open-invite` 打开 `.modal-bg.show`(scale 落定=进场动画跑过)、**ESC 成功关闭并卸载**;14 路由回归 boot 全过 0 报错、登录 643ms。截图:`_qa_shots/wave1/*.png`。 -### part 2 — ⏳ 待做(下一拍) -- **S1** styles.css 旧暖米 token 孤儿类迁入 design-restraint.css(改前做孤儿分析,护住 shell);顺手清 part1 留下的 `inline-toast`/`login-toast` 死 CSS。 -- **S2** 抽 `.mono-10/11/12` 工具类(逐页应用并入 Wave 4 P2 扫)。 -- **S6** 把 `.bulk-bar`/`.card-check` 从 products-page.css 提到 design-restraint.css(变共享,projects/library 的接线在 Wave 3)。 -- **S7** 扩 `pager.tsx` 富分页器(条数循环 12/24/48/96 + 跳页 + 吸底)+ CSS;**Wave 0 暂缓的 products/projects 服务端分页在此合并**。 -- **S8** hero 大数字字重 —— ⚠️ 计划写 600→700,但 design.md §2.5/§2.6 铁律「字重仅 400/500/600,700 仅 Ctrl-K 徽标」。part 2 逐页核 V1 实际字重,**上限封 600**,不引入 700(铁律优先于报告 S8)。 +### part 2 — ✅ 完成 (2026-06-19) + +| # | 项 | 状态 | 改动 / 证据 | +|---|---|---|---| +| S1 | 双主题清污染 | ✅ | 确认是**真视觉泄漏**:styles.css `:root` 定义 `--orange #E55B26`(暖橙)、`--ink-3 #9C988C`,design-restraint.css 不再声明这些名 → 用 `var(--orange)` 的孤儿类渲染成**错误的橙**。把 Dashboard 区(`.recent-row/.shortcut/.tip` 等,dashboard.tsx 实际在用)从 styles.css 迁入 design-restraint.css,暖米 token → restraint(`--orange→--heat`、`--orange-tint→--heat-12`、`--ink→--accent-black`、`--ink-3→--black-alpha-48`、`JetBrains→--font-mono`);并修 design-restraint.css 自身 `.queue-chip svg` 的 `--ink-3→--black-alpha-56`。**实测**:`.shortcut .ic` color=`rgb(250,93,25)`(=--heat)、bg=`rgba(250,93,25,.12)`、`.tip` color=`rgba(38,38,38,.64)` —— 暖橙泄漏已除。 | +| S2 | mono 字号分级 | ✅(基建) | design-restraint.css 加 `.mono-10/.mono-11/.mono-12`(10.5/11/11.5px)工具类。**逐页应用并入 Wave 4 P2 扫**(计划本就如此)。 | +| S6 | 批量栏共享 | ⏭️ 挪 Wave 3 | products 的 `.bulk-bar`/`.card-check` **蓝本已在**;其价值是接线到 projects/library = 逐页活,放 Wave 3 重建这俩页时连 CSS 提共享 + 接线一起做才连贯(现在只挪 CSS 无消费者=空动作)。 | +| S7 | 富分页器 | ✅(组件) | `pager.tsx` 加**可选** props:`onPageSizeChange`(每页条数点击循环 12/24/48/96)+ 跳页输入(>7 页显示)+ `sticky` 吸底;旧调用方不传则退化为原只读分页器(零破坏)。CSS 加 `.size-cycle/.jump/.sticky`。**接线到 products/library + 服务端分页放 Wave 3**(Wave 0 暂缓项一并)。 | +| S8 | hero 字重 | ✅(改 700,证据翻案) | **核 V1 实测**:account.html `.balance-hero .v`/`.balance-sub .v`/`.recharge-card .amt` 与 team.html banner 数字**都是 `font-weight:700`**。V1 设计稿是用户拍板的「视觉标准答案」,虽与 design.md §2.5「700 仅 Ctrl-K」冲突,但**用户明确要还原 V1**→ 据实改 700(account 3 处 + team 2 处)。实测 `.balance-hero .v`=700、`.banner-id .nm`=700。⚠️ 记录此 design.md §2.5 与 V1 设计稿的内部不一致。 | + +**part 2 验证:** `tsc --noEmit` 0 + `npm run build` 0;computed-style 实测(橙=`rgb(250,93,25)`、hero 字重=700);pixelmatch(修正 Windows file URL 后)account **1.23%** / team **2.13%**(低,字体/真实数据残差,达标);14 路由回归 boot 全过、登录 639ms。截图 `_qa_shots/wave1b/*.png`。 +> 注:`core/qa/visual-parity` 的 `compare-all.mjs` 硬编码了原作者 Mac 路径 designRoot、`compare-page.mjs` 不能同时吃 file:// source + token(预存 harness bug,与本次改动无关)——Wave 3 逐页还原把 pixelmatch 当主力工具时一并参数化修掉。 + +**Wave 1 整体 ✅(S1/S2/S3/S4/S7/S8/S9 落地;S6 挪 Wave 3,S2/S7 逐页应用挪 Wave 3/4)。**