补上类型检查和 lint 没过的几处。
测试里的类型标注补全。侧栏拖宽把手改用组件库按钮,样子仍是 6px 透明细条。报告列表换人时不再在渲染中改引用。删掉已经没人用的证据回执类名。 Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
6998dc6ead
commit
4d63580708
@@ -119,3 +119,22 @@
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 上面这几处,加上首页行数和外观菜单 | 在 `frontend` 下 `node node_modules/tsx/dist/cli.mjs --test` 跑清单、校正表面、清单刷新、首屏揭幕、滚动、输入法、字数、旧引导、起步问、首页行数、外观、首屏屏、起步页、提示监听、分栏挂载 | 85 通过,0 失败,0 取消,退出码 0 |
|
||||
|
||||
## 类型检查和 lint
|
||||
|
||||
没有新的产品故障,所以没有新的 BUG 编号。报告列表换人时不在渲染里改引用,和 BUG-620 是同一条规矩。
|
||||
|
||||
- 四处测试只有类型不过:语言切换的循环变量、登录邮箱赋值、清单刷新的路由桩缺 `bfcacheId`、动效测试读透明。断言没改。动效仍是「失败或太慢时不能藏起来」。
|
||||
- 侧栏拖宽把手改成组件库按钮。样子仍是 6px 透明细条。
|
||||
- 报告列表记住「这份名单属于谁」。换人之后,上一位还在写的报告不会被当成这一位刚写完。
|
||||
- 删掉已经没人用的类名 `birth-time-evidence-receipt`。原值在白名单里。新值删掉。原因是旧校正组件删了,这个类名没人用了。
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 类型检查 | 在 `frontend` 下 `node_modules\.bin\tsc.cmd --noEmit` | 退出码 0 |
|
||||
| lint | 在 `frontend` 下 `npm run lint` | 退出码 0,0 error,122 条原有 warning |
|
||||
| 类型那四个文件 | `node node_modules/tsx/dist/cli.mjs --test` 语言切换、登录、清单刷新、动效 | 8 通过,0 失败,0 取消,退出码 0 |
|
||||
| 把手、导出、报告列表、按钮样式、白名单规则 | 同上,六个文件 | 31 通过,0 失败,0 取消,退出码 0 |
|
||||
| 类名白名单 | 同上,`class-name-definition-contract.test.ts` | 3 通过,0 失败,0 取消,退出码 0 |
|
||||
|
||||
全量 `npm test` 这次是在改文件的同时跑的,不能当作交回数字。5208 项,通过 5176,失败 32,取消 0,退出码 1。32 条里,13 条是 Windows 已知的数据库名单。`birth-time-evidence-receipt` 是上面已修的那条。启程阶段那条「300 毫秒内出现第一行」在全量里没赶上,单独重跑通过。其余是 Windows 上旧的双盘符路径、Skill 符号链接、部署脚本和临时目录被占。交回前会再跑一遍干净的全量。
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
| `TASK-rectification-explicit-open-20261023.md` | `PROGRESS-rectification-explicit-open-20261023.md` | **明说要校正只回一句话(issue #103)**:找会话直接读一张对服务角色撤权的表、必然失败被吞(真正进行中的也续不上);`candidate_accepted` 被算作进行中挡住入口和提议卡;兜底只给一句话。产品选 C;只改入口口径、经 RPC 找会话、删兜底,不改共享状态集合、不动表。BUG-1423 | 已合入 staging | 分支 `codex/rectification-explicit-open-20261023` |
|
||||
| `TASK-chart-table-span-20261023.md` | `PROGRESS-chart-table-span-20261023.md` | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发。BUG-1422 | 已合入 staging | 分支 `codex/chart-table-span-20261023` |
|
||||
| `TASK-ui-kit-migration-20261023.md` | `PROGRESS-ui-kit-0-base-20261023.md`;`PROGRESS-ui-kit-1-shell-20261023.md`;`PROGRESS-ui-kit-2-chat-20261023.md`;`PROGRESS-ui-kit-3-chart-20261023.md`;`PROGRESS-ui-kit-4-people-20261023.md`;`PROGRESS-ui-kit-5-pages-20261023.md`;`PROGRESS-ui-kit-6-rectification-20261023.md`;`PROGRESS-ui-kit-7-cleanup-20261023.md` | **界面统一换成组件库**:shadcn base-nova(补 `shadcn/tailwind.css`)· Lucide · Motion · react-resizable-panels · dnd-kit · Zustand;lint 拦手写控件(白名单只减不增);对话旁星盘面板;会话、档案拖动排序(动表);长相不变。产品 2026-10-23 拍板 D1~D8;8 个 issue(K0~K7)串行,取代全站动效 #98 #100 #101 | K0–K7 已在测试环境,待验收 | 原型 https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG |
|
||||
| `TASK-ui-kit-onboarding-fix-20261026.md` | `PROGRESS-ui-kit-onboarding-fix-20261026.md` | **组件库 K0~K7 与启程(二)~(四)事后验收**:桌面首屏后整页重挂、中英切换看不出选中、提示层每次 DOM 变化全页扫描、启程动效对所有人生效且会藏内容、清单数据只拉一次、拖动排序 N 个请求与部分失败、缺真库重读测试、首屏超 2%、依赖卫生、补回削弱的测试、启程(二)三处未做 | 执行中(#111,U1–U6、U9、U10、U11 与顺手项已提交) | `codex/ui-kit-onboarding-fix-20261026` |
|
||||
| `TASK-ui-kit-onboarding-fix-20261026.md` | `PROGRESS-ui-kit-onboarding-fix-20261026.md` | **组件库 K0~K7 与启程(二)~(四)事后验收**:桌面首屏后整页重挂、中英切换看不出选中、提示层每次 DOM 变化全页扫描、启程动效对所有人生效且会藏内容、清单数据只拉一次、拖动排序 N 个请求与部分失败、缺真库重读测试、首屏超 2%、依赖卫生、补回削弱的测试、启程(二)三处未做 | 执行中(#111,U1–U6、U9、U10、U11 与顺手项已提交;类型检查和 lint 已过) | `codex/ui-kit-onboarding-fix-20261026` |
|
||||
| —(门禁修复,无新任务书) | `PROGRESS-birth-journey-contract-20261025.md` | **测试分支过不了门禁,网站停在完整数据版(二)**:BUG-1439 出生时间合同;BUG-1440 测试画布;BUG-1441 侧栏流式检查等第一次补画结束再数帧。页面不改 | 已合入 staging,等门禁 | `codex/gate-birth-journey-contract-20261025` |
|
||||
| `TASK-onboarding-journey-20261022.md` | `PROGRESS-onboarding-1-data-20261022.md`;`PROGRESS-onboarding-2-intake-20261022.md`;`PROGRESS-onboarding-4-motion-20261022.md` | **新用户启程**:建档加「先看谁的盘」(自己 / 帮别人看,本人资料可后补)、人物关系字段、首问免费(服务端原子)、起步三问、启程清单(七项从数据推导,收费项标价,老用户可见)、八条一次性提示(含星盘四页签)、启程流程专用动效。产品 2026-10-22 拍板 D1~D6;动表经产品同意进循环,拆四个 issue 串行。BUG-1420 已被文档去重占用,本轮首问免费记 BUG-1421。(一)T1+T2 在 `codex/onboarding-1-data-20261022`。(二)T3+T4 记 BUG-1431,在 `codex/onboarding-2-intake-20261022`。(四)T7 在 `codex/onboarding-4-motion-20261022` | (一)(二)(三)已在 staging;(四)本分支待验收 | 原型 https://claude.ai/artifact/PcTwMVb9RD611R3qStxv3U |
|
||||
| `TASK-site-motion-20261022.md` | — | **全站动效**:换页按侧栏方向滑入、选中块跟随、页签、盘型切换星体移宫、换人、翻日期、列表补位、弹窗从触发处长出、抽屉、顶部提示、点数滚动、中英交叉淡入;重写 DESIGN §6(推翻 180 ms 上限与 Spatial Instant),不引动画库,先验证 View Transitions。产品 2026-10-22 拍板;三个 issue,排在启程(三)之后、启程(四)之前;**2026-10-23 被 ui-kit-migration 取代** | 已取代 | 原型 https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd |
|
||||
|
||||
+1
-1
@@ -1267,7 +1267,7 @@ Yogini 还在取时,右栏用和 Chara 相同的轨道等待(`OrbitWaiting`
|
||||
|
||||
## 外壳(2026-10-24)
|
||||
|
||||
桌面侧栏宽 200–360px,默认 288,拖右侧把手调整。拖到比 200 更窄就收成 64px 窄轨;收起不把 64 写成宽度。宽度记在本机 `sidebar_width`。侧栏和主区始终在各自的容器里:视口就绪、跨过手机断点,都不会把整页卸掉再挂上。手机用左侧抽屉,宽 `min(86vw, 320px)`。当前页高亮用已有的 `layoutId` 滑动。翻页用已有的页面进入 280ms、离开 150ms,方向跟着侧栏顺序。顶栏点数是描边按钮:硬币图标加数字,44px;「点」只在可读名称里。数字变少时往下滚,变多时往上滚,360ms;系统要求减少动态时立刻换数字。启程清单胶囊仍是 32px,仍在点数左边。账户按钮只显示头像和名字。设置仍是三个分区。会话只在收藏组内、或最近一组内拖动排序;收藏整组仍在最前。翻页、高亮滑动和拖动排序用的库在页面出来后再载入,不进首屏。拖宽不再另载一套分栏库。
|
||||
桌面侧栏宽 200–360px,默认 288。右侧把手是组件库按钮,仍是 6px 透明细条;拖它调整宽度。拖到比 200 更窄就收成 64px 窄轨;收起不把 64 写成宽度。宽度记在本机 `sidebar_width`。侧栏和主区始终在各自的容器里:视口就绪、跨过手机断点,都不会把整页卸掉再挂上。手机用左侧抽屉,宽 `min(86vw, 320px)`。当前页高亮用已有的 `layoutId` 滑动。翻页用已有的页面进入 280ms、离开 150ms,方向跟着侧栏顺序。顶栏点数是描边按钮:硬币图标加数字,44px;「点」只在可读名称里。数字变少时往下滚,变多时往上滚,360ms;系统要求减少动态时立刻换数字。启程清单胶囊仍是 32px,仍在点数左边。账户按钮只显示头像和名字。设置仍是三个分区。会话只在收藏组内、或最近一组内拖动排序;收藏整组仍在最前。翻页、高亮滑动和拖动排序用的库在页面出来后再载入,不进首屏。拖宽不再另载一套分栏库。
|
||||
|
||||
## 对话页(2026-10-25)
|
||||
|
||||
|
||||
@@ -829,7 +829,10 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
|
||||
.shell-inset, .shell-page { min-width: 0; min-height: 0; }
|
||||
.shell-page { height: 100%; }
|
||||
.shell-page-presence { position: relative; height: 100%; min-width: 0; min-height: 0; }
|
||||
.shell-resize-handle { position: absolute; top: 0; right: -3px; z-index: 2; width: 6px; height: 100%; padding: 0; border: 0; background: transparent; cursor: col-resize; }
|
||||
/* The handle is the shared Button. These states keep it a 6px transparent strip. */
|
||||
button.shell-resize-handle,
|
||||
button.shell-resize-handle:hover,
|
||||
button.shell-resize-handle:active { position: absolute; top: 0; right: -3px; z-index: 2; width: 6px; height: 100%; padding: 0; border: 0; border-radius: 0; background: transparent; transform: none; cursor: col-resize; }
|
||||
.sidebar-nav-current { position: absolute; inset: 0; z-index: 0; border-radius: var(--radius-md); background: var(--sidebar-accent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sidebar-ring) 22%, transparent); pointer-events: none; }
|
||||
.sidebar { position: relative; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; border-right: 1px solid var(--sidebar-border); color: var(--sidebar-foreground); padding: var(--space-5) var(--space-3) 0; background: var(--sidebar-background); backdrop-filter: saturate(130%) blur(20px); }
|
||||
[data-sidebar="header"] { flex: 0 0 auto; padding-bottom: var(--space-3); }
|
||||
|
||||
@@ -104,17 +104,19 @@ export function PersonalReportCenter() {
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const deletedIds = useRef(new Set<string>());
|
||||
const cancelled = useRef(false);
|
||||
const listedReports = useRef<readonly ReportListItem[]>(state.reports);
|
||||
// Written when a snapshot arrives, never during render (BUG-620).
|
||||
// A different person does not inherit the previous generating rows.
|
||||
const listedReports = useRef({ subjectId, reports: state.reports });
|
||||
if (seenSubject !== subjectId) {
|
||||
setSeenSubject(subjectId);
|
||||
listedReports.current = [];
|
||||
setState({ phase: "loading", ...EMPTY_LIST });
|
||||
}
|
||||
|
||||
const applySnapshot = useCallback((snapshot: ReportsPageSnapshot) => {
|
||||
const kept = (reports: readonly ReportListItem[]) => reports.filter(report => !deletedIds.current.has(report.id));
|
||||
const remembered = listedReports.current.subjectId === subjectId ? listedReports.current.reports : [];
|
||||
if (snapshot.kind === "unauthorized") {
|
||||
listedReports.current = [];
|
||||
listedReports.current = { subjectId, reports: [] };
|
||||
setState({ phase: "unauthorized", ...EMPTY_LIST });
|
||||
return;
|
||||
}
|
||||
@@ -123,13 +125,13 @@ export function PersonalReportCenter() {
|
||||
return;
|
||||
}
|
||||
const nextReports = kept(snapshot.reports);
|
||||
const generating = new Set(listedReports.current.filter((report) => report.status === "generating").map((report) => report.id));
|
||||
const generating = new Set(remembered.filter((report) => report.status === "generating").map((report) => report.id));
|
||||
const finished = nextReports.filter((report) => report.status === "ready" && generating.has(report.id));
|
||||
listedReports.current = nextReports;
|
||||
listedReports.current = { subjectId, reports: nextReports };
|
||||
if (finished.some((report) => report.reportType === "synastry_full")) notifyOnboardingFactsChanged("syn");
|
||||
if (finished.some((report) => report.reportType !== "synastry_full")) notifyOnboardingFactsChanged("report");
|
||||
setState(current => ({ phase: "ready", ...applyFirstReportPage(current, { reports: nextReports, nextCursor: snapshot.nextCursor }) }));
|
||||
}, []);
|
||||
}, [subjectId]);
|
||||
|
||||
const load = useCallback(async (showLoading = false) => {
|
||||
if (!subjectReady) return;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useRef, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import {
|
||||
SIDEBAR_WIDTH_COLLAPSED,
|
||||
@@ -64,8 +65,9 @@ export function ShellFrame({ sidebar, children }: { sidebar: ReactNode; children
|
||||
<div className="shell-sidebar-slot">
|
||||
{sidebar}
|
||||
{desktop ? (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="shell-resize-handle"
|
||||
aria-label="调整侧栏宽度"
|
||||
onPointerDown={onResizeStart}
|
||||
|
||||
@@ -54,7 +54,9 @@ const tailwindCollisions = new Set([
|
||||
// themselves. The rest are modifiers sitting beside an already-styled class
|
||||
// on the same element.
|
||||
const knownUnstyled = new Set([
|
||||
"birth-time-evidence-receipt",
|
||||
// 原值:白名单含 birth-time-evidence-receipt
|
||||
// 新值:删掉这一项
|
||||
// 原因:唯一使用者随六个旧校正组件删除,白名单不得留陈旧条目
|
||||
// 原值:is-done 在白名单里,因为当时没有样式。
|
||||
// 新值:删掉。chart-page-dasha-panel、chat-chart-resize-handle 加入。
|
||||
// 原因:启程进度条已经给 li.is-done 上色。下面这几个类名落在自己会画样式的
|
||||
|
||||
@@ -76,9 +76,9 @@ function matchesSelector(node: Node, selector: string): boolean {
|
||||
const compounds = selectorCompounds(selector);
|
||||
let cursor: Node | null = node;
|
||||
for (let index = compounds.length - 1; index >= 0; index -= 1) {
|
||||
const start = index === compounds.length - 1 ? cursor : cursor?.parentNode ?? null;
|
||||
const start: Node | null = index === compounds.length - 1 ? cursor : cursor?.parentNode ?? null;
|
||||
let found: Node | null = null;
|
||||
for (let walk = start; walk; walk = index === compounds.length - 1 ? null : walk.parentNode) {
|
||||
for (let walk: Node | null = start; walk; walk = index === compounds.length - 1 ? null : walk.parentNode) {
|
||||
if (matchesCompound(walk, compounds[index])) {
|
||||
found = walk;
|
||||
break;
|
||||
|
||||
@@ -48,7 +48,7 @@ test("login types an email, sends a code, then types that code", async () => {
|
||||
assert.deepEqual(h.errors, []);
|
||||
const email = nodes().find((node) => node.getAttribute("id") === "login-email");
|
||||
assert.ok(email);
|
||||
(email as { value: string }).value = "person@example.com";
|
||||
(email as unknown as { value: string }).value = "person@example.com";
|
||||
await h.event(email, "onChange", { target: email, currentTarget: email });
|
||||
assert.equal(typeof email.props.onChange, "function");
|
||||
const consent = nodes().find((node) => node.getAttribute("role") === "checkbox");
|
||||
|
||||
@@ -20,6 +20,7 @@ const testAppRouter = {
|
||||
push() {},
|
||||
refresh() {},
|
||||
replace() {},
|
||||
bfcacheId: "0",
|
||||
};
|
||||
const ACCOUNT_ID = "11111111-1111-4111-8111-111111111448";
|
||||
const OTHER_ID = "22222222-2222-4222-8222-222222222448";
|
||||
|
||||
@@ -70,19 +70,19 @@ test("a failed or slow Motion load leaves the element visible", async () => {
|
||||
setOnboardingMotionLoader(() => Promise.reject(new Error("motion failed")));
|
||||
const failed = harness.document.createElement("div");
|
||||
failed.setAttribute("data-onboarding-enter", "follow-up");
|
||||
failed.style.opacity = "1";
|
||||
(failed.style as { opacity?: string }).opacity = "1";
|
||||
await playOnboardingEnter(failed as unknown as HTMLElement);
|
||||
assert.notEqual(failed.style.opacity, "0");
|
||||
assert.notEqual((failed.style as { opacity?: string }).opacity, "0");
|
||||
|
||||
resetOnboardingPlays();
|
||||
setOnboardingMotionLoader(() => new Promise(() => {}));
|
||||
const slow = harness.document.createElement("div");
|
||||
slow.setAttribute("data-onboarding-enter", "archive");
|
||||
slow.style.opacity = "1";
|
||||
(slow.style as { opacity?: string }).opacity = "1";
|
||||
const started = Date.now();
|
||||
await playOnboardingEnter(slow as unknown as HTMLElement);
|
||||
assert.ok(Date.now() - started >= 250);
|
||||
assert.notEqual(slow.style.opacity, "0");
|
||||
assert.notEqual((slow.style as { opacity?: string }).opacity, "0");
|
||||
} finally {
|
||||
setOnboardingMotionLoader(null);
|
||||
await harness.close();
|
||||
|
||||
@@ -211,7 +211,9 @@ function trackDocumentListeners(target: object): DomListener[] {
|
||||
return bag;
|
||||
}
|
||||
|
||||
function trackElementListeners(proto: { addEventListener: Function; removeEventListener: Function }) {
|
||||
type DomListenerHandle = (type: string, fn: DomListener["fn"], options?: unknown) => void;
|
||||
|
||||
function trackElementListeners(proto: { addEventListener: DomListenerHandle; removeEventListener: DomListenerHandle }) {
|
||||
const bags = new WeakMap<object, DomListener[]>();
|
||||
const originalAdd = proto.addEventListener;
|
||||
const originalRemove = proto.removeEventListener;
|
||||
@@ -246,7 +248,7 @@ test("mounted drawer closes from Escape and the overlay, and restores focus", as
|
||||
const h = createClientLifecycleHarness(); let closes = 0;
|
||||
const previous = document.createElement("button"); previous.focus();
|
||||
const documentListeners = trackDocumentListeners(h.document);
|
||||
const elementListeners = trackElementListeners(Object.getPrototypeOf(h.document.body) as { addEventListener: Function; removeEventListener: Function });
|
||||
const elementListeners = trackElementListeners(Object.getPrototypeOf(h.document.body) as { addEventListener: DomListenerHandle; removeEventListener: DomListenerHandle });
|
||||
try {
|
||||
await h.render(<ReportExportDrawer editions={editions} onClose={() => { closes += 1; }} />);
|
||||
await h.event(tab(h, "整篇报告")); assert.equal(closes, 0);
|
||||
|
||||
@@ -9,13 +9,15 @@ import { SidebarProvider } from "../src/components/ui/sidebar.tsx";
|
||||
import { useUiStore } from "../src/stores/ui-store.ts";
|
||||
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
|
||||
|
||||
function watchMount(record: { mount: number; unmount: number }) {
|
||||
record.mount += 1;
|
||||
return () => {
|
||||
record.unmount += 1;
|
||||
};
|
||||
}
|
||||
|
||||
function Probe({ record }: { record: { mount: number; unmount: number } }) {
|
||||
useEffect(() => {
|
||||
record.mount += 1;
|
||||
return () => {
|
||||
record.unmount += 1;
|
||||
};
|
||||
}, [record]);
|
||||
useEffect(() => watchMount(record), [record]);
|
||||
return <span>probe</span>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user