← Notes results

Notes

ox alpha · full

Initial view of Notes

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 66,327 bytes · SHA-256 74f541874be5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inkwell — Notes</title>
<style>
/* ============================================================
   TUNABLE CONSTANTS
   ============================================================ */
:root {
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
  --font-editor: Georgia, "Iowan Old Style", "Times New Roman", serif;
  --font-mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace;

  /* light theme */
  --bg-app: #f6f2ec;
  --bg-sidebar: #efe9df;
  --bg-list: #f8f4ee;
  --bg-editor: #fffdf9;
  --bg-hover: rgba(60,45,20,.06);
  --bg-active: rgba(180,120,50,.14);
  --bg-selected: rgba(190,125,55,.18);
  --accent: #b06f2e;
  --accent-strong: #8f5720;
  --text: #33291c;
  --text-dim: #7a6c58;
  --text-faint: #a3957f;
  --border: rgba(80,60,30,.13);
  --shadow: 0 1px 3px rgba(60,40,10,.08), 0 8px 24px rgba(60,40,10,.07);
  --danger: #b3402e;
  --code-bg: rgba(150,110,50,.12);
  --quote-bar: #c99a5e;

  --radius: 8px;
  --sidebar-w: 232px;
  --list-w: 288px;
}

html[data-theme="dark"] {
  --bg-app: #1d1a16;
  --bg-sidebar: #211d18;
  --bg-list: #26221c;
  --bg-editor: #2a251f;
  --bg-hover: rgba(255,235,200,.07);
  --bg-active: rgba(220,160,90,.12);
  --bg-selected: rgba(220,160,90,.18);
  --accent: #d29a58;
  --accent-strong: #e0ad68;
  --text: #ece4d6;
  --text-dim: #a89a85;
  --text-faint: #77695a;
  --border: rgba(255,240,210,.10);
  --shadow: 0 1px 3px rgba(0,0,0,.35), 0 8px 24px rgba(0,0,0,.25);
  --danger: #d96a56;
  --code-bg: rgba(230,180,100,.15);
  --quote-bar: #a97e46;
}

/* ============================================================
   BASE
   ============================================================ */
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  font-family: var(--font-ui);
  background: var(--bg-app);
  color: var(--text);
  font-size: 14px;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}
button { font-family: inherit; color: inherit; background: none; border: none; cursor: pointer; }
input { font-family: inherit; color: inherit; }
::selection { background: rgba(200,140,70,.30); }

/* ============================================================
   APP LAYOUT
   ============================================================ */
#app { display: flex; height: 100vh; }

.pane-divider {
  flex: 0 0 5px;
  cursor: col-resize;
  background: transparent;
  position: relative;
  z-index: 5;
  transition: background .15s;
}
.pane-divider:hover, .pane-divider.dragging { background: var(--accent); opacity:.5; }
body.resizing { cursor: col-resize; user-select: none; }
body.resizing iframe, body.resizing .editor-scroll { pointer-events: none; }

/* ============================================================
   SIDEBAR
   ============================================================ */
#sidebar {
  width: var(--sidebar-w);
  min-width: 170px;
  max-width: 420px;
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  transition: margin-left .28s cubic-bezier(.4,0,.2,1), opacity .28s;
  overflow: hidden;
}
#sidebar.collapsed { margin-left: calc(-1 * var(--sidebar-w)); opacity: 0; }
.sidebar-header {
  display: flex; align-items: center; gap: 8px;
  padding: 14px 14px 10px;
}
.app-title { font-weight: 700; font-size: 15px; letter-spacing: .3px; flex: 1; display:flex; align-items:center; gap:7px;}
.app-logo { width:20px;height:20px;border-radius:6px;background:linear-gradient(135deg,var(--accent),#d9a869);display:inline-flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:800;font-family:var(--font-editor);}
.icon-btn {
  width: 26px; height: 26px; border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-dim); font-size: 14px; line-height: 1;
  transition: background .12s, color .12s;
  flex-shrink: 0;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text); }
.icon-btn.active { background: var(--bg-active); color: var(--accent-strong); }

.sidebar-scroll { flex: 1; overflow-y: auto; padding: 4px 8px 16px; }
.sb-section-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-faint); padding: 12px 8px 5px;
  display: flex; align-items: center;
}
.sb-section-label .icon-btn { margin-left: auto; width: 20px; height: 20px; font-size: 13px; visibility: hidden; }
.sidebar-scroll:hover .sb-section-label .icon-btn, .sb-section-label:hover .icon-btn { visibility: visible; }

.sb-item {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: 7px;
  cursor: pointer; color: var(--text-dim);
  transition: background .12s, color .12s;
  user-select: none;
  font-size: 13.5px;
}
.sb-item:hover { background: var(--bg-hover); color: var(--text); }
.sb-item.selected { background: var(--bg-selected); color: var(--accent-strong); font-weight: 600; }
.sb-item.drop-target { outline: 2px dashed var(--accent); outline-offset: -2px; background: var(--bg-active); }
.sb-item .glyph { width: 17px; text-align: center; font-size: 13px; flex-shrink: 0; }
.sb-item .name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sb-item .count { font-size: 11px; color: var(--text-faint); }
.sb-item .twisty { width: 14px; font-size: 9px; color: var(--text-faint); transition: transform .15s; flex-shrink:0; }
.sb-item .twisty.open { transform: rotate(90deg); }
.sb-notebook-notes { overflow: hidden; transition: max-height .22s ease; }
.sb-notebook-notes.collapsed { display: none; }
.sb-subitem { padding-left: 34px; font-size: 13px; }
.sb-rename-input {
  flex: 1; min-width: 0; background: var(--bg-editor); border: 1px solid var(--accent);
  border-radius: 4px; padding: 2px 6px; font-size: 13px; outline: none;
}

.tag-cloud { padding: 6px 8px; display: flex; flex-wrap: wrap; gap: 5px; }
.tag-chip {
  font-size: 11.5px; padding: 2.5px 9px; border-radius: 999px;
  background: var(--bg-hover); color: var(--text-dim); cursor: pointer;
  border: 1px solid transparent; transition: all .12s;
}
.tag-chip:hover { color: var(--text); background: var(--bg-active); }
.tag-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }

.storage-foot { padding: 8px 14px; font-size: 10.5px; color: var(--text-faint); border-top: 1px solid var(--border); }

/* ============================================================
   NOTE LIST PANE
   ============================================================ */
