fix(web): iPhone 键盘收起后顶栏复位,积分块与盘面芯片同高
Independent Staging Quality Gate / validate (push) Successful in 11m21s
Independent Staging Quality Gate / publish (push) Successful in 10m46s

This commit is contained in:
jesse-ux
2026-09-17 13:13:39 +08:00
parent 77e3626a5c
commit 1b508d5e9a
11 changed files with 345 additions and 10 deletions
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-09-17 — 手机键盘收起后顶栏还在;积分块和「当前盘面」一样高
iPhone 上弹键盘再收起、或带着键盘刷新,整页会停在上移后的位置,顶栏点不到。现在键盘一收,页面回到顶,顶栏随时能点。顶栏右侧「当前盘面」和积分两枚芯片同一套尺寸,积分块不再又宽又扁。Skill 版本不变。
## 2026-09-17 — 不再问「你更接近哪一种相处方式」;灰掉的选项和「再说一件事」不会同屏
分盘风格题(「亲密关系里,你更接近哪一种相处方式?」这类)不再作为计分题出现。关系、事业、家人这些线只在引擎给得出带年份的事件时问「某年前后有没有…」,给不出就直接问下一条线或给结果,不再拿性格描述去淘汰候选时间。性格描述仍然出现在平局时的参考题和交付卡的性格列里。
+32
View File
@@ -11970,3 +11970,35 @@
- 相关记录:BUG-478(两个聊天面统一用 `useConversationScrollAnchor``JumpToLatestButton`,并把浮层改成居中)、7a4360d8(同一现象 2026-08-25 的旧修法是把胶囊右对齐,已被 BUG-478 的居中统一取代,本轮未推翻它)、BUG-918
- 复发自:无
- 修复版本:待发布
## BUG-920 | iPhone 键盘收起 / 刷新后整页上移,顶栏点不到
- 状态:resolved
- 首次发现:2026-09-17
- 最近更新:2026-09-17
- 影响面:`ViewportScrollLock``layout.tsx`
- 用户现象:刷新或收起键盘后顶栏滚出屏幕,底部留下空白,点不到 tab。
- 触发条件:iPhone Safari 上聚焦输入框弹出键盘,随后收起或带着偏移刷新。
- 根因:iOS 不支持 `interactive-widget=resizes-content`,键盘弹出时滚动 `window``html, body { overflow: hidden }` 挡住用户拉回,reload 又按 `scrollRestoration=auto` 还原 `scrollY`。代码里原先没有 window 级复位。
- 修复:挂载时 `history.scrollRestoration = "manual"``scrollTo(0,0)``visualViewport` resize/scroll、pageshow、orientationchange、focusout300ms)在键盘已收且 `scrollY > 0` 时复位。键盘打开期间不动。
- 验证:`frontend/tests/mobile-viewport-scroll-lock-20260917.test.ts`。真机待核。
- 防复发:不得靠用户自己滚回去;键盘打开时不得抢 Safari 的露出输入框滚动。
- 相关记录:BUG-918、BUG-919、BUG-921
- 复发自:无
- 修复版本:待发布
## BUG-921 | 顶栏积分块比「当前盘面」扁
- 状态:resolved
- 首次发现:2026-09-17
- 最近更新:2026-09-17
- 影响面:`.credit-button``.chat-header-rectification .rectification-board-peek`、手机顶栏行高
- 用户现象:手机顶栏右侧积分「✦ N」又宽又矮,和旁边「当前盘面」不成一套。
- 触发条件:校正页顶栏同时出现盘面芯片与积分块。
- 根因:`.chat-header-actions button` 把高度写成 32px,比 `.credit-button` 更具体,积分块被压到 32px 却仍强制 `min-width: 64px`、13px 字。盘面芯片用另一套 44px / 14px。两枚都塞在 46px 行里。
- 修复:两枚共用 40px 高、14px tabular、同圆角同内边距;去掉积分 `min-width``::before` inset -2px 把热区扩到 44px。≤767px 顶栏行 `52px + safe-area`(原 64px 覆盖改为与任务书 52px 对齐)。
- 验证:CSS 声明断言高度/字号/圆角一致、无 64px min-width`touch-target-contract` 仍绿。
- 防复发:顶栏两枚芯片必须走同一组声明,不得再各写一套高度。
- 相关记录:BUG-695、BUG-920
- 复发自:无
- 修复版本:待发布
@@ -0,0 +1,23 @@
# 进度 · 手机顶栏可点 + 积分块不扁(2026-09-17
基线:`origin/staging` @ `77e3626a`
分支:`codex/mobile-viewport-scroll-lock-20260917`
BUG920、921。Skill 未 bump。
## 做了什么
- **T1 / BUG-920**`viewport-scroll-lock.ts` + `ViewportScrollLock` 挂在 `layout.tsx`,与 `StaleClientRecovery` 并列。`scrollRestoration=manual`,键盘收起后 `window.scrollTo(0,0)`。键盘打开(visualViewport 矮于 innerHeight100)不动。`focusout` 按让步做 300ms 兜底。未改 `html, body overflow: hidden`,未动 `page.tsx` 状态。
- **T3 / BUG-921**`.chat-header-actions .credit-button` 与顶栏 `.rectification-board-peek` 共用 40px / 14px / 同圆角。积分去掉 `min-width: 64px``::before` 2px 扩到 44px 热区。≤767px 顶栏行从 `64px + safe-area` 收到 `52px + safe-area`(任务书按 46→52 写;手机原先已是 64px 覆盖,现与 52px 对齐)。
- **T2**BUG_HISTORY / CHANGELOG / DESIGN / 真机清单。
## 测试
| 项 | 结果 |
| --- | --- |
| `tsc --noEmit` | 0 错 |
| 改动文件 eslint | 0 error |
| 本单 + touch-target + 顶栏相关 | **84 passed / 0 fail**(含 BUG-695 触屏热区) |
| Python | 未改,不跑快速门 |
| `next build` | worktree junction,留给 Gitea |
无 iOS / Chrome,真机清单留给产品。
+1 -1
View File
@@ -105,7 +105,7 @@
| `TASK-rectification-dead-d9-choice-20260916.md` | `PROGRESS-rectification-dead-d9-choice-20260916.md` | 真机(6aabbe38):删卡流程已通,但**P0** D9 感情题没有自己的探针,`stampChoiceSchemaWithProbe` 退到最高增益探针,盖上了 **D24 学业六星座对照探针**BUG-375 (1) 复发):答「没有」按 D24 计分,04:48–04:59 收窄不可信;GET 投影又匹配不上 → 无卡(BUG-912);打字答「没有」后平局直接出交付卡待回放定性(BUG-913);`NAKSHATRA_TRAITS` 整对塞进一个 option,卡头把相反两极列成共同点(BUG-914)。**串行在 year-focus-overlay 验收之后** | **验收未通过(P0** | `227a7571`+`8d7dfbf0`:不借探针、一列一句、BUG-913 定性均过;但自建探针只在盖戳时注入内存 state,生产答题路径读持久化 state → `stale_probe` 报错、GET 无卡、idle 立刻 superseded(生产路径回放实证)。修复单见下一行 |
| `TASK-rectification-dead-d9-choice-fix-20260916.md` | `PROGRESS-rectification-dead-d9-choice-fix-20260916.md` | **验收修复单**:自建分盘探针照 `withNakshatraBoundaryProbe` 模式在每个读 state 的地方(盖戳 / 答题 / `previousInferenceFromReceipt` / GET 投影 / idle 过期判定 / 计划承接)从 receipt 确定性重建(BUG-915);测试改走生产路径、删手工塞探针的 fixture;顺带 host 前置挪到会话校验之后、登记 phase(BUG-916);死卡不得配 `collect_waiting` 占位(BUG-917);**产品决策 1c**:风格题默认不作全额计分(沿用 09-09 ±1 拍板),无事件探针就走下一条线。**staging 现状:风格题答不了,先跳过**。§1d 已记 **产品决策 (b)** | 已验收 | `5113d457`BUG-915~917,产品拍板 (b) 风格题不计分)。Claude 独立验收:生产路径测试(receipt 不含自建探针)答题 applied / GET 出卡 / idle 不误伤;合并候选门禁 tsc 0 / lint 0 error / npm test 3427 条 31 红与基线逐条相同 / `/` Static / 首屏 gzip 620,107→620,193+0.01%/ pytest 63 绿 / 快速门 Python 798 绿。真机清单 `docs/testing/rectification-dead-d9-choice-fix-20260916.md` |
| `TASK-rectification-mobile-timeline-readout-20260917.md` | `PROGRESS-rectification-mobile-timeline-readout-20260917.md` | 手机截图:时间轴读数第四项被裁成「已…」(nowrap + inset 内边距,BUG-918);「跳到最新」浮层压住选项 C(BUG-919)。灰卡与「再说一件」矛盾归修复单 BUG-915/917 | 已验收 | `e8e98bbd`(合入时重放为本分支提交,BUG-918/919)。时间轴相关 71 条 + 全量同上;手机上隐藏「已对照 N 件」符合 DESIGN §10;桌面是否也移除待产品拍板。真机清单 `docs/testing/rectification-mobile-timeline-readout-20260917.md` |
| `TASK-mobile-viewport-scroll-lock-20260917.md` | `PROGRESS-mobile-viewport-scroll-lock-20260917.md` | iPhone 上键盘收起 / 刷新后整页上移、顶栏点不到(BUG-920):iOS 不支持 `interactive-widget`,键盘弹出时 Safari 滚动 window`html/body overflow: hidden` 让用户拉不回来,reload 又还原 `scrollY`;代码里无任何 window 级复位。补 `ViewportScrollLock``scrollRestoration=manual` + `visualViewport` 复位)。与今天两单无关,既有缺陷。**T3 顶栏积分块「有点扁」(BUG-921)**:两枚芯片 44px 塞在 46px 顶栏里、字号/内边距/圆角各一套、积分块强制 64px 最小宽度;统一尺寸并把手机顶栏放到 52px | 待领取 | |
| `TASK-mobile-viewport-scroll-lock-20260917.md` | `PROGRESS-mobile-viewport-scroll-lock-20260917.md` | iPhone 上键盘收起 / 刷新后整页上移、顶栏点不到(BUG-920):iOS 不支持 `interactive-widget`,键盘弹出时 Safari 滚动 window`html/body overflow: hidden` 让用户拉不回来,reload 又还原 `scrollY`;代码里无任何 window 级复位。补 `ViewportScrollLock``scrollRestoration=manual` + `visualViewport` 复位)。与今天两单无关,既有缺陷。**T3 顶栏积分块「有点扁」(BUG-921)**:两枚芯片 44px 塞在 46px 顶栏里、字号/内边距/圆角各一套、积分块强制 64px 最小宽度;统一尺寸并把手机顶栏放到 52px | 待验收 | `codex/mobile-viewport-scroll-lock-20260917` |
### 聊天主链路与首页
@@ -0,0 +1,9 @@
# 真机清单 · 手机顶栏可点、芯片不扁(2026-09-17)
在 iPhone Safari 的 staging 上走。本机无 iOS,下列条目留给产品负责人。
1. 打开一段对话或生时校正,点输入框弹出键盘,再收起键盘:顶栏(侧栏按钮、标题、当前盘面、积分)仍在屏幕顶部,点得到。
2. 键盘还开着时刷新同一 URL:顶栏仍在顶部,底部没有整页上移留下的空白带。
3. 横屏再竖屏:没有空白带,顶栏可点。
4. 顶栏右侧「当前盘面」与积分「✦ N」两枚芯片一样高、一样圆角,积分块不扁。
5. Android Chrome:弹键盘、收起、刷新,顶栏行为与修之前一样,没有新的跳动。
+3 -1
View File
@@ -319,9 +319,11 @@ The birth-time rectification session is the consultation transcript plus a house
### Chat header
- One line, 46px. Sidebar trigger, session title, and the credit pill. It was 68px and carried a second line reading `分析对象:{盘名}`.
- One line, 46px on desktop. Sidebar trigger, session title, and the credit pill. It was 68px and carried a second line reading `分析对象:{盘名}`.
- The chart a session analyses is now a quiet `--color-ink-tertiary` chip beside the title (`.chat-header-chart`), and it drops out below 768px rather than wrapping the header to two rows. "分析对象" was internal vocabulary; the chart name alone carries the same fact.
- With the composer strip gone as well, the two pieces of permanent chrome went from 68 + 148 to 46 + 124 px.
- On viewports ≤767px the header row is 52px plus the safe-area inset. The credit pill and the rectification 「当前盘面」 chip share one size: 40px visual height, 14px tabular type, the same radius and padding, no 64px min-width. The 44px touch target is the transparent `::before` ring.
- Window-level scroll is reset by `ViewportScrollLock` after the on-screen keyboard closes, so the header cannot be left above the screen.
### Navigation item
+50 -8
View File
@@ -882,7 +882,51 @@ button:disabled { cursor: default; opacity: .45; }
content: "";
}
.chat-header-actions button > svg { width: 14px; height: 14px; }
.credit-button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 0 var(--space-3); cursor: pointer; font-size: var(--type-caption); font-variant-numeric: tabular-nums; transition: background-color 120ms ease-out, transform 120ms ease-out; min-width: 64px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); font-weight: 500; }
/* Header chips share one size. `.chat-header-actions button` is 32px and
more specific than `.credit-button`, which is why the credit pill looked
shorter than 当前盘面(BUG-921). */
.chat-header-actions .credit-button,
.chat-header-rectification .rectification-board-peek {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
height: 40px;
min-height: 40px;
padding: 0 var(--space-3);
gap: var(--space-2);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-canvas-soft);
color: var(--color-ink-secondary);
font-size: var(--type-body-sm);
font-weight: 500;
font-variant-numeric: tabular-nums;
cursor: pointer;
transition: background-color 120ms ease-out, transform 120ms ease-out;
}
.chat-header-actions .credit-button::before,
.chat-header-rectification .rectification-board-peek::before {
position: absolute;
inset: -2px;
content: "";
}
.credit-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: 0 var(--space-3);
cursor: pointer;
font-size: var(--type-body-sm);
font-variant-numeric: tabular-nums;
transition: background-color 120ms ease-out, transform 120ms ease-out;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-canvas-soft);
color: var(--color-ink-secondary);
font-weight: 500;
}
.conversation { min-width: 0; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding-bottom: var(--composer-reserve); background: var(--color-canvas); }
/* Empty state. The greeting is bottom-aligned inside the conversation row and
@@ -1892,8 +1936,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
[data-sidebar="sidebar"][data-mobile-open="true"] { visibility: visible; transform: translateX(0); }
.sidebar-scrim { position: fixed; z-index: 11; inset: 0; display: block; border: 0; background: var(--color-scrim); opacity: 1; visibility: visible; }
[data-sidebar="rail"] { display: none; }
.chat-panel { grid-template-rows: calc(64px + env(safe-area-inset-top)) minmax(0, 1fr) auto; }
.chat-panel.is-rectification { grid-template-rows: calc(64px + env(safe-area-inset-top)) minmax(0, 1fr); }
.chat-panel { grid-template-rows: calc(52px + env(safe-area-inset-top)) minmax(0, 1fr) auto; }
.chat-panel.is-rectification { grid-template-rows: calc(52px + env(safe-area-inset-top)) minmax(0, 1fr); }
.chat-panel:has(.starter-balance) { grid-template-rows: calc(52px + env(safe-area-inset-top)) minmax(0, 1fr) auto minmax(0, 1fr); }
.chat-header { gap: var(--space-3); padding: max(0px, env(safe-area-inset-top)) var(--space-3) 0; }
.conversation.is-empty {
display: block;
@@ -2962,15 +3007,10 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
display: none;
}
.chat-header-rectification .rectification-board-peek {
display: inline-flex;
align-items: center;
justify-content: flex-end;
width: auto;
max-width: min(11.5rem, 42vw);
min-height: 44px;
margin: 0;
padding: 0 var(--space-3);
gap: var(--space-2);
}
.chat-header-rectification .rectification-board-peek strong,
.chat-header-rectification .rectification-board-peek span {
@@ -2979,6 +3019,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: inherit;
font-variant-numeric: tabular-nums;
}
@media (max-width: 480px) {
.chat-header-rectification .rectification-board-peek span {
+2
View File
@@ -3,6 +3,7 @@ import localFont from "next/font/local";
import Script from "next/script";
import { Toaster } from "@/components/ui/sonner";
import { StaleClientRecovery } from "@/components/stale-client-recovery";
import { ViewportScrollLock } from "@/components/viewport-scroll-lock";
import { themePreferenceBootScript } from "@/lib/theme-preference";
// Vendored Inter latin variable (OFL). Do not switch back to next/font/google;
@@ -53,6 +54,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
</head>
<body>
<StaleClientRecovery />
<ViewportScrollLock />
{children}
<Toaster />
</body>
@@ -0,0 +1,12 @@
"use client";
import { useEffect } from "react";
import { installViewportScrollLock } from "@/lib/viewport-scroll-lock";
export function ViewportScrollLock() {
useEffect(() => {
return installViewportScrollLock(window as unknown as Parameters<typeof installViewportScrollLock>[0]);
}, []);
return null;
}
+85
View File
@@ -0,0 +1,85 @@
/**
* iOS Safari scrolls `window` to keep a focused input in view. `html, body
* { overflow: hidden }` then blocks the user from dragging back, and reload
* restores that offset. Reset window scroll only after the keyboard is gone.
*/
export const KEYBOARD_OPEN_GAP_PX = 100;
export const KEYBOARD_CLOSED_SLACK_PX = 1;
export function isKeyboardOpen(visualHeight: number, innerHeight: number): boolean {
return visualHeight < innerHeight - KEYBOARD_OPEN_GAP_PX;
}
export function shouldResetWindowScroll(input: {
scrollY: number;
innerHeight: number;
visualViewportHeight: number | null;
}): boolean {
if (input.scrollY <= 0) return false;
if (input.visualViewportHeight == null) return true;
if (isKeyboardOpen(input.visualViewportHeight, input.innerHeight)) return false;
return input.visualViewportHeight >= input.innerHeight - KEYBOARD_CLOSED_SLACK_PX;
}
type ViewportLike = {
height: number;
addEventListener: (type: "resize" | "scroll", listener: () => void) => void;
removeEventListener: (type: "resize" | "scroll", listener: () => void) => void;
};
export type ViewportScrollLockHost = {
matchMedia?: (query: string) => { matches: boolean };
visualViewport?: ViewportLike | null;
innerHeight: number;
scrollY: number;
scrollTo: (x: number, y: number) => void;
history: { scrollRestoration: string };
addEventListener: (type: string, listener: () => void) => void;
removeEventListener: (type: string, listener: () => void) => void;
document: {
addEventListener: (type: string, listener: () => void) => void;
removeEventListener: (type: string, listener: () => void) => void;
};
setTimeout?: (fn: () => void, ms: number) => unknown;
};
export function installViewportScrollLock(host: ViewportScrollLockHost): () => void {
const coarse = host.matchMedia?.("(pointer: coarse)").matches === true;
const viewport = host.visualViewport ?? null;
if (!coarse && !viewport) return () => undefined;
host.history.scrollRestoration = "manual";
host.scrollTo(0, 0);
const resetIfSettled = () => {
if (!shouldResetWindowScroll({
scrollY: host.scrollY,
innerHeight: host.innerHeight,
visualViewportHeight: viewport?.height ?? null,
})) return;
host.scrollTo(0, 0);
};
const onPageShow = () => {
host.scrollTo(0, 0);
};
const onFocusOut = () => {
const later = host.setTimeout ?? ((fn: () => void) => fn());
later(resetIfSettled, 300);
};
viewport?.addEventListener("resize", resetIfSettled);
viewport?.addEventListener("scroll", resetIfSettled);
host.addEventListener("pageshow", onPageShow);
host.addEventListener("orientationchange", resetIfSettled);
host.document.addEventListener("focusout", onFocusOut);
return () => {
viewport?.removeEventListener("resize", resetIfSettled);
viewport?.removeEventListener("scroll", resetIfSettled);
host.removeEventListener("pageshow", onPageShow);
host.removeEventListener("orientationchange", resetIfSettled);
host.document.removeEventListener("focusout", onFocusOut);
};
}
@@ -0,0 +1,124 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { cssDeclarations } from "./css-contract-test-support.ts";
import {
installViewportScrollLock,
isKeyboardOpen,
shouldResetWindowScroll,
type ViewportScrollLockHost,
} from "../src/lib/viewport-scroll-lock.ts";
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
const component = readFileSync(new URL("../src/components/viewport-scroll-lock.tsx", import.meta.url), "utf8");
const lock = readFileSync(new URL("../src/lib/viewport-scroll-lock.ts", import.meta.url), "utf8");
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
test("ViewportScrollLock mounts beside StaleClientRecovery and uses the shared installer", () => {
assert.match(layout, /import \{ ViewportScrollLock \} from "@\/components\/viewport-scroll-lock"/);
assert.match(layout, /<StaleClientRecovery \/>\s*<ViewportScrollLock \/>/);
assert.match(component, /installViewportScrollLock/);
assert.match(lock, /history\.scrollRestoration = "manual"/);
assert.match(lock, /addEventListener\("resize"/);
assert.match(lock, /addEventListener\("scroll"/);
assert.match(lock, /addEventListener\("pageshow"/);
assert.match(lock, /addEventListener\("orientationchange"/);
assert.match(lock, /addEventListener\("focusout"/);
assert.match(lock, /KEYBOARD_OPEN_GAP_PX = 100/);
assert.doesNotMatch(lock, /setInterval/);
});
test("keyboard-open window offsets are left alone", () => {
assert.equal(isKeyboardOpen(680, 800), true);
assert.equal(isKeyboardOpen(799, 800), false);
assert.equal(shouldResetWindowScroll({
scrollY: 260,
innerHeight: 800,
visualViewportHeight: 680,
}), false);
assert.equal(shouldResetWindowScroll({
scrollY: 260,
innerHeight: 800,
visualViewportHeight: 800,
}), true);
assert.equal(shouldResetWindowScroll({
scrollY: 0,
innerHeight: 800,
visualViewportHeight: 800,
}), false);
});
test("visualViewport resize resets window scroll once the keyboard has closed", () => {
const calls: Array<[number, number]> = [];
const viewportListeners = new Map<string, () => void>();
const host: ViewportScrollLockHost = {
innerHeight: 800,
scrollY: 260,
scrollTo(x, y) {
host.scrollY = y;
calls.push([x, y]);
},
history: { scrollRestoration: "auto" },
visualViewport: {
height: 800,
addEventListener(type, listener) {
viewportListeners.set(type, listener);
},
removeEventListener(type) {
viewportListeners.delete(type);
},
},
matchMedia: () => ({ matches: true }),
addEventListener() {},
removeEventListener() {},
document: {
addEventListener() {},
removeEventListener() {},
},
};
const uninstall = installViewportScrollLock(host);
assert.equal(host.history.scrollRestoration, "manual");
assert.deepEqual(calls, [[0, 0]]);
host.scrollY = 180;
host.visualViewport!.height = 680;
viewportListeners.get("resize")?.();
assert.equal(calls.length, 1);
host.visualViewport!.height = 800;
viewportListeners.get("resize")?.();
assert.deepEqual(calls.at(-1), [0, 0]);
assert.equal(calls.length, 2);
uninstall();
});
test("header chips share height, type size, and radius, and drop the 64px min-width", () => {
const shared = cssDeclarations(
".chat-header-actions .credit-button,\n.chat-header-rectification .rectification-board-peek",
styles,
);
assert.match(shared, /height:\s*40px/);
assert.match(shared, /min-height:\s*40px/);
assert.match(shared, /font-size:\s*var\(--type-body-sm\)/);
assert.match(shared, /border-radius:\s*var\(--radius-md\)/);
assert.match(shared, /font-variant-numeric:\s*tabular-nums/);
assert.doesNotMatch(shared, /min-width:/);
const credit = cssDeclarations(".credit-button", styles);
assert.doesNotMatch(credit, /min-width:\s*64px/);
const hit = cssDeclarations(
".chat-header-actions .credit-button::before,\n.chat-header-rectification .rectification-board-peek::before",
styles,
);
assert.match(hit, /inset:\s*-2px/);
});
test("compact chat header is 52px plus the safe-area inset", () => {
assert.match(
styles,
/\.chat-panel \{ grid-template-rows: calc\(52px \+ env\(safe-area-inset-top\)\) minmax\(0, 1fr\) auto; \}/,
);
assert.match(
styles,
/\.chat-panel\.is-rectification \{ grid-template-rows: calc\(52px \+ env\(safe-area-inset-top\)\) minmax\(0, 1fr\); \}/,
);
assert.match(styles, /\.chat-panel \{[^}]*grid-template-rows:\s*46px minmax\(0,\s*1fr\) auto/);
});