Skip to content

Commit 0916d12

Browse files
committed
feat(shell): 顶栏新增审校模式入口按钮
复用右簇动作按钮族(TopBarAction)⇒ 零原生 <button> 新增、零新样式常量、零 ui/primitives import。 App.tsx 透传同一份模式位(单一写入方)⇒ 按钮 / 快捷键 / Esc 三条出口合流(控制方裁定 G-A)。 TopBar.test.tsx:类序期望数组扩 1 项(10 → 11,判据变强)+ 新增入口三出口 ①–④ 判据。 豁免表 App.tsx 登记行 561 → 563(§C14:改行数的单元同提交同步登记行)。
1 parent b077d53 commit 0916d12

4 files changed

Lines changed: 74 additions & 3 deletions

File tree

‎app/src/App.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ function MainShell() {
357357
onOpenSettings={() => setPage("settings")}
358358
// 批 3 T11:⌘K 按钮与 Ctrl+K 快捷键都开同一个面板(T7 时这里还是空实现)
359359
onOpenPalette={() => setPaletteOpen(true)}
360+
proofread={proofread}
361+
onToggleProofread={toggleProofread}
360362
right={
361363
<>
362364
<TopBarAction

‎app/src/shell/TopBar.test.tsx‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
*/
2828
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2929
import { afterEach, describe, expect, it, vi } from "vitest";
30+
import { useEffect } from "react";
3031
import { TopBar } from "./TopBar";
32+
import { PROOFREAD_MODE_ATTR, useProofreadMode } from "./proofreadMode";
3133
import { NAV_ENTRIES } from "./navRegistry";
3234
import { BREAKPOINTS } from "./breakpoints";
3335
import {
@@ -142,7 +144,7 @@ describe("TopBar(规格 §6.1)", () => {
142144
expect(
143145
classes,
144146
"顶栏按钮的类序变了:带 `ed-btn` ⇒ 掉出 `button:not(.ed-btn)` 的元素级回执(也就掉出 reduced-motion 名单)",
145-
).toEqual([...NAV_ENTRIES.map(() => "ed-topbar__tab"), "ed-topbar__action", "ed-topbar__action"]);
147+
).toEqual([...NAV_ENTRIES.map(() => "ed-topbar__tab"), "ed-topbar__action", "ed-topbar__action", "ed-topbar__action"]);
146148
// ③a-2 落点覆盖面(今天 0 处声明 ⇒ 条件式;变异体证明有牙:把落点写到 `.ed-topbar__brand`(`<span>`)⇒ 红)
147149
const buttons = new Set(classes.flatMap((c) => c.split(/\s+/)));
148150
const rules = [...CSS.matchAll(/([^{}]+)\{([^{}]*)\}/g)].filter((m) => motionDecls(m[2]).length > 0);
@@ -212,6 +214,55 @@ describe("TopBar(规格 §6.1)", () => {
212214
});
213215
});
214216

217+
/**
218+
* 批 8 T11 · 审校模式**入口按钮**(规格 §4.3① 可发现的入口 + ③ 三条等价出口)。通道层判据在
219+
* `proofreadMode.test.ts` / `.dom.test.tsx`;本节判**装配层**:宿主 `Harness` = `App.tsx:330-339`
220+
* 的**最小镜像**(单一 `useProofreadMode()` + 两条键盘出口)⇒ 三条出口必须**合流**。
221+
*/
222+
describe("审校模式入口(批 8 T11;规格 §4.3①③)", () => {
223+
const mode = () => document.documentElement.getAttribute(PROOFREAD_MODE_ATTR);
224+
afterEach(() => document.documentElement.removeAttribute(PROOFREAD_MODE_ATTR));
225+
function Harness() {
226+
const [proofread, toggleProofread] = useProofreadMode();
227+
useEffect(() => {
228+
const onKey = (e: KeyboardEvent) => {
229+
const hit = e.ctrlKey && e.shiftKey && (e.key === "R" || e.key === "r");
230+
if (hit) { e.preventDefault(); toggleProofread(); }
231+
else if (e.key === "Escape" && proofread === "on") toggleProofread();
232+
};
233+
window.addEventListener("keydown", onKey);
234+
return () => window.removeEventListener("keydown", onKey);
235+
}, [proofread, toggleProofread]);
236+
return <TopBar {...base} proofread={proofread} onToggleProofread={toggleProofread} />;
237+
}
238+
const entry = () => screen.getByTestId("topbar-proofread");
239+
240+
it("① 入口常驻在右簇动作按钮族里、有可读文案与快捷键提示(条件 ① 的字面要求)", () => {
241+
render(<Harness />);
242+
expect([entry().className.includes("ed-topbar__action"), entry().textContent?.trim(), entry().getAttribute("title") ?? ""],
243+
"入口不在动作按钮族 / 缺可读文案 / 缺快捷键提示").toEqual([true, "审校", "审校模式(Ctrl/⌘+Shift+R)"]);
244+
});
245+
246+
it("②③ 点一次 ⇒ `<html>` 置 `on`;再点 ⇒ **摘属性**(不是写成 \"off\")", () => {
247+
render(<Harness />);
248+
expect(document.documentElement.hasAttribute(PROOFREAD_MODE_ATTR), "起点必须干净").toBe(false);
249+
fireEvent.click(entry());
250+
expect(mode(), "点击后没置属性").toBe("on");
251+
fireEvent.click(entry());
252+
expect(document.documentElement.hasAttribute(PROOFREAD_MODE_ATTR), "再点必须能退出(缺省不落属性)").toBe(false);
253+
});
254+
255+
it("④ 三出口合流:按钮打开的模式,`Esc` 与 `Ctrl+Shift+R` 都必须能关掉", () => {
256+
render(<Harness />);
257+
fireEvent.click(entry());
258+
fireEvent.keyDown(window, { key: "Escape" });
259+
expect(mode(), "Esc 对按钮打开的模式无效 ⇒ 按钮与快捷键各持一份 state").toBeNull();
260+
fireEvent.click(entry());
261+
fireEvent.keyDown(window, { key: "R", ctrlKey: true, shiftKey: true });
262+
expect(mode(), "快捷键关不掉按钮打开的模式").toBeNull();
263+
});
264+
});
265+
215266
describe("裁决 A3:AI toast 不在导航行里", () => {
216267
it("toast 的定位/锚点/层级都在原语层(B13/B15 授权的机械改写)", () => {
217268
// 原四条断言(`:182`–`:185`)断的是「`App.tsx` 源码里出现过自足 toast 的特征串」——批 3

‎app/src/shell/TopBar.tsx‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import type { ReactNode } from "react";
2727
import { Icon, type IconName } from "../ui/icons";
2828
import { NAV_ENTRIES, navEntry, type PageKey } from "./navRegistry";
29+
import type { ProofreadMode } from "./proofreadMode";
2930
import "./TopBar.css";
3031

3132
export interface TopBarProps {
@@ -35,6 +36,13 @@ export interface TopBarProps {
3536
onOpenPalette: () => void;
3637
/** 右侧状态区(采集徽标 / 对话面板入口)—— 本组件不读这些状态,由 App 注入 */
3738
right?: ReactNode;
39+
/**
40+
* 审校模式位(规格 §4.3③;批 8 T11)—— **由 App 注入**(单一写入方纪律:属性是全局唯一载体,
41+
* 本组件**不**自己去写 `<html>`,否则按钮与快捷键各持一份 state 必然互擦)。
42+
*/
43+
proofread?: ProofreadMode;
44+
/** 切换审校模式(与快捷键 / `Esc` **同一个**切换器,由 App 透传) */
45+
onToggleProofread?: () => void;
3846
}
3947

4048
export interface TopBarActionProps {
@@ -73,7 +81,7 @@ export function TopBarAction({ testId, icon, label, title, onClick, current, pre
7381
);
7482
}
7583

76-
export function TopBar({ page, onSelect, onOpenSettings, onOpenPalette, right }: TopBarProps) {
84+
export function TopBar({ page, onSelect, onOpenSettings, onOpenPalette, right, proofread, onToggleProofread }: TopBarProps) {
7785
const settings = navEntry("settings");
7886
return (
7987
<nav className="ed-topbar" data-testid="topbar" aria-label="主导航">
@@ -103,6 +111,16 @@ export function TopBar({ page, onSelect, onOpenSettings, onOpenPalette, right }:
103111
onClick={onOpenPalette}
104112
/>
105113
{right}
114+
{/* 审校模式入口(批 8 T11;规格 §4.3①「一个可发现的入口按钮」)—— 复用右簇动作按钮族,
115+
因此**零原生 `<button>` 新增**、零新样式常量;`pressed` 走 `aria-pressed`(与 dock-toggle 同族)。 */}
116+
<TopBarAction
117+
testId="topbar-proofread"
118+
icon="check"
119+
label="审校"
120+
title="审校模式(Ctrl/⌘+Shift+R)"
121+
pressed={proofread === "on"}
122+
onClick={() => onToggleProofread?.()}
123+
/>
106124
<TopBarAction
107125
testId="topbar-settings"
108126
icon={settings.icon}

‎docs/standards/line-limit-exemptions.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
| 文件 | 行数 | 豁免理由 | 拆分计划 |
2222
|---|---|---|---|
23-
| app/src/App.tsx | 561 | 根装配(页面切换/焦点跨页直达状态机/全局事件监听)+ v0.20.5 行动页 + v0.20.10 批 5 复习页 Tab/深链/保活挂载(f702d876)——登记值 363 过期,实测纠偏 | 若再增长:焦点跨页直达 state 族拆至 useFocusRouting.ts |
23+
| app/src/App.tsx | 563 | 根装配(页面切换/焦点跨页直达状态机/全局事件监听)+ v0.20.5 行动页 + v0.20.10 批 5 复习页 Tab/深链/保活挂载(f702d876)——登记值 363 过期,实测纠偏 | 若再增长:焦点跨页直达 state 族拆至 useFocusRouting.ts |
2424
| app/src/pages/ChatPage.tsx | 579 | AI 对话页编排;批 1(REQ-306/307)active 门控/终态订阅接线净增——2026-09-09 实测纠偏(登记值 529 过期) | 若再增长:任务工具条与发起流拆至 ChatTasksToolbar.tsx |
2525
| app/src-tauri/src/db_migrations.rs | 573 | v0.20.11 批 6(REQ-315)再增:note_groups.pin ensure_column + note_group_orders 建表(+21,实测 573——登记值 492 过期;schema 单点收敛理由同左) | 若再增长:kb_* 与 chat_* 表 DDL 拆至 db_migrations_kb.rs |
2626
| app/src-tauri/src/lib.rs | 572 | crate 根 **322 `mod` + 15 `#[cfg]` = 337 行地板**(Task 1 评审实测更正:原写「321 `mod` + 16 `#[cfg]`」,总数 337 不变;322 含本任务新增的 `mod app_commands;`);注册清单已移至 `app_commands.rs`(**数据文件**,同属 300–600 豁免带)—— 结构性下界,非欠账 | 已完成(批 0-C3 Task 1,2026-09-11);余下 161 行模块理由注释 + 装配逻辑,无进一步拆分标的 |

0 commit comments

Comments
 (0)