Skip to content

Commit c7d2095

Browse files
committed
feat(shell): add single source of breakpoints
1 parent d01a623 commit c7d2095

9 files changed

Lines changed: 95 additions & 11 deletions

File tree

‎app/src/components/notes/NotesReadingColumn.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
* 三栏宽度分配);空态占位的中文文案与硬编码色 `#9ca3af` 逐字保留
2020
* (本批不 token 化)。
2121
* @ai-context: 等价红线——`outlineCol` 的 localStorage 键 `notes-outline` 与
22-
* `{default:180,min:140,max:260,autoFoldBelow:1100}` 由页面逐字传入,
23-
* 本组件不得另建列状态;J1-3(只翻 manualFolded 的折叠死局)为既有
24-
* 缺陷,原样保留。
22+
* `{default:180,min:140,max:260,autoFoldBelow:breakpointFor("outlineCol")}`
23+
* (批 3 T5 前该实参逐字为 1100,现由 `shell/breakpoints.ts` 单一真源
24+
* 给 1280)由页面逐字传入,本组件不得另建列状态;J1-3(只翻
25+
* manualFolded 的折叠死局)为既有缺陷,原样保留。
2526
*/
2627
import type { RefObject } from "react";
2728
import type { Note, NoteGroup } from "../../types";

‎app/src/pages/ClassroomPage.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ import { useClassroomShortcuts } from "../hooks/useClassroomShortcuts";
4747
import type { Note, ProfileKind } from "../types";
4848
// Low 清扫:标题截断长度单一定义源(与 MaterialInputPanel 共享)
4949
import { NOTE_TITLE_MAX_LEN } from "../utils/constants";
50+
// 批 3 T5:断点改走单一真源(规格 §6.2 阈值 860→1100,两列页)
51+
import { breakpointFor } from "../shell/breakpoints";
5052

