Files
plainleaf/docs/design-v1/app.js
T
wushenghua 7bd42fa546
CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
docs: add interactive Plainleaf workspace demo
2026-08-12 10:52:04 +08:00

549 lines
26 KiB
JavaScript

const notes = [
{
id: "plainleaf-plan",
title: "Plainleaf 改造计划",
path: "项目/Plainleaf/Plainleaf 改造计划",
updated: "今天 10:42",
favorite: true,
tags: ["产品", "设计", "SilverBullet"],
content: `
<p>Plainleaf 会保留 SilverBullet 最重要的基础:每一篇笔记仍然是 NAS 中可以直接打开的 Markdown 文件。</p>
<blockquote>目标不是复制 Obsidian,而是让文件、搜索和上下文工具变得可见,同时保留 SilverBullet 的查询与自动化能力。</blockquote>
<h2>第一阶段目标</h2>
<ul>
<li>建立常驻的文件树与快速搜索。</li>
<li>保留现有 <code>CodeMirror 6</code> 编辑器和实时预览。</li>
<li>增加大纲、反向链接、属性和页面信息。</li>
<li>为手机设计专用的快速记录流程。</li>
</ul>
<h2>数据原则</h2>
<p>界面只是工作层,文件才是长期资产。即使以后停止使用 Plainleaf,也可以通过 <a href="#">Obsidian</a>、VS Code 或普通文本编辑器继续使用全部内容。</p>
<h2>下一步</h2>
<p>交互 Demo 确认后,将工作区外壳接到 SilverBullet 的 <span class="wiki-link">[[页面索引]]</span> 与现有导航命令上。</p>
`,
outline: ["第一阶段目标", "数据原则", "下一步"],
backlinks: [
["产品路线图", "将 Plainleaf 改造计划作为第一阶段的体验基线。"],
["极空间部署", "新版上线时继续挂载同一个 Markdown Space。"],
["笔记工具对比", "最终选择基于 SilverBullet 构建 Plainleaf。"]
]
},
{
id: "zspace",
title: "极空间部署",
path: "知识库/Docker/极空间部署",
updated: "昨天 22:16",
favorite: true,
tags: ["Docker", "NAS", "部署"],
content: `
<p>极空间负责运行正式环境,Mac 用于开发和构建镜像。笔记目录通过卷挂载到容器的 <code>/space</code>。</p>
<h2>部署结构</h2>
<ul><li>固定镜像版本,避免使用 latest。</li><li>笔记目录和应用配置分别备份。</li><li>公网访问统一经过 HTTPS 与反向代理。</li></ul>
<h2>更新流程</h2>
<p>在本地完成构建和验证后,推送自定义镜像,再由极空间 Compose 切换版本。</p>
`,
outline: ["部署结构", "更新流程"],
backlinks: [["Plainleaf 改造计划", "正式版继续使用极空间现有的 Markdown 目录。"]]
},
{
id: "daily",
title: "2026-08-12",
path: "日记/2026/08/2026-08-12",
updated: "今天 08:30",
favorite: false,
tags: ["日记"],
content: `
<p>今天继续整理个人笔记系统,希望数据保持开放,同时让 Web 端变得更易用。</p>
<h2>今日事项</h2>
<ul><li>确认 Plainleaf 的工作区方向。</li><li>完成桌面与手机交互 Demo。</li><li>整理正式开发任务。</li></ul>
<h2>记录</h2>
<p>文件树应该承担定位,而不是让用户记住所有命令。高级能力可以继续通过命令面板进入。</p>
`,
outline: ["今日事项", "记录"],
backlinks: []
},
{
id: "docker-network",
title: "Docker 容器网络",
path: "知识库/Docker/Docker 容器网络",
updated: "8 月 10 日",
favorite: false,
tags: ["Docker", "网络"],
content: `
<p>同一个 Compose 项目中的服务应通过服务名通信,而不是依赖宿主机端口或孤立容器名称。</p>
<h2>服务发现</h2>
<p>容器加入同一网络后,可以使用 <code>service:port</code> 作为内部地址。</p>
<h2>排查顺序</h2>
<ul><li>确认两个服务属于同一 Docker 网络。</li><li>检查服务名能否解析。</li><li>检查目标端口是否监听在 0.0.0.0。</li></ul>
`,
outline: ["服务发现", "排查顺序"],
backlinks: [["极空间部署", "部署时先确认容器网络和服务发现。"]]
},
{
id: "tools",
title: "笔记工具对比",
path: "收件箱/笔记工具对比",
updated: "8 月 11 日",
favorite: false,
tags: ["调研", "笔记"],
content: `
<p>对比了 Blossom、Blinko、Notesnook、SilverBullet 和 Flatnotes。</p>
<h2>核心结论</h2>
<p>数据库优先的产品通常拥有更现代的成品体验,但文件原生模型更符合长期迁移与多工具使用的要求。</p>
<h2>选择</h2>
<p>以 SilverBullet 为文件与能力内核,在其上构建 Plainleaf 的现代工作区。</p>
`,
outline: ["核心结论", "选择"],
backlinks: [["Plainleaf 改造计划", "选型结论推动了 Plainleaf 项目。"]]
},
{
id: "ideas",
title: "产品灵感收件箱",
path: "收件箱/产品灵感收件箱",
updated: "8 月 9 日",
favorite: false,
tags: ["灵感"],
content: `
<p>临时想法先进入收件箱,稍后再归档到项目、知识库或日记。</p>
<h2>待整理</h2>
<ul><li>浏览器剪藏入口。</li><li>手机语音快速记录。</li><li>Markdown 文件历史对比。</li></ul>
`,
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 `<i data-lucide="${name}"${className ? ` class="${className}"` : ""}></i>`;
}
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 `
<div class="tree-row ${note.id === state.currentNoteId ? "is-active" : ""}" data-note-id="${note.id}" style="padding-left:${7 + depth * 15}px">
${icon("chevron-right", "disclosure is-placeholder")}
${icon("file-text")}
<span class="row-label">${note.title}</span>
</div>`;
}
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 `
<div class="tree-folder">
<div class="tree-row" data-folder-path="${path}" style="padding-left:${7 + depth * 15}px">
${icon(expanded ? "chevron-down" : "chevron-right", "disclosure")}
${icon(expanded ? "folder-open" : "folder")}
<span class="row-label">${node.name}</span>
${node.children.length ? `<span class="row-meta">${node.children.length}</span>` : ""}
</div>
<div class="folder-children ${expanded ? "" : "is-collapsed"}">${children}</div>
</div>`;
}
function filesViewMarkup() {
const favoriteRows = notes.filter((note) => note.favorite).map((note) => `
<div class="nav-row" data-note-id="${note.id}">
${icon("star")}
<span class="row-label">${note.title}</span>
</div>`).join("");
const recentRows = notes.slice(0, 3).map((note) => `
<div class="nav-row" data-note-id="${note.id}">
${icon("history")}
<span class="row-label">${note.title}</span>
<span class="row-meta">${note.updated.split(" ")[0]}</span>
</div>`).join("");
return `
<section class="nav-section">
<div class="section-heading"><span>收藏</span><button title="管理收藏" aria-label="管理收藏">${icon("ellipsis")}</button></div>
${favoriteRows}
</section>
<section class="nav-section">
<div class="section-heading"><span>最近</span><button title="清除最近记录" aria-label="清除最近记录">${icon("rotate-ccw")}</button></div>
${recentRows}
</section>
<section class="nav-section">
<div class="section-heading"><span>文件</span><button id="inlineNewNote" title="新建笔记" aria-label="新建笔记">${icon("plus")}</button></div>
<div class="file-tree">${tree.map((node) => renderTreeNode(node)).join("")}</div>
</section>`;
}
function searchViewMarkup() {
return `
<section class="nav-section">
<div class="section-heading"><span>搜索建议</span></div>
${["最近修改的笔记", "包含待办的页面", "没有标签的笔记", "本周创建的页面"].map((label, index) => `
<div class="nav-row" data-open-search="${label}">
${icon(index === 0 ? "clock-3" : index === 1 ? "list-checks" : index === 2 ? "tag" : "calendar-days")}
<span class="row-label">${label}</span>
</div>`).join("")}
</section>
<section class="nav-section">
<div class="section-heading"><span>搜索历史</span></div>
${["Docker", "Plainleaf", "部署"].map((label) => `<div class="nav-row" data-open-search="${label}">${icon("search")}<span class="row-label">${label}</span></div>`).join("")}
</section>`;
}
function tagsViewMarkup() {
const tags = [["Docker", 3], ["产品", 2], ["设计", 2], ["NAS", 2], ["日记", 1], ["灵感", 1], ["部署", 1]];
return `<section class="nav-section"><div class="section-heading"><span>所有标签</span><span>${tags.length}</span></div>${tags.map(([name, count]) => `<div class="tag-row">${icon("hash")}<span class="row-label">${name}</span><span class="row-meta">${count}</span></div>`).join("")}</section>`;
}
function tasksViewMarkup() {
const tasks = [["完成交互 Demo", false, "今天"], ["确认文件树布局", true, "今天"], ["整理开发任务", false, "明天"], ["构建极空间镜像", false, "稍后"]];
return `<section class="nav-section"><div class="section-heading"><span>开放任务</span><span>3</span></div>${tasks.map(([name, done, date]) => `<div class="task-row">${icon(done ? "circle-check-big" : "circle")}<span class="row-label" style="${done ? "text-decoration:line-through;opacity:.65" : ""}">${name}</span><span class="row-meta">${date}</span></div>`).join("")}</section>`;
}
function graphViewMarkup() {
return `<div class="empty-state">${icon("network")}<h3>关系图保留为高级视图</h3><p>正式实现将复用 SilverBullet 现有对象索引和关系图能力。</p></div>`;
}
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") : ""}<span>${segment}</span>`).join("");
}
function renderContext() {
const note = currentNote();
const heading = { outline: "大纲", backlinks: "反向链接", properties: "属性", info: "页面信息" }[state.context];
let body = "";
if (state.context === "outline") {
body = `<div class="outline-list">${note.outline.length ? note.outline.map((item, index) => `<button class="outline-item ${index === 0 ? "is-active" : ""}" type="button">${item}</button>`).join("") : `<div class="empty-state">${icon("list-tree")}<h3>没有大纲</h3><p>添加二级或三级标题后会显示在这里。</p></div>`}</div>`;
} else if (state.context === "backlinks") {
body = `<div class="backlink-list">${note.backlinks.length ? note.backlinks.map(([source, excerpt]) => `<article class="backlink-item"><div class="backlink-source">${icon("link-2")}<span>${source}</span></div><p class="backlink-excerpt">${excerpt}</p></article>`).join("") : `<div class="empty-state">${icon("links")}<h3>还没有反向链接</h3><p>其他页面引用当前笔记后会显示在这里。</p></div>`}</div>`;
} else if (state.context === "properties") {
body = `<dl class="property-list"><div class="property-row"><dt>tags</dt><dd>${note.tags.map((tag) => `<span class="tag-chip">${tag}</span>`).join("")}</dd></div><div class="property-row"><dt>status</dt><dd>active</dd></div><div class="property-row"><dt>created</dt><dd>2026-08-12</dd></div><div class="property-row"><dt>aliases</dt><dd>暂无</dd></div></dl>`;
} else {
body = `<dl class="info-list"><div class="info-row"><dt>文件</dt><dd>${note.path}.md</dd></div><div class="info-row"><dt>修改</dt><dd>${note.updated}</dd></div><div class="info-row"><dt>编码</dt><dd>UTF-8</dd></div><div class="info-row"><dt>大小</dt><dd>3.4 KB</dd></div><div class="info-row"><dt>权限</dt><dd>读写</dd></div></dl>`;
}
byId("contextContent").innerHTML = `<h2 class="context-title">${heading}</h2>${body}`;
refreshIcons();
}
function renderPreview(note) {
byId("documentPreview").innerHTML = `<h1>${note.title}</h1>${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")}<span id="connectionLabel">${offline ? "离线" : "已连接"}</span>`;
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")}<span>保存中</span>`;
refreshIcons();
saveTimer = setTimeout(() => {
byId("saveState").className = "save-state is-saved";
byId("saveState").innerHTML = `${icon("check")}<span>已保存</span>`;
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) => `
<div class="search-result ${index === state.searchIndex ? "is-selected" : ""}" data-search-note="${note.id}">
<div class="search-result-icon">${icon("file-text")}</div>
<div class="search-result-copy"><div class="search-result-title">${note.title}</div><div class="search-result-path">${note.path}.md</div></div>
<span class="row-meta">${note.updated}</span>
</div>`).join("") : `<div class="empty-state">${icon("search-x")}<h3>没有找到笔记</h3><p>保留当前搜索词,可以直接创建同名 Markdown 文件。</p></div>`;
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: `<p>开始记录...</p>`,
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 ? `<div class="backlink-list">${note.backlinks.map(([source, excerpt]) => `<article class="backlink-item"><div class="backlink-source">${icon("link-2")}<span>${source}</span></div><p class="backlink-excerpt">${excerpt}</p></article>`).join("")}</div>` : `<div class="empty-state">${icon("links")}<h3>还没有反向链接</h3><p>其他页面引用当前笔记后会显示在这里。</p></div>`;
} else {
byId("mobileSheetContent").innerHTML = `<div class="file-tree">${tree.map((node) => renderTreeNode(node)).join("")}</div>`;
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();