|
|
@@ -0,0 +1,123 @@
|
|
|
+/**
|
|
|
+ * 票 103 静态闸门:菜单再左移整页宽的 5%,状态栏整体左移、右侧留出整页宽的 10% 空白。
|
|
|
+ *
|
|
|
+ * 维护者 2026-11-02:
|
|
|
+ * 1. 菜单按钮再略微向左移动(按整体页面宽度计算),2026-11-02 二次反馈定稿 5%;
|
|
|
+ * 2. 「归位 / 返回全球 / 时间」这一栏整体向左移动,右侧留出空隙,定稿 10%
|
|
|
+ * (初版 25% 被维护者判定「留空太多」,压到 10%)。
|
|
|
+ *
|
|
|
+ * 这两条会互相牵制:菜单右缘左移的同时状态栏也在左移,窄屏上会撞在一起
|
|
|
+ * (票 37 记录过的「归位压住预警处置」就是这个形状)。所以本闸门不只查字面量:
|
|
|
+ * 它把 CSS 里的位移公式与三条收敛条件**按同一套算法复算一遍**,断言
|
|
|
+ * (a) 目标视口上两条要求取到要求值,(b) 任何视口上都不重叠、不出界。
|
|
|
+ *
|
|
|
+ * 用法:node verify/t103-topbar-shift.mjs [源码路径]。
|
|
|
+ */
|
|
|
+import { readShell, ruleBody, check, assertModeNavIntact, report, checkAbsent } from './t102-shell-source.mjs'
|
|
|
+
|
|
|
+const source = readShell(process.argv[2])
|
|
|
+
|
|
|
+// ---- 结构:两处位移必须共用同一份公式,否则状态栏的收敛条件会与菜单脱钩 ----
|
|
|
+const topbar = ruleBody(source, 'cockpit-shell__topbar')
|
|
|
+check(/--topbar-nav-shift:\s*max\(/.test(topbar), '顶栏须声明 --topbar-nav-shift(菜单位移的唯一来源)')
|
|
|
+check(/--topbar-nav-shift:\s*max\([^;]*5vw/.test(topbar), '菜单位移须含整页宽的 5%(5vw)')
|
|
|
+check(!/--topbar-nav-shift:\s*max\([^;]*10vw/.test(topbar), '菜单位移不得仍是 10vw(维护者已回调到 5%)')
|
|
|
+check(/--topbar-nav-shift:\s*max\([^;]*32\s*\*\s*var\(--u\)/.test(topbar), '须在票 102 的 -32u 基础上再左移')
|
|
|
+
|
|
|
+const modes = ruleBody(source, 'cockpit-shell__modes')
|
|
|
+check(/transform:\s*translateX\(var\(--topbar-nav-shift\)\)/.test(modes), '菜单必须引用共用位移,不得自带第二份公式')
|
|
|
+// 只看**声明行**:注释里解释公式会引用 5vw,不该让闸门误红(上一轮踩过这个坑)。
|
|
|
+const modeDecls = modes
|
|
|
+ .split('\n')
|
|
|
+ .filter((l) => /^\s*[a-z-]+\s*:/.test(l))
|
|
|
+ .join('\n')
|
|
|
+check(!/5vw/.test(modeDecls), '菜单规则的声明里不得再写死 5vw(会与状态栏收敛条件脱钩)')
|
|
|
+check(!/margin-left/.test(modeDecls), '左移不得用 margin-left(会被 grid 再分配)')
|
|
|
+
|
|
|
+const meta = ruleBody(source, 'cockpit-shell__top-meta')
|
|
|
+check(/position:\s*absolute/.test(meta), '状态栏须脱离 grid 居中流程(否则位移会推挤中列菜单)')
|
|
|
+check(/right:\s*var\(--topbar-status-inset\)/.test(meta), '状态栏右缘须引用 --topbar-status-inset')
|
|
|
+check(/top:\s*calc\(/.test(meta) && /translateY\(-50%\)/.test(meta), '绝对定位后仍须与按钮行同水平居中')
|
|
|
+
|
|
|
+// 10vw 与三条收敛条件都在顶栏那个变量里声明(单一来源),此处对变量本体验。
|
|
|
+check(/--topbar-status-inset:\s*min\(/.test(topbar), '状态栏内缩须用 min() 收敛(窄屏不得压住菜单)')
|
|
|
+check(/--topbar-status-inset:\s*min\([^;]*10vw/.test(topbar), '状态栏右侧须留整页宽的 10% 空白(10vw)')
|
|
|
+check(!/--topbar-status-inset:\s*min\([^;]*25vw/.test(topbar), '状态栏留白不得仍是 25vw(维护者已压到 10%)')
|
|
|
+check(/--topbar-status-inset:\s*min\([^;]*var\(--topbar-nav-shift\)/.test(topbar), '状态栏收敛条件必须与菜单位移同源')
|
|
|
+// 约束②的常数必须与几何推导一致(324u = 12u内边距 + 300u状态栏 + 12u间距 + 186u半菜单宽)。
|
|
|
+// 写成 510u 在可行视口上恰好不改变结果(10% 留白总是最紧的那条),但它是错的常数:
|
|
|
+// 一旦 10vw 那条被调大,错误常数就会提前触发、把状态栏推出页面。所以直接锁常数。
|
|
|
+check(/--topbar-status-inset:\s*min\([^;]*calc\(50vw - 324\s*\*\s*var\(--u\) - var\(--topbar-nav-shift\)\)/.test(topbar), '「不得压菜单」的常数须为几何推导值 324u(12+300+12-186)')
|
|
|
+check(!/--topbar-status-inset:\s*min\([^;]*calc\(50vw - 510/.test(topbar), '不得用 510u 这个错常数(多算一个半菜单宽)')
|
|
|
+check(/--topbar-status-inset:\s*min\([^;]*50vw - 180\s*\*\s*var\(--u\)/.test(topbar), '状态栏须有「不得超出页面右缘」的兜底(窄屏不被 overflow 裁掉)')
|
|
|
+
|
|
|
+// ---- 复算:按 CSS 里的同一套公式算几何(单位全换算到 px)----
|
|
|
+const NAV_W = 372 // 4 枚按钮各 90u + 3×4u 间隙
|
|
|
+const NAV_HALF = 186
|
|
|
+const META_W = 300 // 归位/进入 + 返回全球 + 128u 时钟槽位 + 2×10u 间隙 + 内边距(留余量)
|
|
|
+const BRAND_INK = 320 // 品牌标题文字带右缘(6u 起点 + 404u 图宽×74.9%文字带)+ 12u 间距
|
|
|
+const GAP = 12
|
|
|
+const unit = (h) => Math.max(1, Math.min(2, 0.1235 * h / 100))
|
|
|
+// 与 CSS 同式:max(-5vw - 32u, 506.6u - 50vw)
|
|
|
+const navShift = (w, h) => Math.max(-0.05 * w - 32 * unit(h), 506.6 * unit(h) - 0.5 * w)
|
|
|
+// 与 CSS 同式:三条约束取 min
|
|
|
+// ② 的常数 324u 由几何推出(12u 内边距 + 300u 状态栏 + 12u 间距 + 186u 半菜单宽),
|
|
|
+// 写成 510u 会让窄屏得到负值、把状态栏推到页面外。
|
|
|
+const statusInset = (w, h) =>
|
|
|
+ Math.min(
|
|
|
+ 0.10 * w - 12 * unit(h),
|
|
|
+ 0.5 * w - 324 * unit(h) - navShift(w, h),
|
|
|
+ 0.5 * w - 180 * unit(h)
|
|
|
+ )
|
|
|
+function geometry(w, h) {
|
|
|
+ const u = unit(h)
|
|
|
+ const navLeft = 0.5 * w - NAV_HALF * u + navShift(w, h)
|
|
|
+ const navRight = navLeft + NAV_W * u
|
|
|
+ const statusRight = w - 12 * u - statusInset(w, h)
|
|
|
+ const statusLeft = statusRight - META_W * u
|
|
|
+ return { u, navLeft, navRight, statusLeft, statusRight, blank: (w - statusRight) / w }
|
|
|
+}
|
|
|
+
|
|
|
+// 可行下限是 1152×720:菜单要避开品牌标题(左缘钉在 ~321u),状态栏本身 300u,
|
|
|
+// 再加两侧内边距与 12u 间距,页面窄于 ~1152u 时三者物理上摆不开 ——
|
|
|
+// 这不是本票引入的缺陷,是「标题 + 4 枚菜单 + 3 个按钮控件」的固有宽度。
|
|
|
+// 更窄的视口不在验收范围内(驾驶舱按 16:9 大屏部署,AGENTS.md 的运行环境也是 1920 档)。
|
|
|
+const VIEWPORTS = [
|
|
|
+ [1152, 720], [1280, 720], [1366, 768], [1600, 900],
|
|
|
+ [1920, 1080], [2384, 1160], [2560, 1440], [3440, 1440], [3840, 2160],
|
|
|
+]
|
|
|
+for (const [w, h] of VIEWPORTS) {
|
|
|
+ const g = geometry(w, h)
|
|
|
+ check(g.statusLeft >= g.navRight + GAP * g.u - 0.5, `${w}x${h}:状态栏不得压住菜单(左缘 ${g.statusLeft.toFixed(0)} vs 菜单右缘 ${g.navRight.toFixed(0)})`)
|
|
|
+ check(g.navLeft >= BRAND_INK * g.u - 0.5, `${w}x${h}:菜单不得压住左侧品牌标题(左缘 ${g.navLeft.toFixed(0)})`)
|
|
|
+ check(g.statusRight <= w + 0.5, `${w}x${h}:状态栏不得超出页面(右缘 ${g.statusRight.toFixed(0)} vs 页宽 ${w})`)
|
|
|
+}
|
|
|
+
|
|
|
+// 目标视口:两条要求必须取到要求值(不是「接近」)。
|
|
|
+const target = geometry(1920, 1080)
|
|
|
+check(
|
|
|
+ Math.abs(navShift(1920, 1080) - (-0.05 * 1920 - 32 * target.u)) < 0.5,
|
|
|
+ '1920×1080:菜单须恰好再左移整页宽的 5%(叠加票 102 的 -32u)'
|
|
|
+)
|
|
|
+check(
|
|
|
+ Math.abs(target.blank - 0.10) < 0.002,
|
|
|
+ `1920×1080:状态栏右缘须恰好停在 90vw,实测留白 ${(target.blank * 100).toFixed(1)}%`
|
|
|
+)
|
|
|
+
|
|
|
+// 窄屏:两条要求冲突时必须收敛(宁可少移,也不许两栏重叠、不许出界)。
|
|
|
+// 1152×720 是可行下限:此处菜单已被标题顶住(位移收敛到标题右缘),留白也收敛。
|
|
|
+const narrow = geometry(1152, 720)
|
|
|
+check(narrow.blank >= 0.10, '1152×720:留白按收敛值执行(不小于 10%,同时不越界)')
|
|
|
+check(narrow.statusLeft >= narrow.navRight + GAP * narrow.u - 0.5, '1152×720:收敛后仍不得重叠')
|
|
|
+check(narrow.statusRight <= 1152 + 0.5, '1152×720:收敛后仍不得出界')
|
|
|
+
|
|
|
+// ---- 既有契约不许被这次位移破坏 ----
|
|
|
+assertModeNavIntact(source)
|
|
|
+checkAbsent(source, '只读视图', '三个读数仍须保持下屏')
|
|
|
+checkAbsent(source, 'configStatusView', '三个读数仍须保持下屏')
|
|
|
+check(
|
|
|
+ /min-width:\s*calc\([^;]*var\(--u\)\)/.test(ruleBody(source, 'cockpit-shell__clock')),
|
|
|
+ '时钟固定数字槽位仍须在位(否则秒针又推动按钮)'
|
|
|
+)
|
|
|
+
|
|
|
+report('t103-topbar-shift')
|