Notes
Qwen 3.8 27B GGUF · typescript

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 129,173 bytes · SHA-256 cbdea950a4f9
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notely — Notes</title>
<style>
/* ── css: base ── */
/* ============ RESET & TOKENS ============ */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
:root {
/* fallbacks (JS injects the full palette from constants) */
--bg-app:#f7f5f2; --bg-sidebar:#f0ede8; --bg-list:#f4f1ec; --bg-editor:#fdfcfa;
--tx-1:#2b2723; --tx-2:#6e675e; --tx-3:#a49a8d;
--border:#e4e0d7; --border-soft:#ece8e1;
--hover:rgba(43,39,35,.05); --active:rgba(168,86,46,.10);
--accent:#a8562e; --accent-ink:#8f4a27; --accent-soft:rgba(168,86,46,.12);
--mark:#fbe58f; --mark-ink:#4a3d10;
--shadow:0 6px 24px rgba(43,39,35,.16);
--font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--font-read:"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Charter,Georgia,"Times New Roman",serif;
--font-mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
color-scheme: light;
}
html[data-theme="dark"] { color-scheme: dark; }
body {
font-family: var(--font-ui);
font-size: 13.5px;
color: var(--tx-1);
background: var(--bg-app);
overflow: hidden;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
button { font-family: inherit; font-size: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
input, select { font-family: inherit; color: inherit; }
::selection { background: var(--accent-soft); }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--tx-3); border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-track { background: transparent; }
mark { background: var(--mark); color: var(--mark-ink); border-radius: 3px; padding: 0 1px; }
/* ── css: layout ── */
/* ============ APP SHELL / THREE PANES ============ */
#app { display: flex; height: 100vh; width: 100vw; position: relative; }
.pane { min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
.sidebar {
background: var(--bg-sidebar);
border-right: 1px solid var(--border);
flex: 0 0 auto;
width: 256px;
transition: width .18s ease;
}
#app.sb-collapsed .sidebar { width: 0 !important; border-right: none; }
.list-pane {
background: var(--bg-list);
border-right: 1px solid var(--border);
width: 324px;
flex: 0 0 auto;
}
.editor-pane { flex: 1 1 auto; background: var(--bg-editor); position: relative; }
.resize-handle {
position: absolute; top: 0; bottom: 0; width: 6px; margin-left: -3px;
cursor: col-resize; z-index: 30;
}
.resize-handle:hover, .resizing .resize-handle { background: var(--accent-soft); }
body.resizing { cursor: col-resize; user-select: none; }
body.resizing .pane { transition: none; }
/* ============ SIDEBAR ============ */
.sidebar-head {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 10px 10px 16px;
}
.brand { display: flex; align-items: center; gap: 9px; user-select: none; }
.brand-mark {
width: 24px; height: 24px; border-radius: 7px;
background: var(--accent); color: #fff;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 13px; letter-spacing: .2px;
box-shadow: inset 0 -6px 12px rgba(0,0,0,.14);
}
.brand-name { font-size: 15px; font-weight: 650; letter-spacing: .01em; }
.icon-btn {
width: 26px; height: 26px; border-radius: 7px;
display: inline-flex; align-items: center; justify-content: center;
color: var(--tx-2); flex: 0 0 auto;
transition: background .12s ease, color .12s ease;
}
.icon-btn:hover { background: var(--hover); color: var(--tx-1); }
.icon-btn:active { transform: scale(.94); }
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.sb-section-label {
font-size: 10.5px; font-weight: 650; letter-spacing: .09em; text-transform: uppercase;
color: var(--tx-3); padding: 14px 16px 5px;
display: flex; align-items: center; gap: 5px;
}
.sb-section-label svg { opacity: .8; }
.side-row {
display: flex; align-items: center; gap: 9px;
margin: 1px 8px; padding: 6px 8px; border-radius: 8px;
color: var(--tx-2); cursor: pointer; user-select: none;
transition: background .12s ease, color .12s ease;
}
.side-row:hover { background: var(--hover); color: var(--tx-1); }
.side-row.active { background: var(--active); color: var(--accent-ink); font-weight: 550; }
.row-icon { display: inline-flex; width: 18px; justify-content: center; color: inherit; }
.side-row.active .row-icon { color: var(--accent); }
.row-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.row-count { font-size: 11px; color: var(--tx-3); font-variant-numeric: tabular-nums; }
.side-row.active .row-count { color: var(--accent-ink); opacity: .75; }
.tags-block { display: flex; flex-direction: column; align-items: flex-start; gap: 5px; padding: 2px 14px 0; }
.sidebar-foot { margin-top: auto; border-top: 1px solid var(--border-soft); padding-top: 8px; }
/* tags chips */
.tag-chip {
display: inline-flex; align-items: center; gap: 3px;
font-size: 11.5px; color: var(--tx-2);
background: transparent; border: 1px solid var(--border);
padding: 2px 8px; border-radius: 999px;
transition: background .12s ease, color .12s, border-color .12s;
}
.tag-chip:hover { background: var(--hover); color: var(--tx-1); border-color: var(--tx-3); }
.tag-chip.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-ink); font-weight: 550; }
.tag-chip.add { border-style: dashed; color: var(--tx-3); }
.tag-x {
width: 14px; height: 14px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
color: var(--tx-3); opacity: 0; transition: opacity .12s, background .12s;
}
.tag-chip:hover .tag-x { opacity: 1; }
.tag-x:hover { background: var(--hover); color: var(--tx-1); }
.tag-chip-label { cursor: pointer; }
/* ============ NOTEBOOKS (sidebar) ============ */
.notebooks-block { padding-bottom: 6px; }
.nb-row { margin: 1px 8px; border-radius: 8px; transition: background .12s; }
.nb-row.drop-target { background: var(--accent-soft); outline: 1px dashed var(--accent); outline-offset: -2px; }
.nb-head {
display: flex; align-items: center; gap: 6px;
padding: 6px 8px; border-radius: 8px; cursor: pointer; user-select: none;
color: var(--tx-2);
}
.nb-head:hover { background: var(--hover); color: var(--tx-1); }
.nb-row.active > .nb-head { background: var(--active); color: var(--accent-ink); }
.nb-row.active > .nb-head .nb-icon, .nb-row.active > .nb-head .nb-chev { color: var(--accent); }
.nb-chev {
width: 18px; height: 18px; border-radius: 5px;
display: inline-flex; align-items: center; justify-content: center;
transition: transform .16s ease, background .12s;
flex: 0 0 auto;
}
.nb-chev:hover { background: var(--hover); }
.nb-row.collapsed .nb-chev { transform: rotate(-90deg); }
.nb-icon { display: inline-flex; flex: 0 0 auto; }
.nb-name {
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-weight: 550;
}
.nb-menu { opacity: 0; transition: opacity .12s; }
.nb-head:hover .nb-menu { opacity: 1; }
.nb-notes { padding: 1px 0 3px 24px; display: flex; flex-direction: column; gap: 1px; }
.nb-empty { font-size: 12px; color: var(--tx-3); padding: 3px 8px; font-style: italic; }
.mini-note {
display: flex; align-items: center; gap: 6px;
padding: 4px 8px; border-radius: 6px; cursor: pointer;
color: var(--tx-2); font-size: 12.5px;
transition: background .12s ease, color .12s;
}
.mini-note:hover { background: var(--hover); color: var(--tx-1); }
.mini-note.selected { background: var(--active); color: var(--accent-ink); }
.mini-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mini-pin { color: var(--accent); display: inline-flex; flex: 0 0 auto; }
/* ============ SIDEBAR TOGGLE (collapsed rail) ============ */
.sb-toggle {
position: absolute; top: 12px; left: 12px; z-index: 5;
width: 30px; height: 30px; border-radius: 8px;
display: none; align-items: center; justify-content: center;
color: var(--tx-2); background: var(--bg-list); border: 1px solid var(--border);
transition: background .12s, color .12s;
}
.sb-toggle:hover { color: var(--tx-1); background: var(--hover); }
#app.sb-collapsed .sb-toggle { display: inline-flex; }
/* ── css: list ── */
.search-wrap {
display: flex; align-items: center; gap: 6px;
background: var(--bg-app); border: 1px solid var(--border);
border-radius: 8px; padding: 5px 9px;
color: var(--tx-3);
transition: border-color .12s, box-shadow .12s;
flex: 1 1 auto; min-width: 84px; max-width: 170px;
margin-left: 4px;
}
.search-wrap:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.search-wrap svg { flex: 0 0 auto; }
.search-input { border: none; outline: none; background: transparent; width: 100%; font-size: 12.5px; color: var(--tx-1); min-width: 0; }
.search-input::placeholder { color: var(--tx-3); }
/* ── css: editor ── */
/* ============ EDITOR PANE ============ */
.theme-btn {
position: absolute; top: 10px; right: 10px; z-index: 20;
width: 30px; height: 30px; border-radius: 8px;
background: var(--bg-list); border: 1px solid var(--border);
transition: background .12s, color .12s, transform .3s ease;
}
.theme-btn:hover { color: var(--tx-1); background: var(--hover); }
.theme-btn:active { transform: rotate(40deg); }
.toolbar {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; gap: 1px; flex-wrap: wrap;
padding: 8px 48px 8px 14px;
background: var(--bg-editor);
border-bottom: 1px solid var(--border-soft);
}
.tb-btn {
width: 28px; height: 28px; border-radius: 7px;
display: inline-flex; align-items: center; justify-content: center;
color: var(--tx-2);
transition: background .1s, color .1s;
}
.tb-btn:hover { background: var(--hover); color: var(--tx-1); }
.tb-btn.on { background: var(--accent-soft); color: var(--accent-ink); }
.tb-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
.saved-ind {
position: absolute; top: 14px; right: 48px; z-index: 20;
font-size: 11px; font-weight: 550; color: var(--accent-ink);
background: var(--accent-soft); padding: 3px 9px; border-radius: 999px;
opacity: 0; transform: translateY(-3px);
transition: opacity .25s ease, transform .25s ease;
pointer-events: none;
}
.saved-ind.show { opacity: 1; transform: none; }
.note-view { height: 100%; display: flex; flex-direction: column; overflow: hidden; }
.note-meta {
padding: 10px 26px 0; font-size: 11.5px; color: var(--tx-3);
letter-spacing: .01em; user-select: none;
}
.editor-root {
flex: 1; overflow-y: auto; position: relative;
padding: 12px 26px 40vh;
font-family: var(--font-read);
font-size: 15.5px; line-height: 1.72;
color: var(--tx-1);
caret-color: var(--accent);
outline: none;
}
.editor-root:has(> p:only-child:has(> br:only-child))::before {
content: "Start writing… (try “# ”, “- ” or “[] ” at the start of a line)";
position: absolute; top: 12px; left: 26px;
color: var(--tx-3); font-size: 15.5px;
pointer-events: none;
}
.editor-root > * { margin: 0 0 .55em; }
.editor-root p { min-height: 1.72em; }
.editor-root h1 { font-size: 1.72em; font-weight: 700; line-height: 1.25; margin: .4em 0 .5em; letter-spacing: -.01em; }
.editor-root h2 { font-size: 1.36em; font-weight: 680; line-height: 1.3; margin: .7em 0 .45em; letter-spacing: -.005em; }
.editor-root h3 { font-size: 1.13em; font-weight: 660; line-height: 1.35; margin: .7em 0 .4em; }
.editor-root blockquote {
margin: .8em 0; padding: .15em 0 .15em 18px;
border-left: 3px solid var(--accent);
color: var(--tx-2);
}
.editor-root blockquote > * { margin-bottom: .3em; }
.editor-root ul, .editor-root ol { padding-left: 1.5em; margin: .3em 0 .6em; }
.editor-root li { margin: .18em 0; }
.editor-root ol { list-style: decimal; }
.editor-root ul { list-style: none; }
.editor-root li:not([data-check])::before { content: "•"; color: var(--accent); margin-right: .55em; display: inline-block; }
.editor-root li li:not([data-check])::before { content: "◦"; }
.editor-root hr { border: none; border-top: 1px solid var(--border); margin: 1.3em 0; }
.editor-root code {
font-family: var(--font-mono); font-size: .82em;
background: var(--hover); border: 1px solid var(--border-soft);
padding: .1em .35em; border-radius: 5px; color: var(--accent-ink);
}
.editor-root b, .editor-root strong { font-weight: 700; }
.editor-root input { display: none; }
/* checklist rows (li[data-check]) */
.editor-root li[data-check] { display: block; margin: .22em 0; list-style: none; }
.editor-root li[data-check]::before { content: none; }
.ck-label { display: flex; align-items: flex-start; gap: 9px; cursor: pointer; }
.ck-box {
flex: 0 0 auto; margin-top: .28em;
width: 17px; height: 17px; border-radius: 5px;
border: 1.6px solid var(--tx-3);
display: inline-flex; align-items: center; justify-content: center;
transition: background .13s ease, border-color .13s ease;
}
.ck-box::after {
content: "✓"; font-size: 11px; font-weight: 800; color: #fff;
transform: scale(0); transition: transform .14s cubic-bezier(.34,1.56,.64,1);
font-family: var(--font-ui);
}
.editor-root li[data-check="true"] .ck-box { background: var(--accent); border-color: var(--accent); }
.editor-root li[data-check="true"] .ck-box::after { transform: scale(1); }
.ck-text { flex: 1; }
.editor-root li[data-check="true"] .ck-text { color: var(--tx-3); text-decoration: line-through; text-decoration-color: var(--tx-3); }
/* ============ TAG ROW (under editor) ============ */
.tag-row { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 26px 0; }
.tag-row:empty { padding: 0; }
/* ============ EMPTY STATE ============ */
.empty-state {
position: absolute; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 8px; color: var(--tx-3); text-align: center;
animation: fade-in .35s ease;
}
@keyframes fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.empty-mark {
width: 58px; height: 58px; border-radius: 16px;
background: var(--accent-soft); color: var(--accent);
display: flex; align-items: center; justify-content: center;
margin-bottom: 8px;
}
.empty-mark svg { width: 26px; height: 26px; }
.empty-title { font-family: var(--font-read); font-size: 21px; font-weight: 640; color: var(--tx-2); letter-spacing: .01em; }
.empty-sub { font-size: 13px; max-width: 320px; line-height: 1.5; }
.primary-btn {
margin-top: 16px;
background: var(--accent); color: #fff;
font-size: 13px; font-weight: 600;
padding: 8px 20px; border-radius: 9px;
box-shadow: 0 2px 8px rgba(168,86,46,.35);
transition: transform .12s ease, box-shadow .12s ease, background .12s;
}
.primary-btn:hover { background: var(--accent-ink); transform: translateY(-1px); box-shadow: 0 4px 14px rgba(168,86,46,.4); }
.primary-btn:active { transform: translateY(0); }
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./editor":"src/editor.ts","./view":"src/view.ts","./sanitize":"src/sanitize.ts","./types":"src/types.ts"},"src/state.ts":{"./constants":"src/constants.ts","./types":"src/types.ts","./utils":"src/utils.ts","./sanitize":"src/sanitize.ts"},"src/editor.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./sanitize":"src/sanitize.ts"},"src/view.ts":{"./constants":"src/constants.ts","./types":"src/types.ts","./utils":"src/utils.ts","./sanitize":"src/sanitize.ts","./state":"src/state.ts","./editor":"src/editor.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// MAIN — entry point: seed data, build DOM, wire the editor
// and the store, kick off the render loop.
// ============================================================
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const S = __importStar(require("./state"));
const editor_1 = require("./editor");
const view_1 = require("./view");
const sanitize_1 = require("./sanitize");
function seed() {
const t0 = Date.now() - 86400000 * 12;
const nbWork = { id: 'nb-work', name: 'Work', collapsed: false };
const nbLife = { id: 'nb-life', name: 'Life', collapsed: false };
const n1 = {
id: 'seed-1', notebookId: 'nb-work', pinned: true,
createdAt: t0, updatedAt: Date.now() - 3600000 * 2,
trashedAt: null, trashedFrom: null, tags: ['ideas'],
body: `<h1>Welcome to Notely</h1>
<p>A quiet place to think. Everything you write is saved <b>automatically</b> — look for the “Saved” pulse up top.</p>
<h2>Things that work well</h2>
<ul><li>Full rich-text editing, with a <i>serif reading face</i></li><li>Search that finds text <u>inside</u> bold spans</li><li>Tags, pins, notebooks and a Trash</li></ul>
<h2>Keyboard-first</h2>
<ul>
<li data-check="false"><label class="ck-label"><span class="ck-box"><input class="ck-cb" type="checkbox"></span><span class="ck-text">Try <code>Ctrl</code>+<code>B</code> for bold</span></label><br></li>
<li data-check="true"><label class="ck-label"><span class="ck-box"><input class="ck-cb" type="checkbox" checked></span><span class="ck-text">This one is already done</span></label><br></li>
<li data-check="false"><label class="ck-label"><span class="ck-box"><input class="ck-cb" type="checkbox"></span><span class="ck-text">Click a box to check it — the state persists</span></label><br></li>
</ul>
<blockquote><p>“The scariest moment is always just before you start.”</p></blockquote>
<hr>
<p>Created on day one of this notebook.</p>`,
};
const n2 = {
id: 'seed-2', notebookId: 'nb-work', pinned: false,
createdAt: t0 + 86400000, updatedAt: Date.now() - 3600000 * 26,
trashedAt: null, trashedFrom: null, tags: ['meeting'],
body: `<p>Sync notes for <b>friday review</b>. Bring the roadmaps and the open risks.</p>
<p>- </p>`,
};
n2.body = `<p>Sync notes for <b>friday review</b>. Bring the roadmaps and the open risks.</p>
<ul><li>Roadmap — final pass</li><li>Risks — top three</li><li>Demo — two minutes max</li></ul>`;
const n3 = {
id: 'seed-3', notebookId: 'nb-life', pinned: false,
createdAt: t0 + 86400000 * 2, updatedAt: Date.now() - 86400000 * 1,
trashedAt: null, trashedFrom: null, tags: ['garden'],
body: `<p>What to plant in spring:</p>
<ul><li>Tomatoes — the big one</li><li>Basil, everywhere</li><li><s>Pumpkin</s> — no room this year</li></ul>
<p><i>Read more about raised beds before the soil arrives.</i></p>`,
};
const n4 = {
id: 'seed-4', notebookId: null, pinned: false,
createdAt: t0 + 86400000 * 3, updatedAt: Date.now() - 86400000 * 2,
trashedAt: null, trashedFrom: null, tags: ['draft'],
body: `<p>A longish snippet to prove the list preview truncates nicely: it keeps the first hundred and fifty characters of the <b>visible text</b>, strips every tag, and never lets markup leak into the snippet or the search index. This sentence is deliberately a bit longer so the ellipsis shows up. Done.</p>`,
};
const app = S.getState();
app.notebooks = [nbWork, nbLife];
app.notes = [n1, n2, n3, n4];
app.ui.view = { type: 'all' };
app.ui.selectedNoteId = n1.id;
}
function start() {
// 1. state: try storage, else seed
const loaded = S.loadFromStorage();
if (!loaded)
seed();
S.persistNow();
// 2. static shell
(0, view_1.buildView)();
// 3. editor: toolbar wiring
const ed = (0, editor_1.getEditor)();
const e = (0, view_1.getViewEls)();
const btnDefs = [
['bold', view_1.ICONS.bold], ['italic', view_1.ICONS.italic], ['underline', view_1.ICONS.underline],
['strike', view_1.ICONS.strike], ['code', view_1.ICONS.code],
['h1', view_1.ICONS.h1], ['h2', view_1.ICONS.h2], ['h3', view_1.ICONS.h3],
['ul', view_1.ICONS.ul], ['ol', view_1.ICONS.ol], ['checklist', view_1.ICONS.check],
['quote', view_1.ICONS.quote], ['hr', view_1.ICONS.hr],
];
const btnMap = {};
for (const [key, icon] of btnDefs) {
const b = document.createElement('button');
b.className = 'tb-btn';
b.innerHTML = icon;
b.title = titleFor(key);
b.type = 'button';
b.addEventListener('mousedown', (ev) => ev.preventDefault()); // keep editor focus
b.addEventListener('click', () => act(key));
e.toolbar.appendChild(b);
btnMap[key] = b;
}
ed.setToolbarCallback((st) => {
for (const key of Object.keys(btnMap)) {
btnMap[key].classList.toggle('on', !!st[key]);
}
});
function titleFor(key) {
switch (key) {
case 'bold': return 'Bold (Ctrl+B)';
case 'italic': return 'Italic (Ctrl+I)';
case 'underline': return 'Underline (Ctrl+U)';
case 'strike': return 'Strikethrough (Ctrl+Shift+S)';
case 'code': return 'Inline code (Ctrl+E)';
case 'h1': return 'Heading 1';
case 'h2': return 'Heading 2';
case 'h3': return 'Heading 3';
case 'ul': return 'Bulleted list';
case 'ol': return 'Numbered list';
case 'checklist': return 'Checklist';
case 'quote': return 'Quote';
case 'hr': return 'Divider';
default: return key;
}
}
function act(key) {
switch (key) {
case 'bold':
ed.applyInline('b');
break;
case 'italic':
ed.applyInline('i');
break;
case 'underline':
ed.applyInline('u');
break;
case 'strike':
ed.applyInline('s');
break;
case 'code':
ed.applyInline('code');
break;
case 'h1':
ed.setBlock('H1');
break;
case 'h2':
ed.setBlock('H2');
break;
case 'h3':
ed.setBlock('H3');
break;
case 'ul':
ed.toggleList('ul');
break;
case 'ol':
ed.toggleList('ol');
break;
case 'checklist':
ed.toggleChecklist();
break;
case 'quote':
ed.setBlock('BLOCKQUOTE');
break;
case 'hr':
ed.insertHR();
break;
}
}
// 4. editor extras: checkbox clicks + Ctrl+E code shortcut
e.editorRoot.addEventListener('click', (ev) => {
const t = ev.target;
if (t.classList && t.classList.contains('ck-cb')) {
ev.preventDefault();
const li = t.closest('li[data-check]');
if (li) {
ed.toggleCheck(li);
ev.stopPropagation();
}
}
});
e.editorRoot.addEventListener('keydown', (ev) => {
const mod = ev.metaKey || ev.ctrlKey;
if (mod && !ev.altKey && ev.key.toLowerCase() === 'e') {
ev.preventDefault();
ed.applyInline('code');
}
});
// 5. render loop
S.subscribe(() => {
(0, view_1.renderRoot)();
const ch = S.lastChange();
if (ch.kind === 'note' || ch.kind === 'boot')
(0, view_1.flashSaved)();
});
(0, view_1.renderRoot)();
// 6. window-level niceties
window.addEventListener('beforeunload', () => S.persistNow());
document.addEventListener('keydown', (ev) => {
const mod = ev.metaKey || ev.ctrlKey;
if (mod && ev.key.toLowerCase() === 'k') {
ev.preventDefault();
e.searchInput.focus();
e.searchInput.select();
}
});
// expose for automated probes
window.Notely = {
store: S,
editor: ed,
sanitize: sanitize_1.sanitizeHTML,
getState: S.getState,
};
}
start();
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.C = void 0;
// ============================================================
// TUNABLE CONSTANTS — adjust these to change the app's feel.
// ============================================================
exports.C = {
STORAGE_KEY: 'notely.state.v1',
/** Debounce (ms) between the last keystroke and the write to localStorage. */
AUTOSAVE_MS: 700,
/** Characters shown in a note-list snippet. */
SNIPPET_LEN: 150,
/** Max length of a derived note title. */
TITLE_LEN: 64,
/** Undo/redo history depth (snapshots). */
UNDO_LIMIT: 200,
/** Keystrokes inside this window (ms) merge into a single undo step. */
UNDO_TYPING_GAP_MS: 800,
SIDEBAR: { min: 210, max: 400, def: 256 },
LIST: { min: 250, max: 560, def: 324 },
FONT_UI: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`,
FONT_READ: `"Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Charter, Georgia, "Times New Roman", serif`,
FONT_MONO: `ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace`,
/** Full theme palettes; injected as CSS variables at startup.
* Keys are CSS variable names without the dash prefix. */
PALETTE: {
light: {
'bg-app': '#f7f5f2', 'bg-sidebar': '#f0ede8', 'bg-list': '#f4f1ec', 'bg-editor': '#fdfcfa',
'tx-1': '#2b2723', 'tx-2': '#6e675e', 'tx-3': '#a49a8d',
border: '#e4e0d7', 'border-soft': '#ece8e1',
hover: 'rgba(43,39,35,0.05)', active: 'rgba(168,86,46,0.10)',
accent: '#a8562e', 'accent-ink': '#8f4a27', 'accent-soft': 'rgba(168,86,46,0.12)',
mark: '#fbe58f', 'mark-ink': '#4a3d10',
shadow: '0 6px 24px rgba(43,39,35,0.16)',
},
dark: {
'bg-app': '#1e1b18', 'bg-sidebar': '#262220', 'bg-list': '#221f1c', 'bg-editor': '#2a2622',
'tx-1': '#ece7df', 'tx-2': '#a89f93', 'tx-3': '#6f675d',
border: '#3b352f', 'border-soft': '#322d28',
hover: 'rgba(236,231,223,0.06)', active: 'rgba(208,138,90,0.16)',
accent: '#d08a5a', 'accent-ink': '#e0a878', 'accent-soft': 'rgba(208,138,90,0.16)',
mark: 'rgba(240,215,120,0.28)', 'mark-ink': '#f0d98a',
shadow: '0 6px 24px rgba(0,0,0,0.5)',
},
},
};
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// DATA MODEL — the single source of truth.
// Everything here is JSON-serializable; the DOM is a projection
// of this state, never the other way around.
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.sameView = sameView;
function sameView(a, b) {
if (a.type !== b.type)
return false;
if (a.type === 'notebook' && b.type === 'notebook')
return a.id === b.id;
if (a.type === 'tag' && b.type === 'tag')
return a.tag === b.tag;
return true;
}
};
// ── module: src/utils.ts ──
__mods["src/utils.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// SMALL UTILITIES — ids, dom helpers, time formatting.
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.uid = uid;
exports.clamp = clamp;
exports.esc = esc;
exports.el = el;
exports.relTime = relTime;
exports.svgIcon = svgIcon;
function uid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 9);
}
function clamp(n, a, b) {
return Math.min(b, Math.max(a, n));
}
function esc(s) {
return s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function el(tag, opts = {}) {
const n = document.createElement(tag);
if (opts.cls)
n.className = opts.cls;
if (opts.text != null)
n.textContent = opts.text;
if (opts.html != null)
n.innerHTML = opts.html;
if (opts.title)
n.title = opts.title;
if (opts.attrs) {
for (const [k, v] of Object.entries(opts.attrs)) {
if (v == null)
n.removeAttribute(k);
else
n.setAttribute(k, String(v));
}
}
return n;
}
function relTime(ts) {
const diff = Date.now() - ts;
const m = Math.floor(diff / 60000);
if (m < 1)
return 'just now';
if (m < 60)
return m + 'm ago';
const h = Math.floor(m / 60);
if (h < 24)
return h + 'h ago';
const days = Math.floor(h / 24);
if (days === 1)
return 'yesterday';
if (days < 7)
return days + 'd ago';
const d = new Date(ts);
const sameYear = d.getFullYear() === new Date().getFullYear();
return d.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
...(sameYear ? {} : { year: 'numeric' }),
});
}
function svgIcon(path, size = 16, sw = 1.8) {
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">${path}</svg>`;
}
};
// ── module: src/sanitize.ts ──
__mods["src/sanitize.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// SANITIZER — the only gate through which HTML enters or
// leaves the model. Pasting and every editor commit pass
// through here, so stored content is always a safe subset:
//
// blocks: p, h1, h2, h3, ul, ol, li (checklist rows are
// li[data-check]), blockquote, hr
// inline: b, strong, i, em, u, s, strike, code, label, span
// special: input[type=checkbox] (only inside checklist rows)
// allowed attrs: data-check on li/div
//
// Everything else (scripts, styles, foreign markup, attributes,
// event handlers) is stripped; visible text inside it is kept.
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeHTML = sanitizeHTML;
exports.normalizeHTML = normalizeHTML;
exports.extractText = extractText;
exports.deriveTitle = deriveTitle;
const BLOCK_TAGS = new Set(['P', 'H1', 'H2', 'H3', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'HR', 'DIV']);
const INLINE_TAGS = new Set(['B', 'STRONG', 'I', 'EM', 'U', 'S', 'STRIKE', 'CODE', 'LABEL', 'SPAN']);
const ALLOWED = new Set([
'P', 'H1', 'H2', 'H3', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'HR', 'DIV',
'B', 'STRONG', 'I', 'EM', 'U', 'S', 'STRIKE', 'CODE', 'LABEL', 'SPAN',
]);
const DANGEROUS = new Set([
'SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'LINK', 'META', 'SVG',
'MATH', 'FORM', 'TEXTAREA', 'SELECT', 'BUTTON', 'VIDEO', 'AUDIO',
'NOSCRIPT', 'TEMPLATE', 'IMG', 'BASE', 'SOURCE', 'TRACK',
]);
/** Sanitize an HTML string. Returns a safe HTML string. */
function sanitizeHTML(dirty) {
if (!dirty)
return '';
const tpl = document.createElement('template');
tpl.innerHTML = dirty;
sanitizeNode(tpl.content);
return tpl.innerHTML;
}
function sanitizeNode(node) {
// Depth-first: process children first so removals are stable.
for (const child of Array.from(node.childNodes))
sanitizeNode(child);
if (node.nodeType !== Node.ELEMENT_NODE)
return;
const elem = node;
const tag = elem.tagName;
if (DANGEROUS.has(tag)) {
elem.remove();
return;
}
if (tag === 'BR') {
while (elem.hasAttributes())
elem.removeAttribute(elem.attributes[0].name);
return;
}
if (tag === 'INPUT') {
const isCheckbox = (elem.getAttribute('type') ?? '').toLowerCase() === 'checkbox';
while (elem.hasAttributes())
elem.removeAttribute(elem.attributes[0].name);
if (!isCheckbox) {
elem.remove();
return;
}
elem.setAttribute('type', 'checkbox');
return;
}
if (!ALLOWED.has(tag)) {
// Unknown element: replace with its (already sanitized) children.
const parent = elem.parentNode;
if (!parent)
return;
while (elem.firstChild)
parent.insertBefore(elem.firstChild, elem);
elem.remove();
return;
}
// Strip every attribute; re-apply the single whitelisted one.
const check = (tag === 'LI' || tag === 'DIV') ? elem.dataset.check : null;
while (elem.hasAttributes())
elem.removeAttribute(elem.attributes[0].name);
if (check === 'true' || check === 'false')
elem.dataset.check = check;
}
/* ---------------------------------------------------------- */
/* structural normalization */
/* ---------------------------------------------------------- */
function isBlock(n) {
return n.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has(n.tagName);
}
function endsWithBlockOrBr(n) {
const last = n.lastChild;
if (!last)
return false;
if (isBlock(last))
return true;
if (last.nodeType === Node.ELEMENT_NODE && last.tagName === 'BR')
return true;
return false;
}
/**
* Structural normalization: browsers collapse empty blocks and merge
* lone text runs, which silently breaks markdown shortcuts, Enter
* behavior and persistence. This enforces a canonical shape:
* - content is always >= 1 block element
* - bare text inside list/quote/root becomes a <p> (LI keeps raw text)
* - blocks can park a caret (trailing <br> when empty)
*/
function normalizeHTML(html) {
if (!html || !html.trim())
return '<p><br></p>';
const tpl = document.createElement('template');
tpl.innerHTML = html;
const root = tpl.content;
normalizeNode(root);
if (!Array.from(root.children).some(c => isBlock(c))) {
const p = document.createElement('p');
while (root.firstChild)
p.appendChild(root.firstChild);
if (!p.textContent && !p.querySelector('br'))
p.appendChild(document.createElement('br'));
root.appendChild(p);
}
return tpl.innerHTML;
}
function normalizeNode(node) {
const parentTag = node.tagName ?? '';
const children = Array.from(node.childNodes);
// containers that may only hold blocks: bare text there must become <p>
const wrapParents = new Set(['UL', 'OL', 'BLOCKQUOTE', 'BODY', '#DOCUMENT_FRAGMENT', '']);
for (const child of children) {
if (child.nodeType === Node.TEXT_NODE) {
const txt = child.data;
if (parentTag === 'LI') {
// bare text inside an <li> is valid content
if (!/\S/.test(txt))
child.remove();
continue;
}
if (!/\S/.test(txt)) {
// whitespace: strip between blocks / in containers, keep inside inlines
if (wrapParents.has(parentTag))
child.remove();
continue;
}
if (wrapParents.has(parentTag)) {
const p = document.createElement('p');
node.insertBefore(p, child);
p.appendChild(child);
continue;
}
// text inside P / H* / DIV / inlines is legal — leave it alone
}
else if (child.nodeType === Node.ELEMENT_NODE) {
const e = child;
if (isBlock(e)) {
normalizeNode(e);
}
else if (e.tagName === 'INPUT' || e.tagName === 'BR') {
// keep as-is
}
else if (INLINE_TAGS.has(e.tagName)) {
// hoist blocks smuggled inside an inline, out to after it
const smuggled = Array.from(e.childNodes).filter(k => k.nodeType === Node.ELEMENT_NODE && isBlock(k));
for (const k of smuggled) {
const next = e.nextSibling;
e.parentNode?.insertBefore(k, next);
}
}
}
}
// trailing-br guarantees so a caret can always park
const tag = node.tagName ?? '';
if (tag === 'LI' && !endsWithBlockOrBr(node)) {
node.appendChild(document.createElement('br'));
}
if ((tag === 'DIV' || tag === 'LI') && node.dataset.check !== undefined && !endsWithBlockOrBr(node)) {
node.appendChild(document.createElement('br'));
}
if ((tag === 'UL' || tag === 'OL') && node.childNodes.length === 0) {
node.remove(); // empty list is meaningless
return;
}
if (BLOCK_TAGS.has(tag) && tag !== 'LI' && tag !== 'DIV' && tag !== 'BLOCKQUOTE' && node.childNodes.length === 0) {
node.appendChild(document.createElement('br'));
}
if (tag === 'BLOCKQUOTE' && !Array.from(node.children).some(c => isBlock(c))) {
const p = document.createElement('p');
p.appendChild(document.createElement('br'));
node.appendChild(p);
}
}
/* ---------------------------------------------------------- */
/* text extraction (search + snippets) */
/* ---------------------------------------------------------- */
/**
* Pure text extraction from stored HTML — feeds search and snippets.
* Tags vanish; only visible characters remain (plus a space between
* blocks), so a search for "b" or "div" can never match a tag name.
*/
function extractText(html) {
if (!html)
return '';
const tpl = document.createElement('template');
tpl.innerHTML = html;
const out = [];
const walk = (node) => {
for (const child of Array.from(node.childNodes)) {
if (child.nodeType === Node.TEXT_NODE) {
out.push(child.data);
}
else if (child.nodeType === Node.ELEMENT_NODE) {
const tag = child.tagName;
if (tag === 'BR') {
out.push(' ');
continue;
}
if (tag === 'INPUT')
continue; // checkboxes have no text
walk(child);
if (isBlock(child))
out.push(' ');
}
}
};
walk(tpl.content);
return out.join('').replace(/[ \t]+/g, ' ').replace(/ ?\s ?/g, ' ').trim();
}
/** First text line of a note — used for derived titles. */
function deriveTitle(html) {
const t = extractText(html);
const line = t.split(/\s{2,}|\n/)[0] || '';
return line.slice(0, 64);
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// STATE — the single source of truth: an App object plus
// pure mutators. The store emits 'change' events; the view
// layer listens, decides what changed (via last-change kind)
// and re-renders only what's needed. Persistence: debounced
// write-through to localStorage.
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.getState = void 0;
exports.subscribe = subscribe;
exports.lastChange = lastChange;
exports.emitChange = emitChange;
exports.persistNow = persistNow;
exports.loadFromStorage = loadFromStorage;
exports.createNotebook = createNotebook;
exports.renameNotebook = renameNotebook;
exports.deleteNotebook = deleteNotebook;
exports.toggleNotebookCollapsed = toggleNotebookCollapsed;
exports.noteById = noteById;
exports.createNote = createNote;
exports.updateNoteContent = updateNoteContent;
exports.duplicateNote = duplicateNote;
exports.trashNote = trashNote;
exports.restoreNote = restoreNote;
exports.destroyNote = destroyNote;
exports.moveNote = moveNote;
exports.setPinned = setPinned;
exports.setNoteTags = setNoteTags;
exports.visibleNotes = visibleNotes;
exports.setView = setView;
exports.selectNote = selectNote;
exports.setSort = setSort;
exports.setTheme = setTheme;
exports.setSidebarOpen = setSidebarOpen;
exports.setPaneWidth = setPaneWidth;
exports.commitLayout = commitLayout;
const constants_1 = require("./constants");
const types_1 = require("./types");
const utils_1 = require("./utils");
const sanitize_1 = require("./sanitize");
function titleOf(n) {
return (0, sanitize_1.deriveTitle)(n.body) || 'Untitled';
}
const store = {
app: blank(),
last: { kind: 'boot' },
persistScheduled: false,
version: 0,
};
function blank() {
return {
notebooks: [],
notes: [],
ui: {
theme: 'light',
sort: 'modified',
view: { type: 'all' },
selectedNoteId: null,
sidebarOpen: true,
sidebarW: constants_1.C.SIDEBAR.def,
listW: constants_1.C.LIST.def,
},
};
}
const getState = () => store.app;
exports.getState = getState;
const listeners = new Set();
function subscribe(fn) {
listeners.add(fn);
return () => { listeners.delete(fn); };
}
function emit(kind, noteId) {
store.last = { kind, noteId };
store.version++;
for (const fn of listeners)
fn();
}
function lastChange() {
return store.last;
}
/** For external modules (view/main) to trigger a full re-render. */
function emitChange(kind, noteId) {
emit(kind, noteId);
}
/* ---------------------------------------------------------- */
/* persistence */
/* ---------------------------------------------------------- */
function persistNow() {
try {
localStorage.setItem(constants_1.C.STORAGE_KEY, JSON.stringify(store.app));
}
catch {
/* storage full or unavailable — keep running in memory */
}
}
function schedulePersist() {
if (store.persistScheduled)
return;
store.persistScheduled = true;
setTimeout(() => {
store.persistScheduled = false;
persistNow();
}, constants_1.C.AUTOSAVE_MS);
}
function loadFromStorage() {
let raw = null;
try {
raw = localStorage.getItem(constants_1.C.STORAGE_KEY);
}
catch {
raw = null;
}
if (!raw)
return false;
try {
const data = JSON.parse(raw);
if (!data || !Array.isArray(data.notes) || !Array.isArray(data.notebooks))
return false;
store.app = {
notebooks: data.notebooks.map(n => ({
id: String(n.id ?? (0, utils_1.uid)()),
name: String(n.name ?? 'Untitled'),
collapsed: !!n.collapsed,
})),
notes: data.notes.map(n => ({
id: String(n.id ?? (0, utils_1.uid)()),
notebookId: typeof n.notebookId === 'string' ? n.notebookId : null,
body: (0, sanitize_1.sanitizeHTML)(String(n.body ?? '')),
tags: Array.isArray(n.tags) ? n.tags.map(t => String(t)).filter(Boolean).slice(0, 24) : [],
pinned: !!n.pinned,
createdAt: Number(n.createdAt) || Date.now(),
updatedAt: Number(n.updatedAt) || Date.now(),
trashedAt: typeof n.trashedAt === 'number' ? n.trashedAt : null,
trashedFrom: typeof n.trashedFrom === 'string' ? n.trashedFrom : null,
})),
ui: {
theme: data.ui?.theme === 'dark' ? 'dark' : 'light',
sort: ['modified', 'created', 'alpha'].includes(data.ui?.sort)
? data.ui.sort : 'modified',
view: normalizeView(data.ui?.view),
selectedNoteId: typeof data.ui?.selectedNoteId === 'string' ? data.ui.selectedNoteId : null,
sidebarOpen: data.ui?.sidebarOpen !== false,
sidebarW: clampNum(data.ui?.sidebarW, constants_1.C.SIDEBAR.min, constants_1.C.SIDEBAR.max, constants_1.C.SIDEBAR.def),
listW: clampNum(data.ui?.listW, constants_1.C.LIST.min, constants_1.C.LIST.max, constants_1.C.LIST.def),
},
};
return true;
}
catch {
return false;
}
}
function normalizeView(v) {
if (v && typeof v === 'object') {
const o = v;
if (o.type === 'notebook' && typeof o.id === 'string')
return { type: 'notebook', id: o.id };
if (o.type === 'trash')
return { type: 'trash' };
if (o.type === 'tag' && typeof o.tag === 'string')
return { type: 'tag', tag: o.tag };
}
return { type: 'all' };
}
function clampNum(v, min, max, def) {
return typeof v === 'number' && isFinite(v) ? Math.min(max, Math.max(min, v)) : def;
}
/* ---------------------------------------------------------- */
/* notebook ops */
/* ---------------------------------------------------------- */
function createNotebook(name) {
const nb = { id: (0, utils_1.uid)(), name: name.trim() || 'Untitled', collapsed: false };
store.app.notebooks.push(nb);
store.app.ui.view = { type: 'notebook', id: nb.id };
emit('notebook');
return nb;
}
function renameNotebook(id, name) {
const nb = store.app.notebooks.find(n => n.id === id);
if (nb) {
nb.name = name.trim() || nb.name;
emit('notebook');
}
}
function deleteNotebook(id) {
const i = store.app.notebooks.findIndex(n => n.id === id);
if (i < 0)
return;
store.app.notebooks.splice(i, 1);
for (const note of store.app.notes) {
if (note.notebookId === id && !note.trashedAt) {
note.notebookId = null; // falls back to "All Notes"
}
}
if ((0, types_1.sameView)(store.app.ui.view, { type: 'notebook', id })) {
store.app.ui.view = { type: 'all' };
}
emit('notebook');
}
function toggleNotebookCollapsed(id) {
const nb = store.app.notebooks.find(n => n.id === id);
if (nb) {
nb.collapsed = !nb.collapsed;
emit('notebook');
}
}
/* ---------------------------------------------------------- */
/* note ops */
/* ---------------------------------------------------------- */
function noteById(id) {
if (!id)
return null;
return store.app.notes.find(n => n.id === id) ?? null;
}
function createNote(notebookId, opts = {}) {
const now = Date.now();
const note = {
id: (0, utils_1.uid)(),
notebookId,
body: (0, sanitize_1.sanitizeHTML)(opts.html ?? '<p><br></p>'),
tags: [...(opts.tags ?? [])],
pinned: false,
createdAt: now,
updatedAt: now,
trashedAt: null,
trashedFrom: null,
};
store.app.notes.push(note);
store.app.ui.selectedNoteId = note.id;
emit('note');
return note;
}
function updateNoteContent(id, html, mark) {
const note = noteById(id);
if (!note)
return;
note.body = html;
if (mark)
note.updatedAt = Date.now();
emit('note', id);
if (store.last.kind === 'note')
schedulePersist();
}
function duplicateNote(id) {
const src = noteById(id);
if (!src)
return null;
const now = Date.now();
const copy = {
...src,
id: (0, utils_1.uid)(),
body: src.body,
tags: [...src.tags],
pinned: false,
createdAt: now,
updatedAt: now,
trashedAt: null,
trashedFrom: null,
};
store.app.notes.push(copy);
store.app.ui.selectedNoteId = copy.id;
emit('note');
return copy;
}
function trashNote(id) {
const note = noteById(id);
if (!note || note.trashedAt)
return;
note.trashedFrom = note.notebookId;
note.trashedAt = Date.now();
note.pinned = false;
emit('note');
}
function restoreNote(id) {
const note = noteById(id);
if (!note || !note.trashedAt)
return;
note.trashedAt = null;
// Original notebook if it still exists, else null (All Notes).
const nb = store.app.notebooks.find(n => n.id === note.trashedFrom);
note.notebookId = nb ? nb.id : null;
note.trashedFrom = null;
emit('note');
}
function destroyNote(id) {
const i = store.app.notes.findIndex(n => n.id === id);
if (i < 0)
return;
store.app.notes.splice(i, 1);
if (store.app.ui.selectedNoteId === id)
store.app.ui.selectedNoteId = null;
emit('note');
}
function moveNote(id, notebookId) {
const note = noteById(id);
if (!note || note.trashedAt || note.notebookId === notebookId)
return;
note.notebookId = notebookId;
note.updatedAt = Date.now();
emit('note');
}
function setPinned(id, pinned) {
const note = noteById(id);
if (!note)
return;
note.pinned = pinned;
emit('note');
}
function setNoteTags(id, tags) {
const note = noteById(id);
if (!note)
return;
note.tags = tags;
emit('note');
}
/** Notes visible in the current view, sorted (pinned first). */
function visibleNotes() {
const app = store.app;
const v = app.ui.view;
let pool = app.notes;
if (v.type === 'trash')
pool = pool.filter(n => n.trashedAt);
else if (v.type === 'notebook')
pool = pool.filter(n => !n.trashedAt && n.notebookId === v.id);
else if (v.type === 'tag')
pool = pool.filter(n => !n.trashedAt && n.tags.includes(v.tag));
else
pool = pool.filter(n => !n.trashedAt);
const sortKey = app.ui.sort;
return [...pool].sort((a, b) => {
if (!!a.pinned !== !!b.pinned)
return a.pinned ? -1 : 1;
if (sortKey === 'created')
return b.createdAt - a.createdAt;
if (sortKey === 'alpha') {
const ta = titleOf(a).toLowerCase();
const tb = titleOf(b).toLowerCase();
return ta < tb ? -1 : ta > tb ? 1 : b.updatedAt - a.updatedAt;
}
return b.updatedAt - a.updatedAt;
});
}
function setView(view) {
if (!(0, types_1.sameView)(store.app.ui.view, view)) {
store.app.ui.view = view;
emit('view');
}
}
function selectNote(id) {
if (store.app.ui.selectedNoteId !== id) {
store.app.ui.selectedNoteId = id;
emit('view');
}
}
function setSort(sort) {
store.app.ui.sort = sort;
emit('view');
}
/* ---------------------------------------------------------- */
/* ui / layout ops */
/* ---------------------------------------------------------- */
function setTheme(theme) {
store.app.ui.theme = theme;
emit('layout');
}
function setSidebarOpen(open) {
store.app.ui.sidebarOpen = open;
emit('layout');
}
function setPaneWidth(which, w) {
if (which === 'sidebar')
store.app.ui.sidebarW = w;
else
store.app.ui.listW = w;
emit('layout');
}
function commitLayout() {
persistNow();
}
};
// ── module: src/editor.ts ──
__mods["src/editor.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// EDITOR — contenteditable engine.
// * custom undo/redo (snapshot history with caret restore)
// * markdown shortcuts: "# " "- " "[] " "> " at line start
// * Enter: continues lists, exits empty items, splits blocks
// * Backspace: merges blocks, exits lists/headings/quotes
// * paste sanitized to the supported subset
// * toolbar state mirrors the selection
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.blockOf = blockOf;
exports.getEditor = getEditor;
const constants_1 = require("./constants");
const state_1 = require("./state");
const sanitize_1 = require("./sanitize");
const BLOCK_TAGS = new Set(['P', 'H1', 'H2', 'H3', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'DIV']);
const INLINE_SET = new Set(['B', 'STRONG', 'I', 'EM', 'U', 'S', 'STRIKE', 'CODE']);
/** Non-null-or-range selection helper. */
function sel() {
const s = getSelection();
if (!s || !s.rangeCount)
return null;
return s;
}
class EditorEngine {
constructor(ed) {
this.noteId = null;
this.hist = [];
this.histIdx = -1;
this.baseHtml = '';
this.baseSel = null;
this.lastPushAt = 0;
this.loading = false;
this.composing = false;
this.onToolbar = null;
this.ed = ed;
try {
document.execCommand('defaultParagraphSeparator', false, 'p');
document.execCommand('styleWithCSS', false, 'false');
}
catch { /* execCommand availability varies; fallbacks cover it */ }
ed.addEventListener('input', () => this.onInput());
ed.addEventListener('keydown', (e) => this.onKeyDown(e));
ed.addEventListener('keyup', () => this.refreshToolbar());
ed.addEventListener('click', () => this.refreshToolbar());
ed.addEventListener('paste', (e) => this.onPaste(e));
ed.addEventListener('drop', (e) => e.preventDefault());
ed.addEventListener('compositionstart', () => { this.composing = true; });
ed.addEventListener('compositionend', () => { this.composing = false; this.afterManual(); });
document.addEventListener('selectionchange', () => {
if (this.focusedInEditor())
this.refreshToolbar();
});
}
/* ------------------------------------------------------ */
/* public API */
/* ------------------------------------------------------ */
setToolbarCallback(fn) {
this.onToolbar = fn;
}
getHTML() {
return this.ed.innerHTML;
}
focus() {
this.ed.focus();
}
isActive() {
return document.activeElement === this.ed;
}
/** Load a note into the editor. keepSelection restores caret. */
load(noteId, html, keepSelection) {
// No-op guard: the editor already holds exactly this content (e.g. a
// commit just round-tripped). Skipping here keeps undo history intact.
if (this.noteId === noteId && this.ed.innerHTML === html) {
return;
}
const sel = keepSelection && this.noteId === noteId ? captureSelection(this.ed) : null;
this.loading = true;
this.noteId = noteId;
this.ed.innerHTML = (0, sanitize_1.normalizeHTML)(html) || '<p><br></p>';
this.loading = false;
this.hist = [{ html: this.ed.innerHTML, sel: captureSelection(this.ed) }];
this.histIdx = 0;
this.baseHtml = this.ed.innerHTML;
this.baseSel = captureSelection(this.ed);
this.lastPushAt = 0; // first input always starts a fresh undo burst
if (sel)
restoreSelection(this.ed, sel);
else if (keepSelection)
placeCaretStartIn(this.ed);
this.refreshToolbar();
}
clear() {
this.noteId = null;
this.ed.innerHTML = '<p><br></p>';
this.hist = [];
this.histIdx = -1;
this.refreshToolbar();
}
/* ------------------------------------------------------ */
/* commit / history */
/* ------------------------------------------------------ */
/** Sanitize + normalize live DOM -> model. Call after any mutation. */
commit(mark = true) {
if (!this.noteId || this.loading)
return;
const norm = (0, sanitize_1.normalizeHTML)(this.ed.innerHTML);
if (norm !== this.ed.innerHTML) {
const s = captureSelection(this.ed);
this.ed.innerHTML = norm;
restoreSelection(this.ed, s);
}
this.baseHtml = this.ed.innerHTML;
this.baseSel = captureSelection(this.ed);
(0, state_1.updateNoteContent)(this.noteId, norm, mark);
}
/** A programmatic mutation just happened: record it + sync model. */
afterManual() {
this.pushState();
this.commit(true);
}
/** Record the CURRENT (post-mutation) DOM as a new history state. */
pushState() {
if (this.loading)
return;
const snap = { html: this.ed.innerHTML, sel: captureSelection(this.ed) };
const top = this.hist[this.histIdx];
if (top && top.html === snap.html) {
this.baseHtml = snap.html;
this.baseSel = snap.sel;
return; // no change
}
this.hist.splice(this.histIdx + 1);
this.hist.push(snap);
if (this.hist.length > constants_1.C.UNDO_LIMIT)
this.hist.shift();
this.histIdx = this.hist.length - 1;
this.lastPushAt = Date.now();
this.baseHtml = snap.html;
this.baseSel = snap.sel;
}
undo() {
if (this.histIdx <= 0)
return false;
this.histIdx--;
this.applySnapshot(this.hist[this.histIdx]);
return true;
}
redo() {
if (this.histIdx >= this.hist.length - 1)
return false;
this.histIdx++;
this.applySnapshot(this.hist[this.histIdx]);
return true;
}
applySnapshot(s) {
this.loading = true;
this.ed.innerHTML = s.html || '<p><br></p>';
this.loading = false;
this.baseHtml = s.html;
this.baseSel = s.sel;
this.lastPushAt = Date.now();
restoreSelection(this.ed, s.sel);
this.commit(false);
}
/* ------------------------------------------------------ */
/* formatting commands */
/* ------------------------------------------------------ */
applyInline(tag) {
if (!this.focusedInEditor())
this.focus();
if (tag === 'code') {
this.wrapSelectionInCode();
}
else {
const ok = document.execCommand(tag, false);
if (!ok)
this.wrapSelection(tag === 'b' ? 'b' : tag);
}
this.afterManual();
}
wrapSelection(tag) {
const s = sel();
if (!s)
return;
const r = s.getRangeAt(0);
const wrap = document.createElement(tag);
const frag = r.extractContents();
wrap.appendChild(frag);
r.insertNode(wrap);
const nr = document.createRange();
nr.selectNodeContents(wrap);
nr.collapse(false);
s.removeAllRanges();
s.addRange(nr);
}
wrapSelectionInCode() {
this.wrapSelection('code');
}
setBlock(tag) {
if (!this.focusedInEditor())
this.focus();
const block = blockOf(this.ed, caretNode());
if (block && (block.tagName === 'LI' || blockIsCheck(block)))
return; // lists own their block type
document.execCommand('formatBlock', false, tag);
this.afterManual();
}
toggleList(kind) {
if (!this.focusedInEditor())
this.focus();
const block = blockOf(this.ed, caretNode());
if (block && blockIsCheck(block))
return;
if (block && block.tagName === 'LI') {
const list = block.parentElement;
if (list && (list.tagName === 'UL' || list.tagName === 'OL')) {
listToParagraphs(list);
this.afterManual();
}
return;
}
document.execCommand(kind === 'ul' ? 'insertUnorderedList' : 'insertOrderedList', false);
this.afterManual();
}
toggleChecklist() {
if (!this.focusedInEditor())
this.focus();
const block = blockOf(this.ed, caretNode());
if (block && blockIsCheck(block)) {
// convert check row back to a paragraph
const ul = block.parentElement;
const p = document.createElement('p');
const label = block.querySelector('.ck-text');
if (label)
p.appendChild(label.cloneNode(true));
p.normalize();
if (ul && ul.tagName === 'UL' && ul.children.length === 1)
ul.replaceWith(p);
else
block.replaceWith(p);
placeCaretEnd(p);
this.afterManual();
return;
}
const target = block ?? ensureParagraph(this.ed);
const li = makeCheckRow(false);
const textSpan = li.querySelector('.ck-text');
const frag = document.createDocumentFragment();
while (target.firstChild)
frag.appendChild(target.firstChild);
textSpan.appendChild(frag);
if (!textSpan.textContent && textSpan.childNodes.length === 0) {
textSpan.appendChild(document.createElement('br'));
}
if (target.tagName === 'LI' && target.parentElement?.tagName === 'UL') {
target.replaceWith(li);
}
else {
const ul = document.createElement('ul');
ul.appendChild(li);
target.replaceWith(ul);
}
placeCaretEnd(textSpan);
this.afterManual();
}
insertHR() {
if (!this.focusedInEditor())
this.focus();
document.execCommand('insertHorizontalRule', false);
this.afterManual();
}
/** Toggle the checkbox of the row containing the caret. */
toggleCheckAtCaret() {
const block = blockOf(this.ed, caretNode());
if (!block || !blockIsCheck(block))
return;
this.toggleCheck(block);
}
toggleCheck(li) {
const cb = li.querySelector('.ck-cb');
if (!cb)
return;
cb.checked = !cb.checked;
li.dataset.check = cb.checked ? 'true' : 'false';
this.afterManual();
}
/* ------------------------------------------------------ */
/* key handling */
/* ------------------------------------------------------ */
onInput() {
if (this.composing)
return;
// A new burst (gap since last state > merge window) records the
// post-input DOM as a fresh undo state.
if (this.histIdx >= 0 && Date.now() - this.lastPushAt > constants_1.C.UNDO_TYPING_GAP_MS) {
this.pushState();
return;
}
// Continuous typing: keep baseline fresh, no new state.
this.baseHtml = this.ed.innerHTML;
this.baseSel = captureSelection(this.ed);
this.commit(true);
}
onKeyDown(e) {
const mod = e.metaKey || e.ctrlKey;
if (mod && !e.altKey) {
const k = e.key.toLowerCase();
if (k === 'b') {
e.preventDefault();
this.applyInline('b');
return;
}
if (k === 'i') {
e.preventDefault();
this.applyInline('i');
return;
}
if (k === 'u') {
e.preventDefault();
this.applyInline('u');
return;
}
if (k === 's') {
e.preventDefault();
this.applyInline('s');
return;
}
if (k === 'z') {
e.preventDefault();
if (e.shiftKey)
this.redo();
else
this.undo();
return;
}
if (k === 'y') {
e.preventDefault();
this.redo();
return;
}
}
if (e.isComposing)
return;
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.handleEnter();
return;
}
if (e.key === 'Backspace') {
if (this.handleBackspace(e))
return;
}
if (e.key === 'Tab' && !e.shiftKey) {
e.preventDefault();
this.indentList(1);
return;
}
if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
this.indentList(-1);
return;
}
if (e.key === ' ' && !mod && !e.shiftKey && !e.altKey) {
this.tryMarkdownShortcut(e);
}
}
/* -------- markdown shortcuts -------- */
tryMarkdownShortcut(e) {
const s = sel();
if (!s)
return;
const r = s.getRangeAt(0);
if (!r.collapsed)
return;
const anchor = r.startContainer;
if (anchor.nodeType !== Node.TEXT_NODE)
return;
const node = anchor;
const idx = r.startOffset;
const before = node.data.slice(0, idx);
let marker = '';
if (idx >= 2 && before.slice(idx - 2, idx) === '[]')
marker = '[]';
else if (idx >= 1 && (before[idx - 1] === '#' || before[idx - 1] === '-' || before[idx - 1] === '>')) {
marker = before[idx - 1];
}
if (!marker)
return;
const start = idx - marker.length;
// Must be at the very start of the line: nothing but whitespace before.
if (/\S/.test(before.slice(0, start)))
return;
const parent = node.parentElement;
if (!parent || parent.tagName !== 'P')
return;
let prev = parent.previousSibling;
while (prev) {
if (prev.nodeType === Node.TEXT_NODE) {
if (prev.textContent && prev.textContent.trim() !== '')
return;
}
else {
return;
}
prev = prev.previousSibling;
}
// Convert now and consume the space that would be typed.
e.preventDefault();
const del = document.createRange();
del.setStart(node, start);
del.setEnd(node, idx);
del.deleteContents();
const rest = node.data.slice(idx); // text that already followed the marker
if (marker === '#') {
const h = document.createElement('H1');
if (rest)
h.textContent = rest;
else
h.appendChild(document.createElement('br'));
parent.replaceWith(h);
placeCaretEnd(h);
}
else if (marker === '-') {
const li = document.createElement('LI');
if (rest)
li.textContent = rest;
else
li.appendChild(document.createElement('br'));
const ul = document.createElement('UL');
ul.appendChild(li);
parent.replaceWith(ul);
placeCaretEnd(li);
}
else if (marker === '[]') {
const li = makeCheckRow(false);
const span = li.querySelector('.ck-text');
if (rest)
span.textContent = rest;
else
span.appendChild(document.createElement('br'));
const ul = document.createElement('UL');
ul.appendChild(li);
parent.replaceWith(ul);
placeCaretEnd(span);
}
else if (marker === '>') {
const q = document.createElement('BLOCKQUOTE');
const p2 = document.createElement('P');
if (rest)
p2.textContent = rest;
else
p2.appendChild(document.createElement('br'));
q.appendChild(p2);
parent.replaceWith(q);
placeCaretEnd(p2);
}
this.afterManual();
}
/* -------- Enter -------- */
handleEnter() {
const s = sel();
if (!s)
return;
const r = s.getRangeAt(0);
const block = blockOf(this.ed, r.startContainer);
if (!block) {
const p = ensureParagraph(this.ed);
const np = document.createElement('P');
np.appendChild(document.createElement('br'));
p.after(np);
placeCaretEnd(np);
this.afterManual();
return;
}
if (block.tagName === 'LI') {
this.enterList(block, r);
return;
}
if (blockIsCheck(block)) {
this.enterCheckRow(block, r);
return;
}
if (block.tagName === 'BLOCKQUOTE') {
this.enterQuote(block, r);
return;
}
if (block.tagName === 'HR') {
const p = document.createElement('P');
p.appendChild(document.createElement('br'));
block.after(p);
placeCaretEnd(p);
this.afterManual();
return;
}
// P or H*
const [head, tail] = splitBlock(block, r);
const tag = (block.tagName === 'P' ? 'P' : block.tagName);
const newBlock = document.createElement(tag);
fillOrBr(newBlock, tail);
block.after(newBlock);
ensureNotEmpty(head);
placeCaretEnd(newBlock);
this.afterManual();
}
exitListToParagraph(list, li) {
if (list.children.length === 1) {
const p = document.createElement('P');
p.appendChild(document.createElement('br'));
list.replaceWith(p);
placeCaretEnd(p);
}
else {
li.remove();
const prevLi = li.previousElementSibling;
if (prevLi)
placeCaretEnd(prevLi);
}
}
enterList(li, r) {
const list = li.parentElement;
if (!list || (list.tagName !== 'UL' && list.tagName !== 'OL')) {
this.enterPlainBlock(li, r);
return;
}
if (liIsEmpty(li)) {
// Enter on an empty item exits the list.
this.exitListToParagraph(list, li);
this.afterManual();
return;
}
// Enter at the end (or middle) of a non-empty item continues the list.
const [head, tail] = splitBlock(li, r);
const newLi = document.createElement('LI');
fillOrBr(newLi, tail);
li.after(newLi);
ensureNotEmpty(head);
placeCaretEnd(newLi);
this.afterManual();
}
enterPlainBlock(block, r) {
const [head, tail] = splitBlock(block, r);
const tag = (block.tagName === 'P' ? 'P' : block.tagName);
const newBlock = document.createElement(tag);
fillOrBr(newBlock, tail);
block.after(newBlock);
ensureNotEmpty(head);
placeCaretEnd(newBlock);
this.afterManual();
}
enterCheckRow(div, r) {
const ul = div.parentElement;
const label = div.querySelector('.ck-text');
if (!ul || !label)
return;
const text = label.textContent ?? '';
const atTextEnd = caretAtEndOf(r, label);
if (text.trim() === '') {
// empty row -> exit
if (ul.children.length === 1) {
const p = document.createElement('P');
p.appendChild(document.createElement('br'));
ul.replaceWith(p);
placeCaretEnd(p);
}
else {
div.remove();
const prev = div.previousElementSibling;
const prevText = (prev ? prev.querySelector('.ck-text') : null);
if (prevText)
placeCaretEnd(prevText);
}
this.afterManual();
return;
}
if (atTextEnd) {
const newDiv = makeCheckRow(false);
ul.insertBefore(newDiv, div.nextSibling);
placeCaretEnd(newDiv.querySelector('.ck-text'));
}
else {
const newDiv = makeCheckRow(false);
const newSpan = newDiv.querySelector('.ck-text');
const rng = document.createRange();
rng.setStart(r.startContainer, r.startOffset);
rng.setEnd(label, label.childNodes.length);
const frag = rng.extractContents();
newSpan.appendChild(frag);
ul.insertBefore(newDiv, div.nextSibling);
ensureNotEmptySpan(newSpan);
placeCaretEnd(newSpan);
}
this.afterManual();
}
enterQuote(q, r) {
const inner = q.querySelector('P, H1, H2, H3');
if (inner) {
const [head, tail] = splitBlock(inner, r);
if (tailEmpty(tail)) {
ensureNotEmpty(head);
const p = document.createElement('P');
p.appendChild(document.createElement('br'));
q.after(p);
placeCaretEnd(p);
}
else {
const newP = document.createElement('P');
fillOrBr(newP, tail);
q.appendChild(newP);
ensureNotEmpty(head);
placeCaretEnd(newP);
}
}
else {
const p = document.createElement('P');
p.appendChild(document.createElement('br'));
q.after(p);
placeCaretEnd(p);
}
this.afterManual();
}
/* -------- Backspace -------- */
handleBackspace(e) {
const s = sel();
if (!s)
return false;
const r = s.getRangeAt(0);
if (!r.collapsed)
return false; // let default delete the selection
const block = blockOf(this.ed, r.startContainer);
if (!block)
return false;
const atBlockStart = (() => {
const t = document.createRange();
t.selectNodeContents(block);
t.setEnd(r.startContainer, r.startOffset);
return t.toString() === '';
})();
if (!atBlockStart)
return false;
// Caret at the very start of a block.
if (block.parentElement && block.parentElement.tagName === 'BLOCKQUOTE') {
e.preventDefault();
const q = block.parentElement;
const p = document.createElement('P');
p.appendChild(document.createElement('br'));
q.parentElement?.insertBefore(p, q);
placeCaretEnd(p);
this.afterManual();
return true;
}
if (block.tagName === 'LI' || blockIsCheck(block)) {
e.preventDefault();
const list = block.parentElement;
const prev = previousBlockSibling(block);
if (prev && (prev.tagName === 'LI' || blockIsCheck(prev))) {
mergeInto(prev, block);
}
else {
const p = document.createElement('P');
const frag = document.createDocumentFragment();
if (block.tagName === 'LI') {
while (block.firstChild)
frag.appendChild(block.firstChild);
}
else {
const label = block.querySelector('.ck-text');
if (label)
frag.appendChild(label.cloneNode(true));
}
p.appendChild(frag);
if (list && (list.tagName === 'UL' || list.tagName === 'OL') && list.children.length === 1) {
list.replaceWith(p);
}
else if (list && (list.tagName === 'UL' || list.tagName === 'OL')) {
list.parentElement?.insertBefore(p, list);
list.removeChild(block);
}
else {
block.replaceWith(p);
}
if (!p.textContent && p.childNodes.length === 0)
p.appendChild(document.createElement('br'));
placeCaretStartIn(p);
}
this.afterManual();
return true;
}
if (block.tagName === 'H1' || block.tagName === 'H2' || block.tagName === 'H3') {
const prev = previousBlockSibling(block);
if (!prev) {
e.preventDefault();
const p = document.createElement('P');
while (block.firstChild)
p.appendChild(block.firstChild);
block.replaceWith(p);
placeCaretStartIn(p);
this.afterManual();
return true;
}
}
const prev = previousBlockSibling(block);
if (prev && prev.tagName !== 'BLOCKQUOTE' && prev.tagName !== 'HR') {
e.preventDefault();
mergeInto(prev, block);
placeCaretEnd(prev);
this.afterManual();
return true;
}
return false;
}
/* -------- Tab indent -------- */
indentList(dir) {
const s = sel();
if (!s)
return;
const block = blockOf(this.ed, s.getRangeAt(0).startContainer);
if (!block || block.tagName !== 'LI')
return;
const list = block.parentElement;
if (!list || (list.tagName !== 'UL' && list.tagName !== 'OL'))
return;
if (dir === 1) {
const targets = [];
let cur = list.firstElementChild;
while (cur && cur.tagName === 'LI') {
targets.push(cur);
if (cur === block)
break;
cur = cur.nextElementSibling;
}
if (targets.length === 0)
return;
for (const li of targets) {
li.appendChild(document.createElement(list.tagName === 'OL' ? 'OL' : 'UL'));
}
placeCaretEnd(block);
this.afterManual();
}
else {
if (block.parentElement !== list)
return;
const outerLi = list.parentElement;
if (!outerLi || outerLi.tagName !== 'LI')
return;
outerLi.insertBefore(block, list);
if (list.children.length === 0)
list.remove();
placeCaretEnd(block);
this.afterManual();
}
}
/* -------- paste -------- */
onPaste(e) {
e.preventDefault();
const cd = e.clipboardData;
if (!cd)
return;
const text = cd.getData('text/plain');
const html = cd.getData('text/html');
if (html && html.trim()) {
const clean = (0, sanitize_1.sanitizeHTML)(html);
if (clean.trim())
document.execCommand('insertHTML', false, clean);
}
else if (text) {
const block = blockOf(this.ed, caretNode());
if (text.includes('\n') && block && (block.tagName === 'P' || blockIsCheck(block))) {
const lines = text.split(/\r\n?|\n/);
const fragHtml = lines.map(l => (l ? `<p>${escapeHTML(l)}</p>` : '<p><br></p>')).join('');
document.execCommand('insertHTML', false, fragHtml);
}
else {
document.execCommand('insertText', false, text);
}
}
this.afterManual();
}
/* -------- toolbar state -------- */
focusedInEditor() {
const a = document.activeElement;
return a === this.ed || (a instanceof HTMLElement && this.ed.contains(a));
}
refreshToolbar() {
if (!this.onToolbar)
return;
const state = {
bold: false, italic: false, underline: false, strike: false, code: false,
h1: false, h2: false, h3: false, ul: false, ol: false, checklist: false, quote: false,
active: false,
};
if (!this.focusedInEditor() || this.noteId == null) {
this.onToolbar(state);
return;
}
const s = sel();
if (!s) {
this.onToolbar(state);
return;
}
const tags = new Set();
for (const node of [s.anchorNode, s.focusNode]) {
if (!node)
continue;
let n = node;
while (n && n !== this.ed) {
if (n.nodeType === Node.ELEMENT_NODE) {
const tag = n.tagName;
if (INLINE_SET.has(tag))
tags.add(tag);
}
n = n.parentElement;
}
}
state.bold = tags.has('B') || tags.has('STRONG');
state.italic = tags.has('I') || tags.has('EM');
state.underline = tags.has('U');
state.strike = tags.has('S') || tags.has('STRIKE');
state.code = tags.has('CODE');
const anchor = s.anchorNode || s.focusNode;
const block = anchor ? blockOf(this.ed, anchor) : null;
if (block) {
state.h1 = block.tagName === 'H1';
state.h2 = block.tagName === 'H2';
state.h3 = block.tagName === 'H3';
if (block.tagName === 'LI') {
const list = block.parentElement;
state.ul = list?.tagName === 'UL';
state.ol = list?.tagName === 'OL';
}
state.checklist = blockIsCheck(block);
state.quote = !!block.closest('BLOCKQUOTE');
}
state.active = true;
this.onToolbar(state);
}
}
// ============================================================
// helpers
// ============================================================
function caretNode() {
const s = sel();
return s ? s.getRangeAt(0).startContainer : document.createTextNode('');
}
function blockOf(ed, node) {
let n = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
while (n && n !== ed) {
if (n.nodeType === Node.ELEMENT_NODE) {
const tag = n.tagName;
if (BLOCK_TAGS.has(tag))
return n;
}
n = n.parentElement;
}
return null;
}
function blockIsCheck(el) {
return el.tagName === 'LI' && el.dataset.check !== undefined;
}
function isCheckLi(el) {
return el.tagName === 'LI' && el.dataset.check !== undefined;
}
function makeCheckRow(checked) {
const li = document.createElement('li');
li.dataset.check = checked ? 'true' : 'false';
const label = document.createElement('label');
label.className = 'ck-label';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'ck-cb';
cb.checked = checked;
const box = document.createElement('span');
box.className = 'ck-box';
box.appendChild(cb);
const span = document.createElement('span');
span.className = 'ck-text';
label.appendChild(box);
label.appendChild(span);
li.appendChild(label);
return li;
}
function ensureParagraph(ed) {
const last = ed.lastElementChild;
if (!last || !BLOCK_TAGS.has(last.tagName) || isCheckLi(last)) {
const p = document.createElement('P');
p.appendChild(document.createElement('br'));
ed.appendChild(p);
return p;
}
return last;
}
function liIsEmpty(li) {
const clone = li.cloneNode(true);
for (const br of Array.from(clone.querySelectorAll('br')))
br.remove();
if (clone.querySelector('ul,ol'))
return false;
return (clone.textContent ?? '').trim() === '';
}
function tailEmpty(tail) {
const text = tail.textContent ?? '';
if (text.trim() !== '')
return false;
for (const n of Array.from(tail.childNodes)) {
if (n.nodeType === Node.ELEMENT_NODE && n.tagName !== 'BR')
return false;
}
return true;
}
function splitBlock(block, r) {
const full = document.createRange();
full.setStart(r.startContainer, r.startOffset);
full.selectNodeContents(block);
full.collapse(false); // caret -> end of block
const tail = full.extractContents();
return [block, tail];
}
function fillOrBr(el, frag) {
el.appendChild(frag);
if (!el.textContent && el.querySelectorAll('br').length === 0) {
el.appendChild(document.createElement('br'));
}
}
function ensureNotEmpty(el) {
const text = el.textContent ?? '';
if (text.trim() !== '' || el.querySelector('ul,ol,div[data-check]'))
return;
if (!el.querySelector('br'))
el.appendChild(document.createElement('br'));
}
function ensureNotEmptySpan(span) {
if (!span.textContent && !span.querySelector('br'))
span.appendChild(document.createElement('br'));
}
function previousBlockSibling(block) {
let sib = block.previousElementSibling;
while (sib) {
if (BLOCK_TAGS.has(sib.tagName))
return sib;
sib = sib.previousElementSibling;
}
const parent = block.parentElement;
if (parent && (parent.tagName === 'UL' || parent.tagName === 'OL' || parent.tagName === 'BLOCKQUOTE')) {
let sib2 = parent.previousElementSibling;
while (sib2) {
if (BLOCK_TAGS.has(sib2.tagName))
return sib2;
sib2 = sib2.previousElementSibling;
}
}
return null;
}
function mergeInto(prev, next) {
let dest = prev;
if (blockIsCheck(prev))
dest = prev.querySelector('.ck-text') ?? prev;
const frag = document.createDocumentFragment();
if (blockIsCheck(next)) {
const label = next.querySelector('.ck-text');
if (label) {
while (label.firstChild)
frag.appendChild(label.firstChild);
}
}
else {
while (next.firstChild)
frag.appendChild(next.firstChild);
}
dest.appendChild(frag);
next.remove();
ensureNotEmpty(prev);
}
function listToParagraphs(list) {
const parent = list.parentElement;
if (!parent)
return;
const rows = Array.from(list.children);
const firstP = [];
for (const row of rows) {
const p = document.createElement('P');
if (blockIsCheck(row)) {
const label = row.querySelector('.ck-text');
if (label)
p.appendChild(label.cloneNode(true));
}
else {
const frag = document.createDocumentFragment();
while (row.firstChild)
frag.appendChild(row.firstChild);
p.appendChild(frag);
}
ensureNotEmpty(p);
firstP.push(p);
parent.insertBefore(p, list);
}
list.remove();
if (firstP.length)
placeCaretStartIn(firstP[0]);
}
function escapeHTML(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
function caretAtEndOf(r, el) {
const t = document.createRange();
t.selectNodeContents(el);
t.collapse(false);
const s = sel();
if (!s)
return true;
const cr = s.getRangeAt(0);
return cr.startContainer === t.endContainer && cr.startOffset === t.endOffset;
}
function placeCaretEnd(el) {
const r = document.createRange();
const last = el.lastElementChild;
if (last && last.tagName === 'BR')
r.setStartBefore(last);
else {
r.selectNodeContents(el);
r.collapse(false);
}
r.collapse(true);
const s = getSelection();
s?.removeAllRanges();
s?.addRange(r);
}
function placeCaretStartIn(el) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
const t = walker.nextNode();
const r = document.createRange();
if (t) {
r.setStart(t, 0);
r.collapse(true);
}
else {
r.selectNodeContents(el);
r.collapse(true);
}
const s = getSelection();
s?.removeAllRanges();
s?.addRange(r);
}
function captureSelection(ed) {
const s = sel();
if (!s)
return null;
const r = s.getRangeAt(0);
if (!ed.contains(r.startContainer) && r.startContainer !== ed)
return null;
return { s: selPath(ed, r.startContainer, r.startOffset), e: selPath(ed, r.endContainer, r.endOffset) };
}
function selPath(ed, node, offset) {
const path = [offset];
let cur = node;
while (cur && cur !== ed) {
const parentOf = cur.parentElement;
if (!parentOf)
break;
let idx = 0;
for (const ch of Array.from(parentOf.childNodes)) {
if (ch === cur)
break;
idx++;
}
path.unshift(idx);
cur = parentOf;
}
return path;
}
function restoreSelection(ed, d) {
if (!d) {
placeCaretStartIn(ed);
return;
}
const a = resolvePath(ed, d.s);
const b = resolvePath(ed, d.e) ?? a;
if (!a || !b) {
placeCaretStartIn(ed);
return;
}
const r = document.createRange();
try {
r.setStart(a.node, a.offset);
r.setEnd(b.node, b.offset);
}
catch {
placeCaretStartIn(ed);
return;
}
const s = getSelection();
s?.removeAllRanges();
s?.addRange(r);
}
function resolvePath(ed, p) {
let node = ed;
for (let i = 0; i < p.length - 1; i++) {
const next = node.childNodes[p[i]];
if (!next)
return null;
node = next;
}
const off = p[p.length - 1];
if (node.nodeType === Node.TEXT_NODE) {
return { node, offset: Math.max(0, Math.min(off, node.length)) };
}
return { node, offset: Math.max(0, Math.min(off, node.childNodes.length)) };
}
// ============================================================
// singleton
// ============================================================
let instance = null;
function getEditor() {
if (!instance) {
const ed = document.getElementById('editor-root');
instance = new EditorEngine(ed);
}
return instance;
}
};
// ── module: src/view.ts ──
__mods["src/view.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// VIEW — DOM projection of the App state.
// All rendering here is idempotent: it reads the store and
// rebuilds the parts of the DOM that correspond.
// ============================================================
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ICONS = void 0;
exports.getViewEls = getViewEls;
exports.buildView = buildView;
exports.renderRoot = renderRoot;
exports.renderSidebar = renderSidebar;
exports.renderList = renderList;
exports.flashSaved = flashSaved;
const constants_1 = require("./constants");
const utils_1 = require("./utils");
const sanitize_1 = require("./sanitize");
const S = __importStar(require("./state"));
const editor_1 = require("./editor");
let els = null;
let searchTimer = null;
let draggingNoteId = null;
function getViewEls() {
return els;
}
/* ---------------------------------------------------------- */
/* icons */
/* ---------------------------------------------------------- */
exports.ICONS = {
chevron: (0, utils_1.svgIcon)('<polyline points="6 9 12 15 18 9"/>', 14, 2),
folder: (0, utils_1.svgIcon)('<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"/>', 15, 1.7),
allNotes: (0, utils_1.svgIcon)('<path d="M4 4h16v16H4z"/><line x1="8" y1="9" x2="16" y2="9"/><line x1="8" y1="13" x2="13" y2="13"/>', 15, 1.7),
trash: (0, utils_1.svgIcon)('<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>', 15, 1.7),
tag: (0, utils_1.svgIcon)('<path d="M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3 13V3h10l7.6 7.6a2 2 0 0 1 0 2.8z"/><line x1="7" y1="7" x2="7.01" y2="7"/>', 14, 1.7),
plus: (0, utils_1.svgIcon)('<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>', 15, 2),
search: (0, utils_1.svgIcon)('<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/>', 15, 1.8),
pin: (0, utils_1.svgIcon)('<path d="M12 17v5"/><path d="M9 3h6l1 7 2 2H6l2-2z"/>', 13, 1.8),
dots: (0, utils_1.svgIcon)('<circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="12" cy="19" r="1.4"/>', 15, 2.4),
theme: (0, utils_1.svgIcon)('<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>', 15, 1.8),
restore: (0, utils_1.svgIcon)('<polyline points="1 4 1 10 7 10"/><path d="M3.5 15a9 9 0 1 0 2-9.4L1 10"/>', 13, 1.8),
x: (0, utils_1.svgIcon)('<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>', 13, 2),
bold: (0, utils_1.svgIcon)('<path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/>', 15, 1.9),
italic: (0, utils_1.svgIcon)('<line x1="19" y1="5" x2="10" y2="5"/><line x1="14" y1="19" x2="5" y2="19"/><line x1="15" y1="5" x2="9" y2="19"/>', 15, 1.9),
underline: (0, utils_1.svgIcon)('<path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="5" y1="20" x2="19" y2="20"/>', 15, 1.9),
strike: (0, utils_1.svgIcon)('<path d="M16 4H9a3 3 0 0 0-2.8 4"/><path d="M14 12a4 4 0 0 1 0 8H6"/><line x1="4" y1="12" x2="20" y2="12"/>', 15, 1.9),
code: (0, utils_1.svgIcon)('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>', 15, 1.9),
h1: (0, utils_1.svgIcon)('<path d="M4 5v14M10 5v14M4 12h6"/><path d="M14 13l2-3v8"/>', 15, 1.9),
h2: (0, utils_1.svgIcon)('<path d="M4 5v14M10 5v14M4 12h6"/><path d="M15 10c-1.5.5-2 1.5-1.5 3 .8 2 4 1.5 4 4a3 3 0 0 1-6 .5"/>', 15, 1.9),
h3: (0, utils_1.svgIcon)('<path d="M4 5v14M10 5v14M4 12h6"/><path d="M15 9.5c-1.8 0-2.5 1.2-2 2.5.5 1.3 3.5 1 3.5 3a2.6 2.6 0 0 1-5.2.3"/>', 15, 1.9),
ul: (0, utils_1.svgIcon)('<line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4.5" cy="6" r="1.2"/><circle cx="4.5" cy="12" r="1.2"/><circle cx="4.5" cy="18" r="1.2"/>', 15, 1.9),
ol: (0, utils_1.svgIcon)('<line x1="10" y1="6" x2="20" y2="6"/><line x1="10" y1="12" x2="20" y2="12"/><line x1="10" y1="18" x2="20" y2="18"/><path d="M4 6h2v4M4 10h3"/><path d="M6 18H4c0-1.5 2-1.5 2-3s-2-1.5-3 0"/>', 15, 1.9),
check: (0, utils_1.svgIcon)('<rect x="3" y="3" width="18" height="18" rx="4"/><polyline points="8 12 11 15 16 9"/>', 15, 1.8),
quote: (0, utils_1.svgIcon)('<path d="M9 7c-2.5 1-4 3-4 6v4h5v-5H7c0-2 1-3.5 2-4zM19 7c-2.5 1-4 3-4 6v4h5v-5h-3c0-2 1-3.5 2-4z"/>', 15, 1.6),
hr: (0, utils_1.svgIcon)('<line x1="4" y1="12" x2="20" y2="12"/><circle cx="7" cy="8" r="0.8"/><circle cx="12" cy="8" r="0.8"/><circle cx="17" cy="8" r="0.8"/><circle cx="7" cy="16" r="0.8"/><circle cx="12" cy="16" r="0.8"/><circle cx="17" cy="16" r="0.8"/>', 15, 1.9),
};
/* ---------------------------------------------------------- */
/* boot / build */
/* ---------------------------------------------------------- */
function buildView() {
const root = document.getElementById('app');
const e = {
app: root,
sidebar: (0, utils_1.el)('aside', { cls: 'pane sidebar' }),
sidebarToggle: null,
newNotebookBtn: null,
allNotesRow: null,
trashRow: null,
tagsBlock: null,
notebooksBlock: null,
allCount: null,
trashCount: null,
listHeader: null,
listTitle: null,
listCount: null,
searchInput: null,
sortSelect: null,
newNoteBtn: null,
listScroll: null,
noteItems: new Map(),
editorPane: null,
emptyState: null,
noteView: null,
toolbar: null,
saved: null,
noteMeta: null,
editorRoot: null,
tagRow: null,
resizeHandle: null,
savedTimer: null,
};
root.innerHTML = '';
/* ----- sidebar ----- */
const sbHead = (0, utils_1.el)('div', { cls: 'sidebar-head' });
const brand = (0, utils_1.el)('div', { cls: 'brand' });
brand.innerHTML = `<span class="brand-mark">N</span><span class="brand-name">Notely</span>`;
e.newNotebookBtn = (0, utils_1.el)('button', { cls: 'icon-btn', title: 'New notebook', attrs: { 'aria-label': 'New notebook' } });
e.newNotebookBtn.innerHTML = exports.ICONS.plus;
e.newNotebookBtn.addEventListener('click', onNewNotebook);
sbHead.append(brand, e.newNotebookBtn);
e.sidebar.appendChild(sbHead);
e.sidebar.appendChild((0, utils_1.el)('div', { cls: 'sb-section-label', text: 'Browse' }));
e.allNotesRow = makeSideRow(exports.ICONS.allNotes, 'All Notes', 'all-notes');
e.allCount = (0, utils_1.el)('span', { cls: 'row-count' });
e.allNotesRow.querySelector('.row-label').after(e.allCount);
e.allNotesRow.addEventListener('click', () => S.setView({ type: 'all' }));
e.sidebar.appendChild(e.allNotesRow);
e.tagsBlock = (0, utils_1.el)('div', { cls: 'tags-block' });
e.sidebar.appendChild(e.tagsBlock);
e.notebooksBlock = (0, utils_1.el)('div', { cls: 'notebooks-block' });
e.sidebar.appendChild(e.notebooksBlock);
const sbFoot = (0, utils_1.el)('div', { cls: 'sidebar-foot' });
e.trashRow = makeSideRow(exports.ICONS.trash, 'Trash', 'trash');
e.trashCount = (0, utils_1.el)('span', { cls: 'row-count' });
e.trashRow.querySelector('.row-label').after(e.trashCount);
e.trashRow.addEventListener('click', () => S.setView({ type: 'trash' }));
sbFoot.append(e.trashRow);
e.sidebar.appendChild(sbFoot);
/* ----- list pane ----- */
const list = (0, utils_1.el)('section', { cls: 'pane list-pane' });
e.listHeader = (0, utils_1.el)('div', { cls: 'list-header' });
const lhTop = (0, utils_1.el)('div', { cls: 'lh-top' });
const lhTitleWrap = (0, utils_1.el)('div', { cls: 'lh-title-wrap' });
e.listTitle = (0, utils_1.el)('div', { cls: 'list-title', text: 'All Notes' });
e.listCount = (0, utils_1.el)('div', { cls: 'list-count' });
lhTitleWrap.append(e.listTitle, e.listCount);
const searchWrap = (0, utils_1.el)('div', { cls: 'search-wrap' });
searchWrap.innerHTML = exports.ICONS.search;
e.searchInput = (0, utils_1.el)('input', { cls: 'search-input', attrs: { type: 'text', placeholder: 'Search notes…', 'aria-label': 'Search notes' } });
searchWrap.appendChild(e.searchInput);
e.searchInput.addEventListener('input', onSearchInput);
e.searchInput.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape')
ev.target.blur();
});
const newNoteBtn = (0, utils_1.el)('button', { cls: 'icon-btn', title: 'New note', attrs: { 'aria-label': 'New note' } });
newNoteBtn.innerHTML = exports.ICONS.plus;
newNoteBtn.addEventListener('click', onNewNote);
e.newNoteBtn = newNoteBtn;
lhTop.append(lhTitleWrap, searchWrap, e.newNoteBtn);
const lhBottom = (0, utils_1.el)('div', { cls: 'lh-bottom' });
const sortLabel = (0, utils_1.el)('label', { cls: 'sort-label', text: 'Sort' });
e.sortSelect = (0, utils_1.el)('select', { cls: 'sort-select', attrs: { 'aria-label': 'Sort order' } });
for (const [v, l] of [['modified', 'Modified'], ['created', 'Created'], ['alpha', 'Alphabetical']]) {
e.sortSelect.appendChild((0, utils_1.el)('option', { text: l, attrs: { value: v } }));
}
e.sortSelect.addEventListener('change', () => S.setSort(e.sortSelect.value));
lhBottom.append(sortLabel, e.sortSelect);
e.listHeader.append(lhTop, lhBottom);
list.appendChild(e.listHeader);
e.listScroll = (0, utils_1.el)('div', { cls: 'list-scroll' });
list.appendChild(e.listScroll);
e.editorPane = (0, utils_1.el)('section', { cls: 'pane editor-pane' });
root.append(e.sidebar, list, e.editorPane);
buildEditorPane(e);
wireResizer(e);
wireTheme(e);
wireSidebarToggle(e);
els = e;
}
function makeSideRow(icon, label, cls) {
const row = (0, utils_1.el)('div', { cls: 'side-row ' + cls });
const ic = (0, utils_1.el)('span', { cls: 'row-icon', html: icon });
const lb = (0, utils_1.el)('span', { cls: 'row-label', text: label });
row.append(ic, lb);
return row;
}
function buildEditorPane(e) {
e.emptyState = (0, utils_1.el)('div', { cls: 'empty-state' });
e.emptyState.innerHTML = `
<div class="empty-mark">${exports.ICONS.allNotes}</div>
<div class="empty-title">Select or create a note</div>
<div class="empty-sub">Your thoughts live here — beautifully kept.</div>
<button class="primary-btn" data-act="new">New note</button>`;
e.emptyState.querySelector('[data-act="new"]').addEventListener('click', onNewNote);
e.noteView = (0, utils_1.el)('div', { cls: 'note-view' });
e.noteView.hidden = true;
e.toolbar = (0, utils_1.el)('div', { cls: 'toolbar', attrs: { role: 'toolbar', 'aria-label': 'Formatting' } });
e.saved = (0, utils_1.el)('span', { cls: 'saved-ind', text: 'Saved' });
e.noteMeta = (0, utils_1.el)('div', { cls: 'note-meta' });
e.editorRoot = (0, utils_1.el)('div', {
cls: 'editor-root',
attrs: {
id: 'editor-root',
contenteditable: 'true',
spellcheck: 'false',
role: 'textbox',
'aria-multiline': 'true',
'aria-label': 'Note body',
},
});
e.tagRow = (0, utils_1.el)('div', { cls: 'tag-row' });
e.noteView.append(e.toolbar, e.saved, e.noteMeta, e.editorRoot, e.tagRow);
e.editorPane.append(e.emptyState, e.noteView);
}
/* ---------------------------------------------------------- */
/* render: root dispatcher */
/* ---------------------------------------------------------- */
function renderRoot() {
if (!els)
return;
applyThemeClass();
renderLayout();
const ch = S.lastChange();
switch (ch.kind) {
case 'boot':
case 'notebook':
case 'view':
renderSidebar();
renderList();
renderEditorSlot();
break;
case 'note':
renderSidebar();
renderList();
renderEditorSlot();
break;
case 'layout':
break;
}
}
function renderEditorSlot() {
if (!els)
return;
const sel = S.noteById(S.getState().ui.selectedNoteId);
if (sel && !sel.trashedAt)
renderNotePane(sel);
else
showEmptyState(!!sel && !!sel.trashedAt);
}
function showEmptyState(isTrash) {
if (!els)
return;
(0, editor_1.getEditor)().clear();
els.emptyState.hidden = false;
els.noteView.hidden = true;
const sub = els.emptyState.querySelector('.empty-sub');
const title = els.emptyState.querySelector('.empty-title');
if (isTrash) {
title.textContent = 'Note in Trash';
sub.textContent = 'Use the note menu in the list to restore it or delete it forever.';
}
else {
title.textContent = 'Select or create a note';
sub.textContent = 'Your thoughts live here — beautifully kept.';
}
}
function applyThemeClass() {
const theme = S.getState().ui.theme;
document.documentElement.setAttribute('data-theme', theme);
const pal = constants_1.C.PALETTE[theme];
for (const [k, v] of Object.entries(pal)) {
document.documentElement.style.setProperty('--' + k, v);
}
}
function renderLayout() {
if (!els)
return;
const ui = S.getState().ui;
els.app.classList.toggle('sb-collapsed', !ui.sidebarOpen);
if (ui.sidebarOpen)
els.sidebar.style.width = ui.sidebarW + 'px';
els.resizeHandle.style.left = (ui.sidebarOpen ? ui.sidebarW : 0) + 'px';
}
/* ---------------------------------------------------------- */
/* render: sidebar */
/* ---------------------------------------------------------- */
function renderSidebar() {
if (!els)
return;
const app = S.getState();
const view = app.ui.view;
const alive = app.notes.filter(n => !n.trashedAt);
els.allCount.textContent = String(alive.length);
els.trashCount.textContent = String(app.notes.length - alive.length);
els.allNotesRow.classList.toggle('active', view.type === 'all');
els.trashRow.classList.toggle('active', view.type === 'trash');
// tags (global, from alive notes)
const tagCounts = new Map();
for (const n of alive)
for (const t of n.tags)
tagCounts.set(t, (tagCounts.get(t) ?? 0) + 1);
const tags = [...tagCounts.keys()].sort((a, b) => a.localeCompare(b)).slice(0, 30);
els.tagsBlock.replaceChildren();
if (tags.length) {
const label = (0, utils_1.el)('div', { cls: 'sb-section-label tags-label' });
label.innerHTML = `${exports.ICONS.tag} Tags`;
els.tagsBlock.appendChild(label);
for (const t of tags) {
const chip = (0, utils_1.el)('button', { cls: 'tag-chip', text: '#' + t, title: 'Filter by #' + t });
chip.classList.toggle('active', view.type === 'tag' && view.tag === t);
chip.addEventListener('click', () => S.setView(view.type === 'tag' && view.tag === t ? { type: 'all' } : { type: 'tag', tag: t }));
els.tagsBlock.appendChild(chip);
}
}
// notebooks
els.notebooksBlock.replaceChildren();
for (const nb of app.notebooks) {
els.notebooksBlock.appendChild(renderNotebookRow(nb, view));
}
}
function renderNotebookRow(nb, view) {
const active = view.type === 'notebook' && view.id === nb.id;
const row = (0, utils_1.el)('div', { cls: 'nb-row' + (active ? ' active' : '') + (nb.collapsed ? ' collapsed' : '') });
const head = (0, utils_1.el)('div', { cls: 'nb-head' });
const chev = (0, utils_1.el)('button', { cls: 'nb-chev', title: 'Collapse / expand', attrs: { 'aria-label': 'Toggle notebook' } });
chev.innerHTML = exports.ICONS.chevron;
chev.addEventListener('click', (ev) => {
ev.stopPropagation();
S.toggleNotebookCollapsed(nb.id);
});
const icon = (0, utils_1.el)('span', { cls: 'nb-icon', html: exports.ICONS.folder });
const name = (0, utils_1.el)('span', { cls: 'nb-name', text: nb.name });
const count = (0, utils_1.el)('span', {
cls: 'row-count',
text: String(S.getState().notes.filter(n => !n.trashedAt && n.notebookId === nb.id).length),
});
const menu = (0, utils_1.el)('button', { cls: 'nb-menu icon-btn', title: 'Notebook menu', attrs: { 'aria-label': 'Notebook menu' } });
menu.innerHTML = exports.ICONS.dots;
menu.addEventListener('click', (ev) => {
ev.stopPropagation();
openNotebookMenu(nb, menu);
});
head.append(chev, icon, name, count, menu);
row.appendChild(head);
row.addEventListener('click', () => S.setView({ type: 'notebook', id: nb.id }));
row.addEventListener('dblclick', (ev) => {
ev.stopPropagation();
startNotebookRename(nb, name);
});
row.addEventListener('dragover', (ev) => {
if (!draggingNoteId)
return;
ev.preventDefault();
if (ev.dataTransfer)
ev.dataTransfer.dropEffect = 'move';
row.classList.add('drop-target');
});
row.addEventListener('dragleave', () => row.classList.remove('drop-target'));
row.addEventListener('drop', (ev) => {
ev.preventDefault();
row.classList.remove('drop-target');
const id = (ev.dataTransfer?.getData('text/plain') || draggingNoteId);
if (id)
S.moveNote(id, nb.id);
draggingNoteId = null;
});
if (!nb.collapsed) {
const notes = S.getState().notes
.filter(n => !n.trashedAt && n.notebookId === nb.id)
.sort((a, b) => b.updatedAt - a.updatedAt);
const body = (0, utils_1.el)('div', { cls: 'nb-notes' });
if (notes.length === 0)
body.appendChild((0, utils_1.el)('div', { cls: 'nb-empty', text: 'No notes' }));
for (const n of notes)
body.appendChild(renderMiniNote(n));
row.appendChild(body);
}
return row;
}
function renderMiniNote(n) {
const selected = S.getState().ui.selectedNoteId === n.id;
const row = (0, utils_1.el)('div', { cls: 'mini-note' + (selected ? ' selected' : '') });
const title = (0, utils_1.el)('span', { cls: 'mini-title', text: (0, sanitize_1.deriveTitle)(n.body) || 'Untitled' });
row.append(title);
if (n.pinned)
row.appendChild((0, utils_1.el)('span', { cls: 'mini-pin', html: exports.ICONS.pin, title: 'Pinned' }));
row.addEventListener('click', () => S.selectNote(n.id));
return row;
}
/* ---------------------------------------------------------- */
/* render: note list */
/* ---------------------------------------------------------- */
function onSearchInput() {
if (!els)
return;
if (searchTimer)
window.clearTimeout(searchTimer);
searchTimer = window.setTimeout(() => renderList(), 80);
}
function renderList() {
if (!els)
return;
const app = S.getState();
const view = app.ui.view;
const q = els.searchInput.value.trim().toLowerCase();
if (view.type === 'all')
els.listTitle.textContent = 'All Notes';
else if (view.type === 'trash')
els.listTitle.textContent = 'Trash';
else if (view.type === 'tag')
els.listTitle.textContent = '#' + view.tag;
else {
const nb = app.notebooks.find(n => n.id === view.id);
els.listTitle.textContent = nb ? nb.name : 'All Notes';
}
els.sortSelect.value = app.ui.sort;
let notes = S.visibleNotes();
if (q)
notes = filterSearch(notes, q);
els.listCount.textContent = notes.length === 1 ? '1 note' : notes.length + ' notes';
const scroll = els.listScroll;
scroll.replaceChildren();
els.noteItems.clear();
if (notes.length === 0) {
const empty = (0, utils_1.el)('div', { cls: 'list-empty' });
if (q) {
empty.innerHTML = `<div class="le-icon">${exports.ICONS.search}</div><div class="le-title">No matches for “${(0, utils_1.esc)(q)}”</div><div class="le-sub">Try a different search.</div>`;
}
else if (view.type === 'trash') {
empty.innerHTML = `<div class="le-icon">${exports.ICONS.trash}</div><div class="le-title">Trash is empty</div><div class="le-sub">Deleted notes appear here until you delete them forever.</div>`;
}
else {
empty.innerHTML = `<div class="le-icon">${exports.ICONS.plus}</div><div class="le-title">Nothing here yet</div><div class="le-sub">Click the + to start a note.</div>`;
}
scroll.appendChild(empty);
return;
}
for (const n of notes) {
const item = makeNoteItem(n, q);
scroll.appendChild(item);
els.noteItems.set(n.id, item);
}
}
function filterSearch(notes, q) {
return notes.filter(n => {
if (((0, sanitize_1.deriveTitle)(n.body) || 'Untitled').toLowerCase().includes(q))
return true;
return (0, sanitize_1.extractText)(n.body).toLowerCase().includes(q);
});
}
function makeNoteItem(n, q) {
const selected = S.getState().ui.selectedNoteId === n.id;
const item = (0, utils_1.el)('div', { cls: 'note-item' + (selected ? ' selected' : '') + (n.pinned ? ' pinned' : '') });
item.draggable = true;
item.setAttribute('data-note-id', n.id);
item.setAttribute('role', 'button');
item.setAttribute('tabindex', '0');
const top = (0, utils_1.el)('div', { cls: 'ni-top' });
const title = (0, utils_1.el)('div', { cls: 'ni-title' });
if (q)
title.innerHTML = highlightText((0, sanitize_1.deriveTitle)(n.body) || 'Untitled', q);
else
title.textContent = (0, sanitize_1.deriveTitle)(n.body) || 'Untitled';
const time = (0, utils_1.el)('div', { cls: 'ni-time', text: (0, utils_1.relTime)(n.updatedAt), title: new Date(n.updatedAt).toLocaleString() });
top.append(title, time);
item.appendChild(top);
const snipText = snippetFor(n);
const snip = (0, utils_1.el)('div', { cls: 'ni-snippet' + (snipText ? '' : ' empty') });
if (snipText) {
if (q)
snip.innerHTML = highlightText(snipText, q);
else
snip.textContent = snipText;
}
else
snip.textContent = 'No additional text';
item.appendChild(snip);
const bottom = (0, utils_1.el)('div', { cls: 'ni-bottom' });
for (const t of n.tags.slice(0, 4)) {
const chip = (0, utils_1.el)('button', { cls: 'mini-tag', text: '#' + t });
chip.addEventListener('click', (ev) => {
ev.stopPropagation();
S.setView({ type: 'tag', tag: t });
});
bottom.appendChild(chip);
}
if (n.tags.length > 4)
bottom.appendChild((0, utils_1.el)('span', { cls: 'mini-tag more', text: '+' + (n.tags.length - 4) }));
bottom.appendChild((0, utils_1.el)('span', { cls: 'ni-flex' }));
if (n.pinned && !n.trashedAt)
bottom.appendChild((0, utils_1.el)('span', { cls: 'ni-pin', html: exports.ICONS.pin, title: 'Pinned' }));
const menuBtn = (0, utils_1.el)('button', { cls: 'ni-menu icon-btn', title: 'Note menu', attrs: { 'aria-label': 'Note menu' } });
menuBtn.innerHTML = exports.ICONS.dots;
menuBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
openNoteMenu(n, menuBtn);
});
bottom.appendChild(menuBtn);
item.appendChild(bottom);
item.addEventListener('click', () => S.selectNote(n.id));
item.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter' || ev.key === ' ') {
ev.preventDefault();
S.selectNote(n.id);
}
});
item.addEventListener('dragstart', (ev) => {
draggingNoteId = n.id;
if (ev.dataTransfer) {
ev.dataTransfer.setData('text/plain', n.id);
ev.dataTransfer.effectAllowed = 'move';
}
item.classList.add('dragging');
});
item.addEventListener('dragend', () => {
draggingNoteId = null;
item.classList.remove('dragging');
});
return item;
}
function snippetFor(n) {
const t = (0, sanitize_1.extractText)(n.body).replace(/\s+/g, ' ').trim();
return t.length > constants_1.C.SNIPPET_LEN ? t.slice(0, constants_1.C.SNIPPET_LEN).trimEnd() + '…' : t;
}
function highlightText(text, q) {
if (!q)
return (0, utils_1.esc)(text);
const idx = text.toLowerCase().indexOf(q.toLowerCase());
if (idx < 0)
return (0, utils_1.esc)(text);
return (0, utils_1.esc)(text.slice(0, idx)) + '<mark>' + (0, utils_1.esc)(text.slice(idx, idx + q.length)) + '</mark>' + (0, utils_1.esc)(text.slice(idx + q.length));
}
/* ---------------------------------------------------------- */
/* render: editor pane */
/* ---------------------------------------------------------- */
function renderNotePane(n) {
if (!els)
return;
els.emptyState.hidden = true;
els.noteView.hidden = false;
const ed = (0, editor_1.getEditor)();
const keep = ed.isActive() && ed.noteId === n.id;
ed.load(n.id, n.body, keep);
renderMeta(n);
renderTagRow(n);
els.saved.classList.remove('show');
}
function renderMeta(n) {
if (!els)
return;
const app = S.getState();
const bits = [];
const nb = n.notebookId ? app.notebooks.find(x => x.id === n.notebookId) : null;
bits.push(nb ? nb.name : 'All Notes');
bits.push('Edited ' + (0, utils_1.relTime)(n.updatedAt));
const words = (0, sanitize_1.extractText)(n.body).split(/\s+/).filter(Boolean).length;
if (words)
bits.push(words + (words === 1 ? ' word' : ' words'));
if (n.pinned)
bits.push('Pinned');
els.noteMeta.textContent = bits.join(' · ');
}
function renderTagRow(n) {
if (!els)
return;
const row = els.tagRow;
row.replaceChildren();
for (const t of n.tags) {
const chip = (0, utils_1.el)('span', { cls: 'tag-chip', title: 'Filter by #' + t });
const labelPart = (0, utils_1.el)('button', { cls: 'tag-chip-label', text: '#' + t });
labelPart.addEventListener('click', () => S.setView({ type: 'tag', tag: t }));
const x = (0, utils_1.el)('button', { cls: 'tag-x', title: 'Remove tag', attrs: { 'aria-label': 'Remove tag' } });
x.innerHTML = exports.ICONS.x;
x.addEventListener('click', () => S.setNoteTags(n.id, n.tags.filter(t2 => t2 !== t)));
chip.append(labelPart, x);
row.appendChild(chip);
}
const add = (0, utils_1.el)('button', { cls: 'tag-chip add', text: '+ tag', title: 'Add a tag' });
add.addEventListener('click', () => promptAddTag(n));
row.appendChild(add);
}
function promptAddTag(n) {
const raw = window.prompt('Add tag(s) — separate several with commas:', '');
if (raw == null)
return;
const parts = raw.split(',').map(t => t.trim().replace(/^#/, '').toLowerCase()).filter(Boolean);
const merged = [...n.tags];
for (const p of parts)
if (p && !merged.includes(p))
merged.push(p);
if (merged.length !== n.tags.length)
S.setNoteTags(n.id, merged);
}
/* ---------------------------------------------------------- */
/* menus */
/* ---------------------------------------------------------- */
let openMenu = null;
let menuCleanups = [];
function closeMenu() {
for (const fn of menuCleanups)
fn();
menuCleanups = [];
if (openMenu)
openMenu.remove();
openMenu = null;
}
function menuShell() {
const m = (0, utils_1.el)('div', { cls: 'ctx-menu' });
openMenu = m;
const onDown = (ev) => {
if (openMenu && !openMenu.contains(ev.target))
closeMenu();
};
const onKey = (ev) => {
if (ev.key === 'Escape')
closeMenu();
};
document.addEventListener('mousedown', onDown);
document.addEventListener('keydown', onKey);
menuCleanups.push(() => document.removeEventListener('mousedown', onDown));
menuCleanups.push(() => document.removeEventListener('keydown', onKey));
return m;
}
function menuRow(label, opts = {}) {
const b = (0, utils_1.el)('button', {
cls: 'menu-row' + (opts.danger ? ' danger' : '') + (opts.disabled ? ' disabled' : ''),
text: label,
});
b.disabled = !!opts.disabled;
return b;
}
function placeMenu(m, anchor, alignRight) {
m.style.position = 'fixed';
const r = anchor.getBoundingClientRect();
m.style.top = Math.min(window.innerHeight - m.offsetHeight - 8, r.bottom + 4) + 'px';
m.style.left = Math.max(8, alignRight ? Math.min(r.right - m.offsetWidth, window.innerWidth - m.offsetWidth - 8) : r.left) + 'px';
}
function openNoteMenu(n, anchor) {
closeMenu();
const m = menuShell();
if (n.trashedAt) {
const restore = menuRow('Restore to original notebook');
restore.addEventListener('click', () => {
S.restoreNote(n.id);
closeMenu();
});
const destroy = menuRow('Delete forever', { danger: true });
destroy.addEventListener('click', () => {
if (window.confirm('Delete this note permanently? This cannot be undone.')) {
S.destroyNote(n.id);
closeMenu();
}
});
m.append(restore, destroy);
}
else {
const pin = menuRow(n.pinned ? 'Unpin note' : 'Pin to top');
pin.addEventListener('click', () => {
S.setPinned(n.id, !n.pinned);
closeMenu();
});
const dup = menuRow('Duplicate');
dup.addEventListener('click', () => {
S.duplicateNote(n.id);
closeMenu();
});
m.append(pin, dup);
m.appendChild((0, utils_1.el)('div', { cls: 'menu-label', text: 'Move to notebook' }));
const all = menuRow('All Notes (no notebook)');
all.addEventListener('click', () => {
S.moveNote(n.id, null);
closeMenu();
});
m.appendChild(all);
for (const nb of S.getState().notebooks) {
const row = menuRow(nb.name + (nb.id === n.notebookId ? ' ✓' : ''));
row.disabled = nb.id === n.notebookId;
row.addEventListener('click', () => {
S.moveNote(n.id, nb.id);
closeMenu();
});
m.appendChild(row);
}
m.appendChild((0, utils_1.el)('div', { cls: 'menu-sep' }));
const trash = menuRow('Move to Trash', { danger: true });
trash.addEventListener('click', () => {
S.trashNote(n.id);
closeMenu();
});
m.appendChild(trash);
}
document.body.appendChild(m);
placeMenu(m, anchor, true);
}
function openNotebookMenu(nb, anchor) {
closeMenu();
const m = menuShell();
const rename = menuRow('Rename notebook');
rename.addEventListener('click', () => {
const row = anchor.closest('.nb-row');
const nameEl = row ? row.querySelector('.nb-name') : null;
closeMenu();
if (nameEl)
startNotebookRename(nb, nameEl);
});
const del = menuRow('Delete notebook', { danger: true });
del.addEventListener('click', () => {
const ok = window.confirm(`Delete “${nb.name}”? Its notes stay in All Notes.`);
if (ok) {
S.deleteNotebook(nb.id);
closeMenu();
}
});
m.append(rename, del);
document.body.appendChild(m);
placeMenu(m, anchor, false);
}
function startNotebookRename(nb, nameEl) {
const input = (0, utils_1.el)('input', { cls: 'inline-rename', attrs: { value: nb.name } });
nameEl.replaceWith(input);
input.focus();
input.select();
let done = false;
const finish = (save) => {
if (done)
return;
done = true;
if (save && input.value.trim())
S.renameNotebook(nb.id, input.value);
renderSidebar();
};
input.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
finish(true);
}
if (ev.key === 'Escape') {
ev.preventDefault();
finish(false);
}
});
input.addEventListener('blur', () => finish(true));
}
/* ---------------------------------------------------------- */
/* actions */
/* ---------------------------------------------------------- */
function onNewNote() {
const v = S.getState().ui.view;
if (v.type === 'trash')
return;
const nbId = v.type === 'notebook' ? v.id : null;
S.createNote(nbId);
}
function onNewNotebook() {
const name = window.prompt('Notebook name:', 'New Notebook');
if (name == null)
return;
S.createNotebook(name.trim() || 'New Notebook');
}
/* ---------------------------------------------------------- */
/* theme + sidebar toggle */
/* ---------------------------------------------------------- */
function wireTheme(e) {
const btn = (0, utils_1.el)('button', { cls: 'icon-btn theme-btn', title: 'Toggle theme', attrs: { 'aria-label': 'Toggle theme' } });
btn.innerHTML = exports.ICONS.theme;
btn.addEventListener('click', () => S.setTheme(S.getState().ui.theme === 'dark' ? 'light' : 'dark'));
e.editorPane.appendChild(btn);
}
function wireSidebarToggle(e) {
const btn = (0, utils_1.el)('button', { cls: 'sb-toggle', title: 'Show notebooks', attrs: { 'aria-label': 'Show sidebar' } });
btn.innerHTML = exports.ICONS.folder;
btn.addEventListener('click', () => S.setSidebarOpen(true));
e.editorPane.appendChild(btn);
e.sidebarToggle = btn;
}
/* ---------------------------------------------------------- */
/* saved indicator */
/* ---------------------------------------------------------- */
function flashSaved() {
if (!els)
return;
const s = els.saved;
s.classList.add('show');
if (els.savedTimer)
window.clearTimeout(els.savedTimer);
els.savedTimer = window.setTimeout(() => s.classList.remove('show'), 1600);
}
/* ---------------------------------------------------------- */
/* resizer */
/* ---------------------------------------------------------- */
function wireResizer(e) {
e.resizeHandle = (0, utils_1.el)('div', { cls: 'resize-handle', attrs: { 'aria-hidden': 'true' } });
e.app.appendChild(e.resizeHandle);
let dragging = false;
e.resizeHandle.addEventListener('mousedown', (ev) => {
dragging = true;
document.body.classList.add('resizing');
ev.preventDefault();
});
document.addEventListener('mousemove', (ev) => {
if (!dragging)
return;
S.setPaneWidth('sidebar', (0, utils_1.clamp)(ev.clientX, constants_1.C.SIDEBAR.min, constants_1.C.SIDEBAR.max));
});
document.addEventListener('mouseup', () => {
if (!dragging)
return;
dragging = false;
document.body.classList.remove('resizing');
S.commitLayout();
});
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"ac55e9b5-294e-4b57-8d70-0af349472068","tokensIn":6099830,"tokensOut":309769,"tokensTotal":6409599,"cost":0,"turns":69,"toolCalls":99,"failedToolCalls":5,"timestamp":"2026-08-15T15:36:32.686Z"} -->