Notes
Tiel Coder 35B A3B GGUF MTP · full

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 77,157 bytes · SHA-256 0f8fc1441f1d
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notes</title>
<style>
/* ============================================================
TUNABLE CONSTANTS — colors, fonts, timing, lengths
============================================================ */
:root {
/* Warm light theme */
--bg: #f7f5f0;
--panel: #ffffff;
--panel-2: #fbf9f5;
--sidebar-bg: #f1ede4;
--border: #e7e2d7;
--border-strong: #ddd6c7;
--text: #2c2a24;
--text-2: #5c574d;
--text-muted: #9a9284;
--accent: #c0703d;
--accent-2: #a85a2c;
--accent-soft: #f4e6d6;
--accent-soft-2: #ead9c4;
--hover: #efe9dd;
--hover-2: #e8e1d3;
--selected: #f0e9dc;
--selected-border: #dcbf9a;
--danger: #c0392b;
--danger-soft: #f7e3e1;
--pin: #d99a3f;
--mark: #ffe18a;
--mark-border: #f2c94c;
--shadow: 0 1px 2px rgba(60,50,30,.08), 0 6px 20px rgba(60,50,30,.06);
--radius: 9px;
--radius-sm: 6px;
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-edit: Georgia, "Times New Roman", "IPAGothic", serif;
--autosave-debounce: 450;
--snippet-length: 110;
--sidebar-w: 256px;
--list-w: 320px;
}
html[data-theme="dark"] {
--bg: #1a1917;
--panel: #24221f;
--panel-2: #201f1c;
--sidebar-bg: #1f1d1a;
--border: #34312c;
--border-strong: #413d36;
--text: #e9e4da;
--text-2: #b8b2a5;
--text-muted: #7d776b;
--accent: #e0955a;
--accent-2: #ef9f63;
--accent-soft: #3a2c1e;
--accent-soft-2: #463423;
--hover: #2c2a26;
--hover-2: #33302b;
--selected: #353029;
--selected-border: #6b4d2e;
--danger: #e2604f;
--danger-soft: #3a2421;
--pin: #e6b066;
--mark: #7a6320;
--mark-border: #9a7d28;
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 6px 20px rgba(0,0,0,.35);
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font-family: var(--font-ui);
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
overflow: hidden;
}
button, input, select { font-family: inherit; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); background-clip: padding-box; }
::selection { background: var(--accent-soft-2); color: var(--text); }
/* ============================================================
LAYOUT
============================================================ */
#app {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.pane { height: 100%; overflow: hidden; display: flex; flex-direction: column; }
#sidebar {
width: var(--sidebar-w);
flex-shrink: 0;
min-width: 180px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border);
transition: width .22s cubic-bezier(.4,0,.2,1), opacity .2s ease;
}
#sidebar.collapsed { width: 0; opacity: 0; pointer-events: none; }
#sidebar-inner { width: 100%; height: 100%; display: flex; flex-direction: column; min-width: 180px; }
#listPanel {
width: var(--list-w);
flex-shrink: 0;
min-width: 220px;
background: var(--panel);
border-right: 1px solid var(--border);
}
#editorPanel { flex: 1; min-width: 260px; background: var(--bg); }
.resize-handle {
width: 5px;
flex-shrink: 0;
cursor: col-resize;
background: transparent;
position: relative;
z-index: 20;
}
.resize-handle::after {
content: ""; position: absolute; top: 0; bottom: 0; left: 0; right: 0;
}
.resize-handle:hover { background: var(--accent); opacity: .35; }
.resize-handle.dim { visibility: hidden; }
/* ============================================================
SIDEBAR
============================================================ */
.sidebar-top {
padding: 16px 16px 12px;
display: flex;
flex-direction: column;
gap: 12px;
border-bottom: 1px solid var(--border);
}
.app-brand { display: flex; align-items: center; justify-content: space-between; }
.app-logo { display: flex; align-items: center; gap: 9px; font-weight: 700; font-size: 17px; letter-spacing: -.2px; }
.app-logo .glyph {
width: 26px; height: 26px; border-radius: 7px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
display: grid; place-items: center; color: #fff; font-size: 15px; line-height: 1;
box-shadow: 0 2px 6px rgba(168,90,44,.35);
}
.icon-btns { display: flex; gap: 4px; }
.icon-btn {
width: 32px; height: 32px; border: none; border-radius: var(--radius-sm);
background: transparent; color: var(--text-2); cursor: pointer;
display: grid; place-items: center; font-size: 16px; transition: background .12s, color .12s;
}
.icon-btn:hover { background: var(--hover); color: var(--text); }
.icon-btn:active { transform: scale(.94); }
.new-note-btn {
display: flex; align-items: center; justify-content: center; gap: 7px;
width: 100%; padding: 9px 12px; border: none; border-radius: var(--radius-sm);
background: var(--accent); color: #fff; font-weight: 600; font-size: 13.5px;
cursor: pointer; transition: background .12s, box-shadow .12s, transform .06s;
box-shadow: 0 1px 2px rgba(168,90,44,.3);
}
.new-note-btn:hover { background: var(--accent-2); box-shadow: 0 2px 8px rgba(168,90,44,.35); }
.new-note-btn:active { transform: scale(.98); }
.search-box { position: relative; }
.search-box .s-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 13px; pointer-events: none; }
.search-box input {
width: 100%; padding: 8px 30px 8px 30px; border: 1px solid var(--border);
border-radius: var(--radius-sm); background: var(--panel-2); color: var(--text);
font-size: 13px; outline: none; transition: border-color .12s, box-shadow .12s;
}
.search-box input::placeholder { color: var(--text-muted); }
.search-box input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.search-box .s-clear { position: absolute; right: 7px; top: 50%; transform: translateY(-50%); border: none; background: none; color: var(--text-muted); cursor: pointer; font-size: 14px; display: none; }
.search-box.has-text .s-clear { display: block; }
.sidebar-scroll { flex: 1; overflow-y: auto; padding: 10px 8px 20px; }
.nav-item {
display: flex; align-items: center; gap: 9px; padding: 7px 9px; border-radius: var(--radius-sm);
cursor: pointer; color: var(--text-2); font-size: 13.5px; user-select: none;
transition: background .1s, color .1s; position: relative;
}
.nav-item:hover { background: var(--hover); color: var(--text); }
.nav-item.active { background: var(--accent-soft); color: var(--text); font-weight: 600; }
.nav-item.active .nav-ico { color: var(--accent); }
.nav-item .nav-ico { width: 16px; text-align: center; color: var(--text-muted); font-size: 13px; flex-shrink: 0; }
.nav-item .nav-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item .nav-label[contenteditable="true"] { background: var(--panel); outline: 1px solid var(--accent); padding: 0 2px; }
.nav-item .nav-count { font-size: 11.5px; color: var(--text-muted); background: transparent; padding: 0 6px; border-radius: 10px; }
.nav-item .nav-acts { display: none; gap: 2px; }
.nav-item:hover .nav-acts { display: flex; }
.nav-act { border: none; background: none; color: var(--text-muted); cursor: pointer; font-size: 12px; padding: 2px 3px; border-radius: 4px; }
.nav-act:hover { background: var(--border); color: var(--text); }
.nav-section-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: 12px 10px 5px; font-weight: 700; }
.chevron { width: 14px; text-align: center; color: var(--text-muted); font-size: 10px; transition: transform .16s ease; flex-shrink: 0; }
.notebook.collapsed .chevron { transform: rotate(-90deg); }
.notebook-notes { overflow: hidden; transition: height .2s ease; }
.notebook.collapsed .notebook-notes { height: 0 !important; overflow: hidden; }
.sub-note { padding-left: 34px; }
.sub-note .nav-item { font-size: 13px; }
/* ============================================================
NOTE LIST
============================================================ */
.list-header {
padding: 16px 16px 10px; border-bottom: 1px solid var(--border);
display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; background: var(--panel);
}
.list-title-row { display: flex; align-items: center; justify-content: space-between; }
.list-title { font-size: 15px; font-weight: 700; letter-spacing: -.2px; display: flex; align-items: center; gap: 8px; }
.list-count { font-size: 12px; color: var(--text-muted); font-weight: 500; }
.list-sub { font-size: 12px; color: var(--text-muted); }
.sort-toggle {
display: flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden;
}
.sort-toggle button {
border: none; background: transparent; color: var(--text-muted); cursor: pointer;
padding: 5px 9px; font-size: 11.5px; font-weight: 600; transition: background .1s, color .1s;
}
.sort-toggle button:not(:last-child) { border-right: 1px solid var(--border); }
.sort-toggle button.active { background: var(--accent); color: #fff; }
.sort-toggle button:hover:not(.active) { background: var(--hover); color: var(--text); }
.note-list { flex: 1; overflow-y: auto; padding: 10px; }
.note-card {
padding: 11px 13px; border-radius: var(--radius); cursor: pointer; margin-bottom: 6px;
border: 1px solid transparent; background: var(--panel-2);
transition: background .12s, border-color .12s, box-shadow .12s, transform .05s;
position: relative;
}
.note-card:hover { background: var(--hover); border-color: var(--border); }
.note-card.active { border-color: var(--selected-border); background: var(--selected); box-shadow: var(--shadow); }
.note-card.drag-over { border-color: var(--accent); background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent); }
.note-card-title { font-size: 13.5px; font-weight: 600; margin: 0 0 4px; display: flex; align-items: center; gap: 6px; color: var(--text); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.note-card-title .pin-ico { color: var(--pin); font-size: 12px; flex-shrink: 0; }
.note-card-snippet { font-size: 12px; color: var(--text-muted); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 7px; min-height: 17px; white-space: pre-line; }
.note-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.note-card-time { font-size: 11px; color: var(--text-muted); }
.note-card-tags { display: flex; gap: 4px; flex-wrap: wrap; }
.tag-chip { font-size: 10.5px; padding: 2px 7px; border-radius: 10px; background: var(--accent-soft); color: var(--accent-2); cursor: pointer; transition: background .1s, color .1s; border: 1px solid transparent; }
.tag-chip:hover { background: var(--accent-soft-2); }
.tag-chip.active { border-color: var(--accent); }
.note-card .drag-ghost { display: none; }
mark { background: var(--mark); color: inherit; padding: 0 1px; border-radius: 2px; box-shadow: 0 0 0 1px var(--mark-border); }
/* ============================================================
EDITOR
============================================================ */
.editor-header {
padding: 12px 20px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 12px; flex-shrink: 0; background: var(--panel);
}
.breadcrumb { font-size: 12px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.breadcrumb span { cursor: pointer; }
.breadcrumb span:hover { color: var(--accent); }
.note-title-input {
flex: 1; border: none; outline: none; background: transparent; color: var(--text);
font-size: 17px; font-weight: 600; padding: 6px 4px; border-bottom: 2px solid transparent;
transition: border-color .12s;
}
.note-title-input:focus { border-bottom-color: var(--accent-soft); }
.header-actions { display: flex; align-items: center; gap: 4px; position: relative; }
.save-indicator { font-size: 11px; color: var(--text-muted); margin-right: 6px; min-width: 56px; text-align: right; font-weight: 500; }
.save-indicator.saved { color: #4a9d5c; }
.save-indicator.saving { color: var(--pin); }
html[data-theme="dark"] .save-indicator.saved { color: #7fd08d; }
.toolbar {
padding: 7px 14px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 3px; flex-wrap: wrap; flex-shrink: 0; background: var(--panel);
}
.tool-btn {
min-width: 30px; height: 30px; padding: 0 7px; border: 1px solid transparent; border-radius: var(--radius-sm);
background: transparent; color: var(--text-2); cursor: pointer; font-size: 13px; font-weight: 600;
display: grid; place-items: center; transition: background .1s, color .1s, border-color .1s;
}
.tool-btn:hover { background: var(--hover); color: var(--text); }
.tool-btn:active { transform: scale(.93); }
.tool-btn.active { background: var(--accent-soft); border-color: var(--accent-soft-2); color: var(--accent-2); box-shadow: inset 0 1px 2px rgba(168,90,44,.12); }
.tool-btn:disabled { opacity: .4; cursor: default; }
.tool-sep { width: 1px; height: 20px; background: var(--border); margin: 0 5px; }
.tool-btn .kbd { font-size: 10px; opacity: .7; }
.editor-scroll { flex: 1; overflow-y: auto; }
.editor-wrap { max-width: 760px; margin: 0 auto; padding: 34px 40px 120px; }
#editor {
font-family: var(--font-edit);
font-size: 17px; line-height: 1.72; color: var(--text);
outline: none; min-height: 60vh; letter-spacing: .01em;
word-wrap: break-word; overflow-wrap: break-word;
}
#editor:empty:before {
content: "Start writing…"; color: var(--text-muted); font-style: italic; pointer-events: none;
}
#editor h1 { font-size: 27px; font-weight: 700; margin: 22px 0 10px; line-height: 1.25; letter-spacing: -.3px; }
#editor h2 { font-size: 22px; font-weight: 700; margin: 20px 0 8px; line-height: 1.3; }
#editor h3 { font-size: 18px; font-weight: 700; margin: 18px 0 6px; line-height: 1.35; }
#editor p { margin: 0 0 12px; }
#editor ul, #editor ol { margin: 0 0 12px; padding-left: 26px; }
#editor li { margin-bottom: 5px; }
#editor blockquote {
margin: 0 0 12px; padding: 4px 16px; border-left: 3px solid var(--accent);
color: var(--text-2); font-style: italic; background: var(--panel-2); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
#editor code {
font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 88%;
background: var(--accent-soft); color: var(--accent-2); padding: 1.5px 5px; border-radius: 4px;
}
html[data-theme="dark"] #editor code { background: var(--accent-soft-2); }
#editor hr { border: none; border-top: 1px solid var(--border); margin: 22px 0; }
#editor a { color: var(--accent); text-decoration: underline; }
#editor mark { background: var(--mark); }
/* Checklist */
#editor ul.checklist { list-style: none; padding-left: 6px; }
#editor ul.checklist li { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 6px; }
.checkbox {
--cb: 18px;
width: var(--cb); height: var(--cb); flex-shrink: 0; margin-top: 2px;
border: 1.8px solid var(--border-strong); border-radius: 5px;
display: grid; place-items: center; cursor: pointer; background: var(--panel);
color: #fff; font-size: 12px; line-height: 1; transition: background .12s, border-color .12s;
user-select: none;
}
.checkbox:hover { border-color: var(--accent); }
.checkbox.checked { background: var(--accent); border-color: var(--accent); }
.check-text { flex: 1; outline: none; }
.checkbox.checked + .check-text { text-decoration: line-through; color: var(--text-muted); }
/* ============================================================
EMPTY STATES
============================================================ */
.empty-state {
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: var(--text-muted); text-align: center; padding: 40px; gap: 10px;
}
.empty-state .empty-glyph { font-size: 54px; opacity: .5; margin-bottom: 6px; }
.empty-state h2 { font-size: 18px; font-weight: 600; color: var(--text-2); margin: 0; }
.empty-state p { font-size: 13.5px; margin: 0; max-width: 320px; line-height: 1.5; }
.empty-state .empty-cta {
margin-top: 8px; padding: 9px 18px; border: none; border-radius: var(--radius-sm);
background: var(--accent); color: #fff; font-weight: 600; cursor: pointer; font-size: 13px;
}
.empty-state .empty-cta:hover { background: var(--accent-2); }
.list-empty { padding: 40px 24px; text-align: center; color: var(--text-muted); }
.list-empty .le-glyph { font-size: 40px; opacity: .4; margin-bottom: 8px; }
/* ============================================================
DROPDOWN MENU
============================================================ */
.menu-wrap { position: relative; }
.dropdown {
position: absolute; right: 0; top: calc(100% + 6px); min-width: 200px;
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow); padding: 5px; z-index: 100;
display: none; animation: dd-in .12s ease;
}
.dropdown.open { display: block; }
@keyframes dd-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.dd-item {
display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 10px; border: none;
background: none; color: var(--text-2); cursor: pointer; border-radius: var(--radius-sm);
font-size: 13px; text-align: left; transition: background .1s, color .1s;
}
.dd-item:hover { background: var(--hover); color: var(--text); }
.dd-item.danger { color: var(--danger); }
.dd-item.danger:hover { background: var(--danger-soft); }
.dd-item .dd-ico { width: 16px; text-align: center; color: var(--text-muted); }
.dd-item.danger .dd-ico { color: var(--danger); }
.dd-sep { height: 1px; background: var(--border); margin: 5px 3px; }
.dd-sublabel { font-size: 11px; color: var(--text-muted); margin-left: auto; }
.dd-heading { padding: 7px 10px 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); font-weight: 700; }
.dd-item.has-sub { justify-content: space-between; }
.dd-item.has-sub .dd-arrow { color: var(--text-muted); }
.submenu {
position: relative;
}
.submenu .submenu-pop {
position: absolute; left: calc(100% + 3px); top: -3px; min-width: 170px;
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow); padding: 5px; display: none; z-index: 101;
}
.submenu:hover .submenu-pop { display: block; animation: dd-in .12s ease; }
.dd-item.active-notebook { background: var(--accent-soft); color: var(--text); font-weight: 600; }
/* Tag editor in editor */
.tag-editor {
padding: 10px 20px 0; display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
border-bottom: 1px solid var(--border); background: var(--panel); flex-shrink: 0;
}
.tag-editor .te-label { font-size: 12px; color: var(--text-muted); font-weight: 600; }
.tag-editor .existing-tags { display: flex; gap: 5px; flex-wrap: wrap; }
.tag-editor .tag-pill {
display: inline-flex; align-items: center; gap: 5px; font-size: 12px; padding: 3px 6px 3px 9px;
background: var(--accent-soft); color: var(--accent-2); border-radius: 11px; cursor: default;
transition: background .1s;
}
.tag-editor .tag-pill.clickable { cursor: pointer; }
.tag-editor .tag-pill.clickable:hover { background: var(--accent-soft-2); }
.tag-editor .tag-x { border: none; background: none; color: var(--accent-2); cursor: pointer; font-size: 13px; line-height: 1; padding: 0 1px; border-radius: 50%; }
.tag-editor .tag-x:hover { color: var(--danger); }
.tag-editor .tag-input {
border: none; outline: none; background: transparent; color: var(--text); font-size: 12.5px; flex: 1; min-width: 100px; padding: 3px 0;
}
.tag-editor .tag-input::placeholder { color: var(--text-muted); }
.hidden { display: none !important; }
/* toast */
.toast {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px);
background: var(--text); color: var(--panel); padding: 10px 18px; border-radius: var(--radius);
font-size: 13px; box-shadow: var(--shadow); opacity: 0; transition: opacity .2s, transform .2s; z-index: 500;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<div id="app">
<!-- ===================== SIDEBAR ===================== -->
<aside id="sidebar">
<div id="sidebar-inner">
<div class="sidebar-top">
<div class="app-brand">
<div class="app-logo"><span class="glyph">✎</span> Notes</div>
<div class="icon-btns">
<button class="icon-btn" id="themeToggle" title="Toggle theme" aria-label="Toggle theme">◐</button>
</div>
</div>
<button class="new-note-btn" id="newNoteBtn">+ New note</button>
<div class="search-box" id="searchBox">
<span class="s-icon">⌕</span>
<input type="text" id="globalSearch" placeholder="Search notes…" autocomplete="off" spellcheck="false">
<button class="s-clear" id="searchClear">✕</button>
</div>
</div>
<div class="sidebar-scroll">
<div class="nav-item" id="navAll" data-view="all">
<span class="nav-ico">📝</span><span class="nav-label">All Notes</span><span class="nav-count" id="countAll">0</span>
</div>
<div id="notebooks"><!-- notebooks injected here --></div>
<div class="nav-section-label">Saved</div>
<div class="nav-item" id="navTrash" data-view="trash">
<span class="nav-ico">🗑</span><span class="nav-label">Trash</span><span class="nav-count" id="countTrash">0</span>
</div>
</div>
</div>
</aside>
<div class="resize-handle" id="sidebarHandle"></div>
<!-- ===================== NOTE LIST ===================== -->
<div class="pane" id="listPanel">
<div class="list-header">
<div class="list-title-row">
<div class="list-title" id="listTitle">All Notes</div>
</div>
<div class="sort-toggle" id="sortToggle">
<button data-sort="modified" class="active">Modified</button>
<button data-sort="created">Created</button>
<button data-sort="alpha">A–Z</button>
</div>
</div>
<div class="note-list" id="noteList"></div>
</div>
<div class="resize-handle" id="listHandle"></div>
<!-- ===================== EDITOR ===================== -->
<div class="pane" id="editorPanel">
<div class="editor-header" id="editorHeader">
<div class="breadcrumb" id="breadcrumb"></div>
<input class="note-title-input" id="noteTitleInput" placeholder="Title" autocomplete="off" spellcheck="false">
<div class="header-actions">
<span class="save-indicator" id="saveIndicator">Saved</span>
<button class="icon-btn" id="pinBtn" title="Pin note" aria-label="Pin note">☆</button>
<button class="icon-btn" id="moveBtn" title="Move to notebook" aria-label="Move to notebook">⇶</button>
<button class="icon-btn" id="dupBtn" title="Duplicate" aria-label="Duplicate">⧉</button>
<button class="icon-btn" id="delBtn" title="Delete" aria-label="Delete">🗑</button>
<div class="menu-wrap">
<button class="icon-btn" id="moreBtn" title="More" aria-label="More">⋯</button>
<div class="dropdown" id="moreMenu"></div>
</div>
</div>
</div>
<div class="toolbar" id="toolbar">
<button class="tool-btn" data-cmd="undo" title="Undo (Ctrl+Z)">↶</button>
<button class="tool-btn" data-cmd="redo" title="Redo (Ctrl+Shift+Z)">↷</button>
<span class="tool-sep"></span>
<button class="tool-btn" data-cmd="bold" title="Bold (Ctrl+B)"><b>B</b></button>
<button class="tool-btn" data-cmd="italic" title="Italic (Ctrl+I)"><i>I</i></button>
<button class="tool-btn" data-cmd="underline" title="Underline (Ctrl+U)"><u>U</u></button>
<button class="tool-btn" data-cmd="strikethrough" title="Strikethrough"><s>S</s></button>
<button class="tool-btn" data-cmd="code" title="Inline code"></></button>
<span class="tool-sep"></span>
<button class="tool-btn" data-cmd="h1" title="Heading 1">H1</button>
<button class="tool-btn" data-cmd="h2" title="Heading 2">H2</button>
<button class="tool-btn" data-cmd="h3" title="Heading 3">H3</button>
<button class="tool-btn" data-cmd="quote" title="Quote">❝</button>
<span class="tool-sep"></span>
<button class="tool-btn" data-cmd="bullet" title="Bulleted list">≣</button>
<button class="tool-btn" data-cmd="numbered" title="Numbered list">1.</button>
<button class="tool-btn" data-cmd="checklist" title="Checklist">☑</button>
<span class="tool-sep"></span>
<button class="tool-btn" data-cmd="hr" title="Horizontal rule">―</button>
</div>
<div class="tag-editor hidden" id="tagEditor">
<span class="te-label">Tags</span>
<span class="existing-tags" id="existingTags"></span>
<input class="tag-input" id="tagInput" placeholder="Add tag + Enter">
</div>
<div class="editor-scroll">
<div class="editor-wrap">
<div class="empty-state" id="emptyState">
<div class="empty-glyph">📄</div>
<h2>Select or create a note</h2>
<p>Choose a note from the list on the left, or create a new one to start writing.</p>
<button class="empty-cta" id="emptyNewBtn">+ New note</button>
</div>
<div class="hidden" id="editorContainer">
<div id="editor" contenteditable="true" spellcheck="false"></div>
</div>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
/* ============================================================
NOTES APPLICATION — vanilla JS
Sections: (1) Tunable constants (2) Utilities (3) Data model
(4) Persistence (5) Sanitizer (6) Rendering (7) Toolbar/selection
(8) Editor events + markdown shortcuts (9) Search (10) Actions
(11) Drag-drop (12) Resize (13) Init
============================================================ */
'use strict';
/* ---------- (1) TUNABLE CONSTANTS ---------- */
const CONFIG = {
STORAGE_KEY: 'notesapp.v1',
AUTOSAVE_DEBOUNCE: 450,
SNIPPET_LENGTH: 110,
SORT_DEFAULT: 'modified',
ALLOWED_TAGS: new Set([
'h1','h2','h3','p','br','ul','ol','li','strong','em','b','i','u','s','strike',
'code','blockquote','hr','a','mark','span','div','del','ins'
]),
ALLOWED_ATTR: new Set(['class','data-checked']),
CHECKLIST_UL_CLASS: 'checklist',
};
/* ---------- (2) UTILITIES ---------- */
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
const uid = () => 'id_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
}[c]));
}
// Plain-text extraction: strip tags, decode entities. Used for snippets + search.
function extractText(html) {
if (!html) return '';
// DOM node: return its text directly
if (html.nodeType) return (html.textContent || html.innerText || '');
// String: parse as HTML and extract text
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
}
function decodeEntities(t) {
const div = document.createElement('div');
div.innerHTML = t;
return div.textContent || div.innerText || '';
}
// Build a snippet for the note list: join block-level elements with newlines so
// list items / paragraphs don't run together, then truncate.
function extractSnippet(html, len) {
if (!html) return '';
const div = document.createElement('div');
div.innerHTML = html;
const blockTags = new Set(['P','H1','H2','H3','H4','H5','H6','LI','BLOCKQUOTE','BR','DIV','HR','FIGCAPTION','FIGURE']);
const parts = [];
const walk = (node) => {
node.childNodes.forEach(child => {
if (child.nodeType === 1) {
const tag = child.tagName;
if (tag === 'BR') { parts.push('\n'); }
else if (tag === 'HR') { parts.push('—'); }
else if (blockTags.has(tag)) {
const txt = child.textContent;
if (txt.trim()) parts.push(txt.trim());
} else {
walk(child);
}
} else if (child.nodeType === 3) {
parts.push(child.textContent);
}
});
};
walk(div);
let text = parts.join('\n');
// Collapse multiple blank lines
text = text.replace(/\n{2,}/g, '\n').trim();
const limit = len || CONFIG.SNIPPET_LENGTH || 110;
if (text.length > limit) {
text = text.slice(0, limit - 1).trimEnd() + '…';
}
return text;
}
function fmtTime(ts) {
const d = new Date(ts);
const now = new Date();
const sameDay = d.toDateString() === now.toDateString();
if (sameDay) {
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
const oneWeek = 7 * 24 * 60 * 60 * 1000;
if (now - d < oneWeek) {
return d.toLocaleDateString([], { weekday: 'short' });
}
return d.toLocaleDateString([], { month: 'short', day: 'numeric', year: d.getFullYear() === now.getFullYear() ? undefined : 'numeric' });
}
let toastTimer;
function toast(msg) {
const el = $('#toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 2200);
}
/* ---------- (3) DATA MODEL (single source of truth) ---------- */
// The DOM is a projection of this model. Never write DOM -> model directly.
const DB = {
notebooks: [], // { id, name, collapsed, order }
notes: [], // { id, notebookId, title, body (HTML), tags: [], pinned, deleted, createdAt, updatedAt }
sort: CONFIG.SORT_DEFAULT,
selectedNoteId: null,
};
function createNotebook(name) {
const nb = { id: uid(), name, collapsed: false, order: DB.notebooks.length };
DB.notebooks.push(nb);
return nb;
}
function createNote(notebookId) {
const now = Date.now();
const note = {
id: uid(),
notebookId: notebookId || (DB.notebooks[0] && DB.notebooks[0].id),
title: '',
body: '',
tags: [],
pinned: false,
deleted: false,
createdAt: now,
updatedAt: now,
};
DB.notes.push(note);
return note;
}
function findNote(id) { return DB.notes.find(n => n.id === id); }
function findNotebook(id) { return DB.notebooks.find(n => n.id === id); }
function allNotes() { return DB.notes; }
function getTags() {
const set = new Set();
DB.notes.forEach(n => n.tags.forEach(t => set.add(t)));
return Array.from(set);
}
/* ---------- (4) PERSISTENCE ---------- */
function save() {
try {
localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(DB));
} catch (e) { console.warn('Persist failed', e); }
}
// Debounced auto-save
let saveTimer;
function scheduleSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => { save(); }, CONFIG.AUTOSAVE_DEBOUNCE);
}
// Debounced content-specific save handled by editor layer.
function load() {
let raw = null;
try { raw = localStorage.getItem(CONFIG.STORAGE_KEY); } catch (e) {}
if (!raw) return false;
try {
const data = JSON.parse(raw);
if (data && Array.isArray(data.notebooks) && Array.isArray(data.notes)) {
DB.notebooks = data.notebooks;
DB.notes = data.notes;
DB.sort = data.sort || CONFIG.SORT_DEFAULT;
DB.selectedNoteId = data.selectedNoteId || null;
return true;
}
} catch (e) { console.warn('Load parse failed', e); }
return false;
}
/* ---------- (5) SANITIZER ---------- */
// Pure sanitizer: strips disallowed tags/attrs/scripts. Returns safe HTML string.
function sanitize(html) {
if (!html) return '';
const doc = new DOMParser().parseFromString('<div>' + html + '</div>', 'text/html');
const root = doc.body.firstElementChild || doc.body;
// Remove style, script, template, object, embed, iframe, form tags plus event handlers and javascript: urls
const dangerTags = new Set(['STYLE','SCRIPT','TEMPLATE','OBJECT','EMBED','IFRAME','FORM','INPUT','LINK','META','NOSCRIPT']);
const walk = (node) => {
const children = Array.from(node.childNodes);
children.forEach(child => {
if (child.nodeType === 1) {
const tag = child.tagName;
if (dangerTags.has(tag)) { child.remove(); return; }
// Remove dangerous attributes
const attrs = Array.from(child.attributes);
attrs.forEach(a => {
const name = a.name.toLowerCase();
if (name.startsWith('on')) { child.removeAttribute(a.name); return; }
if (name === 'style') {
// allow minimal styling only if present; strip to be safe (no urls)
const css = (a.value || '').toLowerCase();
if (css.includes('expression') || css.includes('javascript:') || css.includes('url(')) {
child.removeAttribute('style');
}
return;
}
if (name === 'href' || name === 'src') {
const val = (a.value || '').trim().toLowerCase();
if (val.startsWith('javascript:') || val.startsWith('data:') || val.startsWith('vbscript:')) {
child.removeAttribute(a.name);
}
return;
}
if (name === 'class' && !CONFIG.ALLOWED_ATTR.has('class')) {
child.removeAttribute(a.name);
return;
}
if (!CONFIG.ALLOWED_ATTR.has(name)) {
child.removeAttribute(a.name);
}
});
if (dangerTags.has(tag)) return;
walk(child);
} else if (child.nodeType === 3) {
// text node — keep
} else {
child.remove();
}
});
};
walk(root);
return root.innerHTML;
}
// Convert bold/italic/underline <b>/<i>/<u> to strong/em/em for consistency? Keep as-is; browser round-trips fine.
/* ---------- (6) RENDERING ---------- */
// Shared DOM refs
const els = {};
function cacheEls() {
['sidebar','sidebarHandle','listPanel','listHandle','editorPanel',
'themeToggle','newNoteBtn','searchBox','globalSearch','searchClear',
'navAll','countAll','notebooks','navTrash','countTrash','listTitle',
'sortToggle','noteList','editorHeader','breadcrumb','noteTitleInput',
'saveIndicator','pinBtn','moveBtn','dupBtn','delBtn','moreBtn','moreMenu',
'toolbar','tagEditor','existingTags','tagInput','emptyState','emptyNewBtn',
'editorContainer','editor','toast'].forEach(id => { els[id] = document.getElementById(id); });
}
// Cache elements immediately so top-level listeners below have refs.
cacheEls();
// Global state for rendering
const state = {
view: 'all', // 'all' | 'trash' | notebook id
selectedNoteId: null,
sort: CONFIG.SORT_DEFAULT,
search: '',
tagFilter: null, // tag string to filter by
};
/* ---- Sidebar (notebooks) ---- */
function renderSidebar() {
// All Notes count
const active = state.view === 'all' ? 'active' : '';
els.navAll.classList.toggle('active', !!active);
const allCount = DB.notes.filter(n => !n.deleted).length;
els.countAll.textContent = allCount;
// Trash
const trashActive = state.view === 'trash' ? 'active' : '';
els.navTrash.classList.toggle('active', !!trashActive);
const trashCount = DB.notes.filter(n => n.deleted).length;
els.countTrash.textContent = trashCount;
// Notebooks
const wrap = els.notebooks;
const collapsedNotebookIds = new Set();
wrap.querySelectorAll('.notebook').forEach(n => {
const nbId = n.dataset.notebookId;
const data = findNotebook(nbId);
if (data) {
n.classList.toggle('collapsed', !!data.collapsed);
n.dataset.tag = data.tags || '';
}
});
const sortedNB = DB.notebooks.slice().sort((a,b) => a.order - b.order);
wrap.innerHTML = sortedNB.map(nb => {
const isActive = state.view === nb.id ? 'active' : '';
const collapsed = nb.collapsed ? 'collapsed' : '';
const count = DB.notes.filter(n => n.notebookId === nb.id && !n.deleted).length;
return `
<div class="nav-item notebook-row ${collapsed}" data-notebook-row="${nb.id}">
<span class="chevron">▾</span>
<span class="nav-ico">📓</span>
<span class="nav-label" data-notebook-label="${nb.id}">${escapeHtml(nb.name)}</span>
<span class="nav-acts">
<button class="nav-act" data-nb-add="${nb.id}" title="New note here">+</button>
<button class="nav-act" data-nb-rename="${nb.id}" title="Rename">✎</button>
<button class="nav-act" data-nb-del="${nb.id}" title="Delete notebook">🗑</button>
</span>
<span class="nav-count">${count}</span>
</div>
<div class="notebook-notes" data-notebook-notes="${nb.id}">
${DB.notes.filter(n => n.notebookId === nb.id && !n.deleted)
.sort((a,b)=>a.order - b.order)
.map(n => renderSubNavItem(n))
.join('')}
</div>`;
}).join('');
// Attach collapse + label events
wrap.querySelectorAll('.notebook-row').forEach(row => {
const nbId = row.dataset.notebookRow;
const nb = findNotebook(nbId);
const notesEl = wrap.querySelector(`[data-notebook-notes="${nbId}"]`);
row.addEventListener('click', (e) => {
if (e.target.closest('.nav-acts')) return;
if (e.target.dataset.notebookLabel) {
selectNotebook(nbId);
return;
}
if (nb) {
nb.collapsed = !nb.collapsed;
if (nb.collapsed) {
notesEl.style.height = notesEl.scrollHeight + 'px';
requestAnimationFrame(() => { notesEl.style.height = '0px'; });
} else {
notesEl.style.height = 'auto';
}
nb.classList && nb.classList.toggle('collapsed', nb.collapsed);
row.classList.toggle('collapsed', nb.collapsed);
save();
}
});
});
// Sub-note item clicks
wrap.querySelectorAll('[data-sub-note]').forEach(item => {
item.addEventListener('click', () => selectNote(item.dataset.subNote));
});
// Actions
wrap.querySelectorAll('[data-nb-add]').forEach(b => b.addEventListener('click', (e) => {
e.stopPropagation();
const nb = findNotebook(b.dataset.nbAdd);
const n = createNote(nb.id);
noteOrder(n);
renderNoteList();
selectNote(n.id);
save();
}));
wrap.querySelectorAll('[data-nb-rename]').forEach(b => b.addEventListener('click', (e) => {
e.stopPropagation();
renameNotebookInline(b.dataset.nbRename);
}));
wrap.querySelectorAll('[data-nb-del]').forEach(b => b.addEventListener('click', (e) => {
e.stopPropagation();
deleteNotebook(b.dataset.nbDel);
}));
}
function renderSubNavItem(note) {
const isActive = state.selectedNoteId === note.id ? 'active' : '';
return `<div class="nav-item sub-note ${isActive}" data-sub-note="${note.id}">
<span class="nav-ico" style="width:16px;text-align:center;color:var(--text-muted);font-size:11px">${note.pinned ? '📌' : '•'}</span>
<span class="nav-label">${escapeHtml(note.title || 'Untitled')}</span>
</div>`;
}
function selectNotebook(nbId) {
state.view = nbId;
state.tagFilter = null;
const nb = findNotebook(nbId);
els.listTitle.textContent = nb ? nb.name : 'Notebook';
renderSidebar();
renderNoteList();
}
function renameNotebookInline(nbId) {
const nb = findNotebook(nbId);
if (!nb) return;
const label = els.notebooks.querySelector(`[data-notebook-label="${nbId}"]`);
if (!label) return;
label.setAttribute('contenteditable', 'true');
label.focus();
document.getSelection().selectAllChildren(label);
const done = (commit) => {
label.removeAttribute('contenteditable');
const newName = label.textContent.trim() || nb.name;
nb.name = newName;
if (state.view === nbId) els.listTitle.textContent = nb.name;
save();
renderNoteList();
label.textContent = nb.name;
};
label.addEventListener('blur', () => done(true), { once: true });
label.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); label.blur(); }
if (e.key === 'Escape') { e.preventDefault(); label.blur(); }
});
}
function deleteNotebook(nbId) {
const nb = findNotebook(nbId);
if (!nb) return;
const noteCount = DB.notes.filter(n => n.notebookId === nbId).length;
const msg = noteCount
? `Delete notebook "${nb.name}"? ${noteCount} notes will be moved to the first notebook.`
: `Delete notebook "${nb.name}"?`;
if (!confirm(msg)) return;
const firstOther = DB.notebooks.find(n => n.id !== nbId) || createNotebook('Notes');
DB.notes.filter(n => n.notebookId === nbId).forEach(n => n.notebookId = firstOther.id);
DB.notebooks = DB.notebooks.filter(n => n.id !== nbId);
if (state.view === nbId) {
state.view = 'all';
els.listTitle.textContent = 'All Notes';
}
save();
renderSidebar();
renderNoteList();
}
/* ---- Note List ---- */
function getVisibleNotes() {
let notes;
if (state.view === 'trash') {
notes = DB.notes.filter(n => n.deleted);
} else if (state.view === 'all') {
notes = DB.notes.filter(n => !n.deleted);
} else {
notes = DB.notes.filter(n => !n.deleted && n.notebookId === state.view);
}
// tag filter
if (state.tagFilter) {
notes = notes.filter(n => n.tags.includes(state.tagFilter));
}
// text search across title + body text (not raw HTML)
if (state.search && state.search.trim()) {
const q = state.search.trim().toLowerCase();
notes = notes.filter(n => {
const title = (n.title || '').toLowerCase();
const body = extractText(n.body).toLowerCase();
return title.includes(q) || body.includes(q);
});
}
return notes;
}
function sortNotes(notes) {
const arr = notes.slice();
// pinned always float to top
arr.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 === 'alpha') {
const t = (a.title || 'Untitled').localeCompare(b.title || 'Untitled');
return t !== 0 ? t : b.updatedAt - a.updatedAt;
}
return b.updatedAt - a.updatedAt; // modified
});
return arr;
}
function renderNoteList() {
const notes = sortNotes(getVisibleNotes());
const list = els.noteList;
// update counts in sidebar
if (state.view === 'all') els.countAll.textContent = DB.notes.filter(n => !n.deleted).length;
els.countTrash.textContent = DB.notes.filter(n => n.deleted).length;
if (notes.length === 0) {
list.innerHTML = `<div class="list-empty">
<div class="le-glyph">${state.search ? '🔍' : (state.view === 'trash' ? '🗑' : '📭')}</div>
<p>${state.search ? 'No notes match your search.' : (state.view === 'trash' ? 'Trash is empty.' : 'No notes here yet.')}</p>
</div>`;
return;
}
const q = state.search.trim().toLowerCase();
list.innerHTML = notes.map(n => {
const isActive = state.selectedNoteId === n.id ? 'active' : '';
const snippet = extractSnippet(n.body);
const snippetHtml = highlightText(escapeHtml(snippet), q) || '<em style="opacity:.6">No content</em>';
const time = fmtTime(n.updatedAt);
const tags = n.tags.slice(0, 4).map(t =>
`<span class="tag-chip ${state.tagFilter === t ? 'active' : ''}" data-tag="${escapeHtml(t)}">#${escapeHtml(t)}</span>`
).join('');
return `
<div class="note-card ${isActive}" data-note="${n.id}" draggable="true">
<div class="note-card-title">
${n.pinned ? '<span class="pin-ico">📌</span>' : ''}
<span>${escapeHtml(n.title || 'Untitled')}</span>
</div>
<div class="note-card-snippet">${snippetHtml}</div>
<div class="note-card-meta">
<span class="note-card-time">${time}</span>
<div class="note-card-tags">${tags}</div>
</div>
</div>`;
}).join('');
// Card clicks
list.querySelectorAll('.note-card').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.dataset.tag) {
state.tagFilter = e.target.dataset.tag;
els.listTitle.textContent = '#' + state.tagFilter;
renderSidebar();
renderNoteList();
return;
}
selectNote(card.dataset.note);
});
card.addEventListener('dragstart', (e) => {
els.dragNoteId = card.dataset.note;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', card.dataset.note);
});
card.addEventListener('dragend', () => { els.dragNoteId = null; });
});
}
// Highlight matched text within an already-escaped string.
function highlightText(html, q) {
if (!q) return html;
const words = q.split(/\s+/).filter(Boolean);
if (words.length === 0) return html;
const escaped = words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const re = new RegExp('(' + escaped + ')', 'gi');
return html.replace(re, '<mark>$1</mark>');
}
/* ---- Sort toggle ---- */
function renderSortButtons() {
$$('#sortToggle button').forEach(b => {
b.classList.toggle('active', b.dataset.sort === state.sort);
});
}
$$('#sortToggle button').forEach(b => {
b.addEventListener('click', () => {
state.sort = b.dataset.sort;
DB.sort = state.sort;
save();
renderSortButtons();
renderNoteList();
});
});
/* ---- Breadcrumb ---- */
function renderBreadcrumb() {
const note = findNote(state.selectedNoteId);
if (!note) { els.breadcrumb.innerHTML = ''; return; }
const nb = findNotebook(note.notebookId);
const all = `<span data-view="all">All Notes</span>`;
const nbSpan = nb ? `<span data-view="${nb.id}">${escapeHtml(nb.name)}</span>` : '';
els.breadcrumb.innerHTML = `${all}${nbSpan ? ' / ' + nbSpan : ''} / <span>${escapeHtml(note.title || 'Untitled')}</span>`;
els.breadcrumb.querySelectorAll('[data-view]').forEach(s => {
s.addEventListener('click', () => {
if (s.dataset.view === 'all') { state.view = 'all'; els.listTitle.textContent = 'All Notes'; }
else selectNotebook(s.dataset.view);
renderSidebar();
renderNoteList();
});
});
}
/* ---------- NOTE SELECTION + EDITOR ---------- */
let activeNote = null;
function selectNote(noteId) {
state.selectedNoteId = noteId;
DB.selectedNoteId = noteId;
activeNote = findNote(noteId);
// Update sidebar active state
$$('#notebooks .sub-note').forEach(el => el.classList.toggle('active', el.dataset.subNote === noteId));
// Show editor, hide empty state
els.emptyState.classList.add('hidden');
els.editorContainer.classList.remove('hidden');
els.editorHeader.classList.remove('hidden');
els.tagEditor.classList.remove('hidden');
if (!activeNote) {
els.emptyState.classList.remove('hidden');
els.editorContainer.classList.add('hidden');
return;
}
els.noteTitleInput.value = activeNote.title;
els.editor.innerHTML = activeNote.body;
renderBreadcrumb();
renderTags();
saveIndicator('saved');
updateToolbar();
renderNoteList(); // reflect selection highlight
els.editor.focus();
}
function saveIndicator(status) {
els.saveIndicator.textContent = status === 'saved' ? 'Saved' : 'Saving…';
els.saveIndicator.classList.toggle('saved', status === 'saved');
els.saveIndicator.classList.toggle('saving', status === 'saving');
}
/* ---- Tags ---- */
function renderTags() {
if (!activeNote) { els.tagEditor.classList.add('hidden'); return; }
const wrap = els.existingTags;
wrap.innerHTML = activeNote.tags.map(t =>
`<span class="tag-pill clickable" data-tag="${escapeHtml(t)}">#${escapeHtml(t)}<button class="tag-x" data-tagx="${escapeHtml(t)}">×</button></span>`
).join('');
wrap.querySelectorAll('[data-tagx]').forEach(b => b.addEventListener('click', () => {
activeNote.tags = activeNote.tags.filter(t => t !== b.dataset.tagx);
activeNote.updatedAt = Date.now();
save(); scheduleSave();
renderTags();
renderNoteList();
}));
wrap.querySelectorAll('[data-tag]').forEach(chip => chip.addEventListener('click', () => {
state.tagFilter = chip.dataset.tag;
els.listTitle.textContent = '#' + state.tagFilter;
renderSidebar();
renderNoteList();
}));
}
function addTag(tag) {
if (!activeNote) return;
tag = tag.trim().replace(/^#+/, '').toLowerCase();
if (!tag) return;
if (!activeNote.tags.includes(tag)) {
activeNote.tags.push(tag);
activeNote.updatedAt = Date.now();
save(); scheduleSave();
renderTags();
renderNoteList();
}
}
els.tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTag(els.tagInput.value);
els.tagInput.value = '';
}
});
/* ---- Editor change handling + autosave ---- */
let saveTimer2;
function editorChange() {
if (!activeNote) return;
// Normalize list styling: ensure checklist ul has class
normalizeChecklists();
const body = sanitize(els.editor.innerHTML);
activeNote.body = body;
activeNote.updatedAt = Date.now();
saveIndicator('saving');
clearTimeout(saveTimer2);
saveTimer2 = setTimeout(() => {
save();
saveIndicator('saved');
renderNoteList();
}, CONFIG.AUTOSAVE_DEBOUNCE);
}
els.editor.addEventListener('input', editorChange);
/* title input */
let titleTimer;
els.noteTitleInput.addEventListener('input', () => {
if (!activeNote) return;
activeNote.title = els.noteTitleInput.value;
activeNote.updatedAt = Date.now();
clearTimeout(titleTimer);
titleTimer = setTimeout(() => {
save();
renderBreadcrumb();
renderNoteList();
renderSidebar();
}, CONFIG.AUTOSAVE_DEBOUNCE);
});
/* ---- Checklist normalization: ensure checked state persists ---- */
function normalizeChecklists() {
const uls = $$('#editor ul.checklist');
uls.forEach(ul => {
const lis = ul.querySelectorAll('li');
lis.forEach(li => {
let cb = li.querySelector(':scope > .checkbox');
if (!cb) {
cb = document.createElement('span');
cb.className = 'checkbox';
cb.setAttribute('contenteditable', 'false');
li.insertBefore(cb, li.firstChild);
}
const checked = cb.classList.contains('checked');
cb.setAttribute('data-checked', checked ? 'true' : 'false');
cb.textContent = checked ? '✓' : '';
// Ensure text node is the check-text
let txt = li.querySelector(':scope > .check-text');
if (!txt) {
txt = document.createElement('span');
txt.className = 'check-text';
txt.setAttribute('contenteditable', 'true');
li.appendChild(txt);
}
txt.setAttribute('contenteditable', 'true');
});
});
}
/* ---------- TOOLBAR + FORMATTING COMMANDS ---------- */
function isFocusedInEditor() {
return document.activeElement === els.editor || els.editor.contains(document.activeElement);
}
function execCmd(cmd, arg) {
els.editor.focus();
document.execCommand(cmd, false, arg || null);
updateToolbar();
}
// Heading conversion uses formatBlock
function applyHeading(tag) {
els.editor.focus();
document.execCommand('formatBlock', false, tag);
updateToolbar();
}
// Apply inline code to selection by wrapping in <code>
function applyCode() {
els.editor.focus();
const sel = window.getSelection();
if (!sel.rangeCount || sel.isCollapsed) return;
const range = sel.getRangeAt(0);
const selectedText = range.toString();
if (!selectedText.trim()) return;
const code = document.createElement('code');
code.textContent = selectedText;
range.deleteContents();
range.insertNode(code);
// Collapse selection to end of the code span
range.setStartAfter(code);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
updateToolbar();
}
function applyHr() {
els.editor.focus();
document.execCommand('insertHorizontalRule', false);
}
// List commands: use formatBlock ul/ol then normalize, or create new list
function applyList(type) {
els.editor.focus();
const sel = window.getSelection();
const atLineStart = isAtLineStart();
// If at line start with empty/short content, use formatBlock
if (atLineStart) {
document.execCommand('formatBlock', false, type === 'numbered' ? 'ol' : 'ul');
} else {
// Wrap current block into a list
const block = getBlockElement();
if (block) {
const list = document.createElement(type === 'numbered' ? 'ol' : 'ul');
const li = document.createElement('li');
li.innerHTML = block.innerHTML || block.textContent;
list.appendChild(li);
block.replaceWith(list);
}
}
if (type === 'checklist') {
// Convert the last ul into checklist
const uls = $$('#editor ul');
const last = uls[uls.length - 1];
if (last) {
last.classList.add('checklist');
const li = last.querySelector(':scope > li:last-child');
if (li && !li.querySelector('.checkbox')) {
const cb = document.createElement('span');
cb.className = 'checkbox';
cb.setAttribute('contenteditable', 'false');
li.insertBefore(cb, li.firstChild);
let txt = li.querySelector('.check-text');
if (!txt) {
txt = document.createElement('span');
txt.className = 'check-text';
txt.setAttribute('contenteditable', 'true');
li.appendChild(txt);
}
if (!txt.textContent) txt.focus();
}
}
}
updateToolbar();
}
// Toggle helpers for list state
function currentBlockIs(tagName) {
let node = window.getSelection().anchorNode;
while (node && node !== els.editor) {
if (node.nodeType === 1 && node.tagName.toLowerCase() === tagName) return true;
node = node.parentNode;
}
return false;
}
function updateToolbar() {
const active = new Set();
if (isFocusedInEditor()) {
const checks = {
bold: () => document.queryCommandState('bold'),
italic: () => document.queryCommandState('italic'),
underline: () => document.queryCommandState('underline'),
strikethrough: () => document.queryCommandState('strikethrough'),
};
Object.keys(checks).forEach(k => { if (checks[k]()) active.add(k); });
if (currentBlockIs('H1')) active.add('h1');
if (currentBlockIs('H2')) active.add('h2');
if (currentBlockIs('H3')) active.add('h3');
if (currentBlockIs('BLOCKQUOTE')) active.add('quote');
if (currentBlockIs('UL')) active.add('bullet');
if (currentBlockIs('OL')) active.add('numbered');
const sel = window.getSelection();
let n = sel.anchorNode;
while (n && n !== els.editor) {
if (n.nodeType === 1 && n.tagName.toLowerCase() === 'CODE') active.add('code');
n = n.parentNode;
}
}
els.toolbar.querySelectorAll('.tool-btn[data-cmd]').forEach(btn => {
btn.classList.toggle('active', active.has(btn.dataset.cmd));
});
}
function toggleChecklistItem(li) {
const cb = li.querySelector('.checkbox');
if (!cb) return;
const checked = cb.classList.toggle('checked');
cb.setAttribute('data-checked', checked ? 'true' : 'false');
cb.textContent = checked ? '✓' : '';
if (!checked) cb.textContent = '';
saveIndicator('saving');
clearTimeout(saveTimer2);
saveTimer2 = setTimeout(() => { save(); }, 200);
updateToolbar();
}
/* ---------- LINE HELPERS ---------- */
function getBlockElement() {
let node = window.getSelection().anchorNode;
node = node || els.editor;
while (node && node !== els.editor) {
if (node.nodeType === 1) return node;
node = node.parentNode;
}
return els.editor;
}
function isAtLineStart() {
const sel = window.getSelection();
if (!sel.rangeCount) return true;
const range = sel.getRangeAt(0);
const block = getBlockElement();
// Compute offset within block
let offset = 0;
const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
let textNode;
let prevLen = 0;
const pos = range.startContainer;
if (pos === block) {
// selection directly in block (empty)
return true;
}
// Find previous sibling text content length
let before = 0;
let node = block.firstChild;
while (node) {
if (node === range.startContainer) { before += range.startOffset; break; }
if (node.nodeType === 3) before += node.textContent.length;
node = node.nextSibling;
}
// Check that everything before on this line is whitespace
const text = (range.startContainer.textContent || '');
const fullBefore = text.slice(0, range.startOffset);
// get text from block start
const blockText = extractText(block);
// Simpler: check the current line's leading portion
const idx = blockText.indexOf('\n');
// find the line containing the cursor
let lineStart = 0;
let posInBlock = before;
// rebuild position
let accumulated = 0;
let lineStartIndex = 0;
const tw = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
let tn;
while ((tn = tw.nextNode())) {
const len = tn.textContent.length;
if (accumulated + len >= posInBlock) {
lineStartIndex = accumulated;
break;
}
accumulated += len;
}
const lineBefore = blockText.slice(lineStartIndex, before);
return /^\s*$/.test(lineBefore);
}
/* ---------- EDITOR KEY HANDLING (markdown shortcuts, lists, etc.) ---------- */
// Regex for markdown-style shortcuts. Anchored to line start.
const MD_PATTERNS = [
{ type: 'checklist', re: /^\s*\[\s?\]\s(.*)$/i },
{ type: 'bullet', re: /^\s*[-*+]\s(.*)$/ },
{ type: 'numbered', re: /^\s*(\d+)\.\s(.*)$/ },
{ type: 'quote', re: /^\s*>\s?(.*)$/ },
{ type: 'h1', re: /^#{1}\s(.*)$/ },
{ type: 'h2', re: /^#{2}\s(.*)$/ },
{ type: 'h3', re: /^#{3}\s(.*)$/ },
];
els.editor.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) return; // let browser handle undo/redo/shift+undo
const key = e.key;
// Enter: handle list continuation + markdown shortcut + empty-item exit
if (key === 'Enter') {
const sel = window.getSelection();
const atStart = isAtLineStart();
const block = getBlockElement();
const blockTag = block.tagName ? block.tagName.toLowerCase() : '';
// Markdown shortcut on space handled by input event; here handle Enter shortcuts
if (atStart) {
const text = extractText(block);
// Try to match a markdown shortcut at the start
const matched = matchShortcut(text);
if (matched) {
// Defer: let the input event handle it (space triggers conversion)
// For Enter with partial text we do nothing special here.
}
}
// List continuation / empty item exit
if (blockTag === 'li') {
const ul = block.parentElement;
const isEmpty = extractText(block).trim() === '';
if (isEmpty) {
// Empty list item: Enter exits the list
e.preventDefault();
exitList(block);
return;
} else {
// Continue the list
e.preventDefault();
continueList(block, ul);
return;
}
}
}
// Backspace on empty list item: exit list
if (key === 'Backspace') {
const sel = window.getSelection();
const block = getBlockElement();
const blockTag = block.tagName ? block.tagName.toLowerCase() : '';
if (blockTag === 'li' && extractText(block).trim() === '' && isAtLineStart()) {
const ul = block.parentElement;
e.preventDefault();
exitList(block);
return;
}
}
});
function matchShortcut(text) {
const t = text.replace(/^/, '');
for (const p of MD_PATTERNS) {
const m = p.re.exec(text);
if (m) return { type: p.type, rest: m[1] };
}
return null;
}
/* ---------- EDITOR INPUT (markdown shortcut conversion on space) ---------- */
els.editor.addEventListener('input', (e) => {
normalizeChecklists();
const sel = window.getSelection();
if (sel.rangeCount === 0) return;
const range = sel.getRangeAt(0);
const block = getBlockElement();
if (block !== els.editor && block.tagName && !['UL','OL','LI','P','H1','H2','H3','BLOCKQUOTE','DIV'].includes(block.tagName.toUpperCase())) {
return;
}
const text = extractText(block);
// Match a markdown shortcut anchored to line start (mid-sentence "#"/">" etc. won't match).
const matched = matchShortcut(text);
if (!matched) return;
// Only convert when the caret sits right after the trailing space of the marker.
const markerEnd = text.indexOf(' ', 1) === -1 ? text.length : text.indexOf(' ', 1);
const caretAtEnd = range.startOffset >= markerEnd;
if (!caretAtEnd) return;
convertShortcut(block, matched.type);
e.stopPropagation();
});
function convertShortcut(block, type) {
const text = extractText(block);
let newTag, content;
if (type === 'bullet') {
newTag = 'ul'; content = text.replace(/^\s*[-*+]\s/, '');
} else if (type === 'numbered') {
newTag = 'ol'; content = text.replace(/^\s*\d+\.\s/, '');
} else if (type === 'quote') {
newTag = 'blockquote'; content = text.replace(/^\s*>\s/, '');
} else if (type === 'h1') {
newTag = 'h1'; content = text.replace(/^#\s/, '');
} else if (type === 'h2') {
newTag = 'h2'; content = text.replace(/^##\s/, '');
} else if (type === 'h3') {
newTag = 'h3'; content = text.replace(/^###\s/, '');
} else if (type === 'checklist') {
newTag = 'ul'; content = text.replace(/^\s*\[\s?\]\s/, '');
}
if (content === undefined) return;
const newBlock = document.createElement(newTag);
if (newTag === 'ul') newBlock.className = 'checklist';
newBlock.innerHTML = escapeHtml(content).length ? escapeHtml(content) : '';
// Preserve caret position roughly
const caretPos = extractText(block).length;
const old = block;
old.replaceWith(newBlock);
restoreCaret(newBlock, Math.min(caretPos, extractText(newBlock).length));
if (newTag === 'ul') {
// add checklist checkbox
const li = newBlock.querySelector(':scope > li');
if (li) {
const cb = document.createElement('span');
cb.className = 'checkbox';
cb.setAttribute('contenteditable', 'false');
li.insertBefore(cb, li.firstChild);
let txt = li.querySelector('.check-text');
if (!txt) {
txt = document.createElement('span');
txt.className = 'check-text';
txt.setAttribute('contenteditable', 'true');
li.appendChild(txt);
}
if (txt.textContent) txt.focus();
}
}
normalizeChecklists();
updateToolbar();
}
function restoreCaret(block, offset) {
const range = document.createRange();
const textNode = block.lastChild;
if (textNode && textNode.nodeType === 3) {
offset = Math.min(offset, textNode.textContent.length);
range.setStart(textNode, offset);
} else {
range.setStart(block, 0);
}
range.collapse(true);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
/* ---------- LIST CONTINUE / EXIT ---------- */
function continueList(li, ul) {
const newLi = document.createElement('li');
newLi.innerHTML = '';
const listType = ul.tagName.toLowerCase();
if (listType === 'ul' && ul.classList.contains('checklist')) {
const cb = document.createElement('span');
cb.className = 'checkbox';
cb.setAttribute('contenteditable', 'false');
newLi.appendChild(cb);
const txt = document.createElement('span');
txt.className = 'check-text';
txt.setAttribute('contenteditable', 'true');
newLi.appendChild(txt);
txt.focus();
} else {
newLi.focus();
}
ul.insertBefore(newLi, li.nextSibling);
setCaretAtStart(newLi);
normalizeChecklists();
updateToolbar();
}
function exitList(li) {
const ul = li.parentElement;
const p = document.createElement('p');
p.innerHTML = '';
li.replaceWith(p);
setCaretAtStart(p);
// Remove empty list if now empty
if (!ul.querySelector('li')) ul.remove();
updateToolbar();
}
function setCaretAtStart(node) {
const range = document.createRange();
const textNode = node.firstChild;
if (textNode && textNode.nodeType === 3 && textNode.textContent.length) {
range.setStart(textNode, Math.min(1, textNode.textContent.length));
} else {
range.setStart(node, 0);
}
range.collapse(true);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
node.focus();
}
/* ---------- PASTE SANITIZATION ---------- */
els.editor.addEventListener('paste', (e) => {
const html = e.clipboardData.getData('text/html');
const plain = e.clipboardData.getData('text/plain');
if (html) {
e.preventDefault();
const safe = sanitize(html);
document.execCommand('insertHTML', false, safe);
} else {
// plain text: insert as escaped paragraphs
e.preventDefault();
const safe = escapeHtml(plain).replace(/\n\n+/g, '</p><p>').replace(/\n/g, '<br>');
document.execCommand('insertHTML', false, '<p>' + safe + '</p>');
}
});
/* ---------- TOOLBAR CLICKS ---------- */
els.toolbar.querySelectorAll('.tool-btn[data-cmd]').forEach(btn => {
btn.addEventListener('click', () => {
const cmd = btn.dataset.cmd;
switch (cmd) {
case 'bold': execCmd('bold'); break;
case 'italic': execCmd('italic'); break;
case 'underline': execCmd('underline'); break;
case 'strikethrough': execCmd('strikethrough'); break;
case 'code': applyCode(); break;
case 'h1': applyHeading('h1'); break;
case 'h2': applyHeading('h2'); break;
case 'h3': applyHeading('h3'); break;
case 'quote': applyHeading('blockquote'); break;
case 'hr': applyHr(); break;
case 'bullet': applyList('bullet'); break;
case 'numbered': applyList('numbered'); break;
case 'checklist': applyList('checklist'); break;
case 'undo': execCmd('undo'); break;
case 'redo': execCmd('redo'); break;
}
});
});
/* Clicking checklist toggles it */
els.editor.addEventListener('click', (e) => {
const cb = e.target.closest('.checkbox');
if (cb) {
e.preventDefault();
e.stopPropagation();
toggleChecklistItem(cb.closest('li'));
}
});
/* ---------- (9) SEARCH ---------- */
let searchTimer;
els.globalSearch.addEventListener('input', () => {
clearTimeout(searchTimer);
const val = els.globalSearch.value;
els.searchBox.classList.toggle('has-text', val.length > 0);
searchTimer = setTimeout(() => {
state.search = val;
renderNoteList();
}, 120);
});
els.searchClear.addEventListener('click', () => {
els.globalSearch.value = '';
els.searchBox.classList.remove('has-text');
state.search = '';
renderNoteList();
els.globalSearch.focus();
});
/* ---------- (10) ACTIONS ---------- */
function createNoteFromButton() {
let notebookId;
if (state.view && state.view !== 'all' && state.view !== 'trash') {
notebookId = state.view;
} else if (state.selectedNoteId) {
notebookId = findNote(state.selectedNoteId).notebookId;
} else {
notebookId = DB.notebooks[0] && DB.notebooks[0].id;
if (!notebookId) notebookId = createNotebook('Notes').id;
}
const n = createNote(notebookId);
noteOrder(n);
renderSidebar();
renderNoteList();
selectNote(n.id);
els.noteTitleInput.focus();
}
function noteOrder(note) {
const siblings = DB.notes.filter(n => n.notebookId === note.notebookId);
note.order = siblings.length;
}
els.newNoteBtn.addEventListener('click', createNoteFromButton);
els.emptyNewBtn.addEventListener('click', createNoteFromButton);
function pinToggle() {
if (!activeNote) return;
activeNote.pinned = !activeNote.pinned;
activeNote.updatedAt = Date.now();
save();
saveIndicator('saved');
updatePinBtn();
renderSidebar();
renderNoteList();
}
function updatePinBtn() {
if (activeNote) {
els.pinBtn.textContent = activeNote.pinned ? '★' : '☆';
els.pinBtn.style.color = activeNote.pinned ? 'var(--pin)' : '';
}
}
els.pinBtn.addEventListener('click', pinToggle);
function duplicateNote() {
if (!activeNote) return;
const copy = JSON.parse(JSON.stringify(activeNote));
const newId = uid();
const idx = DB.notes.indexOf(activeNote);
const dup = {
...copy,
id: newId,
title: activeNote.title + ' (copy)',
createdAt: Date.now(),
updatedAt: Date.now(),
pinned: false,
deleted: false,
};
DB.notes.splice(idx + 1, 0, dup);
save();
renderSidebar();
renderNoteList();
selectNote(newId);
toast('Note duplicated');
}
els.dupBtn.addEventListener('click', duplicateNote);
function deleteNote() {
if (!activeNote) return;
const wasTrash = state.view === 'trash';
if (wasTrash) {
if (!confirm('Permanently delete this note? This cannot be undone.')) return;
DB.notes = DB.notes.filter(n => n.id !== activeNote.id);
toast('Permanently deleted');
} else {
if (!confirm('Move this note to Trash?')) return;
activeNote.deleted = true;
activeNote.updatedAt = Date.now();
toast('Moved to Trash');
}
const idx = DB.notes.indexOf(activeNote);
DB.selectedNoteId = null;
state.selectedNoteId = null;
activeNote = null;
els.emptyState.classList.remove('hidden');
els.editorContainer.classList.add('hidden');
els.tagEditor.classList.add('hidden');
els.breadcrumb.innerHTML = '';
save();
renderSidebar();
renderNoteList();
if (idx > 0) {
const prev = DB.notes[idx - 1];
if (prev && !prev.deleted) selectNote(prev.id);
}
}
els.delBtn.addEventListener('click', deleteNote);
/* ---- Move menu ---- */
function buildMoveMenu() {
const items = DB.notebooks.filter(nb => nb.id !== (activeNote && activeNote.notebookId))
.sort((a,b) => a.order - b.order);
els.moreMenu.innerHTML = `
<div class="dd-heading">Move to</div>
${items.length ? items.map(nb =>
`<button class="dd-item ${activeNote && activeNote.notebookId === nb.id ? 'active-notebook' : ''}" data-move="${nb.id}">
<span class="dd-ico">📓</span>${escapeHtml(nb.name)}
</button>`).join('') : '<div class="dd-item" disabled>No notebooks</div>'}
<div class="dd-sep"></div>
<button class="dd-item" data-move-current><span class="dd-ico">📓</span>Current notebook</button>
<div class="dd-sep"></div>
<button class="dd-item danger" data-delete-note><span class="dd-ico">🗑</span>${state.view === 'trash' ? 'Permanently delete' : 'Delete to Trash'}</button>
`;
els.moreMenu.querySelectorAll('[data-move]').forEach(b => b.addEventListener('click', () => {
if (!activeNote) return;
activeNote.notebookId = b.dataset.move;
activeNote.updatedAt = Date.now();
save();
toast('Moved to notebook');
renderSidebar();
renderNoteList();
renderBreadcrumb();
closeMoveMenu();
}));
els.moreMenu.querySelectorAll('[data-move-current]').forEach(b => b.addEventListener('click', () => {
closeMoveMenu();
}));
els.moreMenu.querySelectorAll('[data-delete-note]').forEach(b => b.addEventListener('click', () => {
closeMoveMenu();
deleteNote();
}));
}
function closeMoveMenu() { els.moreMenu.classList.remove('open'); }
els.moveBtn.addEventListener('click', (e) => {
e.stopPropagation();
buildMoveMenu();
els.moreMenu.classList.toggle('open');
});
els.moreBtn.addEventListener('click', (e) => {
e.stopPropagation();
buildMoveMenu();
els.moreMenu.classList.toggle('open');
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.menu-wrap')) closeMoveMenu();
});
els.moreMenu.addEventListener('mousedown', (e) => e.stopPropagation());
/* ---------- (11) DRAG-DROP between sidebar notebooks ---------- */
els.notebooks.addEventListener('dragover', (e) => {
const row = e.target.closest('[data-notebook-row]');
$$('#notebooks .notebook-row').forEach(r => r.classList.remove('drag-over'));
if (row && !e.target.closest('.sub-note')) {
row.classList.add('drag-over');
e.preventDefault();
}
});
els.notebooks.addEventListener('dragleave', (e) => {
if (!els.notebooks.contains(e.relatedTarget)) {
$$('#notebooks .notebook-row').forEach(r => r.classList.remove('drag-over'));
}
});
els.notebooks.addEventListener('drop', (e) => {
const row = e.target.closest('[data-notebook-row]');
$$('#notebooks .notebook-row').forEach(r => r.classList.remove('drag-over'));
if (!row) return;
const nbId = row.dataset.notebookRow;
if (state.view === 'trash' || state.view === nbId) return;
const noteId = e.dataTransfer.getData('text/plain') || els.dragNoteId;
const note = findNote(noteId);
if (!note) return;
const wasTrash = (state.view === 'trash');
if (wasTrash) {
note.deleted = false;
}
note.notebookId = nbId;
note.updatedAt = Date.now();
save();
renderSidebar();
renderNoteList();
toast('Note moved');
});
/* Trash drop: restore */
els.navTrash.addEventListener('dragover', (e) => {
e.preventDefault();
});
els.navTrash.addEventListener('drop', (e) => {
e.preventDefault();
const noteId = e.dataTransfer.getData('text/plain') || els.dragNoteId;
const note = findNote(noteId);
if (!note) return;
note.deleted = true;
note.updatedAt = Date.now();
save();
renderSidebar();
renderNoteList();
toast('Moved to Trash');
});
/* Double-click notebook row to create note there */
els.notebooks.addEventListener('dblclick', (e) => {
const row = e.target.closest('[data-notebook-row]');
if (!row) return;
const nbId = row.dataset.notebookRow;
const n = createNote(nbId);
noteOrder(n);
renderSidebar();
renderNoteList();
selectNote(n.id);
});
/* ---------- (13) INIT ---------- */
function updateThemeIcon() {
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
els.themeToggle.textContent = dark ? '☀' : '☾';
}
els.themeToggle.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('notesapp.theme', next);
updateThemeIcon();
});
function init() {
cacheEls();
renderSortButtons();
let loaded = load();
if (!loaded) {
// seed demo data
const nb = createNotebook('Getting Started');
createDemoNotes(nb.id);
renderSidebar();
renderNoteList();
save();
return;
}
// restore theme
const theme = localStorage.getItem('notesapp.theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
updateThemeIcon();
// restore view
if (state.view && state.view !== 'all') {
if (findNotebook(state.view)) {
els.listTitle.textContent = findNotebook(state.view).name;
} else {
state.view = 'all';
els.listTitle.textContent = 'All Notes';
}
} else if (state.view === 'trash') {
els.listTitle.textContent = 'Trash';
} else {
els.listTitle.textContent = 'All Notes';
}
renderSidebar();
renderNoteList();
if (state.selectedNoteId) selectNote(state.selectedNoteId);
}
function createDemoNotes(nbId) {
const n1 = createNote(nbId);
n1.title = 'Welcome to Notes';
n1.body = '<h1>Welcome 👋</h1><p>This is a <strong>polished</strong> notes app. Try <em>formatting</em>, checklists, and <code>inline code</code>.</p><ul><li>Click a heading, or type <code>#</code> at line start</li><li>Type <code>-</code> for a bullet list</li><li>Type <code>[]</code> for a checklist</li></ul><p>Everything saves automatically to your browser.</p>';
n1.tags = ['welcome', 'tutorial'];
n1.pinned = true;
const n2 = createNote(nbId);
n2.title = 'Markdown shortcuts';
n2.body = '<h2>Try these</h2><ul><li><span class="checkbox" data-checked="false" contenteditable="false"></span><span class="check-text" contenteditable="true">Type <code>[]</code> then space for a checklist</span></li><li><span class="checkbox" data-checked="true" contenteditable="false">✓</span><span class="check-text" contenteditable="true">Checkboxes persist on reload</span></li><li><span class="checkbox" data-checked="false" contenteditable="false"></span><span class="check-text" contenteditable="true">Type <code>></code> for a blockquote</span></li></ul><p>Start writing your own notes here.</p>';
n2.tags = ['tutorial'];
const n3 = createNote(nbId);
n3.title = 'Shopping list';
n3.body = '<h2>For the weekend</h2><ul><li><span class="checkbox" data-checked="false" contenteditable="false"></span><span class="check-text" contenteditable="true">Coffee beans</span></li><li><span class="checkbox" data-checked="false" contenteditable="false"></span><span class="check-text" contenteditable="true">Oat milk</span></li><li><span class="checkbox" data-checked="true" contenteditable="false">✓</span><span class="check-text" contenteditable="true">Sourdough bread</span></li></ul>';
n3.tags = ['personal'];
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"57a15267-e70b-4b37-ad54-4321e232a6db","tokensIn":10654024,"tokensOut":77406,"tokensTotal":10731430,"cost":0,"turns":119,"toolCalls":118,"failedToolCalls":3,"timestamp":"2026-09-04T20:41:40.213Z"} -->