#notelist-pane {
  width: var(--list-w);
  min-width: 200px;
  max-width: 480px;
  background: var(--bg-list);
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column;
}
.nl-header { padding: 12px 12px 8px; }
.nl-title-row { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.nl-title { font-size: 15px; font-weight: 700; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-wrap { position: relative; }
.search-wrap svg { position:absolute; left:9px; top:50%; transform:translateY(-50%); color:var(--text-faint); }
#search-input {
  width: 100%; padding: 7px 10px 7px 30px;
  border-radius: 8px; border: 1px solid var(--border);
  background: var(--bg-editor); font-size: 13px; outline: none;
  transition: border-color .15s, box-shadow .15s;
}
#search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(176,111,46,.15); }
.nl-controls { display: flex; align-items: center; padding: 6px 4px 2px; gap: 4px; }
.sort-btn {
  font-size: 11.5px; color: var(--text-dim); padding: 3px 8px; border-radius: 6px;
  transition: all .12s; display:inline-flex; align-items:center; gap:4px;
}
.sort-btn:hover { background: var(--bg-hover); }
.sort-btn.active { color: var(--accent-strong); font-weight: 600; background: var(--bg-active); }
.nl-count { font-size: 11px; color: var(--text-faint); margin-left: auto; }

.nl-scroll { flex: 1; overflow-y: auto; padding: 4px 8px 16px; }
.note-card {
  padding: 10px 12px; border-radius: var(--radius);
  cursor: pointer; margin-bottom: 2px;
  border: 1px solid transparent;
  transition: background .13s, transform .13s, box-shadow .13s;
  animation: cardIn .18s ease both;
}
@keyframes cardIn { from { opacity: 0; transform: translateY(4px);} to {opacity:1; transform:none;} }
.note-card:hover { background: var(--bg-hover); }
.note-card.selected { background: var(--bg-editor); border-color: var(--border); box-shadow: var(--shadow); }
.note-card.dragging { opacity: .45; }
.nc-title-row { display: flex; align-items: center; gap: 6px; }
.nc-title { font-weight: 600; font-size: 13.5px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pin-glyph { font-size: 11px; color: var(--accent); }
.nc-snippet { font-size: 12px; color: var(--text-dim); margin-top: 3px; line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.nc-meta { font-size: 11px; color: var(--text-faint); margin-top: 5px; display: flex; gap: 6px; align-items: center; }
.nc-tags { display:flex; gap:4px; flex-wrap: wrap; }
.nc-tag { font-size: 10px; background: var(--bg-active); color: var(--text-dim); border-radius: 999px; padding: 1px 7px; }
mark.hl { background: rgba(230,180,80,.45); color: inherit; border-radius: 2px; padding: 0 1px; }
.section-head { font-size: 10.5px; font-weight:700; text-transform:uppercase; letter-spacing:.08em; color: var(--text-faint); padding: 8px 6px 4px; }
.empty-pane { text-align:center; color: var(--text-faint); padding: 40px 20px; font-size: 13px; }

/* ============================================================
   EDITOR PANE
   ============================================================ */
#editor-pane { flex: 1; min-width: 300px; display: flex; flex-direction: column; background: var(--bg-editor); }
.ed-toolbar {
  display: flex; align-items: center; gap: 2px; padding: 7px 12px;
  border-bottom: 1px solid var(--border); flex-wrap: wrap;
  background: var(--bg-editor);
}
.tb-btn {
  min-width: 28px; height: 28px; padding: 0 6px; border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-dim); font-size: 13px; transition: all .12s;
}
.tb-btn:hover { background: var(--bg-hover); color: var(--text); }
.tb-btn.active { background: var(--bg-active); color: var(--accent-strong); }
.tb-sep { width: 1px; height: 18px; background: var(--border); margin: 0 5px; }
.tb-btn b, .tb-btn i { font-style:normal; }
.tb-btn .serif-i { font-family: Georgia, serif; font-style: italic; }
.ed-scroll { flex: 1; overflow-y: auto; }
.ed-inner { max-width: 720px; margin: 0 auto; padding: 36px 48px 40vh; }

.ed-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.ed-time { font-size: 11.5px; color: var(--text-faint); }
.saved-ind { font-size: 11px; color: var(--text-faint); opacity: 0; transition: opacity .3s; display:inline-flex; align-items:center; gap:4px; }
.saved-ind.show { opacity: 1; }
.saved-dot { width:6px;height:6px;border-radius:50%;background:#5aa05a; }

#title-input {
  width: 100%; border: none; outline: none; background: transparent;
  font-family: var(--font-editor); font-size: 30px; font-weight: 700;
  color: var(--text); padding: 6px 0 4px; border-bottom: 1px solid transparent;
  transition: border-color .2s;
}
#title-input:focus { border-color: var(--border); }
#title-input::placeholder { color: var(--text-faint); }

.tags-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 8px 0 4px; }
.ed-tag {
  font-size: 11.5px; background: var(--bg-active); color: var(--text-dim);
  border-radius: 999px; padding: 3px 6px 3px 10px; display: inline-flex; align-items: center; gap: 5px;
}
.ed-tag button { font-size: 12px; color: var(--text-faint); border-radius: 50%; width: 15px; height: 15px; line-height: 1; }
.ed-tag button:hover { color: var(--danger); }
#tag-input {
  border: none; outline: none; background: transparent; font-size: 11.5px;
  width: 90px; color: var(--text-dim);
}
#tag-input::placeholder { color: var(--text-faint); }

#editor {
  font-family: var(--font-editor);
  font-size: 16.5px; line-height: 1.72;
  outline: none; padding-top: 10px; color: var(--text);
  min-height: 300px; word-wrap: break-word;
}
#editor:focus { outline: none; }
#editor h1 { font-size: 27px; margin: 18px 0 8px; line-height: 1.25; font-weight: 700; }
#editor h2 { font-size: 22px; margin: 16px 0 6px; font-weight: 700; }
#editor h3 { font-size: 18.5px; margin: 14px 0 5px; font-weight: 600; }
#editor p { margin: 6px 0; }
#editor ul, #editor ol { padding-left: 26px; margin: 6px 0; }
#editor li { margin: 2px 0; }
#editor blockquote {
  border-left: 3px solid var(--quote-bar); margin: 10px 0; padding: 4px 16px;
  color: var(--text-dim); font-style: italic; background: var(--bg-hover); border-radius: 0 6px 6px 0;
}
#editor hr { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
#editor code, .inline-code {
  font-family: var(--font-mono); font-size: .84em;
  background: var(--code-bg); padding: 2px 6px; border-radius: 5px;
}
#editor a { color: var(--accent-strong); }

/* checklists */
#editor ul.checklist { list-style: none; padding-left: 4px; }
#editor ul.checklist li {
  position: relative; padding-left: 27px; margin: 3px 0;
}
#editor ul.checklist li::before {
  content: ""; position: absolute; left: 2px; top: .42em;
  width: 16px; height: 16px; border-radius: 5px;
  border: 1.6px solid var(--text-faint); cursor: pointer;
  transition: all .15s; background: transparent;
}
#editor ul.checklist li[data-checked="true"] { color: var(--text-faint); text-decoration: line-through; text-decoration-color: var(--text-faint); }
#editor ul.checklist li[data-checked="true"]::before {
  background: var(--accent); border-color: var(--accent);
}
#editor ul.checklist li[data-checked="true"]::after {
  content: ""; position: absolute; left: 7px; top: .52em;
  width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg); pointer-events: none;
}

/* empty state */
.editor-empty {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  color: var(--text-faint); gap: 14px;
}
.editor-empty .big-glyph { font-size: 44px; opacity: .55; }
.editor-empty h2 { font-family: var(--font-editor); font-weight: 500; font-size: 19px; color: var(--text-dim); }
.btn-primary {
  background: var(--accent); color: #fff; padding: 8px 18px; border-radius: 8px;
  font-size: 13.5px; font-weight: 600; transition: all .15s; box-shadow: var(--shadow);
}
.btn-primary:hover { background: var(--accent-strong); transform: translateY(-1px); }

/* menus & dialogs */
.ctx-menu {
  position: fixed; z-index: 100; min-width: 168px;
  background: var(--bg-editor); border: 1px solid var(--border);
  border-radius: 10px; box-shadow: var(--shadow); padding: 5px;
  animation: menuIn .12s ease both;
}
@keyframes menuIn { from { opacity:0; transform: scale(.97) translateY(-3px);} to{opacity:1;transform:none;} }
.ctx-menu button {
  display: flex; width: 100%; text-align: left; padding: 7px 10px;
  border-radius: 6px; font-size: 13px; gap: 9px; align-items: center; color: var(--text);
}
.ctx-menu button:hover { background: var(--bg-hover); }
.ctx-menu button.danger { color: var(--danger); }
.ctx-menu hr { border:none; border-top:1px solid var(--border); margin:4px 0; }
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(30,20,8,.35); z-index: 90;
  display: flex; align-items: flex-start; justify-content: center; padding-top: 18vh;
  animation: fadeIn .15s ease;
}
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
.modal {
  background: var(--bg-editor); border-radius: 14px; box-shadow: var(--shadow);
  padding: 22px 24px; width: 360px; animation: menuIn .18s ease;
}
.modal h3 { font-size: 15px; margin-bottom: 14px; }
.modal input[type=text] {
  width: 100%; padding: 8px 11px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--bg-app);
  font-size: 14px; outline: none;
}
.modal input[type=text]:focus { border-color: var(--accent); }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
.btn-ghost { padding: 7px 14px; border-radius: 8px; color: var(--text-dim); font-size: 13px; }
.btn-ghost:hover { background: var(--bg-hover); }

/* scrollbars */
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; border: 2px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); background-clip: content-box; }

