feat(ui): 工作台精修 + 拆分(P3-3..7)

- Workbench.tsx 937→467 行,抽出 AiToolbar/WorkbenchToolbar/MobileContextBar
- P3-3 AI 工具条分组留白 + 眉题 + 主 CTA 凸显
- P3-4 专注写作模式(useFocusMode hook+vitest,隐藏侧栏加宽正文)+ 左双栏面色区分
- P3-5 右栏本章参考小标题去衬线(font-sans) + 空态引导(无标题保 h2→h3 大纲)
- P3-6 底栏信息/操作/状态三区 + TOC 当前章高亮;TOC 状态点因无每章状态数据跳过
- P3-7 展示层流式跟随滚动(不触碰 SSE 逻辑,尊重 reduced-motion)
- AppShell 加可选 hideNav(加法式,默认 false)
This commit is contained in:
Yaojia Wang
2026-07-11 08:04:56 +02:00
parent 67e30a6863
commit 5674158707
9 changed files with 787 additions and 549 deletions

View File

@@ -0,0 +1,61 @@
// @vitest-environment jsdom
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { FOCUS_MODE_KEY, useFocusMode } from "./useFocusMode";
// 本仓库 jsdom 未提供可用的 StoragesetItem/clear 缺失),装一个内存版 localStorage。
function installMemoryStorage(): Storage {
const map = new Map<string, string>();
const storage: Storage = {
get length() {
return map.size;
},
clear: () => map.clear(),
getItem: (key) => (map.has(key) ? (map.get(key) ?? null) : null),
key: (index) => Array.from(map.keys())[index] ?? null,
removeItem: (key) => map.delete(key),
setItem: (key, value) => map.set(key, String(value)),
};
Object.defineProperty(window, "localStorage", {
value: storage,
configurable: true,
});
return storage;
}
describe("useFocusMode", () => {
beforeEach(() => {
installMemoryStorage();
});
afterEach(() => {
window.localStorage.clear();
});
it("默认关闭(无存档时 focus=false", () => {
const { result } = renderHook(() => useFocusMode());
expect(result.current.focus).toBe(false);
});
it("toggle 翻转并把状态写入 localStorage", () => {
const { result } = renderHook(() => useFocusMode());
act(() => result.current.toggle());
expect(result.current.focus).toBe(true);
expect(window.localStorage.getItem(FOCUS_MODE_KEY)).toBe("1");
act(() => result.current.toggle());
expect(result.current.focus).toBe(false);
expect(window.localStorage.getItem(FOCUS_MODE_KEY)).toBe("0");
});
it("挂载时从 localStorage 恢复已开启状态", () => {
window.localStorage.setItem(FOCUS_MODE_KEY, "1");
const { result } = renderHook(() => useFocusMode());
expect(result.current.focus).toBe(true);
});
});

View File

@@ -0,0 +1,46 @@
"use client";
import { useCallback, useEffect, useState } from "react";
// 「专注写作」偏好的持久化键(对齐既有 `ww.` 前缀约定)。
export const FOCUS_MODE_KEY = "ww.workbench_focus_mode";
export interface FocusMode {
// true=专注模式:隐藏左侧图标导航 + 目录 TOC让宽给正文false=常规四栏。
focus: boolean;
toggle: () => void;
}
function persist(focus: boolean): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(FOCUS_MODE_KEY, focus ? "1" : "0");
} catch {
// 隐私模式/配额异常忽略:专注模式是纯 UI 偏好,写盘失败不影响写作功能。
}
}
// 记住「专注写作」开关P3-4SSR 首帧恒关(避免 hydration 不一致),
// 挂载后再从 localStorage 恢复toggle 即时写回。仅桌面≥xl改变栅格窄屏本就单列不受影响。
export function useFocusMode(): FocusMode {
const [focus, setFocus] = useState(false);
useEffect(() => {
if (typeof window === "undefined") return;
try {
setFocus(window.localStorage.getItem(FOCUS_MODE_KEY) === "1");
} catch {
// 读盘失败则维持默认关闭。
}
}, []);
const toggle = useCallback(() => {
setFocus((prev) => {
const next = !prev;
persist(next);
return next;
});
}, []);
return { focus, toggle };
}