Notes
GLM 5.2 · middle

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 56,397 bytes · SHA-256 e85c19776828
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vellum — Notes</title>
<style>
/* ============ RESET & BASE ============ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--text);
background: var(--bg);
font-size: 15px;
line-height: 1.6;
}
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
input { font-family: inherit; }
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scroll); border-radius: 6px; border: 2px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: var(--scroll-hover); background-clip: padding-box; }
/* ============ THEME ============ */
:root {
--bg: #ffffff;
--bg-list: #fbfbfa;
--bg-sidebar: #f6f5f2;
--bg-hover: #eeede9;
--bg-active: #e7e5df;
--bg-elevated: #ffffff;
--border: #eceae5;
--border-strong: #dbd8d1;
--text: #2f2e2c;
--text-secondary: #6f6d68;
--text-tertiary: #a3a09a;
--accent: #6d5efc;
--accent-hover: #5a49f0;
--accent-soft: #ece8ff;
--accent-text: #ffffff;
--star: #f0b400;
--danger: #e5484d;
--danger-soft: #fde8e8;
--scroll: #d8d5cf;
--scroll-hover: #c2beb6;
--code-bg: #f3f2ef;
--shadow: 0 4px 14px rgba(20,18,15,.08);
--shadow-lg: 0 12px 40px rgba(20,18,15,.14);
--radius: 9px;
--radius-sm: 6px;
--side-w: 252px;
--list-w: 320px;
}
[data-theme="dark"] {
--bg: #1c1b1a;
--bg-list: #20201f;
--bg-sidebar: #171716;
--bg-hover: #2a2928;
--bg-active: #34322f;
--bg-elevated: #28272a;
--border: #2c2b2a;
--border-strong: #3a3937;
--text: #e8e6e2;
--text-secondary: #9c9a94;
--text-tertiary: #6a6863;
--accent: #9381ff;
--accent-hover: #a594ff;
--accent-soft: #2b2540;
--star: #ffce4d;
--danger: #ff6166;
--danger-soft: #3a2326;
--scroll: #3a3937;
--scroll-hover: #4a4946;
--code-bg: #2a292b;
--shadow: 0 4px 14px rgba(0,0,0,.4);
--shadow-lg: 0 12px 40px rgba(0,0,0,.55);
}
/* ============ LAYOUT ============ */
.app { display: flex; height: 100vh; width: 100vw; }
/* ============ SIDEBAR ============ */
.sidebar {
width: var(--side-w);
flex: 0 0 var(--side-w);
background: var(--bg-sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: margin-left .28s cubic-bezier(.4,0,.2,1);
z-index: 40;
}
.sidebar-head {
display: flex; align-items: center; gap: 8px;
padding: 16px 16px 12px;
}
.brand { display: flex; align-items: center; gap: 9px; flex: 1; min-width: 0; }
.brand-logo {
width: 26px; height: 26px; border-radius: 7px;
background: linear-gradient(135deg, var(--accent), #b06bff);
display: grid; place-items: center; flex: 0 0 auto;
box-shadow: 0 2px 6px rgba(109,94,252,.4);
}
.brand-logo svg { width: 15px; height: 15px; color: #fff; }
.brand-name { font-weight: 700; font-size: 15px; letter-spacing: -.2px; }
.icon-btn {
width: 30px; height: 30px; border-radius: 7px; flex: 0 0 auto;
display: grid; place-items: center; color: var(--text-secondary);
transition: background .15s, color .15s;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text); }
.icon-btn svg { width: 17px; height: 17px; }
.search-wrap { padding: 0 12px 8px; position: relative; }
.search-input {
width: 100%; height: 34px; border-radius: 8px;
background: var(--bg-hover); border: 1px solid transparent;
padding: 0 12px 0 34px; font-size: 13.5px; color: var(--text);
outline: none; transition: border .15s, background .15s;
}
.search-input::placeholder { color: var(--text-tertiary); }
.search-input:focus { background: var(--bg-elevated); border-color: var(--accent); }
.search-wrap > svg { position: absolute; left: 24px; top: 50%; transform: translateY(-50%); width: 15px; height: 15px; color: var(--text-tertiary); pointer-events: none; }
.side-scroll { flex: 1; overflow-y: auto; padding-bottom: 8px; }
.new-note-btn {
margin: 4px 12px 6px; height: 36px; border-radius: 8px;
background: var(--accent); color: var(--accent-text);
display: flex; align-items: center; gap: 8px; padding: 0 12px;
font-size: 13.5px; font-weight: 600;
transition: background .15s, transform .08s;
}
.new-note-btn:hover { background: var(--accent-hover); }
.new-note-btn:active { transform: scale(.98); }
.new-note-btn svg { width: 16px; height: 16px; }
.nav-section { padding: 6px 8px 2px; }
.section-head {
display: flex; align-items: center; gap: 4px; width: 100%;
padding: 6px 8px; border-radius: 6px; color: var(--text-tertiary);
font-size: 11.5px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
}
.section-head .label { flex: 1; text-align: left; }
.caret { width: 14px; height: 14px; transition: transform .18s; flex: 0 0 auto; opacity: .7; }
.section-head.collapsed .caret { transform: rotate(-90deg); }
.add-mini {
width: 22px; height: 22px; border-radius: 5px; flex: 0 0 auto;
display: grid; place-items: center; opacity: 0; transition: opacity .15s, background .15s;
}
.nav-section:hover .add-mini { opacity: 1; }
.add-mini:hover { background: var(--bg-hover); color: var(--text); }
.add-mini svg { width: 14px; height: 14px; }
.section-items { overflow: hidden; transition: max-height .25s ease; }
.section-items.hidden { display: none; }
.nav-item {
display: flex; align-items: center; gap: 9px; width: 100%;
padding: 6px 8px; border-radius: 7px; font-size: 13.5px; color: var(--text);
position: relative; transition: background .12s; user-select: none;
}
.nav-item:hover { background: var(--bg-hover); }
.nav-item.active { background: var(--bg-active); font-weight: 600; }
.nav-item .ni-icon { width: 16px; height: 16px; flex: 0 0 auto; color: var(--text-secondary); }
.nav-item.active .ni-icon { color: var(--accent); }
.nav-item .ni-label { flex: 1; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item .ni-count { font-size: 11.5px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
.nav-item .ni-del {
position: absolute; right: 6px; width: 20px; height: 20px; border-radius: 5px;
display: none; place-items: center; color: var(--text-tertiary);
}
.nav-item:hover .ni-del { display: grid; }
.nav-item:hover .ni-count { display: none; }
.nav-item .ni-del:hover { background: var(--danger-soft); color: var(--danger); }
.nav-item .ni-del svg { width: 14px; height: 14px; }
.tag-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; margin-left: 3px; }
.folder-input {
width: 100%; height: 30px; border-radius: 6px; padding: 0 10px;
background: var(--bg-elevated); border: 1px solid var(--accent); font-size: 13px; color: var(--text); outline: none;
}
/* ============ NOTE LIST ============ */
.note-list {
width: var(--list-w); flex: 0 0 var(--list-w);
background: var(--bg-list); border-right: 1px solid var(--border);
display: flex; flex-direction: column; transition: margin-left .28s cubic-bezier(.4,0,.2,1);
}
.list-head {
display: flex; align-items: center; gap: 6px; padding: 16px 16px 10px;
}
.list-title { font-size: 17px; font-weight: 700; letter-spacing: -.3px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-meta { font-size: 12px; color: var(--text-tertiary); font-weight: 500; margin-top: 1px; }
.sort-row { display: flex; align-items: center; gap: 4px; padding: 0 12px 8px; }
.sort-btn {
height: 26px; padding: 0 9px; border-radius: 6px; font-size: 12px; color: var(--text-secondary);
display: flex; align-items: center; gap: 5px; transition: background .12s, color .12s;
}
.sort-btn:hover { background: var(--bg-hover); color: var(--text); }
.sort-btn svg { width: 13px; height: 13px; }
.notes-scroll { flex: 1; overflow-y: auto; padding: 0 8px 16px; }
.note-card {
padding: 11px 12px; border-radius: 9px; cursor: pointer; position: relative;
transition: background .12s; border: 1px solid transparent;
}
.note-card + .note-card { margin-top: 2px; }
.note-card:hover { background: var(--bg-hover); }
.note-card.active { background: var(--bg-elevated); border-color: var(--border-strong); box-shadow: var(--shadow); }
.nc-top { display: flex; align-items: baseline; gap: 7px; margin-bottom: 4px; }
.nc-title { font-size: 14px; font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.nc-pin { width: 13px; height: 13px; color: var(--star); flex: 0 0 auto; }
.nc-snippet { font-size: 12.5px; color: var(--text-secondary); line-height: 1.5; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.nc-snippet em { color: var(--accent); font-style: normal; font-weight: 600; background: var(--accent-soft); padding: 0 2px; border-radius: 3px; }
.nc-foot { display: flex; align-items: center; gap: 6px; margin-top: 7px; }
.nc-date { font-size: 11px; color: var(--text-tertiary); }
.nc-tags { display: flex; gap: 4px; overflow: hidden; }
.nc-tag { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: var(--bg-active); color: var(--text-secondary); white-space: nowrap; }
.empty-list { padding: 50px 24px; text-align: center; color: var(--text-tertiary); }
.empty-list svg { width: 40px; height: 40px; margin-bottom: 14px; opacity: .5; }
.empty-list h4 { font-size: 14px; color: var(--text-secondary); font-weight: 600; margin-bottom: 5px; }
.empty-list p { font-size: 12.5px; line-height: 1.5; }
/* ============ EDITOR ============ */
.editor {
flex: 1; min-width: 0; background: var(--bg);
display: flex; flex-direction: column; position: relative;
}
.editor-empty {
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: var(--text-tertiary); gap: 18px; padding: 40px;
}
.editor-empty .big-logo {
width: 64px; height: 64px; border-radius: 18px;
background: linear-gradient(135deg, var(--accent-soft), transparent);
border: 1px solid var(--border); display: grid; place-items: center;
}
.editor-empty .big-logo svg { width: 30px; height: 30px; color: var(--accent); }
.editor-empty h2 { font-size: 19px; color: var(--text-secondary); font-weight: 700; }
.editor-empty p { font-size: 13.5px; max-width: 300px; text-align: center; line-height: 1.6; }
.editor-empty kbd { font-family: inherit; background: var(--bg-hover); border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: 5px; padding: 1px 7px; font-size: 11.5px; color: var(--text-secondary); }
.editor-topbar {
display: flex; align-items: center; gap: 4px; padding: 10px 14px;
border-bottom: 1px solid var(--border); flex: 0 0 auto;
}
.breadcrumb { flex: 1; display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-tertiary); min-width: 0; }
.breadcrumb .crumb { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.breadcrumb svg { width: 12px; height: 12px; opacity: .6; flex: 0 0 auto; }
.topbar-btn { height: 30px; min-width: 30px; padding: 0 8px; border-radius: 7px; display: flex; align-items: center; gap: 6px; color: var(--text-secondary); font-size: 12.5px; transition: background .12s, color .12s; }
.topbar-btn:hover { background: var(--bg-hover); color: var(--text); }
.topbar-btn.on { color: var(--star); }
.topbar-btn svg { width: 16px; height: 16px; }
.format-bar {
display: flex; align-items: center; gap: 1px; padding: 7px 16px; flex-wrap: wrap;
border-bottom: 1px solid var(--border); flex: 0 0 auto; position: sticky; top: 0; background: var(--bg); z-index: 5;
}
.fmt-btn {
height: 30px; min-width: 30px; padding: 0 7px; border-radius: 6px;
display: grid; place-items: center; color: var(--text-secondary);
transition: background .12s, color .12s; font-size: 12.5px;
}
.fmt-btn:hover { background: var(--bg-hover); color: var(--text); }
.fmt-btn.active { background: var(--accent-soft); color: var(--accent); }
.fmt-btn svg { width: 16px; height: 16px; }
.fmt-divider { width: 1px; height: 18px; background: var(--border); margin: 0 5px; }
.editor-scroll { flex: 1; overflow-y: auto; }
.editor-paper {
max-width: 720px; margin: 0 auto; padding: 44px 56px 160px;
}
.note-title {
font-size: 32px; font-weight: 800; letter-spacing: -.6px; line-height: 1.2;
color: var(--text); outline: none; word-break: break-word; margin-bottom: 4px;
}
.note-title[data-empty="true"]:before {
content: attr(data-placeholder); color: var(--text-tertiary); pointer-events: none;
}
.note-body {
outline: none; min-height: 60vh; font-size: 16px; line-height: 1.75; color: var(--text);
}
.note-body[data-empty="true"]:before {
content: attr(data-placeholder); color: var(--text-tertiary); pointer-events: none;
}
.note-body.serif { font-family: "Iowan Old Style", "Apple Garamond", Georgia, "Times New Roman", serif; font-size: 17px; }
/* editor content styles */
.note-body > *:first-child { margin-top: 0; }
.note-body h1 { font-size: 26px; font-weight: 800; letter-spacing: -.4px; margin: 28px 0 8px; line-height: 1.25; }
.note-body h2 { font-size: 21px; font-weight: 700; letter-spacing: -.3px; margin: 24px 0 6px; line-height: 1.3; }
.note-body h3 { font-size: 17px; font-weight: 700; margin: 20px 0 4px; }
.note-body p { margin: 0 0 4px; }
.note-body ul, .note-body ol { margin: 6px 0 10px; padding-left: 26px; }
.note-body li { margin: 2px 0; }
.note-body li::marker { color: var(--accent); }
.note-body blockquote {
border-left: 3px solid var(--accent); padding: 4px 0 4px 16px; margin: 10px 0;
color: var(--text-secondary); font-style: italic;
}
.note-body code {
font-family: "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
font-size: .86em; background: var(--code-bg); padding: 2px 6px; border-radius: 5px;
border: 1px solid var(--border);
}
.note-body pre {
background: var(--code-bg); border: 1px solid var(--border); border-radius: 10px;
padding: 14px 16px; margin: 12px 0; overflow-x: auto; line-height: 1.6;
}
.note-body pre code { background: none; border: none; padding: 0; font-size: 13px; }
.note-body a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1px; }
.note-body hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
.note-body input[type="checkbox"] { margin-right: 7px; transform: translateY(1px); accent-color: var(--accent); }
.note-body b, .note-body strong { font-weight: 700; }
.status-bar {
display: flex; align-items: center; gap: 14px; padding: 8px 24px;
border-top: 1px solid var(--border); font-size: 11.5px; color: var(--text-tertiary);
flex: 0 0 auto; background: var(--bg);
}
.status-bar .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--text-tertiary); opacity: .5; }
.status-bar .saved { color: var(--accent); }
/* dropdown menu */
.menu {
position: fixed; background: var(--bg-elevated); border: 1px solid var(--border-strong);
border-radius: 10px; box-shadow: var(--shadow-lg); padding: 5px; z-index: 100; min-width: 180px;
animation: pop .12s ease;
}
@keyframes pop { from { opacity: 0; transform: scale(.96) translateY(-4px); } to { opacity: 1; transform: scale(1); } }
.menu-item { display: flex; align-items: center; gap: 9px; padding: 8px 10px; border-radius: 6px; font-size: 13px; width: 100%; text-align: left; color: var(--text); transition: background .1s; }
.menu-item:hover { background: var(--bg-hover); }
.menu-item.danger { color: var(--danger); }
.menu-item.danger:hover { background: var(--danger-soft); }
.menu-item svg { width: 15px; height: 15px; flex: 0 0 auto; }
.menu-item .shortcut { margin-left: auto; font-size: 11px; color: var(--text-tertiary); }
.menu-sep { height: 1px; background: var(--border); margin: 4px 2px; }
/* toast */
.toast {
position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%) translateY(20px);
background: var(--text); color: var(--bg); padding: 10px 18px; border-radius: 9px;
font-size: 13px; font-weight: 500; box-shadow: var(--shadow-lg); z-index: 200;
opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; display: flex; align-items: center; gap: 10px;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast button { color: var(--accent); font-weight: 700; font-size: 13px; }
/* mobile toggle */
.menu-toggle { display: none; }
.sidebar-scrim { display: none; }
@media (max-width: 900px) {
.sidebar { position: fixed; left: 0; top: 0; bottom: 0; margin-left: calc(-1 * var(--side-w)); box-shadow: var(--shadow-lg); }
.sidebar.open { margin-left: 0; }
.sidebar-scrim.show { display: block; position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 35; }
.menu-toggle { display: grid; }
}
@media (max-width: 640px) {
.note-list { position: fixed; left: 0; top: 0; bottom: 0; z-index: 30; width: 100%; flex: none; margin-left: 0; }
.note-list.hide-mobile { margin-left: -100%; }
.editor-paper { padding: 28px 22px 140px; }
:root { --side-w: 230px; }
}
</style>
</head>
<body>
<div class="app">
<!-- ===== SIDEBAR ===== -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-head">
<div class="brand">
<div class="brand-logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h12l4 4v12H4z"/><path d="M8 9h6M8 13h8M8 17h5"/></svg>
</div>
<span class="brand-name">Vellum</span>
</div>
<button class="icon-btn" id="themeToggle" title="Toggle theme">
<svg id="themeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></svg>
</button>
</div>
<div class="search-wrap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input class="search-input" id="searchInput" placeholder="Search notes…" />
</div>
<button class="new-note-btn" id="newNoteBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
New Note
</button>
<div class="side-scroll">
<div class="nav-section" id="smartSection">
<div id="smartItems"></div>
</div>
<div class="nav-section" data-collapsible="folders">
<button class="section-head" data-toggle="folders">
<svg class="caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
<span class="label">Folders</span>
<span class="add-mini" data-add="folder" title="New folder">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
</span>
</button>
<div class="section-items" id="folderItems"></div>
</div>
<div class="nav-section" data-collapsible="tags">
<button class="section-head" data-toggle="tags">
<svg class="caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
<span class="label">Tags</span>
<span class="add-mini" data-add="tag" title="New tag">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
</span>
</button>
<div class="section-items" id="tagItems"></div>
</div>
</div>
</aside>
<div class="sidebar-scrim" id="scrim"></div>
<!-- ===== NOTE LIST ===== -->
<section class="note-list" id="noteList">
<div class="list-head">
<button class="icon-btn menu-toggle" id="menuToggle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<div style="flex:1;min-width:0">
<div class="list-title" id="listTitle">All Notes</div>
<div class="list-meta" id="listMeta"></div>
</div>
<button class="icon-btn" id="sortBtn" title="Sort">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h13M3 12h9M3 18h5M17 16l3 3 3-3M20 5v14"/></svg>
</button>
</div>
<div class="notes-scroll" id="notesScroll"></div>
</section>
<!-- ===== EDITOR ===== -->
<main class="editor" id="editorPane">
<div id="editorContent"></div>
<div class="status-bar" id="statusBar" style="display:none"></div>
</main>
</div>
<div class="toast" id="toast"></div>
<script>
"use strict";
/* ============ STORAGE & STATE ============ */
const KEY = 'vellum_notes_v2';
const TAG_COLORS = ['#e57373','#f0b400','#7cb342','#26a69a','#42a5f5','#ab47bc','#ec407a','#8d6e63','#78909c','#ff7043'];
function uid(){ return Date.now().toString(36)+Math.random().toString(36).slice(2,7); }
function defaultData(){
const now = Date.now();
const fPersonal = uid(), fWork = uid(), fIdeas = uid();
const t1 = uid(), t2 = uid(), t3 = uid();
return {
theme: 'light',
collapsed: { folders:false, tags:false },
sort: 'updated',
folders: [
{ id:fPersonal, name:'Personal', color:'#26a69a' },
{ id:fWork, name:'Work', color:'#42a5f5' },
{ id:fIdeas, name:'Ideas', color:'#ab47bc' },
],
tags: [
{ id:t1, name:'important', color:'#e57373' },
{ id:t2, name:'reading', color:'#7cb342' },
{ id:t3, name:'quick', color:'#f0b400' },
],
view: { type:'all' },
activeNoteId: null,
notes: [
{
id: uid(), folderId: fPersonal, title:'Welcome to Vellum ✨', pinned:true, trashed:false, serif:false,
tags:[t1], createdAt: now-86400000*3, updatedAt: now-3600000,
body: `<p>This is your space to think, write, and organize. Vellum is a calm, fast notebook inspired by the best of Obsidian, Notion, and Bear.</p>
<h2>Getting around</h2>
<ul>
<li><b>Three panels</b> — navigate folders on the left, browse notes in the middle, write on the right.</li>
<li>Click <b>New Note</b> or press <code>Ctrl/⌘ + N</code> to start writing.</li>
<li>Search everything instantly from the top of the sidebar.</li>
</ul>
<h2>Formatting</h2>
<p>Use the toolbar above, or try markdown shortcuts:</p>
<ul>
<li>Type <code># </code> <code>## </code> <code>### </code> for headings</li>
<li><code>- </code> or <code>* </code> for a bullet list</li>
<li><code>> </code> for a quote</li>
<li><code>\`\`\`</code> for a code block</li>
<li><code>1. </code> for a numbered list</li>
</ul>
<blockquote>The palest ink is better than the best memory.</blockquote>
<p>Everything saves automatically to your browser. Enjoy writing! 🖋️</p>`
},
{
id: uid(), folderId: fWork, title:'Q4 Product Roadmap', pinned:false, trashed:false, serif:false,
tags:[t1,t2], createdAt: now-86400000*5, updatedAt: now-86400000*2,
body:`<h1>Q4 Roadmap</h1><p>Key initiatives for the quarter:</p><ol><li>Ship the new editor <b>(in progress)</b></li><li>Mobile app beta</li><li>Offline sync overhaul</li></ol><h3>Risks</h3><ul><li>Timeline pressure on sync</li><li>Hiring bandwidth</li></ul><blockquote>Prioritize the editor launch above all else.</blockquote>`
},
{
id: uid(), folderId: fIdeas, title:'App idea: a quiet garden', pinned:false, trashed:false, serif:true,
tags:[t3], createdAt: now-86400000*8, updatedAt: now-86400000*4,
body:`<p>A plant-care app that's deliberately slow. No streaks, no notifications shouting at you. Just a gentle, living garden that grows as you remember to water it.</p><h2>Why</h2><p>Most habit apps feel like performance reviews. Plants deserve patience — and so do people.</p>`
},
{
id: uid(), folderId: fPersonal, title:'Books to read', pinned:false, trashed:false, serif:false,
tags:[t2], createdAt: now-86400000*10, updatedAt: now-86400000*6,
body:`<h2>Reading list</h2><ul><li>The Beginning of Infinity — David Deutsch</li><li>Four Thousand Weeks — Oliver Burkeman</li><li>Piranesi — Susanna Clarke</li></ul>`
},
]
};
}
let state;
function load(){
try {
const raw = localStorage.getItem(KEY);
state = raw ? JSON.parse(raw) : defaultData();
} catch(e){ state = defaultData(); }
if(!state.notes) state = defaultData();
}
let saveTimer;
function save(){
clearTimeout(saveTimer);
saveTimer = setTimeout(()=>{ localStorage.setItem(KEY, JSON.stringify(state)); }, 350);
}
/* ============ HELPERS ============ */
const $ = s => document.querySelector(s);
const icons = {
search:'<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
notes:'<path d="M4 4h12l4 4v12H4z"/><path d="M8 9h6M8 13h8M8 17h5"/>',
star:'<path d="M12 2.5 14.9 8.6l6.6.9-4.8 4.6 1.2 6.6L12 17.8 6.1 20.7l1.2-6.6L2.5 9.5l6.6-.9z"/>',
trash:'<path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/>',
folder:'<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
tag:'<path d="M20.6 13.4 13 21l-9-9V4h8z"/><circle cx="7.5" cy="7.5" r="1.3"/>',
pin:'<path d="M9 4h6l-1 6 4 3v2h-5v5l-1 1-1-1v-5H6v-2l4-3z"/>',
sun:'<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19"/>',
moon:'<path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/>',
plus:'<path d="M12 5v14M5 12h14"/>',
sort:'<path d="M3 6h13M3 12h9M3 18h5M17 16l3 3 3-3M20 5v14"/>',
bold:'<path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/>',
italic:'<path d="M19 5h-6M11 19H5M15 5 9 19"/>',
underline:'<path d="M7 4v7a5 5 0 0 0 10 0V4M5 21h14"/>',
strike:'<path d="M4 12h16M9 5.5A5 5 0 0 1 16 8M9 18.5A5 5 0 0 0 16 16"/>',
h1:'<path d="M4 5v14M14 5v14M4 12h10M18 12l3-1v8"/>',
h2:'<path d="M4 5v14M14 5v14M4 12h10M17 9a2.5 2.5 0 1 1 4.5 1.5L17 19h5"/>',
quote:'<path d="M7 7H4v4h4l-1 4M17 7h-3v4h4l-1 4"/>',
code:'<path d="m9 8-4 4 4 4M15 8l4 4-4 4"/>',
list:'<path d="M8 6h12M8 12h12M8 18h12M3.5 6h.01M3.5 12h.01M3.5 18h.01"/>',
num:'<path d="M9 6h12M9 12h12M9 18h12M4 5l-1 1 1 1M3 11h2v3"/>',
link:'<path d="M10 13a4 4 0 0 0 5.7 0l3-3a4 4 0 1 0-5.7-5.7L11 6M14 11a4 4 0 0 0-5.7 0l-3 3a4 4 0 1 0 5.7 5.7L13 18"/>',
check:'<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
del:'<path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/>',
copy:'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h8"/>',
restore:'<path d="M3 12a9 9 0 1 0 3-6.7L3 8M3 4v4h4"/>',
pen:'<path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>',
empty:'<path d="M14 3v5h5M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>',
chevron:'<path d="m9 6 6 6-6 6"/>',
};
function svg(name, sw=2){ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">${icons[name]||''}</svg>`; }
function getNote(id){ return state.notes.find(n=>n.id===id); }
function folderName(id){ const f=state.folders.find(x=>x.id===id); return f?f.name:'Unsorted'; }
function tagById(id){ return state.tags.find(t=>t.id===id); }
function tagName(id){ const t=tagById(id); return t?t.name:''; }
function tagColor(id){ const t=tagById(id); return t?t.color:'#999'; }
function folderColor(id){ const f=state.folders.find(x=>x.id===id); return f?f.color:'#78909c'; }
function relativeDate(ts){
const d = new Date(ts), now = new Date();
const diff = now - d;
const day = 86400000;
if (diff < 60000) return 'just now';
if (diff < 3600000) return Math.floor(diff/60000)+'m ago';
if (d.toDateString() === now.toDateString()) return 'Today';
if (d.toDateString() === new Date(now-day).toDateString()) return 'Yesterday';
if (diff < day*7) return Math.floor(diff/day)+'d ago';
return d.toLocaleDateString(undefined,{month:'short',day:'numeric'});
}
function plainText(html){
const tmp = document.createElement('div');
tmp.innerHTML = html || '';
return (tmp.textContent || '').replace(/\s+/g,' ').trim();
}
function snippet(html, n=110){
const t = plainText(html);
return t.length>n ? t.slice(0,n)+'…' : t;
}
/* ============ VIEW FILTERING ============ */
function visibleNotes(){
let list = state.notes.filter(n=>!n.trashed);
const v = state.view;
if (v.type==='folder') list = list.filter(n=>n.folderId===v.id);
else if (v.type==='tag') list = list.filter(n=>n.tags.includes(v.id));
else if (v.type==='favorites') list = list.filter(n=>n.pinned);
const q = (state.search||'').toLowerCase().trim();
if (q){
list = list.filter(n=>{
const hay = (n.title+' '+plainText(n.body)+' '+n.tags.map(tagName).join(' ')).toLowerCase();
return hay.includes(q);
});
}
// sort
list.sort((a,b)=>{
if (a.pinned !== b.pinned) return a.pinned? -1:1;
if (state.sort==='created') return b.createdAt-a.createdAt;
if (state.sort==='title') return (a.title||'Untitled').localeCompare(b.title||'Untitled');
return b.updatedAt-a.updatedAt;
});
return list;
}
function trashNotes(){ return state.notes.filter(n=>n.trashed).sort((a,b)=>b.updatedAt-a.updatedAt); }
/* ============ RENDER SIDEBAR ============ */
function renderSidebar(){
const total = state.notes.filter(n=>!n.trashed).length;
const fav = state.notes.filter(n=>!n.trashed && n.pinned).length;
const trash = state.notes.filter(n=>n.trashed).length;
const smart = $('#smartItems');
smart.innerHTML = `
${navRow('all','notes','All Notes',total)}
${navRow('favorites','star','Favorites',fav)}
${navRow('trash','trash','Trash',trash)}
`;
// folders
const fEl = $('#folderItems');
fEl.innerHTML = state.folders.map(f=>{
const cnt = state.notes.filter(n=>!n.trashed && n.folderId===f.id).length;
return navRow('folder', folderIcon(f.color), f.name, cnt, f.id, 'folder');
}).join('');
// tags
const tEl = $('#tagItems');
if (state.tags.length === 0){
tEl.innerHTML = '<div style="padding:6px 12px;font-size:12px;color:var(--text-tertiary)">No tags yet</div>';
} else {
tEl.innerHTML = state.tags.map(t=>{
const cnt = state.notes.filter(n=>!n.trashed && n.tags.includes(t.id)).length;
return navRow('tag', `<span class="tag-dot" style="background:${t.color}"></span>`, t.name, cnt, t.id, 'tag');
}).join('');
}
// collapsed state
applyCollapsed();
bindNavClicks();
}
function folderIcon(color){
return `<span class="ni-icon"><svg viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></span>`;
}
function navRow(type, iconHtml, label, count, id, kind){
const v = state.view;
const active = (v.type===type && (!id || v.id===id)) ? 'active':'';
let del = '';
if (kind){ del = `<span class="ni-del" data-del-kind="${kind}" data-del-id="${id}" title="Delete">${svg('del',2)}</span>`; }
return `<button class="nav-item ${active}" data-nav-type="${type}" data-nav-id="${id||''}">
${typeof iconHtml==='string' && iconHtml.startsWith('<') ? iconHtml : `<span class="ni-icon">${svg(iconHtml)}</span>`}
<span class="ni-label">${escapeHtml(label)}</span>
<span class="ni-count">${count}</span>
${del}
</button>`;
}
function escapeHtml(s){ return (s||'').replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function applyCollapsed(){
['folders','tags'].forEach(sec=>{
const head = document.querySelector(`[data-toggle="${sec}"]`);
const items = $('#'+sec+'Items');
if (state.collapsed[sec]){ head.classList.add('collapsed'); items.classList.add('hidden'); }
else { head.classList.remove('collapsed'); items.classList.remove('hidden'); }
});
}
function bindNavClicks(){
document.querySelectorAll('.nav-item').forEach(el=>{
el.addEventListener('click', e=>{
if (e.target.closest('.ni-del')) return;
const type = el.dataset.navType;
const id = el.dataset.navId || null;
setView(type, id);
});
});
document.querySelectorAll('.ni-del').forEach(el=>{
el.addEventListener('click', e=>{
e.stopPropagation();
const kind = el.dataset.delKind, id = el.dataset.delId;
deleteCollection(kind, id);
});
});
}
function setView(type, id){
if (type==='trash'){ state.view={type:'trash'}; state.activeNoteId=null; renderAll(); closeMobileSidebar(); return; }
state.view = { type, id: id||null };
state.activeNoteId = null;
state.search = '';
$('#searchInput').value = '';
renderAll();
closeMobileSidebar();
}
/* ============ RENDER NOTE LIST ============ */
function renderNoteList(){
const isTrash = state.view.type==='trash';
const list = isTrash ? trashNotes() : visibleNotes();
const titleEl = $('#listTitle');
const metaEl = $('#listMeta');
// title
const titles = { all:'All Notes', favorites:'Favorites', trash:'Trash' };
if (state.view.type==='folder') titles.folder = folderName(state.view.id);
if (state.view.type==='tag') titles.tag = '#'+tagName(state.view.id);
titleEl.textContent = titles[state.view.type] || 'Notes';
metaEl.innerHTML = list.length + (list.length===1?' note':' notes');
if (isTrash && list.length){
const eb = document.createElement('button');
eb.textContent = 'Empty Trash';
eb.style.cssText = 'margin-left:8px;background:none;border:none;color:var(--danger);font-size:12px;cursor:pointer;font-weight:600';
eb.addEventListener('click', emptyTrash);
metaEl.appendChild(eb);
}
const wrap = $('#notesScroll');
if (list.length===0){
const msg = isTrash ? 'Trash is empty' : (state.search ? 'No matches' : 'No notes here yet');
const sub = isTrash ? 'Deleted notes will appear here.' : (state.search ? 'Try a different search.' : 'Create your first note to get started.');
wrap.innerHTML = `<div class="empty-list">${svg('empty',1.5)}<h4>${msg}</h4><p>${sub}</p></div>`;
return;
}
const q = (state.search||'').toLowerCase().trim();
wrap.innerHTML = list.map(n=>{
const active = n.id===state.activeNoteId ? 'active':'';
const title = n.title || 'Untitled';
const snip = snippet(n.body);
const tagsHtml = n.tags.slice(0,2).map(t=>`<span class="nc-tag" style="border-left:2px solid ${tagColor(t)};padding-left:5px">${escapeHtml(tagName(t))}</span>`).join('');
return `<div class="note-card ${active}" data-note="${n.id}">
<div class="nc-top">
<span class="nc-title">${highlight(title,q)}</span>
${n.pinned? svg('pin',2).replace('<svg','<svg class="nc-pin"') : ''}
</div>
<div class="nc-snippet">${highlight(snip,q)}</div>
<div class="nc-foot">
<span class="nc-date">${relativeDate(n.updatedAt)}</span>
<span class="nc-tags">${tagsHtml}</span>
</div>
</div>`;
}).join('');
wrap.querySelectorAll('.note-card').forEach(c=>{
c.addEventListener('click', ()=> openNote(c.dataset.note));
});
}
function highlight(text, q){
let html = escapeHtml(text);
if (!q) return html;
const re = new RegExp('('+q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+')','gi');
return html.replace(re,'<em>$1</em>');
}
/* ============ RENDER EDITOR ============ */
function renderEditor(){
const pane = $('#editorContent');
const statusBar = $('#statusBar');
const note = state.activeNoteId ? getNote(state.activeNoteId) : null;
if (!note){
statusBar.style.display='none';
pane.innerHTML = `<div class="editor-empty">
<div class="big-logo">${svg('notes',1.8)}</div>
<h2>Nothing selected</h2>
<p>Pick a note from the list, or press <kbd>Ctrl</kbd>+<kbd>N</kbd> to write something new.</p>
</div>`;
return;
}
const inTrash = note.trashed;
statusBar.style.display='flex';
pane.innerHTML = `
<div class="editor-topbar">
<div class="breadcrumb">
${svg('folder',2)}
<span class="crumb">${escapeHtml(folderName(note.folderId))}</span>
${note.tags.length? svg('chevron',2.5)+'<span class="crumb">'+note.tags.map(tagName).map(t=>'#'+escapeHtml(t)).join(' ')+'</span>':''}
</div>
<button class="topbar-btn ${note.serif?'on':''}" id="fontToggle" title="Toggle serif font">${svg('pen',1.8)}</button>
<button class="topbar-btn ${note.pinned?'on':''}" id="pinBtn" title="Pin to favorites">${svg('star',2)}</button>
<button class="topbar-btn" id="moreBtn" title="More">${svg('sort',2)}</button>
</div>
${inTrash ? '' : `
<div class="format-bar">
<button class="fmt-btn" data-cmd="bold" title="Bold (Ctrl+B)">${svg('bold',2.1)}</button>
<button class="fmt-btn" data-cmd="italic" title="Italic (Ctrl+I)">${svg('italic',2)}</button>
<button class="fmt-btn" data-cmd="underline" title="Underline (Ctrl+U)">${svg('underline',2)}</button>
<button class="fmt-btn" data-cmd="strikeThrough" title="Strikethrough">${svg('strike',2)}</button>
<span class="fmt-divider"></span>
<button class="fmt-btn" data-block="H1" title="Heading 1">${svg('h1',2)}</button>
<button class="fmt-btn" data-block="H2" title="Heading 2">${svg('h2',2)}</button>
<span class="fmt-divider"></span>
<button class="fmt-btn" data-block="UL" title="Bullet list">${svg('list',2)}</button>
<button class="fmt-btn" data-block="OL" title="Numbered list">${svg('num',2)}</button>
<button class="fmt-btn" data-block="BLOCKQUOTE" title="Quote">${svg('quote',2)}</button>
<button class="fmt-btn" data-block="PRE" title="Code block">${svg('code',2)}</button>
<span class="fmt-divider"></span>
<button class="fmt-btn" data-inline="code" title="Inline code">${svg('code',2)}</button>
<button class="fmt-btn" data-cmd="createLink" title="Link">${svg('link',2)}</button>
</div>`}
<div class="editor-scroll">
<div class="editor-paper">
<h1 class="note-title ${note.serif?'serif':''}" contenteditable="${!inTrash}" data-empty="true" data-placeholder="Untitled" id="noteTitle">${escapeHtml(note.title)}</h1>
<div class="note-body ${note.serif?'serif':''}" contenteditable="${!inTrash}" data-empty="true" data-placeholder="Start writing…" id="noteBody">${note.body||''}</div>
</div>
</div>
`;
bindEditor();
updateStatus();
}
let saveIndicatorTimer;
function updateStatus(){
const note = state.activeNoteId ? getNote(state.activeNoteId) : null;
const bar = $('#statusBar');
if (!note){ bar.style.display='none'; return; }
const body = $('#noteBody');
const words = body ? (plainText(body.innerHTML).match(/\S+/g)||[]).length : 0;
const chars = body ? plainText(body.innerHTML).length : 0;
bar.innerHTML = `
<span class="saved" id="saveDot">● Saved</span>
<span class="dot"></span>
<span>${words} ${words===1?'word':'words'}</span>
<span class="dot"></span>
<span>${chars} characters</span>
<span class="dot"></span>
<span>Edited ${relativeDate(note.updatedAt)}</span>
`;
}
function bindEditor(){
const note = getNote(state.activeNoteId);
if (!note) return;
const titleEl = $('#noteTitle');
const bodyEl = $('#noteBody');
if (titleEl){
checkEmpty(titleEl);
titleEl.addEventListener('input', ()=>{
note.title = titleEl.textContent.trim();
note.updatedAt = Date.now();
checkEmpty(titleEl);
updateNoteListItem(note.id);
updateStatus();
save();
});
titleEl.addEventListener('keydown', e=>{
if (e.key==='Enter'){ e.preventDefault(); bodyEl.focus(); }
});
}
if (bodyEl){
checkEmpty(bodyEl);
bodyEl.addEventListener('input', ()=>{
note.body = bodyEl.innerHTML;
note.updatedAt = Date.now();
checkEmpty(bodyEl);
updateNoteListItem(note.id);
updateStatus();
save();
});
bodyEl.addEventListener('keydown', handleEditorKeydown);
bodyEl.addEventListener('keyup', updateFmtState);
bodyEl.addEventListener('mouseup', updateFmtState);
}
// toolbar
document.querySelectorAll('.fmt-btn').forEach(btn=>{
btn.addEventListener('mousedown', e=> e.preventDefault());
btn.addEventListener('click', ()=>{
bodyEl.focus();
if (btn.dataset.cmd){
if (btn.dataset.cmd==='createLink'){
const url = prompt('Enter URL:','https://');
if (url){ document.execCommand('createLink', false, url); }
} else {
document.execCommand(btn.dataset.cmd, false, null);
}
} else if (btn.dataset.block){
const tag = btn.dataset.block;
document.execCommand('formatBlock', false, tag);
} else if (btn.dataset.inline==='code'){
wrapInline('code');
}
note.body = bodyEl.innerHTML;
note.updatedAt = Date.now();
updateStatus(); updateNoteListItem(note.id); save();
});
});
// topbar buttons
const pin = $('#pinBtn');
if (pin) pin.addEventListener('click', ()=>{
note.pinned = !note.pinned;
renderEditor(); renderNoteList(); renderSidebar(); save();
toast(note.pinned?'Pinned to favorites':'Removed from favorites');
});
const fontT = $('#fontToggle');
if (fontT) fontT.addEventListener('click', ()=>{
note.serif = !note.serif; renderEditor(); save();
toast(note.serif?'Serif font on':'Serif font off');
});
const more = $('#moreBtn');
if (more) more.addEventListener('click', e=> openMoreMenu(e, note));
}
function checkEmpty(el){
if (!el) return;
const empty = el.textContent.trim()==='' && !el.querySelector('img,hr,ul,ol,blockquote,pre,li');
el.setAttribute('data-empty', empty?'true':'false');
}
function updateFmtState(){
document.querySelectorAll('.fmt-btn').forEach(btn=>{
let on=false;
if (btn.dataset.cmd && !btn.dataset.cmd.startsWith('create')){
try{ on = document.queryCommandState(btn.dataset.cmd); }catch(e){}
}
btn.classList.toggle('active', on);
});
}
function wrapInline(tag){
const sel = window.getSelection();
if (!sel.rangeCount || sel.isCollapsed) return;
const range = sel.getRangeAt(0);
const node = document.createElement(tag);
try {
node.appendChild(range.extractContents());
range.insertNode(node);
sel.removeAllRanges();
const r = document.createRange();
r.selectNodeContents(node);
r.collapse(false);
sel.addRange(r);
} catch(e){}
}
/* markdown shortcuts */
function getBlock(node, root){
let el = node.nodeType===3 ? node.parentNode : node;
if (el === root) return null;
while (el && el.parentNode !== root){ el = el.parentNode; }
return el;
}
function handleEditorKeydown(e){
const body = $('#noteBody');
if (e.key===' ' && !e.shiftKey){
markdownBlock(e, body);
}
if (e.key==='Tab'){
e.preventDefault();
document.execCommand(e.shiftKey?'outdent':'indent');
}
}
function markdownBlock(e, root){
const sel = window.getSelection();
if (!sel.rangeCount) return;
const range = sel.getRangeAt(0);
const node = range.startContainer;
if (node.nodeType!==3) return;
const block = getBlock(node, root);
if (!block || block===root) return;
if (node.parentNode !== block) return; // simple flat block
const text = node.textContent;
if (range.startOffset !== text.length) return;
const map = [
{ re:/^#\s$/, tag:'H1' },
{ re:/^##\s$/, tag:'H2' },
{ re:/^###\s$/, tag:'H3' },
{ re:/^>\s$/, tag:'BLOCKQUOTE' },
{ re:/^```$/, tag:'PRE' },
];
for (const m of map){
if (m.re.test(text)){ e.preventDefault(); block.textContent=''; document.execCommand('formatBlock', false, m.tag); return; }
}
if (/^[-*]\s$/.test(text)){ e.preventDefault(); block.textContent=''; document.execCommand('insertUnorderedList'); return; }
if (/^1\.\s$/.test(text)){ e.preventDefault(); block.textContent=''; document.execCommand('insertOrderedList'); return; }
if (/^\[\]\s$/.test(text)){
e.preventDefault(); block.textContent='';
document.execCommand('formatBlock', false, 'P');
block.innerHTML = '<input type="checkbox"> ';
placeCaretEnd(block);
return;
}
}
function placeCaretEnd(el){
const sel = window.getSelection();
const r = document.createRange();
r.selectNodeContents(el); r.collapse(false);
sel.removeAllRanges(); sel.addRange(r);
}
function updateNoteListItem(id){
const note = getNote(id);
const card = document.querySelector(`.note-card[data-note="${id}"]`);
if (!card || !note) return;
const titleEl = card.querySelector('.nc-title');
const snipEl = card.querySelector('.nc-snippet');
const dateEl = card.querySelector('.nc-date');
if (titleEl) titleEl.textContent = note.title || 'Untitled';
if (snipEl) snipEl.textContent = snippet(note.body);
if (dateEl) dateEl.textContent = relativeDate(note.updatedAt);
}
/* ============ ACTIONS ============ */
function newNote(){
// if in trash view, switch to current folder context or all
let folderId = null;
if (state.view.type==='folder') folderId = state.view.id;
else folderId = state.folders[0]?.id || null;
const note = {
id: uid(), folderId, title:'', body:'', pinned:false, trashed:false, serif:false,
tags:[], createdAt:Date.now(), updatedAt:Date.now()
};
state.notes.unshift(note);
state.activeNoteId = note.id;
if (state.view.type==='trash' || state.view.type==='tag' || state.view.type==='favorites'){
state.view = { type:'all' };
}
save();
renderSidebar(); renderNoteList(); renderEditor();
setTimeout(()=>{ const t=$('#noteTitle'); if(t) t.focus(); }, 30);
}
function openNote(id){
state.activeNoteId = id;
renderNoteList(); renderEditor();
if (window.innerWidth <= 640){ $('#noteList').classList.add('hide-mobile'); }
}
function deleteNote(id){
const note = getNote(id);
if (!note) return;
if (note.trashed){
state.notes = state.notes.filter(n=>n.id!==id);
toast('Note permanently deleted');
} else {
note.trashed = true;
toast('Moved to Trash', 'Undo', ()=>{ note.trashed=false; renderAll(); });
}
if (state.activeNoteId===id) state.activeNoteId=null;
save(); renderAll();
}
function restoreNote(id){
const note = getNote(id); if(!note) return;
note.trashed=false; note.updatedAt=Date.now();
save(); renderAll(); toast('Note restored');
}
function deleteCollection(kind, id){
if (kind==='folder'){
if (!confirm('Delete this folder? Notes inside will move to "All Notes" (kept, not deleted).')) return;
state.notes.forEach(n=>{ if(n.folderId===id) n.folderId=null; });
state.folders = state.folders.filter(f=>f.id!==id);
if (state.view.type==='folder' && state.view.id===id) state.view={type:'all'};
} else if (kind==='tag'){
state.notes.forEach(n=>{ n.tags = n.tags.filter(t=>t!==id); });
state.tags = state.tags.filter(t=>t.id!==id);
if (state.view.type==='tag' && state.view.id===id) state.view={type:'all'};
}
save(); renderAll();
}
function addCollection(kind){
const name = prompt(kind==='folder'?'New folder name:':'New tag name:');
if (!name || !name.trim()) return;
const clean = name.trim();
if (kind==='folder'){
const id = uid();
state.folders.push({ id, name:clean, color: TAG_COLORS[state.folders.length % TAG_COLORS.length] });
state.view = { type:'folder', id };
} else {
if (state.tags.some(t=>t.name.toLowerCase()===clean.toLowerCase())){ toast('Tag already exists'); return; }
const id = uid();
state.tags.push({ id, name:clean, color: TAG_COLORS[state.tags.length % TAG_COLORS.length] });
state.view = { type:'tag', id };
}
save(); renderAll();
}
function emptyTrash(){
if (!confirm('Permanently delete all notes in Trash?')) return;
state.notes = state.notes.filter(n=>!n.trashed);
save(); renderAll(); toast('Trash emptied');
}
/* ============ MORE MENU ============ */
let openMenu=null;
function closeMenu(){ if(openMenu){ openMenu.remove(); openMenu=null; document.removeEventListener('mousedown', onDocClick); } }
function onDocClick(e){ if (openMenu && !openMenu.contains(e.target)) closeMenu(); }
function openMoreMenu(e, note){
closeMenu();
const menu = document.createElement('div');
menu.className='menu';
const inTrash = note.trashed;
let html='';
if (inTrash){
html += `<button class="menu-item" data-act="restore">${svg('restore',2)} Restore</button>`;
html += `<div class="menu-sep"></div>`;
html += `<button class="menu-item danger" data-act="delete">${svg('del',2)} Delete forever</button>`;
} else {
html += `<button class="menu-item" data-act="pin">${svg('star',2)} ${note.pinned?'Unpin':'Pin'} note</button>`;
html += `<button class="menu-item" data-act="copy">${svg('copy',2)} Copy as text</button>`;
html += `<div class="menu-sep"></div>`;
html += `<div style="padding:6px 10px 2px;font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--text-tertiary);font-weight:700">Move to</div>`;
html += state.folders.map(f=>`<button class="menu-item" data-act="move" data-fid="${f.id}"><span style="width:12px;height:9px;border-radius:2px;background:${f.color};display:inline-block"></span> ${escapeHtml(f.name)}</button>`).join('');
html += `<div class="menu-sep"></div>`;
html += `<div style="padding:6px 10px 2px;font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--text-tertiary);font-weight:700">Tags</div>`;
html += state.tags.map(t=>{
const on = note.tags.includes(t.id);
return `<button class="menu-item" data-act="tag" data-tid="${t.id}"><span class="tag-dot" style="background:${t.color}"></span> ${escapeHtml(t.name)} <span style="margin-left:auto;color:var(--accent)">${on?'✓':''}</span></button>`;
}).join('');
if (state.tags.length) html += `<div class="menu-sep"></div>`;
html += `<button class="menu-item danger" data-act="trash">${svg('del',2)} Move to Trash</button>`;
}
menu.innerHTML = html;
document.body.appendChild(menu);
const rect = e.currentTarget.getBoundingClientRect();
let left = rect.right - menu.offsetWidth;
let top = rect.bottom + 6;
if (top + menu.offsetHeight > window.innerHeight-10) top = rect.top - menu.offsetHeight - 6;
menu.style.left = Math.max(10,left)+'px';
menu.style.top = top+'px';
openMenu = menu;
menu.querySelectorAll('.menu-item').forEach(mi=>{
mi.addEventListener('click', ()=>{
const act = mi.dataset.act;
if (act==='pin'){ note.pinned=!note.pinned; }
else if (act==='copy'){ copyNoteText(note); }
else if (act==='move'){ note.folderId=mi.dataset.fid; }
else if (act==='tag'){
const tid=mi.dataset.tid;
if (note.tags.includes(tid)) note.tags=note.tags.filter(x=>x!==tid);
else note.tags.push(tid);
}
else if (act==='trash'){ deleteNote(note.id); closeMenu(); return; }
else if (act==='restore'){ restoreNote(note.id); closeMenu(); return; }
else if (act==='delete'){ deleteNote(note.id); closeMenu(); return; }
note.updatedAt=Date.now(); save(); renderAll(); closeMenu();
});
});
setTimeout(()=> document.addEventListener('mousedown', onDocClick), 0);
}
function copyNoteText(note){
const text = (note.title? note.title+'\n\n':'') + plainText(note.body);
navigator.clipboard.writeText(text).then(()=>toast('Copied to clipboard'));
}
/* ============ SORT MENU ============ */
function openSortMenu(e){
closeMenu();
const menu = document.createElement('div');
menu.className='menu';
const opts = [
{k:'updated', label:'Date updated'},
{k:'created', label:'Date created'},
{k:'title', label:'Title (A–Z)'},
];
menu.innerHTML = opts.map(o=>`<button class="menu-item" data-sort="${o.k}">${escapeHtml(o.label)} <span style="margin-left:auto;color:var(--accent)">${state.sort===o.k?'✓':''}</span></button>`).join('');
document.body.appendChild(menu);
const rect = e.currentTarget.getBoundingClientRect();
menu.style.left = (rect.right - menu.offsetWidth)+'px';
menu.style.top = (rect.bottom+6)+'px';
openMenu = menu;
menu.querySelectorAll('.menu-item').forEach(mi=> mi.addEventListener('click', ()=>{
state.sort = mi.dataset.sort; save(); renderNoteList(); closeMenu();
}));
setTimeout(()=> document.addEventListener('mousedown', onDocClick), 0);
}
/* ============ TOAST ============ */
let toastTimer;
function toast(msg, actionLabel, actionFn){
const t = $('#toast');
t.innerHTML = '';
const span = document.createElement('span'); span.textContent = msg;
t.appendChild(span);
if (actionLabel){
const b = document.createElement('button'); b.textContent = actionLabel;
b.addEventListener('click', ()=>{ actionFn&&actionFn(); hideToast(); });
t.appendChild(b);
}
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(hideToast, 3800);
function hideToast(){ t.classList.remove('show'); }
}
/* ============ THEME ============ */
function setTheme(theme){
state.theme = theme;
document.documentElement.setAttribute('data-theme', theme);
$('#themeIcon').innerHTML = theme==='dark'? icons.sun : icons.moon;
save();
}
/* ============ MOBILE ============ */
function openMobileSidebar(){ $('#sidebar').classList.add('open'); $('#scrim').classList.add('show'); }
function closeMobileSidebar(){ $('#sidebar').classList.remove('open'); $('#scrim').classList.remove('show'); }
/* ============ MASTER RENDER ============ */
function renderAll(){ renderSidebar(); renderNoteList(); renderEditor(); }
/* ============ INIT ============ */
function init(){
load();
setTheme(state.theme || 'light');
// theme toggle
$('#themeToggle').addEventListener('click', ()=> setTheme(state.theme==='dark'?'light':'dark'));
// new note
$('#newNoteBtn').addEventListener('click', newNote);
// search
$('#searchInput').addEventListener('input', e=>{
state.search = e.target.value;
if (state.view.type==='trash') state.view={type:'all'};
renderNoteList();
});
// collapsible sections
document.querySelectorAll('[data-toggle]').forEach(h=>{
h.addEventListener('click', ()=>{
const sec = h.dataset.toggle;
state.collapsed[sec] = !state.collapsed[sec];
applyCollapsed(); save();
});
});
// add folder/tag
document.querySelectorAll('[data-add]').forEach(b=>{
b.addEventListener('click', e=>{ e.stopPropagation(); addCollection(b.dataset.add); });
});
// sort menu
$('#sortBtn').addEventListener('click', e=>{ e.stopPropagation(); openSortMenu(e); });
// mobile
$('#menuToggle').addEventListener('click', openMobileSidebar);
$('#scrim').addEventListener('click', closeMobileSidebar);
// keyboard shortcuts
document.addEventListener('keydown', e=>{
const mod = e.ctrlKey || e.metaKey;
if (mod && e.key.toLowerCase()==='n'){ e.preventDefault(); newNote(); }
else if (mod && e.key.toLowerCase()==='f'){ e.preventDefault(); $('#searchInput').focus(); }
else if (mod && e.key.toLowerCase()==='k'){ e.preventDefault(); $('#searchInput').focus(); }
else if (mod && e.key==='\\'){ e.preventDefault(); toggleSidebar(); }
else if (e.key==='Escape'){ closeMenu(); closeMobileSidebar(); }
if (mod && e.shiftKey && e.key.toLowerCase()==='d'){
const n = getNote(state.activeNoteId);
if (n){ e.preventDefault(); deleteNote(n.id); }
}
});
renderAll();
}
function toggleSidebar(){
const sb = $('#sidebar');
if (window.innerWidth <= 900){ sb.classList.toggle('open'); $('#scrim').classList.toggle('show'); }
else { sb.style.marginLeft = sb.style.marginLeft==='0px' ? '' : '0px'; }
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
<!-- agent-meta {"model":"glm-5.2","provider":"zai","persona":"middle","sessionId":"d6f6489e-78ba-4131-9381-074b515e2bed","tokensIn":494920,"tokensOut":28427,"tokensTotal":523347,"turns":18,"toolCalls":17,"failedToolCalls":0,"timestamp":"2026-07-12T02:06:27.384Z"} -->