← Notes results

Notes

Qwen 3.8 · typescript

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 117,160 bytes · SHA-256 8919a83d42cb
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notes — a calm place to write</title>
<style>
/* ── css: base ── */
/* ============ base: reset, variables, app frame ============ */
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
body {
  font-family: var(--font-ui);
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}
:root {
  --side-w: 264px;
  --list-w: 320px;
}
#app {
  display: grid;
  grid-template-columns: var(--side-w) 6px var(--list-w) 6px 1fr;
  height: 100vh;
  width: 100vw;
  transition: grid-template-columns 0.28s cubic-bezier(0.4, 0, 0.2, 1);
  position: relative;
}
#app.side-collapsed { grid-template-columns: 0 0 var(--list-w) 6px 1fr; }
body.resizing { cursor: col-resize; user-select: none; }
body.resizing * { cursor: col-resize !important; }

.pane { min-width: 0; min-height: 0; overflow: hidden; }
.pane-sidebar {
  background: var(--panel);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#app.side-collapsed .pane-sidebar,
#app.side-collapsed #rs-sidebar { opacity: 0; pointer-events: none; }

.side-toggle {
  position: absolute;
  left: calc(var(--side-w) - 6px);
  top: 12px;
  z-index: 30;
  transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: 7px;
  background: var(--panel2);
  border: 1px solid var(--line);
  color: var(--text2);
  font-size: 13px;
  cursor: pointer;
}
.side-toggle:hover { color: var(--text); background: var(--hover); }
/* when collapsed the button rests at the left edge, fully visible */
#app.side-collapsed .side-toggle { left: 8px; }

.rs {
  width: 6px;
  cursor: col-resize;
  position: relative;
  z-index: 20;
}
.rs::before {
  content: '';
  position: absolute; left: 2px; top: 0; bottom: 0; width: 1px;
  background: var(--line);
  transition: background 0.15s, width 0.15s, left 0.15s;
}
.rs:hover::before { width: 3px; left: 1.5px; background: var(--accent); }
/* ── css: sidebar ── */
/* ============ sidebar ============ */
.pane-sidebar > * { overflow: hidden; }
.side-group { padding: 10px 10px 4px; }
.side-group.grow { flex: 1; overflow-y: auto; }
.side-label {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text3);
  padding: 4px 8px 6px;
}
.side-rows { display: flex; flex-direction: column; gap: 1px; }
.side-row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px;
  border-radius: 8px;
  cursor: pointer;
  color: var(--text);
  font-size: 13.5px;
  line-height: 1.35;
  transition: background 0.12s, color 0.12s;
  position: relative;
  user-select: none;
}
.side-row:hover { background: var(--hover); }
.side-row.active { background: var(--accentSoft); color: var(--accent); font-weight: 600; }
.side-row .row-name {
  flex: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.side-row .row-count {
  font-size: 11px; color: var(--text3); font-variant-numeric: tabular-nums;
}
.side-row.active .row-count { color: var(--accent); opacity: 0.7; }
.side-row.drop-hint { background: var(--accentSoft); outline: 1.5px dashed var(--accent); outline-offset: -2px; }
.side-row .ic { width: 15px; height: 15px; flex: none; color: var(--text3); }
.side-row.active .ic { color: var(--accent); }
.side-row .chev { width: 16px; height: 16px; font-size: 11px; color: var(--text3); }
.side-row .nb-menu { opacity: 0; }
.side-row:hover .nb-menu { opacity: 1; }
.ic { display: block; }
.ic path, .ic rect, .ic circle {
  fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
}
.tag-dot {
  width: 8px; height: 8px; border-radius: 3px; flex: none;
  background: var(--accent); opacity: 0.65;
}
.nb-notes { padding: 2px 0 6px 26px; display: flex; flex-direction: column; gap: 1px; }
.nb-note {
  font-size: 12.5px; color: var(--text2);
  padding: 4px 8px; border-radius: 6px;
  cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: background 0.12s, color 0.12s;
}
.nb-note:hover { background: var(--hover); color: var(--text); }
.nb-note.sel { background: var(--accentSoft); color: var(--accent); }
.nb-add-note {
  border: none; background: none; text-align: left;
  font-size: 12px; color: var(--text3);
  padding: 4px 8px; border-radius: 6px; cursor: pointer;
  font-family: var(--font-ui);
}
.nb-add-note:hover { background: var(--hover); color: var(--accent); }
.nb-empty { font-size: 12px; color: var(--text3); padding: 4px 8px; font-style: italic; }
.side-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px 12px;
  border-top: 1px solid var(--line);
}
.side-foot-note { font-size: 11px; color: var(--text3); flex: 1; }
/* ── css: note-list ── */
/* ============ note list pane ============ */
.pane-list {
  background: var(--panel);
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--line);
}
#list-head { padding: 14px 14px 10px; border-bottom: 1px solid var(--line); }
.lh-row1 { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.lh-title { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lh-actions { display: flex; gap: 2px; flex: none; }
.lh-row2 { margin-top: 2px; }
.lh-count { font-size: 11.5px; color: var(--text3); }
.list-body { flex: 1; overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.note-card {
  background: var(--editor-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px 12px 9px;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, transform 0.12s, box-shadow 0.15s;
  position: relative;
}
.note-card:hover { border-color: var(--text3); box-shadow: 0 1px 4px rgba(0,0,0,0.05); }
.note-card.sel {
  border-color: var(--accent);
  background: var(--accentSoft);
  box-shadow: 0 1px 6px rgba(0,0,0,0.06);
}
.nc-title {
  font-size: 13.5px; font-weight: 600; line-height: 1.35;
  display: flex; align-items: center; gap: 6px;
  overflow: hidden;
}
.nc-title > *:not(.pin-dot) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nc-title { white-space: nowrap; }
.nc-title .pin-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex: none; opacity: 0.9; }
.nc-snippet {
  font-size: 12.5px; color: var(--text2);
  margin-top: 3px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  line-height: 1.45;
}
.nc-meta {
  display: flex; align-items: center; gap: 6px; margin-top: 7px;
  font-size: 11px; color: var(--text3);
}
.nc-date { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nc-tag {
  font-size: 10.5px; color: var(--accent); background: var(--accentSoft);
  padding: 1px 6px; border-radius: 5px; flex: none;
}
#list-foot { padding: 8px 12px; border-top: 1px solid var(--line); }
.list-empty {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  color: var(--text3); font-size: 13px;
  padding: 40px 16px; text-align: center;
}
.le-ic { font-size: 26px; opacity: 0.7; }
/* ── css: editor ── */
/* ============ editor pane ============ */
.pane-editor {
  background: var(--editor-bg);
  display: flex;
  flex-direction: column;
  position: relative;
}
.ed-topbar {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--editor-bg);
  min-height: 44px;
}
#ed-title-area { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.tb-note { font-size: 12px; color: var(--text3); font-variant-numeric: tabular-nums; }
.tb-sep { opacity: 0.5; margin: 0 4px; }
#ed-meta-right { display: flex; align-items: center; gap: 10px; flex: none; }
.ed-toolbar {
  display: flex; align-items: center; gap: 1px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 2px;
  flex-wrap: nowrap;
  max-width: 100%;
  overflow-x: auto;
}
.ed-toolbar.disabled { opacity: 0.55; }
.tb-btn {
  border: none; background: none;
  min-width: 28px; height: 28px;
  padding: 0 7px;
  border-radius: 6px;
  font-family: var(--font-ui);
  font-size: 12.5px; font-weight: 600;
  color: var(--text2);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 4px;
  transition: background 0.12s, color 0.12s;
  white-space: nowrap;
}
.tb-btn:hover { background: var(--hover); color: var(--text); }
.tb-btn.active { background: var(--accentSoft); color: var(--accent); }
.tb-sep { width: 1px; height: 18px; background: var(--line); margin: 0 3px; flex: none; }
.tb-code { font-family: var(--font-mono); font-size: 11px; font-weight: 700; }
.tb-ul { font-size: 15px; line-height: 1; }
.tb-ol { font-family: var(--font-mono); font-size: 11px; font-weight: 700; }
.tb-task { font-size: 12px; }
.saved-indicator {
  font-size: 11.5px; color: var(--text3);
  opacity: 0; transition: opacity 0.3s;
  font-variant-numeric: tabular-nums;
}
.saved-indicator.show { opacity: 1; }
.saved-indicator.flash { color: var(--accent); }
#editor-host { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.ed-scroll { flex: 1; overflow-y: auto; }
.ed-doc {
  max-width: 780px;
  margin: 0 auto;
  padding: 34px 48px 120px;
  width: 100%;
}
.ed-meta { margin-bottom: 6px; }
.ed-title {
  font-family: var(--font-ui);
  font-size: 27px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.25;
  outline: none;
  caret-color: var(--accent);
  min-height: 34px;
}
.ed-title:empty::before { content: attr(data-ph); color: var(--text3); }
.ed-title.qn-empty-title:empty::before { content: 'Untitled'; }
.ed-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; min-height: 0; }
.ed-tags:empty { display: none; }
.tag-chip {
  display: inline-flex; align-items: center; gap: 3px;
  background: var(--accentSoft); color: var(--accent);
  border-radius: 6px; padding: 2px 4px 2px 8px;
  font-size: 11.5px; font-weight: 600;
  transition: filter 0.12s;
}
.tag-chip:hover { filter: brightness(0.96); }
.tag-x {
  border: none; background: none; color: inherit; cursor: pointer;
  font-size: 13px; line-height: 1; padding: 1px 4px; border-radius: 4px; opacity: 0.7;
}
.tag-x:hover { opacity: 1; background: rgba(0,0,0,0.06); }
.tag-input {
  border: none; background: none; outline: none;
  font-family: var(--font-ui); font-size: 11.5px; color: var(--text2);
  width: 84px; padding: 3px 4px; border-radius: 6px;
}
.tag-input::placeholder { color: var(--text3); }
.tag-input:focus { background: var(--hover); }

/* --- editor content (serif reading typography) --- */
.ed-content {
  font-family: var(--font-editor);
  font-size: 16.5px;
  line-height: 1.72;
  outline: none;
  caret-color: var(--accent);
  color: var(--text);
  word-wrap: break-word;
}
.ed-content p { margin: 0 0 0.65em; }
.ed-content p:empty { min-height: 1.4em; }
.ed-content p:only-child:empty { margin: 0; }
.ed-content h1 { font-size: 1.85em; line-height: 1.25; margin: 0.9em 0 0.5em; letter-spacing: -0.02em; }
.ed-content h2 { font-size: 1.45em; line-height: 1.3; margin: 0.9em 0 0.45em; letter-spacing: -0.015em; }
.ed-content h3 { font-size: 1.18em; line-height: 1.35; margin: 0.85em 0 0.4em; }
.ed-content h1:first-child, .ed-content h2:first-child, .ed-content h3:first-child { margin-top: 0.2em; }
.ed-content blockquote {
  border-left: 3px solid var(--accent);
  margin: 0.8em 0;
  padding: 0.35em 1.1em;
  color: var(--text2);
  background: var(--accentSoft);
  border-radius: 0 8px 8px 0;
}
.ed-content blockquote p { margin-bottom: 0.2em; }
.ed-content blockquote p:last-child { margin-bottom: 0; }
.ed-content ul, .ed-content ol { margin: 0.4em 0 0.8em; padding-left: 1.5em; }
.ed-content li { margin: 0.18em 0; }
.ed-content li::marker { color: var(--text3); }
.ed-content ul { list-style: disc; }
.ed-content ol { list-style: decimal; }
/* task list */
.ed-content li.qn-task { list-style: none; margin-left: -1.45em; }
.ed-content li.qn-task .qn-task-box {
  display: inline-block;
  width: 15px; height: 15px;
  border: 1.5px solid var(--text3);
  border-radius: 4px;
  margin-right: 9px;
  vertical-align: -2px;
  cursor: pointer;
  position: relative;
  transition: background 0.12s, border-color 0.12s;
  background: transparent;
}
.ed-content li.qn-task .qn-task-box:hover { border-color: var(--accent); }
.ed-content li.qn-task[data-checked="1"] .qn-task-box {
  background: var(--accent);
  border-color: var(--accent);
}
.ed-content li.qn-task[data-checked="1"] .qn-task-box::after {
  content: '✓';
  position: absolute; inset: 0;
  display: grid; place-items: center;
  color: #fff; font-size: 10px; font-weight: 700;
  font-family: var(--font-ui);
}
.ed-content li.qn-task[data-checked="1"] .qn-task-label {
  text-decoration: line-through;
  color: var(--text3);
}
.ed-content .qn-task-label { display: inline; }
.ed-content code {
  font-family: var(--font-mono);
  font-size: 0.82em;
  background: var(--panel2);
  border-radius: 4px;
  padding: 1.5px 5px;
  border: 1px solid var(--line);
}
.ed-content hr {
  border: none;
  border-top: 1px solid var(--line);
  margin: 1.6em 0;
}
.ed-content b, .ed-content strong { font-weight: 700; }

/* empty state */
.ed-empty {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px;
  color: var(--text3);
  pointer-events: none;
  transition: opacity 0.2s;
}
.ed-empty.hidden { opacity: 0; }
.ee-ic {
  font-size: 34px;
  width: 74px; height: 74px;
  display: grid; place-items: center;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 20px;
  color: var(--accent);
  margin-bottom: 10px;
}
.ee-title { font-size: 16px; font-weight: 600; color: var(--text2); }
.ee-sub { font-size: 13px; }
.ee-hints { display: flex; gap: 14px; margin-top: 16px; font-size: 12px; }
kbd {
  font-family: var(--font-ui);
  font-size: 10.5px;
  background: var(--panel2);
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  color: var(--text2);
}
/* ── css: overlays ── */
/* ============ search overlay, menus, buttons ============ */
.overlay {
  position: fixed; inset: 0;
  background: rgba(30, 24, 12, 0.28);
  backdrop-filter: blur(2px);
  z-index: 100;
  display: none;
  padding: 9vh 0 0;
}
.overlay.open { display: block; }
.search-panel {
  width: min(620px, 88vw);
  margin: 0 auto;
  background: var(--editor-bg);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: var(--shadow);
  overflow: hidden;
  animation: dropIn 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
@keyframes dropIn {
  from { opacity: 0; transform: translateY(-10px) scale(0.985); }
  to { opacity: 1; transform: none; }
}
.search-input-row {
  display: flex; align-items: center; gap: 10px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line);
}
.search-input-row .ic { width: 17px; height: 17px; color: var(--text3); }
.search-input {
  flex: 1; border: none; background: none; outline: none;
  font-family: var(--font-ui);
  font-size: 15.5px; color: var(--text);
}
.search-input::placeholder { color: var(--text3); }
.search-results { max-height: 52vh; overflow-y: auto; }
.sr-empty { padding: 22px 16px; color: var(--text3); font-size: 13px; text-align: center; }
.sr-row {
  padding: 10px 16px;
  cursor: pointer;
  border-bottom: 1px solid var(--line);
  transition: background 0.1s;
}
.sr-row:last-child { border-bottom: none; }
.sr-row:hover { background: var(--hover); }
.sr-name { font-size: 13.5px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
.sr-hit {
  font-size: 10px; font-weight: 600; color: var(--accent);
  background: var(--accentSoft); padding: 1px 6px; border-radius: 4px;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.sr-ctx { font-size: 12.5px; color: var(--text2); margin-top: 2px; line-height: 1.5; font-family: var(--font-editor); }
.sr-ctx mark, .nc-title mark, mark {
  background: var(--mark); color: inherit; border-radius: 2px; padding: 0 1px;
}

.menu {
  position: fixed; z-index: 200;
  background: var(--editor-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow);
  padding: 5px;
  min-width: 190px;
  display: flex; flex-direction: column; gap: 1px;
  animation: menuIn 0.12s ease-out;
}
@keyframes menuIn { from { opacity: 0; transform: translateY(-3px) scale(0.98); } to { opacity: 1; transform: none; } }
.menu-item {
  border: none; background: none; text-align: left;
  font-family: var(--font-ui);
  font-size: 13px; color: var(--text);
  padding: 7px 10px;
  border-radius: 7px;
  cursor: pointer;
  transition: background 0.1s;
  white-space: nowrap;
}
.menu-item:hover { background: var(--hover); }
.menu-item.danger { color: var(--danger); }
.menu-item.danger:hover { background: rgba(191, 59, 31, 0.1); }
.menu-item.active { color: var(--accent); font-weight: 600; }

/* buttons */
.btn {
  font-family: var(--font-ui);
  font-size: 13px; font-weight: 600;
  border-radius: 8px;
  padding: 7px 14px;
  cursor: pointer;
  border: 1px solid var(--line);
  background: var(--panel2);
  color: var(--text);
  transition: background 0.12s, border-color 0.12s;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.ghost { background: none; border: none; color: var(--accent); padding: 6px 10px; }
.btn.ghost:hover { background: var(--accentSoft); }
.btn.danger-ghost { background: none; border: none; color: var(--danger); width: 100%; text-align: center; font-family: var(--font-ui); font-size: 12.5px; font-weight: 600; padding: 6px; border-radius: 7px; cursor: pointer; }
.btn.danger-ghost:hover { background: rgba(191, 59, 31, 0.1); }

/* icon buttons */
.icon-btn {
  border: none; background: none;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: 7px;
  color: var(--text2);
  font-size: 14px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
  font-family: var(--font-ui);
}
.icon-btn:hover { background: var(--hover); color: var(--text); }
.icon-btn.tiny { width: 22px; height: 22px; font-size: 12px; }

/* scrollbars */
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 6px;
  border: 2px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--text3); background-clip: content-box; border: 2px solid transparent; }
::-webkit-scrollbar-track { background: none; }
/* ── css: vars-alias ── */
/* ============ var aliases (camel → kebab) ============ */
:root {
  --accentSoft: var(--accent-soft);
  --editorBg: var(--editor-bg);
}
/* ── css: editor-extra ── */
/* ============ editor pane additions ============ */
.pane-editor.no-note #editor-host { visibility: hidden; }
.pane-editor.no-note .tb-note { visibility: hidden; }
</style>
</head>
<body>
<div id="app">
  <div id="side-toggle" class="icon-btn side-toggle" title="Toggle sidebar">☰</div>
  <aside id="sidebar" class="pane pane-sidebar"></aside>
  <div id="rs-sidebar" class="rs" title="Drag to resize"></div>
  <section id="note-list" class="pane pane-list">
    <div id="list-head"></div>
    <div id="list-body" class="list-body"></div>
    <div id="list-foot"></div>
  </section>
  <div id="rs-list" class="rs" title="Drag to resize"></div>
  <main id="editor-pane" class="pane pane-editor">
    <div class="ed-topbar">
      <div id="ed-title-area"></div>
      <div id="ed-toolbar" class="ed-toolbar"></div>
      <div id="ed-meta-right">
        <span id="saved-indicator" class="saved-indicator"></span>
      </div>
    </div>
    <div id="editor-host"></div>
    <div id="ed-empty" class="ed-empty">
      <div class="ee-ic">✎</div>
      <div class="ee-title">Select or create a note</div>
      <div class="ee-sub">Notes you pin float to the top of their list.</div>
      <div class="ee-hints">
        <span><kbd>⌘K</kbd> search</span><span><kbd>⌘B</kbd> bold</span><span><kbd>⌘Z</kbd> undo</span>
      </div>
    </div>
  </main>
  <div id="overlay" class="overlay"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./ui":"src/ui.ts","./editor":"src/editor.ts","./text":"src/text.ts","./types":"src/types.ts"},"src/state.ts":{"./constants":"src/constants.ts","./text":"src/text.ts","./types":"src/types.ts"},"src/editor.ts":{"./state":"src/state.ts","./types":"src/types.ts","./text":"src/text.ts"},"src/ui.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./text":"src/text.ts","./types":"src/types.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 — boot sequence: load state, wire UI + editor, sync.
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const ui_1 = require("./ui");
const editor_1 = require("./editor");
const text_1 = require("./text");
function escapeAttr(t) {
    return t.replace(/"/g, '&quot;');
}
function main() {
    const s = (0, state_1.load)();
    const sidebar = document.getElementById('sidebar');
    const list = document.getElementById('note-list');
    const editorHost = document.getElementById('editor-host');
    const overlay = document.getElementById('overlay');
    const savedEl = document.getElementById('saved-indicator');
    const toolbar = document.getElementById('ed-toolbar');
    buildToolbar(toolbar);
    let editor = null;
    const ui = (0, ui_1.initUi)(s, { sidebar, list, editorHost, overlay, saved: savedEl }, () => {
        syncEditor();
    });
    editor = (0, editor_1.createEditor)(editorHost, s, () => {
        ui.render();
    });
    (0, ui_1.applyTheme)();
    let lastSyncedId = null;
    const paneEditor = document.getElementById('editor-pane');
    function syncEditor() {
        if (!editor)
            return;
        const note = (0, state_1.getNote)(s, s.selectedNoteId);
        const id = note && !note.deleted ? note.id : null;
        const emptyEl = document.getElementById('ed-empty');
        if (id !== lastSyncedId) {
            lastSyncedId = id;
            if (note && !note.deleted) {
                emptyEl.classList.add('hidden');
                paneEditor.classList.remove('no-note');
                editor.selectNote(note);
                // newly created, untitled note → jump straight into the title
                if (!note.title) {
                    const t = document.querySelector('.ed-title');
                    if (t)
                        t.focus();
                }
            }
            else {
                emptyEl.classList.remove('hidden');
                paneEditor.classList.add('no-note');
                editor.selectNote(null);
            }
        }
        else {
            // same note — refresh metadata (tags row) in place
            editor.syncNote(note);
        }
        if (note && !note.deleted)
            updateMeta(note);
        (0, ui_1.updateSavedEl)(s);
    }
    function updateMeta(n) {
        const area = document.getElementById('ed-title-area');
        const words = n.content.replace(/<[^>]+>/g, ' ').trim().split(/\s+/).filter(Boolean).length;
        area.innerHTML = `<span class="tb-note">${n.title ? escapeAttr(n.title) : 'Untitled'} <span class="tb-sep">·</span> ${words} words</span>`;
    }
    syncEditor();
    ui.render();
    // keep the "Saved" clock fresh
    setInterval(() => (0, ui_1.updateSavedEl)(s), 2000);
    window.__notes = {
        state: s,
        flush: () => (0, state_1.flushSave)(s),
        editor: () => editor,
        render: () => ui.render(),
        actions: state_1.actions,
        search: (q) => (0, state_1.searchNotes)(s, q),
        sanitize: text_1.sanitizeHtml,
        extract: text_1.extractText,
    };
}
// --- formatting toolbar -----------------------------------------
function buildToolbar(toolbar) {
    const mk = (cmd, title, html) => {
        const b = document.createElement('button');
        b.className = 'tb-btn';
        b.dataset.cmd = cmd;
        b.title = title;
        b.innerHTML = html;
        b.addEventListener('mousedown', e => e.preventDefault()); // keep editor focus
        toolbar.appendChild(b);
    };
    const sep = () => {
        const d = document.createElement('span');
        d.className = 'tb-sep';
        toolbar.appendChild(d);
    };
    mk('bold', 'Bold (⌘/Ctrl+B)', '<b>B</b>');
    mk('italic', 'Italic (⌘/Ctrl+I)', '<i>I</i>');
    mk('underline', 'Underline (⌘/Ctrl+U)', '<u>U</u>');
    mk('strike', 'Strikethrough', '<s>S</s>');
    mk('code', 'Inline code', '<span class="tb-code">&lt;/&gt;</span>');
    sep();
    mk('h1', 'Heading 1', 'H1');
    mk('h2', 'Heading 2', 'H2');
    mk('h3', 'Heading 3', 'H3');
    sep();
    mk('ul', 'Bulleted list', '<span class="tb-ul">•</span>List');
    mk('ol', 'Numbered list', '<span class="tb-ol">1.</span>List');
    mk('task', 'To-do list', '<span class="tb-task">☑</span>Tasks');
    sep();
    mk('quote', 'Quote', '❝');
    mk('hr', 'Divider', '—');
    toolbar.addEventListener('click', e => {
        const b = e.target.closest('.tb-btn');
        if (!b)
            return;
        const cmd = b.dataset.cmd;
        const ed = window.__notes;
        ed?.editor()?.command(cmd);
    });
}
main();
};

// ── 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 — everything the app looks/feels is here.
// Colors are applied as CSS custom properties at boot.
// ============================================================
exports.C = {
    // --- persistence ---
    STORAGE_KEY: 'quietnotes.v1',
    AUTOSAVE_DEBOUNCE_MS: 600, // typing → "Saved" latency
    HISTORY_LIMIT: 120, // undo/redo snapshots per note
    // --- content ---
    SNIPPET_LEN: 110, // characters shown in the note list
    SEARCH_BEFORE: 28, // context chars before a match in results
    SEARCH_AFTER: 52, // context chars after a match in results
    // --- layout ---
    SIDEBAR_W: 264,
    LIST_W: 320,
    SIDEBAR_MIN: 200,
    SIDEBAR_MAX: 380,
    LIST_MIN: 230,
    LIST_MAX: 500,
    // --- typography ---
    FONT_UI: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, 'Helvetica Neue', Arial, sans-serif",
    FONT_EDITOR: "Georgia, 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif",
    FONT_MONO: "ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace",
    // --- color themes (keys become --kebab-case vars) ---
    THEMES: {
        light: {
            bg: '#f6f3ec',
            panel: '#efeae0',
            panel2: '#e6dfd0',
            editorBg: '#faf8f3',
            text: '#2a2418',
            text2: '#6e6553',
            text3: '#9d927d',
            line: '#e0d8c6',
            accent: '#b45309',
            accentSoft: 'rgba(180,83,9,0.10)',
            hover: 'rgba(70,55,30,0.06)',
            mark: '#fbe08a',
            danger: '#bf3b1f',
            shadow: '0 6px 20px rgba(70,50,20,0.14)',
        },
        dark: {
            bg: '#16130e',
            panel: '#1d1913',
            panel2: '#262019',
            editorBg: '#1a1712',
            text: '#ebe4d6',
            text2: '#a2957f',
            text3: '#776c58',
            line: '#2d271d',
            accent: '#e0a458',
            accentSoft: 'rgba(224,164,88,0.14)',
            hover: 'rgba(255,245,225,0.055)',
            mark: 'rgba(224,164,88,0.34)',
            danger: '#e2725b',
            shadow: '0 6px 20px rgba(0,0,0,0.45)',
        },
    },
};
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// Data model — the single source of truth is serializable JSON.
// The DOM is only ever a projection of this model.
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
};

// ── module: src/text.ts ──
__mods["src/text.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// Text & content utilities — pure functions.
// sanitizeHtml is the single gate for all incoming HTML
// (paste, drag, and anything the model stores).
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.uid = uid;
exports.escapeHtml = escapeHtml;
exports.escapeReg = escapeReg;
exports.sanitizeHtml = sanitizeHtml;
exports.normalizeHtml = normalizeHtml;
exports.extractText = extractText;
exports.snippet = snippet;
exports.countWords = countWords;
exports.fmtDate = fmtDate;
function uid() {
    return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
function escapeHtml(s) {
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function escapeReg(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// --- Allowed formatting subset -------------------------------
// Everything a note can contain. Anything else is dropped by
// the sanitizer, so the model can never hold foreign markup.
const ALLOWED = {
    p: true, div: true, br: true, b: true, strong: true, i: true, em: true,
    u: true, s: true, del: true, code: true,
    h1: true, h2: true, h3: true,
    ul: true, ol: true, li: true,
    blockquote: true, hr: true,
    span: true, // only with our own data-* / class markers
};
const DROP_TAGS = ['script', 'style', 'iframe', 'object', 'img', 'video', 'audio', 'meta', 'link', 'embed', 'form', 'input', 'button', 'table', 'tr', 'td', 'th', 'iframe'];
/** Strip everything that isn't in ALLOWED; drop scripts, attrs, events, styles. */
function sanitizeHtml(html) {
    const doc = new DOMParser().parseFromString(`<div id="qn-root">${html}</div>`, 'text/html');
    const root = doc.getElementById('qn-root');
    const process = (el) => {
        const tag = el.tagName.toLowerCase();
        if (DROP_TAGS.includes(tag)) {
            el.remove();
            return;
        }
        if (!ALLOWED[tag]) {
            // unwrap unknown containers; children are kept (and still sanitized)
            const frag = doc.createDocumentFragment();
            while (el.firstChild)
                frag.appendChild(el.firstChild);
            el.replaceWith(frag);
            return;
        }
        if (tag === 'strike') {
            const s = doc.createElement('s');
            const frag = doc.createDocumentFragment();
            while (el.firstChild)
                frag.appendChild(el.firstChild);
            s.appendChild(frag);
            el.replaceWith(s);
            return;
        }
        // clean attributes
        for (const attr of Array.from(el.attributes)) {
            const n = attr.name.toLowerCase();
            const v = attr.value;
            if (n.startsWith('on') || n === 'href' || n === 'src' || n === 'style' ||
                n === 'class' && !/^(qn-[\w-]+|qn-task( checked)?|qn-title( empty)?)$/.test(v)) {
                el.removeAttribute(attr.name);
            }
            else if (n === 'data-tag' && (v.includes('"') || v.includes(' ') || v.length > 60)) {
                el.removeAttribute(attr.name);
            }
        }
        if (tag === 'li') {
            // no nested lists — flatten them out
            for (const inner of Array.from(el.querySelectorAll('ul,ol')))
                inner.remove();
        }
    };
    const walk = (el) => {
        for (const child of Array.from(el.children)) {
            process(child);
            walk(child);
        }
    };
    walk(root);
    root.innerHTML = normalizeHtml(root.innerHTML);
    return root.innerHTML;
}
/** Normalize <div>/<br> soup into tidy p / h* / li blocks. */
function normalizeHtml(html) {
    const doc = new DOMParser().parseFromString(`<div id="qn-root">${html}</div>`, 'text/html');
    const root = doc.getElementById('qn-root');
    const BLOCK = /^(H1|H2|H3|UL|OL|BLOCKQUOTE|HR|DIV|P)$/;
    // 1) split DIV/P containing <br> into one block per line
    for (const el of Array.from(root.querySelectorAll('div,p'))) {
        let cur = el;
        for (const br of Array.from(el.children).filter(c => c.nodeName === 'BR')) {
            const frag = doc.createDocumentFragment();
            let n = br.nextSibling;
            while (n) {
                const nx = n.nextSibling;
                frag.appendChild(n);
                n = nx;
            }
            const sib = doc.createElement('div');
            cur.after(sib);
            sib.appendChild(frag);
            cur = sib;
        }
    }
    // 2) DIV → P unless it contains block children
    for (const el of Array.from(root.querySelectorAll('div'))) {
        const hasBlockChild = Array.from(el.children).some(c => BLOCK.test(c.nodeName) && c.nodeName !== 'DIV');
        if (!hasBlockChild) {
            const p = doc.createElement('p');
            const frag = doc.createDocumentFragment();
            while (el.firstChild)
                frag.appendChild(el.firstChild);
            p.appendChild(frag);
            el.replaceWith(p);
        }
        else {
            // container holding block children: unwrap (children keep their place)
            const frag = doc.createDocumentFragment();
            while (el.firstChild)
                frag.appendChild(el.firstChild);
            el.replaceWith(frag);
        }
    }
    // 3) empty lines get a <br> so they stay visible/editable
    for (const el of Array.from(root.querySelectorAll('p,div'))) {
        if (el.textContent === '' && !el.querySelector('br'))
            el.innerHTML = '<br>';
    }
    // 4) strip trailing <br> inside li and blockquote
    for (const el of Array.from(root.querySelectorAll('li,blockquote'))) {
        while (el.lastChild && el.lastChild.nodeName === 'BR')
            el.lastChild.remove();
    }
    // 5) blockquotes: recursively tidy inner HTML
    for (const q of Array.from(root.querySelectorAll('blockquote'))) {
        q.innerHTML = normalizeHtml(q.innerHTML);
    }
    return root.innerHTML;
}
/** Pure text extraction — feeds search & snippets. Ignores tags,
 *  includes task labels, never yields tag names like "b" or "div". */
function extractText(html) {
    const doc = new DOMParser().parseFromString(`<div id="qn-root">${html}</div>`, 'text/html');
    const root = doc.getElementById('qn-root');
    const lines = [];
    const walk = (el) => {
        for (const child of Array.from(el.childNodes)) {
            if (child.nodeType === 3) {
                lines.push('T:' + (child.textContent || ''));
                continue;
            }
            if (child.nodeType !== 1)
                continue;
            const tag = child.tagName.toLowerCase();
            if (tag === 'br')
                continue;
            if (tag === 'hr') {
                lines.push('B:hr');
                continue;
            }
            const block = /^(h1|h2|h3|li|blockquote|p|div)$/.test(tag);
            walk(child);
            if (block)
                lines.push('B:');
        }
    };
    walk(root);
    // fold: T segments join with spaces inside a block, B starts a new block
    const out = [];
    let cur = '';
    for (const seg of lines) {
        if (seg.startsWith('T:')) {
            const t = seg.slice(2);
            if (t) {
                if (cur && !cur.endsWith(' ') && !t.startsWith(' '))
                    cur += ' ';
                cur += t;
            }
        }
        else {
            out.push(cur);
            cur = seg === 'B:hr' ? '\u0000hr' : '';
        }
    }
    out.push(cur);
    return out
        .map(s => (s === '\u0000hr' ? '——————' : s.trim()))
        .filter((s, i, a) => s !== '' || (i > 0 && i < a.length - 1))
        .join('\n')
        .replace(/\n{3,}/g, '\n\n')
        .trim();
}
/** One-line snippet for the note list. */
function snippet(html, len = 110) {
    const t = extractText(html).replace(/\n+/g, ' ').trim();
    return t.length > len ? t.slice(0, len).trimEnd() + '…' : t;
}
/** Word count (title + body) for the status bar. */
function countWords(html, title) {
    const t = (title + ' ' + extractText(html)).trim();
    return t ? t.split(/\s+/).length : 0;
}
function fmtDate(ts) {
    const d = new Date(ts);
    const now = new Date();
    const sameDay = d.toDateString() === now.toDateString();
    const sameYear = d.getFullYear() === now.getFullYear();
    const hm = d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
    const md = d.toLocaleDateString([], { month: 'short', day: 'numeric' });
    if (sameDay)
        return hm;
    if (sameYear)
        return md;
    return d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
}
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// State — single source of truth, persistence, and actions.
// Every mutation flows through an action which mutates the
// model, bumps savedness, and persists (debounced).
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.actions = void 0;
exports.defaultState = defaultState;
exports.load = load;
exports.scheduleSave = scheduleSave;
exports.flushSave = flushSave;
exports.getNote = getNote;
exports.getNotebook = getNotebook;
exports.allTags = allTags;
exports.visibleNotes = visibleNotes;
exports.sortNotes = sortNotes;
exports.searchNotes = searchNotes;
const constants_1 = require("./constants");
const text_1 = require("./text");
// --- load / save --------------------------------------------
function defaultState() {
    const t = Date.now();
    const nb = [
        { id: 'nb-personal', name: 'Personal', createdAt: t },
        { id: 'nb-work', name: 'Work', createdAt: t },
    ];
    const welcome = {
        id: 'note-welcome',
        title: 'Welcome to Notes',
        content: '<p>A quiet place to think. This is your first note — everything you write is saved automatically on this device.</p>' +
            '<h2>Editing</h2>' +
            '<p>Select text and use the toolbar, or press <b>Ctrl/⌘</b> <i>B</i> for bold, <i>I</i> for italic, <i>U</i> for underline. Undo and redo work the way you expect.</p>' +
            '<h2>Markdown shortcuts</h2>' +
            '<ul>' +
            '<li>Start a line with <code>#</code> <code>space</code> for a heading</li>' +
            '<li>Start a line with <code>-</code> <code>space</code> for a list</li>' +
            '<li>Start a line with <code>[ ]</code> <code>space</code> for a task list</li>' +
            '</ul>' +
            '<h2>Tasks</h2>' +
            '<ul>' +
            '<li class="qn-task" data-checked="1"><span class="qn-task-box"></span><span class="qn-task-label">Check off a task</span></li>' +
            '<li class="qn-task" data-checked="0"><span class="qn-task-box"></span><span class="qn-task-label">Try toggling the box above</span></li>' +
            '</ul>' +
            '<blockquote>Deleted notes go to the Trash, where you can restore them.</blockquote>' +
            '<p>Add tags from the field under the title. Drag a note in the list onto a notebook in the sidebar to move it.</p>',
        notebookId: 'nb-personal',
        tags: ['welcome'],
        pinned: true,
        createdAt: t - 86400000,
        updatedAt: t - 3600000,
        deleted: false,
    };
    const recipes = {
        id: 'note-recipes',
        title: 'Reading list',
        content: '<p>Books and essays I want to sit with.</p>' +
            '<ul>' +
            '<li class="qn-task" data-checked="0"><span class="qn-task-box"></span><span class="qn-task-label">The Shallows — Nicholas Carr</span></li>' +
            '<li class="qn-task" data-checked="0"><span class="qn-task-box"></span><span class="qn-task-label">Four Thousand Weeks</span></li>' +
            '</ul>',
        notebookId: 'nb-personal',
        tags: ['books'],
        pinned: false,
        createdAt: t - 7200000,
        updatedAt: t - 1800000,
        deleted: false,
    };
    return {
        notebooks: nb,
        notes: [welcome, recipes],
        view: { kind: 'notebook', id: 'nb-personal' },
        selectedNoteId: 'note-welcome',
        sortBy: 'updated',
        collapsed: {},
        theme: 'light',
        layout: { sidebarCollapsed: false, sidebarW: constants_1.C.SIDEBAR_W, listW: constants_1.C.LIST_W },
        dirty: false,
        lastSaved: t,
    };
}
function load() {
    try {
        const raw = localStorage.getItem(constants_1.C.STORAGE_KEY);
        if (!raw)
            return defaultState();
        const p = JSON.parse(raw);
        if (!p || p.v !== 1 || !Array.isArray(p.notes) || !Array.isArray(p.notebooks))
            return defaultState();
        // sanitize stored HTML once at boot
        for (const n of p.notes)
            n.content = (0, text_1.sanitizeHtml)(n.content || '');
        return {
            notebooks: p.notebooks,
            notes: p.notes,
            view: p.view && p.view.kind ? p.view : { kind: 'all', id: null },
            selectedNoteId: p.selectedNoteId ?? null,
            sortBy: ['updated', 'created', 'alpha'].includes(p.sortBy) ? p.sortBy : 'updated',
            collapsed: p.collapsed || {},
            theme: p.theme === 'dark' ? 'dark' : 'light',
            layout: {
                sidebarCollapsed: !!p.layout?.sidebarCollapsed,
                sidebarW: p.layout?.sidebarW ?? constants_1.C.SIDEBAR_W,
                listW: p.layout?.listW ?? constants_1.C.LIST_W,
            },
            dirty: false,
            lastSaved: Date.now(),
        };
    }
    catch {
        return defaultState();
    }
}
let saveTimer;
function persistNow(s) {
    const p = {
        v: 1,
        notebooks: s.notebooks,
        notes: s.notes,
        view: s.view,
        selectedNoteId: s.selectedNoteId,
        sortBy: s.sortBy,
        collapsed: s.collapsed,
        theme: s.theme,
        layout: s.layout,
    };
    try {
        localStorage.setItem(constants_1.C.STORAGE_KEY, JSON.stringify(p));
        s.dirty = false;
        s.lastSaved = Date.now();
    }
    catch {
        /* storage full — keep running in memory */
    }
}
function scheduleSave(s) {
    s.dirty = true;
    clearTimeout(saveTimer);
    saveTimer = window.setTimeout(() => persistNow(s), constants_1.C.AUTOSAVE_DEBOUNCE_MS);
}
function flushSave(s) {
    clearTimeout(saveTimer);
    if (s.dirty)
        persistNow(s);
}
// --- lookups --------------------------------------------------
function getNote(s, id) {
    return s.notes.find(n => n.id === id) || null;
}
function getNotebook(s, id) {
    return s.notebooks.find(n => n.id === id) || null;
}
function allTags(s) {
    const set = new Set();
    for (const n of s.notes)
        if (!n.deleted)
            for (const t of n.tags)
                set.add(t);
    return Array.from(set).sort((a, b) => a.localeCompare(b));
}
function visibleNotes(s) {
    let list = s.notes.filter(n => !n.deleted);
    switch (s.view.kind) {
        case 'notebook':
            list = list.filter(n => n.notebookId === s.view.id);
            break;
        case 'trash':
            list = s.notes.filter(n => n.deleted);
            break;
        case 'tag':
            list = list.filter(n => n.tags.includes(s.view.id));
            break;
    }
    return sortNotes(list, s.sortBy);
}
function sortNotes(list, sortBy) {
    const arr = list.slice();
    arr.sort((a, b) => {
        if (a.pinned !== b.pinned)
            return a.pinned ? -1 : 1;
        switch (sortBy) {
            case 'created': return b.createdAt - a.createdAt;
            case 'alpha': return (a.title || 'Untitled').localeCompare(b.title || 'Untitled') || b.updatedAt - a.updatedAt;
            default: return b.updatedAt - a.updatedAt;
        }
    });
    return arr;
}
// --- actions --------------------------------------------------
function touch(n) { n.updatedAt = Date.now(); }
exports.actions = {
    /** Create a note in a notebook (or unfiled), select it, return it. */
    createNote(s, notebookId) {
        const t = Date.now();
        const n = {
            id: (0, text_1.uid)(), title: '', content: '<p><br></p>',
            notebookId, tags: [], pinned: false,
            createdAt: t, updatedAt: t, deleted: false,
        };
        s.notes.unshift(n);
        s.selectedNoteId = n.id;
        if (notebookId && s.view.kind === 'notebook' && s.view.id !== notebookId) {
            s.view = { kind: 'notebook', id: notebookId };
        }
        if (s.view.kind === 'trash')
            s.view = { kind: 'all', id: null };
        scheduleSave(s);
        return n;
    },
    updateContent(s, noteId, html) {
        const n = getNote(s, noteId);
        if (!n || n.content === html)
            return;
        n.content = (0, text_1.sanitizeHtml)(html);
        touch(n);
        scheduleSave(s);
    },
    updateTitle(s, noteId, title) {
        const n = getNote(s, noteId);
        if (!n || n.title === title)
            return;
        n.title = title.slice(0, 200);
        touch(n);
        scheduleSave(s);
    },
    selectNote(s, noteId) {
        s.selectedNoteId = noteId;
        scheduleSave(s);
    },
    setView(s, view, selectFirst = true) {
        s.view = view;
        if (selectFirst) {
            const list = visibleNotes(s);
            s.selectedNoteId = list.length ? list[0].id : null;
        }
        scheduleSave(s);
    },
    setSort(s, sortBy) { s.sortBy = sortBy; scheduleSave(s); },
    setTheme(s, theme) { s.theme = theme; scheduleSave(s); },
    toggleCollapse(s, nbId) {
        s.collapsed[nbId] = !s.collapsed[nbId];
        scheduleSave(s);
    },
    setSidebarCollapsed(s, collapsed) {
        s.layout.sidebarCollapsed = collapsed;
        scheduleSave(s);
    },
    setLayout(s, sidebarW, listW) {
        if (sidebarW !== undefined)
            s.layout.sidebarW = Math.max(constants_1.C.SIDEBAR_MIN, Math.min(constants_1.C.SIDEBAR_MAX, sidebarW));
        if (listW !== undefined)
            s.layout.listW = Math.max(constants_1.C.LIST_MIN, Math.min(constants_1.C.LIST_MAX, listW));
        scheduleSave(s);
    },
    pinNote(s, noteId) {
        const n = getNote(s, noteId);
        if (!n)
            return;
        n.pinned = !n.pinned;
        touch(n);
        scheduleSave(s);
    },
    duplicateNote(s, noteId) {
        const n = getNote(s, noteId);
        if (!n)
            return null;
        const t = Date.now();
        const copy = {
            ...n, id: (0, text_1.uid)(),
            title: n.title ? n.title + ' (copy)' : 'Untitled (copy)',
            createdAt: t, updatedAt: t, pinned: false,
        };
        s.notes.unshift(copy);
        s.selectedNoteId = copy.id;
        scheduleSave(s);
        return copy;
    },
    /** Soft-delete → Trash (remembers its notebook for restore). */
    deleteNote(s, noteId) {
        const n = getNote(s, noteId);
        if (!n || n.deleted)
            return;
        n.deleted = true;
        n.deletedAt = Date.now();
        if (s.selectedNoteId === noteId)
            s.selectedNoteId = null;
        scheduleSave(s);
    },
    restoreNote(s, noteId) {
        const n = getNote(s, noteId);
        if (!n || !n.deleted)
            return;
        n.deleted = false;
        delete n.deletedAt;
        touch(n);
        // jump back to its home notebook if we're not already in a sensible view
        if (n.notebookId)
            s.view = { kind: 'notebook', id: n.notebookId };
        s.selectedNoteId = n.id;
        scheduleSave(s);
    },
    /** Permanent delete — no coming back. */
    destroyNote(s, noteId) {
        const i = s.notes.findIndex(n => n.id === noteId);
        if (i < 0)
            return;
        s.notes.splice(i, 1);
        if (s.selectedNoteId === noteId)
            s.selectedNoteId = null;
        scheduleSave(s);
    },
    moveNote(s, noteId, toNotebookId) {
        const n = getNote(s, noteId);
        if (!n || n.notebookId === toNotebookId)
            return;
        n.notebookId = toNotebookId;
        touch(n);
        scheduleSave(s);
    },
    addNote(s, notebookId) {
        exports.actions.createNote(s, notebookId);
    },
    createNotebook(s, name) {
        const nb = { id: (0, text_1.uid)(), name: name.trim() || 'Untitled', createdAt: Date.now() };
        s.notebooks.push(nb);
        s.view = { kind: 'notebook', id: nb.id };
        scheduleSave(s);
        return nb;
    },
    renameNotebook(s, id, name) {
        const nb = getNotebook(s, id);
        if (!nb)
            return;
        nb.name = name.trim() || nb.name;
        scheduleSave(s);
    },
    deleteNotebook(s, id) {
        const nb = getNotebook(s, id);
        if (!nb)
            return;
        s.notebooks = s.notebooks.filter(x => x.id !== id);
        for (const n of s.notes)
            if (n.notebookId === id)
                n.notebookId = null;
        delete s.collapsed[id];
        if (s.view.kind === 'notebook' && s.view.id === id) {
            s.view = { kind: 'all', id: null };
            const list = visibleNotes(s);
            s.selectedNoteId = list.length ? list[0].id : null;
        }
        scheduleSave(s);
    },
    setNoteTags(s, noteId, tags) {
        const n = getNote(s, noteId);
        if (!n)
            return;
        const cleaned = Array.from(new Set(tags.map(t => t.trim()).filter(Boolean)));
        if (cleaned.join('\u0000') === n.tags.join('\u0000'))
            return;
        n.tags = cleaned.slice(0, 24);
        touch(n);
        scheduleSave(s);
    },
};
/** Case-insensitive live search over titles + body text (not HTML tags). */
function searchNotes(s, query) {
    const q = query.trim().toLowerCase();
    if (!q)
        return [];
    const hits = [];
    const live = s.notes.filter(n => !n.deleted);
    const scored = live.map(n => {
        const title = (n.title || '').toLowerCase();
        const body = (0, text_1.extractText)(n.content).toLowerCase();
        let ti = title.indexOf(q);
        let bi = body.indexOf(q);
        let score = -1;
        if (ti >= 0)
            score = 0;
        else if (bi >= 0)
            score = 1;
        return { n, ti, bi, score };
    }).filter(x => x.score >= 0).sort((a, b) => a.score - b.score || b.n.updatedAt - a.n.updatedAt);
    for (const { n, ti, bi, score } of scored) {
        if (score === 0) {
            hits.push({ note: n, inTitle: true, context: n.title || 'Untitled' });
        }
        else {
            const plain = (0, text_1.extractText)(n.content);
            const idx = plain.toLowerCase().indexOf(q);
            const start = Math.max(0, idx - constants_1.C.SEARCH_BEFORE);
            const end = Math.min(plain.length, idx + q.length + constants_1.C.SEARCH_AFTER);
            let ctx = plain.slice(start, end).replace(/\n+/g, ' ');
            if (start > 0)
                ctx = '…' + ctx;
            if (end < plain.length)
                ctx = ctx + '…';
            hits.push({ note: n, inTitle: false, context: ctx });
        }
    }
    return hits;
}
};

// ── module: src/editor.ts ──
__mods["src/editor.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// Editor — contenteditable core: toolbar + state reflection,
// markdown input shortcuts, list behavior, task lists,
// sanitized paste, and a reliable snapshot undo/redo.
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.createEditor = createEditor;
const state_1 = require("./state");
const text_1 = require("./text");
const HISTORY_LIMIT = 120;
const BURST_MS = 400; // keystrokes within 400ms coalesce into one undo step
function createEditor(host, state, onChange) {
    // ----------------------------------------------------------
    // DOM scaffolding
    // ----------------------------------------------------------
    host.innerHTML = `
    <div class="ed-scroll">
      <div class="ed-doc">
        <div class="ed-meta">
          <div class="ed-title" contenteditable="true" spellcheck="false" data-ph="Untitled"></div>
          <div class="ed-tags"></div>
        </div>
        <div class="ed-content" contenteditable="true" spellcheck="false"></div>
      </div>
    </div>`;
    const titleEl = host.querySelector('.ed-title');
    const tagsEl = host.querySelector('.ed-tags');
    const editor = host.querySelector('.ed-content');
    let note = null;
    let history = []; // snapshots of {title\0html}; [0] = state at select
    let redoStack = [];
    let lastBurst = 0;
    let suppressInput = false; // while we mutate the DOM programmatically
    const serialize = () => (titleEl.textContent || '') + '\u0000' + editor.innerHTML;
    const deserialize = (s) => {
        const i = s.indexOf('\u0000');
        const t = s.slice(0, i);
        const h = s.slice(i + 1);
        suppressInput = true;
        titleEl.textContent = t;
        syncTitleEmpty();
        editor.innerHTML = h;
        suppressInput = false;
    };
    // ----------------------------------------------------------
    // Caret offset helpers (text-length based) — used to restore
    // the caret after undo/redo.
    // ----------------------------------------------------------
    function caretOffset(el) {
        const s = window.getSelection();
        if (!s || !s.rangeCount)
            return null;
        const r = s.getRangeAt(0);
        const sc = r.startContainer;
        if (!el.contains(sc))
            return null;
        const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT);
        let off = 0;
        let t;
        if (sc.nodeType === 3) {
            while ((t = it.nextNode())) {
                if (t === sc)
                    return off + r.startOffset;
                off += t.length;
            }
            return null;
        }
        const kids = sc.childNodes;
        const start = Math.min(r.startOffset, kids.length);
        for (let i = 0; i < start; i++)
            off += (kids[i].textContent || '').length;
        return off;
    }
    function restoreCaretOffset(el, off) {
        const sel = window.getSelection();
        if (!sel)
            return;
        sel.removeAllRanges();
        if (off == null) {
            caretAtEnd(el);
            return;
        }
        let remaining = off;
        const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT);
        let t;
        while ((t = it.nextNode())) {
            if (remaining <= t.length) {
                const r = document.createRange();
                r.setStart(t, remaining);
                r.collapse(true);
                sel.addRange(r);
                return;
            }
            remaining -= t.length;
        }
        caretAtEnd(el);
    }
    function caretAtEnd(el) {
        const sel = window.getSelection();
        if (!sel)
            return;
        sel.removeAllRanges();
        const r = document.createRange();
        r.selectNodeContents(el);
        r.collapse(false);
        sel.addRange(r);
    }
    function caretAtStart(el) {
        const sel = window.getSelection();
        if (!sel)
            return;
        sel.removeAllRanges();
        const r = document.createRange();
        r.selectNodeContents(el);
        r.collapse(true);
        sel.addRange(r);
    }
    // ----------------------------------------------------------
    // History — history[] is [initial state, pre-edit snapshot of
    // each burst/structural change…]; undo pops to the previous.
    // Keystrokes within BURST_MS coalesce into a single step.
    // ----------------------------------------------------------
    function pushHistory(snap) {
        history.push(snap);
        if (history.length > HISTORY_LIMIT)
            history.shift();
        redoStack = [];
    }
    let pending = null;
    let lastPost = ''; // state after the last committed change = pre-state of the next burst
    function commitPending() {
        if (pending != null) {
            history.push(pending);
            if (history.length > HISTORY_LIMIT)
                history.shift();
            redoStack = [];
        }
        pending = null;
    }
    /** Apply a structural edit: record pre-state, run fn. Structural
     *  edits are their own undo boundary — typing afterwards starts
     *  a fresh burst. */
    function edit(fn) {
        const pre = serialize();
        fn();
        pushHistory(pre);
        pending = null;
        lastBurst = Date.now();
        lastPost = serialize();
    }
    function doUndo() {
        commitPending();
        if (history.length <= 1)
            return;
        redoStack.push(serialize());
        const snap = history.pop();
        const off = caretOffset(editor);
        suppressInput = true;
        deserialize(snap);
        suppressInput = false;
        lastPost = serialize();
        restoreCaretOffset(editor, off);
        emitChange();
        refreshToolbar();
    }
    function doRedo() {
        commitPending();
        if (!redoStack.length)
            return;
        history.push(serialize());
        const snap = redoStack.pop();
        const off = caretOffset(editor);
        suppressInput = true;
        deserialize(snap);
        suppressInput = false;
        lastPost = serialize();
        restoreCaretOffset(editor, off);
        emitChange();
        refreshToolbar();
    }
    function emitChange() {
        if (!note || suppressInput)
            return;
        state_1.actions.updateContent(state, note.id, editor.innerHTML);
        onChange();
    }
    // ----------------------------------------------------------
    // Selection / block helpers
    // ----------------------------------------------------------
    function getSel() {
        const s = window.getSelection();
        return s && s.anchorNode && (editor.contains(s.anchorNode) || titleEl.contains(s.anchorNode)) ? s : null;
    }
    function liOf(node) {
        if (!node)
            return null;
        const el = node.nodeType === 1 ? node : node.parentElement;
        if (!el)
            return null;
        const li = el.closest('li');
        return li && editor.contains(li) ? li : null;
    }
    function blockOf(node) {
        if (!node)
            return null;
        const el = node.nodeType === 1 ? node : node.parentElement;
        if (!el)
            return null;
        const b = el.closest('li,blockquote,h1,h2,h3,p,div');
        return (b && editor.contains(b)) ? b : null;
    }
    function ensureCaretVisible(el) {
        if (!el.textContent && !el.querySelector('br')) {
            el.appendChild(document.createElement('br'));
        }
    }
    function makeTaskLi(checked, labelHtml) {
        const li = document.createElement('li');
        li.className = 'qn-task';
        li.dataset.checked = checked ? '1' : '0';
        const box = document.createElement('span');
        box.className = 'qn-task-box';
        box.setAttribute('contenteditable', 'false');
        const label = document.createElement('span');
        label.className = 'qn-task-label';
        label.innerHTML = labelHtml || '<br>';
        li.appendChild(box);
        li.appendChild(label);
        return li;
    }
    function stripLeadingText(block, count) {
        let remaining = count;
        const it = document.createNodeIterator(block, NodeFilter.SHOW_TEXT);
        let t;
        while ((t = it.nextNode()) && remaining > 0) {
            if (remaining >= t.length) {
                t.remove();
                remaining -= t.length;
            }
            else {
                t.data = (t.textContent || '').slice(remaining);
                remaining = 0;
            }
        }
    }
    /** Replace a block with a freshly-built element, handling li→list unwrap. */
    function replaceBlockWith(block, newEl) {
        const parent = block.parentElement;
        if (block.tagName === 'LI' && parent && (parent.tagName === 'UL' || parent.tagName === 'OL')) {
            if (parent.children.length === 1) {
                parent.replaceWith(newEl);
            }
            else {
                parent.after(newEl);
                block.remove();
                if (!parent.children.length)
                    parent.remove();
            }
        }
        else {
            block.replaceWith(newEl);
        }
    }
    // ----------------------------------------------------------
    // Markdown input shortcuts — only at line start.
    // Checked on keydown before the space is inserted: the text
    // "so far on this line" must be exactly one of the patterns.
    // ----------------------------------------------------------
    const MD_PATTERNS = ['#', '-', '*', '->', '>', '[ ]', '[]', '1.'];
    function markdownKeydown(e) {
        const sel = window.getSelection();
        if (!sel || !sel.isCollapsed)
            return false;
        const block = blockOf(sel.anchorNode);
        if (!block || block === editor)
            return false; // only inside real blocks
        const r0 = sel.getRangeAt(0);
        const upto = document.createRange();
        upto.selectNodeContents(block);
        upto.setEnd(r0.startContainer, r0.startOffset);
        const prefix = upto.toString().split('\n').pop() || '';
        const pat = MD_PATTERNS.find(p => prefix === p);
        if (!pat)
            return false;
        e.preventDefault();
        const kind = pat === '#' ? 'h1'
            : (pat === '-' || pat === '*' || pat === '->') ? 'ul'
                : pat === '>' ? 'quote'
                    : (pat === '[]' || pat === '[ ]') ? 'task'
                        : 'ol';
        const after = document.createRange();
        after.selectNodeContents(block);
        after.setStart(r0.startContainer, r0.startOffset);
        const suffixHtml = after.cloneContents();
        edit(() => {
            stripLeadingText(block, pat.length);
            let newEl;
            let caretEl;
            if (kind === 'h1') {
                newEl = document.createElement('h1');
                newEl.appendChild(suffixHtml);
                caretEl = newEl;
            }
            else if (kind === 'quote') {
                newEl = document.createElement('blockquote');
                const p = document.createElement('p');
                p.appendChild(suffixHtml);
                newEl.appendChild(p);
                caretEl = p;
            }
            else if (kind === 'task') {
                newEl = document.createElement('ul');
                const li = makeTaskLi(false, '');
                const label = li.querySelector('.qn-task-label');
                label.innerHTML = '';
                label.appendChild(suffixHtml);
                ensureCaretVisible(label);
                newEl.appendChild(li);
                caretEl = label;
            }
            else if (kind === 'ol') {
                newEl = document.createElement('ol');
                const li = document.createElement('li');
                li.appendChild(suffixHtml);
                ensureCaretVisible(li);
                newEl.appendChild(li);
                caretEl = li;
            }
            else {
                newEl = document.createElement('ul');
                const li = document.createElement('li');
                li.appendChild(suffixHtml);
                ensureCaretVisible(li);
                newEl.appendChild(li);
                caretEl = li;
            }
            replaceBlockWith(block, newEl);
            editor.focus();
            caretAtStart(caretEl);
            ensureCaretVisible(caretEl);
        });
        emitChange();
        refreshToolbar();
        return true;
    }
    // ----------------------------------------------------------
    // List behaviors: Enter continuation, exit, Escape
    // ----------------------------------------------------------
    function isLiEmpty(li) {
        return li.textContent.trim() === '' && !li.querySelector('blockquote,hr,ul,ol');
    }
    function enterInList(e) {
        const sel = window.getSelection();
        if (!sel || !sel.isCollapsed)
            return false;
        const li = liOf(sel.anchorNode);
        if (!li)
            return false;
        e.preventDefault();
        if (isLiEmpty(li)) {
            edit(() => {
                const p = document.createElement('p');
                p.innerHTML = '<br>';
                const ul = li.parentElement;
                if (ul && (ul.tagName === 'UL' || ul.tagName === 'OL')) {
                    if (ul.children.length === 1)
                        ul.replaceWith(p);
                    else {
                        ul.after(p);
                        li.remove();
                        if (!ul.children.length)
                            ul.remove();
                    }
                }
                else {
                    li.replaceWith(p);
                }
                editor.focus();
                caretAtStart(p);
            });
            emitChange();
            refreshToolbar();
            return true;
        }
        edit(() => {
            const r = sel.getRangeAt(0);
            const after = document.createRange();
            after.selectNodeContents(li);
            after.setStart(r.startContainer, r.startOffset);
            const frag = after.cloneContents();
            after.deleteContents();
            const newLi = document.createElement('li');
            if (li.classList.contains('qn-task')) {
                newLi.className = 'qn-task';
                newLi.dataset.checked = '0';
                const box = document.createElement('span');
                box.className = 'qn-task-box';
                box.setAttribute('contenteditable', 'false');
                const label = document.createElement('span');
                label.className = 'qn-task-label';
                newLi.appendChild(box);
                newLi.appendChild(label);
                label.appendChild(frag);
                ensureCaretVisible(label);
            }
            else {
                newLi.appendChild(frag);
                ensureCaretVisible(newLi);
            }
            li.after(newLi);
            editor.focus();
            caretAtStart(newLi);
        });
        emitChange();
        refreshToolbar();
        return true;
    }
    function escapeInList(e) {
        const sel = window.getSelection();
        if (!sel)
            return false;
        const li = liOf(sel.anchorNode);
        if (!li || !isLiEmpty(li))
            return false;
        e.preventDefault();
        edit(() => {
            const ul = li.parentElement;
            if (ul && (ul.tagName === 'UL' || ul.tagName === 'OL')) {
                li.remove();
                if (!ul.children.length)
                    ul.remove();
            }
            else {
                li.remove();
            }
            editor.focus();
        });
        emitChange();
        refreshToolbar();
        return true;
    }
    // ----------------------------------------------------------
    // Toolbar actions
    // ----------------------------------------------------------
    function withSel(fn) {
        editor.focus();
        edit(fn);
        emitChange();
        refreshToolbar();
    }
    function toggleBlock(kind) {
        const sel = getSel();
        if (!sel)
            return;
        const block = blockOf(sel.anchorNode);
        const parent = block ? block.parentElement : null;
        const inUl = !!(block && block.tagName === 'LI' && parent && parent.tagName === 'UL');
        const inOl = !!(block && block.tagName === 'LI' && parent && parent.tagName === 'OL');
        const inTask = inUl && !!block.classList.contains('qn-task');
        const inQuote = !!block && block.tagName === 'BLOCKQUOTE';
        const fmt = (tag) => withSel(() => document.execCommand('formatBlock', false, tag));
        if (kind === 'h1') {
            if (block && block.tagName === 'H1')
                fmt('p');
            else
                fmt('h1');
            return;
        }
        if (kind === 'h2') {
            if (block && block.tagName === 'H2')
                fmt('p');
            else
                fmt('h2');
            return;
        }
        if (kind === 'h3') {
            if (block && block.tagName === 'H3')
                fmt('p');
            else
                fmt('h3');
            return;
        }
        if (kind === 'quote') {
            if (inQuote)
                fmt('p');
            else
                fmt('blockquote');
            return;
        }
        if (kind === 'task') {
            if (inTask) {
                withSel(() => {
                    const ul = parent;
                    for (const item of Array.from(ul.querySelectorAll(':scope > li'))) {
                        const liE = item;
                        const label = liE.querySelector('.qn-task-label');
                        if (label) {
                            const frag = document.createDocumentFragment();
                            while (label.firstChild)
                                frag.appendChild(label.firstChild);
                            liE.innerHTML = '';
                            liE.appendChild(frag);
                            liE.classList.remove('qn-task');
                            delete liE.dataset.checked;
                        }
                    }
                });
                return;
            }
            withSel(() => {
                if (inUl) {
                    const ul = parent;
                    for (const item of Array.from(ul.querySelectorAll(':scope > li'))) {
                        const liE = item;
                        if (!liE.classList.contains('qn-task')) {
                            liE.replaceWith(makeTaskLi(false, liE.innerHTML));
                        }
                    }
                }
                else if (block && block !== editor) {
                    const content = block.innerHTML;
                    const newUl = document.createElement('ul');
                    const liE = makeTaskLi(false, content);
                    newUl.appendChild(liE);
                    replaceBlockWith(block, newUl);
                    editor.focus();
                    caretAtStart(liE.querySelector('.qn-task-label'));
                }
                else {
                    const newUl = document.createElement('ul');
                    const liE = makeTaskLi(false, '');
                    newUl.appendChild(liE);
                    editor.appendChild(newUl);
                    editor.focus();
                    caretAtStart(liE.querySelector('.qn-task-label'));
                }
            });
            return;
        }
        if (kind === 'ul' && inUl && !inTask) {
            withSel(unlist);
            return;
        }
        if (kind === 'ol' && inOl) {
            withSel(unlist);
            return;
        }
        withSel(() => document.execCommand(kind === 'ul' ? 'insertUnorderedList' : 'insertOrderedList', false));
    }
    function unlist() {
        const sel = window.getSelection();
        if (!sel)
            return;
        const li = liOf(sel.anchorNode);
        if (!li)
            return;
        const ul = li.parentElement;
        if (!ul || (ul.tagName !== 'UL' && ul.tagName !== 'OL'))
            return;
        const items = Array.from(ul.children);
        const ps = items.map(item => {
            const p = document.createElement('p');
            const frag = document.createDocumentFragment();
            while (item.firstChild)
                frag.appendChild(item.firstChild);
            p.appendChild(frag);
            ensureCaretVisible(p);
            return p;
        });
        ul.replaceWith(...ps);
        editor.focus();
        caretAtStart(ps[0] || editor);
    }
    function toggleInlineCode() {
        const sel = getSel();
        if (!sel)
            return;
        withSel(() => {
            const s = window.getSelection();
            if (!s || !s.rangeCount)
                return;
            const anchor = s.anchorNode;
            const codeEl = anchor
                ? (anchor.nodeType === 1 ? anchor.closest('code') : anchor.parentElement?.closest('code'))
                : null;
            if (codeEl) {
                const code = codeEl;
                const frag = document.createDocumentFragment();
                while (code.firstChild)
                    frag.appendChild(code.firstChild);
                code.replaceWith(frag);
            }
            else if (s.toString().length > 0) {
                const r = s.getRangeAt(0);
                const code = document.createElement('code');
                try {
                    r.surroundContents(code);
                }
                catch {
                    const text = s.toString();
                    r.deleteContents();
                    code.textContent = text;
                    r.insertNode(code);
                }
            }
        });
    }
    function insertHr() {
        withSel(() => {
            const sel = window.getSelection();
            const block = sel ? blockOf(sel.anchorNode) : null;
            const hr = document.createElement('hr');
            const p = document.createElement('p');
            p.innerHTML = '<br>';
            if (block && block !== editor) {
                block.after(hr);
                hr.after(p);
            }
            else {
                editor.appendChild(hr);
                editor.appendChild(p);
            }
            editor.focus();
            caretAtStart(p);
        });
    }
    // ----------------------------------------------------------
    // Paste — sanitize everything that arrives, then insert at
    // block level (splitting the anchor block when needed).
    // ----------------------------------------------------------
    function onPaste(e) {
        e.preventDefault();
        const cd = e.clipboardData;
        if (!cd)
            return;
        const sel = window.getSelection();
        if (!sel || !editor.contains(sel.anchorNode)) {
            editor.focus();
            return;
        }
        const html = cd.getData('text/html');
        const text = cd.getData('text/plain');
        let insertHtml = '';
        if (html && /<[a-z][\s\S]*>/i.test(html)) {
            const cleaned = (0, text_1.sanitizeHtml)(html);
            if (cleaned.replace(/<br\s*\/?>/gi, ' ').replace(/<[^>]+>/g, '').trim())
                insertHtml = cleaned;
        }
        const s = window.getSelection();
        const range = s && s.rangeCount ? s.getRangeAt(0) : null;
        if (!range) {
            editor.focus();
            return;
        }
        edit(() => {
            if (insertHtml) {
                const tmp = document.createElement('div');
                tmp.innerHTML = insertHtml;
                const lastChild = tmp.lastElementChild;
                if (lastChild && /^(P|DIV)$/.test(lastChild.tagName) && !lastChild.textContent.trim())
                    lastChild.remove();
                const kids = Array.from(tmp.children);
                const hasBlocks = kids.some(k => /^(P|DIV|UL|OL|H1|H2|H3|BLOCKQUOTE|HR)$/.test(k.tagName));
                if (!hasBlocks) {
                    // plain inline content → insert at the caret
                    const frag = document.createDocumentFragment();
                    while (tmp.firstChild)
                        frag.appendChild(tmp.firstChild);
                    range.deleteContents();
                    range.insertNode(frag);
                    editor.focus();
                    const blk = blockOf(range.startContainer);
                    caretAtEnd(blk && blk !== editor ? blk : editor);
                    return;
                }
                // block content → insert at block level
                const anchor = s ? s.anchorNode : null;
                const B = anchor ? blockOf(anchor) : null;
                let target = null;
                if (B && B !== editor && /^(P|DIV)$/.test(B.tagName)) {
                    const before = document.createRange();
                    before.selectNodeContents(B);
                    before.setEnd(range.startContainer, range.startOffset);
                    const after = document.createRange();
                    after.selectNodeContents(B);
                    after.setStart(range.endContainer, range.endOffset);
                    const sequence = [];
                    if (before.toString().trim()) {
                        const pb = document.createElement('p');
                        pb.appendChild(before.cloneContents());
                        sequence.push(pb);
                    }
                    sequence.push(...kids);
                    const ph = document.createElement('p');
                    ph.innerHTML = '<br>';
                    sequence.push(ph);
                    if (after.toString().trim()) {
                        const pa = document.createElement('p');
                        pa.appendChild(after.cloneContents());
                        sequence.push(pa);
                    }
                    B.replaceWith(...sequence);
                    target = kids[kids.length - 1] || ph;
                }
                else if (B && B !== editor) {
                    B.after(...kids);
                    target = kids[kids.length - 1];
                }
                else {
                    const off = caretOffset(editor) || 0;
                    if (off === 0 && editor.firstElementChild) {
                        editor.firstElementChild.before(...kids);
                    }
                    else {
                        editor.append(...kids);
                    }
                    target = kids[kids.length - 1];
                }
                editor.focus();
                const r2 = document.createRange();
                r2.setStartAfter(target);
                r2.collapse(true);
                const sel2 = window.getSelection();
                if (sel2) {
                    sel2.removeAllRanges();
                    sel2.addRange(r2);
                }
            }
            else {
                const t = (text || '').replace(/\r\n/g, '\n');
                document.execCommand('insertText', false, t);
            }
        });
        emitChange();
        refreshToolbar();
    }
    // ----------------------------------------------------------
    // Toolbar state reflection
    // ----------------------------------------------------------
    const toolbar = host.closest('.pane-editor')?.querySelector('.ed-toolbar');
    function refreshToolbar() {
        if (!toolbar)
            return;
        const sel = getSel();
        const inEditor = !!sel && !!sel.anchorNode && editor.contains(sel.anchorNode);
        toolbar.classList.toggle('disabled', !inEditor);
        const block = sel && sel.anchorNode ? blockOf(sel.anchorNode) : null;
        const anchorEl = sel && sel.anchorNode
            ? (sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement)
            : null;
        const inTag = (t) => !!(anchorEl && editor.contains(anchorEl) && anchorEl.closest(t));
        const set = (id, on) => {
            const b = toolbar.querySelector(`[data-cmd="${id}"]`);
            if (b)
                b.classList.toggle('active', on);
        };
        set('bold', inEditor && inTag('b,strong'));
        set('italic', inEditor && inTag('i,em'));
        set('underline', inEditor && inTag('u'));
        set('strike', inEditor && inTag('s,del'));
        set('code', inEditor && inTag('code'));
        set('h1', !!block && block.tagName === 'H1');
        set('h2', !!block && block.tagName === 'H2');
        set('h3', !!block && block.tagName === 'H3');
        const inUl = !!(block && block.tagName === 'LI' && block.parentElement && block.parentElement.tagName === 'UL');
        const inOl = !!(block && block.tagName === 'LI' && block.parentElement && block.parentElement.tagName === 'OL');
        const inTask = inUl && block.classList.contains('qn-task');
        set('ul', inUl && !inTask);
        set('ol', inOl);
        set('task', inTask);
        set('quote', !!block && block.tagName === 'BLOCKQUOTE');
    }
    // ----------------------------------------------------------
    // Tags row
    // ----------------------------------------------------------
    function renderTags() {
        if (!note) {
            tagsEl.innerHTML = '';
            return;
        }
        tagsEl.innerHTML = '';
        for (const t of note.tags) {
            const chip = document.createElement('span');
            chip.className = 'tag-chip';
            chip.innerHTML = `<span class="tag-name">#${(0, text_1.escapeHtml)(t)}</span><button class="tag-x" title="Remove tag">×</button>`;
            chip.querySelector('.tag-x').addEventListener('click', ev => {
                ev.stopPropagation();
                if (!note)
                    return;
                state_1.actions.setNoteTags(state, note.id, note.tags.filter(x => x !== t));
                renderTags();
                onChange();
            });
            // clicking the chip (not ×) filters the note list by this tag
            chip.addEventListener('click', () => {
                state_1.actions.setView(state, { kind: 'tag', id: t });
                onChange();
            });
            tagsEl.appendChild(chip);
        }
        const input = document.createElement('input');
        input.className = 'tag-input';
        input.placeholder = 'add tag…';
        input.spellcheck = false;
        input.addEventListener('keydown', e => {
            if (!note)
                return;
            if (e.key === 'Enter' || e.key === ',') {
                e.preventDefault();
                const v = input.value.trim().replace(/^#/, '');
                if (v && !note.tags.includes(v)) {
                    state_1.actions.setNoteTags(state, note.id, [...note.tags, v]);
                    renderTags();
                    onChange();
                }
                input.value = '';
            }
            else if (e.key === 'Backspace' && input.value === '' && note.tags.length) {
                state_1.actions.setNoteTags(state, note.id, note.tags.slice(0, -1));
                renderTags();
                onChange();
            }
        });
        input.addEventListener('blur', () => {
            if (!note)
                return;
            const v = input.value.trim().replace(/^#/, '');
            if (v && !note.tags.includes(v)) {
                state_1.actions.setNoteTags(state, note.id, [...note.tags, v]);
                onChange();
            }
            renderTags();
        });
        tagsEl.appendChild(input);
    }
    function syncTitleEmpty() {
        titleEl.classList.toggle('qn-empty-title', titleEl.textContent.trim() === '');
    }
    // ----------------------------------------------------------
    // In-place sanitizer: strip anything the input events let in.
    // ----------------------------------------------------------
    function sanitizeInPlace() {
        for (const el of Array.from(editor.querySelectorAll('*'))) {
            const tag = el.tagName.toLowerCase();
            if (!/^(p|div|br|b|strong|i|em|u|s|del|code|h1|h2|h3|ul|ol|li|blockquote|hr|span)$/.test(tag)) {
                const frag = document.createDocumentFragment();
                while (el.firstChild)
                    frag.appendChild(el.firstChild);
                el.replaceWith(frag);
            }
            else {
                for (const attr of Array.from(el.attributes)) {
                    const n = attr.name.toLowerCase();
                    const ok = (n === 'class' && /^(qn-[\w-]+|qn-task( checked)?|qn-title( empty)?)$/.test(attr.value)) ||
                        n === 'data-checked' ||
                        (n === 'data-tag' && !attr.value.includes('"') && !attr.value.includes(' ') && attr.value.length <= 60);
                    if (!ok)
                        el.removeAttribute(attr.name);
                }
            }
        }
    }
    // ----------------------------------------------------------
    // Wire events
    // ----------------------------------------------------------
    function onEditorInput() {
        if (suppressInput)
            return;
        // first input of a new burst: its pre-state is lastPost (the post-state
        // of the previous committed change)
        const now = Date.now();
        if (now - lastBurst > BURST_MS) {
            commitPending();
            pending = lastPost || serialize();
            lastBurst = now;
        }
        else if (pending == null) {
            pending = lastPost || serialize(); // burst continuing after a structural edit
            lastBurst = now;
        }
        sanitizeInPlace();
        emitChange();
        lastPost = serialize();
        refreshToolbar();
    }
    function onTitleInput() {
        if (suppressInput)
            return;
        const now = Date.now();
        if (now - lastBurst > BURST_MS) {
            commitPending();
            pending = lastPost || serialize();
            lastBurst = now;
        }
        else if (pending == null) {
            pending = lastPost || serialize();
            lastBurst = now;
        }
        if (!note)
            return;
        state_1.actions.updateTitle(state, note.id, titleEl.textContent || '');
        syncTitleEmpty();
        lastPost = serialize();
        onChange();
    }
    function onKeyDown(e) {
        if (titleEl.contains(e.target)) {
            if (e.key === 'Enter') {
                e.preventDefault();
                editor.focus();
                caretAtEnd(editor);
            }
            return;
        }
        if ((e.metaKey || e.ctrlKey) && (e.key === 'z' || e.key === 'Z' || e.key === 'y')) {
            e.preventDefault();
            if (e.shiftKey)
                doRedo();
            else
                doUndo();
            return;
        }
        if (e.key === 'Escape') {
            if (escapeInList(e))
                return;
            e.target.blur();
            return;
        }
        if (e.key === 'Enter' && !e.shiftKey) {
            if (enterInList(e))
                return;
            return;
        }
        if (e.key === ' ' && !e.metaKey && !e.ctrlKey && !e.altKey) {
            if (markdownKeydown(e))
                return;
            return;
        }
    }
    function onTaskClick(e) {
        const box = e.target.closest?.('.qn-task-box');
        if (!box)
            return;
        e.preventDefault();
        const li = box.closest('li');
        if (!li)
            return;
        edit(() => {
            li.dataset.checked = li.dataset.checked === '1' ? '0' : '1';
        });
        emitChange();
        refreshToolbar();
    }
    function onSelectionChange() {
        refreshToolbar();
    }
    editor.addEventListener('input', onEditorInput);
    titleEl.addEventListener('input', onTitleInput);
    editor.addEventListener('keydown', onKeyDown);
    editor.addEventListener('paste', onPaste);
    editor.addEventListener('click', onTaskClick);
    document.addEventListener('selectionchange', onSelectionChange);
    // ----------------------------------------------------------
    // Public API
    // ----------------------------------------------------------
    function selectNote(n) {
        note = n;
        history = [];
        redoStack = [];
        pending = null;
        lastBurst = 0;
        suppressInput = true;
        if (!n) {
            titleEl.textContent = '';
            editor.innerHTML = '<p><br></p>';
        }
        else {
            titleEl.textContent = n.title;
            editor.innerHTML = n.content || '<p><br></p>';
        }
        suppressInput = false;
        syncTitleEmpty();
        renderTags();
        refreshToolbar();
        history.push(serialize());
        lastPost = serialize();
    }
    function syncNote(n) {
        if (n && note && n.id === note.id) {
            note = n;
            renderTags(); // tag chips may have changed in the model
        }
        else {
            selectNote(n);
        }
    }
    function command(cmd) {
        switch (cmd) {
            case 'bold':
                withSel(() => document.execCommand('bold'));
                break;
            case 'italic':
                withSel(() => document.execCommand('italic'));
                break;
            case 'underline':
                withSel(() => document.execCommand('underline'));
                break;
            case 'strike':
                withSel(() => document.execCommand('strikeThrough'));
                break;
            case 'code':
                toggleInlineCode();
                break;
            case 'h1':
            case 'h2':
            case 'h3':
            case 'ul':
            case 'ol':
            case 'task':
            case 'quote':
                toggleBlock(cmd);
                break;
            case 'hr':
                insertHr();
                break;
        }
    }
    return {
        root: host,
        selectNote,
        refreshToolbar,
        focusBody: () => { editor.focus(); caretAtEnd(editor); },
        getHtml: () => editor.innerHTML,
        command,
        syncNote,
        dispose: () => {
            document.removeEventListener('selectionchange', onSelectionChange);
            host.innerHTML = '';
        },
    };
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// UI — the three panes, search overlay, context menus,
// drag-and-drop, resize handles, and the "Saved" indicator.
// The DOM is a projection of State; render() reconciles it.
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.initUi = initUi;
exports.updateSavedEl = updateSavedEl;
exports.applyTheme = applyTheme;
const constants_1 = require("./constants");
const state_1 = require("./state");
const text_1 = require("./text");
let els = {};
let state;
let onChangeCb;
let noteRefs = new Map();
function initUi(s, refs, onChange) {
    state = s;
    onChangeCb = onChange;
    els = {
        app: document.getElementById('app'),
        sidebar: refs.sidebar,
        list: refs.list,
        listHead: document.getElementById('list-head'),
        listBody: document.getElementById('list-body'),
        listFoot: document.getElementById('list-foot'),
        editorHost: refs.editorHost,
        overlay: refs.overlay,
        saved: refs.saved,
    };
    buildStaticDom();
    wireGlobalEvents();
    return { render: () => renderAll(), openSearch };
}
// ------------------------------------------------------------------
// Static scaffolding built once (search overlay, side nav groups)
// ------------------------------------------------------------------
function buildStaticDom() {
    els.sidebar.innerHTML = `
    <div class="side-group">
      <div class="side-label">Notebooks</div>
      <div class="side-rows">
        <div class="side-row view-row" data-view="all">
          <svg class="ic" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
          <span class="row-name">All Notes</span>
          <span class="row-count"></span>
        </div>
        <div class="side-row view-row" data-view="trash">
          <svg class="ic" viewBox="0 0 24 24"><path d="M5 7h14M9 7V5h6v2M7 7l1 12h8l1-12"/></svg>
          <span class="row-name">Trash</span>
          <span class="row-count"></span>
        </div>
      </div>
    </div>
    <div class="side-group grow">
      <div class="side-label">
        <span class="lbl-text">Notebooks</span>
        <button class="icon-btn tiny" id="nb-add" title="New notebook">+</button>
      </div>
      <div class="side-rows" id="side-nbs"></div>
    </div>
    <div class="side-group">
      <div class="side-label">Tags</div>
      <div class="side-rows" id="side-tags-rows"></div>
    </div>
    <div class="side-foot" id="side-foot">
      <button class="icon-btn tiny" id="theme-btn" title="Toggle theme">◐</button>
      <span class="side-foot-note">Notes stay on this device</span>
    </div>`;
    // search overlay
    els.overlay.innerHTML = `
    <div class="search-panel">
      <div class="search-input-row">
        <svg class="ic" viewBox="0 0 24 24"><circle cx="10" cy="10" r="6"/><path d="M15 15l6 6"/></svg>
        <input class="search-input" id="search-input" placeholder="Search notes…" spellcheck="false" />
        <kbd>esc</kbd>
      </div>
      <div class="search-results" id="search-results"></div>
    </div>`;
    const input = els.overlay.querySelector('#search-input');
    input.addEventListener('input', () => renderSearch(input.value));
    input.addEventListener('keydown', e => {
        if (e.key === 'Escape') {
            closeSearch();
        }
        else if (e.key === 'Enter') {
            const hits = input.__hits;
            if (hits && hits.length) {
                state_1.actions.selectNote(state, hits[0].note.id);
                if (state.view.kind === 'trash')
                    state.view = { kind: 'all', id: null };
                closeSearch();
                renderAll();
            }
        }
    });
    els.overlay.addEventListener('mousedown', e => {
        if (e.target === els.overlay)
            closeSearch();
    });
    // virtual views: All Notes + Trash
    const allRow = els.sidebar.querySelector('[data-view="all"]');
    const trashRowEl = els.sidebar.querySelector('[data-view="trash"]');
    allRow.addEventListener('click', () => {
        state_1.actions.setView(state, { kind: 'all', id: null });
        renderAll();
    });
    trashRowEl.addEventListener('click', () => {
        state_1.actions.setView(state, { kind: 'trash', id: null });
        renderAll();
    });
    // dropping a note on "All Notes" unfles it from its notebook
    allRow.addEventListener('dragover', e => {
        e.preventDefault();
        allRow.classList.add('drop-hint');
    });
    allRow.addEventListener('dragleave', () => allRow.classList.remove('drop-hint'));
    allRow.addEventListener('drop', e => {
        e.preventDefault();
        allRow.classList.remove('drop-hint');
        const id = e.dataTransfer.getData('text/qn-note');
        if (id) {
            state_1.actions.moveNote(state, id, null);
            renderAll();
        }
    });
}
function closeSearch() {
    els.overlay.classList.remove('open');
    els.overlay.querySelector('#search-input').value = '';
}
// ------------------------------------------------------------------
// Saved indicator
// ------------------------------------------------------------------
function updateSavedEl(s) {
    const el = els.saved;
    if (!el)
        return;
    if (s.dirty) {
        el.textContent = 'Saving…';
        el.classList.add('show');
    }
    else {
        el.textContent = 'Saved ' + (0, text_1.fmtDate)(s.lastSaved);
        el.classList.add('show');
        el.classList.add('flash');
        setTimeout(() => el.classList.remove('flash'), 500);
    }
}
// ------------------------------------------------------------------
// Sidebar rendering
// ------------------------------------------------------------------
function viewActive(kind, id) {
    return state.view.kind === kind && state.view.id === id;
}
function rowEl(active, title) {
    const row = document.createElement('div');
    row.className = 'side-row' + (active ? ' active' : '');
    row.title = title;
    return row;
}
function renderSidebar() {
    const appAll = els.sidebar.querySelector('[data-view="all"]');
    const trashRow = els.sidebar.querySelector('[data-view="trash"]');
    appAll.classList.toggle('active', viewActive('all', null));
    trashRow.classList.toggle('active', viewActive('trash', null));
    appAll.querySelector('.row-count').textContent =
        String(state.notes.filter(n => !n.deleted).length);
    trashRow.querySelector('.row-count').textContent =
        String(state.notes.filter(n => n.deleted).length);
    // notebooks
    const nbHost = els.sidebar.querySelector('#side-nbs');
    nbHost.innerHTML = '';
    for (const nb of state.notebooks) {
        const collapsed = !!state.collapsed[nb.id];
        const count = state.notes.filter(n => !n.deleted && n.notebookId === nb.id).length;
        const active = viewActive('notebook', nb.id);
        const row = rowEl(active, nb.name);
        row.innerHTML = `
      <button class="icon-btn tiny chev" title="${collapsed ? 'Expand' : 'Collapse'}">${collapsed ? '›' : '⌄'}</button>
      <svg class="ic" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M8 4v16"/></svg>
      <span class="row-name">${(0, text_1.escapeHtml)(nb.name)}</span>
      <span class="row-count">${count}</span>
      <button class="icon-btn tiny nb-menu" title="Notebook options">⋯</button>`;
        row.addEventListener('click', e => {
            const t = e.target;
            if (t.closest('.chev')) {
                state_1.actions.toggleCollapse(state, nb.id);
                renderAll();
                return;
            }
            if (t.closest('.nb-menu')) {
                openMenu(e, [
                    { label: 'Rename…', fn: () => renameNotebookPrompt(nb) },
                    { label: 'Delete', danger: true, fn: () => deleteNotebookPrompt(nb) },
                ]);
                return;
            }
            state_1.actions.setView(state, { kind: 'notebook', id: nb.id });
            renderAll();
        });
        row.addEventListener('dragover', e => {
            if (e.dataTransfer)
                e.dataTransfer.dropEffect = 'move';
            row.classList.add('drop-hint');
        });
        row.addEventListener('dragleave', () => row.classList.remove('drop-hint'));
        row.addEventListener('drop', e => {
            row.classList.remove('drop-hint');
            const id = e.dataTransfer.getData('text/qn-note');
            if (id) {
                state_1.actions.moveNote(state, id, nb.id);
                renderAll();
            }
        });
        nbHost.appendChild(row);
        if (!collapsed) {
            const notes = visibleNotesOfNotebook(nb.id);
            const wrap = document.createElement('div');
            wrap.className = 'nb-notes';
            if (notes.length === 0) {
                wrap.innerHTML = `<div class="nb-empty">No notes — drag one here or add below.</div>`;
            }
            else {
                for (const n of notes) {
                    const i = document.createElement('div');
                    i.className = 'nb-note' + (state.selectedNoteId === n.id ? ' sel' : '');
                    i.textContent = n.title || 'Untitled';
                    i.addEventListener('click', () => { state_1.actions.selectNote(state, n.id); renderAll(); });
                    wrap.appendChild(i);
                }
            }
            const add = document.createElement('button');
            add.className = 'nb-add-note';
            add.innerHTML = `+ New note`;
            add.addEventListener('click', () => { state_1.actions.createNote(state, nb.id); renderAll(); });
            wrap.appendChild(add);
            nbHost.appendChild(wrap);
        }
    }
    // tags
    const tagHost = els.sidebar.querySelector('#side-tags-rows');
    tagHost.innerHTML = '';
    const tags = (0, state_1.allTags)(state);
    if (!tags.length) {
        tagHost.innerHTML = `<div class="nb-empty">Tags you add appear here.</div>`;
    }
    for (const t of tags) {
        const count = state.notes.filter(n => !n.deleted && n.tags.includes(t)).length;
        const row = rowEl(viewActive('tag', t), '#' + t);
        row.innerHTML = `
      <span class="tag-dot"></span>
      <span class="row-name">${(0, text_1.escapeHtml)(t)}</span>
      <span class="row-count">${count}</span>`;
        row.addEventListener('click', () => {
            state_1.actions.setView(state, { kind: 'tag', id: t });
            renderAll();
        });
        tagHost.appendChild(row);
    }
    // collapse class
    els.app.classList.toggle('side-collapsed', state.layout.sidebarCollapsed);
}
function visibleNotesOfNotebook(nbId) {
    return state.notes
        .filter(n => !n.deleted && n.notebookId === nbId)
        .sort((a, b) => b.updatedAt - a.updatedAt);
}
// ------------------------------------------------------------------
// Note list pane
// ------------------------------------------------------------------
function viewTitle() {
    switch (state.view.kind) {
        case 'all': return 'All Notes';
        case 'trash': return 'Trash';
        case 'tag': return '#' + state.view.id;
        default: return (0, state_1.getNotebook)(state, state.view.id)?.name || 'Notes';
    }
}
function renderList() {
    els.listHead.innerHTML = `
    <div class="lh-row1">
      <span class="lh-title">${(0, text_1.escapeHtml)(viewTitle())}</span>
      <div class="lh-actions">
        <button class="icon-btn tiny" id="sort-btn" title="Sort order">⇅</button>
        <button class="icon-btn tiny" id="list-add" title="New note">+</button>
      </div>
    </div>
    <div class="lh-row2">
      <span class="lh-count">${(0, state_1.visibleNotes)(state).length} note${(0, state_1.visibleNotes)(state).length === 1 ? '' : 's'}</span>
    </div>`;
    els.listHead.querySelector('#sort-btn').addEventListener('click', e => {
        const order = ['updated', 'created', 'alpha'];
        const labels = { updated: 'Last modified', created: 'Date created', alpha: 'Alphabetical' };
        openMenu(e, order.map(o => ({
            label: labels[o],
            active: state.sortBy === o,
            fn: () => { state_1.actions.setSort(state, o); renderAll(); },
        })));
    });
    els.listHead.querySelector('#list-add').addEventListener('click', () => {
        const nb = state.view.kind === 'notebook' ? state.view.id : state.view.kind === 'tag' ? null : (state.view.kind === 'all' ? null : null);
        state_1.actions.createNote(state, nb);
        renderAll();
    });
    const notes = (0, state_1.visibleNotes)(state);
    els.listBody.innerHTML = '';
    noteRefs.clear();
    for (const n of notes) {
        const card = document.createElement('div');
        card.className = 'note-card' + (state.selectedNoteId === n.id ? ' sel' : '') + (n.pinned ? ' pinned' : '');
        card.draggable = true;
        const title = document.createElement('div');
        title.className = 'nc-title';
        title.innerHTML = (n.pinned ? '<span class="pin-dot" title="Pinned"></span>' : '') +
            (0, text_1.escapeHtml)(n.title || 'Untitled');
        const snip = document.createElement('div');
        snip.className = 'nc-snippet';
        snip.textContent = (0, text_1.snippet)(n.content, constants_1.C.SNIPPET_LEN) || (state.view.kind === 'trash' ? 'Empty note' : 'No additional text');
        const meta = document.createElement('div');
        meta.className = 'nc-meta';
        const tagHtml = n.tags.slice(0, 3).map(t => `<span class="nc-tag">${(0, text_1.escapeHtml)(t)}</span>`).join('');
        meta.innerHTML = `<span class="nc-date">${(0, text_1.fmtDate)(n.updatedAt)}</span>${tagHtml}`;
        card.appendChild(title);
        card.appendChild(snip);
        card.appendChild(meta);
        noteRefs.set(n.id, { title, snip });
        card.addEventListener('click', () => { state_1.actions.selectNote(state, n.id); renderAll(); });
        card.addEventListener('contextmenu', e => { e.preventDefault(); openNoteMenu(e, n); });
        card.addEventListener('dragstart', e => {
            e.dataTransfer.setData('text/qn-note', n.id);
            e.dataTransfer.effectAllowed = 'move';
        });
        els.listBody.appendChild(card);
    }
    if (!notes.length) {
        els.listBody.innerHTML = state.view.kind === 'trash'
            ? `<div class="list-empty"><div class="le-ic">🗑</div><div>Trash is empty.</div></div>`
            : `<div class="list-empty"><div class="le-ic">✍</div><div>No notes here yet.</div><button class="btn ghost" id="empty-add">Create a note</button></div>`;
        const b = els.listBody.querySelector('#empty-add');
        if (b)
            b.addEventListener('click', () => {
                const nb = state.view.kind === 'notebook' ? state.view.id : null;
                state_1.actions.createNote(state, nb);
                renderAll();
            });
    }
    els.listFoot.innerHTML = state.view.kind === 'trash'
        ? `<button class="btn danger-ghost" id="purge-btn">Empty Trash</button>`
        : '';
    const purge = els.listFoot.querySelector('#purge-btn');
    if (purge)
        purge.addEventListener('click', () => {
            const trashed = state.notes.filter(n => n.deleted);
            trashed.forEach(n => state.notes.splice(state.notes.indexOf(n), 1));
            if (state.selectedNoteId) {
                const still = state.notes.find(x => x.id === state.selectedNoteId);
                if (!still)
                    state.selectedNoteId = null;
            }
            (0, state_1.flushSave)(state);
            renderAll();
        });
}
// ------------------------------------------------------------------
// Search
// ------------------------------------------------------------------
function openSearch() {
    els.overlay.classList.add('open');
    const input = els.overlay.querySelector('#search-input');
    setTimeout(() => input.focus(), 10);
    renderSearch(input.value);
}
function renderSearch(q) {
    const host = els.overlay.querySelector('#search-results');
    const hits = (0, state_1.searchNotes)(state, q);
    els.overlay.querySelector('#search-input').__hits = hits;
    if (!q.trim()) {
        host.innerHTML = `<div class="sr-empty">Type to search titles and note content…</div>`;
        return;
    }
    if (!hits.length) {
        host.innerHTML = `<div class="sr-empty">No notes match “${(0, text_1.escapeHtml)(q)}”.</div>`;
        return;
    }
    host.innerHTML = '';
    for (const h of hits) {
        const row = document.createElement('div');
        row.className = 'sr-row';
        const name = h.inTitle ? h.note.title || 'Untitled' : (h.note.title || 'Untitled');
        const ctx = h.inTitle ? (h.note.title || 'Untitled') : h.context;
        const html = (0, text_1.escapeHtml)(ctx).replace(new RegExp((0, text_1.escapeReg)((0, text_1.escapeHtml)(q)), 'gi'), m => `<mark>${m}</mark>`);
        row.innerHTML = `
      <div class="sr-name">${(0, text_1.escapeHtml)(name)}${h.inTitle ? ' <span class="sr-hit">title</span>' : ''}</div>
      <div class="sr-ctx">${html}</div>`;
        row.addEventListener('click', () => {
            state_1.actions.selectNote(state, h.note.id);
            if (state.view.kind === 'trash')
                state.view = { kind: 'all', id: null };
            closeSearch();
            renderAll();
        });
        host.appendChild(row);
    }
}
// ------------------------------------------------------------------
// Context menus
// ------------------------------------------------------------------
let menuEl = null;
function openMenu(e, items) {
    closeMenu();
    menuEl = document.createElement('div');
    menuEl.className = 'menu';
    for (const it of items) {
        const b = document.createElement('button');
        b.className = 'menu-item' + (it.danger ? ' danger' : '') + (it.active ? ' active' : '');
        b.innerHTML = (it.active ? '✓ ' : '') + (0, text_1.escapeHtml)(it.label);
        b.addEventListener('click', () => { closeMenu(); it.fn(); });
        menuEl.appendChild(b);
    }
    document.body.appendChild(menuEl);
    const rect = e.clientX !== undefined
        ? { x: e.clientX, y: e.clientY }
        : { x: window.innerWidth - 200, y: window.innerHeight - 200 };
    const mw = menuEl.offsetWidth, mh = menuEl.offsetHeight;
    menuEl.style.left = Math.min(rect.x, window.innerWidth - mw - 8) + 'px';
    menuEl.style.top = Math.min(rect.y, window.innerHeight - mh - 8) + 'px';
}
function closeMenu() {
    if (menuEl) {
        menuEl.remove();
        menuEl = null;
    }
}
function openNoteMenu(e, n) {
    const inTrash = n.deleted;
    const items = [];
    if (inTrash) {
        items.push({ label: 'Restore to notebook', fn: () => { state_1.actions.restoreNote(state, n.id); renderAll(); } });
        items.push({ label: 'Delete forever', danger: true, fn: () => { state_1.actions.destroyNote(state, n.id); renderAll(); } });
    }
    else {
        items.push({ label: n.pinned ? 'Unpin' : 'Pin', fn: () => { state_1.actions.pinNote(state, n.id); renderAll(); } });
        items.push({ label: 'Duplicate', fn: () => { state_1.actions.duplicateNote(state, n.id); renderAll(); } });
        for (const nb of state.notebooks) {
            if (nb.id !== n.notebookId) {
                items.push({ label: 'Move to ' + nb.name, fn: () => { state_1.actions.moveNote(state, n.id, nb.id); renderAll(); } });
            }
        }
        items.push({ label: 'Delete', danger: true, fn: () => { state_1.actions.deleteNote(state, n.id); renderAll(); } });
    }
    openMenu(e, items);
}
function renameNotebookPrompt(nb) {
    const name = window.prompt('Rename notebook', nb.name);
    if (name !== null && name.trim()) {
        state_1.actions.renameNotebook(state, nb.id, name);
        renderAll();
    }
}
function deleteNotebookPrompt(nb) {
    const count = state.notes.filter(n => n.notebookId === nb.id && !n.deleted).length;
    const msg = count
        ? `Delete notebook “${nb.name}”? Its ${count} note${count === 1 ? '' : 's'} will be kept in All Notes.`
        : `Delete notebook “${nb.name}”?`;
    if (window.confirm(msg)) {
        state_1.actions.deleteNotebook(state, nb.id);
        renderAll();
    }
}
// ------------------------------------------------------------------
// Global wiring: theme, new notebook, resize, shortcuts, saved
// ------------------------------------------------------------------
function wireGlobalEvents() {
    // theme
    const themeBtn = () => document.getElementById('theme-btn');
    document.getElementById('side-foot')?.addEventListener('click', e => {
        if (e.target.closest('#theme-btn')) {
            state.theme = state.theme === 'light' ? 'dark' : 'light';
            (0, state_1.scheduleSave)(state);
            applyTheme();
        }
    });
    document.addEventListener('click', e => {
        const add = document.getElementById('nb-add');
        if (add && e.target.closest('#nb-add')) {
            const name = window.prompt('New notebook name');
            if (name !== null && name.trim()) {
                state_1.actions.createNotebook(state, name);
                renderAll();
            }
        }
    });
    document.addEventListener('mousedown', e => {
        // close menus unless the press lands on the menu itself
        if (menuEl && !menuEl.contains(e.target))
            closeMenu();
    }, true);
    // resize handles
    const rs = document.getElementById('rs-sidebar');
    const rl = document.getElementById('rs-list');
    startResize(rs, () => state.layout.sidebarW, (w) => {
        if (state.layout.sidebarCollapsed) {
            state.layout.sidebarCollapsed = false;
            (0, state_1.scheduleSave)(state);
        }
        state_1.actions.setLayout(state, w);
    });
    startResize(rl, () => state.layout.listW, (w) => {
        state_1.actions.setLayout(state, undefined, w);
    });
    // collapse toggle button
    const sideToggle = document.getElementById('side-toggle');
    sideToggle.addEventListener('click', () => {
        state.layout.sidebarCollapsed = !state.layout.sidebarCollapsed;
        (0, state_1.scheduleSave)(state);
        renderAll();
    });
    // keyboard shortcuts
    document.addEventListener('keydown', e => {
        const inField = document.activeElement?.matches?.('input,textarea,[contenteditable="true"],[contenteditable=""]');
        if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
            e.preventDefault();
            openSearch();
            return;
        }
        if (inField)
            return;
        if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
            e.preventDefault();
            const nb = state.view.kind === 'notebook' ? state.view.id : null;
            state_1.actions.createNote(state, nb);
            renderAll();
        }
    });
    // beforeunload — flush pending save
    window.addEventListener('beforeunload', () => (0, state_1.flushSave)(state));
}
function startResize(handle, getStartWidth, cb) {
    if (!handle)
        return;
    let startX = 0, startW = 0;
    const move = (e) => {
        cb(startW + (e.clientX - startX));
    };
    const up = () => {
        document.removeEventListener('mousemove', move);
        document.removeEventListener('mouseup', up);
        document.body.classList.remove('resizing');
    };
    handle.addEventListener('mousedown', e => {
        e.preventDefault();
        startX = e.clientX;
        startW = getStartWidth();
        document.body.classList.add('resizing');
        document.addEventListener('mousemove', move);
        document.addEventListener('mouseup', up);
    });
}
// ------------------------------------------------------------------
// Theme
// ------------------------------------------------------------------
function applyTheme() {
    const t = constants_1.C.THEMES[state.theme];
    const root = document.documentElement;
    for (const [k, v] of Object.entries(t)) {
        root.style.setProperty('--' + k.replace(/([A-Z])/g, '-$1').toLowerCase(), v);
    }
    root.style.setProperty('--font-ui', constants_1.C.FONT_UI);
    root.style.setProperty('--font-editor', constants_1.C.FONT_EDITOR);
    root.style.setProperty('--font-mono', constants_1.C.FONT_MONO);
    root.dataset.theme = state.theme;
}
// ------------------------------------------------------------------
// Highlight a note's title+snippet when it matches current search?
// (List cards already show snippet; matches are shown in search
//  overlay — keep the list plain for a calm surface.)
// ------------------------------------------------------------------
// ------------------------------------------------------------------
// Master render
// ------------------------------------------------------------------
function renderAll() {
    renderSidebar();
    renderList();
    applyTheme();
    // layout vars
    const grid = document.getElementById('app');
    grid.style.setProperty('--side-w', (state.layout.sidebarCollapsed ? 0 : state.layout.sidebarW) + 'px');
    grid.style.setProperty('--list-w', state.layout.listW + 'px');
    updateSavedEl(state);
    onChangeCb();
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"typescript","sessionId":"c3dce893-a800-43a7-96c3-7a0b6d9fdf6a","tokensIn":11073626,"tokensOut":221817,"tokensTotal":11295443,"cost":0,"turns":108,"toolCalls":150,"failedToolCalls":7,"timestamp":"2026-09-09T07:45:46.635Z"} -->