Notes
Deepseek v4 Flash 0731 · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 108,532 bytes · SHA-256 4f9f603b086e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notes</title>
<style>
/* ── css: base ── */
:root{
--font-ui: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--font-editor: 'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
--bg-app:#f4f2ee;
--bg-sidebar:#edeae4;
--bg-notes:#f6f4f1;
--bg-editor:#ffffff;
--bg-elev:#ffffff;
--bg-hover:rgba(0,0,0,0.05);
--bg-active:rgba(0,0,0,0.075);
--bg-selected:#e6ded2;
--text:#2a2420;
--text-soft:#6b6258;
--text-faint:#9a9083;
--border:#e2ddd4;
--border-strong:#d4cdc1;
--accent:#a65a33;
--accent-strong:#8f4b27;
--accent-soft:#f1e3d6;
--accent-text:#7c4a28;
--chip:#ece7de;
--mark:#ffe08a;
--danger:#c0392b;
--danger-soft:#fbeae7;
--shadow-1:0 1px 2px rgba(40,30,20,.06);
--shadow-2:0 6px 24px rgba(40,30,20,.12);
--shadow-3:0 12px 44px rgba(40,30,20,.18);
--radius:10px;
--radius-s:7px;
--trans:150ms cubic-bezier(.4,0,.2,1);
}
html.dark{
--bg-app:#1a1816;
--bg-sidebar:#211e1b;
--bg-notes:#1d1a18;
--bg-editor:#23201d;
--bg-elev:#2a2622;
--bg-hover:rgba(255,255,255,0.06);
--bg-active:rgba(255,255,255,0.09);
--bg-selected:#3a3128;
--text:#ece6dd;
--text-soft:#a89e90;
--text-faint:#6f6659;
--border:#36302a;
--border-strong:#453d34;
--accent:#d5895c;
--accent-strong:#e09b70;
--accent-soft:#3a2c22;
--accent-text:#e2ab85;
--chip:#322c25;
--mark:#6b5420;
--danger:#e06c5c;
--danger-soft:#3a2622;
--shadow-1:0 1px 2px rgba(0,0,0,.3);
--shadow-2:0 6px 24px rgba(0,0,0,.4);
--shadow-3:0 12px 44px rgba(0,0,0,.5);
}
*{box-sizing:border-box;}
html,body{height:100%;margin:0;}
body{
font-family:var(--font-ui);
background:var(--bg-app);
color:var(--text);
font-size:14px;
-webkit-font-smoothing:antialiased;
overflow:hidden;
}
#app{height:100vh;display:flex;position:relative;}
button{font-family:inherit;color:inherit;background:none;border:none;cursor:pointer;padding:0;}
input,textarea{font-family:inherit;color:inherit;}
::selection{background:var(--mark);color:inherit;}
/* scrollbars */
::-webkit-scrollbar{width:11px;height:11px;}
::-webkit-scrollbar-thumb{background:transparent;border-radius:6px;border:3px solid transparent;background-clip:content-box;}
::-webkit-scrollbar-thumb:hover{background:var(--border-strong);background-clip:content-box;border:3px solid transparent;}
::-webkit-scrollbar-corner{background:transparent;}
.pane{overflow:visible;position:relative;}
/* ── css: layout ── */
#sidebar{
width:260px;flex:0 0 auto;background:var(--bg-sidebar);
display:flex;flex-direction:column;overflow:hidden;min-width:0;
padding:0 10px;
border-right:1px solid var(--border);
transition:width var(--trans), flex-basis var(--trans), padding var(--trans);
}
#app.sidebar-collapsed #sidebar{width:0;flex-basis:0;border-right:none;min-width:0;padding:0;overflow:hidden;opacity:0;pointer-events:none;}
#sidebar.collapsing{overflow:hidden;}
/* ── css: sidebar ── */
/* sidebar */
#sidebar{padding:0 10px;gap:2px;}
.side-head{display:flex;align-items:center;justify-content:space-between;padding:14px 6px 12px;}
.app-brand{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px;letter-spacing:-.2px;color:var(--text);}
.brand-mark{color:var(--accent);font-size:18px;}
.icon-btn{
width:30px;height:30px;border-radius:var(--radius-s);display:grid;place-items:center;
color:var(--text-soft);transition:background var(--trans),color var(--trans);
}
.icon-btn:hover{background:var(--bg-hover);color:var(--text);}
.icon-btn.active{background:var(--accent-soft);color:var(--accent-text);}
.icon-btn:active{transform:scale(.94);}
.search-wrap{position:relative;margin:0 2px 12px;}
.search-ico{position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--text-faint);pointer-events:none;}
.search-input{
width:100%;height:36px;border-radius:9px;border:1px solid transparent;
background:var(--bg-elev);padding:0 30px 0 34px;font-size:13px;outline:none;
box-shadow:var(--shadow-1);transition:border-color var(--trans),box-shadow var(--trans);
}
.search-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);}
.search-input::placeholder{color:var(--text-faint);}
.search-clear{
position:absolute;right:8px;top:50%;transform:translateY(-50%);
width:20px;height:20px;border-radius:50%;display:grid;place-items:center;
color:var(--text-soft);background:var(--bg-hover);font-size:14px;line-height:1;
}
.search-clear:hover{background:var(--bg-active);color:var(--text);}
.side-nav{display:flex;flex-direction:column;gap:2px;margin-bottom:6px;}
.nav-row{
display:flex;align-items:center;gap:10px;padding:7px 9px;border-radius:var(--radius-s);
color:var(--text-soft);font-size:13.5px;font-weight:500;text-align:left;
transition:background var(--trans),color var(--trans);
}
.nav-row svg{flex:0 0 auto;opacity:.85;}
.nav-row:hover{background:var(--bg-hover);color:var(--text);}
.nav-row.active{background:var(--accent-soft);color:var(--accent-text);font-weight:600;}
.nav-count{margin-left:auto;font-size:11px;color:var(--text-faint);background:var(--chip);padding:1px 7px;border-radius:20px;}
.nav-row.active .nav-count{background:var(--accent);color:#fff;}
.side-section-head{
display:flex;align-items:center;justify-content:space-between;
padding:12px 9px 5px;font-size:10.5px;font-weight:700;letter-spacing:.12em;color:var(--text-faint);
}
.mini-btn{
width:20px;height:20px;border-radius:5px;display:grid;place-items:center;
color:var(--text-faint);font-size:15px;line-height:1;transition:background var(--trans),color var(--trans);
}
.mini-btn:hover{background:var(--bg-hover);color:var(--accent);}
.notebook-list{display:flex;flex-direction:column;gap:1px;overflow-y:auto;min-height:0;}
.tag-list{display:flex;flex-direction:column;gap:1px;margin-bottom:4px;max-height:40vh;overflow-y:auto;}
.nb-row{
display:flex;align-items:center;gap:6px;border-radius:var(--radius-s);
padding:3px 6px 3px 4px;color:var(--text-soft);cursor:pointer;
transition:background var(--trans),color var(--trans);
}
.nb-row:hover{background:var(--bg-hover);}
.nb-row.active{background:var(--accent-soft);color:var(--accent-text);}
.nb-caret{
width:18px;height:18px;flex:0 0 auto;display:grid;place-items:center;
color:var(--text-faint);border-radius:4px;transition:transform var(--trans);
}
.nb-caret svg{transition:transform var(--trans);}
.nb-row.collapsed .nb-caret svg{transform:rotate(-90deg);}
.nb-main{
display:flex;align-items:center;gap:8px;flex:1;padding:4px 4px;border-radius:var(--radius-s);
min-width:0;transition:background var(--trans);
}
.nb-row:hover .nb-main{background:var(--bg-hover);}
.nb-ico{flex:0 0 auto;color:var(--accent);display:grid;place-items:center;}
.nb-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;}
.nb-count{font-size:11px;color:var(--text-faint);padding-right:4px;}
.nb-actions{display:flex;gap:0;opacity:0;transition:opacity var(--trans);}
.nb-row:hover .nb-actions{opacity:1;}
.nb-act{
width:22px;height:22px;border-radius:5px;display:grid;place-items:center;
color:var(--text-soft);font-size:13px;
}
.nb-act:hover{background:var(--bg-active);color:var(--text);}
.nb-notes{padding:1px 0 2px 28px;display:flex;flex-direction:column;gap:1px;max-height:50vh;overflow-y:auto;}
.nb-note-item{display:flex;gap:7px;align-items:baseline;}
/* nudge indent so caret sits above nested notes */
.nb-note-item .nb-caret-indent{width:18px;flex:0 0 auto;}
.nb-notes .nb-note-link{
flex:1;text-align:left;color:var(--text-soft);font-size:12.5px;padding:2px 4px;border-radius:5px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.nb-notes .nb-note-link:hover{background:var(--bg-hover);color:var(--text);}
.nb-notes .nb-note-link.active{background:var(--accent-soft);color:var(--accent-text);font-weight:600;}
.nb-note-meta{font-size:10.5px;color:var(--text-faint);}
.tag-item{
display:flex;align-items:center;gap:8px;padding:5px 9px;border-radius:var(--radius-s);
color:var(--text-soft);font-size:13px;cursor:pointer;transition:background var(--trans),color var(--trans);
}
.tag-item:hover{background:var(--bg-hover);color:var(--text);}
.tag-item.active{background:var(--accent-soft);color:var(--accent-text);font-weight:600;}
.tag-hash{color:var(--text-faint);font-weight:600;}
.tag-item.active .tag-hash,.tag-item.active .tag-count{color:var(--accent-text);}
.tag-count{margin-left:auto;font-size:11px;color:var(--text-faint);}
/* notebook context menu */
.menu{
position:fixed;z-index:80;min-width:170px;background:var(--bg-elev);border:1px solid var(--border);
border-radius:var(--radius);box-shadow:var(--shadow-3);padding:5px;animation:pop .12s ease;
}
@keyframes pop{from{opacity:0;transform:scale(.96) translateY(-3px);}to{opacity:1;transform:none;}}
.menu-item{
display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border-radius:6px;
font-size:13px;text-align:left;color:var(--text);transition:background var(--trans);
}
.menu-item:hover{background:var(--bg-hover);}
.menu-item.danger{color:var(--danger);}
.menu-item.danger:hover{background:var(--danger-soft);}
.menu-sep{height:1px;background:var(--border);margin:5px 6px;}
.menu-sublabel{font-size:10px;color:var(--text-faint);padding:6px 10px 3px;font-weight:700;letter-spacing:.08em;}
.menu-item .mi-label{flex:1;}
.menu-item .mi-kbd{font-size:11px;color:var(--text-faint);}
/* ── css: notes-pane ── */
/* notes list pane */
.notes-head{
display:flex;align-items:center;gap:8px;padding:12px 12px;border-bottom:1px solid var(--border);
}
.notes-head-title{flex:1;font-weight:700;font-size:15px;letter-spacing:-.2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.notes-head-actions{display:flex;align-items:center;gap:4px;}
.new-note-btn{
height:30px;padding:0 12px;border-radius:var(--radius-s);background:var(--accent);color:#fff;
font-size:14px;font-weight:600;display:flex;align-items:center;gap:6px;box-shadow:var(--shadow-1);
transition:background var(--trans),transform var(--trans);
}
.new-note-btn:hover{background:var(--accent-strong);}
.new-note-btn:active{transform:scale(.96);}
.notes-toolbar{
display:flex;align-items:center;gap:8px;padding:6px 12px;border-bottom:1px solid var(--border);
background:var(--accent-soft);font-size:12px;color:var(--accent-text);animation:fadeIn .2s ease;
}
@keyframes fadeIn{from{opacity:0;}to{opacity:1;}}
.notes-tb-label{flex:1;font-weight:600;}
.notes-tb-clear{width:18px;height:18px;border-radius:50%;display:grid;place-items:center;font-size:13px;line-height:1;color:var(--accent-text);}
.notes-tb-clear:hover{background:var(--accent);color:#fff;}
.notes-list{flex:1;overflow-y:auto;padding:6px 8px;}
.note-item{
position:relative;padding:10px 12px 9px;border-radius:var(--radius);cursor:pointer;margin-bottom:2px;
border:1px solid transparent;transition:background var(--trans),border-color var(--trans),box-shadow var(--trans);
animation:itemIn .2s ease;
}
@keyframes itemIn{from{opacity:0;transform:translateY(2px);}to{opacity:1;transform:none;}}
.note-item:hover{background:var(--bg-hover);}
.note-item.selected{background:var(--bg-selected);border-color:var(--border-strong);box-shadow:var(--shadow-1);}
.note-item.dragging{opacity:.4;}
.note-item.drop-target{outline:2px dashed var(--accent);outline-offset:-2px;}
.note-item-top{display:flex;align-items:center;gap:6px;}
.ni-pin{flex:0 0 auto;color:var(--accent);display:grid;place-items:center;opacity:0;transition:opacity var(--trans);}
.note-item:hover .ni-pin,.note-item.pinned .ni-pin{opacity:1;}
.note-item.pinned .ni-pin{opacity:1;}
.ni-title{
flex:1;font-weight:600;font-size:13.5px;color:var(--text);line-height:1.35;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.ni-pin-btn{flex:0 0 auto;width:22px;height:22px;border-radius:5px;display:grid;place-items:center;color:var(--text-faint);opacity:0;transition:opacity var(--trans),background var(--trans),color var(--trans);}
.note-item:hover .ni-pin-btn{opacity:1;}
.ni-pin-btn:hover{background:var(--bg-active);}
.ni-pin-btn.pinned{opacity:1;color:var(--accent);}
.ni-snippet{
font-size:12px;color:var(--text-soft);margin-top:2px;line-height:1.4;display:-webkit-box;
-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word;
}
.ni-snippet em.match{font-style:normal;background:var(--mark);color:inherit;border-radius:2px;padding:0 1px;}
.ni-meta{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:10.5px;color:var(--text-faint);}
.ni-meta .dot{width:3px;height:3px;border-radius:50%;background:var(--text-faint);}
.ni-tags{display:flex;gap:4px;margin-left:auto;overflow:hidden;}
.ni-tag{font-size:10px;color:var(--accent-text);background:var(--chip);padding:1px 6px;border-radius:20px;white-space:nowrap;}
.trash-badge{font-size:10px;color:var(--danger);border:1px solid var(--danger);padding:0 5px;border-radius:20px;}
.notes-empty{
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:8px;padding:24px;text-align:center;color:var(--text-soft);animation:fadeIn .3s ease;
}
.notes-empty .ne-ico{color:var(--text-faint);opacity:.7;}
.notes-empty h3{margin:0;font-size:14px;font-weight:600;color:var(--text);}
.notes-empty p{margin:0;font-size:12.5px;max-width:220px;line-height:1.4;}
/* ── css: toolbar ── */
/* editor toolbar */
.toolbar{
display:flex;align-items:center;gap:4px;padding:8px 14px;border-bottom:1px solid var(--border);
background:var(--bg-editor);flex:0 0 auto;position:relative;z-index:5;
}
.tb-group{display:flex;align-items:center;gap:2px;}
.tb-sep{width:1px;height:22px;background:var(--border);margin:0 6px;}
.tb-btn{
min-width:30px;height:30px;padding:0 7px;border-radius:7px;display:inline-flex;align-items:center;
justify-content:center;color:var(--text-soft);font-size:13px;gap:2px;position:relative;
transition:background var(--trans),color var(--trans);
}
.tb-btn:hover{background:var(--bg-hover);color:var(--text);}
.tb-btn.active{background:var(--accent-soft);color:var(--accent-text);}
.tb-btn:active{transform:scale(.94);}
.tb-btn b{font-weight:800;}
.tb-btn i{font-style:italic;}
.tb-btn u{text-decoration:underline;}
.tb-btn s{text-decoration:line-through;}
.tb-btn .code-glyph{font-family:'SFMono-Regular',Consolas,'Liberation Mono',monospace;font-size:12px;background:var(--chip);border-radius:3px;padding:1px 3px;}
.tb-btn.head{font-size:11px;font-weight:700;letter-spacing:.02em;color:var(--text);}
.tb-btn .sep-label{font-size:9px;color:var(--text-faint);font-weight:700;}
.tb-btn.quote-glyph{font-size:16px;font-weight:800;color:var(--text-soft);}
.tb-spacer{flex:1;}
.saved{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--text-faint);transition:opacity var(--trans);}
.saved .dot{width:7px;height:7px;border-radius:50%;background:var(--text-faint);transition:background var(--trans);}
.saved.saving{color:var(--text-soft);}
.saved.saving .dot{background:var(--accent);animation:pulse 1s infinite;}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.4;}}
.saved.saved-ok{color:#4a7c3a;}
html.dark .saved.saved-ok{color:#8fc47a;}
.saved.saved-ok .dot{background:#4a7c3a;}
html.dark .saved.saved-ok .dot{background:#8fc47a;}
/* ── css: editor ── */
/* editor content */
.editor-scroll{flex:1;overflow-y:auto;}
.ed-inner{max-width:760px;margin:0 auto;padding:44px 52px 120px;}
.note-title{
width:100%;border:none;outline:none;background:transparent;font-family:var(--font-editor);
font-size:34px;font-weight:700;letter-spacing:-.6px;color:var(--text);line-height:1.2;
padding:0 2px 6px;margin-bottom:4px;resize:none;
}
.note-title::placeholder{color:var(--text-faint);opacity:.6;}
.editor{
min-height:60vh;outline:none;caret-color:var(--accent);
font-family:var(--font-editor);font-size:17px;line-height:1.65;color:var(--text);
word-wrap:break-word;overflow-wrap:break-word;
}
.editor:empty::before,.editor[data-empty="true"]::before{
content:attr(data-placeholder);color:var(--text-faint);opacity:.55;pointer-events:none;
}
.editor p{margin:0 0 .55em;}
.editor p:empty{height:.55em;margin-bottom:0;}
.editor h1{font-size:1.75em;font-weight:800;margin:.8em 0 .4em;line-height:1.25;letter-spacing:-.3px;}
.editor h2{font-size:1.4em;font-weight:700;margin:.7em 0 .35em;line-height:1.3;letter-spacing:-.2px;}
.editor h3{font-size:1.15em;font-weight:700;margin:.6em 0 .3em;line-height:1.35;}
.editor h1:first-child,.editor h2:first-child,.editor h3:first-child,.editor p:first-child{margin-top:0;}
.editor ul,.editor ol{margin:.2em 0 .6em;padding-left:1.5em;}
.editor li{margin:.12em 0;}
.editor code{
font-family:'SFMono-Regular',Consolas,'Liberation Mono',ui-monospace,monospace;font-size:.86em;
background:var(--chip);border:1px solid var(--border);border-radius:4px;padding:.12em .35em;color:var(--text);
}
.editor blockquote{
margin:.4em 0;padding:.15em 0 .15em 1em;border-left:3px solid var(--accent);
color:var(--text-soft);font-style:italic;
}
.editor hr{border:none;border-top:2px solid var(--border-strong);margin:1.2em 0;}
.editor a{color:var(--accent);text-decoration:underline;}
/* task lists */
.editor ul.task-list{list-style:none;padding-left:.3em;}
.editor ul.task-list>li{display:flex;align-items:flex-start;gap:9px;margin:.1em 0;}
.editor ul.task-list>li>input[type=checkbox]{
appearance:none;-webkit-appearance:none;flex:0 0 auto;width:16px;height:16px;margin:4px 0 0 2px;
border:1.5px solid var(--border-strong);border-radius:4px;cursor:pointer;position:relative;
background:var(--bg-elev);transition:background var(--trans),border-color var(--trans);
}
.editor ul.task-list>li>input[type=checkbox]:hover{border-color:var(--accent);}
.editor ul.task-list>li>input[type=checkbox]:checked{background:var(--accent);border-color:var(--accent);}
.editor ul.task-list>li>input[type=checkbox]:checked::after{
content:'';position:absolute;left:4.5px;top:1.5px;width:4px;height:8px;
border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg);
}
.editor ul.task-list>li.checked{color:var(--text-faint);}
.editor ul.task-list>li.checked .task-text{text-decoration:line-through;opacity:.75;}
/* tags footer */
.ed-footer{border-top:1px solid var(--border);margin-top:36px;padding-top:14px;}
.ed-footer-inner{display:flex;flex-wrap:wrap;gap:8px;align-items:center;}
.note-tags{display:flex;flex-wrap:wrap;gap:6px;}
.tag-chip{
display:inline-flex;align-items:center;gap:5px;background:var(--chip);color:var(--accent-text);
font-size:12px;padding:3px 6px 3px 10px;border-radius:20px;font-weight:500;animation:chipIn .15s ease;
}
@keyframes chipIn{from{opacity:0;transform:scale(.9);}to{opacity:1;transform:none;}}
.tag-chip .hash{color:var(--text-faint);font-weight:600;}
.tag-chip .x{width:15px;height:15px;border-radius:50%;display:grid;place-items:center;font-size:12px;line-height:1;color:var(--text-soft);}
.tag-chip .x:hover{background:var(--bg-active);color:var(--text);}
.tag-chip.clickable{cursor:pointer;transition:background var(--trans);}
.tag-chip.clickable:hover{background:var(--accent-soft);}
.tag-input{background:transparent;border:none;outline:none;font-size:12.5px;color:var(--text);width:90px;}
.tag-input::placeholder{color:var(--text-faint);}
/* empty state */
.editor-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--bg-editor);z-index:4;}
.empty-card{display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;max-width:320px;padding:40px;animation:itemIn .25s ease;}
.empty-ico{
width:64px;height:64px;border-radius:20px;display:grid;place-items:center;margin-bottom:8px;
background:var(--accent-soft);color:var(--accent);box-shadow:var(--shadow-1);
}
.empty-card h2{margin:0;font-size:19px;font-weight:700;color:var(--text);}
.empty-card p{margin:0 0 10px;font-size:13.5px;color:var(--text-soft);line-height:1.5;}
.btn-primary{
height:36px;padding:0 18px;border-radius:9px;background:var(--accent);color:#fff;
font-size:13.5px;font-weight:600;box-shadow:var(--shadow-1);transition:background var(--trans),transform var(--trans);
}
.btn-primary:hover{background:var(--accent-strong);}
.btn-primary:active{transform:scale(.97);}
/* ── css: misc ── */
/* inline rename input + drag-drop highlight + small helpers */
.rename-input{
width:100%;border:1px solid var(--accent);border-radius:5px;background:var(--bg-elev);
color:var(--text);font-size:13px;padding:3px 6px;outline:none;box-shadow:0 0 0 3px var(--accent-soft);
}
.nb-row.drop-ontop{outline:2px dashed var(--accent);outline-offset:-2px;border-radius:10px;}
.nav-row.drop-ontop{outline:2px dashed var(--accent);outline-offset:-2px;border-radius:10px;}
.drag-ghost{position:fixed;pointer-events:none;z-index:90;opacity:.9;animation:pop .12s ease;}
.dragging-source{opacity:.35;}
.sort-menu .menu-item .check{width:16px;color:var(--accent);}
.kbd{border:1px solid var(--border);border-bottom-width:2px;border-radius:4px;padding:0 4px;font-size:10.5px;color:var(--text-soft);background:var(--bg-hover);}
.toast{
position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:120;
background:var(--bg-elev);border:1px solid var(--border);box-shadow:var(--shadow-3);
padding:10px 18px;border-radius:10px;font-size:13px;font-weight:500;color:var(--text);
animation:toastIn .25s ease;
}
@keyframes toastIn{from{opacity:0;transform:translate(-50%,10px);}to{opacity:1;transform:translate(-50%,0);}}
</style>
</head>
<body>
<div id="app">
<div id="scrim" class="scrim"></div>
<aside id="sidebar" class="pane">
<div class="side-head">
<div class="app-brand"><span class="brand-mark">❖</span><span>Notes</span></div>
<button id="theme-toggle" class="icon-btn" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"></path></svg>
</button>
</div>
<div class="search-wrap">
<svg class="search-ico" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M21 21l-4.3-4.3"></path></svg>
<input id="global-search" class="search-input" placeholder="Search notes" spellcheck="false">
<span id="search-clear" class="search-clear" hidden="">×</span>
</div>
<nav class="side-nav">
<button class="nav-row" id="nav-all">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v16H4z"></path><path d="M4 9h16"></path><path d="M9 4v5"></path></svg>
<span class="nav-label">All Notes</span><span class="nav-count" id="count-all"></span>
</button>
<button class="nav-row" id="nav-trash">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M8 6V4h8v2"></path><path d="M19 6l-1 14H6L5 6"></path><path d="M10 11v6M14 11v6"></path></svg>
<span class="nav-label">Trash</span><span class="nav-count" id="count-trash"></span>
</button>
</nav>
<div class="side-section-head"><span>TAGS</span></div>
<div class="tag-list" id="tag-list"></div>
<div class="side-section-head">
<span>NOTEBOOKS</span>
<button id="new-notebook" class="mini-btn" title="New notebook">+</button>
</div>
<div class="notebook-list" id="notebook-list"></div>
</aside>
<div id="resizer-a" class="resizer"></div>
<section id="notes-pane" class="pane">
<div class="notes-head">
<button id="sidebar-toggle" class="icon-btn" title="Toggle sidebar" aria-label="Toggle sidebar">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16M4 12h10M4 18h16"></path></svg>
</button>
<div class="notes-head-title" id="notes-head-title"></div>
<div class="notes-head-actions">
<button id="sort-toggle" class="icon-btn" title="Sort notes" aria-label="Sort notes">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 4v13M8 17l-3-3M8 17l3-3"></path><path d="M16 20V7M16 7l-3 3M16 7l3 3"></path></svg>
</button>
<button id="new-note" class="new-note-btn" title="New note">+</button>
</div>
</div>
<div class="notes-toolbar" id="notes-toolbar" hidden="">
<span class="notes-tb-label" id="notes-tb-label"></span>
<button id="notes-tb-clear" class="notes-tb-clear">×</button>
</div>
<div id="notes-list" class="notes-list"></div>
<div id="notes-empty" class="notes-empty" hidden=""></div>
</section>
<div id="resizer-b" class="resizer"></div>
<main id="editor-pane" class="pane">
<div id="toolbar" class="toolbar"><div class="saved" id="saved-indicator"><span class="dot"></span><span id="saved-text">Saved</span></div></div>
<div id="editor-scroll" class="editor-scroll">
<div class="ed-inner">
<input id="note-title" class="note-title" placeholder="Untitled note" spellcheck="false">
<div id="editor" contenteditable="true" class="editor" data-placeholder="Start writing…"></div>
<div class="ed-footer">
<div class="ed-footer-inner">
<div id="note-tags" class="note-tags"></div>
<div id="tag-input-wrap" class="tag-input-wrap">
<input id="tag-input" class="tag-input" placeholder="+ add tag" spellcheck="false">
</div>
</div>
</div>
</div>
</div>
<div id="editor-empty" class="editor-empty" hidden="">
<div class="empty-card">
<div class="empty-ico">
<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M16 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><path d="M16 3v5h5"></path></svg>
</div>
<h2>No note selected</h2>
<p>Select a note from the list, or start something new.</p>
<button id="empty-new" class="btn-primary">+ New note</button>
</div>
</div>
</main>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./store":"src/store.ts","./types":"src/types.ts","./editor":"src/editor.ts","./ui":"src/ui.ts"},"src/store.ts":{"./types":"src/types.ts"},"src/editor.ts":{"./types":"src/types.ts","./store":"src/store.ts"},"src/ui.ts":{"./store":"src/store.ts","./types":"src/types.ts","./editor":"src/editor.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// ─────────────────────────────────────────────────────────────
// main.ts — entry: wires the editor, binds UI, manages layout,
// theme, sidebar collapse, autosave, and global shortcuts.
// ─────────────────────────────────────────────────────────────
const store_1 = require("./store");
const types_1 = require("./types");
const editor_1 = require("./editor");
const ui_1 = require("./ui");
const $ = (sel) => document.querySelector(sel);
// ── autosave (debounced persistence) ─────────────────────────
let saveTimer = 0;
function scheduleSave() {
window.clearTimeout(saveTimer);
saveTimer = window.setTimeout(() => (0, store_1.save)(), 120);
}
function onNoteChanged(_noteId) {
scheduleSave();
(0, ui_1.renderAll)();
}
// ── layout ───────────────────────────────────────────────────
function applyLayout() {
const s = (0, store_1.getState)().layout;
const app = $('#app');
app.classList.toggle('sidebar-collapsed', s.sidebarCollapsed);
const sidebar = $('#sidebar');
if (s.sidebarCollapsed) {
sidebar.style.width = '0px';
sidebar.style.flexBasis = '0px';
sidebar.style.borderRight = 'none';
sidebar.style.padding = '0';
sidebar.style.opacity = '0';
}
else {
sidebar.style.width = s.sidebar + 'px';
sidebar.style.flexBasis = s.sidebar + 'px';
sidebar.style.borderRight = '';
sidebar.style.padding = '';
sidebar.style.opacity = '';
}
$('#notes-pane').style.width = s.notes + 'px';
$('#notes-pane').style.flexBasis = s.notes + 'px';
$('#resizer-a').style.display = s.sidebarCollapsed ? 'none' : '';
}
function startResize(handle, which) {
const startX = handle === document.activeElement ? 0 : 0;
const move = (ev) => {
const s = (0, store_1.getState)().layout;
if (which === 'sidebar') {
const w = Math.round(ev.clientX);
const clamped = Math.max(types_1.CONST.MIN_SIDEBAR, Math.min(types_1.CONST.MAX_SIDEBAR, w));
(0, store_1.setLayout)({ sidebar: clamped });
const sidebar = $('#sidebar');
sidebar.style.width = clamped + 'px';
sidebar.style.flexBasis = clamped + 'px';
}
else {
const rect = $('#editor-pane').getBoundingClientRect();
const w = Math.round(ev.clientX - $('#notes-pane').getBoundingClientRect().left);
const maxNotes = window.innerWidth - 360 - ((0, store_1.getState)().layout.sidebarCollapsed ? 0 : (0, store_1.getState)().layout.sidebar);
const clamped = Math.max(types_1.CONST.MIN_NOTES, Math.min(types_1.CONST.MAX_NOTES, maxNotes, w));
(0, store_1.setLayout)({ notes: clamped });
const np = $('#notes-pane');
np.style.width = clamped + 'px';
np.style.flexBasis = clamped + 'px';
void rect;
}
scheduleSave();
};
const up = () => {
document.body.classList.remove('resizing');
handle.classList.remove('dragging');
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
handle.addEventListener('pointerdown', (e) => {
e.preventDefault();
document.body.classList.add('resizing');
handle.classList.add('dragging');
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
});
void startX;
}
// ── theme ────────────────────────────────────────────────────
function applyTheme() {
document.documentElement.classList.toggle('dark', (0, store_1.getState)().layout.theme === 'dark');
}
// ── new note action ──────────────────────────────────────────
function onNewNote(notebookId) {
const note = (0, store_1.createNote)(notebookId);
(0, store_1.setSelectedNote)(note.id);
(0, store_1.save)();
(0, ui_1.renderAll)();
(0, editor_1.loadNote)(note);
(0, ui_1.showEditorEmpty)(false);
(0, ui_1.focusTitle)();
}
// ── keyboard shortcuts ───────────────────────────────────────
function onGlobalKey(e) {
const tag = e.target.tagName;
const inField = tag === 'INPUT' || tag === 'TEXTAREA' || e.target.isContentEditable;
const mod = e.metaKey || e.ctrlKey;
// New note
if (mod && e.key.toLowerCase() === 'n' && !e.shiftKey) {
if (!inField || tag === 'INPUT') {
e.preventDefault();
onNewNote(notebookForNew());
return;
}
}
// Toggle sidebar
if (mod && e.key.toLowerCase() === '\\') {
e.preventDefault();
toggleSidebar();
return;
}
// Delete selected note (Del on note list)
if ((e.key === 'Delete' || e.key === 'Backspace') && !inField && (0, editor_1.getCurrentNoteId)()) {
const view = (0, store_1.getState)().view.kind;
if (view !== 'trash') {
e.preventDefault();
(0, ui_1.deleteSelectedNote)();
}
}
}
function notebookForNew() {
const s = (0, store_1.getState)();
if (s.view.kind === 'notebook')
return s.view.notebookId || null;
const cur = (0, store_1.findNote)(s.selectedNoteId);
if (cur && cur.notebookId)
return cur.notebookId;
return s.notebooks[0]?.id || null;
}
// ── sidebar collapse ─────────────────────────────────────────
function toggleSidebar() {
const s = (0, store_1.getState)();
(0, store_1.setSidebarCollapsed)(!s.layout.sidebarCollapsed);
applyLayout();
(0, store_1.save)();
}
// ── startup ──────────────────────────────────────────────────
function start() {
applyTheme();
const s = (0, store_1.getState)();
(0, editor_1.initEditor)({
editor: $('#editor'),
title: $('#note-title'),
toolbar: $('#toolbar'),
savedEl: $('.saved'),
savedText: $('#saved-text'),
tagWrap: $('#note-tags'),
tagInput: $('#tag-input'),
onTagClick: (tag) => (0, ui_1.setQtagFilter)(tag === (0, ui_1.getQtagFilter)() ? null : tag),
onNoteChanged,
});
(0, ui_1.bindUI)({ onNewNote, focusTitle: ui_1.focusTitle });
// layout + resize
applyLayout();
startResize($('#resizer-a'), 'sidebar');
startResize($('#resizer-b'), 'notes');
// theme toggle
$('#theme-toggle').addEventListener('click', () => {
(0, store_1.setTheme)((0, store_1.getState)().layout.theme === 'dark' ? 'light' : 'dark');
applyTheme();
(0, store_1.save)();
});
$('#sidebar-toggle').addEventListener('click', toggleSidebar);
// initial render + select
(0, ui_1.renderAll)();
const selected = (0, store_1.findNote)(s.selectedNoteId);
if (selected && selected.deletedAt === null) {
(0, editor_1.loadNote)(selected);
(0, ui_1.showEditorEmpty)(false);
}
else {
(0, editor_1.clearNote)();
(0, ui_1.showEditorEmpty)(true);
}
window.addEventListener('keydown', onGlobalKey);
window.addEventListener('beforeunload', () => (0, store_1.save)());
}
start();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─────────────────────────────────────────────────────────────
// types.ts — shared types & the tunable constants block
// ─────────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONST = void 0;
exports.uid = uid;
// ── Tunable constants ────────────────────────────────────────
exports.CONST = {
// storage
STORAGE_KEY: 'notes-app-state-v1',
AUTOSAVE_DEBOUNCE_MS: 400,
SAVED_HINT_MS: 1400,
// layout defaults (px)
SIDEBAR_WIDTH: 250,
NOTES_WIDTH: 300,
MIN_SIDEBAR: 180,
MAX_SIDEBAR: 460,
MIN_NOTES: 200,
MAX_NOTES: 520,
// content
SNIPPET_LENGTH: 110,
UNDO_LIMIT: 120,
// typography
EDITOR_FONT: "'Source Serif 4','Iowan Old Style','Palatino Linotype',Georgia,serif",
UI_FONT: "'Inter','-apple-system','Segoe UI',Roboto,sans-serif",
};
// ── id + time helpers ────────────────────────────────────────
function uid() {
return (Date.now().toString(36) + Math.random().toString(36).slice(2, 8)).toLowerCase();
}
};
// ── module: src/store.ts ──
__mods["src/store.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getState = getState;
exports.save = save;
exports.createNotebook = createNotebook;
exports.renameNotebook = renameNotebook;
exports.deleteNotebook = deleteNotebook;
exports.toggleNotebookExpanded = toggleNotebookExpanded;
exports.createNote = createNote;
exports.updateNoteTitle = updateNoteTitle;
exports.updateNoteHtml = updateNoteHtml;
exports.setPinned = setPinned;
exports.togglePinned = togglePinned;
exports.moveNoteTo = moveNoteTo;
exports.addTag = addTag;
exports.removeTag = removeTag;
exports.duplicateNote = duplicateNote;
exports.trashNote = trashNote;
exports.restoreNote = restoreNote;
exports.permanentDelete = permanentDelete;
exports.findNote = findNote;
exports.findNotebook = findNotebook;
exports.setSelectedNote = setSelectedNote;
exports.setView = setView;
exports.setSort = setSort;
exports.setTheme = setTheme;
exports.setSidebarCollapsed = setSidebarCollapsed;
exports.setLayout = setLayout;
exports.visibleNotes = visibleNotes;
exports.countInTrash = countInTrash;
exports.htmlToNodes = htmlToNodes;
exports.plainText = plainText;
exports.snippetFor = snippetFor;
// ─────────────────────────────────────────────────────────────
// store.ts — the single source of truth: model + persistence +
// pure helpers. The DOM is always a projection of this state.
// ─────────────────────────────────────────────────────────────
const types_1 = require("./types");
// seed content helper
function seed() {
const now = Date.now();
const nbWelcome = { id: (0, types_1.uid)(), name: 'Welcome', expanded: true };
const nbIdeas = { id: (0, types_1.uid)(), name: 'Ideas', expanded: true };
const welcome = {
id: (0, types_1.uid)(),
notebookId: nbWelcome.id,
title: 'Welcome to Notes',
html: '<h1>Welcome to Notes</h1><p>This is a note. Everything you type is saved automatically.</p>' +
'<h2>Try these</h2><ul><li>Type <code># </code> at the start of a line for a heading</li>' +
'<li>Type <code>- </code> for a bullet, <code>1. </code> for a numbered list</li>' +
'<li>Type <code>[] </code> for a checklist, <code>> </code> for a quote</li></ul>' +
'<p>Bold, <em>italic</em>, <u>underline</u> and more are in the toolbar above.</p>',
tags: ['getting-started'],
pinned: true,
createdAt: now - 120000,
updatedAt: now - 4000,
deletedAt: null,
};
const ideas = {
id: (0, types_1.uid)(),
notebookId: nbIdeas.id,
title: 'Snippet ideas',
html: '<p>Collect snippets worth keeping. Search is live from the sidebar — try searching for a word <strong>deep inside a bold span</strong>.</p>' +
'<blockquote>“Simplicity is the ultimate sophistication.”</blockquote>',
tags: ['ideas'],
pinned: false,
createdAt: now - 60000,
updatedAt: now - 20000,
deletedAt: null,
};
const tasks = {
id: (0, types_1.uid)(),
notebookId: nbIdeas.id,
title: 'Checklists',
html: '<p>Checklist items are clickable and persist across reloads:</p>' +
'<ul class="task-list"><li><input type="checkbox"> Ship the notes app</li>' +
'<li><input type="checkbox" checked=""> E-mail the team</li></ul>',
tags: [],
pinned: false,
createdAt: now - 30000,
updatedAt: now - 1000,
deletedAt: null,
};
return {
version: 3,
notebooks: [nbWelcome, nbIdeas],
notes: [welcome, ideas, tasks],
selectedNoteId: welcome.id,
view: { kind: 'notebook', notebookId: nbWelcome.id },
layout: {
sidebar: types_1.CONST.SIDEBAR_WIDTH,
notes: types_1.CONST.NOTES_WIDTH,
sidebarCollapsed: false,
theme: 'light',
sort: 'modified',
},
};
}
let state = load();
function getState() {
return state;
}
function load() {
try {
const raw = localStorage.getItem(types_1.CONST.STORAGE_KEY);
if (!raw)
return seed();
const parsed = JSON.parse(raw);
if (!parsed || parsed.version !== 3 || !Array.isArray(parsed.notebooks))
return seed();
// ensure required fields (defensive against old/corrupt data)
parsed.notebooks = parsed.notebooks.map((n) => ({
id: typeof n.id === 'string' ? n.id : (0, types_1.uid)(),
name: typeof n.name === 'string' ? n.name : 'Untitled',
expanded: n.expanded !== false,
}));
parsed.notes = parsed.notes.map((n) => ({
id: typeof n.id === 'string' ? n.id : (0, types_1.uid)(),
notebookId: n.notebookId ?? null,
title: n.title ?? '',
html: n.html ?? '',
tags: Array.isArray(n.tags) ? n.tags : [],
pinned: !!n.pinned,
createdAt: n.createdAt ?? 0,
updatedAt: n.updatedAt ?? 0,
deletedAt: n.deletedAt ?? null,
}));
return parsed;
}
catch {
return seed();
}
}
/** Persist state (debounced upstream). Always serializes current model. */
function save() {
try {
localStorage.setItem(types_1.CONST.STORAGE_KEY, JSON.stringify(state));
}
catch {
/* storage full / private mode — ignore */
}
}
// ── Mutations (all go through these so any caller can persist) ──
function createNotebook(name) {
const nb = { id: (0, types_1.uid)(), name: name || 'Untitled', expanded: true };
state.notebooks.push(nb);
return nb;
}
function renameNotebook(id, name) {
const nb = state.notebooks.find((n) => n.id === id);
if (nb)
nb.name = name;
}
function deleteNotebook(id) {
const notes = state.notes.filter((n) => n.notebookId === id && n.deletedAt === null);
const trash = state.notes.filter((n) => n.notebookId === id && n.deletedAt !== null);
// notes inside a deleted notebook move to Trash; trashed ones are purged
for (const n of notes)
n.deletedAt = Date.now();
for (const n of trash)
n.deletedAt = -1; // mark for purge below
state.notes = state.notes.filter((n) => n.deletedAt !== -1);
state.notebooks = state.notebooks.filter((n) => n.id !== id);
if (state.view.notebookId === id)
state.view = { kind: 'all' };
}
function toggleNotebookExpanded(id) {
const nb = state.notebooks.find((n) => n.id === id);
if (nb)
nb.expanded = !nb.expanded;
}
function createNote(notebookId, bodyHtml = '') {
const note = {
id: (0, types_1.uid)(),
notebookId,
title: '',
html: bodyHtml,
tags: [],
pinned: false,
createdAt: Date.now(),
updatedAt: Date.now(),
deletedAt: null,
};
state.notes.push(note);
return note;
}
function updateNoteTitle(id, title) {
const n = findNote(id);
if (n) {
n.title = title;
n.updatedAt = Date.now();
}
}
function updateNoteHtml(id, html) {
const n = findNote(id);
if (n) {
n.html = html;
n.updatedAt = Date.now();
}
}
function setPinned(id, pinned) {
const n = findNote(id);
if (n)
n.pinned = pinned;
}
function togglePinned(id) {
const n = findNote(id);
if (n) {
n.pinned = !n.pinned;
n.updatedAt = Date.now();
}
}
function moveNoteTo(id, notebookId) {
const n = findNote(id);
if (n) {
n.notebookId = notebookId;
n.updatedAt = Date.now();
}
}
function addTag(id, tag) {
const n = findNote(id);
if (!n)
return;
const t = tag.trim().toLowerCase();
if (t && !n.tags.includes(t))
n.tags.push(t);
}
function removeTag(id, tag) {
const n = findNote(id);
if (n)
n.tags = n.tags.filter((t) => t !== tag);
}
function duplicateNote(id) {
const src = findNote(id);
if (!src)
throw new Error('no note');
const copy = {
...src,
id: (0, types_1.uid)(),
title: src.title ? src.title + ' (copy)' : '',
pinned: false,
createdAt: Date.now(),
updatedAt: Date.now(),
deletedAt: null,
};
state.notes.push(copy);
return copy;
}
function trashNote(id) {
const n = findNote(id);
if (n) {
n.deletedAt = Date.now();
n.updatedAt = Date.now();
}
}
function restoreNote(id) {
const n = findNote(id);
if (n)
n.deletedAt = null;
}
function permanentDelete(id) {
state.notes = state.notes.filter((n) => n.id !== id);
if (state.selectedNoteId === id)
state.selectedNoteId = null;
}
function findNote(id) {
return id ? state.notes.find((n) => n.id === id) : undefined;
}
function findNotebook(id) {
return id ? state.notebooks.find((n) => n.id === id) : undefined;
}
function setSelectedNote(id) {
state.selectedNoteId = id;
}
function setView(view) {
state.view = view;
}
function setSort(sort) {
state.layout.sort = sort;
}
function setTheme(theme) {
state.layout.theme = theme;
}
function setSidebarCollapsed(c) {
state.layout.sidebarCollapsed = c;
}
function setLayout(layout) {
Object.assign(state.layout, layout);
}
// ── Projection helpers (pure) ────────────────────────────────
function visibleNotes(view, sort, query = '', queryTags = []) {
let notes = state.notes.filter((n) => {
// trash view shows deleted
if (view.kind === 'trash')
return n.deletedAt !== null;
// every other view hides deleted
if (n.deletedAt !== null)
return false;
if (view.kind === 'notebook')
return n.notebookId === view.notebookId;
if (view.kind === 'tag')
return n.tags.includes(view.tag || '');
return true; // all / search
});
// secondary tag filter (clicked tag chips in the results pane)
for (const t of queryTags) {
if (t)
notes = notes.filter((n) => n.tags.includes(t));
}
// global search live filter on text content
if (query.trim()) {
const q = query.trim().toLowerCase();
notes = notes.filter((n) => {
const hay = (plainText(n.html) + ' ' + n.title).toLowerCase();
return hay.includes(q);
});
}
// sort
notes = [...notes].sort((a, b) => {
if (a.pinned !== b.pinned)
return a.pinned ? -1 : 1;
if (sort === 'created')
return b.createdAt - a.createdAt;
if (sort === 'alpha')
return a.title.localeCompare(b.title);
return b.updatedAt - a.updatedAt;
});
return notes;
}
function countInTrash() {
return state.notes.filter((n) => n.deletedAt !== null).length;
}
// ── Text extraction (for snippets + search; covers content, never tag text) ──
function htmlToNodes(html) {
const tpl = document.createElement('template');
tpl.innerHTML = html;
return Array.from(tpl.content.childNodes);
}
function plainText(html) {
return plainTextFromNodes(htmlToNodes(html));
}
function plainTextFromNodes(nodes) {
let out = '';
for (const node of nodes) {
if (node.nodeType === Node.TEXT_NODE) {
out += node.textContent || '';
}
else if (node.nodeType === Node.ELEMENT_NODE) {
const el = node;
if (el.tagName === 'BR') {
out += '\n';
continue;
}
const block = ['P', 'DIV', 'H1', 'H2', 'H3', 'LI', 'BLOCKQUOTE', 'TR'].includes(el.tagName);
if (block && out && !out.endsWith('\n'))
out += '\n';
out += plainTextFromNodes(Array.from(el.childNodes));
if (block && !out.endsWith('\n'))
out += '\n';
}
}
return out;
}
/** Build a snippet with {before, match, after} so search can highlight. */
function snippetFor(note, query) {
const body = plainText(note.html).replace(/\s+/g, ' ').trim();
const full = (note.title ? note.title + ' — ' : '') + body;
const q = (query || '').trim().toLowerCase();
const ranges = [];
const text = full.slice(0, types_1.CONST.SNIPPET_LENGTH);
if (q) {
const lower = text.toLowerCase();
let idx = lower.indexOf(q);
while (idx !== -1) {
ranges.push([idx, idx + q.length]);
idx = lower.indexOf(q, idx + q.length);
}
}
return { text, ranges };
}
};
// ── module: src/editor.ts ──
__mods["src/editor.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadNote = loadNote;
exports.clearNote = clearNote;
exports.getCurrentNoteId = getCurrentNoteId;
exports.initEditor = initEditor;
// ─────────────────────────────────────────────────────────────
// editor.ts — contenteditable editor: formatting toolbar, markdown
// shortcuts, checklist handling, list continuation, sanitized paste,
// reliable custom undo/redo. The DOM is a projection of store state.
// ─────────────────────────────────────────────────────────────
const types_1 = require("./types");
const store_1 = require("./store");
let refs;
let noteId = null;
let applying = false;
let undoStack = [];
let redoStack = [];
function pathOf(root, node) {
const path = [];
let n = node;
while (n && n !== root) {
const parent = n.parentNode;
if (!parent)
break;
path.unshift(Array.prototype.indexOf.call(parent.childNodes, n));
n = parent;
}
return path;
}
function nodeAt(root, path) {
let n = root;
for (let i = 0; i < path.length; i++) {
const next = n.childNodes[path[i]];
if (!next)
return null;
n = next;
}
return n;
}
function saveSel() {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return null;
const anchorNode = sel.anchorNode;
const focusNode = sel.focusNode;
if (!anchorNode || !focusNode)
return null;
const root = refs.editor;
if (!root.contains(anchorNode))
return null;
const clamp = (node, off) => node.nodeType === Node.TEXT_NODE ? Math.max(0, Math.min(off, node.nodeValue ? node.nodeValue.length : 0)) : off;
return {
anchor: { path: pathOf(root, anchorNode), offset: clamp(anchorNode, sel.anchorOffset) },
focus: { path: pathOf(root, focusNode), offset: clamp(focusNode, sel.focusOffset) },
};
}
function restoreSel(s) {
if (!s)
return;
try {
const sel = window.getSelection();
if (!sel)
return;
const range = document.createRange();
const a = nodeAt(refs.editor, s.anchor.path);
const f = nodeAt(refs.editor, s.focus.path);
if (!a || !f)
return;
range.setStart(a, s.anchor.offset);
range.setEnd(f, s.focus.offset);
sel.removeAllRanges();
sel.addRange(range);
}
catch { /* ignore */ }
}
function pushHistory() {
undoStack.push({ html: refs.editor.innerHTML, sel: saveSel() });
if (undoStack.length > types_1.CONST.UNDO_LIMIT)
undoStack.shift();
redoStack.length = 0;
}
function setContent(html, sel) {
applying = true;
refs.editor.innerHTML = html;
normalizeTree(refs.editor);
restoreSel(sel);
applying = false;
updatePlaceholder();
}
function undo() {
if (!undoStack.length)
return;
const prev = undoStack.pop();
redoStack.push({ html: refs.editor.innerHTML, sel: saveSel() });
setContent(prev.html, prev.sel);
commit(false);
}
function redo() {
if (!redoStack.length)
return;
const next = redoStack.pop();
undoStack.push({ html: refs.editor.innerHTML, sel: saveSel() });
setContent(next.html, next.sel);
commit(false);
}
// ── public load ──────────────────────────────────────────────
function loadNote(note) {
noteId = note.id;
undoStack = [];
redoStack = [];
applying = true;
refs.title.value = note.title;
refs.editor.innerHTML = note.html;
normalizeTree(refs.editor);
applying = false;
updatePlaceholder();
renderTags();
setSaved('idle');
}
function clearNote() {
noteId = null;
applying = true;
refs.title.value = '';
refs.editor.innerHTML = '';
applying = false;
undoStack = [];
redoStack = [];
renderTags();
updatePlaceholder();
}
function getCurrentNoteId() { return noteId; }
function commit(schedule = true) {
if (!noteId)
return;
(0, store_1.updateNoteHtml)(noteId, refs.editor.innerHTML);
(0, store_1.updateNoteTitle)(noteId, refs.title.value.trim());
if (schedule)
setSaved('saving');
refs.onNoteChanged(noteId);
}
// Mark browser-visible selection-caret update + template fixes inside editor
function updatePlaceholder() {
const empty = !refs.editor.childElementCount && !(refs.editor.textContent || '').trim();
if (empty)
refs.editor.setAttribute('data-empty', 'true');
else
refs.editor.removeAttribute('data-empty');
}
// deletes empty <p><br></p> noise, wraps bare text
function normalizeTree(root) {
// wrap stray text nodes in <p>
for (const node of Array.from(root.childNodes)) {
if (node.nodeType === Node.TEXT_NODE && (node.textContent || '').trim().length) {
const p = document.createElement('p');
root.replaceChild(p, node);
p.appendChild(node);
}
}
}
// ── saved indicator ──────────────────────────────────────────
let savedTimer = 0;
let savedTimeout = 0;
function setSaved(mode) {
clearTimeout(savedTimeout);
refs.savedEl.className = 'saved' + (mode === 'saving' ? ' saving' : mode === 'ok' ? ' saved-ok' : '');
refs.savedText.textContent = mode === 'saving' ? 'Saving…' : 'Saved';
if (mode === 'saving') {
savedTimer = window.setTimeout(() => setSaved('ok'), types_1.CONST.AUTOSAVE_DEBOUNCE_MS + 80);
}
}
// ── caret / block helpers ────────────────────────────────────
function getBlockAtCaret(root) {
const sel = window.getSelection();
if (!sel || !sel.anchorNode)
return null;
let n = sel.anchorNode;
if (n.nodeType === Node.TEXT_NODE && n.parentElement)
n = n.parentElement;
while (n && n !== root) {
if (n instanceof HTMLElement && /^(P|DIV|LI|H1|H2|H3|BLOCKQUOTE|UL|OL)$/.test(n.tagName))
return n;
n = n.parentElement;
}
return root;
}
function textOf(li) {
// exclude checkbox inputs from text
let s = '';
for (const node of Array.from(li.childNodes)) {
if (node instanceof HTMLInputElement)
continue;
s += node.textContent || '';
}
return s;
}
function placeCaretAtEnd(el) {
el.normalize();
const sel = window.getSelection();
if (!sel)
return;
const range = document.createRange();
const node = el.lastChild || el;
const off = node.nodeType === Node.TEXT_NODE ? node.nodeValue ? node.nodeValue.length : 0 : el.childNodes.length - (el.lastChild ? 1 : 0) + 1;
range.setStart(node, node.nodeType === Node.TEXT_NODE ? off : 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
function placeCaretInText(textEl, offset) {
const sel = window.getSelection();
if (!sel)
return;
const range = document.createRange();
range.setStart(textEl, Math.max(0, Math.min(offset, (textEl.textContent || '').length)));
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
// ── markdown shortcuts ───────────────────────────────────────
const MD_TOKENS = [
{ re: /^(### )(.*)$/, tag: 'h3' },
{ re: /^(## )(.*)$/, tag: 'h2' },
{ re: /^(# )(.*)$/, tag: 'h1' },
{ re: /^(\[\] )(.*)$/, tag: 'task' },
{ re: /^(\d+\. )(.*)$/, tag: 'ol' },
{ re: /^(- )(.*)$/, tag: 'ul' },
{ re: /^(> )(.*)$/, tag: 'quote' },
];
function maybeMarkdownAtCaret() {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0 || !sel.anchorNode)
return false;
if (sel.anchorNode !== sel.focusNode || sel.anchorOffset !== sel.focusOffset)
return false; // only collapsed
// caret must be in a text node
const textNode = sel.anchorNode;
if (textNode.nodeType !== Node.TEXT_NODE)
return false;
const block = getBlockAtCaret(refs.editor);
if (!block)
return false;
const lineStart = block.textContent || '';
const prefix = lineStart.slice(0, sel.anchorOffset);
for (const tok of MD_TOKENS) {
// prefix must be exactly the token + a trailing space
const m = tok.re.exec(prefix + ' '); // add sentinel space
if (m && (prefix + ' ') === m[0] && prefix.endsWith(' ')) {
const rest = lineStart.slice(prefix.length);
convertBlock(block, tok.tag, rest);
return true;
}
}
return false;
}
function convertBlock(block, tag, rest) {
let newEl;
if (tag === 'task') {
const ul = document.createElement('ul');
ul.className = 'task-list';
const li = document.createElement('li');
const cb = document.createElement('input');
cb.type = 'checkbox';
const span = document.createElement('span');
span.className = 'task-text';
span.textContent = rest;
li.appendChild(cb);
li.appendChild(span);
ul.appendChild(li);
newEl = ul;
}
else if (tag === 'ul') {
const ul = document.createElement('ul');
const li = document.createElement('li');
li.textContent = rest;
ul.appendChild(li);
newEl = ul;
}
else if (tag === 'ol') {
const ol = document.createElement('ol');
const li = document.createElement('li');
li.textContent = rest;
ol.appendChild(li);
newEl = ol;
}
else if (tag === 'quote') {
newEl = document.createElement('blockquote');
newEl.textContent = rest;
}
else {
newEl = document.createElement(tag);
newEl.textContent = rest;
}
block.replaceWith(newEl);
// place caret at end of new content
const lastText = newEl.lastChild;
if (lastText && lastText.nodeType === Node.TEXT_NODE)
placeCaretInText(lastText, (lastText.textContent || '').length);
else if (newEl.childElementCount)
placeCaretAtEnd(newEl);
else
placeCaretAtEnd(newEl);
}
// ── toolbar commands ─────────────────────────────────────────
function toggleInlineCode() {
pushHistory();
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0 || sel.isCollapsed)
return;
const node = sel.anchorNode;
const inCode = node && node.parentElement && node.parentElement.closest('code');
if (inCode) { // unwrap
const codeEl = inCode;
const range = sel.getRangeAt(0);
const content = codeEl.innerHTML;
range.deleteContents();
const t = document.createElement('span');
t.innerHTML = content;
range.insertNode(t);
codeEl.replaceWith(t);
document.execCommand('selectAll'); // nudge
}
else {
document.execCommand('insertHTML', false, '<code>' + sel.toString() + '</code>');
}
}
function applyCommand(cmd, value) {
pushHistory();
document.execCommand(cmd, false, value);
commit(true);
}
function initEditor(opts) {
refs = opts;
buildToolbar();
// typing + markdown
refs.editor.addEventListener('beforeinput', (e) => {
if (applying)
return;
if (e.inputType && e.inputType.startsWith('history'))
return;
try {
pushHistory();
}
catch { /* */ }
});
refs.editor.addEventListener('input', () => {
if (applying)
return;
applying = true;
try {
maybeMarkdownAtCaret();
refs.editor.normalize();
}
finally {
applying = false;
}
updatePlaceholder();
commit(true);
updateToolbar();
});
// checkbox toggling
refs.editor.addEventListener('input', (e) => {
const t = e.target;
if (t && t.type === 'checkbox') {
t.checked ? t.setAttribute('checked', '') : t.removeAttribute('checked');
const li = t.closest('li');
if (li)
li.classList.toggle('checked', t.checked);
commit(true);
}
});
// keyboard: list continuation + empty exit + undo/redo
refs.editor.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && /^[z]$/i.test(e.key)) {
e.preventDefault();
undo();
return;
}
if ((e.metaKey || e.ctrlKey) && e.shiftKey && /^[z]$/i.test(e.key)) {
e.preventDefault();
redo();
return;
}
if (e.key === 'Enter')
onEnter(e);
});
// title input
refs.title.addEventListener('input', () => {
if (noteId) {
(0, store_1.updateNoteTitle)(noteId, refs.title.value.trim());
refs.onNoteChanged(noteId);
setSaved('saving');
}
});
// tag input
refs.tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const v = refs.tagInput.value.trim();
if (v && noteId) {
(0, store_1.addTag)(noteId, v);
renderTags();
commit(false);
}
refs.tagInput.value = '';
}
else if (e.key === 'Backspace' && !refs.tagInput.value && noteId) {
const n = (0, store_1.getState)().notes.find((x) => x.id === noteId);
if (n && n.tags.length) {
(0, store_1.removeTag)(noteId, n.tags[n.tags.length - 1]);
renderTags();
commit(false);
}
}
});
// paste sanitization
refs.editor.addEventListener('paste', onPaste);
// toolbar state reflection
document.addEventListener('selectionchange', () => {
if (refs.editor.contains(document.activeElement) || (window.getSelection() && refs.editor.contains(window.getSelection().anchorNode))) {
updateToolbar();
}
});
}
function onEnter(e) {
const block = getBlockAtCaret(refs.editor);
if (!block || block.tagName !== 'LI')
return;
const list = block.parentElement;
if (!list)
return;
if (list.classList.contains('task-list')) {
e.preventDefault();
pushHistory();
const isEmpty = textOf(block).trim() === '';
if (isEmpty) {
// remove this li; if it's the only one, replace list with empty p after
list.removeChild(block);
const p = document.createElement('p');
p.innerHTML = '<br>';
if (list.childElementCount === 0) {
list.replaceWith(p);
}
else {
list.parentElement.insertBefore(p, list.nextSibling);
}
const sel = window.getSelection();
const r = document.createRange();
r.setStart(p, 0);
r.collapse(true);
sel.removeAllRanges();
sel.addRange(r);
}
else {
const li = document.createElement('li');
const cb = document.createElement('input');
cb.type = 'checkbox';
const span = document.createElement('span');
span.className = 'task-text';
li.appendChild(cb);
li.appendChild(span);
list.insertBefore(li, block.nextSibling);
const sel = window.getSelection();
const r = document.createRange();
r.setStart(span, 0);
r.collapse(true);
sel.removeAllRanges();
sel.addRange(r);
}
commit(true);
}
}
// ── paste sanitization ───────────────────────────────────────
const ALLOWED = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'UL', 'OL', 'LI', 'BLOCKQUOTE', 'BR', 'HR', 'STRONG', 'B', 'EM', 'I', 'U', 'S', 'STRIKE', 'DEL', 'CODE', 'PRE', 'SPAN', 'A', 'INPUT']);
function sanitizeNode(node, outParent) {
if (node.nodeType === Node.TEXT_NODE) {
const t = document.createTextNode(node.textContent || '');
if (outParent)
outParent.appendChild(t);
return t;
}
if (node.nodeType !== Node.ELEMENT_NODE)
return null;
const el = node;
const tag = el.tagName;
if (!ALLOWED.has(tag)) {
// unwrap: copy children into parent
for (const child of Array.from(el.childNodes))
sanitizeNode(child, outParent);
return null;
}
let targetTag = tag;
if (tag === 'B')
targetTag = 'STRONG';
if (tag === 'I')
targetTag = 'EM';
if (tag === 'STRIKE' || tag === 'DEL')
targetTag = 'S';
if (tag === 'DIV')
targetTag = 'P';
if (tag === 'PRE')
targetTag = 'CODE';
const n = document.createElement(targetTag);
if (tag === 'A' && el.getAttribute && el.getAttribute('href'))
n.setAttribute('href', el.getAttribute('href'));
if (tag === 'INPUT') {
const inp = document.createElement('input');
inp.type = 'checkbox';
if (el.checked)
inp.setAttribute('checked', '');
if (outParent)
outParent.appendChild(inp);
return inp;
}
if (targetTag === 'UL' && el.classList.contains('task-list'))
n.classList.add('task-list');
for (const child of Array.from(el.childNodes)) {
if (child instanceof HTMLInputElement && child.type === 'checkbox') {
const inp = document.createElement('input');
inp.type = 'checkbox';
if (child.checked)
inp.setAttribute('checked', '');
n.appendChild(inp);
}
else {
const c = sanitizeNode(child, null);
if (c)
n.appendChild(c);
}
}
if (outParent)
outParent.appendChild(n);
return n;
}
function sanitizeHtml(htmlText) {
const doc = new DOMParser().parseFromString(htmlText, 'text/html');
const build = document.createElement('div');
for (const child of Array.from(doc.body.childNodes)) {
const out = sanitizeNode(child, build);
if (out && !out.parentNode)
build.appendChild(out);
}
return build.innerHTML || '';
}
function onPaste(e) {
e.preventDefault();
pushHistory();
const cd = e.clipboardData;
if (!cd)
return;
const html = cd.getData('text/html');
const text = cd.getData('text/plain');
if (html && html.trim()) {
const clean = sanitizeHtml(html);
if (clean)
document.execCommand('insertHTML', false, clean);
else if (text)
insertPlainText(text);
}
else if (text) {
insertPlainText(text);
}
commit(true);
}
function insertPlainText(text) {
const escaped = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
document.execCommand('insertHTML', false, escaped);
}
// ── toolbar UI ───────────────────────────────────────────────
function tbBtn(cmd, inner, title, cls = '') {
const b = document.createElement('button');
b.className = 'tb-btn' + (cls ? ' ' + cls : '');
b.dataset.cmd = cmd;
b.title = title;
b.innerHTML = inner;
return b;
}
function sep() { const d = document.createElement('div'); d.className = 'tb-sep'; return d; }
function buildToolbar() {
const t = refs.toolbar;
t.innerHTML = '';
const g1 = document.createElement('div');
g1.className = 'tb-group';
g1.append(tbBtn('bold', '<b>B</b>', 'Bold (Ctrl+B)'), tbBtn('italic', '<i>I</i>', 'Italic (Ctrl+I)'), tbBtn('underline', '<u>U</u>', 'Underline (Ctrl+U)'), tbBtn('strikeThrough', '<s>S</s>', 'Strikethrough'), tbBtn('code', '<span class="code-glyph">{ }</span>', 'Inline code'));
const g2 = document.createElement('div');
g2.className = 'tb-group';
g2.append(tbBtn('h1', 'H<span class="sep-label">1</span>', 'Heading 1', 'head'), tbBtn('h2', 'H<span class="sep-label">2</span>', 'Heading 2', 'head'), tbBtn('h3', 'H<span class="sep-label">3</span>', 'Heading 3', 'head'));
const g3 = document.createElement('div');
g3.className = 'tb-group';
g3.append(tbBtn('ul', '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 6h12M8 12h12M8 18h12"/><circle cx="4" cy="6" r="1.2" fill="currentColor"/><circle cx="4" cy="12" r="1.2" fill="currentColor"/><circle cx="4" cy="18" r="1.2" fill="currentColor"/></svg>', 'Bulleted list'), tbBtn('ol', '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10 6h10M10 12h10M10 18h10"/><path d="M4 5h2v3M4 12h2v3c0 .5-.3.8-.8.8H4"/></svg>', 'Numbered list'), tbBtn('task', '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M9 12l2 2 4-4"/></svg>', 'Checklist'), tbBtn('quote', '<span class="quote-glyph">“</span>', 'Blockquote', 'quote-glyph'), tbBtn('hr', '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 12h16"/><path d="M4 5h6M14 5h6M4 19h6M14 19h6"/></svg>', 'Horizontal rule'));
t.append(g1, sep(), g2, sep(), g3);
const spacer = document.createElement('div');
spacer.className = 'tb-spacer';
t.append(spacer, refs.savedEl);
t.addEventListener('click', (e) => {
const btn = e.target.closest('.tb-btn');
if (!btn)
return;
const cmd = btn.dataset.cmd;
if (cmd === 'bold' || cmd === 'italic' || cmd === 'underline' || cmd === 'strikeThrough')
applyCommand(cmd);
else if (cmd === 'code') {
toggleInlineCode();
commit(true);
updateToolbar();
}
else if (cmd === 'h1' || cmd === 'h2' || cmd === 'h3') {
const block = getBlockAtCaret(refs.editor);
const isActive = block && block.tagName === cmd.toUpperCase();
applyCommand('formatBlock', isActive ? 'p' : cmd);
updateToolbar();
}
else if (cmd === 'ul') {
applyCommand('insertUnorderedList');
commit(true);
updateToolbar();
}
else if (cmd === 'ol') {
applyCommand('insertOrderedList');
commit(true);
updateToolbar();
}
else if (cmd === 'quote') {
const block = getBlockAtCaret(refs.editor);
const isActive = block && block.tagName === 'BLOCKQUOTE';
applyCommand('formatBlock', isActive ? 'p' : 'blockquote');
updateToolbar();
}
else if (cmd === 'task') {
toggleChecklistToolbar();
}
else if (cmd === 'hr') {
applyCommand('insertHorizontalRule');
commit(true);
updateToolbar();
}
refs.editor.focus();
});
}
function toggleChecklistToolbar() {
pushHistory();
const blocks = selectedBlocks();
const paragraphs = blocks.filter((b) => b.tagName === 'P');
const allTask = blocks.length && blocks.every((b) => b.closest('ul.task-list'));
if (allTask) {
// convert task list back to bullets (unwrap class + remove checkboxes)
for (const b of blocks) {
const li = b.closest('li');
if (!li)
continue;
for (const cb of Array.from(li.querySelectorAll('input[type=checkbox]')))
cb.remove();
const ul = li.parentElement;
if (ul && ul.classList.contains('task-list'))
ul.classList.remove('task-list');
}
}
else if (paragraphs.length) {
const ul = document.createElement('ul');
ul.className = 'task-list';
for (const p of paragraphs) {
const li = document.createElement('li');
const cb = document.createElement('input');
cb.type = 'checkbox';
const span = document.createElement('span');
span.className = 'task-text';
span.innerHTML = p.innerHTML;
li.appendChild(cb);
li.appendChild(span);
ul.appendChild(li);
p.replaceWith(li);
}
const first = paragraphs[0];
first.parentElement.insertBefore(ul, first);
}
else {
// make current single block a task item
const block = getBlockAtCaret(refs.editor);
if (block) {
const ul = document.createElement('ul');
ul.className = 'task-list';
const li = document.createElement('li');
const cb = document.createElement('input');
cb.type = 'checkbox';
const span = document.createElement('span');
span.className = 'task-text';
span.innerHTML = block.innerHTML;
li.appendChild(cb);
li.appendChild(span);
ul.appendChild(li);
block.replaceWith(ul);
}
}
commit(true);
updateToolbar();
}
function selectedBlocks() {
const sel = window.getSelection();
const blocks = [];
if (!sel || sel.rangeCount === 0)
return blocks;
const range = sel.getRangeAt(0);
let node = range.startContainer;
const seen = new Set();
const walk = (n) => {
if (n.nodeType === Node.ELEMENT_NODE) {
const el = n;
if (/^(P|LI|H1|H2|H3|BLOCKQUOTE)$/.test(el.tagName) && !seen.has(el)) {
seen.add(el);
blocks.push(el);
}
}
for (const c of Array.from(n.childNodes))
walk(c);
};
if (range.startContainer instanceof HTMLElement)
walk(range.startContainer);
// simple: walk the nearest common ancestor
const common = range.commonAncestorContainer instanceof HTMLElement ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement;
if (common) {
seen.clear();
blocks.length = 0;
walk(common);
}
return blocks;
}
// ── toolbar state reflection ─────────────────────────────────
function updateToolbar() {
const btns = Array.from(refs.toolbar.querySelectorAll('.tb-btn'));
const q = (cmd) => { try {
return document.queryCommandState(cmd);
}
catch {
return false;
} };
const bolds = { bold: q('bold'), italic: q('italic'), underline: q('underline'), strikeThrough: q('strikeThrough') };
for (const b of btns) {
const cmd = b.dataset.cmd;
let on = false;
if (cmd in bolds)
on = bolds[cmd];
else if (cmd === 'code')
on = !!inCode();
else if (cmd === 'h1' || cmd === 'h2' || cmd === 'h3') {
const block = getBlockAtCaret(refs.editor);
on = !!block && block.tagName === cmd.toUpperCase();
}
else if (cmd === 'quote') {
const block = getBlockAtCaret(refs.editor);
on = !!block && block.tagName === 'BLOCKQUOTE';
}
else if (cmd === 'ul' || cmd === 'ol' || cmd === 'task') {
const li = closestLi(getBlockAtCaret(refs.editor));
if (!li)
on = false;
else if (cmd === 'ul')
on = li.parentElement?.tagName === 'UL' && !li.parentElement.classList.contains('task-list');
else if (cmd === 'ol')
on = li.parentElement?.tagName === 'OL';
else
on = !!li.closest('ul.task-list');
}
b.classList.toggle('active', !!on);
}
}
function closestLi(b) {
let n = b;
while (n && !/^(LI|P|H1|H2|H3|BLOCKQUOTE|UL|OL)$/.test(n.tagName))
n = n.parentElement;
return n;
}
function inCode() {
const sel = window.getSelection();
if (!sel || !sel.anchorNode || !sel.anchorNode.parentElement)
return false;
return !!sel.anchorNode.parentElement.closest('code');
}
// ── tags rendering ───────────────────────────────────────────
function renderTags() {
refs.tagWrap.innerHTML = '';
if (!noteId)
return;
const note = (0, store_1.getState)().notes.find((n) => n.id === noteId);
if (!note)
return;
for (const tag of note.tags) {
const chip = document.createElement('span');
chip.className = 'tag-chip clickable';
chip.innerHTML = '<span class="hash">#</span><span>' + escapeHtml(tag) + '</span><span class="x" data-tag="' + escapeHtml(tag) + '">×</span>';
chip.title = 'Filter by #' + tag;
chip.addEventListener('click', (e) => {
if (e.target.classList.contains('x')) {
if (noteId) {
(0, store_1.removeTag)(noteId, tag);
renderTags();
commit(false);
}
e.stopPropagation();
}
else {
refs.onTagClick(tag);
}
});
refs.tagWrap.appendChild(chip);
}
}
function escapeHtml(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports._setView = void 0;
exports.showToast = showToast;
exports.setQueryInput = setQueryInput;
exports.renderAll = renderAll;
exports.bindUI = bindUI;
exports.focusTitle = focusTitle;
exports.trash = trash;
exports.deleteSelectedNote = deleteSelectedNote;
exports.getQtagFilter = getQtagFilter;
exports.setQtagFilter = setQtagFilter;
exports.showEditorEmpty = showEditorEmpty;
exports.activeViewKind = activeViewKind;
// ─────────────────────────────────────────────────────────────
// ui.ts — sidebar, notes list, search, tags, context menus,
// sort menu, drag-and-drop. Rendering is driven entirely by
// store state (DOM is a projection). Editor loading is delegated.
// ─────────────────────────────────────────────────────────────
const store_1 = require("./store");
Object.defineProperty(exports, "_setView", { enumerable: true, get: function () { return store_1.setView; } });
const editor_1 = require("./editor");
// ── transient UI state ───────────────────────────────────────
let query = '';
let qtagFilter = null;
const $ = (sel) => document.querySelector(sel);
function titleOf(view) {
if (view.kind === 'notebook') {
const n = (0, store_1.findNotebook)(view.notebookId ?? null);
return n ? n.name : 'Notes';
}
if (view.kind === 'tag')
return '#' + (view.tag || '');
if (view.kind === 'trash')
return 'Trash';
return 'All Notes';
}
// ── selection + navigation ───────────────────────────────────
function selectNote(id) {
(0, store_1.setSelectedNote)(id);
const note = (0, store_1.findNote)(id);
renderAll();
if (note && note.deletedAt === null)
(0, editor_1.loadNote)(note);
else if (note) {
(0, editor_1.clearNote)();
showEditorEmpty(true);
}
(0, store_1.save)();
}
function goTo(view) {
(0, store_1.setView)(view);
qtagFilter = null;
renderAll();
(0, store_1.save)();
}
function clearQuery() { query = ''; setQueryInput(); renderAll(); }
function activeViewKind() {
return (0, store_1.getState)().view.kind;
}
function isTrashView() { return activeViewKind() === 'trash'; }
// ── sidebar render ───────────────────────────────────────────
function renderSidebar() {
const s = (0, store_1.getState)();
const counts = { all: 0, trash: (0, store_1.countInTrash)() };
s.notes.forEach((n) => { if (n.deletedAt === null)
counts.all++; });
const allBtn = $('#nav-all');
const trashBtn = $('#nav-trash');
allBtn.classList.toggle('active', !query && s.view.kind === 'all');
trashBtn.classList.toggle('active', !query && s.view.kind === 'trash');
$('#count-all').textContent = counts.all <= 0 ? '' : String(counts.all);
$('#count-trash').textContent = counts.trash <= 0 ? '' : String(counts.trash);
// tags
const tagCounts = new Map();
s.notes.forEach((n) => { if (n.deletedAt === null)
n.tags.forEach((t) => tagCounts.set(t, (tagCounts.get(t) || 0) + 1)); });
const tags = Array.from(tagCounts.entries()).sort((a, b) => a[0].localeCompare(b[0]));
const tagList = $('#tag-list');
tagList.innerHTML = '';
if (!tags.length) {
const empty = document.createElement('div');
empty.className = 'tag-item';
empty.style.color = 'var(--text-faint)';
empty.textContent = 'No tags yet';
tagList.appendChild(empty);
}
for (const [tag, n] of tags) {
const row = document.createElement('div');
row.className = 'tag-item' + (s.view.kind === 'tag' && s.view.tag === tag && !query ? ' active' : '');
row.innerHTML = '<span class="tag-hash">#</span><span>' + escapeHtml(tag) + '</span><span class="tag-count">' + n + '</span>';
row.addEventListener('click', () => goTo({ kind: 'tag', tag }));
tagList.appendChild(row);
}
// notebooks
const list = $('#notebook-list');
list.innerHTML = '';
if (!s.notebooks.length) {
const empty = document.createElement('div');
empty.className = 'tag-item';
empty.style.color = 'var(--text-faint)';
empty.textContent = 'No notebooks';
list.appendChild(empty);
}
for (const nb of s.notebooks)
list.appendChild(buildNotebookRow(nb));
}
function buildNotebookRow(nb) {
const row = document.createElement('div');
row.className = 'nb-row' + ((0, store_1.getState)().view.kind === 'notebook' && (0, store_1.getState)().view.notebookId === nb.id ? ' active' : '') + (nb.expanded ? '' : ' collapsed');
row.dataset.drop = 'notebook';
row.dataset.id = nb.id;
const caret = document.createElement('button');
caret.className = 'nb-caret';
caret.innerHTML = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>';
caret.addEventListener('click', (e) => { e.stopPropagation(); (0, store_1.toggleNotebookExpanded)(nb.id); renderAll(); (0, store_1.save)(); });
const main = document.createElement('div');
main.className = 'nb-main';
main.innerHTML = '<span class="nb-ico"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></span><span class="nb-name">' + escapeHtml(nb.name) + '</span>';
const ncount = (0, store_1.getState)().notes.filter((n) => n.notebookId === nb.id && n.deletedAt === null).length;
main.insertAdjacentHTML('beforeend', '<span class="nb-count">' + ncount + '</span>');
const actions = document.createElement('div');
actions.className = 'nb-actions';
actions.innerHTML = '<button class="nb-act" data-act="rename" title="Rename">✎</button><button class="nb-act" data-act="delete" title="Delete">🗑</button>';
actions.addEventListener('click', (e) => {
const b = e.target.closest('.nb-act');
if (!b)
return;
e.stopPropagation();
if (b.dataset.act === 'rename')
startRenameNotebook(main, nb);
else
openNotebookMenu(b.getBoundingClientRect(), nb.id);
});
main.appendChild(actions);
main.addEventListener('click', (e) => {
if (e.target.closest('.nb-act'))
return;
goTo({ kind: 'notebook', notebookId: nb.id });
});
main.addEventListener('contextmenu', (e) => { e.preventDefault(); openNotebookMenu(e, nb.id); });
main.addEventListener('dragover', (e) => { e.preventDefault(); row.classList.add('drop-ontop'); });
main.addEventListener('dragleave', () => row.classList.remove('drop-ontop'));
main.addEventListener('drop', (e) => { e.preventDefault(); row.classList.remove('drop-ontop'); onDropNotebook(e, nb.id); });
row.appendChild(caret);
row.appendChild(main);
if (nb.expanded) {
const sub = document.createElement('div');
sub.className = 'nb-notes';
const notes = (0, store_1.visibleNotes)({ kind: 'notebook', notebookId: nb.id }, (0, store_1.getState)().layout.sort).slice(0, 120);
if (!notes.length) {
const hint = document.createElement('div');
hint.className = 'nb-note-item';
hint.style.color = 'var(--text-faint)';
hint.style.fontSize = '11.5px';
hint.style.padding = '2px 4px';
hint.textContent = 'Empty notebook';
sub.appendChild(hint);
}
for (const n of notes) {
sub.appendChild(buildNotebookNote(n));
}
row.appendChild(sub);
}
return row;
}
function buildNotebookNote(n) {
const item = document.createElement('div');
item.className = 'nb-note-item';
const link = document.createElement('button');
link.className = 'nb-note-link' + ((0, store_1.getState)().selectedNoteId === n.id ? ' active' : '');
link.textContent = n.title || 'Untitled';
link.title = n.title || 'Untitled';
link.addEventListener('click', () => { (0, store_1.setView)({ kind: 'notebook', notebookId: n.notebookId }); selectNote(n.id); });
const meta = document.createElement('span');
meta.className = 'nb-note-meta';
meta.textContent = n.pinned ? '★' : '';
item.append(link, meta);
return item;
}
function startRenameNotebook(main, nb) {
const nameEl = main.querySelector('.nb-name');
const name = nb.name;
const input = document.createElement('input');
input.className = 'rename-input';
input.value = name;
nameEl.replaceWith(input);
input.focus();
input.select();
const finish = (commit) => {
const v = input.value.trim();
if (commit && v) {
(0, store_1.renameNotebook)(nb.id, v);
(0, store_1.save)();
}
renderAll();
};
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter')
finish(true);
else if (e.key === 'Escape')
finish(false);
e.stopPropagation();
});
input.addEventListener('blur', () => finish(true));
}
// ── notes list render ────────────────────────────────────────
function renderNotes() {
const s = (0, store_1.getState)();
const effectiveView = query.trim() ? { kind: 'all' } : s.view;
const notes = (0, store_1.visibleNotes)(effectiveView, s.layout.sort, query.trim(), qtagFilter ? [qtagFilter] : []);
$('#notes-head-title').textContent = query.trim() ? 'Search results' : titleOf(s.view);
// tag filter bar
const bar = $('#notes-toolbar');
const label = $('#notes-tb-label');
if (qtagFilter) {
bar.hidden = false;
label.innerHTML = '<span class="hash">#</span> ' + escapeHtml(qtagFilter) + ' · ' + notes.length + (notes.length === 1 ? ' note' : ' notes');
}
else if (query.trim()) {
bar.hidden = false;
label.textContent = '“' + query.trim() + '” · ' + notes.length + (notes.length === 1 ? ' match' : ' matches');
}
else {
bar.hidden = true;
}
const list = $('#notes-list');
const empty = $('#notes-empty');
list.innerHTML = '';
if (!notes.length) {
empty.hidden = false;
const q = query.trim();
empty.innerHTML =
'<div class="ne-ico"><svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 21l-4.3-4.3"/><circle cx="11" cy="11" r="7"/></svg></div>' +
(q ? '<h3>No results for “' + escapeHtml(q) + '”</h3><p>Try a different search, or clear the filter.</p>'
: isTrashView() ? '<h3>Trash is empty</h3><p>Notes you delete will appear here.</p>'
: '<h3>Nothing here yet</h3><p>Create a new note to get started.</p>');
return;
}
empty.hidden = true;
for (const note of notes)
list.appendChild(buildNoteItem(note));
}
function buildNoteItem(note) {
const item = document.createElement('div');
item.className = 'note-item' + ((0, store_1.getState)().selectedNoteId === note.id ? ' selected' : '') + (note.pinned ? ' pinned' : '');
item.draggable = true;
item.dataset.nid = note.id;
const q = query.trim();
const snip = (0, store_1.snippetFor)(note, q);
// pins first (sort already handles), just render
let titleHtml = escapeHtml(note.title || 'Untitled');
if (q)
titleHtml = highlight(titleHtml, note.title, q);
let snipHtml = escapeHtml(snip.text);
if (q && snip.ranges.length) {
snipHtml = escapeHtml(snip.text);
// rebuild with ranges
let out = '';
let last = 0;
for (const [a, b] of snip.ranges) {
out += escapeHtml(snip.text.slice(last, a)) + '<em class="match">' + escapeHtml(snip.text.slice(a, b)) + '</em>';
last = b;
}
out += escapeHtml(snip.text.slice(last));
snipHtml = out;
}
const tagsHtml = note.tags.map((t) => '<span class="ni-tag">#' + escapeHtml(t) + '</span>').join('');
const toast = note.deletedAt !== null ? '<span class="trash-badge">Trash</span>' : '';
item.innerHTML =
'<div class="note-item-top">' +
'<span class="ni-pin"><svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M16 3l5 5-4 1-4 4-1 5-4-4-3 3-1-1 3-3-4-4 5-1 4-4z"/></svg></span>' +
'<span class="ni-title">' + titleHtml + '</span>' +
'<button class="ni-pin-btn' + (note.pinned ? ' pinned' : '') + '" title="' + (note.pinned ? 'Unpin' : 'Pin') + '"><svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor"><path d="M16 3l5 5-4 1-4 4-1 5-4-4-3 3-1-1 3-3-4-4 5-1 4-4z"/></svg></button>' +
'</div>' +
'<div class="ni-snippet">' + (snipHtml || '<span style="color:var(--text-faint)">No text</span>') + '</div>' +
'<div class="ni-meta"><span>' + formatTime(note) + '</span><span class="dot"></span>' + (note.pinned ? '<span>Pinned</span>' + '<span class="dot"></span>' : '') + '<span class="ni-tags">' + tagsHtml + '</span>' + toast + '</div>';
item.addEventListener('click', () => selectNote(note.id));
const pinBtn = item.querySelector('.ni-pin-btn');
pinBtn.addEventListener('click', (e) => { e.stopPropagation(); (0, store_1.setPinned)(note.id, !note.pinned); renderAll(); (0, store_1.save)(); });
item.addEventListener('contextmenu', (e) => { e.preventDefault(); openNoteMenu(e, note.id); });
item.addEventListener('dragstart', (e) => { onDragStart(e, note.id); });
item.addEventListener('dragend', () => { document.querySelectorAll('.dragging-source').forEach((x) => x.classList.remove('dragging-source')); });
return item;
}
function highlight(titleHtml, rawTitle, q) {
const low = rawTitle.toLowerCase();
const ql = q.toLowerCase();
if (low.indexOf(ql) === -1)
return titleHtml;
let out = '';
let last = 0;
let idx = low.indexOf(ql);
while (idx !== -1) {
out += escapeHtml(rawTitle.slice(last, idx)) + '<em class="match">' + escapeHtml(rawTitle.slice(idx, idx + ql.length)) + '</em>';
last = idx + ql.length;
idx = low.indexOf(ql, last);
}
out += escapeHtml(rawTitle.slice(last));
return out;
}
function formatTime(n) {
const t = n.deletedAt !== null ? n.deletedAt : n.updatedAt;
const diff = Date.now() - t;
const d = new Date(t);
const mins = Math.floor(diff / 60000);
if (mins < 1)
return 'just now';
if (mins < 60)
return mins + 'm ago';
const hrs = Math.floor(mins / 60);
if (hrs < 24)
return hrs + 'h ago';
const days = Math.floor(hrs / 24);
if (days === 1)
return 'yesterday';
if (days < 7)
return days + 'd ago';
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}
// ── menus ────────────────────────────────────────────────────
function closeMenus() {
document.querySelectorAll('.menu').forEach((m) => m.remove());
}
function placeMenu(menu, x, y) {
document.body.appendChild(menu);
const r = menu.getBoundingClientRect();
let left = x, top = y;
if (left + r.width > window.innerWidth - 8)
left = window.innerWidth - r.width - 8;
if (top + r.height > window.innerHeight - 8)
top = window.innerHeight - r.height - 8;
menu.style.left = Math.max(8, left) + 'px';
menu.style.top = Math.max(8, top) + 'px';
}
function openNoteMenu(e, noteId) {
closeMenus();
const note = (0, store_1.findNote)(noteId);
if (!note)
return;
const menu = document.createElement('div');
menu.className = 'menu';
const inTrash = note.deletedAt !== null;
menu.innerHTML =
'<div class="menu-sublabel">' + escapeHtml(note.title || 'Untitled') + '</div>' +
(inTrash ?
'<button class="menu-item" data-a="restore"><span>↩</span><span class="mi-label">Restore</span></button>' +
'<div class="menu-sep"></div>' +
'<button class="menu-item danger" data-a="purge"><span>🗑</span><span class="mi-label">Delete forever</span></button>'
:
'<button class="menu-item" data-a="open"><span>✎</span><span class="mi-label">Open</span></button>' +
'<div class="menu-sublabel">Move to</div>' +
moveToItems(note) +
'<div class="menu-sep"></div>' +
'<button class="menu-item" data-a="pin"><span>📌</span><span class="mi-label">' + (note.pinned ? 'Unpin' : 'Pin') + '</span></button>' +
'<button class="menu-item" data-a="dupe"><span>⧉</span><span class="mi-label">Duplicate</span></button>' +
'<button class="menu-item" data-a="tags"><span>#</span><span class="mi-label">Add tag…</span></button>' +
'<div class="menu-sep"></div>' +
'<button class="menu-item danger" data-a="trash"><span>🗑</span><span class="mi-label">Delete</span><span class="mi-kbd">Del</span></button>');
menu.addEventListener('click', (ev) => {
const b = ev.target.closest('.menu-item');
if (!b)
return;
const a = b.dataset.a;
if (a === 'open')
selectNote(noteId);
else if (a === 'pin') {
(0, store_1.setPinned)(noteId, !note.pinned);
(0, store_1.save)();
renderAll();
}
else if (a === 'dupe') {
const c = (0, store_1.duplicateNote)(noteId);
(0, store_1.save)();
renderAll();
selectNote(c.id);
}
else if (a === 'trash')
trash();
else if (a === 'restore') {
(0, store_1.restoreNote)(noteId);
(0, store_1.save)();
renderAll();
}
else if (a === 'purge') {
(0, store_1.permanentDelete)(noteId);
(0, store_1.save)();
renderAll();
if ((0, editor_1.getCurrentNoteId)() === noteId) {
(0, editor_1.clearNote)();
showEditorEmpty(true);
}
}
else if (a && a.startsWith('move:')) {
(0, store_1.moveNoteTo)(noteId, a.slice(5) === '__none__' ? null : a.slice(5));
(0, store_1.save)();
renderAll();
}
else if (a === 'tags') {
$('#tag-input')?.focus();
}
closeMenus();
});
placeMenu(menu, e.clientX, e.clientY);
setTimeout(() => document.addEventListener('mousedown', onDocDown, { once: true }), 0);
}
function onDocDown(e) {
if (!e.target.closest('.menu'))
closeMenus();
}
function moveToItems(note) {
const s = (0, store_1.getState)();
let out = '';
out += '<button class="menu-item" data-a="move:__none__"><span>🗂</span><span class="mi-label">All Notes</span>' + (note.notebookId === null ? ' <span class="check">✓</span>' : '') + '</button>';
for (const nb of s.notebooks) {
out += '<button class="menu-item" data-a="move:' + nb.id + '"><span>🗂</span><span class="mi-label">' + escapeHtml(nb.name) + '</span>' + (note.notebookId === nb.id ? ' <span class="check">✓</span>' : '') + '</button>';
}
// snippets
return out;
}
function openNotebookMenu(e, notebookId) {
closeMenus();
const menu = document.createElement('div');
menu.className = 'menu';
const nb = (0, store_1.findNotebook)(notebookId);
const name = nb ? nb.name : 'Notebook';
menu.innerHTML =
'<div class="menu-sublabel">' + escapeHtml(name) + '</div>' +
'<button class="menu-item" data-a="newnote"><span>+</span><span class="mi-label">New note</span></button>' +
'<button class="menu-item" data-a="rename"><span>✎</span><span class="mi-label">Rename</span></button>' +
'<div class="menu-sep"></div>' +
'<button class="menu-item danger" data-a="delete"><span>🗑</span><span class="mi-label">Delete notebook</span></button>';
menu.addEventListener('click', (ev) => {
const b = ev.target.closest('.menu-item');
if (!b)
return;
closeMenus();
const a = b.dataset.a;
if (a === 'newnote') {
const n = (0, store_1.createNote)(notebookId);
(0, store_1.save)();
renderAll();
selectNote(n.id);
focusTitle();
}
else if (a === 'rename') {
const main = document.querySelector('.nb-row[data-id="' + notebookId + '"] .nb-main');
if (main)
startRenameNotebook(main, nb || { id: notebookId, name, expanded: true });
}
else if (a === 'delete') {
(0, store_1.deleteNotebook)(notebookId);
(0, store_1.save)();
renderAll();
if ((0, editor_1.getCurrentNoteId)()) {
const cur = (0, store_1.findNote)((0, editor_1.getCurrentNoteId)());
if (cur && cur.notebookId === notebookId) {
(0, editor_1.clearNote)();
showEditorEmpty(true);
}
}
}
});
const x = e instanceof MouseEvent ? e.clientX : e.left;
const y = e instanceof MouseEvent ? e.clientY : e.bottom + 4;
placeMenu(menu, x, y);
setTimeout(() => document.addEventListener('mousedown', onDocDown, { once: true }), 0);
}
function showSortMenu(anchor) {
closeMenus();
const menu = document.createElement('div');
menu.className = 'menu sort-menu';
const cur = (0, store_1.getState)().layout.sort;
const items = [['modified', 'Date modified'], ['created', 'Date created'], ['alpha', 'Title A–Z']];
for (const [ord, label] of items) {
const b = document.createElement('button');
b.className = 'menu-item';
b.innerHTML = '<span class="check">' + (cur === ord ? '✓' : '') + '</span><span class="mi-label">' + label + '</span>';
b.addEventListener('click', () => { (0, store_1.setSort)(ord); (0, store_1.save)(); renderAll(); closeMenus(); });
menu.appendChild(b);
}
placeMenu(menu, anchor.getBoundingClientRect().right - 180, anchor.getBoundingClientRect().bottom + 4);
setTimeout(() => document.addEventListener('mousedown', () => closeMenus(), { once: true }), 0);
}
// ── drag & drop: note → notebook / trash ─────────────────────
let dragNoteId = null;
function onDragStart(e, noteId) {
dragNoteId = noteId;
const note = (0, store_1.findNote)(noteId);
if (!note)
return;
e.dataTransfer?.setData('text/plain', noteId);
e.dataTransfer.effectAllowed = 'move';
e.currentTarget.classList.add('dragging-source');
}
function onDropNotebook(e, notebookId) {
const id = e.dataTransfer?.getData('text/plain') || dragNoteId;
if (!id)
return;
const note = (0, store_1.findNote)(id);
if (note) {
if (note.deletedAt !== null)
(0, store_1.restoreNote)(id);
(0, store_1.moveNoteTo)(id, notebookId);
(0, store_1.save)();
renderAll();
if ((0, editor_1.getCurrentNoteId)() === id) {
const nn = (0, store_1.findNote)(id);
if (nn)
(0, editor_1.loadNote)(nn);
}
}
}
// ── toast ────────────────────────────────────────────────────
let toastTimer = 0;
function showToast(msg) {
document.querySelectorAll('.toast').forEach((t) => t.remove());
const t = document.createElement('div');
t.className = 'toast';
t.textContent = msg;
document.body.appendChild(t);
clearTimeout(toastTimer);
toastTimer = window.setTimeout(() => t.remove(), 1800);
}
// ── editor empty state ───────────────────────────────────────
function showEditorEmpty(show) { $('#editor-empty').hidden = !show; }
function setQueryInput() { $('#global-search').value = query; }
// ── main render ──────────────────────────────────────────────
function renderAll() {
renderSidebar();
renderNotes();
}
// ── bind static UI ───────────────────────────────────────────
function bindUI(handlers) {
// search
const search = $('#global-search');
search.addEventListener('input', () => {
query = search.value.trim();
qtagFilter = null;
$('#search-clear').hidden = !query;
renderNotes();
renderSidebar();
});
$('#search-clear').addEventListener('click', () => { clearQuery(); });
// nav
$('#nav-all').addEventListener('click', () => goTo({ kind: 'all' }));
$('#nav-trash').addEventListener('click', () => goTo({ kind: 'trash' }));
// new notebook
$('#new-notebook').addEventListener('click', () => {
const nb = (0, store_1.createNotebook)('New notebook');
(0, store_1.save)();
renderAll();
const main = document.querySelector('.nb-row[data-id="' + nb.id + '"] .nb-main');
if (main)
startRenameNotebook(main, nb);
});
// new note
$('#new-note').addEventListener('click', () => handlers.onNewNote(currentNotebookForNew()));
$('#empty-new').addEventListener('click', () => handlers.onNewNote(currentNotebookForNew()));
// sort
$('#sort-toggle').addEventListener('click', (e) => { e.stopPropagation(); showSortMenu($('#sort-toggle')); });
// notes-toolbar clear
$('#notes-tb-clear').addEventListener('click', () => { qtagFilter = null; renderAll(); });
// global document menu close
document.addEventListener('click', () => { });
}
function currentNotebookForNew() {
const s = (0, store_1.getState)();
if (s.view.kind === 'notebook')
return s.view.notebookId || null;
const cur = (0, store_1.findNote)(s.selectedNoteId);
if (cur && cur.notebookId)
return cur.notebookId;
return s.notebooks[0]?.id || null;
}
function focusTitle() { $('#note-title').focus(); }
function trash() {
const id = (0, editor_1.getCurrentNoteId)();
if (!id)
return;
(0, store_1.trashNote)(id);
(0, store_1.save)();
(0, store_1.setSelectedNote)(null);
(0, editor_1.clearNote)();
showEditorEmpty(true);
renderAll();
}
function deleteSelectedNote() { trash(); }
function escapeHtml(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
// exposed for main / editor
function getQtagFilter() { return qtagFilter; }
function setQtagFilter(t) { qtagFilter = t; (0, store_1.save)(); renderAll(); }
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4-flash-0731","provider":"openrouter","persona":"typescript","sessionId":"ef612721-439c-421f-9605-cc6a06f62b39","tokensIn":6165739,"tokensOut":81146,"tokensTotal":6246885,"cost":0.13793290200000002,"turns":83,"toolCalls":82,"failedToolCalls":0,"timestamp":"2026-08-01T16:41:25.207Z"} -->