.trashed-banner {
  background: var(--bg-active); color: var(--accent-strong);
  padding: 8px 14px; border-radius: 8px; font-size: 12.5px;
  display: flex; align-items: center; gap: 10px; margin-bottom: 8px;
}
.trashed-banner button { color: var(--accent-strong); font-weight: 600; text-decoration: underline; font-size: 12.5px; }
.trashed-banner button.del { color: var(--danger); }
[contenteditable][data-placeholder]:empty::before,
#editor[data-empty="true"]::before {
  content: attr(data-placeholder); color: var(--text-faint); pointer-events: none;
  font-style: italic;
}
</style>
</head>
<body>
<div id="app">
  <!-- ================= SIDEBAR ================= -->
  <aside id="sidebar">
    <div class="sidebar-header">
      <span class="app-title"><span class="app-logo">N</span> Inkwell</span>
      <button class="icon-btn" id="btn-theme" title="Toggle theme">◐</button>
      <button class="icon-btn" id="btn-collapse-sb" title="Collapse sidebar">«</button>
    </div>
    <div class="sidebar-scroll" id="sidebar-nav"></div>
    <div class="storage-foot" id="storage-foot">All data stored locally</div>
  </aside>
  <div class="pane-divider" id="divider-sb"></div>

  <!-- ================= NOTE LIST ================= -->
  <section id="notelist-pane">
    <div class="nl-header">
      <div class="nl-title-row">
        <span class="nl-title" id="nl-title">All Notes</span>
        <button class="icon-btn" id="btn-new-note" title="New note">+</button>
        <button class="icon-btn" id="btn-nl-menu" title="Notebook options">⋯</button>
      </div>
      <div class="search-wrap">
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
        <input id="search-input" type="text" placeholder="Search notes…" autocomplete="off">
      </div>
      <div class="nl-controls" id="sort-controls">
        <button class="sort-btn active" data-sort="updated">Modified ↓</button>
        <button class="sort-btn" data-sort="created">Created</button>
        <button class="sort-btn" data-sort="alpha">A–Z</button>
        <span class="nl-count" id="nl-count"></span>
      </div>
    </div>
    <div class="nl-scroll" id="note-list"></div>
  </section>
  <div class="pane-divider" id="divider-nl"></div>

  <!-- ================= EDITOR ================= -->
  <section id="editor-pane"></section>
</div>
<script>
"use strict";
/* ============================================================
   TUNABLE CONSTANTS
   ============================================================ */
const CFG = {
  STORAGE_KEY: "inkwell.notes.v1",
  AUTOSAVE_DEBOUNCE: 450,
  SNIPPET_LEN: 120,
  UNDO_LIMIT: 200,
  SEARCH_HL: true,
};

/* ============================================================
   UTILITIES
   ============================================================ */
const $ = (sel, el = document) => el.querySelector(sel);
const $$ = (sel, el = document) => [...el.querySelectorAll(sel)];
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
const esc = s => s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));

function relTime(ts) {
  const d = new Date(ts), now = Date.now(), diff = (now - d) / 1000;
  if (diff < 60) return "just now";
  if (diff < 3600) return Math.floor(diff / 60) + "m ago";
  if (diff < 86400 && new Date().getDate() === d.getDate()) return "Today " + d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
  if (diff < 172800) return "Yesterday " + d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
  return d.toLocaleDateString([], { month: "short", day: "numeric", year: d.getFullYear() !== new Date().getFullYear() ? "numeric" : undefined });
}

/* ============================================================
   DATA MODEL  (single source of truth)
   ============================================================ */
let state = null;

function defaultState() {
  const nb1 = { id: uid(), name: "Personal", collapsed: false };
  const nb2 = { id: uid(), name: "Work", collapsed: false };
  const t = Date.now();
  return {
    notebooks: [nb1, nb2],
    notes: [
      {
        id: uid(), notebookId: nb1.id, title: "Welcome to Inkwell",
        html: `<h1>Welcome ✨</h1><p>This is your <b>notebook</b>. A few things to try:</p><ul class="checklist"><li data-checked="true">Type <code># </code> at a line start for a heading</li><li data-checked="false">Type <code>- </code> for bullets or <code>[] </code> for checklists</li><li data-checked="false">Drag this note onto another notebook</li></ul><blockquote>Everything saves automatically to your browser.</blockquote><p>Select some text and use the toolbar, or press <code>Ctrl+B</code>.</p>`,
        tags: ["guide"], pinned: true, createdAt: t, updatedAt: t, trashed: false,
      },
      {
        id: uid(), notebookId: nb2.id, title: "Project kickoff",
        html: "<h2>Kickoff agenda</h2><ol><li>Intros</li><li>Scope review</li><li>Timeline</li></ol>",
        tags: ["meeting"], pinned: false, createdAt: t - 86400000, updatedAt: t - 3600000, trashed: false,
      },
    ],
    ui: {
      view: "all",            // 'all' | 'trash' | notebookId
      selectedNoteId: null,
      sortMode: "updated",
      theme: "light",
      sidebarCollapsed: false,
      sbWidth: 232,
      nlWidth: 288,
      search: "",
      tagFilter: null,
    },
  };
}

function saveState() {
  localStorage.setItem(CFG.STORAGE_KEY, JSON.stringify(state));
}
function loadState() {
  try {
    const raw = localStorage.getItem(CFG.STORAGE_KEY);
    if (raw) { state = JSON.parse(raw); if (state && state.notes && state.notebooks) return; }
  } catch (e) { /* fall through */ }
  state = defaultState();
}

const getNote = id => state.notes.find(n => n.id === id);
const getNotebook = id => state.notebooks.find(n => n.id === id);

/* ============================================================
   TEXT EXTRACTION & SANITIZATION
   ============================================================ */
function extractText(html) {
  const div = document.createElement("div");
  div.innerHTML = html;
  // drop checkbox pseudo-content; textContent is enough
  return (div.textContent || "").replace(/\s+/g, " ").trim();
}

