525 lines
33 KiB
HTML
525 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
<!--
|
||
lang="zh-CN" + dir="ltr"(Round 12 P2 audit):CJK 默认 ltr,但 a11y lint
|
||
通常要求显式 dir。Electron 桌面应用虽不像移动 web 那样卡 viewport 缩放,
|
||
但 viewport meta 仍然是为未来 web 模式合规铺路(P1 audit)。
|
||
-->
|
||
<html lang="zh-CN" dir="ltr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<!-- viewport meta (Round 12 P1 audit):桌面 Electron 也有 webview / web mode 兼容场景 -->
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<!--
|
||
Content Security Policy:
|
||
- default-src 'self' 只允许加载同源资源
|
||
- style-src 'self' 'unsafe-inline' 允许 inline style(marked 渲染的代码块可能会用到)
|
||
- script-src 'self' 'unsafe-inline' 允许同源 + 内联脚本(用于 importmap)
|
||
- img-src 'self' data: file: 允许 markdown 内嵌图片(本地 + base64)
|
||
|
||
关于 'unsafe-inline':本应用唯一的内联脚本是 importmap(用于映射 CodeMirror 包)。
|
||
渲染端的实际代码全部走 .js 文件加载。Markdown 内容经 DOMPurify 清洗,<script> 会被剥离。
|
||
在 contextIsolation + nodeIntegration:false 下,加 'unsafe-inline' 不引入额外 XSS 风险。
|
||
-->
|
||
<!--
|
||
Content Security Policy (加固版):
|
||
- default-src 'self' 只允许加载同源资源
|
||
- style-src 'self' 'unsafe-inline' 允许 inline style(marked 渲染的代码块可能会用到)
|
||
- script-src 'self' 'unsafe-inline' 允许同源 + 内联脚本(用于 importmap)
|
||
- img-src 'self' data: file: 允许 markdown 内嵌图片(本地 + base64)
|
||
- object-src 'none' 禁止 <object>/<embed>/<applet>(插件 XSS 通道)
|
||
- base-uri 'none' 禁止 <base> 标签劫持相对 URL
|
||
- form-action 'none' 禁止 form 提交外发(即使有 <form> 也是死链)
|
||
|
||
关于 'unsafe-inline':本应用唯一的内联脚本是 importmap(用于映射 CodeMirror 包)。
|
||
渲染端的实际代码全部走 .js 文件加载。Markdown 内容经 DOMPurify 清洗,<script> 会被剥离。
|
||
在 contextIsolation + nodeIntegration:false 下,加 'unsafe-inline' 不引入额外 XSS 风险。
|
||
未来若改用 nonce 或 hash,可去除 'unsafe-inline'。
|
||
-->
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data: file:; object-src 'none'; base-uri 'none'; form-action 'none';">
|
||
<title>Notes</title>
|
||
|
||
<!--
|
||
Import map: 把 CodeMirror 6 及其传递依赖映射到本地 node_modules。
|
||
浏览器原生 ESM 不解析 package.json 的 exports 字段,所以必须写完整相对路径。
|
||
style-mod 的 ESM 在 src/ 而不是 dist/(dist/ 只有 CJS)—— 这是常见坑。
|
||
-->
|
||
<script type="importmap">
|
||
{
|
||
"imports": {
|
||
"codemirror": "./node_modules/codemirror/dist/index.js",
|
||
"@codemirror/state": "./node_modules/@codemirror/state/dist/index.js",
|
||
"@codemirror/view": "./node_modules/@codemirror/view/dist/index.js",
|
||
"@codemirror/commands": "./node_modules/@codemirror/commands/dist/index.js",
|
||
"@codemirror/language": "./node_modules/@codemirror/language/dist/index.js",
|
||
"@codemirror/lang-markdown": "./node_modules/@codemirror/lang-markdown/dist/index.js",
|
||
"@codemirror/lang-css": "./node_modules/@codemirror/lang-css/dist/index.js",
|
||
"@codemirror/lang-html": "./node_modules/@codemirror/lang-html/dist/index.js",
|
||
"@codemirror/lang-javascript": "./node_modules/@codemirror/lang-javascript/dist/index.js",
|
||
"@codemirror/search": "./node_modules/@codemirror/search/dist/index.js",
|
||
"@codemirror/autocomplete": "./node_modules/@codemirror/autocomplete/dist/index.js",
|
||
"@codemirror/lint": "./node_modules/@codemirror/lint/dist/index.js",
|
||
"@codemirror/theme-one-dark": "./node_modules/@codemirror/theme-one-dark/dist/index.js",
|
||
"@lezer/common": "./node_modules/@lezer/common/dist/index.js",
|
||
"@lezer/highlight": "./node_modules/@lezer/highlight/dist/index.js",
|
||
"@lezer/markdown": "./node_modules/@lezer/markdown/dist/index.js",
|
||
"@lezer/css": "./node_modules/@lezer/css/dist/index.js",
|
||
"@lezer/html": "./node_modules/@lezer/html/dist/index.js",
|
||
"@lezer/javascript": "./node_modules/@lezer/javascript/dist/index.js",
|
||
"@lezer/lr": "./node_modules/@lezer/lr/dist/index.js",
|
||
"@marijn/find-cluster-break": "./node_modules/@marijn/find-cluster-break/src/index.js",
|
||
"style-mod": "./node_modules/style-mod/src/style-mod.js",
|
||
"w3c-keyname": "./node_modules/w3c-keyname/index.js",
|
||
"crelt": "./node_modules/crelt/index.js"
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<link rel="stylesheet" href="src/styles.css">
|
||
</head>
|
||
<body data-theme="dark" data-theme-palette="default" data-focus-mode="false">
|
||
|
||
<!-- 聚焦模式顶边热区:触发时滑出工具栏 -->
|
||
<div class="focus-hotzone" aria-hidden="true"></div>
|
||
|
||
<!--
|
||
Skip link (Phase M a11y):
|
||
键盘用户 Tab 时第一站直达主内容,跳过工具栏 9 个按钮 + 侧栏若干入口。
|
||
默认视觉隐藏(不占布局),focus 时滑出右上角。target 是 #viewer-main,
|
||
- 标记 <main id="viewer-main" tabindex="-1">:让非交互元素也能 focus,
|
||
以承接 hash 跳转 + 屏幕阅读器宣告「进入主区域」。
|
||
-->
|
||
<a href="#viewer-main" class="skip-link">跳到主内容</a>
|
||
|
||
<!-- 启动错误兜底(window.api 不可用 / bootstrap 抛错时显示) -->
|
||
<!-- a11y #2(critical):role="alert" + aria-live="assertive" 让屏幕阅读器在出现
|
||
bootstrap 失败的瞬间立即播报,不会漏掉关键故障。hidden 时虽然 [hidden] 会
|
||
display:none,但 role/aria-live 仍声明「一旦显示就立即播报」。 -->
|
||
<div id="boot-error" hidden role="alert" aria-live="assertive">
|
||
<h2>⚠ 启动失败</h2>
|
||
<p>无法连接到主进程。请重启应用。</p>
|
||
<p class="boot-error-detail"></p>
|
||
</div>
|
||
|
||
<!-- 自定义标题栏:可拖拽 + 窗口控件 -->
|
||
<header class="toolbar">
|
||
<div class="toolbar-left drag-region">
|
||
<!-- 应用图标:由 preload 读 icon.ico 转 PNG data URL,渲染端注入 src。
|
||
hidden 直到 src load 成功,避免破图占位闪烁。src 失败 → 走 .app-title::before 占位 -->
|
||
<img id="app-icon" class="app-icon" alt="" draggable="false" hidden>
|
||
<!--
|
||
长文件名截断(fix:长文件名导致 toolbar 偏移):
|
||
- flex 容器里的直接文本节点 min-content 是整段文本,无法自然收缩;
|
||
必须把文本包到 .app-title-text 里,给它 min-width:0 + ellipsis 才能截断
|
||
- 渲染端(updateWindowTitle)写 textContent 到 #app-title-text;
|
||
没有这个 span 时退回 #app-title textContent(向后兼容)
|
||
-->
|
||
<span id="app-title" class="app-title"><span id="app-title-text" class="app-title-text">Notes</span></span>
|
||
</div>
|
||
<div class="toolbar-right no-drag">
|
||
<!-- 应用组:主题 + 置顶 -->
|
||
<div class="toolbar-group">
|
||
<div class="btn-group" role="group" aria-label="外观">
|
||
<button class="btn-icon btn-group-item" id="btn-theme" title="切换主题 (当前: 暗色主题)" aria-label="切换主题">
|
||
<svg class="theme-icon theme-icon-sun" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<circle cx="12" cy="12" r="5"></circle>
|
||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||
</svg>
|
||
<svg class="theme-icon theme-icon-moon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||
</svg>
|
||
</button>
|
||
<button class="btn-icon btn-group-item" id="btn-pin" title="始终置顶" aria-label="始终置顶">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<line x1="12" y1="17" x2="12" y2="22"></line>
|
||
<path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h-6v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V17z"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<span class="toolbar-divider" aria-hidden="true"></span>
|
||
|
||
<!-- 视图组:预览 / 编辑 / 双栏(图标 + 文字按钮,比纯图标更易识别) -->
|
||
<div class="toolbar-group">
|
||
<div class="btn-group btn-group-text" role="group" aria-label="视图模式">
|
||
<button class="btn-icon-text btn-group-item" id="btn-mode-preview" title="预览模式 (Ctrl+E 切换)" aria-label="预览模式" aria-pressed="true">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||
<circle cx="12" cy="12" r="3"></circle>
|
||
</svg>
|
||
<span class="btn-label">预览</span>
|
||
</button>
|
||
<button class="btn-icon-text btn-group-item" id="btn-mode-edit" title="编辑模式 (Ctrl+E 切换)" aria-label="编辑模式" aria-pressed="false">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M12 20h9"></path>
|
||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||
</svg>
|
||
<span class="btn-label">编辑</span>
|
||
</button>
|
||
<button class="btn-icon-text btn-group-item" id="btn-mode-split" title="双栏模式 (Ctrl+E 切换)" aria-label="双栏模式" aria-pressed="false">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
||
<line x1="12" y1="3" x2="12" y2="21"></line>
|
||
</svg>
|
||
<span class="btn-label">双栏</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<span class="toolbar-divider" aria-hidden="true"></span>
|
||
|
||
<!-- 布局组:恢复默认侧栏宽度 + 双栏比例 -->
|
||
<div class="toolbar-group">
|
||
<button class="btn-icon" id="btn-reset-layout" title="重置布局(恢复默认侧栏宽度 + 双栏比例)" aria-label="重置布局">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M3 12a9 9 0 1 0 3-6.7"></path>
|
||
<polyline points="3 4 3 9 8 9"></polyline>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<span class="toolbar-divider" aria-hidden="true"></span>
|
||
|
||
<!-- 行为组:自动保存开关(贴近编辑器,作用在编辑器 → 放顶部工具栏;见 [[feedback-prefer-near-affected-area]])。
|
||
iOS pill switch 替代原先的「按钮 + status dot」,让开关语义更明显。
|
||
role="switch" 让屏幕阅读器读「开关 开/关」而非「按钮 按/未按」。
|
||
初始 aria-pressed="true" 与 settings-schema DEFAULT_SETTINGS.autoSaveDebounceMs=500 一致,
|
||
避免首帧 FOUC 闪一下「关 → 开」(audit D1)。 -->
|
||
<div class="toolbar-group">
|
||
<button class="btn-icon-text toolbar-toggle" id="toolbar-autosave" type="button"
|
||
title="自动保存:开启(停打后 500ms 保存)· 点击关闭"
|
||
aria-label="自动保存"
|
||
aria-pressed="true"
|
||
role="switch">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<circle cx="12" cy="12" r="9"></circle>
|
||
<polyline points="12 7 12 12 15 14"></polyline>
|
||
</svg>
|
||
<span class="btn-label">自动保存</span>
|
||
<span class="toggle-switch" aria-hidden="true">
|
||
<span class="toggle-switch-track"></span>
|
||
<span class="toggle-switch-thumb"></span>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<span class="toolbar-divider" aria-hidden="true"></span>
|
||
|
||
<!-- 操作组:保存 + 设置 -->
|
||
<div class="toolbar-group">
|
||
<!--
|
||
保存按钮始终显示在工具栏;需要保存(脏)时进入 is-dirty 提醒态(warning 色 + 脉动)。
|
||
见 [[feedback-save-button-always-visible]]。
|
||
-->
|
||
<button class="btn-icon" id="btn-save" title="保存 (Ctrl+S)" aria-label="保存">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
||
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
||
<polyline points="7 3 7 8 15 8"></polyline>
|
||
</svg>
|
||
</button>
|
||
<button class="btn-icon" id="btn-settings" title="设置 (Ctrl+,)" aria-label="设置">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="3"></circle>
|
||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<span class="toolbar-divider" aria-hidden="true"></span>
|
||
|
||
<!-- 窗口组:最小化 / 最大化 / 关闭 -->
|
||
<div class="toolbar-group">
|
||
<div class="window-controls">
|
||
<button class="btn-window" id="btn-min" title="最小化" aria-label="最小化">
|
||
<svg aria-hidden="true" viewBox="0 0 10 10" width="10" height="10"><line x1="0" y1="5" x2="10" y2="5" stroke="currentColor" stroke-width="1"/></svg>
|
||
</button>
|
||
<button class="btn-window" id="btn-max" title="最大化" aria-label="最大化">
|
||
<svg aria-hidden="true" viewBox="0 0 10 10" width="10" height="10"><rect x="0.5" y="0.5" width="9" height="9" fill="none" stroke="currentColor" stroke-width="1"/></svg>
|
||
</button>
|
||
<button class="btn-window btn-close" id="btn-close" title="关闭" aria-label="关闭">
|
||
<svg aria-hidden="true" viewBox="0 0 10 10" width="10" height="10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 主区:侧边栏 + (可选编辑器 + 分割条) + 阅读区 -->
|
||
<!--
|
||
id="app" 是 modal.js:126 背景 inert 隔离机制的锚点:modal 打开时给 #app
|
||
打 inert 让背景 DOM(侧栏/工具栏/状态栏)失活,避免键盘 Tab 跳出 modal、
|
||
屏幕阅读器朗读背景。HTML 里必须显式存在该 id,否则 getElementById 返回
|
||
null,整套 inert 隔离静默失效(Round 12 P0 audit 发现)。
|
||
不要把 inert 挂在 body —— 那会让 modal 自身也失活;#app 是 app-shell,
|
||
跟 modal-root(独立容器)平级。
|
||
-->
|
||
<div class="app-shell" id="app">
|
||
<aside class="sidebar" id="sidebar" aria-label="文件与目录">
|
||
<div class="sidebar-header">
|
||
<div class="search-wrap">
|
||
<svg aria-hidden="true" class="search-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="11" cy="11" r="7"></circle>
|
||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||
</svg>
|
||
<input
|
||
type="search"
|
||
id="search-input"
|
||
class="search-input"
|
||
placeholder="搜索文件..."
|
||
aria-label="搜索文件"
|
||
>
|
||
</div>
|
||
</div>
|
||
<!-- 新建笔记:放在搜索框下方的独立主操作行,不再和搜索框挤在同一排抢首屏注意力(见 [[feedback-prefer-near-affected-area]]) -->
|
||
<div class="sidebar-new-row">
|
||
<button id="btn-new-file" class="sidebar-new-btn" type="button" title="新建笔记 (Ctrl+N)" aria-label="新建笔记">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||
</svg>
|
||
<span>新建笔记</span>
|
||
</button>
|
||
</div>
|
||
<!-- 排序工具条:紧贴它作用的列表,避免埋进设置对话框(见 [[feedback-prefer-near-affected-area]]) -->
|
||
<div class="sidebar-sort" role="group" aria-label="文件列表排序">
|
||
<button id="btn-sort-name" type="button" class="sidebar-sort-btn is-active" aria-pressed="true" title="按名称 A → Z 排序">
|
||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M3 6h13M3 12h9M3 18h5"></path>
|
||
<path d="M19 4v16M16 17l3 3 3-3"></path>
|
||
</svg>
|
||
<span>名称</span>
|
||
</button>
|
||
<button id="btn-sort-mtime" type="button" class="sidebar-sort-btn" aria-pressed="false" title="按修改时间排序(最近修改在前)">
|
||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<circle cx="12" cy="12" r="9"></circle>
|
||
<polyline points="12 7 12 12 15 14"></polyline>
|
||
</svg>
|
||
<span>修改时间</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Folder Browser(Stage 8):面包屑 + 「回到根」按钮,贴近它作用的文件列表。
|
||
根目录时整段 hidden,避免空栏占位。渲染端控制 .sidebar-breadcrumb[data-rel-dir] 与各段按钮 -->
|
||
<nav id="sidebar-breadcrumb" class="sidebar-breadcrumb" aria-label="当前目录" hidden>
|
||
<button id="btn-breadcrumb-root" type="button" class="sidebar-breadcrumb-root" title="返回数据文件夹根目录" aria-label="返回根目录">
|
||
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M3 12L12 4l9 8"></path>
|
||
<path d="M5 10v9h14v-9"></path>
|
||
</svg>
|
||
<span>根</span>
|
||
</button>
|
||
<span class="sidebar-breadcrumb-sep" aria-hidden="true">/</span>
|
||
<span id="sidebar-breadcrumb-trail" class="sidebar-breadcrumb-trail"></span>
|
||
</nav>
|
||
|
||
<ul id="file-list" class="file-list" role="listbox" aria-label="文件列表" tabindex="-1" data-empty="此文件夹为空">
|
||
<!-- 文件项由 file-list.js 注入 -->
|
||
</ul>
|
||
<div class="sidebar-footer">
|
||
<button type="button" id="link-open-folder" class="link-subtle footer-action" title="在系统文件管理器中打开数据文件夹">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||
</svg>
|
||
<span>打开数据文件夹</span>
|
||
</button>
|
||
<button type="button" id="btn-switch-folder" class="link-subtle footer-action" title="切换到其他数据文件夹">
|
||
<!-- folder + 双向 swap 箭头 (Lucide folder-sync 风格) -->
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1-2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||
<polyline points="16 3 19 3 19 6"></polyline>
|
||
<polyline points="8 21 5 21 5 18"></polyline>
|
||
<line x1="19" y1="3" x2="13" y2="9"></line>
|
||
<line x1="5" y1="21" x2="11" y2="15"></line>
|
||
</svg>
|
||
<span>切换数据文件夹</span>
|
||
</button>
|
||
<!-- auto-fallback 2026-08:「回到默认」按钮 —— 清空持久化的 dataDir 并切回 ~/Notes。
|
||
始终显示:用户切到非默认目录后想回到默认不需要先「切换」再选;救命的入口
|
||
不能藏在条件分支里。当已经在默认文件夹时按钮可点(toast 提示「已在默认」),
|
||
复用现有 .link-subtle .footer-action 样式,不新增 CSS。 -->
|
||
<button type="button" id="btn-reset-folder" class="link-subtle footer-action" title="回到默认数据文件夹(~/Notes)">
|
||
<!-- home + 屋顶(表达「回到原始默认」) -->
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M3 12l9-9 9 9"></path>
|
||
<path d="M5 10v10h14V10"></path>
|
||
</svg>
|
||
<span>回到默认</span>
|
||
</button>
|
||
<!-- 视觉分隔:把上方"打开/切换数据目录"二级链接和下方 AI 主操作分开 -->
|
||
<div class="sidebar-footer-sep" aria-hidden="true"></div>
|
||
<!-- AI 入口:放在侧栏最底,与顶部"主操作(新建笔记)→ 列表 → 排序 → 二级链接"形成层次。
|
||
默认沿用 .sidebar-new-btn 的描边主操作样式;展开后切到实心强调态(见 src/styles/11-ai.css 的 [aria-pressed="true"])。
|
||
AI 是「点开就弹,关闭就退」的命令式入口,与 settingsStore 无交集。 -->
|
||
<button id="btn-ai" class="sidebar-new-btn" type="button" title="AI 修改 (Ctrl+Shift+A)" aria-label="AI 修改" aria-pressed="false">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M12 2a4 4 0 0 0-4 4v1H6a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3v-9a3 3 0 0 0-3-3h-2V6a4 4 0 0 0-4-4z"></path>
|
||
<circle cx="9" cy="13" r="1.2" fill="currentColor" stroke="none"></circle>
|
||
<circle cx="15" cy="13" r="1.2" fill="currentColor" stroke="none"></circle>
|
||
<path d="M9 17h6"></path>
|
||
</svg>
|
||
<span>AI 修改</span>
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 侧栏拖拽分割条(拖动调侧栏宽度)。
|
||
视觉宽度 4px,命中区通过 -4px margin 扩到 12px。
|
||
内嵌 ↺ 按钮:hover splitter 时显形,点击把侧栏恢复到 CSS 默认宽度(220px),
|
||
并写回 sidebarWidth=null 让下次启动也走默认。 -->
|
||
<div id="sidebar-splitter" class="sidebar-splitter" role="separator" aria-orientation="vertical" aria-label="拖动调整侧栏宽度" aria-controls="sidebar" title="拖动调整侧栏宽度">
|
||
<button id="btn-sidebar-reset-width" class="sidebar-splitter-reset" type="button" title="恢复默认侧栏宽度" aria-label="恢复默认侧栏宽度" tabindex="-1">
|
||
<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M3 12a9 9 0 1 0 3-6.7L3 8"></path>
|
||
<polyline points="3 3 3 8 8 8"></polyline>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div id="editor-pane" class="editor-pane">
|
||
<!-- CodeMirror 挂载到这里 -->
|
||
</div>
|
||
|
||
<!-- fix(audit 2026-08):#splitter 加 ARIA separator role,键盘用户能感知
|
||
可拖拽边界(与 #sidebar-splitter / #ai-splitter 一致) -->
|
||
<div id="splitter" class="splitter" role="separator" aria-orientation="vertical" aria-label="拖动调整编辑器与阅读视图宽度" aria-controls="editor-pane viewer-main"></div>
|
||
|
||
<!--
|
||
重要:CSS Grid auto-placement 按 DOM 顺序处理。
|
||
.app-ai (grid-column:5) / .ai-splitter (grid-column:6) 都是显式钉位,
|
||
.viewer 是 auto。
|
||
若 .viewer 出现在 .app-ai 之前,DOM 处理时 .viewer 会先占 col 5,
|
||
.app-ai 显式 col 5 与 .viewer 冲突、被挤到 row 2(错位)。
|
||
所以 .app-ai / .ai-splitter 必须在 .viewer 之前。
|
||
此外 .app-ai 必须先于 .ai-splitter —— cursor 处理到 .app-ai (col 5)
|
||
后再前进到 col 6,.ai-splitter (col 6) 顺势放下,最后 .viewer
|
||
auto 落到 col 7;倒过来 .ai-splitter 先显式占 col 6,.app-ai 再
|
||
显式占 col 5,cursor 被推到 col 7,.viewer 反过来覆盖 .app-ai、
|
||
把 .app-ai 挤到 row 2(实测验证)。
|
||
-->
|
||
|
||
<!--
|
||
AI 修改差异中间面板:
|
||
- 默认 hidden,由 ai-controller 切换 body.dataset.aiDiffOpen 控制可见性
|
||
- 渲染走 ai-diff-panel.js,与 viewer 共享 markdown-body 渲染管线
|
||
- 必须在 #ai-splitter 之前(CSS Grid auto-placement 按 DOM 顺序处理,
|
||
app-ai 显式 grid-column:5 必须先于 viewer 处理,详见 splitter 上方注释)
|
||
-->
|
||
<aside id="app-ai" class="app-ai" hidden aria-label="AI 修改预览"></aside>
|
||
|
||
<!--
|
||
AI 拖动分割条(拖动调整 AI 中间面板宽度)。
|
||
与 #splitter / #sidebar-splitter 同设计:视觉 4px,命中区由 margin 扩展。
|
||
由 body[data-ai-diff-open="true"] 显示;与 .app-ai 紧邻。
|
||
- 必须在 .app-ai 之后、.viewer 之前(CSS Grid auto-placement:
|
||
app-ai col 5 放好后,ai-splitter col 6 用光 row 1 的中段,
|
||
viewer 才能 auto 落到 col 7;倒过来顺序会让 ai-splitter 把 col 5
|
||
让给 viewer,app-ai 显式 col 5 与 viewer 冲突、被挤到 row 2)。
|
||
-->
|
||
<div id="ai-splitter" class="ai-splitter" role="separator" aria-orientation="vertical" aria-label="拖动调整 AI 面板宽度" aria-controls="app-ai" title="拖动调整 AI 面板宽度" hidden></div>
|
||
|
||
<main class="viewer" id="viewer-main" tabindex="-1">
|
||
<article id="markdown-body" class="markdown-body" hidden>
|
||
<!-- markdown 渲染输出 -->
|
||
</article>
|
||
<div id="empty-state" class="empty-state">
|
||
<div class="empty-state-icon">
|
||
<svg aria-hidden="true" viewBox="0 0 24 24" width="36" height="36" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
<line x1="9" y1="13" x2="15" y2="13"></line>
|
||
<line x1="9" y1="17" x2="15" y2="17"></line>
|
||
</svg>
|
||
</div>
|
||
<p>选择一个 Markdown 文件开始阅读</p>
|
||
<p class="hint">
|
||
把 <code>.md</code> 文件放入数据文件夹后,<br>
|
||
会自动出现在左侧列表。
|
||
</p>
|
||
<p class="hint hint-secondary">左下角可切换数据文件夹,或在系统文件管理器中打开。</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!--
|
||
AI 对话输入面板(底部 dock):
|
||
- 位于 body grid 第 3 行(app-shell 与 statusbar 之间),
|
||
由 03-base.css 的 .ai-chat-dock { grid-row: 3 } 显式钉位
|
||
- 默认 hidden,点工具栏 btn-ai 展开;展开时挤占 app-shell 高度,
|
||
状态栏自动让位、始终钉底
|
||
- 与中部 diff 面板联动(diff 由 ai-controller 写入 .app-ai)
|
||
-->
|
||
<div class="ai-chat-dock" id="ai-chat-dock" hidden aria-label="AI 修改输入">
|
||
<div class="ai-chat-form-row">
|
||
<button class="ai-chat-toggle" type="button" title="关闭 AI 面板" aria-label="关闭 AI 面板" aria-controls="ai-chat-dock">×</button>
|
||
<div class="ai-chat-input-wrap">
|
||
<input
|
||
type="text"
|
||
class="ai-chat-input"
|
||
placeholder="描述你想如何修改当前文档..."
|
||
aria-label="AI 修改提示"
|
||
autocomplete="off"
|
||
autocapitalize="off"
|
||
spellcheck="false"
|
||
/>
|
||
</div>
|
||
<button class="ai-chat-submit" type="button" data-mode="submit" disabled>生成修改</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 状态栏:3 段式(路径弱化 / 字数核心 / 模式 + 进度 + 文件数)
|
||
segment 之间用 .status-spacer 推开,让眼睛先扫到核心信息 -->
|
||
<footer class="statusbar">
|
||
<div class="status-segment status-segment-left">
|
||
<button id="status-path" class="status-path status-path-clickable" type="button" title="在文件管理器中显示" aria-label="在文件管理器中显示当前文件">—</button>
|
||
<span class="status-divider"></span>
|
||
<span id="status-meta" class="status-meta"></span>
|
||
</div>
|
||
<div class="status-spacer"></div>
|
||
<div class="status-segment status-segment-center">
|
||
<span id="status-stats" class="status-chip status-chip-stats" hidden></span>
|
||
<span id="status-heading" class="status-heading" hidden></span>
|
||
</div>
|
||
<div class="status-spacer"></div>
|
||
<div class="status-segment status-segment-right">
|
||
<span id="status-scroll" class="status-chip status-chip-muted" hidden>0%</span>
|
||
<button id="status-mode" class="status-chip status-chip-clickable status-chip-mode" data-mode="preview" type="button" title="切换视图模式 (Ctrl+E)" aria-label="切换视图模式(当前:预览)">
|
||
<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<polyline points="17 1 21 5 17 9"></polyline>
|
||
<path d="M3 11V9a4 4 0 0 1 4-4h14"></path>
|
||
<polyline points="7 23 3 19 7 15"></polyline>
|
||
<path d="M21 13v2a4 4 0 0 1-4 4H3"></path>
|
||
</svg>
|
||
<span id="status-mode-label">预览</span>
|
||
</button>
|
||
<button id="status-ai" class="status-chip status-chip-clickable status-chip-ai" data-ai-state="idle" type="button" title="AI 修改" aria-label="AI 修改" aria-live="polite">
|
||
<span id="status-ai-label" class="status-chip-ai-text">AI</span>
|
||
</button>
|
||
<!-- 未保存指示器(prefers-reduced-motion 下也保留,比 toolbar is-dirty 按钮更显眼)
|
||
aria-live=polite 让屏幕阅读器在 dirty 状态切换时朗读「未保存」(Phase N a11y 修复)。-->
|
||
<span id="status-unsaved" class="status-chip status-chip-unsaved" hidden aria-live="polite">未保存</span>
|
||
<span class="status-divider"></span>
|
||
<span id="status-count" class="status-count"></span>
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- Toast 容器 -->
|
||
<div id="toast-container" class="toast-container" aria-live="polite"></div>
|
||
|
||
<!-- 设置对话框模板(由 settings-dialog.js 复用填充) -->
|
||
<div id="modal-root"></div>
|
||
|
||
<!-- 聚焦模式退出提示:鼠标悬停时显出,按 Ctrl+Shift+F 退出 -->
|
||
<div class="focus-exit-hint" aria-hidden="true">按 <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd> 退出聚焦</div>
|
||
|
||
<script type="module" src="src/app.js"></script>
|
||
</body>
|
||
</html>
|