Skip to content

Commit f673bef

Browse files
committed
feat(shell): 新增审校模式的模式位通道
1 parent 0191360 commit f673bef

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

‎app/src/shell/proofreadMode.ts‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* proofreadMode.ts — 阅读面**审校模式**的模式位通道(规格 §4.3 条件③;批 8 T9)。
3+
*
4+
* @ai-context 🔴 **消歧(控制方 §2 B2 逐字)**:本模式 = **阅读面审校模式**(规格 §4.3 条件③:
5+
* `⌘/Ctrl+Shift+R` 开、`Esc` 关,退出后墨度回常态);**与既有的 LLM 文本校对
6+
* (`proofread_estimate` / `proofread_run` / `ProofreadPanel`)无关**。`proofread` 一名已被后者占用
7+
* (2026-09-13 实测,域 = `git ls-files` 1616 文件:全仓 305 处 / 51 文件;`app/src` 75 处 / 19 文件)
8+
* ⇒ 本功能的属性名取 **`data-proofread-mode`**,**不得**用 `data-proofread`。
9+
* @ai-context 载体与范式:照 `shell/shellPhase.ts`(`data-shell-phase` 的同宿主形态,R3.1/R4.4)——
10+
* 属性写在 `<html>` 上,CSS 侧一条 `html[data-proofread-mode="on"] …` 就能重绑墨度 token,零 JS 参与。
11+
* @ai-context 三处降级**一律静默**(同 `applyShellPhase` 口径):① 无 `document`(node 测试环境)
12+
* ⇒ 不写 DOM、不抛;② 显式传 `null` 且无 `document` ⇒ 不写;③ 非法值 ⇒ **不动**既有属性
13+
* —— **不**回退成 `"off"`(静默不动比替调用方猜一个值安全)。
14+
* @ai-context 🔴 **「缺省不落属性」(规格 §4.3 条件④ 逐字)**:`off` 走 **`removeAttribute`** ——
15+
* 若写成 `setAttribute(…, "off")`,「缺省态」与「显式关」在 DOM 上不可分,且首屏白白多一个属性。
16+
* 本条的牙在 `proofreadMode.test.ts`「退出后必须不落属性」与 jsdom 面的卸载用例。
17+
* @ai-context 边界:本件**只建通道**,不含任何 CSS、不含任何 UI —— 覆盖规则在 `ui/proofread.css`
18+
* (由 `main.tsx` 在 token CSS **之后** import),入口按钮归 T11。
19+
*/
20+
import { useCallback, useEffect, useState } from "react";
21+
22+
/** 属性名逐字(控制方 §2 B2);CSS 侧消费形态 = `html[data-proofread-mode="on"] …` */
23+
export const PROOFREAD_MODE_ATTR = "data-proofread-mode";
24+
25+
/** 两态:`on` = 审校模式开(阅读面墨度加深);`off` = 常态(**不落属性**)。 */
26+
export type ProofreadMode = "on" | "off";
27+
28+
/** 取值守卫:只有两个合法字面量能过(外部输入 / 旧版遗留值一律不放行)。 */
29+
const isMode = (value: string): value is ProofreadMode => value === "on" || value === "off";
30+
31+
/** 宿主:显式元素优先,否则 `<html>`;无 `document`(node 环境)⇒ `null`。 */
32+
const hostOf = (el?: HTMLElement | null): HTMLElement | null =>
33+
el ?? (typeof document === "undefined" ? null : document.documentElement);
34+
35+
/** 写模式位(默认目标 = `<html>`):`on` ⇒ 置属性;`off` ⇒ **摘属性**(缺省不落属性);无宿主 / 非法值 ⇒ 静默不动。 */
36+
export function applyProofreadMode(mode: ProofreadMode, el?: HTMLElement | null): void {
37+
if (!isMode(mode)) return;
38+
const target = hostOf(el);
39+
if (target === null) return;
40+
if (mode === "on") target.setAttribute(PROOFREAD_MODE_ATTR, "on");
41+
else target.removeAttribute(PROOFREAD_MODE_ATTR);
42+
}
43+
44+
/** 读模式位:无宿主 / 属性缺失 / 值不是 `"on"` ⇒ `null`(调用方按「未设置」= 常态处理)。 */
45+
export function readProofreadMode(el?: HTMLElement | null): ProofreadMode | null {
46+
const raw = hostOf(el)?.getAttribute(PROOFREAD_MODE_ATTR) ?? null;
47+
return raw !== null && isMode(raw) ? raw : null;
48+
}
49+
50+
/**
51+
* 模式位 hook:挂载即应用、`mode` 变更跟手,**卸载清回 `off`**(页面保活挂载下防模式残留)。
52+
* 返回 `[mode, toggle]`:`toggle` 恒稳定 —— 快捷键(`App.tsx`)与 T11 的入口按钮必须是**同一个切换器**,
53+
* 否则两处各持一份状态就会互擦。
54+
*/
55+
export function useProofreadMode(): readonly [ProofreadMode, () => void] {
56+
const [mode, setMode] = useState<ProofreadMode>("off");
57+
useEffect(() => {
58+
applyProofreadMode(mode);
59+
return () => applyProofreadMode("off");
60+
}, [mode]);
61+
const toggle = useCallback(() => setMode((m) => (m === "on" ? "off" : "on")), []);
62+
return [mode, toggle] as const;
63+
}

0 commit comments

Comments
 (0)