// Allowlist sanitizer for stored/pasted HTML
const ALLOWED = {
  tags: new Set(["B","STRONG","I","EM","U","S","STRIKE","DEL","CODE","H1","H2","H3",
    "P","DIV","BR","UL","OL","LI","BLOCKQUOTE","HR","A","SPAN"]),
  voidTags: new Set(["BR","HR"]),
};
function sanitizeNode(node, out) {
  for (const child of [...node.childNodes]) {
    if (child.nodeType === Node.TEXT_NODE) { out.appendChild(document.createTextNode(child.textContent)); continue; }
    if (child.nodeType !== Node.ELEMENT_NODE) continue;
    const tag = child.tagName;
    if (!ALLOWED.tags.has(tag)) { sanitizeNode(child, out); continue; }
    if (ALLOWED.voidTags.has(tag)) { out.appendChild(document.createElement(tag)); continue; }
    const el = document.createElement(tag);
    if (tag === "LI") {
      const chk = child.getAttribute("data-checked");
      el.setAttribute("data-checked", chk === "true" ? "true" : "false");
    }
    if (tag === "A") {
      const href = child.getAttribute("href") || "";
      if (/^https?:\/\//i.test(href)) el.setAttribute("href", href);
      else el.removeAttribute("href");
    }
    sanitizeNode(child, el);
    // unwrap empty non-list containers
    if ((tag === "P" || tag === "DIV" || tag === "SPAN") && !el.textContent.trim() && !el.querySelector("br,img")) continue;
    out.appendChild(el);
  }
}
function sanitizeHTML(html) {
  const inDiv = document.createElement("div");
  inDiv.innerHTML = html || "";
  const out = document.createElement("div");
  sanitizeNode(inDiv, out);
  let h = out.innerHTML;
  // normalize checklist items
  h = h.replace(/<li([^>]*)>/gi, (m, attrs) => {
    const mm = attrs.match(/data-checked=["']?(true|false)/i);
    return `<li${mm ? ` data-checked="${mm[1].toLowerCase()}"` : ""}>`;
  });
  // keep checklist class on lists that contain checklist items
  h = h.replace(/<ul>((?:(?!<\/ul>).)*?<li data-checked)/gis, "<ul class=\"checklist\">$1");
  h = h.replace(/<div>(<br>)?<\/div>/gi, "");
  return h.trim();
}
function ensureChecklistClass(html) {
  return html.replace(/<ul>(?=\s*<li)/gi, m => {
    // only mark as checklist if any li has data-checked OR it was already marked
    return m;
  });
}

/* ============================================================
   RENDERING — SIDEBAR
   ============================================================ */
function visibleNotes() {
  const v = state.ui.view;
  let list;
  if (v === "trash") list = state.notes.filter(n => n.trashed);
  else list = state.notes.filter(n => !n.trashed && (v === "all" || n.notebookId === v));
  const q = state.ui.search.trim().toLowerCase();
  if (state.ui.tagFilter) list = list.filter(n => n.tags.includes(state.ui.tagFilter));
  if (q) list = list.filter(n =>
    n.title.toLowerCase().includes(q) || extractText(n.html).toLowerCase().includes(q));
  return list;
}

function sortedNotes(list) {
  const pinned = list.filter(n => n.pinned), rest = list.filter(n => !n.pinned);
  const cmp = {
    updated: (a, b) => b.updatedAt - a.updatedAt,
    created: (a, b) => b.createdAt - a.createdAt,
    alpha: (a, b) => a.title.localeCompare(b.title),
  }[state.ui.sortMode];
  return [...pinned.sort(cmp), ...rest.sort(cmp)];
}

function renderSidebar() {
  const nav = $("#sidebar-nav");
  const v = state.ui.view;
  const counts = id => state.notes.filter(n => !n.trashed && n.notebookId === id).length;
  let h = "";

  h += sbItem("all", v, "🗂", "All Notes", state.notes.filter(n => !n.trashed).length);
  h += `<div class="sb-section-label">Notebooks<button class="icon-btn" id="btn-new-nb" title="New notebook">+</button></div>`;
  for (const nb of state.notebooks) {
    h += `<div class="sb-item ${v === nb.id ? "selected" : ""}" data-view="${nb.id}" data-droppable="1">
      <span class="twisty ${nb.collapsed ? "" : "open"}" data-twisty="${nb.id}">▶</span>
      <span class="glyph">📓</span>
      <span class="name">${esc(nb.name)}</span>
      <span class="count">${counts(nb.id)}</span>
      <button class="icon-btn" data-nb-menu="${nb.id}" style="width:20px;height:20px;font-size:12px">⋯</button>
    </div>`;
    if (!nb.collapsed) {
      const subs = sortedNotes(state.notes.filter(n => !n.trashed && n.notebookId === nb.id)).slice(0, 6);
      h += `<div class="sb-notebook-notes">` + subs.map(n =>
        `<div class="sb-item sb-subitem ${state.ui.selectedNoteId === n.id ? "selected" : ""}" data-open-note="${n.id}">
          <span class="glyph">•</span><span class="name">${esc(n.title || "Untitled")}</span>
        </div>`).join("") + `</div>`;
    }
  }
  h += `<div class="sb-section-label">Tags</div>`;
  const tagSet = [...new Set(state.notes.filter(n => !n.trashed).flatMap(n => n.tags))].sort();
  h += tagSet.length
    ? `<div class="tag-cloud">` + tagSet.map(t =>
        `<span class="tag-chip ${state.ui.tagFilter === t ? "active" : ""}" data-tag-filter="${esc(t)}">#${esc(t)}</span>`).join("") + `</div>`
    : `<div style="padding:2px 10px;font-size:11.5px;color:var(--text-faint)">No tags yet</div>`;
  h += `<div class="sb-section-label">More</div>`;
  h += sbItem("trash", v, "🗑", "Trash", state.notes.filter(n => n.trashed).length);
  nav.innerHTML = h;

  $$(".sb-item[data-view]", nav).forEach(el => {
    el.addEventListener("click", e => {
      if (e.target.closest("[data-twisty],[data-nb-menu]")) return;
      selectView(el.dataset.view);
    });
  });
  $$("[data-twisty]", nav).forEach(el => el.addEventListener("click", () => {
    const nb = getNotebook(el.dataset.twisty); nb.collapsed = !nb.collapsed; saveState(); renderSidebar();
  }));
  $$("[data-open-note]", nav).forEach(el => el.addEventListener("click", () => openNote(el.dataset.openNote)));
  $$("[data-tag-filter]", nav).forEach(el => el.addEventListener("click", () => {
    state.ui.tagFilter = state.ui.tagFilter === el.dataset.tagFilter ? null : el.dataset.tagFilter;
    renderSidebar(); renderNoteList();
  }));
  $("#btn-new-nb").addEventListener("click", () => promptModal("New Notebook", "", name => {
    if (name.trim()) { state.notebooks.push({ id: uid(), name: name.trim(), collapsed: false }); saveState(); renderAll(); }
  }));
  $$("[data-nb-menu]", nav).forEach(el => el.addEventListener("click", e => {
    e.stopPropagation();
    showMenu(e.clientX, e.clientY, [
      { label: "Rename", icon: "✏️", fn: () => renameNotebook(el.dataset.nbMenu) },
      { label: "Delete notebook", icon: "🗑", danger: true, fn: () => deleteNotebook(el.dataset.nbMenu) },
    ]);
  }));

  setupSidebarDnD(nav);
}
function sbItem(view, curView, glyph, name, count) {
  return `<div class="sb-item ${curView === view ? "selected" : ""}" data-view="${view}">
    <span class="glyph">${glyph}</span><span class="name">${name}</span><span class="count">${count}</span></div>`;
}

function renameNotebook(id) {
  const nb = getNotebook(id);
  promptModal("Rename Notebook", nb.name, name => {
    if (name.trim()) { nb.name = name.trim(); saveState(); renderAll(); }
  });
}
function deleteNotebook(id) {
  const count = state.notes.filter(n => n.notebookId === id && !n.trashed).length;
  confirmModal(`Delete "${getNotebook(id).name}"?`, `${count} note(s) will move to Trash.`,
    () => {
      state.notes.forEach(n => { if (n.notebookId === id) { n.trashed = true; n.trashedFrom = n.notebookId; } });
      state.notebooks = state.notebooks.filter(n => n.id !== id);
      if (state.ui.view === id) state.ui.view = "all";
      saveState(); renderAll();
    });
}

/* ============================================================
   SIDEBAR DRAG-AND-DROP (move notes onto notebooks)
   ============================================================ */
let dragNoteIds = null;
function setupSidebarDnD(root) {
  root.addEventListener("dragover", e => {
    const target = e.target.closest('[data-droppable]');
    $$(".drop-target", root).forEach(x => x.classList.remove("drop-target"));
    if (target && dragNoteIds) { e.preventDefault(); target.classList.add("drop-target"); }
  });
  root.addEventListener("dragleave", e => {
    const t = e.target.closest('[data-droppable]'); if (t) t.classList.remove("drop-target");
  });
  root.addEventListener("drop", e => {
    const target = e.target.closest('[data-droppable]');
    $$(".drop-target", root).forEach(x => x.classList.remove("drop-target"));
    if (!target || !dragNoteIds) return;
    e.preventDefault();
    moveNotes(dragNoteIds, target.dataset.view);
    dragNoteIds = null;
  });
}
function moveNotes(ids, nbId) {
  ids.forEach(id => { const n = getNote(id); if (n) { n.notebookId = nbId; n.updatedAt = Date.now(); } });
  saveState(); renderAll();
}

/* ============================================================
   RENDERING — NOTE LIST
   ============================================================ */
function highlightSnippet(text, q) {
  let snip = text.slice(0, CFG.SNIPPET_LEN * 2);
  if (q && CFG.SEARCH_HL) {
    const i = snip.toLowerCase().indexOf(q);
    if (i >= 0) {
      const start = Math.max(0, i - 30);
      snip = (start > 0 ? "…" : "") + snip.slice(start, start + CFG.SNIPPET_LEN + 30);
      const j = snip.toLowerCase().indexOf(q);
      snip = esc(snip.slice(0, j)) + "<mark class='hl'>" + esc(snip.slice(j, j + q.length)) + "</mark>" + esc(snip.slice(j + q.length));
      return snip;
    }
  }
  return esc(text.slice(0, CFG.SNIPPET_LEN));
}

function renderNoteList() {
  const v = state.ui.view;
  $("#nl-title").textContent =
    v === "all" ? "All Notes" : v === "trash" ? "Trash" : getNotebook(v)?.name || "Notes";
  const q = state.ui.search.trim().toLowerCase();

  $$("#sort-controls .sort-btn").forEach(b => {
    b.classList.toggle("active", b.dataset.sort === state.ui.sortMode);
    const labels = { updated: "Modified", created: "Created", alpha: "A–Z" };
    b.textContent = labels[b.dataset.sort] + (b.dataset.sort === state.ui.sortMode ? " ↓" : "");
  });

  const listEl = $("#note-list");
  const notes = sortedNotes(visibleNotes());
  $("#nl-count").textContent = notes.length + " note" + (notes.length === 1 ? "" : "s");

  if (!notes.length) {
    listEl.innerHTML = `<div class="empty-pane">${
      v === "trash" ? "Trash is empty" :
      q ? "No notes match your search." :
      state.ui.tagFilter ? `No notes tagged #${esc(state.ui.tagFilter)}.` :
      "No notes yet.<br>Press + to create one."}</div>`;
    return;
  }

  let h = "";
  if (v !== "trash") {
    const pinned = notes.filter(n => n.pinned), others = notes.filter(n => !n.pinned);
    if (pinned.length) h += `<div class="section-head">📌 Pinned</div>`;
    h += pinned.map(n => noteCard(n, q)).join("");
    if (others.length) {
      h += (pinned.length ? `<div class="section-head">Notes</div>` : "") + others.map(n => noteCard(n, q)).join("");
    }
  } else {
    h = notes.map(n => noteCard(n, q)).join("");
  }
  listEl.innerHTML = h;

  $$(".note-card", listEl).forEach(card => {
    card.addEventListener("click", () => openNote(card.dataset.id));
    card.addEventListener("contextmenu", e => { e.preventDefault(); noteContextMenu(e, card.dataset.id); });
    card.addEventListener("dragstart", e => {
      const sel = getSelectedIds(card.dataset.id);
      dragNoteIds = sel;
      e.dataTransfer.setData("text/plain", sel.join(","));
      e.dataTransfer.effectAllowed = "move";
      card.classList.add("dragging");
    });
    card.addEventListener("dragend", () => card.classList.remove("dragging"));
  });
}
function noteCard(n, q) {
  const snippet = highlightSnippet(extractText(n.html), q);
  const titleMatch = q && n.title.toLowerCase().includes(q) && CFG.SEARCH_HL;
  const titleHtml = titleMatch
    ? esc(n.title).replace(new RegExp("(" + q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "ig"), "<mark class='hl'>$1</mark>")
    : esc(n.title || "Untitled");
  return `<div class="note-card ${state.ui.selectedNoteId === n.id ? "selected" : ""}" data-id="${n.id}" draggable="true">
    <div class="nc-title-row">
      ${n.pinned ? '<span class="pin-glyph">📌</span>' : ""}
      <span class="nc-title">${titleHtml}</span>
    </div>
    <div class="nc-snippet">${snippet || "<i style='color:var(--text-faint)'>Empty note</i>"}</div>
    <div class="nc-meta">
      <span>${relTime(n.updatedAt)}</span>
      <span class="nc-tags">${n.tags.map(t => `<span class="nc-tag">#${esc(t)}</span>`).join("")}</span>
    </div>
  </div>`;
}
function getSelectedIds(clickedId) {
  if (multiSelection.has(clickedId)) return [...multiSelection];
  return [clickedId];
}
const multiSelection = new Set();

function noteContextMenu(e, id) {
  const n = getNote(id);
  if (n.trashed) {
    showMenu(e.clientX, e.clientY, [
      { label: "Restore", icon: "↩️", fn: () => restoreFromTrash([id]) },
      { label: "Delete permanently", icon: "🗑", danger: true, fn: () => purgeNotes([id]) },
    ]);
    return;
  }
  const nbItems = state.notebooks.map(nb => ({
    label: "Move to " + nb.name, icon: "📓", fn: () => moveNotes([id], nb.id),
  }));
  showMenu(e.clientX, e.clientY, [
    { label: n.pinned ? "Unpin" : "Pin to top", icon: "📌", fn: () => { n.pinned = !n.pinned; saveState(); renderAll(); } },
    { label: "Duplicate", icon: "⧉", fn: () => duplicateNote(id) },
    ...nbItems,
    { sep: true },
    { label: "Move to Trash", icon: "🗑", danger: true, fn: () => trashNotes([id]) },
  ]);
}
/* ============================================================
   EDITOR — RENDER & MODEL SYNC
   ============================================================ */
let currentNote = null;
let undoStack = [], redoStack = [];
let suppressHistory = false;

function renderEditorPane() {
  const pane = $("#editor-pane");
  const n = currentNote ? getNote(currentNote) : null;
  if (!n || n.trashed && state.ui.view !== "trash") {
    pane.innerHTML = `<div class="editor-empty">
      <div class="big-glyph">🖋</div>
      <h2>Select or create a note</h2>
      <button class="btn-primary" id="btn-empty-new">+ New note</button>
    </div>`;
    $("#btn-empty-new").addEventListener("click", createNote);
    return;
  }
  pane.innerHTML = `
    <div class="ed-toolbar" id="ed-toolbar">
      <button class="tb-btn" data-cmd="bold" title="Bold (Ctrl+B)"><b>B</b></button>
      <button class="tb-btn" data-cmd="italic" title="Italic (Ctrl+I)"><span class="serif-i">I</span></button>
      <button class="tb-btn" data-cmd="underline" title="Underline (Ctrl+U)"><u>U</u></button>
      <button class="tb-btn" data-cmd="strikeThrough" title="Strikethrough"><s>S</s></button>
      <span class="tb-sep"></span>
      <button class="tb-btn" data-block="code" title="Inline code" style="font-family:var(--font-mono);font-size:12px">&lt;/&gt;</button>
      <span class="tb-sep"></span>
      <button class="tb-btn" data-block="h1" title="Heading 1">H1</button>
      <button class="tb-btn" data-block="h2" title="Heading 2">H2</button>
      <button class="tb-btn" data-block="h3" title="Heading 3">H3</button>
      <button class="tb-btn" data-block="p" title="Body text">¶</button>
      <span class="tb-sep"></span>
      <button class="tb-btn" data-list="ul" title="Bulleted list">• ≡</button>
      <button class="tb-btn" data-list="ol" title="Numbered list">1. ≡</button>
      <button class="tb-btn" data-list="checklist" title="Checklist">☑</button>
      <span class="tb-sep"></span>
      <button class="tb-btn" data-block="blockquote" title="Blockquote">❝</button>
      <button class="tb-btn" data-hr="1" title="Horizontal rule">—</button>
      <span style="flex:1"></span>
      <button class="icon-btn" id="btn-ed-menu" title="More actions">⋯</button>
    </div>
    <div class="ed-scroll">
      <div class="ed-inner">
        ${n.trashed ? `<div class="trashed-banner">🗑 This note is in the Trash.
          <button id="btn-restore-here">Restore</button><span style="flex:1"></span>
          <button class="del" id="btn-purge-here">Delete forever</button></div>` : ""}
        <div class="ed-meta">
          <span class="ed-time">${n.trashed ? "Deleted · " : "Edited "}${relTime(n.updatedAt)}</span>
          <span class="saved-ind" id="saved-ind"><span class="saved-dot"></span> Saved</span>
          ${state.ui.view !== "trash" ? `<span style="flex:1"></span>
            <button class="icon-btn" id="btn-pin" title="${n.pinned ? 'Unpin' : 'Pin'}">${n.pinned ? "📌" : "📍"}</button>` : ""}
        </div>
        <input id="title-input" type="text" placeholder="Untitled" value="${esc(n.title)}">
        <div class="tags-row" id="tags-row">
          ${n.tags.map(t => `<span class="ed-tag">#${esc(t)}<button data-rm-tag="${esc(t)}">×</button></span>`).join("")}
          ${!n.trashed ? `<input id="tag-input" type="text" placeholder="+ add tag">` : ""}
        </div>
        <div id="editor" contenteditable="true" spellcheck="true"
             ${n.html.trim() ? "" : 'data-placeholder="Start writing…"'}>${n.html}</div>
      </div>
    </div>`;

  bindEditorEvents();
}

function openNote(id) {
  flushPendingSave();
  currentNote = id;
  state.ui.selectedNoteId = id;
  multiSelection.clear();
  undoStack = []; redoStack = [];
  saveState();
  renderSidebar(); renderNoteList(); renderEditorPane();
}
function selectView(v) {
  flushPendingSave();
  state.ui.view = v;
  state.ui.tagFilter = null;
  if (v === "trash") { /* keep selection */ }
  saveState();
  renderSidebar(); renderNoteList(); renderEditorPane();
}

/* ---------- autosave ---------- */
let saveTimer = null, pendingHtml = null, pendingTitle = null;
function scheduleSave() {
  $("#saved-ind")?.classList.remove("show");
  clearTimeout(saveTimer);
  saveTimer = setTimeout(commitSave, CFG.AUTOSAVE_DEBOUNCE);
}
function commitSave() {
  const n = currentNote && getNote(currentNote);
  if (!n) return;
  let changed = false;
  if (pendingHtml !== null) {
    const clean = ensureChecklist(sanitizeHTML(pendingHtml));
    if (clean !== n.html) { n.html = clean; changed = true; }
    pendingHtml = null;
  }
  if (pendingTitle !== null) { n.title = pendingTitle; pendingTitle = null; changed = true; }
  if (changed) { n.updatedAt = Date.now(); saveState(); showSaved(); renderSidebar(); renderNoteList(); }
}
function flushPendingSave() { clearTimeout(saveTimer); commitSave(); }
function showSaved() {
  const el = $("#saved-ind");
  if (!el) return;
  el.classList.add("show");
  setTimeout(() => el.classList.remove("show"), 1600);
}

// make sure a <ul> containing checklist items carries .checklist
function ensureChecklist(html) {
  if (!/<li[^>]*data-checked/.test(html)) return html.replace(/<ul class="checklist">/g, "<ul>");
  return html.replace(/<ul>(?![^<]*<\/li>\s*<\/ul>\s*$)/g, "<ul>");
}

/* ============================================================
   UNDO / REDO (snapshot-based, per note)
   ============================================================ */
function pushUndo() {
  const n = currentNote && getNote(currentNote);
  if (!n) return;
  undoStack.push({ html: n.html, title: n.title });
  if (undoStack.length > CFG.UNDO_LIMIT) undoStack.shift();
  redoStack.length = 0;
}
function applySnapshot(s) {
  const n = getNote(currentNote); if (!n) return;
  n.html = s.html; n.title = s.title; n.updatedAt = Date.now();
  saveState();
  renderEditorPane(); renderNoteList(); renderSidebar();
}
function doUndo() {
  if (!undoStack.length) { document.execCommand("undo"); return; }
  const n = getNote(currentNote); if (!n) return;
  redoStack.push({ html: n.html, title: n.title });
  applySnapshot(undoStack.pop());
}
function doRedo() {
  if (!redoStack.length) return;
  const n = getNote(currentNote); if (!n) return;
  undoStack.push({ html: n.html, title: n.title });
  applySnapshot(redoStack.pop());
}

/* ============================================================
   EDITOR EVENTS
   ============================================================ */
function bindEditorEvents() {
  const ed = $("#editor");
  const titleInput = $("#title-input");

  // --- title ---
  titleInput.addEventListener("input", () => {
    pendingTitle = titleInput.value;
    scheduleSave();
  });
  titleInput.addEventListener("keydown", e => {
    if (e.key === "Enter") { e.preventDefault(); ed.focus(); }
  });

  // --- body input: autosave + markdown shortcuts + history push ---
  ed.addEventListener("input", () => {
    ed.removeAttribute("data-placeholder");
    // debounce model write; capture pre-change snapshot for undo once per burst
    if (!pendingHtml) pushUndo();
    pendingHtml = ed.innerHTML;
    handleMarkdownShortcut(ed);
    scheduleSave();
  });

  // --- keyboard ---
  ed.addEventListener("keydown", e => {
    const mod = e.ctrlKey || e.metaKey;
    if (mod && !e.shiftKey && e.key.toLowerCase() === "z") { e.preventDefault(); doUndo(); return; }
    if ((mod && e.shiftKey && e.key.toLowerCase() === "z") || (mod && e.key.toLowerCase() === "y")) { e.preventDefault(); doRedo(); return; }
    if (mod && e.key.toLowerCase() === "b") { e.preventDefault(); exec("bold"); updateToolbarState(); return; }
    if (mod && e.key.toLowerCase() === "i") { e.preventDefault(); exec("italic"); updateToolbarState(); return; }
    if (mod && e.key.toLowerCase() === "u") { e.preventDefault(); exec("underline"); updateToolbarState(); return; }
    if (e.key === "Enter") handleEnter(e, ed);
    if (e.key === "Tab") {
      e.preventDefault();
      document.execCommand(e.shiftKey ? "outdent" : "indent");
    }
  });

  // --- toolbar state reflection ---
  ["keyup", "mouseup", "focus", "selectionchange-proxy"].forEach(ev =>
    ed.addEventListener(ev, updateToolbarState));
  document.addEventListener("selectionchange", () => {
    const sel = window.getSelection();
    if (sel.anchorNode && ed.contains(sel.anchorNode)) updateToolbarState();
  });

  // --- toolbar clicks ---
  $("#ed-toolbar").addEventListener("mousedown", e => e.preventDefault()); // keep selection
  $("#ed-toolbar").addEventListener("click", e => {
    const btn = e.target.closest(".tb-btn"); if (!btn) return;
    ed.focus({ preventScroll: true });
    restoreRange();
    if (btn.dataset.cmd) exec(btn.dataset.cmd);
    else if (btn.dataset.block) applyBlock(btn.dataset.block);
    else if (btn.dataset.list) applyList(btn.dataset.list);
    else if (btn.dataset.hr) insertHR();
    pushUndoFromDOM();
    updateToolbarState();
    scheduleSave();
  });

  // --- checkbox clicks ---
  ed.addEventListener("click", e => {
    const li = e.target.closest("ul.checklist li");
    if (!li) return;
    const r = document.createRange(), rect = li.getBoundingClientRect();
    r.selectNodeContents(li);
    const boxX = rect.left + 10;
    if (e.clientX <= boxX) {
      li.dataset.checked = li.dataset.checked === "true" ? "false" : "true";
      pushUndoFromDOM();
      pendingHtml = ed.innerHTML; scheduleSave();
    }
  });

  // --- sanitized paste ---
  ed.addEventListener("paste", e => {
    e.preventDefault();
    let html = e.clipboardData.getData("text/html");
    if (!html) {
      const text = e.clipboardData.getData("text/plain");
      // simple markdown-ish plain text paste: split lines into paragraphs
      html = text.split(/\n{2,}/).map(p =>
        "<p>" + esc(p).replace(/\n/g, "<br>") + "</p>").join("");
    }
    const clean = sanitizeHTML(html);
    document.execCommand("insertHTML", false, clean || esc(e.clipboardData.getData("text/plain")));
    pushUndoFromDOM();
    pendingHtml = ed.innerHTML;
    scheduleSave();
  });

  // --- tags ---
  $$("#tags-row [data-rm-tag]").forEach(b => b.addEventListener("click", () => {
    const n = getNote(currentNote);
    n.tags = n.tags.filter(t => t !== b.dataset.rmTag);
    saveState(); renderAll();
  }));
  const tagIn = $("#tag-input");
  tagIn?.addEventListener("keydown", e => {
    if (e.key === "Enter" || e.key === ",") {
      e.preventDefault();
      addTag(tagIn.value);
    } else if (e.key === "Backspace" && !tagIn.value) {
      const n = getNote(currentNote);
      if (n.tags.length) { n.tags.pop(); saveState(); renderAll(); }
    }
  });
  tagIn?.addEventListener("blur", () => tagIn.value && addTag(tagIn.value));

  // --- header buttons ---
  $("#btn-pin")?.addEventListener("click", () => {
    const n = getNote(currentNote); n.pinned = !n.pinned; saveState(); renderAll();
  });
  $("#btn-restore-here")?.addEventListener("click", () => restoreFromTrash([currentNote]));
  $("#btn-purge-here")?.addEventListener("click", () => purgeNotes([currentNote]));
  $("#btn-ed-menu").addEventListener("click", e => {
    const n = getNote(currentNote);
    if (n.trashed) {
      showMenu(e.clientX, e.clientY, [
        { label: "Restore", icon: "↩️", fn: () => restoreFromTrash([currentNote]) },
        { label: "Delete permanently", icon: "🗑", danger: true, fn: () => purgeNotes([currentNote]) },
      ]);
    } else {
      showMenu(e.clientX, e.clientY, [
        { label: n.pinned ? "Unpin" : "Pin to top", icon: "📌", fn: () => { n.pinned = !n.pinned; saveState(); renderAll(); } },
        { label: "Duplicate", icon: "⧉", fn: () => duplicateNote(currentNote) },
        ...state.notebooks.map(nb => ({ label: "Move to " + nb.name, icon: "📓", fn: () => moveNotes([currentNote], nb.id) })),
        { sep: true },
        { label: "Move to Trash", icon: "🗑", danger: true, fn: () => trashNotes([currentNote]) },
      ]);
    }
  });
}

function addTag(raw) {
  const t = raw.trim().replace(/^#/, "").toLowerCase();
  const tagIn = $("#tag-input");
  if (!t) { if (tagIn) tagIn.value = ""; return; }
  const n = getNote(currentNote);
  if (!n.tags.includes(t)) n.tags.push(t);
  saveState(); renderAll();
}

/* ---------- selection helpers for toolbar ---------- */
let savedRange = null;
document.addEventListener("selectionchange", () => {
  const sel = window.getSelection();
  if (sel.rangeCount && !sel.isCollapsed) savedRange = sel.getRangeAt(0).cloneRange();
});
function restoreRange() {
  const sel = window.getSelection();
  if (savedRange) { sel.removeAllRanges(); sel.addRange(savedRange); }
}

function exec(cmd, val) {
  document.execCommand(cmd, false, val);
  pendingHtml = $("#editor")?.innerHTML ?? pendingHtml;
}
function pushUndoFromDOM() {
  // called after DOM mutation via execCommand: record previous model state already pushed on input;
  // here we just mark dirty
  pendingHtml = $("#editor")?.innerHTML ?? null;
}

function currentBlock(el) {
  const blocks = "H1,H2,H3,P,DIV,BLOCKQUOTE,LI,PRE";
  let node = el;
  while (node && node.id !== "editor") {
    if (node.matches?.(blocks)) return node;
    node = node.parentNode;
  }
  return null;
}

function updateToolbarState() {
  const ed = $("#editor"); if (!ed) return;
  const sel = window.getSelection();
  const inEd = sel.anchorNode && ed.contains(sel.anchorNode);
  $$("[data-cmd]", ed.closest("#editor-pane")).forEach(b =>
    b.classList.toggle("active", inEd && document.queryCommandState(b.dataset.cmd)));
  if (!inEd) return;
  const block = currentBlock(sel.anchorNode);
  const blockType = block ? block.tagName.toLowerCase()
    : block?.closest?.("blockquote") ? "blockquote" : "";
  const inQuote = block && !!block.closest("blockquote");
  $$("[data-block]", ed.closest("#editor-pane")).forEach(b => {
    let active = false;
    if (b.dataset.block === "blockquote") active = inQuote;
    else if (block) active = blockType === b.dataset.block ||
      (b.dataset.block === "p" && ["p","div"].includes(blockType));
    b.classList.toggle("active", active);
  });
  $$("[data-list]", ed.closest("#editor-pane")).forEach(b => {
    const li = block && block.tagName === "LI" ? block : null;
    let active = false;
    if (li) {
      const parentList = li.parentElement;
      if (b.dataset.list === "ul") active = parentList.tagName === "UL" && !parentList.classList.contains("checklist");
      if (b.dataset.list === "ol") active = parentList.tagName === "OL";
      if (b.dataset.list === "checklist") active = parentList.classList.contains("checklist");
    }
    b.classList.toggle("active", active);
  });
}

/* ============================================================
   BLOCK FORMATTING
   ============================================================ */
function applyBlock(type) {
  if (type === "code") {
    const sel = window.getSelection();
    if (!sel.isCollapsed) document.execCommand("insertHTML", false,
      "<code>" + sel.toString().replace(/</g, "&lt;") + "</code>");
    else insertHTMLAtCaret("<code>&nbsp;</code>");
    return;
  }
  if (type === "blockquote") {
    const b = currentBlock(window.getSelection().anchorNode);
    if (b && b.closest("blockquote")) unwrapBlockquote(b);
    else document.execCommand("formatBlock", false, "blockquote");
    return;
  }
  document.execCommand("formatBlock", false, type === "p" ? "p" : type);
}
function unwrapBlockquote(b) {
  const quote = b.closest("blockquote");
  while (quote.firstChild) quote.parentNode.insertBefore(quote.firstChild, quote);
  quote.remove();
}
function insertHR() {
  insertHTMLAtCaret("<hr>");
}
function insertHTMLAtCaret(html) {
  document.execCommand("insertHTML", false, html);
}

function applyList(kind) {
  const sel = window.getSelection();
  const block = currentBlock(sel.anchorNode);
  if (kind === "checklist") {
    let li = block && (block.tagName === "LI" ? block : null);
    if (!li) { document.execCommand("insertUnorderedList"); li = currentBlock(sel.anchorNode); }
    const list = li?.parentElement;
    if (list) {
      if (list.classList.contains("checklist")) {
        list.classList.remove("checklist");
        $$("li", list).forEach(l => l.removeAttribute("data-checked"));
      } else {
        list.classList.add("checklist");
        $$("li", list).forEach(l => l.setAttribute("data-checked", "false"));
      }
    }
    return;
  }
  document.execCommand(kind === "ol" ? "insertOrderedList" : "insertUnorderedList");
}

/* ============================================================
   ENTER BEHAVIOR — lists continue / exit on empty item
   ============================================================ */
function handleEnter(e, ed) {
  const sel = window.getSelection();
  const block = currentBlock(sel.anchorNode);
  if (!block || block.tagName !== "LI") return;
  const li = block;
  const isEmpty = !li.textContent.trim();

  if (isEmpty) {
    e.preventDefault();
    const list = li.parentElement;
    const isLast = !li.nextElementSibling;
    if (isLast) {
      li.remove();
      if (!list.children.length) list.remove();
      // insert paragraph after list
      const p = document.createElement("p");
      p.innerHTML = "<br>";
      list.parentNode.insertBefore(p, list.nextSibling);
      placeCaret(p, 0);
    } else {
      // split: remove this empty li, put caret in new para after it
      const p = document.createElement("p"); p.innerHTML = "<br>";
      li.parentNode.insertBefore(p, li.nextSibling);
      li.remove();
      placeCaret(p, 0);
    }
    return;
  }

  // non-empty last checklist/bullet item: browser continues list fine,
  // but ensure checklist items carry data-checked="false"
  setTimeout(() => {
    const s2 = window.getSelection();
    const b2 = currentBlock(s2.anchorNode);
    if (b2 && b2.tagName === "LI" && b2.parentElement.classList.contains("checklist")
        && !b2.hasAttribute("data-checked")) {
      b2.setAttribute("data-checked", "false");
    }
  }, 0);
}
function placeCaret(el, offset) {
  const r = document.createRange();
  r.setStart(el, Math.min(offset, el.childNodes.length));
  r.collapse(true);
  const s = window.getSelection(); s.removeAllRanges(); s.addRange(r);
}

/* ============================================================
   MARKDOWN INPUT SHORTCUTS
   ============================================================ */
const MD_RULES = [
  { re: /^#$/, block: "h1" },
  { re: /^##$/, block: "h2" },
  { re: /^###$/, block: "h3" },
  { re: /^[-*]$/, list: "ul" },
  { re: /^1[.)]$/, list: "ol" },
  { re: /^\[\]\(?$/, checklist: true },
  { re: /^>$/, block: "blockquote" },
];
function handleMarkdownShortcut(ed) {
  const sel = window.getSelection();
  if (!sel.isCollapsed || !sel.anchorNode) return;
  const node = sel.anchorNode;
  if (node.nodeType !== Node.TEXT_NODE) return;
  const block = currentBlock(node);
  if (!block || block.tagName === "LI") return;   // only at line start of plain blocks

  const textBefore = node.textContent.slice(0, sel.anchorOffset);
  const m = textBefore.match(/^(#{1,3}|[-*]|1[.)]|\[\]\(?)$/);
  if (!m) return;
  // must be at line start: no earlier text in this block
  if (getLineTextBefore(block, node, sel.anchorOffset).replace(m[0], "") !== "") return;

  const rule = MD_RULES.find(r => r.re.test(textBefore));
  if (!rule) return;

  // remove the marker characters
  node.textContent = node.textContent.slice(sel.anchorOffset);
  if (rule.block) {
    document.execCommand("formatBlock", false, rule.block);
  } else if (rule.checklist) {
    document.execCommand("insertUnorderedList");
    const li = currentBlock(window.getSelection().anchorNode);
    if (li) {
      const list = li.parentElement;
      list.classList.add("checklist");
      $$("li", list).forEach(l => l.setAttribute("data-checked", "false"));
    }
  } else if (rule.list) {
    document.execCommand(rule.list === "ol" ? "insertOrderedList" : "insertUnorderedList");
  }
}
function getLineTextBefore(block, node, offset) {
  let text = "";
  const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
  let cur;
  while ((cur = walker.nextNode())) {
    if (cur === node) { text += cur.textContent.slice(0, offset); break; }
    text += cur.textContent;
  }
  return text;
}
/* ============================================================
   NOTE CRUD / TRASH
   ============================================================ */
function createNote() {
  flushPendingSave();
  let nbId = state.ui.view;
  if (nbId === "all" || nbId === "trash" || !getNotebook(nbId)) {
    nbId = state.notebooks[0]?.id;
    if (!nbId) { state.notebooks.push({ id: uid(), name: "Notebook", collapsed: false }); nbId = state.notebooks[0].id; }
  }
  const t = Date.now();
  const n = {
    id: uid(), notebookId: nbId, title: "",
    html: "<p><br></p>", tags: [], pinned: false,
    createdAt: t, updatedAt: t, trashed: false,
  };
  state.notes.unshift(n);
  if (state.ui.view === "trash") state.ui.view = "all";
  saveState();
  openNote(n.id);
  setTimeout(() => $("#title-input")?.focus(), 30);
}
function duplicateNote(id) {
  const n = getNote(id); if (!n) return;
  const copy = { ...n, id: uid(), title: n.title + " (copy)", pinned: false, createdAt: Date.now(), updatedAt: Date.now(), tags: [...n.tags] };
  state.notes.splice(state.notes.indexOf(n) + 1, 0, copy);
  saveState(); renderAll();
}
function trashNotes(ids) {
  ids.forEach(id => {
    const n = getNote(id);
    if (n) { n.trashed = true; n.trashedFrom = n.notebookId; n.trashedAt = Date.now(); }
  });
  if (ids.includes(state.ui.selectedNoteId)) { currentNote = null; state.ui.selectedNoteId = null; }
  saveState(); renderAll();
}
function restoreFromTrash(ids) {
  ids.forEach(id => {
    const n = getNote(id);
    if (n) {
      n.trashed = false;
      n.notebookId = (n.trashedFrom && getNotebook(n.trashedFrom)) ? n.trashedFrom : (state.notebooks[0]?.id);
      delete n.trashedAt;
    }
  });
  saveState(); renderAll();
}
function purgeNotes(ids) {
  state.notes = state.notes.filter(n => !ids.includes(n.id));
  if (ids.includes(state.ui.selectedNoteId)) { currentNote = null; state.ui.selectedNoteId = null; }
  saveState(); renderAll();
}

/* ============================================================
   MENUS & MODALS
   ============================================================ */
let openMenuEl = null;
function closeMenu() { openMenuEl?.remove(); openMenuEl = null; }
function showMenu(x, y, items) {
  closeMenu();
  const m = document.createElement("div");
  m.className = "ctx-menu";
  m.innerHTML = items.map(it => it.sep ? "<hr>" :
    `<button class="${it.danger ? "danger" : ""}" data-mi="${esc(it.label)}">${it.icon || ""}<span>${esc(it.label)}</span></button>`).join("");
  document.body.appendChild(m);
  const r = m.getBoundingClientRect();
  m.style.left = Math.min(x, innerWidth - r.width - 8) + "px";
  m.style.top = Math.min(y, innerHeight - r.height - 8) + "px";
  openMenuEl = m;
  items.forEach(it => {
    if (it.sep) return;
    m.querySelector(`[data-mi="${CSS.escape(it.label)}"]`).addEventListener("click", () => { closeMenu(); it.fn(); });
  });
}
document.addEventListener("click", e => { if (openMenuEl && !openMenuEl.contains(e.target)) closeMenu(); });
document.addEventListener("keydown", e => { if (e.key === "Escape") closeMenu(); });

function promptModal(title, value, onOk) {
  const bd = document.createElement("div");
  bd.className = "modal-backdrop";
  bd.innerHTML = `<div class="modal"><h3>${esc(title)}</h3>
    <input type="text" value="${esc(value)}">
    <div class="modal-actions"><button class="btn-ghost" data-cancel>Cancel</button>
    <button class="btn-primary" data-ok>Save</button></div></div>`;
  document.body.appendChild(bd);
  const inp = $("input", bd);
  inp.focus(); inp.select();
  const done = ok => {
    bd.remove();
    if (ok) onOk(inp.value);
  };
  $("[data-ok]", bd).addEventListener("click", () => done(true));
  $("[data-cancel]", bd).addEventListener("click", () => done(false));
  bd.addEventListener("keydown", e => {
    if (e.key === "Enter") done(true);
    if (e.key === "Escape") done(false);
  });
  bd.addEventListener("click", e => { if (e.target === bd) done(false); });
}
function confirmModal(title, msg, onOk) {
  const bd = document.createElement("div");
  bd.className = "modal-backdrop";
  bd.innerHTML = `<div class="modal"><h3>${esc(title)}</h3>
    <p style="color:var(--text-dim);font-size:13px">${esc(msg)}</p>
    <div class="modal-actions"><button class="btn-ghost" data-cancel>Cancel</button>
    <button class="btn-primary" data-ok style="background:var(--danger)">Delete</button></div></div>`;
  document.body.appendChild(bd);
  $("[data-ok]", bd).addEventListener("click", () => { bd.remove(); onOk(); });
  $("[data-cancel]", bd).addEventListener("click", () => bd.remove());
  bd.addEventListener("click", e => { if (e.target === bd) bd.remove(); });
}

/* ============================================================
   TOP-LEVEL UI EVENTS
   ============================================================ */
function bindChrome() {
  // theme
  const applyTheme = () => document.documentElement.dataset.theme = state.ui.theme;
  applyTheme();
  $("#btn-theme").addEventListener("click", () => {
    state.ui.theme = state.ui.theme === "dark" ? "light" : "dark";
    applyTheme(); saveState();
  });

  // sidebar collapse
  const sb = $("#sidebar");
  sb.classList.toggle("collapsed", state.ui.sidebarCollapsed);
  $("#btn-collapse-sb").addEventListener("click", () => {
    state.ui.sidebarCollapsed = !state.ui.sidebarCollapsed;
    sb.classList.toggle("collapsed", state.ui.sidebarCollapsed);
    saveState();
  });
  document.addEventListener("keydown", e => {
    if ((e.ctrlKey || e.metaKey) && e.key === "\\") {
      e.preventDefault();
      $("#btn-collapse-sb").click();
    }
  });

  // new note
  $("#btn-new-note").addEventListener("click", createNote);
  $("#btn-nl-menu").addEventListener("click", e => {
    const v = state.ui.view;
    const items = [];
    if (v !== "all" && v !== "trash") {
      items.push({ label: "Rename notebook", icon: "✏️", fn: () => renameNotebook(v) });
      items.push({ label: "Delete notebook", icon: "🗑", danger: true, fn: () => deleteNotebook(v) });
      items.push({ sep: true });
    }
    items.push({ label: "New note", icon: "📝", fn: createNote });
    items.push({ label: "New notebook", icon: "📓", fn: () => $("#btn-new-nb").click() });
    showMenu(e.clientX, e.clientY, items);
  });

  // search
  const searchInput = $("#search-input");
  searchInput.value = state.ui.search || "";
  let searchTimer;
  searchInput.addEventListener("input", () => {
    clearTimeout(searchTimer);
    searchTimer = setTimeout(() => {
      state.ui.search = searchInput.value;
      renderNoteList();
    }, 120);
  });
  searchInput.addEventListener("keydown", e => { if (e.key === "Escape") { searchInput.value = ""; state.ui.search = ""; renderNoteList(); } });

  // sort
  $("#sort-controls").addEventListener("click", e => {
    const b = e.target.closest(".sort-btn"); if (!b) return;
    state.ui.sortMode = b.dataset.sort;
    saveState(); renderNoteList();
  });

  // pane resizing
  setupDivider($("#divider-sb"), w => {
    state.ui.sbWidth = Math.min(420, Math.max(170, w));
    document.documentElement.style.setProperty("--sidebar-w", state.ui.sbWidth + "px");
  });
  setupDivider($("#divider-nl"), w => {
    state.ui.nlWidth = Math.min(480, Math.max(200, w));
    document.documentElement.style.setProperty("--list-w", state.ui.nlWidth + "px");
  });
  document.documentElement.style.setProperty("--sidebar-w", state.ui.sbWidth + "px");
  document.documentElement.style.setProperty("--list-w", state.ui.nlWidth + "px");

  // global shortcuts
  document.addEventListener("keydown", e => {
    const mod = e.ctrlKey || e.metaKey;
    if (mod && e.shiftKey && e.key.toLowerCase() === "n") { e.preventDefault(); createNote(); }
    if (mod && e.key.toLowerCase() === "f") { e.preventDefault(); searchInput.focus(); searchInput.select(); }
  });
  window.addEventListener("beforeunload", flushPendingSave);
}

function setupDivider(handle, onDrag) {
  handle.addEventListener("mousedown", e => {
    e.preventDefault();
    handle.classList.add("dragging");
    document.body.classList.add("resizing");
    const startX = e.clientX;
    const pane = handle.previousElementSibling;
    const startW = pane.getBoundingClientRect().width;
    const move = ev => onDrag(startW + (ev.clientX - startX));
    const up = () => {
      handle.classList.remove("dragging");
      document.body.classList.remove("resizing");
      document.removeEventListener("mousemove", move);
      document.removeEventListener("mouseup", up);
      saveState();
    };
    document.addEventListener("mousemove", move);
    document.addEventListener("mouseup", up);
  });
}

/* ============================================================
   RENDER ALL & INIT
   ============================================================ */
function renderAll() {
  renderSidebar();
  renderNoteList();
  renderEditorPane();
}

function init() {
  loadState();
  bindChrome();
  renderAll();
  // restore selection
  if (state.ui.selectedNoteId && getNote(state.ui.selectedNoteId) && !getNote(state.ui.selectedNoteId).trashed) {
    openNote(state.ui.selectedNoteId);
  } else {
    state.ui.selectedNoteId = null;
    renderEditorPane();
  }
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"75bb24ad-8a32-4c13-a776-aaac884a0d24","tokensIn":1048313,"tokensOut":25466,"tokensTotal":1073779,"cost":0,"turns":27,"toolCalls":26,"failedToolCalls":1,"timestamp":"2026-08-21T21:42:11.727Z"} -->