diff --git a/docs/design-v1/app.js b/docs/design-v1/app.js
new file mode 100644
index 00000000..99f270c2
--- /dev/null
+++ b/docs/design-v1/app.js
@@ -0,0 +1,548 @@
+const notes = [
+ {
+ id: "plainleaf-plan",
+ title: "Plainleaf 改造计划",
+ path: "项目/Plainleaf/Plainleaf 改造计划",
+ updated: "今天 10:42",
+ favorite: true,
+ tags: ["产品", "设计", "SilverBullet"],
+ content: `
+
Plainleaf 会保留 SilverBullet 最重要的基础:每一篇笔记仍然是 NAS 中可以直接打开的 Markdown 文件。
+ 目标不是复制 Obsidian,而是让文件、搜索和上下文工具变得可见,同时保留 SilverBullet 的查询与自动化能力。
+ 第一阶段目标
+
+ - 建立常驻的文件树与快速搜索。
+ - 保留现有
CodeMirror 6 编辑器和实时预览。
+ - 增加大纲、反向链接、属性和页面信息。
+ - 为手机设计专用的快速记录流程。
+
+ 数据原则
+ 界面只是工作层,文件才是长期资产。即使以后停止使用 Plainleaf,也可以通过 Obsidian、VS Code 或普通文本编辑器继续使用全部内容。
+ 下一步
+ 交互 Demo 确认后,将工作区外壳接到 SilverBullet 的 [[页面索引]] 与现有导航命令上。
+ `,
+ outline: ["第一阶段目标", "数据原则", "下一步"],
+ backlinks: [
+ ["产品路线图", "将 Plainleaf 改造计划作为第一阶段的体验基线。"],
+ ["极空间部署", "新版上线时继续挂载同一个 Markdown Space。"],
+ ["笔记工具对比", "最终选择基于 SilverBullet 构建 Plainleaf。"]
+ ]
+ },
+ {
+ id: "zspace",
+ title: "极空间部署",
+ path: "知识库/Docker/极空间部署",
+ updated: "昨天 22:16",
+ favorite: true,
+ tags: ["Docker", "NAS", "部署"],
+ content: `
+ 极空间负责运行正式环境,Mac 用于开发和构建镜像。笔记目录通过卷挂载到容器的 /space。
+ 部署结构
+ - 固定镜像版本,避免使用 latest。
- 笔记目录和应用配置分别备份。
- 公网访问统一经过 HTTPS 与反向代理。
+ 更新流程
+ 在本地完成构建和验证后,推送自定义镜像,再由极空间 Compose 切换版本。
+ `,
+ outline: ["部署结构", "更新流程"],
+ backlinks: [["Plainleaf 改造计划", "正式版继续使用极空间现有的 Markdown 目录。"]]
+ },
+ {
+ id: "daily",
+ title: "2026-08-12",
+ path: "日记/2026/08/2026-08-12",
+ updated: "今天 08:30",
+ favorite: false,
+ tags: ["日记"],
+ content: `
+ 今天继续整理个人笔记系统,希望数据保持开放,同时让 Web 端变得更易用。
+ 今日事项
+ - 确认 Plainleaf 的工作区方向。
- 完成桌面与手机交互 Demo。
- 整理正式开发任务。
+ 记录
+ 文件树应该承担定位,而不是让用户记住所有命令。高级能力可以继续通过命令面板进入。
+ `,
+ outline: ["今日事项", "记录"],
+ backlinks: []
+ },
+ {
+ id: "docker-network",
+ title: "Docker 容器网络",
+ path: "知识库/Docker/Docker 容器网络",
+ updated: "8 月 10 日",
+ favorite: false,
+ tags: ["Docker", "网络"],
+ content: `
+ 同一个 Compose 项目中的服务应通过服务名通信,而不是依赖宿主机端口或孤立容器名称。
+ 服务发现
+ 容器加入同一网络后,可以使用 service:port 作为内部地址。
+ 排查顺序
+ - 确认两个服务属于同一 Docker 网络。
- 检查服务名能否解析。
- 检查目标端口是否监听在 0.0.0.0。
+ `,
+ outline: ["服务发现", "排查顺序"],
+ backlinks: [["极空间部署", "部署时先确认容器网络和服务发现。"]]
+ },
+ {
+ id: "tools",
+ title: "笔记工具对比",
+ path: "收件箱/笔记工具对比",
+ updated: "8 月 11 日",
+ favorite: false,
+ tags: ["调研", "笔记"],
+ content: `
+ 对比了 Blossom、Blinko、Notesnook、SilverBullet 和 Flatnotes。
+ 核心结论
+ 数据库优先的产品通常拥有更现代的成品体验,但文件原生模型更符合长期迁移与多工具使用的要求。
+ 选择
+ 以 SilverBullet 为文件与能力内核,在其上构建 Plainleaf 的现代工作区。
+ `,
+ outline: ["核心结论", "选择"],
+ backlinks: [["Plainleaf 改造计划", "选型结论推动了 Plainleaf 项目。"]]
+ },
+ {
+ id: "ideas",
+ title: "产品灵感收件箱",
+ path: "收件箱/产品灵感收件箱",
+ updated: "8 月 9 日",
+ favorite: false,
+ tags: ["灵感"],
+ content: `
+ 临时想法先进入收件箱,稍后再归档到项目、知识库或日记。
+ 待整理
+ - 浏览器剪藏入口。
- 手机语音快速记录。
- Markdown 文件历史对比。
+ `,
+ outline: ["待整理"],
+ backlinks: []
+ }
+];
+
+const tree = [
+ { type: "folder", name: "收件箱", children: ["tools", "ideas"] },
+ {
+ type: "folder",
+ name: "项目",
+ children: [
+ { type: "folder", name: "Plainleaf", children: ["plainleaf-plan"] },
+ { type: "folder", name: "网站改版", children: [] }
+ ]
+ },
+ {
+ type: "folder",
+ name: "知识库",
+ children: [
+ { type: "folder", name: "Docker", children: ["zspace", "docker-network"] },
+ { type: "folder", name: "AI", children: [] }
+ ]
+ },
+ {
+ type: "folder",
+ name: "日记",
+ children: [{ type: "folder", name: "2026", children: [{ type: "folder", name: "08", children: ["daily"] }] }]
+ }
+];
+
+const state = {
+ currentNoteId: "plainleaf-plan",
+ tool: "files",
+ context: "outline",
+ mode: "edit",
+ theme: "light",
+ offline: false,
+ searchIndex: 0,
+ expandedFolders: new Set(["收件箱", "项目", "项目/Plainleaf", "知识库", "知识库/Docker", "日记", "日记/2026", "日记/2026/08"])
+};
+
+const byId = (id) => document.getElementById(id);
+const currentNote = () => notes.find((note) => note.id === state.currentNoteId);
+
+function icon(name, className = "") {
+ return ``;
+}
+
+function refreshIcons() {
+ if (window.lucide) window.lucide.createIcons();
+}
+
+function renderTreeNode(node, depth = 0, parentPath = "") {
+ if (typeof node === "string") {
+ const note = notes.find((item) => item.id === node);
+ return `
+
+ ${icon("chevron-right", "disclosure is-placeholder")}
+ ${icon("file-text")}
+ ${note.title}
+
`;
+ }
+
+ const path = parentPath ? `${parentPath}/${node.name}` : node.name;
+ const expanded = state.expandedFolders.has(path);
+ const children = node.children.map((child) => renderTreeNode(child, depth + 1, path)).join("");
+ return `
+
+
+ ${icon(expanded ? "chevron-down" : "chevron-right", "disclosure")}
+ ${icon(expanded ? "folder-open" : "folder")}
+ ${node.name}
+ ${node.children.length ? `${node.children.length}` : ""}
+
+
${children}
+
`;
+}
+
+function filesViewMarkup() {
+ const favoriteRows = notes.filter((note) => note.favorite).map((note) => `
+
+ ${icon("star")}
+ ${note.title}
+
`).join("");
+ const recentRows = notes.slice(0, 3).map((note) => `
+
+ ${icon("history")}
+ ${note.title}
+ ${note.updated.split(" ")[0]}
+
`).join("");
+ return `
+
+ 收藏
+ ${favoriteRows}
+
+
+ 最近
+ ${recentRows}
+
+
+ 文件
+ ${tree.map((node) => renderTreeNode(node)).join("")}
+ `;
+}
+
+function searchViewMarkup() {
+ return `
+
+ 搜索建议
+ ${["最近修改的笔记", "包含待办的页面", "没有标签的笔记", "本周创建的页面"].map((label, index) => `
+
+ ${icon(index === 0 ? "clock-3" : index === 1 ? "list-checks" : index === 2 ? "tag" : "calendar-days")}
+ ${label}
+
`).join("")}
+
+
+ 搜索历史
+ ${["Docker", "Plainleaf", "部署"].map((label) => `${icon("search")}${label}
`).join("")}
+ `;
+}
+
+function tagsViewMarkup() {
+ const tags = [["Docker", 3], ["产品", 2], ["设计", 2], ["NAS", 2], ["日记", 1], ["灵感", 1], ["部署", 1]];
+ return `所有标签${tags.length}
${tags.map(([name, count]) => `${icon("hash")}${name}${count}
`).join("")}`;
+}
+
+function tasksViewMarkup() {
+ const tasks = [["完成交互 Demo", false, "今天"], ["确认文件树布局", true, "今天"], ["整理开发任务", false, "明天"], ["构建极空间镜像", false, "稍后"]];
+ return `开放任务3
${tasks.map(([name, done, date]) => `${icon(done ? "circle-check-big" : "circle")}${name}${date}
`).join("")}`;
+}
+
+function graphViewMarkup() {
+ return `${icon("network")}
关系图保留为高级视图
正式实现将复用 SilverBullet 现有对象索引和关系图能力。
`;
+}
+
+function bindNavigationRows(root = byId("navigationContent"), mobileSheet = false) {
+ root.querySelectorAll("[data-note-id]").forEach((row) => row.addEventListener("click", () => openNote(row.dataset.noteId)));
+ root.querySelectorAll("[data-folder-path]").forEach((row) => row.addEventListener("click", () => {
+ const path = row.dataset.folderPath;
+ if (state.expandedFolders.has(path)) state.expandedFolders.delete(path);
+ else state.expandedFolders.add(path);
+ if (mobileSheet) renderMobileSheet("files");
+ else renderNavigation();
+ }));
+ root.querySelectorAll("[data-open-search]").forEach((row) => row.addEventListener("click", () => openSearch(row.dataset.openSearch)));
+ root.querySelector("#inlineNewNote")?.addEventListener("click", openNewNote);
+}
+
+function renderNavigation() {
+ const titles = { files: "文件", search: "搜索", tags: "标签", tasks: "任务", graph: "关系图" };
+ byId("paneTitle").textContent = titles[state.tool];
+ const views = { files: filesViewMarkup, search: searchViewMarkup, tags: tagsViewMarkup, tasks: tasksViewMarkup, graph: graphViewMarkup };
+ byId("navigationContent").innerHTML = views[state.tool]();
+ bindNavigationRows();
+ refreshIcons();
+}
+
+function renderBreadcrumb(note) {
+ const segments = note.path.split("/");
+ byId("breadcrumb").innerHTML = segments.map((segment, index) => `${index ? icon("chevron-right") : ""}${segment}`).join("");
+}
+
+function renderContext() {
+ const note = currentNote();
+ const heading = { outline: "大纲", backlinks: "反向链接", properties: "属性", info: "页面信息" }[state.context];
+ let body = "";
+ if (state.context === "outline") {
+ body = `${note.outline.length ? note.outline.map((item, index) => `
`).join("") : `
${icon("list-tree")}
没有大纲
添加二级或三级标题后会显示在这里。
`}
`;
+ } else if (state.context === "backlinks") {
+ body = `${note.backlinks.length ? note.backlinks.map(([source, excerpt]) => `
${icon("link-2")}${source}
${excerpt}
`).join("") : `
${icon("links")}
还没有反向链接
其他页面引用当前笔记后会显示在这里。
`}
`;
+ } else if (state.context === "properties") {
+ body = `- tags
- ${note.tags.map((tag) => `${tag}`).join("")}
- status
- active
- created
- 2026-08-12
- aliases
- 暂无
`;
+ } else {
+ body = `- 文件
- ${note.path}.md
- 修改
- ${note.updated}
- 编码
- UTF-8
- 大小
- 3.4 KB
- 权限
- 读写
`;
+ }
+ byId("contextContent").innerHTML = `${heading}
${body}`;
+ refreshIcons();
+}
+
+function renderPreview(note) {
+ byId("documentPreview").innerHTML = `${note.title}
${note.content}`;
+}
+
+function openNote(id) {
+ const note = notes.find((item) => item.id === id);
+ if (!note) return;
+ state.currentNoteId = id;
+ byId("pageTitleInput").value = note.title;
+ byId("editorContent").innerHTML = note.content;
+ byId("updatedTime").textContent = `${note.updated} 更新`;
+ byId("pageLocation").textContent = note.path.split("/").slice(0, -1).join(" / ") || "根目录";
+ renderBreadcrumb(note);
+ renderPreview(note);
+ renderContext();
+ renderNavigation();
+ closeAllOverlays();
+ byId("mobileSheetOverlay").classList.remove("is-visible");
+ refreshIcons();
+}
+
+function setMode(mode) {
+ state.mode = mode;
+ byId("documentStage").dataset.mode = mode;
+ document.querySelectorAll(".segment").forEach((button) => button.classList.toggle("is-active", button.dataset.mode === mode));
+}
+
+function setTheme(theme) {
+ state.theme = theme;
+ document.documentElement.dataset.theme = theme;
+ const iconName = theme === "dark" ? "sun" : "moon";
+ byId("themeButton").innerHTML = icon(iconName);
+ refreshIcons();
+}
+
+function toggleTheme() {
+ setTheme(state.theme === "light" ? "dark" : "light");
+}
+
+function setOffline(offline) {
+ state.offline = offline;
+ byId("offlineBanner").classList.toggle("is-visible", offline);
+ byId("connectionLabel").textContent = offline ? "离线" : "已连接";
+ byId("connectionButton").innerHTML = `${icon(offline ? "cloud-off" : "cloud-check")}${offline ? "离线" : "已连接"}`;
+ byId("navStatusDot").classList.toggle("is-online", !offline);
+ byId("navStatusLabel").textContent = offline ? "等待连接后同步" : "本地文件已同步";
+ byId("menuOfflineButton").querySelector("span").textContent = offline ? "恢复在线" : "模拟离线";
+ refreshIcons();
+}
+
+let saveTimer;
+function markChanged() {
+ clearTimeout(saveTimer);
+ byId("saveState").className = "save-state is-saving";
+ byId("saveState").innerHTML = `${icon("loader-circle")}保存中`;
+ refreshIcons();
+ saveTimer = setTimeout(() => {
+ byId("saveState").className = "save-state is-saved";
+ byId("saveState").innerHTML = `${icon("check")}已保存`;
+ refreshIcons();
+ }, 900);
+}
+
+function filteredNotes(query) {
+ const normalized = query.trim().toLowerCase();
+ if (!normalized) return notes.slice(0, 5);
+ return notes.filter((note) => `${note.title} ${note.path} ${note.tags.join(" ")} ${note.content.replace(/<[^>]+>/g, " ")}`.toLowerCase().includes(normalized));
+}
+
+function renderSearchResults(query = "") {
+ const results = filteredNotes(query);
+ state.searchIndex = Math.min(state.searchIndex, Math.max(results.length - 1, 0));
+ byId("searchResults").innerHTML = results.length ? results.map((note, index) => `
+
+
${icon("file-text")}
+
${note.title}
${note.path}.md
+
${note.updated}
+
`).join("") : `${icon("search-x")}
没有找到笔记
保留当前搜索词,可以直接创建同名 Markdown 文件。
`;
+ byId("searchResults").querySelectorAll("[data-search-note]").forEach((row) => row.addEventListener("click", () => openNote(row.dataset.searchNote)));
+ refreshIcons();
+}
+
+function openSearch(query = "") {
+ state.searchIndex = 0;
+ byId("searchOverlay").classList.add("is-visible");
+ byId("searchOverlay").setAttribute("aria-hidden", "false");
+ byId("globalSearchInput").value = query;
+ renderSearchResults(query);
+ requestAnimationFrame(() => byId("globalSearchInput").focus());
+}
+
+function openNewNote() {
+ byId("newNoteOverlay").classList.add("is-visible");
+ byId("newNoteOverlay").setAttribute("aria-hidden", "false");
+ byId("newNoteName").value = "";
+ requestAnimationFrame(() => byId("newNoteName").focus());
+}
+
+function closeAllOverlays() {
+ document.querySelectorAll(".overlay").forEach((overlay) => {
+ overlay.classList.remove("is-visible");
+ overlay.setAttribute("aria-hidden", "true");
+ });
+}
+
+function createNote() {
+ const title = byId("newNoteName").value.trim() || "未命名笔记";
+ const folder = byId("newNoteFolder").value;
+ const id = `new-${Date.now()}`;
+ const note = {
+ id,
+ title,
+ path: `${folder}/${title}`,
+ updated: "刚刚",
+ favorite: false,
+ tags: [],
+ content: `开始记录...
`,
+ outline: [],
+ backlinks: []
+ };
+ notes.unshift(note);
+ const targetFolder = findFolder(tree, folder.split("/"));
+ if (targetFolder) targetFolder.children.unshift(id);
+ state.expandedFolders.add(folder);
+ openNote(id);
+ requestAnimationFrame(() => byId("editorContent").focus());
+}
+
+function findFolder(nodes, parts) {
+ const [head, ...rest] = parts;
+ const found = nodes.find((node) => typeof node !== "string" && node.name === head);
+ if (!found) return null;
+ if (!rest.length) return found;
+ return findFolder(found.children.filter((child) => typeof child !== "string"), rest);
+}
+
+function renderMobileSheet(kind) {
+ const title = kind === "links" ? "反向链接" : "文件";
+ byId("mobileSheetTitle").textContent = title;
+ if (kind === "links") {
+ const note = currentNote();
+ byId("mobileSheetContent").innerHTML = note.backlinks.length ? `${note.backlinks.map(([source, excerpt]) => `
${icon("link-2")}${source}
${excerpt}
`).join("")}
` : `${icon("links")}
还没有反向链接
其他页面引用当前笔记后会显示在这里。
`;
+ } else {
+ byId("mobileSheetContent").innerHTML = `${tree.map((node) => renderTreeNode(node)).join("")}
`;
+ bindNavigationRows(byId("mobileSheetContent"), true);
+ }
+ byId("mobileSheetOverlay").classList.add("is-visible");
+ byId("mobileSheetOverlay").setAttribute("aria-hidden", "false");
+ refreshIcons();
+}
+
+function setupResizer(resizer, side) {
+ resizer.addEventListener("pointerdown", (event) => {
+ event.preventDefault();
+ resizer.setPointerCapture(event.pointerId);
+ resizer.classList.add("is-dragging");
+ const onMove = (moveEvent) => {
+ const value = side === "left" ? moveEvent.clientX - 48 : window.innerWidth - moveEvent.clientX;
+ document.documentElement.style.setProperty(side === "left" ? "--nav-width" : "--context-width", `${Math.max(side === "left" ? 220 : 240, Math.min(side === "left" ? 380 : 380, value))}px`);
+ };
+ const onUp = () => {
+ resizer.classList.remove("is-dragging");
+ resizer.removeEventListener("pointermove", onMove);
+ resizer.removeEventListener("pointerup", onUp);
+ };
+ resizer.addEventListener("pointermove", onMove);
+ resizer.addEventListener("pointerup", onUp);
+ });
+}
+
+function initEvents() {
+ document.querySelectorAll("[data-tool]").forEach((button) => button.addEventListener("click", () => {
+ state.tool = button.dataset.tool;
+ document.querySelectorAll("[data-tool]").forEach((item) => item.classList.toggle("is-active", item === button));
+ renderNavigation();
+ }));
+ document.querySelectorAll("[data-mode]").forEach((button) => button.addEventListener("click", () => setMode(button.dataset.mode)));
+ document.querySelectorAll("[data-context]").forEach((button) => button.addEventListener("click", () => {
+ state.context = button.dataset.context;
+ document.querySelectorAll("[data-context]").forEach((item) => item.classList.toggle("is-active", item === button));
+ renderContext();
+ }));
+ document.querySelectorAll("[data-mobile-action]").forEach((button) => button.addEventListener("click", () => {
+ const action = button.dataset.mobileAction;
+ if (action === "files") renderMobileSheet("files");
+ if (action === "search") openSearch();
+ if (action === "new") openNewNote();
+ if (action === "links") renderMobileSheet("links");
+ if (action === "more") byId("moreMenu").classList.toggle("is-visible");
+ }));
+ byId("searchTrigger").addEventListener("click", () => openSearch());
+ byId("inPageSearchButton").addEventListener("click", () => openSearch(currentNote().title));
+ byId("newNoteButton").addEventListener("click", openNewNote);
+ byId("createNoteButton").addEventListener("click", createNote);
+ document.querySelectorAll(".close-overlay-button").forEach((button) => button.addEventListener("click", closeAllOverlays));
+ byId("globalSearchInput").addEventListener("input", (event) => {
+ state.searchIndex = 0;
+ renderSearchResults(event.target.value);
+ });
+ byId("globalSearchInput").addEventListener("keydown", (event) => {
+ const results = filteredNotes(event.currentTarget.value);
+ if (event.key === "ArrowDown" && results.length) {
+ event.preventDefault(); state.searchIndex = (state.searchIndex + 1) % results.length; renderSearchResults(event.currentTarget.value);
+ }
+ if (event.key === "ArrowUp" && results.length) {
+ event.preventDefault(); state.searchIndex = (state.searchIndex - 1 + results.length) % results.length; renderSearchResults(event.currentTarget.value);
+ }
+ if (event.key === "Enter" && results[state.searchIndex]) openNote(results[state.searchIndex].id);
+ });
+ byId("pageTitleInput").addEventListener("input", (event) => {
+ const note = currentNote();
+ note.title = event.target.value;
+ const pathParts = note.path.split("/");
+ pathParts[pathParts.length - 1] = note.title;
+ note.path = pathParts.join("/");
+ renderBreadcrumb(note);
+ renderPreview(note);
+ renderNavigation();
+ markChanged();
+ });
+ byId("editorContent").addEventListener("input", () => {
+ currentNote().content = byId("editorContent").innerHTML;
+ renderPreview(currentNote());
+ const count = byId("editorContent").innerText.replace(/\s+/g, "").length;
+ byId("wordCount").textContent = `${count} 字`;
+ markChanged();
+ });
+ byId("themeButton").addEventListener("click", toggleTheme);
+ byId("menuThemeButton").addEventListener("click", () => { toggleTheme(); byId("moreMenu").classList.remove("is-visible"); });
+ byId("connectionButton").addEventListener("click", () => setOffline(!state.offline));
+ byId("menuOfflineButton").addEventListener("click", () => { setOffline(!state.offline); byId("moreMenu").classList.remove("is-visible"); });
+ byId("moreButton").addEventListener("click", (event) => { event.stopPropagation(); byId("moreMenu").classList.toggle("is-visible"); });
+ byId("collapseNavButton").addEventListener("click", () => byId("appShell").classList.add("is-nav-collapsed"));
+ byId("showNavButton").addEventListener("click", () => {
+ if (window.innerWidth <= 760) renderMobileSheet("files");
+ else byId("appShell").classList.remove("is-nav-collapsed");
+ });
+ byId("toggleContextButton").addEventListener("click", () => byId("appShell").classList.toggle("is-context-collapsed"));
+ byId("closeContextButton").addEventListener("click", () => byId("appShell").classList.add("is-context-collapsed"));
+ byId("closeMobileSheet").addEventListener("click", closeAllOverlays);
+ document.querySelectorAll(".overlay").forEach((overlay) => overlay.addEventListener("pointerdown", (event) => { if (event.target === overlay) closeAllOverlays(); }));
+ document.addEventListener("click", (event) => { if (!byId("moreMenu").contains(event.target) && event.target !== byId("moreButton")) byId("moreMenu").classList.remove("is-visible"); });
+ document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape") { closeAllOverlays(); byId("moreMenu").classList.remove("is-visible"); }
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); openSearch(); }
+ });
+ setupResizer(byId("navResizer"), "left");
+ setupResizer(byId("contextResizer"), "right");
+}
+
+function init() {
+ byId("documentStage").dataset.mode = state.mode;
+ renderNavigation();
+ openNote(state.currentNoteId);
+ initEvents();
+ refreshIcons();
+}
+
+init();
diff --git a/docs/design-v1/index.html b/docs/design-v1/index.html
new file mode 100644
index 00000000..4766ff05
--- /dev/null
+++ b/docs/design-v1/index.html
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+ Plainleaf Workspace Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 当前离线,修改会保存在本机并在连接恢复后同步。
+
+
+
+
+
+
+ 今天 10:42 更新
+ ·
+ 项目 / Plainleaf
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ESC
+
+ 快速打开
+
+
+
+
+
+
+
+
+
+
NEW MARKDOWN FILE
+
新建笔记
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/design-v1/styles.css b/docs/design-v1/styles.css
new file mode 100644
index 00000000..b7786201
--- /dev/null
+++ b/docs/design-v1/styles.css
@@ -0,0 +1,1470 @@
+:root {
+ color-scheme: light;
+ --canvas: #f7f8f6;
+ --editor: #ffffff;
+ --navigation: #eef1ee;
+ --rail: #202621;
+ --rail-muted: #9ca69e;
+ --rail-active: #f0f5f1;
+ --text: #20241f;
+ --muted: #687169;
+ --faint: #929a93;
+ --border: #dce2dc;
+ --border-strong: #cbd3cc;
+ --accent: #397453;
+ --accent-hover: #2f6447;
+ --accent-soft: #dcece2;
+ --accent-contrast: #ffffff;
+ --link: #376d8a;
+ --warning: #a86718;
+ --warning-soft: #fff4dc;
+ --danger: #b64a4a;
+ --selection: #e2eee6;
+ --code-bg: #f2f4f2;
+ --shadow-modal: 0 18px 60px rgb(29 36 31 / 22%);
+ --nav-width: 272px;
+ --context-width: 296px;
+ --top-height: 44px;
+ --status-height: 28px;
+ --rail-width: 48px;
+ --radius: 5px;
+ --ui-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "Microsoft YaHei", sans-serif;
+ --body-font: Inter, "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --code-font: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
+}
+
+html[data-theme="dark"] {
+ color-scheme: dark;
+ --canvas: #171a18;
+ --editor: #1e221f;
+ --navigation: #202521;
+ --rail: #111411;
+ --rail-muted: #89938b;
+ --rail-active: #e1e8e2;
+ --text: #e8ece8;
+ --muted: #9ba69d;
+ --faint: #727c74;
+ --border: #343b35;
+ --border-strong: #465048;
+ --accent: #75b18d;
+ --accent-hover: #88c09e;
+ --accent-soft: #283c30;
+ --accent-contrast: #122018;
+ --link: #75a8c2;
+ --warning: #dfa84f;
+ --warning-soft: #3b3020;
+ --danger: #dc7d7d;
+ --selection: #2a3d31;
+ --code-bg: #292e2a;
+ --shadow-modal: 0 22px 70px rgb(0 0 0 / 48%);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ background: var(--canvas);
+ color: var(--text);
+ font-family: var(--ui-font);
+ letter-spacing: 0;
+}
+
+button,
+input,
+select {
+ color: inherit;
+ font: inherit;
+ letter-spacing: 0;
+}
+
+button {
+ border: 0;
+}
+
+button:focus-visible,
+input:focus-visible,
+select:focus-visible,
+[contenteditable="true"]:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 1px;
+}
+
+.app-shell {
+ display: grid;
+ grid-template-columns: var(--rail-width) var(--nav-width) 4px minmax(420px, 1fr) 4px var(--context-width);
+ width: 100%;
+ height: 100%;
+ background: var(--canvas);
+}
+
+.activity-rail {
+ z-index: 20;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ grid-column: 1;
+ background: var(--rail);
+ color: var(--rail-muted);
+ border-right: 1px solid rgb(255 255 255 / 7%);
+}
+
+.brand-mark,
+.rail-button {
+ display: grid;
+ place-items: center;
+ position: relative;
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ border-radius: var(--radius);
+ color: inherit;
+ background: transparent;
+ cursor: pointer;
+}
+
+.brand-mark {
+ width: 34px;
+ height: 34px;
+ margin: 8px 0 10px;
+ color: #dce9df;
+ background: #315f45;
+}
+
+.brand-mark svg {
+ width: 19px;
+ height: 19px;
+}
+
+.rail-tools,
+.rail-footer {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ align-items: center;
+}
+
+.rail-tools {
+ flex: 1;
+}
+
+.rail-footer {
+ margin-bottom: 8px;
+}
+
+.rail-button:hover,
+.rail-button.is-active {
+ color: var(--rail-active);
+ background: rgb(255 255 255 / 8%);
+}
+
+.rail-button.is-active::before {
+ position: absolute;
+ left: -6px;
+ width: 3px;
+ height: 20px;
+ border-radius: 0 3px 3px 0;
+ background: #79b38e;
+ content: "";
+}
+
+.rail-button svg,
+.icon-button svg {
+ width: 18px;
+ height: 18px;
+ stroke-width: 1.8;
+}
+
+.navigation-pane {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ grid-column: 2;
+ background: var(--navigation);
+ border-right: 1px solid var(--border);
+ overflow: hidden;
+}
+
+.pane-header,
+.context-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex: 0 0 auto;
+}
+
+.pane-header {
+ height: 72px;
+ padding: 12px 12px 8px 16px;
+}
+
+.workspace-kicker,
+.dialog-kicker {
+ margin: 0 0 3px;
+ color: var(--faint);
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 14px;
+}
+
+.pane-header h1,
+.new-note-dialog h2,
+.mobile-sheet h2 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 650;
+ line-height: 22px;
+}
+
+.header-actions,
+.toolbar-actions,
+.toolbar-leading,
+.context-tabs,
+.status-group {
+ display: flex;
+ align-items: center;
+}
+
+.header-actions,
+.toolbar-actions,
+.context-tabs {
+ gap: 3px;
+}
+
+.icon-button {
+ display: grid;
+ place-items: center;
+ flex: 0 0 32px;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ border-radius: var(--radius);
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+}
+
+.icon-button:hover,
+.context-tab:hover,
+.context-tab.is-active {
+ color: var(--text);
+ background: var(--selection);
+}
+
+.search-trigger {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: calc(100% - 24px);
+ height: 34px;
+ margin: 0 12px 10px;
+ padding: 0 9px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ color: var(--muted);
+ background: var(--editor);
+ cursor: pointer;
+ text-align: left;
+}
+
+.search-trigger:hover {
+ border-color: var(--border-strong);
+}
+
+.search-trigger svg {
+ width: 15px;
+ height: 15px;
+}
+
+.search-trigger span {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+kbd {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 22px;
+ height: 20px;
+ padding: 0 5px;
+ border: 1px solid var(--border);
+ border-bottom-color: var(--border-strong);
+ border-radius: 4px;
+ color: var(--faint);
+ background: var(--canvas);
+ font-family: var(--ui-font);
+ font-size: 11px;
+}
+
+.navigation-content {
+ flex: 1;
+ min-height: 0;
+ padding: 0 8px 12px;
+ overflow: auto;
+}
+
+.nav-section {
+ margin-top: 10px;
+}
+
+.section-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 24px;
+ padding: 0 8px;
+ color: var(--faint);
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.section-heading button {
+ display: grid;
+ place-items: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border-radius: 4px;
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+}
+
+.section-heading button:hover {
+ background: var(--selection);
+}
+
+.nav-row,
+.tree-row,
+.tag-row,
+.task-row,
+.search-result {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ border-radius: var(--radius);
+ cursor: pointer;
+}
+
+.nav-row,
+.tree-row,
+.tag-row,
+.task-row {
+ height: 28px;
+ padding: 0 7px;
+ color: var(--muted);
+ font-size: 13px;
+}
+
+.nav-row:hover,
+.tree-row:hover,
+.tag-row:hover,
+.task-row:hover,
+.tree-row.is-active {
+ color: var(--text);
+ background: var(--selection);
+}
+
+.tree-row.is-active {
+ font-weight: 600;
+}
+
+.tree-row svg,
+.nav-row svg,
+.tag-row svg,
+.task-row svg {
+ flex: 0 0 auto;
+ width: 15px;
+ height: 15px;
+ margin-right: 7px;
+ stroke-width: 1.8;
+}
+
+.tree-row .disclosure {
+ width: 14px;
+ margin-right: 3px;
+}
+
+.tree-row .disclosure.is-placeholder {
+ visibility: hidden;
+}
+
+.row-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.row-meta {
+ flex: 0 0 auto;
+ margin-left: 8px;
+ color: var(--faint);
+ font-size: 11px;
+}
+
+.folder-children.is-collapsed {
+ display: none;
+}
+
+.navigation-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex: 0 0 40px;
+ min-width: 0;
+ padding: 0 7px 0 14px;
+ border-top: 1px solid var(--border);
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.sync-summary {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ gap: 7px;
+}
+
+.sync-summary span:last-child {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-dot {
+ flex: 0 0 7px;
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--warning);
+}
+
+.status-dot.is-online {
+ background: #4f956b;
+}
+
+.pane-resizer {
+ z-index: 12;
+ cursor: col-resize;
+ background: transparent;
+}
+
+.pane-resizer:hover,
+.pane-resizer.is-dragging {
+ background: var(--accent);
+}
+
+.nav-resizer {
+ grid-column: 3;
+}
+
+.context-resizer {
+ grid-column: 5;
+}
+
+.workspace {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ grid-column: 4;
+ background: var(--editor);
+}
+
+.workspace-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex: 0 0 var(--top-height);
+ min-width: 0;
+ padding: 0 8px 0 14px;
+ border-bottom: 1px solid var(--border);
+ background: var(--editor);
+}
+
+.toolbar-leading {
+ min-width: 0;
+}
+
+.breadcrumb {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+ color: var(--muted);
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.breadcrumb span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.breadcrumb svg {
+ flex: 0 0 13px;
+ width: 13px;
+ height: 13px;
+ margin: 0 2px;
+ color: var(--faint);
+}
+
+.save-state {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ width: 66px;
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.save-state svg {
+ width: 14px;
+ height: 14px;
+}
+
+.save-state.is-saving {
+ color: var(--warning);
+}
+
+.segmented-control {
+ display: grid;
+ grid-template-columns: repeat(3, 42px);
+ height: 30px;
+ padding: 2px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ background: var(--canvas);
+}
+
+.segment {
+ padding: 0;
+ border-radius: 3px;
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+ font-size: 11px;
+}
+
+.segment:hover,
+.segment.is-active {
+ color: var(--text);
+ background: var(--editor);
+ box-shadow: 0 1px 2px rgb(24 30 25 / 10%);
+}
+
+.show-nav-button {
+ display: none;
+ margin-right: 5px;
+}
+
+.offline-banner {
+ display: none;
+ align-items: center;
+ gap: 8px;
+ flex: 0 0 34px;
+ padding: 0 16px;
+ color: var(--warning);
+ background: var(--warning-soft);
+ border-bottom: 1px solid color-mix(in srgb, var(--warning), transparent 70%);
+ font-size: 12px;
+}
+
+.offline-banner.is-visible {
+ display: flex;
+}
+
+.offline-banner svg {
+ width: 15px;
+ height: 15px;
+}
+
+.document-stage {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 0 minmax(0, 0);
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.document-stage[data-mode="read"] {
+ grid-template-columns: 0 0 minmax(0, 1fr);
+}
+
+.document-stage[data-mode="split"] {
+ grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
+}
+
+.document-stage[data-mode="split"] .split-divider {
+ background: var(--border);
+}
+
+.document-editor,
+.document-preview {
+ min-width: 0;
+ min-height: 0;
+ overflow: auto;
+}
+
+.document-editor {
+ padding: 48px max(40px, calc((100% - 760px) / 2)) 80px;
+}
+
+.document-preview {
+ padding: 48px max(40px, calc((100% - 760px) / 2)) 80px;
+}
+
+.document-stage[data-mode="split"] .document-editor,
+.document-stage[data-mode="split"] .document-preview {
+ padding-right: 36px;
+ padding-left: 36px;
+}
+
+.page-title-input {
+ display: block;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ color: var(--text);
+ background: transparent;
+ font-family: var(--body-font);
+ font-size: 30px;
+ font-weight: 650;
+ line-height: 38px;
+}
+
+.page-title-input:focus {
+ outline: 0;
+}
+
+.page-meta-line {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin: 7px 0 34px;
+ color: var(--faint);
+ font-size: 12px;
+ line-height: 18px;
+}
+
+.editor-content,
+.document-preview {
+ color: var(--text);
+ font-family: var(--body-font);
+ font-size: 16px;
+ line-height: 28px;
+}
+
+.editor-content:focus {
+ outline: 0;
+}
+
+.editor-content h2,
+.document-preview h2 {
+ margin: 34px 0 10px;
+ color: var(--text);
+ font-size: 21px;
+ font-weight: 650;
+ line-height: 30px;
+}
+
+.editor-content p,
+.document-preview p {
+ margin: 0 0 18px;
+}
+
+.editor-content ul,
+.document-preview ul {
+ margin: 0 0 22px;
+ padding-left: 23px;
+}
+
+.editor-content li,
+.document-preview li {
+ margin: 5px 0;
+ padding-left: 3px;
+}
+
+.editor-content code,
+.document-preview code {
+ padding: 2px 5px;
+ border-radius: 4px;
+ background: var(--code-bg);
+ color: var(--danger);
+ font-family: var(--code-font);
+ font-size: 0.9em;
+}
+
+.editor-content a,
+.document-preview a,
+.wiki-link {
+ color: var(--link);
+ text-decoration: none;
+}
+
+.editor-content blockquote,
+.document-preview blockquote {
+ margin: 24px 0;
+ padding: 4px 0 4px 16px;
+ border-left: 3px solid var(--accent);
+ color: var(--muted);
+}
+
+.document-preview > h1 {
+ margin: 0 0 34px;
+ font-size: 30px;
+ line-height: 38px;
+}
+
+.status-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex: 0 0 var(--status-height);
+ min-width: 0;
+ padding: 0 10px;
+ border-top: 1px solid var(--border);
+ color: var(--faint);
+ background: var(--canvas);
+ font-size: 10px;
+}
+
+.status-group {
+ min-width: 0;
+ gap: 12px;
+}
+
+.status-button {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ height: 22px;
+ padding: 0;
+ color: inherit;
+ background: transparent;
+ cursor: pointer;
+}
+
+.status-button:hover {
+ color: var(--text);
+}
+
+.status-button svg {
+ width: 13px;
+ height: 13px;
+}
+
+.context-pane {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ grid-column: 6;
+ background: var(--canvas);
+ border-left: 1px solid var(--border);
+ overflow: hidden;
+}
+
+.context-header {
+ flex: 0 0 var(--top-height);
+ padding: 0 7px 0 10px;
+ border-bottom: 1px solid var(--border);
+}
+
+.context-tab {
+ display: grid;
+ place-items: center;
+ width: 32px;
+ height: 30px;
+ padding: 0;
+ border-radius: var(--radius);
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+}
+
+.context-tab svg {
+ width: 16px;
+ height: 16px;
+}
+
+.context-content {
+ flex: 1;
+ min-height: 0;
+ padding: 16px 14px 28px;
+ overflow: auto;
+}
+
+.context-title {
+ margin: 0 0 14px;
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.outline-list,
+.backlink-list,
+.property-list,
+.info-list {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.outline-item {
+ display: block;
+ width: 100%;
+ min-height: 29px;
+ padding: 5px 7px;
+ border-radius: var(--radius);
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+ text-align: left;
+ font-size: 12px;
+ line-height: 18px;
+}
+
+.outline-item.is-sub {
+ padding-left: 20px;
+}
+
+.outline-item:hover,
+.outline-item.is-active {
+ color: var(--text);
+ background: var(--selection);
+}
+
+.backlink-item {
+ padding: 8px 6px 10px;
+ border-bottom: 1px solid var(--border);
+}
+
+.backlink-item:last-child {
+ border-bottom: 0;
+}
+
+.backlink-source {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 5px;
+ color: var(--text);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.backlink-source svg {
+ width: 13px;
+ height: 13px;
+ color: var(--link);
+}
+
+.backlink-excerpt {
+ margin: 0;
+ color: var(--muted);
+ font-size: 11px;
+ line-height: 18px;
+}
+
+.property-row,
+.info-row {
+ display: grid;
+ grid-template-columns: 78px minmax(0, 1fr);
+ gap: 8px;
+ min-height: 30px;
+ padding: 5px 6px;
+ border-bottom: 1px solid var(--border);
+ font-size: 11px;
+ line-height: 19px;
+}
+
+.property-row dt,
+.info-row dt {
+ color: var(--faint);
+}
+
+.property-row dd,
+.info-row dd {
+ min-width: 0;
+ margin: 0;
+ overflow-wrap: anywhere;
+}
+
+.tag-chip {
+ display: inline-flex;
+ align-items: center;
+ height: 20px;
+ margin: 0 4px 4px 0;
+ padding: 0 7px;
+ border-radius: 4px;
+ color: var(--accent);
+ background: var(--accent-soft);
+ font-size: 10px;
+}
+
+.overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ z-index: 100;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: min(16vh, 140px);
+ background: rgb(15 19 16 / 28%);
+ backdrop-filter: blur(2px);
+}
+
+.overlay.is-visible {
+ display: flex;
+}
+
+.command-dialog,
+.new-note-dialog {
+ width: min(620px, calc(100vw - 32px));
+ max-height: min(600px, calc(100vh - 80px));
+ border: 1px solid var(--border-strong);
+ border-radius: 8px;
+ background: var(--editor);
+ box-shadow: var(--shadow-modal);
+ overflow: hidden;
+}
+
+.dialog-search-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ height: 52px;
+ padding: 0 14px;
+ border-bottom: 1px solid var(--border);
+}
+
+.dialog-search-row svg {
+ width: 18px;
+ height: 18px;
+ color: var(--muted);
+}
+
+.dialog-search-row input {
+ flex: 1;
+ min-width: 0;
+ height: 42px;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ color: var(--text);
+ background: transparent;
+ font-size: 16px;
+}
+
+.dialog-section-label {
+ padding: 12px 14px 5px;
+ color: var(--faint);
+ font-size: 10px;
+ font-weight: 700;
+}
+
+.search-results {
+ max-height: 360px;
+ padding: 4px 7px 8px;
+ overflow: auto;
+}
+
+.search-result {
+ min-height: 54px;
+ padding: 7px 9px;
+ color: var(--text);
+}
+
+.search-result:hover,
+.search-result.is-selected {
+ background: var(--selection);
+}
+
+.search-result-icon {
+ display: grid;
+ place-items: center;
+ flex: 0 0 30px;
+ width: 30px;
+ height: 30px;
+ margin-right: 10px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ color: var(--muted);
+ background: var(--canvas);
+}
+
+.search-result-icon svg {
+ width: 15px;
+ height: 15px;
+}
+
+.search-result-copy {
+ flex: 1;
+ min-width: 0;
+}
+
+.search-result-title,
+.search-result-path {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.search-result-title {
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.search-result-path {
+ margin-top: 2px;
+ color: var(--faint);
+ font-size: 11px;
+}
+
+.dialog-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 14px;
+ height: 36px;
+ padding: 0 12px;
+ border-top: 1px solid var(--border);
+ color: var(--faint);
+ background: var(--canvas);
+ font-size: 10px;
+}
+
+.dialog-footer span {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+}
+
+.new-note-dialog {
+ width: min(440px, calc(100vw - 32px));
+ padding: 18px;
+}
+
+.new-note-dialog header,
+.new-note-dialog footer,
+.mobile-sheet header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.new-note-dialog label {
+ display: block;
+ margin-top: 18px;
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.new-note-dialog label span {
+ display: block;
+ margin-bottom: 6px;
+}
+
+.new-note-dialog input,
+.new-note-dialog select {
+ width: 100%;
+ height: 38px;
+ padding: 0 10px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ color: var(--text);
+ background: var(--canvas);
+}
+
+.new-note-dialog footer {
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 22px;
+}
+
+.primary-button,
+.secondary-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ height: 36px;
+ padding: 0 13px;
+ border-radius: var(--radius);
+ cursor: pointer;
+ font-weight: 600;
+ font-size: 12px;
+}
+
+.primary-button {
+ color: var(--accent-contrast);
+ background: var(--accent);
+}
+
+.primary-button:hover {
+ background: var(--accent-hover);
+}
+
+.secondary-button {
+ border: 1px solid var(--border);
+ background: var(--canvas);
+}
+
+.primary-button svg {
+ width: 15px;
+ height: 15px;
+}
+
+.popover-menu {
+ display: none;
+ position: fixed;
+ z-index: 90;
+ top: 40px;
+ right: calc(var(--context-width) + 16px);
+ width: 210px;
+ padding: 5px;
+ border: 1px solid var(--border-strong);
+ border-radius: 7px;
+ background: var(--editor);
+ box-shadow: var(--shadow-modal);
+}
+
+.app-shell.is-context-collapsed ~ .popover-menu,
+.popover-menu.is-context-collapsed {
+ right: 12px;
+}
+
+.popover-menu.is-visible {
+ display: block;
+}
+
+.popover-menu button {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ width: 100%;
+ height: 32px;
+ padding: 0 8px;
+ border-radius: var(--radius);
+ color: var(--text);
+ background: transparent;
+ cursor: pointer;
+ text-align: left;
+ font-size: 12px;
+}
+
+.popover-menu button:hover {
+ background: var(--selection);
+}
+
+.popover-menu button span {
+ flex: 1;
+}
+
+.popover-menu svg {
+ width: 15px;
+ height: 15px;
+ color: var(--muted);
+}
+
+.mobile-navigation,
+.mobile-sheet-overlay {
+ display: none;
+}
+
+.app-shell.is-nav-collapsed {
+ grid-template-columns: var(--rail-width) 0 0 minmax(420px, 1fr) 4px var(--context-width);
+}
+
+.app-shell.is-nav-collapsed .navigation-pane,
+.app-shell.is-nav-collapsed .nav-resizer {
+ display: none;
+}
+
+.app-shell.is-nav-collapsed .show-nav-button {
+ display: grid;
+}
+
+.app-shell.is-context-collapsed {
+ grid-template-columns: var(--rail-width) var(--nav-width) 4px minmax(420px, 1fr) 0 0;
+}
+
+.app-shell.is-context-collapsed .context-pane,
+.app-shell.is-context-collapsed .context-resizer {
+ display: none;
+}
+
+.app-shell.is-nav-collapsed.is-context-collapsed {
+ grid-template-columns: var(--rail-width) 0 0 minmax(420px, 1fr) 0 0;
+}
+
+.empty-state {
+ display: flex;
+ min-height: 220px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 28px;
+ color: var(--muted);
+ text-align: center;
+}
+
+.empty-state svg {
+ width: 28px;
+ height: 28px;
+ margin-bottom: 12px;
+ color: var(--faint);
+}
+
+.empty-state h3 {
+ margin: 0 0 6px;
+ color: var(--text);
+ font-size: 14px;
+}
+
+.empty-state p {
+ max-width: 240px;
+ margin: 0;
+ font-size: 12px;
+ line-height: 19px;
+}
+
+@media (max-width: 1180px) {
+ :root {
+ --context-width: 270px;
+ }
+
+ .app-shell {
+ grid-template-columns: var(--rail-width) var(--nav-width) 4px minmax(420px, 1fr) 0 0;
+ }
+
+ .context-pane,
+ .context-resizer {
+ display: none;
+ }
+
+ .popover-menu {
+ right: 12px;
+ }
+}
+
+@media (max-width: 760px) {
+ :root {
+ --top-height: 48px;
+ --status-height: 24px;
+ }
+
+ .app-shell,
+ .app-shell.is-nav-collapsed,
+ .app-shell.is-context-collapsed,
+ .app-shell.is-nav-collapsed.is-context-collapsed {
+ display: block;
+ height: 100%;
+ }
+
+ .activity-rail,
+ .navigation-pane,
+ .pane-resizer,
+ .context-pane,
+ .segmented-control,
+ #toggleContextButton,
+ #inPageSearchButton,
+ .save-state,
+ .status-bar {
+ display: none !important;
+ }
+
+ .workspace {
+ height: 100%;
+ padding-bottom: calc(58px + env(safe-area-inset-bottom));
+ }
+
+ .workspace-toolbar {
+ padding: 0 8px;
+ }
+
+ .toolbar-leading {
+ flex: 1;
+ }
+
+ .show-nav-button {
+ display: grid !important;
+ }
+
+ .breadcrumb {
+ max-width: calc(100vw - 154px);
+ color: var(--text);
+ font-size: 13px;
+ font-weight: 600;
+ }
+
+ .breadcrumb span:not(:last-child),
+ .breadcrumb svg {
+ display: none;
+ }
+
+ .document-stage,
+ .document-stage[data-mode="read"],
+ .document-stage[data-mode="split"] {
+ display: block;
+ }
+
+ .document-editor {
+ height: 100%;
+ padding: 28px 22px 80px;
+ }
+
+ .document-preview,
+ .split-divider {
+ display: none;
+ }
+
+ .page-title-input {
+ font-size: 26px;
+ line-height: 34px;
+ }
+
+ .page-meta-line {
+ margin-bottom: 28px;
+ }
+
+ .editor-content {
+ font-size: 16px;
+ line-height: 27px;
+ }
+
+ .offline-banner {
+ min-height: 38px;
+ flex-basis: auto;
+ padding: 7px 12px;
+ line-height: 17px;
+ }
+
+ .mobile-navigation {
+ display: grid;
+ position: fixed;
+ z-index: 40;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ grid-template-columns: repeat(5, 1fr);
+ height: calc(58px + env(safe-area-inset-bottom));
+ padding: 4px 8px env(safe-area-inset-bottom);
+ border-top: 1px solid var(--border);
+ background: color-mix(in srgb, var(--editor) 94%, transparent);
+ backdrop-filter: blur(16px);
+ }
+
+ .mobile-navigation button {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ height: 50px;
+ padding: 0;
+ color: var(--muted);
+ background: transparent;
+ cursor: pointer;
+ font-size: 10px;
+ }
+
+ .mobile-navigation button:hover {
+ color: var(--accent);
+ }
+
+ .mobile-navigation svg {
+ width: 19px;
+ height: 19px;
+ }
+
+ .mobile-navigation .mobile-create {
+ align-self: center;
+ justify-self: center;
+ width: 44px;
+ height: 44px;
+ margin-top: -11px;
+ border-radius: 50%;
+ color: var(--accent-contrast);
+ background: var(--accent);
+ box-shadow: 0 5px 14px rgb(31 76 49 / 28%);
+ }
+
+ .mobile-navigation .mobile-create svg {
+ width: 22px;
+ height: 22px;
+ }
+
+ .overlay {
+ align-items: flex-end;
+ padding: 0;
+ }
+
+ .command-dialog,
+ .new-note-dialog {
+ width: 100%;
+ max-height: min(82vh, 680px);
+ border-width: 1px 0 0;
+ border-radius: 8px 8px 0 0;
+ }
+
+ .new-note-dialog {
+ padding-bottom: calc(18px + env(safe-area-inset-bottom));
+ }
+
+ .mobile-sheet-overlay.is-visible {
+ display: flex;
+ }
+
+ .mobile-sheet {
+ width: 100%;
+ max-height: 86vh;
+ padding: 0 12px calc(18px + env(safe-area-inset-bottom));
+ border-top: 1px solid var(--border-strong);
+ border-radius: 8px 8px 0 0;
+ background: var(--navigation);
+ box-shadow: var(--shadow-modal);
+ overflow: auto;
+ }
+
+ .sheet-handle {
+ width: 34px;
+ height: 4px;
+ margin: 7px auto 4px;
+ border-radius: 4px;
+ background: var(--border-strong);
+ }
+
+ .mobile-sheet header {
+ height: 48px;
+ padding: 0 2px 0 5px;
+ }
+
+ .popover-menu {
+ top: 46px;
+ right: 8px;
+ }
+}
+
+@media (max-width: 390px) {
+ .document-editor {
+ padding-right: 18px;
+ padding-left: 18px;
+ }
+
+ .page-meta-line {
+ flex-wrap: wrap;
+ }
+}
+
diff --git a/docs/design-v1/verify-demo.mjs b/docs/design-v1/verify-demo.mjs
new file mode 100644
index 00000000..5125bbf0
--- /dev/null
+++ b/docs/design-v1/verify-demo.mjs
@@ -0,0 +1,85 @@
+import fs from "node:fs/promises";
+import { createRequire } from "node:module";
+
+const require = createRequire(import.meta.url);
+const { chromium } = require("playwright");
+
+const baseUrl = process.env.PLAINLEAF_DEMO_URL || "http://127.0.0.1:4173/";
+const outputDir = "/tmp/plainleaf-design-v1";
+await fs.mkdir(outputDir, { recursive: true });
+
+const browser = await chromium.launch({ headless: true });
+const viewports = [
+ { name: "desktop-1440", width: 1440, height: 900 },
+ { name: "desktop-1024", width: 1024, height: 768 },
+ { name: "mobile-390", width: 390, height: 844 },
+ { name: "mobile-430", width: 430, height: 932 },
+];
+
+const failures = [];
+const results = [];
+
+for (const viewport of viewports) {
+ const page = await browser.newPage({ viewport });
+ const errors = [];
+ page.on("pageerror", (error) => errors.push(error.message));
+ page.on("console", (message) => {
+ if (message.type() === "error") errors.push(message.text());
+ });
+
+ await page.goto(baseUrl, { waitUntil: "networkidle" });
+ await page.waitForSelector("#documentEditor");
+
+ const metrics = await page.evaluate(() => {
+ const visible = (element) => {
+ const style = getComputedStyle(element);
+ const rect = element.getBoundingClientRect();
+ return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
+ };
+ const overflow = [...document.querySelectorAll("button, .pane-header, .workspace-toolbar, .mobile-navigation, .page-title-input")]
+ .filter(visible)
+ .filter((element) => element.scrollWidth > element.clientWidth + 1 || element.scrollHeight > element.clientHeight + 1)
+ .map((element) => ({
+ selector: element.id || element.className || element.tagName,
+ scrollWidth: element.scrollWidth,
+ clientWidth: element.clientWidth,
+ scrollHeight: element.scrollHeight,
+ clientHeight: element.clientHeight,
+ }));
+ const viewportOverflow = document.documentElement.scrollWidth > innerWidth + 1 || document.documentElement.scrollHeight > innerHeight + 1;
+ return { overflow, viewportOverflow, title: document.querySelector("#pageTitleInput")?.value };
+ });
+
+ if (viewport.width > 760) {
+ await page.locator('[data-tool="search"]').click();
+ await page.locator('[data-open-search="Plainleaf"]').click();
+ await page.waitForSelector("#searchOverlay.is-visible");
+ await page.keyboard.press("Escape");
+ await page.locator('[data-tool="files"]').click();
+ await page.locator('[data-note-id="zspace"]').first().click();
+ const title = await page.locator("#pageTitleInput").inputValue();
+ if (title !== "极空间部署") failures.push(`${viewport.name}: note navigation failed`);
+ await page.locator('[data-mode="split"]').click();
+ await page.locator("#themeButton").click();
+ } else {
+ await page.locator('[data-mobile-action="files"]').click();
+ await page.waitForSelector("#mobileSheetOverlay.is-visible");
+ await page.locator('#mobileSheetContent [data-note-id="daily"]').click();
+ const title = await page.locator("#pageTitleInput").inputValue();
+ if (title !== "2026-08-12") failures.push(`${viewport.name}: mobile note navigation failed`);
+ await page.locator('[data-mobile-action="search"]').click();
+ await page.waitForSelector("#searchOverlay.is-visible");
+ await page.keyboard.press("Escape");
+ }
+
+ await page.screenshot({ path: `${outputDir}/${viewport.name}.png`, fullPage: true });
+ if (errors.length) failures.push(`${viewport.name}: ${errors.join(" | ")}`);
+ if (metrics.viewportOverflow) failures.push(`${viewport.name}: viewport overflow`);
+ if (metrics.overflow.length) failures.push(`${viewport.name}: element overflow ${JSON.stringify(metrics.overflow)}`);
+ results.push({ viewport: viewport.name, ...metrics, errors });
+ await page.close();
+}
+
+await browser.close();
+console.log(JSON.stringify({ baseUrl, outputDir, results, failures }, null, 2));
+if (failures.length) process.exit(1);
diff --git a/docs/product-design/decisions-v1.md b/docs/product-design/decisions-v1.md
new file mode 100644
index 00000000..57241047
--- /dev/null
+++ b/docs/product-design/decisions-v1.md
@@ -0,0 +1,56 @@
+# Plainleaf V1 Product Decisions
+
+Status: Accepted defaults for interactive prototype
+
+## Product Position
+
+Plainleaf is a file-native personal knowledge workspace. It keeps
+SilverBullet's Markdown, local-first, programmable foundation and presents it
+through a familiar, discoverable workspace.
+
+The product is inspired by Obsidian's working model, but it is not an Obsidian
+skin. Plainleaf should be easier to approach, quieter during writing, and retain
+SilverBullet's scripting, query, and extension strengths.
+
+## Accepted Decisions
+
+1. **Compact workbench density.** Navigation is compact while document text
+ remains comfortable to read.
+2. **Files-first entry.** The file tree is the default navigation surface.
+ Recent notes, favorites, tags, and tasks remain one click away.
+3. **Single-document V1.** Tabs are deferred until the workspace shell and
+ editor lifecycle are stable.
+4. **Plainleaf product identity.** The prototype uses the Plainleaf name and a
+ restrained leaf-green accent, while upstream attribution remains in About
+ and repository documentation.
+5. **Markdown-first V1.** Plain text and sandboxed HTML editing are phase-two
+ capabilities.
+6. **Native desktop shell, purpose-built mobile shell.** Mobile uses sheets and
+ bottom navigation instead of compressing desktop panes.
+7. **No storage changes.** Ordinary files in the SilverBullet Space remain the
+ source of truth.
+
+## V1 Scope
+
+- Activity rail and persistent file navigation.
+- Slash-path folder tree.
+- Recent and favorite notes.
+- Search and quick switcher.
+- Existing CodeMirror editor in edit, reading, and split presentation modes.
+- Collapsible/resizable navigation and context panes.
+- Outline, backlinks, properties, and page information.
+- Create, rename, move, and delete entry points backed by existing commands.
+- Light and dark themes.
+- Mobile files/search sheets and quick create.
+- Existing command palette, plugs, Lua, queries, offline sync, and external
+ file reconciliation remain available.
+
+## Deferred
+
+- Multi-document tabs.
+- AI features.
+- Plugin marketplace.
+- `.txt` and `.html` editing.
+- Multi-window desktop behavior.
+- A new server, database, or sync model.
+
diff --git a/docs/product-design/design-review-v1.md b/docs/product-design/design-review-v1.md
new file mode 100644
index 00000000..78e4915e
--- /dev/null
+++ b/docs/product-design/design-review-v1.md
@@ -0,0 +1,49 @@
+# Plainleaf V1 Design Review
+
+Status: Approved for interactive prototype
+
+## Designer Review
+
+- **Must fix before core implementation:** Validate long Chinese and English
+ filenames, nested folder depth, small desktop widths, and mobile keyboard
+ viewport behavior in the prototype.
+- **Must fix before core implementation:** Make active, hover, focus, modified,
+ offline, and disabled states visually distinct without relying on color alone.
+- **Recommended:** Keep the editor title at 30px rather than adopting oversized
+ document headings from marketing-oriented interfaces.
+- **Later:** User-selectable density and typography presets.
+
+## Product Review
+
+- **Must fix before core implementation:** New note, search, rename, and delete
+ must be visible actions, not command-palette-only flows.
+- **Must fix before core implementation:** The default launch state must work
+ with an empty Space and with thousands of pages.
+- **Recommended:** Defer tabs. Recent history plus quick switcher is sufficient
+ to validate the workspace model.
+- **Later:** A configurable home/dashboard page.
+
+## Architecture Review
+
+- **Must fix before core implementation:** Do not duplicate `allPages` into a
+ second persistent tree index. Derive a memoized tree view.
+- **Must fix before core implementation:** Keep a single CodeMirror mount in V1.
+ Presentation modes must not create competing editor instances.
+- **Must fix before core implementation:** UI commands must invoke existing
+ navigation and file operations so plugins and events continue to work.
+- **Recommended:** Build the shell from new isolated Preact components and one
+ SCSS entry, minimizing edits to `editor_ui.tsx`.
+- **Later:** Define stable plug extension slots after the shell behavior is
+ validated.
+
+## Prototype Gate
+
+Approved with these checks:
+
+1. Desktop at 1440x900 and 1024x768.
+2. Mobile at 390x844 and 430x932.
+3. File, search, tag, and task rail switching.
+4. Note switching, folder expansion, pane collapse, resizing, and theme change.
+5. No overlapping controls or clipped labels at tested sizes.
+6. Keyboard focus and Escape behavior for overlays.
+
diff --git a/docs/product-design/design-spec-v1.md b/docs/product-design/design-spec-v1.md
new file mode 100644
index 00000000..9e9bff33
--- /dev/null
+++ b/docs/product-design/design-spec-v1.md
@@ -0,0 +1,197 @@
+# Plainleaf Workspace Design Specification V1
+
+Status: Prototype specification
+
+## 1. Experience Principles
+
+1. **Files stay legible.** UI labels and actions should reinforce that the user
+ is working with ordinary files and folders.
+2. **Writing owns the canvas.** Navigation supports the editor and never turns
+ the product into a dashboard of decorative cards.
+3. **Power is progressive.** Basic actions are visible; Lua, queries, commands,
+ and plug surfaces remain available without dominating first use.
+4. **Stable geometry.** Toolbars, panes, counters, and controls use stable
+ dimensions so content changes do not shift the workspace.
+5. **Mobile is for capture, search, and reading first.** Editing remains fully
+ available, but the layout prioritizes common narrow-screen workflows.
+
+## 2. Design System
+
+### Color
+
+Light theme:
+
+| Token | Value | Use |
+| --- | --- | --- |
+| Canvas | `#f7f8f6` | Application background |
+| Editor | `#ffffff` | Document surface |
+| Navigation | `#eef1ee` | Left navigation |
+| Rail | `#202621` | Activity rail |
+| Primary text | `#20241f` | Headings and body |
+| Secondary text | `#687169` | Metadata |
+| Border | `#dce2dc` | Pane and control separation |
+| Accent | `#397453` | Selection, links, primary action |
+| Accent soft | `#dcece2` | Selected navigation row |
+| Informational | `#376d8a` | Links and sync information |
+| Warning | `#a86718` | Unsaved or degraded state |
+| Danger | `#b64a4a` | Destructive actions |
+
+Dark theme:
+
+| Token | Value | Use |
+| --- | --- | --- |
+| Canvas | `#171a18` | Application background |
+| Editor | `#1e221f` | Document surface |
+| Navigation | `#202521` | Left navigation |
+| Rail | `#111411` | Activity rail |
+| Primary text | `#e8ece8` | Headings and body |
+| Secondary text | `#9ba69d` | Metadata |
+| Border | `#343b35` | Pane and control separation |
+| Accent | `#75b18d` | Selection, links, primary action |
+| Accent soft | `#283c30` | Selected navigation row |
+
+Accent green is limited to active state and commands. Blue is reserved for
+links and sync, amber for pending work, and red for destructive or failed
+states. The interface should never read as a monochromatic green theme.
+
+### Typography
+
+- UI: system sans (`-apple-system`, `Segoe UI`, `Noto Sans SC`, sans-serif).
+- Editor: `Inter`, `Noto Sans SC`, system sans by default.
+- Code: `JetBrains Mono`, `SFMono-Regular`, monospace.
+- Navigation: 13px / 20px.
+- Controls: 13px / 18px.
+- Body: 16px / 28px.
+- Page title: 30px / 38px, weight 650.
+- Section heading: 21px / 30px, weight 650.
+- Letter spacing is always `0`.
+
+### Spacing and Geometry
+
+- Base spacing unit: 4px.
+- Activity rail: 48px.
+- Desktop top bar: 44px.
+- Navigation pane: default 272px; range 220-380px.
+- Context pane: default 296px; range 240-380px.
+- Editor readable width: 760px, with 40px minimum lateral padding.
+- Icon buttons: 32x32px; mobile primary create: 44x44px.
+- Control radius: 5px; modal radius: 8px.
+- Borders, not floating cards, define workspace regions.
+
+### Interaction
+
+- Hover: 120ms background/color transition.
+- Pane expansion: 180ms ease-out.
+- Sheet/modal entry: 180ms opacity and translate.
+- Save status changes without moving the title or toolbar.
+- Tooltips appear for icon-only controls after a short hover delay.
+- Focus rings remain visible and use the accent color.
+
+## 3. Navigation Architecture
+
+```text
+Plainleaf
+├── Files
+│ ├── Favorites
+│ ├── Recent
+│ └── Folder tree
+├── Search
+│ ├── Title and content search
+│ └── Saved filters (later)
+├── Tags
+├── Tasks
+├── Graph (existing capability, secondary)
+├── Command palette
+└── Settings
+```
+
+The activity rail changes the navigation pane's content. It does not navigate
+away from the open document.
+
+## 4. Desktop Workspace
+
+### Activity Rail
+
+- Plainleaf mark at the top.
+- Files, Search, Tags, Tasks, and Graph tools.
+- Settings, theme, and account/about actions at the bottom.
+- The active tool has a left indicator and a stronger icon color.
+
+### Navigation Pane
+
+- Header names the current tool and exposes contextual add/more actions.
+- Files view shows quick search, favorites, recent, then the tree.
+- Rows are 28px high. Folder disclosure, icon, label, and optional count have
+ fixed positions.
+- Clicking a note opens it. Double-click behavior is not required in V1.
+- Context menus expose rename, move, duplicate, and delete through existing
+ commands.
+
+### Document Workspace
+
+- Compact toolbar: breadcrumb, save state, presentation mode, search, more.
+- Title is a clear input backed by the existing rename command.
+- Existing CodeMirror is mounted without replacement.
+- Edit is the default. Reading and split are presentation choices, not separate
+ copies of the note.
+- A narrow status row shows words, links, cursor position, Markdown, and sync.
+
+### Context Pane
+
+- Tabs: Outline, Backlinks, Properties, Info.
+- Outline follows document headings and highlights the current section.
+- Backlinks show source page and a short matched excerpt.
+- Properties edit frontmatter through existing editor operations.
+- The pane is optional and hidden at widths below 1100px.
+
+## 5. Mobile Workspace
+
+- Top bar contains back/files, current page title, sync state, and more.
+- Bottom navigation: Files, Search, New, Links, More.
+- Files and search open in full-height sheets above the editor.
+- New creates a note in the current folder when possible, then focuses the
+ editor.
+- Outline, backlinks, properties, and commands live in sheets.
+- Toolbar items remain at least 40px tall and account for safe-area insets.
+
+## 6. States
+
+- **Loading:** stable skeleton rows and document lines; no centered spinner
+ that changes layout.
+- **Empty Space:** tree remains present with a single clear new-note command.
+- **No Search Results:** preserves query and offers create-with-title.
+- **Modified:** amber dot and `Saving...`; resolves to a quiet checkmark.
+- **Offline:** persistent but nonblocking status; edits remain available.
+- **Conflict:** blocks silent overwrite and opens a comparison action.
+- **Read-only:** controls remain visible but disabled with a clear reason.
+
+## 7. Prototype Screens
+
+The interactive prototype must demonstrate:
+
+1. Desktop editor with files and outline.
+2. Desktop dark theme with backlinks.
+3. Search/command overlay.
+4. Mobile editor.
+5. Mobile file sheet.
+6. Empty/offline/save states through interactive controls.
+
+No generated visual assets are required. Product identity is represented by a
+small CSS mark and Lucide icons; the actual application experience is the
+primary visual.
+
+## 8. Engineering Mapping
+
+| Prototype element | SilverBullet integration |
+| --- | --- |
+| File tree | Derive from `AppViewState.allPages` |
+| Active page | `viewState.current` |
+| Open page | `Client.navigate(parseToRef(name))` |
+| Search | Page navigator and object index |
+| Rename | Existing `index.renamePageCommand` |
+| Delete | Existing Space/command flow |
+| Editor | Existing `#sb-editor` CodeMirror mount |
+| Outline/backlinks | Object index/query APIs |
+| Pane state | Existing client store/config |
+| Mobile state | Existing `isMobile` plus CSS media queries |
+