5153
export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (sessionId: number) => void }) {
5254
// v0.15:左栏列状态(可拖拽 + 记忆 + 窄窗折叠;默认 320=历史值)
53-
const leftCol = useColumnLayout("classroom-left", { default: 320, min: 240, max: 420, autoFoldBelow: 860 });
55+
const leftCol = useColumnLayout("classroom-left", { default: 320, min: 240, max: 420, autoFoldBelow: breakpointFor("twoCol") });
5456
// 批 2b:采集生命周期单一状态源(挂载拉取+事件+看门狗在 provider;本页消费)
5557
const { active, sessionId, pausedReason, starting, stopping, pending, notice, start, pause, resume, stop } =
5658
useCaptureControl();

‎app/src/pages/KnowledgePage.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ function withSystemMock() {
6464
}
6565

6666
beforeEach(() => {
67+
// 批 3 T5:体系列阈值 860→1100(规格 §6.2)——本仓 jsdom 默认视口恰为 1024,
68+
// 落到阈值之下会**自动折叠左列**,system-global 便不再渲染。本文件测的是
69+
// 「未折叠」三栏形态 ⇒ 显式声明默认窗宽 1280(规格 §1 决策 17)。
70+
// 先例:SelectionActionMenu.test.tsx 同样显式声明 innerWidth。
71+
window.innerWidth = 1280;
6772
invokeMock.mockReset();
6873
invokeMock.mockImplementation(async (cmd: string) => {
6974
switch (cmd) {

‎app/src/pages/KnowledgePage.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import KnowledgeModelDialog from "../components/KnowledgeModelDialog";
3333
import ConceptCardRow from "../components/ConceptCardRow";
3434
import KnowledgeSampleView from "../components/KnowledgeSampleView";
3535
import KnowledgeGraphView from "../components/KnowledgeGraphView";
36+
// 批 3 T5:断点改走单一真源(规格 §6.2 阈值 860→1100,两列页)
37+
import { breakpointFor } from "../shell/breakpoints";
3638

3739
type MiddleView = "tree" | "canvas" | "concept" | "model" | "graph";
3840

@@ -57,7 +59,7 @@ interface Props {
5759

5860
export default function KnowledgePage({ focusSystemId, onOpenNote, onOpenGroup, createSystemSignal }: Props) {
5961
// v0.15:左列可拖拽/记忆/窄窗折叠(默认 260=历史值);详情面板宽度由父层持有
60-
const leftCol = useColumnLayout("knowledge-left", { default: 260, min: 200, max: 360, autoFoldBelow: 860 });
62+
const leftCol = useColumnLayout("knowledge-left", { default: 260, min: 200, max: 360, autoFoldBelow: breakpointFor("twoCol") });
6163
const detailCol = useColumnLayout("knowledge-detail", { default: 320, min: 260, max: 420 });
6264
const [systems, setSystems] = useState<KnowledgeSystem[]>([]);
6365
const [selectedSystemId, setSelectedSystemId] = useState<number | null>(null);

‎app/src/pages/NotesPage.tsx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import { useNotesPageEditing } from "../hooks/useNotesPageEditing";
3030
import { useNotesBatchActions } from "../hooks/useNotesBatchActions";
3131
import { useNotesListData } from "../hooks/useNotesListData";
3232
import { useNotesDeepLink } from "../hooks/useNotesDeepLink";
33+
// 批 3 T5:断点改走单一真源——组列/列表列 860 与 700 统一为三列页 1024;
34+
// 大纲列 1100 抬到规格的 1280(最晚折叠,规格 §6.2)
35+
import { breakpointFor } from "../shell/breakpoints";
3336

3437
interface Props {
3538
focusNoteId?: number | null;
@@ -86,9 +89,9 @@ export default function NotesPage({ focusNoteId, focusNoteSearch, focusGroupId,
8689
onStatus: list.setStatus,
8790
});
8891
// v0.15:三栏列状态(可拖拽 + 宽度记忆 + 窄窗自动折叠;默认值=历史固定宽度)
89-
const groupsCol = useColumnLayout("notes-groups", { default: 240, min: 180, max: 320, autoFoldBelow: 860 });
90-
const listCol = useColumnLayout("notes-list", { default: 320, min: 240, max: 420, autoFoldBelow: 700 });
91-
const outlineCol = useColumnLayout("notes-outline", { default: 180, min: 140, max: 260, autoFoldBelow: 1100 });
92+
const groupsCol = useColumnLayout("notes-groups", { default: 240, min: 180, max: 320, autoFoldBelow: breakpointFor("threeCol") });
93+
const listCol = useColumnLayout("notes-list", { default: 320, min: 240, max: 420, autoFoldBelow: breakpointFor("threeCol") });
94+
const outlineCol = useColumnLayout("notes-outline", { default: 180, min: 140, max: 260, autoFoldBelow: breakpointFor("outlineCol") });
9295

9396
// A6:注意力跟踪
9497
useNoteAttention(selected?.id ?? null, selected?.title ?? "");

‎app/src/pages/SessionsPage.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { useDbRefresh } from "../hooks/useDbRefresh";
2323
import type {
2424
BatchNoteResult, BatchSessionDeleteResult, CourseGroup, SessionDetail, SessionListItem,
2525
} from "../types";
26+
// 批 3 T5:断点改走单一真源(规格 §6.2 阈值 860→1100,两列页)
27+
import { breakpointFor } from "../shell/breakpoints";
2628

2729
interface Props {
2830
focusSessionId?: number | null;
@@ -45,7 +47,7 @@ interface Toast {
4547

4648
export default function SessionsPage({ focusSessionId, focusRefineTaskId, onFocusRefineTaskConsumed, onRefineTaskStarted, active, onOpenNote }: Props) {
4749
// v0.15:左栏列状态(可拖拽 + 记忆 + 窄窗折叠;默认值=历史固定宽度 320)
48-
const listCol = useColumnLayout("sessions-list", { default: 320, min: 240, max: 420, autoFoldBelow: 860 });
50+
const listCol = useColumnLayout("sessions-list", { default: 320, min: 240, max: 420, autoFoldBelow: breakpointFor("twoCol") });
4951
const [items, setItems] = useState<SessionListItem[]>([]);
5052
const [groups, setGroups] = useState<CourseGroup[] | null>(null); // REQ-078:课程分组模式
5153
const [grouped, setGrouped] = useState(false);

‎app/src/shell/breakpoints.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @ai-context 断点契约守卫(规格 §6.2 阈值口径)。
3+
*
4+
* Why 断言**相对序**而不是只断言三个常数:规格的推导规则是「三列页 < 两列页 < 大纲列」
5+
* (列越多越早折叠)。只钉数字的话,将来有人把 `outlineCol` 改小会静默破坏规则。
6+
*
7+
* 口径:`nav === NAV_MIN_WIDTH`(导航下限必须等于最小窗宽,否则 1024 档无意义)。
8+
*/
9+
import { describe, expect, it } from "vitest";
10+
import { BREAKPOINTS, breakpointFor } from "./breakpoints";
11+
import { NAV_MIN_WIDTH } from "./windowSize";
12+
13+
describe("断点(规格 §1 决策 16 / §6.2 阈值口径)", () => {
14+
it("三个规格数字逐字就位", () => {
15+
expect(BREAKPOINTS.nav).toBe(1024);
16+
expect(BREAKPOINTS.twoCol).toBe(1100);
17+
expect(BREAKPOINTS.navFull).toBe(1180);
18+
});
19+
20+
it("相对序:三列页最早折叠 < 两列页 < 大纲列最晚折叠", () => {
21+
expect(BREAKPOINTS.threeCol).toBeLessThan(BREAKPOINTS.twoCol);
22+
expect(BREAKPOINTS.twoCol).toBeLessThan(BREAKPOINTS.outlineCol);
23+
});
24+
25+
it("导航下限 = 最小窗宽(两处不许各写一个 1024)", () => {
26+
expect(BREAKPOINTS.nav).toBe(NAV_MIN_WIDTH);
27+
});
28+
29+
it("breakpointFor 对所有键都返回正数(防漏键导致 undefined 静默成 NaN 比较)", () => {
30+
for (const k of Object.keys(BREAKPOINTS) as (keyof typeof BREAKPOINTS)[]) {
31+
expect(breakpointFor(k), k).toBeGreaterThan(0);
32+
}
33+
});
34+
});

‎app/src/shell/breakpoints.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @ai-context 全站断点的**单一真源**(规格 §1 决策 16「断点 1024/1100/1180」+ §6.2 阈值口径)。
3+
*
4+
* Why 收成一处:今日同一个「窄窗自动折叠」概念在 6 个调用点写成了 3 个不同的数
5+
* (860 出现 4 次、700 一次、1100 一次),且**没有一处能从数字反推出理由**。
6+
* 规格 §6.2 给了推导规则:「按折叠后正文是否仍 ≥520px 反推」——
7+
* 三列页 1024 · 两列页 1100 · 大纲列 1280。本模块把规则与数字放在一起。
8+
*
9+
* 口径:这些值是 `window.innerWidth` 的**下限**(视口宽 ≥ 该值时不折叠);
10+
* 比较一律用严格小于(见 useColumnLayout: `window.innerWidth < autoFoldBelow` ⇒ 折叠)。
11+
* ⇒ 写下 1100 的含义是「1099 折叠、1100 不折叠」。
12+
*
13+
* 副作用:无(纯数据)。
14+
* 边界:本模块**不**含 1180 之外的导航档位算术;顶栏的档位判定在 T7 的 TopBar 里,
15+
* 但它必须引用本模块的 `nav` / `navFull`,不得另写数字。
16+
*/
17+
export const BREAKPOINTS = {
18+
/** 导航下限 = 最小窗宽(规格 §6.1「<1024 由最小窗兜底」) */
19+
nav: 1024,
20+
/** 导航满档:≥1180 图标+文字,1024–1180 仅图标(规格 §6.1 溢出两级) */
21+
navFull: 1180,
22+
/** 两列页的自动折叠阈值(课堂源列 / 会话列表 / 体系左列 / 目标左列 / AI 侧栏) */
23+
twoCol: 1100,
24+
/** 三列页的自动折叠阈值(笔记组列 + 列表列) */
25+
threeCol: 1024,
26+
/** 大纲列(笔记第三列):最晚折叠(规格 §6.2「大纲列 1280」) */
27+
outlineCol: 1280,
28+
} as const;
29+
30+
export type BreakpointKind = keyof typeof BREAKPOINTS;
31+
32+
/** 取阈值的唯一入口(写 `BREAKPOINTS.twoCol` 也可,但走函数便于将来加权/改写) */
33+
export function breakpointFor(kind: BreakpointKind): number {
34+
return BREAKPOINTS[kind];
35+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555
| app/src-tauri/src/commands_session.rs | 443 | v0.6.0 M6 + v0.7.6 审查硬拆后回归:会话命令域(CRUD/质量报告/课程分组/段搜索)内聚;笔记转换管线已拆至 commands_session_note.rs;M2 修复(search_ocr_blocks 传 data_dir 参数) | 若再增长:course/search 拆至 commands_session_extra.rs |
5656
| app/src-tauri/src/symbol_normalize.rs | 443 | v0.6.0 M1(REQ-060):口语符号映射域(映射表/上下文守卫/中文数字解析)内聚;数字解析与守卫共享字符判定 | 若再增长:parse_chinese_number/replace_number_runs 拆至 symbol_numbers.rs |
5757
| app/src-tauri/src/commands_goals_plan.rs | 442 | 规划=单次同步调用 + spawn_blocking(10-30s 交互等待可接受,对话框 loading 态);审计完整(ai_tasks op_type='goal_plan',trajectory/成本落库——轨迹可见性达成,任务化轮询留观察项)。(自动摘取,待细化) | 若再增长:按职责拆分 |
58+
| app/src/pages/KnowledgePage.tsx | 439 | v0.13.8 画布接线越线(原 296):中栏「画布」标签 + 树/画布双入口 + v0.15 左列/详情列状态迁移(useColumnLayout + 折叠窄条 + 宽度 prop)——页面编排层内聚(数据获取/选中态/标签态为页面本地状态),子组件已全部下沉(Tree/Canvas/DetailPanel/Wizard/ConceptCardRow/Sample) | 若再增长:中栏视图块(树/画布/概念/模型 + 标签栏)拆至 KnowledgeMiddlePane.tsx,SystemCard 拆至 SystemSidebarCard.tsx |
5859
| app/src-tauri/src/screens.rs | 437 | v0.7.3(REQ-155/156/160):画面要点屏构建编排(分组/聚类/图匹配 IO)+ 可消费块过滤扩展——编排与纯函数分层(纯函数在 screen_merge.rs) | 若再增长:filter_usable_blocks 拆至 screen_filter.rs |
59-
| app/src/pages/KnowledgePage.tsx | 437 | v0.13.8 画布接线越线(原 296):中栏「画布」标签 + 树/画布双入口 + v0.15 左列/详情列状态迁移(useColumnLayout + 折叠窄条 + 宽度 prop)——页面编排层内聚(数据获取/选中态/标签态为页面本地状态),子组件已全部下沉(Tree/Canvas/DetailPanel/Wizard/ConceptCardRow/Sample) | 若再增长:中栏视图块(树/画布/概念/模型 + 标签栏)拆至 KnowledgeMiddlePane.tsx,SystemCard 拆至 SystemSidebarCard.tsx |
6060
| app/src-tauri/src/commands_knowledge_tests.rs | 435 | v0.13.1(REQ-202~205):知识体系命令层单测域(校验纯函数 + inner 编排 + 四类 target/审计信号聚合)单模块 #[path] 挂载 | 若再增长:引用与审计组拆至 commands_knowledge_links_tests.rs |
6161
| app/src-tauri/src/screens_tests.rs | 433 | 画面要点屏构建测试域(分组/聚类/图匹配/可消费块过滤回归)单模块 #[path] 挂载 | 若再增长:可消费块过滤组拆至 screens_filter_tests.rs |
6262
| app/src-tauri/src/note_filter_golden_tests.rs | 431 | v0.7.5(REQ-172):黄金语料回归域(会话31/29 实证 + 结构渲染 2 例 + 审查补测)单模块 #[path] 挂载 | 若再增长:结构渲染组拆至 note_filter_golden_structure_tests.rs |
@@ -91,10 +91,10 @@
9191
| app/src-tauri/src/ocr_cache.rs | 358 | OCR 结果缓存域(内容指纹键/容量淘汰/命中率统计)内聚;缓存策略与指纹算法共享上下文 | 若再增长:指纹算法拆至 ocr_fingerprint.rs |
9292
| app/src-tauri/src/commands_window.rs | 357 | 浮窗窗口命令域(v0.12.3 架构升级计划:全部窗口操作集中单文件)+ v0.12.6(ADR-025)显隐链路与全局快捷键三态切换核心(open/close/locked/topmost/toggle 核心 fn + 命令薄包装 + 状态机/序列化单测内联);拆分需跨 fn 传递 AppHandle/State,内聚性优先 | 若再增长:浮窗核心逻辑拆至 float_core.rs(命令薄包装保留本文件) |
9393
| app/src-tauri/src/db_sessions_tests.rs | 354 | 由 db_sessions.rs 以 #[cfg(test)] #[path] 引入,保持实现文件 ≤300 行(AGENTS.md §3 模块化)。(自动摘取,待细化) | 若再增长:按职责拆分 |
94+
| app/src/pages/SessionsPage.tsx | 354 | 本层为状态宿主与数据编排:会话列表/详情状态、事件驱动刷新(live:status/session:fused/切页 active)、转化与删除操作;左栏列表 UI 拆至 SessionListPanel、右栏详情拆至 SessionDetailPanel(豁免清单拆分计划落地,本文件 ≤300 行)。(自动摘取,待细化) | 若再增长:按职责拆分 |
9495
| app/src-tauri/src/live_session_pause.rs | 353 | 批 2 暂停边沿收敛域 + 审查修复轮 1(own_pending 吸收/丢采样恢复沿补发)净增——2026-09-09 纠偏实测 353(交付口径 258 失真),300-600 档登记 | 若再增长:合成事件对构造拆至 live_session_pause_synth.rs |
9596
| app/src-tauri/src/ai_client_tests.rs | 352 | ai_client.rs 单测域(22 例:payload 构造族 build_chat_payload/build_plain_payload、JSON 前置条件提示、chat_completions_url 拼接、响应提取与弱化解析、fallback_provider_ids;env 变量以静态 Mutex 串行化,不触网)——测试模块由 `#[cfg(test)] #[path]` 单点挂载(ai_client.rs:504),纯函数用例与实现同域便于契约对齐。**本条目由生成器补登(该文件无 @ai-context 头注释),理由为 2026-09-11 重建时人工补写** | 若再增长:payload 构造组拆至 ai_client_payload_tests.rs |
9697
| app/src/components/AiProviderSettings.tsx | 352 | v0.11.6 M1 code-review 修复(2026-08-22):删除/清钥加 window.confirm、window.prompt 改卡片内联 password 输入(+2 state + 内联表单)、模型列表 input 改 textarea、fallbackOrder 透传 initial、run 置"处理中"反馈、预设双源 presetOptions 后端拉取——修复净增约 13 行越线(实测 329,含 4 行豁免头注释) | 若再增长:内联密钥表单拆至 AiProviderKeyInput.tsx |
97-
| app/src/pages/SessionsPage.tsx | 352 | 本层为状态宿主与数据编排:会话列表/详情状态、事件驱动刷新(live:status/session:fused/切页 active)、转化与删除操作;左栏列表 UI 拆至 SessionListPanel、右栏详情拆至 SessionDetailPanel(豁免清单拆分计划落地,本文件 ≤300 行)。(自动摘取,待细化) | 若再增长:按职责拆分 |
9898
| app/src/components/NoteLinkToSystem.tsx | 351 | v0.19.7(REQ-286)重构:挂体系选择器(体系下拉 + 三 tab + LinkEntityPicker 搜索树列表 + 三类内联轻建编排 + 既有反查/撤链/钳制语义保持)——实体选择/创建交互已下沉 LinkEntityPicker.tsx(134 行),本文件保留编排与数据装载内聚 | 若再增长:树行构建(flattenNodeRows)与实体装载拆至 useSystemEntities.ts hook |
9999
| app/src-tauri/src/structure_models.rs | 343 | v0.5.0 模型版:模型清单/独立状态机下载器(进度事件/.part 原子写/按需启用三分类)+ 磁盘就绪判定(disk_done)内聚 | 若再增长:download_one 拆至 structure_download.rs |
100100
| app/src-tauri/src/db_ai_tasks.rs | 340 | 任务注册表在 AppState 内存(HashMap)——重启即失、未采纳结果无法恢复。本模块把任务记录落 SQLite(ai_tasks 表):状态/结果 JSON/成本/耗时/模型/错误全量可查;应用启动时恢复未采纳的成功结果到注册表(重启不丢,任务面板可见历史);采纳落库时标记 adopted(防重启后重复采纳产生重复笔记)。(自动摘取,待细化) | 若再增长:按职责拆分 |

0 commit comments

Comments
 (0)