Notes
k3 · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 98,548 bytes · SHA-256 f64f3a9f7340
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inkwell — Notes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&family=Source+Serif+4:opsz,wght@8..60,400;8..60,600;8..60,700&display=swap" rel="stylesheet">
<style>
/* ============ TOKENS ============ */
:root{
--font-ui:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;
--font-mono:'JetBrains Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace;
--font-serif:'Source Serif 4',Georgia,'Times New Roman',serif;
--radius:10px;
--radius-sm:7px;
}
[data-theme="light"]{
--bg:#efede9;
--sidebar:#e9e6e1;
--sidebar-border:rgba(30,28,25,.08);
--list:#f6f5f2;
--editor:#fbfbf9;
--card:#ffffff;
--hover:rgba(30,28,25,.055);
--active:rgba(30,28,25,.09);
--border:rgba(30,28,25,.10);
--border-strong:rgba(30,28,25,.18);
--text:#232220;
--text-2:#6e6b64;
--text-3:#a5a29a;
--accent:#5b54e8;
--accent-hover:#4a43d6;
--accent-weak:rgba(91,84,232,.11);
--accent-text:#ffffff;
--danger:#dc3b3b;
--danger-weak:rgba(220,59,59,.10);
--code-bg:#f0efeb;
--pre-bg:#f3f2ee;
--shadow-sm:0 1px 2px rgba(25,23,20,.05),0 3px 10px -4px rgba(25,23,20,.08);
--shadow-lg:0 4px 12px rgba(25,23,20,.06),0 16px 48px -8px rgba(25,23,20,.18);
--overlay:rgba(35,32,28,.35);
--tooltip:#232220;
--selection:rgba(91,84,232,.18);
}
[data-theme="dark"]{
--bg:#101114;
--sidebar:#16171c;
--sidebar-border:rgba(255,255,255,.06);
--list:#1a1b21;
--editor:#131418;
--card:#20222a;
--hover:rgba(255,255,255,.055);
--active:rgba(255,255,255,.09);
--border:rgba(255,255,255,.075);
--border-strong:rgba(255,255,255,.16);
--text:#e8e9ee;
--text-2:#9a9da8;
--text-3:#5f626d;
--accent:#8f89ff;
--accent-hover:#a09aff;
--accent-weak:rgba(143,137,255,.15);
--accent-text:#12131a;
--danger:#f0616b;
--danger-weak:rgba(240,97,107,.13);
--code-bg:#22242c;
--pre-bg:#0d0e12;
--shadow-sm:0 1px 2px rgba(0,0,0,.3),0 3px 10px -4px rgba(0,0,0,.4);
--shadow-lg:0 4px 12px rgba(0,0,0,.35),0 20px 56px -8px rgba(0,0,0,.6);
--overlay:rgba(0,0,0,.55);
--tooltip:#2b2d36;
--selection:rgba(143,137,255,.28);
}
/* ============ BASE ============ */
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
font-family:var(--font-ui);
font-size:14px;line-height:1.5;
color:var(--text);
background:var(--bg);
overflow:hidden;
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
}
::selection{background:var(--selection)}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:6px;border:3px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:var(--text-3);border:3px solid transparent;background-clip:content-box}
button{font-family:inherit;font-size:inherit;color:inherit;background:none;border:none;cursor:pointer}
input,textarea,select{font-family:inherit;font-size:inherit;color:inherit}
svg{display:block;flex-shrink:0}
.mono{font-family:var(--font-mono)}
#app{display:flex;height:100vh;width:100vw;overflow:hidden}
/* ============ ICON BUTTONS ============ */
.icon-btn{
display:inline-flex;align-items:center;justify-content:center;
width:30px;height:30px;border-radius:8px;color:var(--text-2);
transition:background .12s,color .12s;position:relative;flex-shrink:0;
}
.icon-btn:hover{background:var(--hover);color:var(--text)}
.icon-btn.on{color:var(--accent);background:var(--accent-weak)}
.icon-btn:disabled{opacity:.35;pointer-events:none}
/* ============ SIDEBAR ============ */
#sidebar{
width:248px;min-width:180px;max-width:420px;
background:var(--sidebar);
border-right:1px solid var(--sidebar-border);
display:flex;flex-direction:column;flex-shrink:0;
user-select:none;
}
.sb-head{display:flex;align-items:center;gap:10px;padding:16px 16px 12px}
.logo{
width:32px;height:32px;border-radius:9px;background:var(--accent);
display:flex;align-items:center;justify-content:center;color:var(--accent-text);
box-shadow:var(--shadow-sm);flex-shrink:0;
}
.logo svg{width:17px;height:17px}
.sb-title{font-size:15px;font-weight:700;letter-spacing:-.01em;flex:1;min-width:0}
.sb-title small{display:block;font-size:10.5px;font-weight:500;color:var(--text-3);letter-spacing:.02em}
.sb-actions{padding:2px 14px 10px}
.new-note-btn{
width:100%;display:flex;align-items:center;gap:9px;
padding:8px 11px;border-radius:9px;
background:var(--card);border:1px solid var(--border);
font-weight:600;font-size:13px;color:var(--text);
box-shadow:var(--shadow-sm);transition:border-color .12s,transform .08s,box-shadow .12s;
}
.new-note-btn:hover{border-color:var(--accent);box-shadow:0 1px 2px rgba(25,23,20,.05),0 4px 14px -4px var(--accent-weak)}
.new-note-btn:active{transform:scale(.985)}
.new-note-btn svg{color:var(--accent)}
.new-note-btn kbd{
margin-left:auto;font-family:var(--font-mono);font-size:10px;font-weight:500;
color:var(--text-3);background:var(--hover);padding:2px 6px;border-radius:5px;
}
.sb-scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:2px 8px 12px}
.sb-nav{display:flex;flex-direction:column;gap:1px;margin-bottom:6px}
.sb-item{
display:flex;align-items:center;gap:10px;
padding:6.5px 9px;border-radius:8px;cursor:pointer;
font-size:13px;font-weight:500;color:var(--text-2);
transition:background .1s,color .1s;position:relative;width:100%;text-align:left;
}
.sb-item svg{width:16px;height:16px;opacity:.85}
.sb-item:hover{background:var(--hover);color:var(--text)}
.sb-item.active{background:var(--accent-weak);color:var(--accent);font-weight:600}
.sb-item .count{
margin-left:auto;font-size:11px;font-weight:600;color:var(--text-3);
font-variant-numeric:tabular-nums;
}
.sb-item.active .count{color:var(--accent);opacity:.8}
.sb-item .dot{width:9px;height:9px;border-radius:50%;flex-shrink:0;margin:0 3.5px}
.sb-item.drag-over{background:var(--accent-weak);outline:1.5px dashed var(--accent);outline-offset:-1.5px}
.sb-section{margin-top:14px}
.sb-section-head{
display:flex;align-items:center;gap:4px;
padding:4px 9px 5px;font-size:10.5px;font-weight:700;
letter-spacing:.09em;color:var(--text-3);text-transform:uppercase;
cursor:pointer;border-radius:6px;width:100%;text-align:left;
}
.sb-section-head:hover{color:var(--text-2)}
.sb-section-head .chev{transition:transform .15s;opacity:.7}
.sb-section.collapsed .chev{transform:rotate(-90deg)}
.sb-section-head .add-btn{
margin-left:auto;width:20px;height:20px;border-radius:6px;
display:flex;align-items:center;justify-content:center;
color:var(--text-3);opacity:0;transition:opacity .12s,background .12s;
}
.sb-section-head:hover .add-btn{opacity:1}
.sb-section-head .add-btn:hover{background:var(--hover);color:var(--text)}
.sb-section.collapsed .sb-section-body{display:none}
.sb-inline-input{
width:100%;padding:6px 9px;margin:2px 0;border-radius:8px;
border:1.5px solid var(--accent);background:var(--card);
font-size:13px;outline:none;box-shadow:0 0 0 3px var(--accent-weak);
}
.sb-foot{
padding:10px 12px;border-top:1px solid var(--sidebar-border);
display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--text-3);
}
.sb-foot .grow{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tag-row{display:flex;flex-wrap:wrap;gap:5px;padding:3px 9px}
.tag-chip{
display:inline-flex;align-items:center;gap:4px;
padding:3px 9px;border-radius:999px;font-size:12px;font-weight:500;
background:var(--hover);color:var(--text-2);cursor:pointer;border:1px solid transparent;
transition:all .12s;
}
.tag-chip:hover{background:var(--active);color:var(--text)}
.tag-chip.active{background:var(--accent-weak);color:var(--accent);border-color:var(--accent)}
.tag-chip .n{opacity:.55;font-size:10.5px;font-weight:600}
/* ============ RESIZER ============ */
.resizer{width:5px;cursor:col-resize;flex-shrink:0;margin:0 -2px;z-index:20;position:relative}
.resizer:hover::after,.resizer.dragging::after{
content:'';position:absolute;inset:0 2px;background:var(--accent);opacity:.55;border-radius:2px;
}
/* ============ NOTE LIST ============ */
#listPane{
width:320px;min-width:220px;max-width:520px;
background:var(--list);border-right:1px solid var(--border);
display:flex;flex-direction:column;flex-shrink:0;
}
.list-head{padding:14px 16px 10px;border-bottom:1px solid var(--border)}
.list-title-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}
.list-title{font-size:17px;font-weight:700;letter-spacing:-.015em;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.list-count{font-size:11.5px;font-weight:600;color:var(--text-3);background:var(--hover);padding:2px 8px;border-radius:999px;flex-shrink:0}
.search-wrap{position:relative}
.search-wrap svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-3);width:15px;height:15px;pointer-events:none}
#searchInput{
width:100%;padding:7px 30px 7px 32px;border-radius:9px;
border:1px solid var(--border);background:var(--card);
font-size:13px;outline:none;transition:border-color .12s,box-shadow .12s;
}
#searchInput:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-weak)}
#searchInput::placeholder{color:var(--text-3)}
.search-clear{
position:absolute;right:6px;top:50%;transform:translateY(-50%);
width:20px;height:20px;border-radius:6px;display:none;
align-items:center;justify-content:center;color:var(--text-3);
}
.search-clear:hover{background:var(--hover);color:var(--text)}
.search-clear.show{display:flex}
#notesScroll{flex:1;overflow-y:auto;padding:6px 0 20px}
.list-label{
padding:12px 18px 4px;font-size:10.5px;font-weight:700;
letter-spacing:.09em;text-transform:uppercase;color:var(--text-3);
}
.note-card{
margin:4px 10px;padding:12px 13px 11px;border-radius:12px;
border:1px solid transparent;cursor:pointer;position:relative;
transition:background .12s,border-color .12s,box-shadow .12s;
}
.note-card:hover{background:var(--card);box-shadow:var(--shadow-sm)}
.note-card.active{
background:var(--card);border-color:var(--accent);
box-shadow:var(--shadow-sm),0 0 0 1px var(--accent);
}
.note-card.dragging{opacity:.4}
.nc-top{display:flex;align-items:flex-start;gap:6px}
.nc-title{
font-size:13.5px;font-weight:600;letter-spacing:-.005em;line-height:1.35;
flex:1;min-width:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;
}
.nc-title.empty{color:var(--text-3);font-style:italic;font-weight:500}
.nc-pin{color:var(--accent);width:13px;height:13px;margin-top:2px}
.nc-fav{color:#e8a23d;width:13px;height:13px;margin-top:2px}
[data-theme="dark"] .nc-fav{color:#f0b45a}
.nc-excerpt{
font-size:12px;color:var(--text-2);line-height:1.45;margin-top:3px;
overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
word-break:break-word;
}
.nc-meta{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:11px;color:var(--text-3)}
.nc-nb{display:inline-flex;align-items:center;gap:5px;font-weight:500}
.nc-nb .dot{width:7px;height:7px;border-radius:50%}
.nc-tags{display:flex;gap:4px;overflow:hidden}
.nc-tag{background:var(--hover);padding:1px 7px;border-radius:999px;font-size:10.5px;font-weight:500;white-space:nowrap}
.nc-date{margin-left:auto;white-space:nowrap;font-variant-numeric:tabular-nums}
.list-empty{
padding:48px 28px;text-align:center;color:var(--text-3);
}
.list-empty svg{width:34px;height:34px;margin:0 auto 12px;opacity:.5}
.list-empty p{font-size:13px;font-weight:500}
.list-empty span{font-size:12px;display:block;margin-top:4px}
</style>
<style>
/* ============ EDITOR PANE ============ */
#editorPane{flex:1;display:flex;flex-direction:column;background:var(--editor);min-width:0;position:relative}
.editor-topbar{
display:flex;align-items:center;gap:6px;
padding:10px 18px;border-bottom:1px solid var(--border);
flex-shrink:0;
}
.crumb{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--text-2);flex:1;min-width:0}
.crumb .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.crumb b{font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.crumb .sep{color:var(--text-3)}
.top-actions{display:flex;align-items:center;gap:2px}
.editor-toolbar{
display:flex;align-items:center;gap:2px;flex-wrap:wrap;
padding:7px 18px;border-bottom:1px solid var(--border);
flex-shrink:0;
}
.tb-sep{width:1px;height:18px;background:var(--border-strong);margin:0 6px;flex-shrink:0}
.tb-btn{
display:inline-flex;align-items:center;justify-content:center;
min-width:30px;height:30px;padding:0 6px;border-radius:8px;
color:var(--text-2);font-size:12.5px;font-weight:600;
transition:background .1s,color .1s;
}
.tb-btn:hover{background:var(--hover);color:var(--text)}
.tb-btn.on{background:var(--accent-weak);color:var(--accent)}
.tb-btn svg{width:15.5px;height:15.5px}
.editor-scroll{flex:1;overflow-y:auto;overflow-x:hidden}
.editor-inner{max-width:780px;margin:0 auto;padding:44px 56px 120px}
#titleInput{
width:100%;border:none;outline:none;background:transparent;
font-size:34px;font-weight:800;letter-spacing:-.02em;line-height:1.2;
color:var(--text);padding:0;margin-bottom:6px;font-family:var(--font-ui);
}
#titleInput::placeholder{color:var(--text-3)}
.meta-line{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-3);margin-bottom:26px;flex-wrap:wrap}
.meta-line .dot-sep{opacity:.6}
#nbSelect{
border:1px solid var(--border);background:transparent;border-radius:7px;
font-size:12px;color:var(--text-2);padding:2.5px 6px;outline:none;cursor:pointer;
font-weight:500;
}
#nbSelect:hover{border-color:var(--border-strong);color:var(--text)}
#editor{outline:none;font-size:15.5px;line-height:1.72;letter-spacing:-.003em;min-height:300px;word-wrap:break-word}
body[data-efont="serif"] #editor{font-family:var(--font-serif);font-size:17px}
body[data-efont="mono"] #editor{font-family:var(--font-mono);font-size:14px;line-height:1.7}
#editor.placeholder::before{
content:attr(data-ph);color:var(--text-3);position:absolute;pointer-events:none;
}
#editor p{margin:0 0 10px}
#editor h1{font-size:27px;font-weight:800;letter-spacing:-.02em;margin:26px 0 10px;line-height:1.3}
#editor h2{font-size:21px;font-weight:700;letter-spacing:-.015em;margin:22px 0 8px;line-height:1.35}
#editor h3{font-size:17px;font-weight:700;margin:18px 0 6px;line-height:1.4}
#editor h1:first-child,#editor h2:first-child,#editor h3:first-child,#editor p:first-child{margin-top:0}
#editor ul,#editor ol{margin:0 0 10px;padding-left:24px}
#editor li{margin:3px 0}
#editor li::marker{color:var(--text-3)}
#editor blockquote{
margin:0 0 12px;padding:6px 16px;border-left:3px solid var(--accent);
color:var(--text-2);background:var(--accent-weak);border-radius:0 8px 8px 0;
}
#editor pre{
margin:0 0 12px;padding:14px 16px;background:var(--pre-bg);
border:1px solid var(--border);border-radius:10px;
font-family:var(--font-mono);font-size:13px;line-height:1.6;
overflow-x:auto;white-space:pre-wrap;word-break:break-word;
}
#editor code{
font-family:var(--font-mono);font-size:.88em;
background:var(--code-bg);padding:2px 6px;border-radius:6px;
border:1px solid var(--border);
}
#editor pre code{background:none;border:none;padding:0;font-size:inherit}
#editor mark{
background:color-mix(in srgb,var(--accent) 26%,transparent);
color:inherit;padding:1px 3px;border-radius:4px;
}
#editor a{color:var(--accent);text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;cursor:pointer}
#editor hr{border:none;border-top:1.5px solid var(--border-strong);margin:22px 0}
#editor ul.todo{list-style:none;padding-left:2px}
#editor ul.todo li{display:flex;align-items:flex-start;gap:9px;margin:5px 0}
#editor ul.todo input[type=checkbox]{
appearance:none;-webkit-appearance:none;flex-shrink:0;
width:17px;height:17px;margin-top:4px;border-radius:5.5px;
border:1.5px solid var(--border-strong);background:var(--card);
cursor:pointer;position:relative;transition:all .12s;
}
#editor ul.todo input[type=checkbox]:hover{border-color:var(--accent)}
#editor ul.todo input[type=checkbox]:checked{background:var(--accent);border-color:var(--accent)}
#editor ul.todo input[type=checkbox]:checked::after{
content:'';position:absolute;left:5px;top:2px;width:4px;height:8px;
border:solid var(--accent-text);border-width:0 2px 2px 0;transform:rotate(42deg);
}
#editor ul.todo li:has(input:checked){color:var(--text-3)}
#editor ul.todo li:has(input:checked) .td-text,#editor ul.todo li.checked{text-decoration:line-through}
.editor-footer{
display:flex;align-items:center;gap:10px;flex-wrap:wrap;
padding:9px 20px;border-top:1px solid var(--border);
font-size:11.5px;color:var(--text-3);flex-shrink:0;background:var(--editor);
}
.ed-tags{display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex:1;min-width:0}
.ed-tag{
display:inline-flex;align-items:center;gap:5px;
background:var(--hover);padding:2.5px 5px 2.5px 9px;border-radius:999px;
font-size:11.5px;font-weight:500;color:var(--text-2);
}
.ed-tag button{
width:15px;height:15px;border-radius:50%;display:flex;align-items:center;justify-content:center;
color:var(--text-3);font-size:12px;line-height:1;
}
.ed-tag button:hover{background:var(--danger-weak);color:var(--danger)}
#tagInput{
border:none;outline:none;background:transparent;font-size:12px;
color:var(--text);width:90px;padding:3px 0;
}
#tagInput::placeholder{color:var(--text-3)}
.word-stats{white-space:nowrap;font-variant-numeric:tabular-nums}
.editor-empty{
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
color:var(--text-3);text-align:center;padding:40px;gap:6px;
}
.editor-empty .big-icon{
width:64px;height:64px;border-radius:18px;background:var(--hover);
display:flex;align-items:center;justify-content:center;margin-bottom:10px;color:var(--text-3);
}
.editor-empty h3{font-size:16px;font-weight:600;color:var(--text-2)}
.editor-empty p{font-size:13px;max-width:340px}
.editor-empty kbd{
font-family:var(--font-mono);font-size:10.5px;background:var(--hover);
padding:2px 6px;border-radius:5px;border:1px solid var(--border);
}
/* focus mode */
body.focus #sidebar,body.focus #listPane,body.focus .resizer{display:none}
/* ============ COMMAND PALETTE ============ */
.overlay{
position:fixed;inset:0;background:var(--overlay);z-index:100;
display:none;align-items:flex-start;justify-content:center;
backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);
animation:fadeIn .12s ease;
}
.overlay.show{display:flex}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
#palettePanel{
width:min(580px,calc(100vw - 40px));margin-top:14vh;
background:var(--card);border:1px solid var(--border);
border-radius:16px;box-shadow:var(--shadow-lg);overflow:hidden;
animation:popIn .16s cubic-bezier(.2,.9,.3,1.2);
}
@keyframes popIn{from{opacity:0;transform:scale(.96) translateY(8px)}to{opacity:1;transform:none}}
.palette-input-wrap{display:flex;align-items:center;gap:11px;padding:15px 18px;border-bottom:1px solid var(--border)}
.palette-input-wrap svg{color:var(--text-3);width:17px;height:17px}
#paletteInput{flex:1;border:none;outline:none;background:transparent;font-size:15px;color:var(--text)}
#paletteInput::placeholder{color:var(--text-3)}
.palette-input-wrap kbd{
font-family:var(--font-mono);font-size:10px;color:var(--text-3);
border:1px solid var(--border);border-radius:5px;padding:2.5px 6px;
}
#paletteResults{max-height:380px;overflow-y:auto;padding:7px}
.pal-group{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3);padding:9px 11px 4px}
.pal-item{
display:flex;align-items:center;gap:11px;padding:8.5px 11px;border-radius:9px;
cursor:pointer;font-size:13.5px;font-weight:500;
}
.pal-item svg{width:16px;height:16px;color:var(--text-3)}
.pal-item .sub{margin-left:auto;font-size:11.5px;color:var(--text-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}
.pal-item.sel{background:var(--accent-weak)}
.pal-item.sel svg{color:var(--accent)}
.pal-empty{padding:28px;text-align:center;color:var(--text-3);font-size:13px}
/* ============ CONTEXT MENU / DROPDOWN ============ */
.ctx-menu{
position:fixed;z-index:200;min-width:210px;
background:var(--card);border:1px solid var(--border);
border-radius:12px;box-shadow:var(--shadow-lg);padding:5px;
animation:popIn .12s cubic-bezier(.2,.9,.3,1.15);
}
.ctx-item{
display:flex;align-items:center;gap:10px;width:100%;
padding:7px 10px;border-radius:8px;font-size:13px;font-weight:500;
color:var(--text);cursor:pointer;text-align:left;position:relative;
}
.ctx-item:hover{background:var(--hover)}
.ctx-item svg{width:15px;height:15px;color:var(--text-2)}
.ctx-item.danger{color:var(--danger)}
.ctx-item.danger svg{color:var(--danger)}
.ctx-item.danger:hover{background:var(--danger-weak)}
.ctx-item .arrow{margin-left:auto;color:var(--text-3);width:13px;height:13px}
.ctx-item .check{margin-left:auto;color:var(--accent);width:15px;height:15px}
.ctx-divider{height:1px;background:var(--border);margin:5px 8px}
.ctx-label{font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-3);padding:7px 10px 3px}
.ctx-item .dot{width:9px;height:9px;border-radius:50%;margin:0 3px}
.ctx-submenu{
position:absolute;left:calc(100% - 4px);top:-5px;min-width:180px;
background:var(--card);border:1px solid var(--border);
border-radius:12px;box-shadow:var(--shadow-lg);padding:5px;display:none;
max-height:280px;overflow-y:auto;
}
.ctx-item.has-sub:hover>.ctx-submenu{display:block}
/* ============ MODAL DIALOG ============ */
#dialogOverlay{align-items:center}
#dialogPanel{
width:min(400px,calc(100vw - 40px));
background:var(--card);border:1px solid var(--border);
border-radius:16px;box-shadow:var(--shadow-lg);padding:22px;
animation:popIn .16s cubic-bezier(.2,.9,.3,1.2);
}
#dialogPanel h3{font-size:15.5px;font-weight:700;margin-bottom:6px}
#dialogPanel .dlg-msg{font-size:13px;color:var(--text-2);margin-bottom:16px;line-height:1.5}
#dialogInput{
width:100%;padding:9px 12px;border-radius:9px;margin-bottom:16px;
border:1.5px solid var(--border);background:var(--editor);
font-size:14px;outline:none;transition:border-color .12s,box-shadow .12s;
}
#dialogInput:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-weak)}
.dlg-actions{display:flex;justify-content:flex-end;gap:8px}
.btn{
padding:8px 15px;border-radius:9px;font-size:13px;font-weight:600;
transition:all .12s;border:1px solid var(--border);
}
.btn:hover{background:var(--hover)}
.btn.primary{background:var(--accent);color:var(--accent-text);border-color:transparent}
.btn.primary:hover{background:var(--accent-hover)}
.btn.danger{background:var(--danger);color:#fff;border-color:transparent}
.btn.danger:hover{filter:brightness(1.08)}
/* ============ TOAST ============ */
#toast{
position:fixed;bottom:26px;left:50%;transform:translateX(-50%) translateY(16px);
background:var(--tooltip);color:var(--editor);
padding:9px 17px;border-radius:999px;font-size:13px;font-weight:500;
box-shadow:var(--shadow-lg);z-index:300;opacity:0;pointer-events:none;
transition:opacity .2s,transform .2s;display:flex;align-items:center;gap:8px;
}
[data-theme="dark"] #toast{color:var(--text)}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
#toast svg{width:15px;height:15px;color:var(--accent)}
/* ============ RESPONSIVE ============ */
.mobile-only{display:none!important}
@media (max-width:960px){
.mobile-only{display:inline-flex!important}
#sidebar{
position:fixed;left:0;top:0;bottom:0;z-index:90;width:270px;
transform:translateX(-105%);transition:transform .22s ease;
box-shadow:var(--shadow-lg);
}
body.sb-open #sidebar{transform:none}
#sidebarScrim{
position:fixed;inset:0;background:var(--overlay);z-index:85;
display:none;backdrop-filter:blur(2px);
}
body.sb-open #sidebarScrim{display:block}
.resizer{display:none}
#listPane{flex:1;width:auto;max-width:none;border-right:none}
body.pane-editor #listPane{display:none}
body.pane-editor #editorPane{display:flex}
#editorPane{display:none;position:fixed;inset:0;z-index:60}
.editor-inner{padding:32px 22px 100px}
#titleInput{font-size:27px}
}
</style>
</head>
<body data-efont="sans">
<div id="app">
<!-- ============ SIDEBAR ============ -->
<aside id="sidebar">
<div class="sb-head">
<div class="logo" id="logoMark"></div>
<div class="sb-title">Inkwell<small>Personal Knowledge</small></div>
<button class="icon-btn" id="themeBtn" title="Toggle theme (⌘⇧L)"></button>
</div>
<div class="sb-actions">
<button class="new-note-btn" id="newNoteBtn"><span id="newNoteIcon"></span>New note<kbd>⌘N</kbd></button>
</div>
<div class="sb-scroll" id="sbScroll"></div>
<div class="sb-foot">
<span class="grow" id="storageInfo"></span>
<button class="icon-btn" id="settingsBtn" title="Settings"></button>
</div>
</aside>
<div class="resizer" id="resizer1"></div>
<!-- ============ NOTE LIST ============ -->
<section id="listPane">
<div class="list-head">
<div class="list-title-row">
<button class="icon-btn mobile-only" id="hamburgerBtn" title="Menu"></button>
<div class="list-title" id="listTitle">All Notes</div>
<span class="list-count" id="listCount">0</span>
<button class="icon-btn" id="sortBtn" title="Sort notes"></button>
</div>
<div class="search-wrap">
<span id="searchIcon"></span>
<input id="searchInput" type="text" placeholder="Search notes…" autocomplete="off" spellcheck="false">
<button class="search-clear" id="searchClear" title="Clear search"></button>
</div>
</div>
<div id="notesScroll"></div>
</section>
<div class="resizer" id="resizer2"></div>
<!-- ============ EDITOR ============ -->
<main id="editorPane">
<div class="editor-topbar">
<button class="icon-btn mobile-only" id="backBtn" title="Back to list"></button>
<div class="crumb" id="crumb"></div>
<div class="top-actions">
<button class="icon-btn" id="pinBtn" title="Pin note"></button>
<button class="icon-btn" id="favBtn" title="Favorite"></button>
<button class="icon-btn" id="focusBtn" title="Focus mode"></button>
<button class="icon-btn" id="moreBtn" title="More actions"></button>
</div>
</div>
<div class="editor-toolbar" id="toolbar"></div>
<div class="editor-scroll" id="editorScroll">
<div class="editor-inner">
<input id="titleInput" type="text" placeholder="Untitled" autocomplete="off" spellcheck="false">
<div class="meta-line" id="metaLine"></div>
<div id="editor" contenteditable="true" spellcheck="true" data-ph="Start writing, or type '/' for ideas — # heading, - list, [] to-do, > quote, ``` code…"></div>
</div>
</div>
<div class="editor-footer">
<div class="ed-tags" id="edTags"></div>
<span class="word-stats" id="wordStats"></span>
</div>
</main>
</div>
<div id="sidebarScrim"></div>
<!-- Command palette -->
<div class="overlay" id="paletteOverlay">
<div id="palettePanel">
<div class="palette-input-wrap">
<span id="palIcon"></span>
<input id="paletteInput" type="text" placeholder="Search notes or run a command…" autocomplete="off" spellcheck="false">
<kbd>esc</kbd>
</div>
<div id="paletteResults"></div>
</div>
</div>
<!-- Dialog -->
<div class="overlay" id="dialogOverlay">
<div id="dialogPanel">
<h3 id="dlgTitle"></h3>
<div class="dlg-msg" id="dlgMsg"></div>
<input id="dialogInput" type="text" autocomplete="off" spellcheck="false" style="display:none">
<div class="dlg-actions">
<button class="btn" id="dlgCancel">Cancel</button>
<button class="btn primary" id="dlgOk">OK</button>
</div>
</div>
</div>
<div id="toast"></div>
<input type="file" id="importFile" accept=".json,application/json" style="display:none">
<script>
'use strict';
/* ================================================================
INKWELL — a single-file notes app
================================================================ */
/* ---------- utils ---------- */
const $ = (s, r=document) => r.querySelector(s);
const $$ = (s, r=document) => [...r.querySelectorAll(s)];
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
function plainText(html){
const d = document.createElement('div');
d.innerHTML = html || '';
return (d.textContent || '').replace(/\s+/g, ' ').trim();
}
function wordCount(text){
const w = text.trim().split(/\s+/).filter(Boolean);
return w.length;
}
function relDate(ts){
const d = new Date(ts), now = new Date();
const sameDay = d.toDateString() === now.toDateString();
const yest = new Date(now); yest.setDate(now.getDate() - 1);
if (sameDay) return d.toLocaleTimeString([], {hour:'numeric', minute:'2-digit'});
if (d.toDateString() === yest.toDateString()) return 'Yesterday';
if (now - d < 6 * 864e5) return d.toLocaleDateString([], {weekday:'short'});
if (d.getFullYear() === now.getFullYear()) return d.toLocaleDateString([], {month:'short', day:'numeric'});
return d.toLocaleDateString([], {month:'short', day:'numeric', year:'numeric'});
}
function fullDate(ts){
return new Date(ts).toLocaleDateString([], {month:'long', day:'numeric', year:'numeric'});
}
function fuzzyScore(q, str){
q = q.toLowerCase(); str = str.toLowerCase();
if (!q) return 1;
if (str.startsWith(q)) return 100 - (str.length - q.length) * .01;
if (str.includes(q)) return 60 - str.indexOf(q) * .1;
let qi = 0, score = 0;
for (let i = 0; i < str.length && qi < q.length; i++){
if (str[i] === q[qi]){ score += 10; qi++; }
}
return qi === q.length ? score * .3 : 0;
}
/* ---------- icons (feather-style) ---------- */
const P = (d, extra='') => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ${extra}>${d}</svg>`;
const ICONS = {
logo: P('<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>'),
plus: P('<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>'),
search: P('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'),
note: P('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>'),
book: P('<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>'),
pin: P('<path d="M9 4h6l1 7 3 3v1H5v-1l3-3 1-7z"/><line x1="12" y1="15" x2="12" y2="21"/>'),
star: P('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>'),
trash: P('<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>'),
tag: P('<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/>'),
sun: P('<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>'),
moon: P('<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>'),
chevD: P('<polyline points="6 9 12 15 18 9"/>'),
chevR: P('<polyline points="9 18 15 12 9 6"/>'),
bold: P('<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>'),
italic: P('<line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/>'),
under: P('<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/>'),
strike: P('<line x1="5" y1="12" x2="19" y2="12"/><path d="M16 6c-.9-1.6-2.6-2.5-4.5-2.5C8.4 3.5 6 5.2 6 8c0 1.2.4 2 1.2 2.7"/><path d="M8 18c.9 1.6 2.6 2.5 4.5 2.5 3.1 0 5.5-1.7 5.5-4.5 0-.7-.1-1.2-.4-1.7"/>'),
h1: P('<path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M17 12l3-2v8"/>'),
h2: P('<path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"/>'),
h3: P('<path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 1 1-4 0"/><path d="M21 17.5a2 2 0 1 1-4 0c0-1.5 1.8-2.5 3.5-1.5"/>'),
ul: P('<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>'),
ol: P('<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>'),
quote: P('<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/>'),
code: P('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'),
link: P('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'),
todo: P('<polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>'),
mark: P('<path d="M9 11l-6 6v3h9l3-3"/><path d="M22 12l-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4l8 8z"/>'),
hr: P('<line x1="3" y1="12" x2="21" y2="12"/><polyline points="8 8 12 4 16 8"/><polyline points="16 16 12 20 8 16"/>'),
more: P('<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>'),
x: P('<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>'),
check: P('<polyline points="20 6 9 17 4 12"/>'),
sort: P('<line x1="4" y1="6" x2="16" y2="6"/><line x1="4" y1="12" x2="12" y2="12"/><line x1="4" y1="18" x2="8" y2="18"/><polyline points="15 15 18 18 21 15"/><line x1="18" y1="9" x2="18" y2="18"/>'),
settings: P('<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>'),
download: P('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>'),
upload: P('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>'),
focus: P('<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>'),
back: P('<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>'),
menu: P('<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>'),
copy: P('<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'),
edit: P('<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>'),
restore: P('<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>'),
hash: P('<line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/>'),
clock: P('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>'),
cmd: P('<path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/>'),
};
const icon = name => ICONS[name] || ICONS.note;
const NB_COLORS = ['#e0604f','#e8963d','#e3c04a','#6fbf5a','#3fb8a6','#4f9de8','#7b6fe8','#b06fd8','#e05f9a','#8a919e'];
</script>
<script>
/* ---------- state & storage ---------- */
const STORE_KEY = 'inkwell.v1';
let state = null;
function defaultState(){
return {
notes: [], notebooks: [],
view: { type: 'all', id: null },
selectedId: null,
search: '',
sort: 'updated',
theme: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
efont: 'sans',
focus: false,
collapsed: {},
widths: { sidebar: 248, list: 320 },
nbColorIdx: 0,
};
}
function save(){
try { localStorage.setItem(STORE_KEY, JSON.stringify(state)); } catch(e){}
}
const saveSoon = debounce(save, 250);
function load(){
try {
const raw = localStorage.getItem(STORE_KEY);
if (raw){ state = Object.assign(defaultState(), JSON.parse(raw)); return; }
} catch(e){}
state = defaultState();
seed();
save();
}
/* ---------- seed content ---------- */
function seed(){
const now = Date.now(), H = 36e5, D = 864e5;
const nb1 = { id: uid(), name: 'Personal', color: NB_COLORS[6] };
const nb2 = { id: uid(), name: 'Work', color: NB_COLORS[5] };
const nb3 = { id: uid(), name: 'Reading', color: NB_COLORS[4] };
state.notebooks = [nb1, nb2, nb3];
state.nbColorIdx = 3;
const welcome = {
id: uid(), title: 'Welcome to Inkwell ✦', notebook: nb1.id, tags: ['guide'],
pinned: true, fav: true, trashed: false,
created: now - 2 * D, updated: now - 2 * H,
content:
`<h1>Welcome to Inkwell</h1>
<p>A calm place for your thoughts. Everything lives in this one file and syncs to your browser's local storage automatically — no account, no cloud, no fuss.</p>
<h2>Writing</h2>
<p>The editor supports rich text with Markdown-style shortcuts. Try typing these at the start of a line:</p>
<ul><li><code>#</code> then space — big heading (also <code>##</code> and <code>###</code>)</li><li><code>-</code> or <code>*</code> then space — bullet list</li><li><code>1.</code> then space — numbered list</li><li><code>[]</code> then space — a to-do checkbox</li><li><code>></code> then space — a quote block</li><li><code>```</code> then enter — a code block</li><li><code>---</code> then enter — a divider</li></ul>
<h2>Your to-do list works too</h2>
<ul class="todo"><li><input type="checkbox" checked contenteditable="false"><span class="td-text">Click this checkbox — it persists</span></li><li><input type="checkbox" contenteditable="false"><span class="td-text">Press <kbd>⌘K</kbd> to open the command palette</span></li><li><input type="checkbox" contenteditable="false"><span class="td-text">Drag a note onto a notebook in the sidebar</span></li></ul>
<h2>Stay organized</h2>
<p>Create <mark>notebooks</mark> and <mark>tags</mark>, pin what matters, and star your favorites. Right-click any note for more options, or hit the <strong>⋯</strong> menu up top.</p>
<blockquote>Simplicity is the soul of efficiency. — Austin Freeman</blockquote>
<p>Happy writing. — The Inkwell Team</p>`
};
const meeting = {
id: uid(), title: 'Q3 Planning — Meeting Notes', notebook: nb2.id, tags: ['meetings'],
pinned: false, fav: false, trashed: false,
created: now - D, updated: now - 5 * H,
content:
`<h2>Q3 Planning</h2>
<p><strong>Attendees:</strong> Maya, Jon, Priya, Sam</p>
<h3>Decisions</h3>
<ol><li>Ship the onboarding revamp by <mark>Aug 15</mark></li><li>Freeze new features during the migration window</li><li>Weekly sync moves to Tuesdays</li></ol>
<h3>Action items</h3>
<ul class="todo"><li><input type="checkbox" checked contenteditable="false"><span class="td-text">Maya — draft roadmap doc</span></li><li><input type="checkbox" contenteditable="false"><span class="td-text">Jon — audit analytics events</span></li><li><input type="checkbox" contenteditable="false"><span class="td-text">Priya — schedule user interviews</span></li></ul>
<pre><code>// velocity target
const goal = lastQuarter.points * 1.1;</code></pre>
<p>Next check-in: <a href="#">roadmap review</a> on Friday.</p>`
};
const reading = {
id: uid(), title: 'Reading List', notebook: nb3.id, tags: ['books', 'queue'],
pinned: true, fav: false, trashed: false,
created: now - 6 * D, updated: now - D,
content:
`<h2>To read</h2>
<ul class="todo"><li><input type="checkbox" checked contenteditable="false"><span class="td-text">The Design of Everyday Things — Don Norman</span></li><li><input type="checkbox" contenteditable="false"><span class="td-text">A Pattern Language — Christopher Alexander</span></li><li><input type="checkbox" contenteditable="false"><span class="td-text">The Creative Act — Rick Rubin</span></li><li><input type="checkbox" contenteditable="false"><span class="td-text">Several Short Sentences About Writing — Klinkenborg</span></li></ul>
<hr>
<h3>Notes on <em>Design of Everyday Things</em></h3>
<p>Affordances vs. signifiers — a door that begs to be pulled but must be pushed is a design failure, not a user failure. Good design <mark>makes the right action obvious</mark>.</p>
<blockquote>When you have trouble with things, it's not your fault — it's bad design.</blockquote>`
};
const ideas = {
id: uid(), title: 'Ideas parking lot', notebook: nb1.id, tags: ['ideas'],
pinned: false, fav: false, trashed: false,
created: now - 3 * D, updated: now - 26 * H,
content:
`<p>A scratch pad for half-formed thoughts:</p>
<ul><li>A tiny CLI that turns meeting audio into action items</li><li>Weekend project: mechanical-keyboard soundboard</li><li>Essay — <em>software as a garden, not a building</em></li><li>Gift idea for Ana: letterpress workshop</li></ul>
<p><code>idea ≠ plan</code> — park it here, promote it later.</p>`
};
const trashed = {
id: uid(), title: 'Old grocery list', notebook: null, tags: [],
pinned: false, fav: false, trashed: true,
created: now - 12 * D, updated: now - 10 * D,
content: `<p>Eggs, spinach, sourdough, olive oil, coffee beans, parmesan.</p>`
};
state.notes = [welcome, meeting, reading, ideas, trashed];
state.selectedId = welcome.id;
}
/* ---------- queries ---------- */
const getNote = id => state.notes.find(n => n.id === id);
const getNb = id => state.notebooks.find(b => b.id === id);
const activeNote = () => getNote(state.selectedId);
function visibleNotes(){
const v = state.view, q = state.search.trim().toLowerCase();
let list = state.notes.filter(n => {
if (v.type === 'trash') return n.trashed;
if (n.trashed) return false;
if (v.type === 'notebook') return n.notebook === v.id;
if (v.type === 'tag') return (n.tags || []).includes(v.id);
if (v.type === 'fav') return n.fav;
if (v.type === 'pinned') return n.pinned;
return true;
});
if (q){
list = list.filter(n =>
(n.title || '').toLowerCase().includes(q) ||
plainText(n.content).toLowerCase().includes(q) ||
(n.tags || []).some(t => t.toLowerCase().includes(q))
);
}
const by = {
updated: (a,b) => b.updated - a.updated,
created: (a,b) => b.created - a.created,
title: (a,b) => (a.title || 'Untitled').localeCompare(b.title || 'Untitled'),
}[state.sort] || ((a,b) => b.updated - a.updated);
list.sort(by);
if (state.view.type !== 'trash' && !q && state.sort !== 'title'){
list.sort((a,b) => (b.pinned - a.pinned));
}
return list;
}
function allTags(){
const m = new Map();
state.notes.forEach(n => { if (!n.trashed) (n.tags || []).forEach(t => m.set(t, (m.get(t) || 0) + 1)); });
return [...m.entries()].sort((a,b) => a[0].localeCompare(b[0]));
}
const nbCount = id => state.notes.filter(n => !n.trashed && n.notebook === id).length;
/* ---------- render: sidebar ---------- */
function renderSidebar(){
const sb = $('#sbScroll');
const v = state.view;
const act = (type, id=null) => (v.type === type && v.id === id) ? ' active' : '';
const live = state.notes.filter(n => !n.trashed);
let html = `<nav class="sb-nav">
<button class="sb-item${act('all')}" data-view="all">${icon('note')}<span>All Notes</span><span class="count">${live.length}</span></button>
<button class="sb-item${act('pinned')}" data-view="pinned">${icon('pin')}<span>Pinned</span><span class="count">${live.filter(n=>n.pinned).length}</span></button>
<button class="sb-item${act('fav')}" data-view="fav">${icon('star')}<span>Favorites</span><span class="count">${live.filter(n=>n.fav).length}</span></button>
<button class="sb-item${act('trash')}" data-view="trash">${icon('trash')}<span>Trash</span><span class="count">${state.notes.filter(n=>n.trashed).length}</span></button>
</nav>`;
const nbCollapsed = state.collapsed.notebooks ? ' collapsed' : '';
html += `<div class="sb-section${nbCollapsed}">
<button class="sb-section-head" data-section="notebooks">
<span class="chev">${icon('chevD')}</span>Notebooks
<span class="add-btn" data-addnb title="New notebook">${icon('plus')}</span>
</button>
<div class="sb-section-body">`;
state.notebooks.forEach(nb => {
html += `<button class="sb-item${act('notebook', nb.id)}" data-view="notebook" data-id="${nb.id}" data-nb="${nb.id}">
<span class="dot" style="background:${nb.color}"></span>
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(nb.name)}</span>
<span class="count">${nbCount(nb.id)}</span></button>`;
});
html += `</div></div>`;
const tags = allTags();
const tgCollapsed = state.collapsed.tags ? ' collapsed' : '';
html += `<div class="sb-section${tgCollapsed}">
<button class="sb-section-head" data-section="tags"><span class="chev">${icon('chevD')}</span>Tags</button>
<div class="sb-section-body"><div class="tag-row">`;
if (!tags.length) html += `<span style="font-size:12px;color:var(--text-3);padding:2px 0">No tags yet — add one in the editor footer.</span>`;
tags.forEach(([t, c]) => {
html += `<span class="tag-chip${act('tag', t)}" data-view="tag" data-id="${esc(t)}">${esc(t)}<span class="n">${c}</span></span>`;
});
html += `</div></div></div>`;
sb.innerHTML = html;
$('#storageInfo').textContent = `${state.notes.length} notes · ${state.notebooks.length} notebooks`;
}
/* ---------- render: list ---------- */
const SORT_LABELS = { updated:'Last updated', created:'Date created', title:'Title A–Z' };
function viewTitle(){
const v = state.view;
if (v.type === 'notebook') return getNb(v.id)?.name || 'Notebook';
if (v.type === 'tag') return '#' + v.id;
return { all:'All Notes', pinned:'Pinned', fav:'Favorites', trash:'Trash' }[v.type] || 'Notes';
}
function renderList(){
const notes = visibleNotes();
$('#listTitle').textContent = viewTitle();
$('#listCount').textContent = notes.length;
const scroll = $('#notesScroll');
const keepScroll = scroll.scrollTop;
if (!notes.length){
const msgs = {
trash: ['Trash is empty', 'Deleted notes will rest here.'],
tag: ['No notes with this tag', 'Add tags in the editor footer.'],
};
const [t, s] = msgs[state.view.type] || (state.search
? ['No results', `Nothing matches “${state.search}”.`]
: ['Nothing here yet', 'Press ⌘N to write your first note.']);
scroll.innerHTML = `<div class="list-empty">${icon('note')}<p>${esc(t)}</p><span>${esc(s)}</span></div>`;
return;
}
let html = '';
const pinned = notes.filter(n => n.pinned), rest = notes.filter(n => !n.pinned);
const grouped = state.view.type !== 'trash' && !state.search && state.sort !== 'title' && pinned.length && rest.length;
const card = n => {
const nb = getNb(n.notebook);
const text = plainText(n.content);
return `<div class="note-card${n.id === state.selectedId ? ' active' : ''}" data-id="${n.id}" draggable="true">
<div class="nc-top">
${n.pinned ? `<span class="nc-pin">${icon('pin')}</span>` : ''}
${n.fav ? `<span class="nc-fav">${icon('star')}</span>` : ''}
<div class="nc-title${n.title ? '' : ' empty'}">${esc(n.title || 'Untitled')}</div>
</div>
${text ? `<div class="nc-excerpt">${esc(text.slice(0, 140))}</div>` : ''}
<div class="nc-meta">
${nb ? `<span class="nc-nb"><span class="dot" style="background:${nb.color}"></span>${esc(nb.name)}</span>` : ''}
<span class="nc-tags">${(n.tags||[]).slice(0,2).map(t => `<span class="nc-tag">#${esc(t)}</span>`).join('')}</span>
<span class="nc-date">${relDate(n.updated)}</span>
</div>
</div>`;
};
if (grouped){
html += `<div class="list-label">Pinned</div>` + pinned.map(card).join('');
html += `<div class="list-label">Notes</div>` + rest.map(card).join('');
} else {
html += notes.map(card).join('');
}
scroll.innerHTML = html;
scroll.scrollTop = keepScroll;
}
function renderAll(){ renderSidebar(); renderList(); }
</script>
<script>
/* ---------- render: editor ---------- */
const editorEl = () => $('#editor');
let savedRange = null;
function renderEditor(){
const n = activeNote();
const pane = $('#editorPane');
const hasEditor = !!n && !n.trashed;
['#toolbar', '#editorScroll', '.editor-footer'].forEach(s => $(s).style.display = hasEditor ? '' : 'none');
$('#pinBtn').style.display = $('#favBtn').style.display = $('#moreBtn').style.display = hasEditor ? '' : 'none';
const crumb = $('#crumb');
if (!n){
crumb.innerHTML = '';
pane.querySelector('.editor-scroll').style.display = 'none';
if (!$('.editor-empty', pane)){
const d = document.createElement('div');
d.className = 'editor-empty';
d.innerHTML = `<div class="big-icon">${icon('logo')}</div>
<h3>No note selected</h3>
<p>Pick a note from the list, or press <kbd>⌘N</kbd> to start a new one. Press <kbd>⌘K</kbd> for the command palette.</p>`;
pane.appendChild(d);
}
$('.editor-empty', pane).style.display = 'flex';
return;
}
const ee = $('.editor-empty', pane); if (ee) ee.style.display = 'none';
const nb = getNb(n.notebook);
crumb.innerHTML = nb
? `<span class="dot" style="background:${nb.color}"></span><span>${esc(nb.name)}</span><span class="sep">/</span><b>${esc(n.title || 'Untitled')}</b>`
: `<span>${n.trashed ? 'Trash' : 'Notes'}</span><span class="sep">/</span><b>${esc(n.title || 'Untitled')}</b>`;
$('#pinBtn').classList.toggle('on', !!n.pinned);
$('#favBtn').classList.toggle('on', !!n.fav);
if (hasEditor){
const ti = $('#titleInput'), ed = editorEl();
if (ti.value !== (n.title || '')) ti.value = n.title || '';
if (ed.dataset.noteId !== n.id){
ed.innerHTML = n.content || '<p><br></p>';
ed.dataset.noteId = n.id;
}
ed.contentEditable = 'true';
updateMetaLine(n);
renderTags(n);
updateStats(n);
updatePlaceholder();
}
}
function updateMetaLine(n){
$('#metaLine').innerHTML =
`<span style="display:inline-flex;align-items:center;gap:5px" title="Created ${fullDate(n.created)}">${icon('clock')}${fullDate(n.created)}</span>
<span class="dot-sep">·</span>
<span>edited ${relDate(n.updated)}</span>
<span class="dot-sep">·</span>
<select id="nbSelect" title="Notebook">
<option value="">No notebook</option>
${state.notebooks.map(b => `<option value="${b.id}" ${b.id === n.notebook ? 'selected' : ''}>${esc(b.name)}</option>`).join('')}
</select>`;
}
function renderTags(n){
$('#edTags').innerHTML =
(n.tags || []).map(t => `<span class="ed-tag">#${esc(t)}<button data-rmtag="${esc(t)}" title="Remove tag">×</button></span>`).join('') +
`<input id="tagInput" type="text" placeholder="+ add tag" autocomplete="off" spellcheck="false">`;
}
function updateStats(n){
const text = plainText(n ? n.content : '');
const words = wordCount(text);
$('#wordStats').textContent = `${words} word${words === 1 ? '' : 's'} · ${text.length} chars`;
}
function updatePlaceholder(){
const ed = editorEl();
const empty = !plainText(ed.innerHTML) && !ed.querySelector('img,hr,ul,ol,pre,blockquote,h1,h2,h3');
ed.classList.toggle('placeholder', empty);
}
/* ---------- toolbar ---------- */
const TB = [
{ c:'bold', i:'bold', t:'Bold (⌘B)' }, { c:'italic', i:'italic', t:'Italic (⌘I)' },
{ c:'underline', i:'under', t:'Underline (⌘U)' }, { c:'strikeThrough', i:'strike', t:'Strikethrough (⌘⇧X)' },
'sep',
{ c:'h1', i:'h1', t:'Heading 1' }, { c:'h2', i:'h2', t:'Heading 2' }, { c:'h3', i:'h3', t:'Heading 3' },
'sep',
{ c:'insertUnorderedList', i:'ul', t:'Bullet list' }, { c:'insertOrderedList', i:'ol', t:'Numbered list' },
{ c:'todo', i:'todo', t:'To-do list' },
'sep',
{ c:'blockquote', i:'quote', t:'Quote' }, { c:'codeblock', i:'code', t:'Code block' },
{ c:'inlinecode', i:'cmd', t:'Inline code (⌘E)' }, { c:'mark', i:'mark', t:'Highlight' },
'sep',
{ c:'link', i:'link', t:'Insert link' }, { c:'hr', i:'hr', t:'Divider' }, { c:'clear', i:'x', t:'Clear formatting' },
];
function buildToolbar(){
$('#toolbar').innerHTML = TB.map(b => b === 'sep'
? '<span class="tb-sep"></span>'
: `<button class="tb-btn" data-cmd="${b.c}" title="${b.t}">${icon(b.i)}</button>`).join('');
}
function updateToolbarState(){
const n = activeNote(); if (!n) return;
$$('.tb-btn').forEach(btn => {
const c = btn.dataset.cmd;
let on = false;
try {
if (['bold','italic','underline','strikeThrough','insertUnorderedList','insertOrderedList'].includes(c))
on = document.queryCommandState(c);
else if (['h1','h2','h3','blockquote'].includes(c)){
const b = currentBlock(); on = b && b.tagName && b.tagName.toLowerCase() === c;
} else if (c === 'codeblock'){ on = !!inPre(); }
} catch(e){}
btn.classList.toggle('on', !!on);
});
}
/* ---------- selection helpers ---------- */
function currentBlock(){
const sel = getSelection(); if (!sel.rangeCount) return null;
let node = sel.anchorNode, ed = editorEl();
if (!node || !ed.contains(node) || node === ed) return null;
if (node.nodeType === 3) node = node.parentNode;
while (node && node.parentNode && node.parentNode !== ed) node = node.parentNode;
return (node && node.parentNode === ed) ? node : null;
}
function inPre(){
const sel = getSelection(); if (!sel.rangeCount) return null;
let node = sel.anchorNode;
if (node && node.nodeType === 3) node = node.parentNode;
return node ? node.closest && node.closest('#editor pre') : null;
}
function saveSelection(){
const sel = getSelection();
if (sel.rangeCount && editorEl().contains(sel.anchorNode)) savedRange = sel.getRangeAt(0).cloneRange();
}
function restoreSelection(){
if (!savedRange) return;
const sel = getSelection(); sel.removeAllRanges();
try { sel.addRange(savedRange); } catch(e){}
}
function placeCaret(el, atEnd = true){
const r = document.createRange(); r.selectNodeContents(el); r.collapse(!atEnd);
const s = getSelection(); s.removeAllRanges(); s.addRange(r);
}
/* ---------- editor commands ---------- */
function exec(cmd){
const ed = editorEl(); ed.focus(); restoreSelection();
switch(cmd){
case 'h1': case 'h2': case 'h3': {
const b = currentBlock();
const tag = b && b.tagName && b.tagName.toLowerCase() === cmd ? 'p' : cmd;
document.execCommand('formatBlock', false, tag); break;
}
case 'blockquote': {
const b = currentBlock();
document.execCommand('formatBlock', false, b && b.tagName === 'BLOCKQUOTE' ? 'p' : 'blockquote'); break;
}
case 'codeblock': insertCodeBlock(); break;
case 'inlinecode': wrapSelection('code'); break;
case 'mark': wrapSelection('mark'); break;
case 'todo': insertTodo(); break;
case 'hr': insertHR(); break;
case 'link': insertLink(); break;
case 'clear':
document.execCommand('removeFormat'); document.execCommand('formatBlock', false, 'p'); break;
default: document.execCommand(cmd, false, null);
}
onEditorInput();
updateToolbarState();
}
function wrapSelection(tag){
const sel = getSelection();
if (!sel.rangeCount) return;
const r = sel.getRangeAt(0);
const container = r.commonAncestorContainer;
const existing = (container.nodeType === 3 ? container.parentNode : container).closest
? (container.nodeType === 3 ? container.parentNode : container).closest(tag) : null;
if (existing && editorEl().contains(existing)){ // unwrap
const parent = existing.parentNode;
while (existing.firstChild) parent.insertBefore(existing.firstChild, existing);
parent.removeChild(existing);
return;
}
if (r.collapsed) return;
const el = document.createElement(tag);
try { r.surroundContents(el); }
catch(e){ el.appendChild(r.extractContents()); r.insertNode(el); }
sel.removeAllRanges();
const nr = document.createRange(); nr.selectNodeContents(el); nr.collapse(false);
sel.addRange(nr);
}
function insertCodeBlock(){
const pre = document.createElement('pre');
pre.innerHTML = '<code><br></code>';
const b = currentBlock();
if (b && !plainText(b.outerHTML) && b.tagName !== 'PRE'){ b.replaceWith(pre); }
else {
const sel = getSelection();
if (sel.rangeCount){ const r = sel.getRangeAt(0); r.deleteContents(); r.insertNode(pre); }
else editorEl().appendChild(pre);
}
placeCaret(pre.querySelector('code') || pre);
}
function insertHR(){
const hr = document.createElement('hr');
const p = document.createElement('p'); p.innerHTML = '<br>';
const b = currentBlock();
if (b){ b.after(hr); hr.after(p); } else { editorEl().append(hr, p); }
placeCaret(p, false);
}
function insertLink(){
saveSelection();
uiPrompt('Insert link', 'Paste a URL to link the selected text.', 'https://').then(url => {
if (!url) return;
restoreSelection();
document.execCommand('createLink', false, url);
$$('#editor a').forEach(a => { a.target = '_blank'; a.rel = 'noopener'; });
onEditorInput();
});
}
function makeTodoLI(checked = false){
const li = document.createElement('li');
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.setAttribute('contenteditable', 'false');
if (checked) cb.setAttribute('checked', '');
const span = document.createElement('span'); span.className = 'td-text'; span.innerHTML = '<br>';
li.append(cb, span);
return li;
}
function insertTodo(){
const ul = document.createElement('ul'); ul.className = 'todo';
const li = makeTodoLI();
ul.appendChild(li);
const b = currentBlock();
if (b && !plainText(b.outerHTML) && !['UL','OL','PRE'].includes(b.tagName)) b.replaceWith(ul);
else {
const sel = getSelection();
if (sel.rangeCount){ const r = sel.getRangeAt(0); r.deleteContents(); r.insertNode(ul); }
else editorEl().appendChild(ul);
}
placeCaret(li.querySelector('.td-text'), false);
}
</script>
<script>
/* ---------- editor input & markdown shortcuts ---------- */
const persistEditor = debounce(() => {
const n = activeNote(); if (!n) return;
n.content = editorEl().innerHTML;
n.updated = Date.now();
saveSoon();
updateStats(n);
renderTagsIfIdle(n);
}, 350);
const rerenderListSoon = debounce(() => { renderSidebar(); renderList(); }, 900);
function renderTagsIfIdle(n){
if (document.activeElement && document.activeElement.id === 'tagInput') return;
renderTags(n);
}
function onEditorInput(){
const n = activeNote(); if (!n) return;
updatePlaceholder();
persistEditor();
rerenderListSoon();
}
function editorKeydown(e){
const mod = e.metaKey || e.ctrlKey;
const block = currentBlock();
const pre = inPre();
const txt = block ? block.textContent.replace(/\u00a0/g, ' ') : '';
// mod shortcuts
if (mod && !e.shiftKey && e.key.toLowerCase() === 'e'){ e.preventDefault(); exec('inlinecode'); return; }
if (mod && e.shiftKey && e.key.toLowerCase() === 'x'){ e.preventDefault(); exec('strikeThrough'); return; }
if (mod && ['1','2','3'].includes(e.key)){ e.preventDefault(); exec('h' + e.key); return; }
if (mod && e.shiftKey && e.key.toLowerCase() === 't'){ e.preventDefault(); exec('todo'); return; }
// --- inside code block ---
if (pre){
if (e.key === 'Enter' && !e.shiftKey){
e.preventDefault();
const sel = getSelection();
// caret offset within the pre's text
let offset = sel.anchorOffset;
let node = sel.anchorNode;
if (node !== pre.firstChild && pre.contains(node)){
// compute absolute offset across text nodes
offset = 0;
const walker = document.createTreeWalker(pre, NodeFilter.SHOW_TEXT);
let tn;
while ((tn = walker.nextNode())){
if (tn === node){ offset += sel.anchorOffset; break; }
offset += tn.textContent.length;
}
}
const text = pre.textContent;
const before = text.slice(0, offset);
// exit: caret at end and current line empty
if (offset >= text.length && /\n\n?$/.test(before)){
pre.textContent = text.replace(/\n+$/, '') || '\n';
const p = document.createElement('p'); p.innerHTML = '<br>';
pre.after(p);
placeCaret(p, false);
} else {
document.execCommand('insertText', false, '\n');
}
onEditorInput(); return;
}
if (e.key === 'Tab'){ e.preventDefault(); document.execCommand('insertText', false, ' '); return; }
if (e.key === 'Backspace' && !pre.textContent.trim()){
e.preventDefault();
const p = document.createElement('p'); p.innerHTML = '<br>';
pre.replaceWith(p); placeCaret(p, false); onEditorInput(); return;
}
return;
}
// --- space-triggered markdown ---
if (e.key === ' ' && block && !e.shiftKey){
const t = txt.trim();
let handled = true;
if (/^#{1,3}$/.test(t)){ replaceBlockWith(block, 'h' + t.length); }
else if (t === '-' || t === '*'){ clearBlock(block); document.execCommand('insertUnorderedList'); }
else if (t === '1.'){ clearBlock(block); document.execCommand('insertOrderedList'); }
else if (t === '>'){ clearBlock(block); document.execCommand('formatBlock', false, 'blockquote'); }
else if (t === '[]' || t === '[ ]'){ block.remove(); insertTodo(); }
else handled = false;
if (handled){ e.preventDefault(); onEditorInput(); return; }
}
// --- enter-triggered markdown ---
if (e.key === 'Enter' && !e.shiftKey && block){
const t = txt.trim();
if (t === '```'){ e.preventDefault(); block.remove(); insertCodeBlock(); onEditorInput(); return; }
if (/^-{3,}$/.test(t)){ e.preventDefault(); block.remove(); insertHR(); onEditorInput(); return; }
// exit heading on enter
if (/^H[1-3]$/.test(block.tagName)){
const sel = getSelection();
const r = sel.getRangeAt(0);
const atEnd = r.endOffset === (r.endContainer.textContent || '').length;
if (atEnd && sel.isCollapsed){
e.preventDefault();
const p = document.createElement('p'); p.innerHTML = '<br>';
block.after(p); placeCaret(p, false);
onEditorInput(); return;
}
}
// todo list: enter creates next todo, empty todo exits
const selNode = getSelection().anchorNode;
const selEl = selNode ? (selNode.nodeType === 3 ? selNode.parentNode : selNode) : null;
const liEl = selEl && selEl.closest ? selEl.closest('li') : null;
if (liEl && liEl.parentElement && liEl.parentElement.classList.contains('todo')){
e.preventDefault();
const td = liEl.querySelector('.td-text');
const ulEl = liEl.parentElement;
if (!td || !plainText(td.innerHTML)){
const p = document.createElement('p'); p.innerHTML = '<br>';
ulEl.after(p);
liEl.remove();
if (!ulEl.children.length) ulEl.remove();
placeCaret(p, false);
} else {
const nl = makeTodoLI();
liEl.after(nl);
placeCaret(nl.querySelector('.td-text'), false);
}
onEditorInput(); return;
}
}
// --- backspace in empty special blocks -> paragraph ---
if (e.key === 'Backspace' && block && !txt && ['BLOCKQUOTE'].includes(block.tagName)){
e.preventDefault();
const p = document.createElement('p'); p.innerHTML = '<br>';
block.replaceWith(p); placeCaret(p, false); onEditorInput(); return;
}
}
function replaceBlockWith(block, tag){
const el = document.createElement(tag); el.innerHTML = '<br>';
block.replaceWith(el); placeCaret(el, false);
}
function clearBlock(block){ block.innerHTML = ''; }
/* ---------- note actions ---------- */
function createNote(opts = {}){
const v = state.view;
const n = {
id: uid(), title: '', content: '',
notebook: v.type === 'notebook' ? v.id : (opts.notebook ?? null),
tags: v.type === 'tag' ? [v.id] : [],
pinned: false, fav: false, trashed: false,
created: Date.now(), updated: Date.now(),
};
state.notes.unshift(n);
state.selectedId = n.id;
save(); renderAll(); renderEditor();
$('#titleInput').focus();
if (matchMedia('(max-width: 960px)').matches) document.body.classList.add('pane-editor');
return n;
}
function selectNote(id, { focusEditor = false } = {}){
state.selectedId = id;
saveSoon(); renderList(); renderEditor();
if (focusEditor) editorEl().focus();
if (matchMedia('(max-width: 960px)').matches) document.body.classList.add('pane-editor');
}
function setView(type, id = null){
state.view = { type, id };
state.search = ''; $('#searchInput').value = ''; $('#searchClear').classList.remove('show');
const vis = visibleNotes();
if (!vis.find(n => n.id === state.selectedId)) state.selectedId = vis[0]?.id || null;
saveSoon(); renderAll(); renderEditor();
document.body.classList.remove('sb-open');
}
function trashNote(id){
const n = getNote(id); if (!n) return;
n.trashed = true; n.pinned = false;
if (state.selectedId === id){
const vis = visibleNotes();
state.selectedId = vis.find(x => x.id !== id)?.id || null;
}
save(); renderAll(); renderEditor();
toast('Moved to trash', 'trash');
}
function deleteForever(id){
state.notes = state.notes.filter(n => n.id !== id);
if (state.selectedId === id) state.selectedId = visibleNotes()[0]?.id || null;
save(); renderAll(); renderEditor();
toast('Deleted permanently', 'trash');
}
function restoreNote(id){
const n = getNote(id); if (!n) return;
n.trashed = false; save(); renderAll(); renderEditor();
toast('Note restored', 'restore');
}
function duplicateNote(id){
const n = getNote(id); if (!n) return;
const c = { ...n, id: uid(), title: (n.title || 'Untitled') + ' copy', pinned: false,
created: Date.now(), updated: Date.now(), tags: [...(n.tags || [])] };
state.notes.unshift(c); state.selectedId = c.id;
save(); renderAll(); renderEditor(); toast('Note duplicated', 'copy');
}
function togglePin(id){ const n = getNote(id); if (!n) return; n.pinned = !n.pinned; save(); renderAll(); renderEditor(); }
function toggleFav(id){ const n = getNote(id); if (!n) return; n.fav = !n.fav; save(); renderAll(); renderEditor(); }
function moveNote(id, nbId){
const n = getNote(id); if (!n) return;
n.notebook = nbId || null; n.updated = Date.now();
save(); renderAll(); renderEditor();
toast(nbId ? `Moved to ${getNb(nbId)?.name}` : 'Removed from notebook', 'book');
}
/* ---------- notebooks ---------- */
let nbInputOpen = false;
function openNotebookInput(){
if (nbInputOpen) return; nbInputOpen = true;
const body = $('.sb-section:not(.collapsed) [data-section="notebooks"]')?.closest('.sb-section')?.querySelector('.sb-section-body')
|| $$('.sb-section')[0].querySelector('.sb-section-body');
const input = document.createElement('input');
input.className = 'sb-inline-input'; input.placeholder = 'Notebook name…';
body.prepend(input); input.focus();
const done = commit => {
nbInputOpen = false;
const name = input.value.trim();
input.remove();
if (commit && name) addNotebook(name);
else renderSidebar();
};
input.addEventListener('keydown', e => {
if (e.key === 'Enter') done(true);
if (e.key === 'Escape') done(false);
e.stopPropagation();
});
input.addEventListener('blur', () => done(true));
}
function addNotebook(name){
const nb = { id: uid(), name, color: NB_COLORS[state.nbColorIdx++ % NB_COLORS.length] };
state.notebooks.push(nb);
save(); renderSidebar();
setView('notebook', nb.id);
toast(`Notebook “${name}” created`, 'book');
}
function renameNotebook(id){
const nb = getNb(id); if (!nb) return;
uiPrompt('Rename notebook', '', nb.name).then(v => {
if (v && v.trim()){ nb.name = v.trim(); save(); renderAll(); renderEditor(); }
});
}
function deleteNotebook(id){
const nb = getNb(id); if (!nb) return;
const count = nbCount(id);
uiConfirm('Delete notebook', `“${nb.name}” will be removed. ${count} note${count === 1 ? '' : 's'} inside will be kept and moved to “No notebook”.`, 'Delete').then(ok => {
if (!ok) return;
state.notes.forEach(n => { if (n.notebook === id) n.notebook = null; });
state.notebooks = state.notebooks.filter(b => b.id !== id);
if (state.view.type === 'notebook' && state.view.id === id) state.view = { type: 'all', id: null };
save(); renderAll(); renderEditor();
toast('Notebook deleted', 'trash');
});
}
/* ---------- markdown export ---------- */
function htmlToMd(html){
const d = document.createElement('div'); d.innerHTML = html || '';
const inline = node => {
let out = '';
node.childNodes.forEach(ch => {
if (ch.nodeType === 3){ out += ch.textContent; return; }
const t = ch.tagName?.toLowerCase();
const inner = inline(ch);
if (t === 'strong' || t === 'b') out += `**${inner}**`;
else if (t === 'em' || t === 'i') out += `*${inner}*`;
else if (t === 's' || t === 'strike' || t === 'del') out += `~~${inner}~~`;
else if (t === 'code') out += '`' + ch.textContent + '`';
else if (t === 'mark') out += `==${inner}==`;
else if (t === 'a') out += `[${inner}](${ch.getAttribute('href') || ''})`;
else if (t === 'br') out += ' \n';
else out += inner;
});
return out;
};
const blocks = (node, depth = 0) => {
let out = '';
node.childNodes.forEach(ch => {
if (ch.nodeType === 3){ const t = ch.textContent.trim(); if (t) out += t + '\n\n'; return; }
const t = ch.tagName?.toLowerCase();
if (t === 'h1') out += `# ${inline(ch).trim()}\n\n`;
else if (t === 'h2') out += `## ${inline(ch).trim()}\n\n`;
else if (t === 'h3') out += `### ${inline(ch).trim()}\n\n`;
else if (t === 'p' || t === 'div'){ const s = inline(ch).trim(); out += (s || '') + '\n\n'; }
else if (t === 'blockquote') out += inline(ch).trim().split('\n').map(l => '> ' + l).join('\n') + '\n\n';
else if (t === 'pre') out += '```\n' + ch.textContent.replace(/\n$/, '') + '\n```\n\n';
else if (t === 'hr') out += '---\n\n';
else if (t === 'ul' || t === 'ol'){
const todo = ch.classList.contains('todo');
[...ch.children].forEach((li, i) => {
const pad = ' '.repeat(depth);
if (todo){
const cb = li.querySelector('input');
const txt = inline(li).trim();
out += `${pad}- [${cb && cb.hasAttribute('checked') ? 'x' : ' '}] ${txt}\n`;
} else {
const bullet = t === 'ul' ? '-' : (i + 1) + '.';
out += `${pad}${bullet} ${inline(li).trim()}\n`;
}
});
out += '\n';
}
else out += blocks(ch, depth);
});
return out;
};
return blocks(d).replace(/\n{3,}/g, '\n\n').trim() + '\n';
}
function noteToMd(n){
const nb = getNb(n.notebook);
let fm = `---\ntitle: ${n.title || 'Untitled'}\ncreated: ${new Date(n.created).toISOString()}\n`;
if (nb) fm += `notebook: ${nb.name}\n`;
if (n.tags?.length) fm += `tags: ${n.tags.join(', ')}\n`;
fm += `---\n\n`;
return fm + (n.title ? `# ${n.title}\n\n` : '') + htmlToMd(n.content);
}
function downloadFile(name, content, type = 'text/plain'){
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([content], { type }));
a.download = name; a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 2000);
}
function exportNote(id){
const n = getNote(id); if (!n) return;
downloadFile((n.title || 'untitled').replace(/[^\w\- ]+/g, '').trim().replace(/\s+/g, '-').toLowerCase() + '.md', noteToMd(n), 'text/markdown');
toast('Exported as Markdown', 'download');
}
</script>
<script>
/* ---------- toast ---------- */
let toastTimer;
function toast(msg, ic = 'check'){
const t = $('#toast');
t.innerHTML = icon(ic) + esc(msg);
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 2200);
}
/* ---------- dialog (prompt / confirm) ---------- */
function uiDialog({ title, msg = '', value = null, placeholder = '', okText = 'OK', danger = false }){
return new Promise(resolve => {
const ov = $('#dialogOverlay'), input = $('#dialogInput');
$('#dlgTitle').textContent = title;
$('#dlgMsg').textContent = msg;
$('#dlgMsg').style.display = msg ? '' : 'none';
const ok = $('#dlgOk');
ok.textContent = okText;
ok.className = 'btn ' + (danger ? 'danger' : 'primary');
input.style.display = value !== null ? '' : 'none';
input.value = value ?? '';
input.placeholder = placeholder;
ov.classList.add('show');
if (value !== null){ input.focus(); input.select(); }
else ok.focus();
const done = val => {
ov.classList.remove('show');
ok.onclick = $('#dlgCancel').onclick = input.onkeydown = ov.onclick = null;
resolve(val);
};
ok.onclick = () => done(value !== null ? input.value : true);
$('#dlgCancel').onclick = () => done(null);
ov.onclick = e => { if (e.target === ov) done(null); };
input.onkeydown = e => {
if (e.key === 'Enter') done(input.value);
if (e.key === 'Escape') done(null);
e.stopPropagation();
};
ov.onkeydown = e => { if (e.key === 'Escape') done(null); };
});
}
const uiPrompt = (title, msg, value = '', placeholder = '') => uiDialog({ title, msg, value, placeholder });
const uiConfirm = (title, msg, okText = 'Delete') => uiDialog({ title, msg, okText, danger: true });
/* ---------- context menu ---------- */
let ctxEl = null;
function closeMenu(){ if (ctxEl){ ctxEl.remove(); ctxEl = null; } }
function openMenu(items, x, y){
closeMenu();
const m = document.createElement('div');
m.className = 'ctx-menu';
items.forEach(it => {
if (it === '-'){ m.insertAdjacentHTML('beforeend', '<div class="ctx-divider"></div>'); return; }
if (it.label2){ m.insertAdjacentHTML('beforeend', `<div class="ctx-label">${esc(it.label2)}</div>`); return; }
const b = document.createElement('button');
b.className = 'ctx-item' + (it.danger ? ' danger' : '') + (it.sub ? ' has-sub' : '');
b.innerHTML = `${it.icon ? icon(it.icon) : ''}${it.dot ? `<span class="dot" style="background:${it.dot}"></span>` : ''}
<span>${esc(it.label)}</span>
${it.checked ? `<span class="check">${icon('check')}</span>` : ''}
${it.sub ? `<span class="arrow">${icon('chevR')}</span>` : ''}`;
if (it.sub){
const sub = document.createElement('div');
sub.className = 'ctx-submenu';
it.sub.forEach(s => {
const sb = document.createElement('button');
sb.className = 'ctx-item';
sb.innerHTML = `${s.dot ? `<span class="dot" style="background:${s.dot}"></span>` : ''}<span>${esc(s.label)}</span>${s.checked ? `<span class="check">${icon('check')}</span>` : ''}`;
sb.onclick = e => { e.stopPropagation(); closeMenu(); s.action && s.action(); };
sub.appendChild(sb);
});
b.appendChild(sub);
}
if (it.action) b.onclick = e => { e.stopPropagation(); closeMenu(); it.action(); };
m.appendChild(b);
});
document.body.appendChild(m);
ctxEl = m;
const r = m.getBoundingClientRect();
m.style.left = clamp(x, 8, innerWidth - r.width - 8) + 'px';
m.style.top = clamp(y, 8, innerHeight - r.height - 8) + 'px';
}
document.addEventListener('pointerdown', e => { if (ctxEl && !ctxEl.contains(e.target)) closeMenu(); }, true);
addEventListener('blur', closeMenu);
function noteMenu(id, x, y){
const n = getNote(id); if (!n) return;
let items;
if (n.trashed){
items = [
{ label: 'Restore', icon: 'restore', action: () => restoreNote(id) },
'-',
{ label: 'Delete forever', icon: 'trash', danger: true,
action: () => uiConfirm('Delete forever', 'This note will be permanently erased. This cannot be undone.').then(ok => ok && deleteForever(id)) },
];
} else {
items = [
{ label: n.pinned ? 'Unpin' : 'Pin to top', icon: 'pin', action: () => togglePin(id) },
{ label: n.fav ? 'Remove favorite' : 'Add to favorites', icon: 'star', action: () => toggleFav(id) },
{ label: 'Move to', icon: 'book', sub: [
{ label: 'No notebook', dot: 'var(--text-3)', checked: !n.notebook, action: () => moveNote(id, null) },
...state.notebooks.map(b => ({ label: b.name, dot: b.color, checked: n.notebook === b.id, action: () => moveNote(id, b.id) })),
] },
{ label: 'Duplicate', icon: 'copy', action: () => duplicateNote(id) },
'-',
{ label: 'Copy as Markdown', icon: 'cmd', action: () => { navigator.clipboard?.writeText(noteToMd(n)); toast('Markdown copied', 'copy'); } },
{ label: 'Export .md', icon: 'download', action: () => exportNote(id) },
'-',
{ label: 'Move to trash', icon: 'trash', danger: true, action: () => trashNote(id) },
];
}
openMenu(items, x, y);
}
function notebookMenu(id, x, y){
const nb = getNb(id); if (!nb) return;
openMenu([
{ label: 'Rename', icon: 'edit', action: () => renameNotebook(id) },
{ label: 'Change color', icon: 'tag', sub: NB_COLORS.map(c => ({
label: '', dot: c, checked: nb.color === c,
action: () => { nb.color = c; save(); renderAll(); renderEditor(); } })) },
'-',
{ label: 'Delete notebook', icon: 'trash', danger: true, action: () => deleteNotebook(id) },
], x, y);
}
/* ---------- command palette ---------- */
let palIdx = 0, palItems = [];
function openPalette(){
$('#paletteOverlay').classList.add('show');
const inp = $('#paletteInput');
inp.value = ''; palIdx = 0;
renderPalette('');
setTimeout(() => inp.focus(), 30);
}
function closePalette(){ $('#paletteOverlay').classList.remove('show'); }
function paletteCommands(){
return [
{ group: 'Actions', label: 'New note', icon: 'plus', hint: '⌘N', run: () => createNote() },
{ group: 'Actions', label: 'New notebook', icon: 'book', run: () => { closePalette(); openNotebookInput(); } },
{ group: 'Actions', label: 'Toggle theme', icon: 'moon', hint: '⌘⇧L', run: toggleTheme },
{ group: 'Actions', label: 'Toggle focus mode', icon: 'focus', run: toggleFocus },
{ group: 'Actions', label: 'Export current note (.md)', icon: 'download', run: () => activeNote() && exportNote(activeNote().id) },
{ group: 'Actions', label: 'Export all data (JSON)', icon: 'download', run: exportAll },
{ group: 'View', label: 'Go to All Notes', icon: 'note', run: () => setView('all') },
{ group: 'View', label: 'Go to Pinned', icon: 'pin', run: () => setView('pinned') },
{ group: 'View', label: 'Go to Favorites', icon: 'star', run: () => setView('fav') },
{ group: 'View', label: 'Go to Trash', icon: 'trash', run: () => setView('trash') },
...state.notebooks.map(b => ({ group: 'Notebooks', label: 'Go to ' + b.name, icon: 'book', dot: b.color, run: () => setView('notebook', b.id) })),
];
}
function renderPalette(q){
const res = $('#paletteResults');
palItems = [];
let html = '', lastGroup = null;
const addGroup = g => { if (g !== lastGroup){ html += `<div class="pal-group">${g}</div>`; lastGroup = g; } };
paletteCommands().forEach(c => {
const s = fuzzyScore(q, c.label);
if (q && s <= 0) return;
palItems.push({ ...c, score: s });
});
palItems.sort((a, b) => b.score - a.score);
const cmds = palItems.splice(0, palItems.length);
const notes = state.notes
.map(n => ({ n, score: Math.max(fuzzyScore(q, n.title || 'Untitled'), fuzzyScore(q, plainText(n.content)) * .6) }))
.filter(x => !q || x.score > 0)
.sort((a, b) => b.score - a.score || b.n.updated - a.n.updated)
.slice(0, q ? 7 : 5);
const items = [...cmds.slice(0, q ? 8 : 6), ...notes.map(x => ({
group: x.n.trashed ? 'Trash' : 'Notes',
label: x.n.title || 'Untitled',
icon: x.n.trashed ? 'trash' : 'note',
sub: plainText(x.n.content).slice(0, 60),
run: () => { setView(x.n.trashed ? 'trash' : 'all'); selectNote(x.n.id); },
}))];
palItems = items;
items.forEach((it, i) => {
addGroup(it.group);
html += `<div class="pal-item${i === palIdx ? ' sel' : ''}" data-i="${i}">
${it.dot ? `<span class="dot" style="width:9px;height:9px;border-radius:50%;background:${it.dot}"></span>` : icon(it.icon)}
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(it.label)}</span>
${it.hint ? `<span class="sub mono">${it.hint}</span>` : it.sub ? `<span class="sub">${esc(it.sub)}</span>` : ''}
</div>`;
});
res.innerHTML = html || `<div class="pal-empty">No matches. Try a different search.</div>`;
}
function paletteKey(e){
if (e.key === 'Escape'){ closePalette(); return; }
if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
e.preventDefault();
palIdx = (palIdx + (e.key === 'ArrowDown' ? 1 : -1) + palItems.length) % Math.max(palItems.length, 1);
$$('.pal-item').forEach((el, i) => el.classList.toggle('sel', i === palIdx));
$('.pal-item.sel')?.scrollIntoView({ block: 'nearest' });
return;
}
if (e.key === 'Enter'){
const it = palItems[palIdx];
if (it){ closePalette(); it.run(); }
}
}
/* ---------- theme / focus / fonts ---------- */
function applyTheme(){
document.documentElement.dataset.theme = state.theme;
$('#themeBtn').innerHTML = icon(state.theme === 'dark' ? 'sun' : 'moon');
}
function toggleTheme(){
state.theme = state.theme === 'dark' ? 'light' : 'dark';
applyTheme(); saveSoon();
}
function toggleFocus(){
state.focus = !state.focus;
document.body.classList.toggle('focus', state.focus);
$('#focusBtn').classList.toggle('on', state.focus);
saveSoon();
if (state.focus) editorEl().focus();
}
function applyPrefs(){
applyTheme();
document.body.dataset.efont = state.efont;
document.body.classList.toggle('focus', state.focus);
$('#focusBtn').classList.toggle('on', state.focus);
$('#sidebar').style.width = state.widths.sidebar + 'px';
$('#listPane').style.width = state.widths.list + 'px';
}
/* ---------- import / export ---------- */
function exportAll(){
downloadFile('inkwell-backup.json', JSON.stringify({ notes: state.notes, notebooks: state.notebooks }, null, 2), 'application/json');
toast('Backup exported', 'download');
}
function importAll(file){
const r = new FileReader();
r.onload = () => {
try {
const data = JSON.parse(r.result);
if (!Array.isArray(data.notes) || !Array.isArray(data.notebooks)) throw 0;
state.notes = data.notes; state.notebooks = data.notebooks;
state.selectedId = data.notes.find(n => !n.trashed)?.id || null;
save(); renderAll(); renderEditor();
toast(`Imported ${data.notes.length} notes`, 'upload');
} catch(e){ toast('Import failed — invalid file', 'x'); }
};
r.readAsText(file);
}
/* ---------- settings menu ---------- */
function settingsMenu(x, y){
openMenu([
{ label2: 'Appearance' },
{ label: state.theme === 'dark' ? 'Switch to light' : 'Switch to dark', icon: state.theme === 'dark' ? 'sun' : 'moon', action: toggleTheme },
{ label: 'Editor font', icon: 'edit', sub: [
{ label: 'Sans', checked: state.efont === 'sans', action: () => setFont('sans') },
{ label: 'Serif', checked: state.efont === 'serif', action: () => setFont('serif') },
{ label: 'Mono', checked: state.efont === 'mono', action: () => setFont('mono') },
] },
{ label2: 'Sort notes by' },
{ label: 'Last updated', icon: 'sort', checked: state.sort === 'updated', action: () => setSort('updated') },
{ label: 'Date created', icon: 'sort', checked: state.sort === 'created', action: () => setSort('created') },
{ label: 'Title A–Z', icon: 'sort', checked: state.sort === 'title', action: () => setSort('title') },
{ label2: 'Data' },
{ label: 'Export backup (JSON)', icon: 'download', action: exportAll },
{ label: 'Import backup…', icon: 'upload', action: () => $('#importFile').click() },
{ label: 'Restore sample data', icon: 'restore', action: () =>
uiConfirm('Restore sample data', 'Your current notes will be replaced with the original sample content.', 'Restore').then(ok => {
if (!ok) return; seed(); save(); applyPrefs(); renderAll(); renderEditor(); toast('Sample data restored', 'restore');
}) },
'-',
{ label: 'Erase everything', icon: 'trash', danger: true, action: () =>
uiConfirm('Erase everything', 'All notes and notebooks will be permanently deleted from this browser.', 'Erase').then(ok => {
if (!ok) return; state.notes = []; state.notebooks = []; state.selectedId = null;
save(); renderAll(); renderEditor(); toast('All data erased', 'trash');
}) },
], x, y);
}
function setFont(f){ state.efont = f; document.body.dataset.efont = f; saveSoon(); }
function setSort(s){ state.sort = s; saveSoon(); renderList(); }
</script>
<script>
/* ---------- drag & drop notes -> notebooks ---------- */
function initDnD(){
const scroll = $('#notesScroll');
scroll.addEventListener('dragstart', e => {
const card = e.target.closest('.note-card'); if (!card) return;
e.dataTransfer.setData('text/note-id', card.dataset.id);
e.dataTransfer.effectAllowed = 'move';
card.classList.add('dragging');
});
scroll.addEventListener('dragend', () => $$('.note-card.dragging').forEach(c => c.classList.remove('dragging')));
$('#sbScroll').addEventListener('dragover', e => {
const item = e.target.closest('[data-nb]');
if (item && e.dataTransfer.types.includes('text/note-id')){
e.preventDefault(); e.dataTransfer.dropEffect = 'move';
$$('.sb-item.drag-over').forEach(el => el.classList.remove('drag-over'));
item.classList.add('drag-over');
}
});
$('#sbScroll').addEventListener('dragleave', e => {
if (!e.relatedTarget || !$('#sbScroll').contains(e.relatedTarget))
$$('.sb-item.drag-over').forEach(el => el.classList.remove('drag-over'));
});
$('#sbScroll').addEventListener('drop', e => {
const item = e.target.closest('[data-nb]');
$$('.sb-item.drag-over').forEach(el => el.classList.remove('drag-over'));
if (!item) return;
e.preventDefault();
const id = e.dataTransfer.getData('text/note-id');
if (id) moveNote(id, item.dataset.nb);
});
}
/* ---------- resizers ---------- */
function initResizers(){
const bind = (el, get, set, min, max) => {
el.addEventListener('pointerdown', e => {
e.preventDefault();
el.classList.add('dragging');
const startX = e.clientX, startW = get();
const move = ev => set(clamp(startW + ev.clientX - startX, min, max));
const up = () => {
el.classList.remove('dragging');
removeEventListener('pointermove', move); removeEventListener('pointerup', up);
saveSoon();
};
addEventListener('pointermove', move); addEventListener('pointerup', up);
});
};
bind($('#resizer1'),
() => $('#sidebar').offsetWidth,
w => { $('#sidebar').style.width = w + 'px'; state.widths.sidebar = w; }, 180, 400);
bind($('#resizer2'),
() => $('#listPane').offsetWidth,
w => { $('#listPane').style.width = w + 'px'; state.widths.list = w; }, 220, 520);
}
/* ---------- events ---------- */
function initEvents(){
/* static buttons */
$('#logoMark').innerHTML = icon('logo');
$('#newNoteIcon').innerHTML = icon('plus');
$('#searchIcon').innerHTML = icon('search');
$('#searchClear').innerHTML = icon('x');
$('#sortBtn').innerHTML = icon('sort');
$('#settingsBtn').innerHTML = icon('settings');
$('#hamburgerBtn').innerHTML = icon('menu');
$('#backBtn').innerHTML = icon('back');
$('#pinBtn').innerHTML = icon('pin');
$('#favBtn').innerHTML = icon('star');
$('#focusBtn').innerHTML = icon('focus');
$('#moreBtn').innerHTML = icon('more');
$('#palIcon').innerHTML = icon('cmd');
$('#newNoteBtn').onclick = () => createNote();
$('#themeBtn').onclick = toggleTheme;
$('#settingsBtn').onclick = e => { const r = e.currentTarget.getBoundingClientRect(); settingsMenu(r.left, r.top - 8); };
$('#hamburgerBtn').onclick = () => document.body.classList.add('sb-open');
$('#sidebarScrim').onclick = () => document.body.classList.remove('sb-open');
$('#backBtn').onclick = () => document.body.classList.remove('pane-editor');
$('#pinBtn').onclick = () => togglePin(state.selectedId);
$('#favBtn').onclick = () => toggleFav(state.selectedId);
$('#focusBtn').onclick = toggleFocus;
$('#moreBtn').onclick = e => {
const r = e.currentTarget.getBoundingClientRect();
noteMenu(state.selectedId, r.right - 210, r.bottom + 6);
};
$('#sortBtn').onclick = e => {
const r = e.currentTarget.getBoundingClientRect();
openMenu([
{ label2: 'Sort by' },
{ label: 'Last updated', icon: 'clock', checked: state.sort === 'updated', action: () => setSort('updated') },
{ label: 'Date created', icon: 'plus', checked: state.sort === 'created', action: () => setSort('created') },
{ label: 'Title A–Z', icon: 'sort', checked: state.sort === 'title', action: () => setSort('title') },
], r.right - 200, r.bottom + 6);
};
$('#importFile').addEventListener('change', e => { if (e.target.files[0]) importAll(e.target.files[0]); e.target.value = ''; });
/* sidebar */
const sb = $('#sbScroll');
sb.addEventListener('click', e => {
const sec = e.target.closest('.sb-section-head');
const addNb = e.target.closest('[data-addnb]');
if (addNb){ e.stopPropagation(); state.collapsed.notebooks = false; renderSidebar(); openNotebookInput(); return; }
if (sec){
const key = sec.dataset.section;
state.collapsed[key] = !state.collapsed[key];
saveSoon(); renderSidebar(); return;
}
const item = e.target.closest('[data-view]');
if (item) setView(item.dataset.view, item.dataset.id || null);
});
sb.addEventListener('contextmenu', e => {
const item = e.target.closest('[data-nb]');
if (item){ e.preventDefault(); notebookMenu(item.dataset.nb, e.clientX, e.clientY); }
});
/* note list */
const scroll = $('#notesScroll');
scroll.addEventListener('click', e => {
const card = e.target.closest('.note-card');
if (card) selectNote(card.dataset.id);
});
scroll.addEventListener('contextmenu', e => {
const card = e.target.closest('.note-card');
if (card){ e.preventDefault(); if (state.selectedId !== card.dataset.id) selectNote(card.dataset.id); noteMenu(card.dataset.id, e.clientX, e.clientY); }
});
scroll.addEventListener('dblclick', e => {
const card = e.target.closest('.note-card');
if (card && matchMedia('(min-width: 961px)').matches){ selectNote(card.dataset.id); $('#titleInput').focus(); }
});
/* search */
const si = $('#searchInput');
si.addEventListener('input', () => {
state.search = si.value;
$('#searchClear').classList.toggle('show', !!si.value);
renderList();
});
$('#searchClear').onclick = () => { si.value = ''; state.search = ''; $('#searchClear').classList.remove('show'); renderList(); si.focus(); };
/* toolbar */
const tb = $('#toolbar');
tb.addEventListener('pointerdown', e => { if (e.target.closest('.tb-btn')) e.preventDefault(); });
tb.addEventListener('click', e => {
const b = e.target.closest('.tb-btn'); if (!b) return;
exec(b.dataset.cmd);
});
/* title */
const ti = $('#titleInput');
ti.addEventListener('input', () => {
const n = activeNote(); if (!n) return;
n.title = ti.value; n.updated = Date.now();
saveSoon(); rerenderListSoon();
$('#crumb b') && ($('#crumb b').textContent = n.title || 'Untitled');
});
ti.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === 'ArrowDown'){ e.preventDefault(); editorEl().focus(); placeCaretAtStart(); }
});
function placeCaretAtStart(){
const ed = editorEl();
const r = document.createRange(); r.selectNodeContents(ed); r.collapse(true);
const s = getSelection(); s.removeAllRanges(); s.addRange(r);
}
/* editor */
const ed = editorEl();
ed.addEventListener('input', onEditorInput);
ed.addEventListener('keydown', editorKeydown);
ed.addEventListener('click', e => {
if (e.target.matches('ul.todo input[type=checkbox]')){
if (e.target.checked) e.target.setAttribute('checked', ''); else e.target.removeAttribute('checked');
onEditorInput();
}
const a = e.target.closest('a');
if (a && (e.metaKey || e.ctrlKey)){ window.open(a.href, '_blank'); }
});
ed.addEventListener('keyup', updateToolbarState);
ed.addEventListener('mouseup', () => { updateToolbarState(); saveSelection(); });
ed.addEventListener('paste', e => {
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
});
document.addEventListener('selectionchange', () => {
if (document.activeElement === ed) updateToolbarState();
});
/* meta line: notebook select (delegated, element re-created) */
$('#metaLine').addEventListener('change', e => {
if (e.target.id !== 'nbSelect') return;
const n = activeNote(); if (!n) return;
n.notebook = e.target.value || null;
save(); renderAll(); renderEditor();
});
/* tags */
const ft = $('.editor-footer');
ft.addEventListener('click', e => {
const rm = e.target.closest('[data-rmtag]');
if (rm){
const n = activeNote(); if (!n) return;
n.tags = (n.tags || []).filter(t => t !== rm.dataset.rmtag);
save(); renderTags(n); renderAll(); renderTags(n);
}
});
ft.addEventListener('keydown', e => {
if (e.target.id !== 'tagInput') return;
e.stopPropagation();
const n = activeNote(); if (!n) return;
if ((e.key === 'Enter' || e.key === ',') && e.target.value.trim()){
e.preventDefault();
const t = e.target.value.trim().replace(/^#/, '').toLowerCase();
n.tags = n.tags || [];
if (!n.tags.includes(t)) n.tags.push(t);
save(); renderTags(n); renderAll();
$('#tagInput')?.focus();
}
if (e.key === 'Backspace' && !e.target.value && n.tags?.length){
n.tags.pop(); save(); renderTags(n); renderAll(); $('#tagInput')?.focus();
}
});
/* palette */
$('#paletteInput').addEventListener('input', e => { palIdx = 0; renderPalette(e.target.value); });
$('#paletteInput').addEventListener('keydown', paletteKey);
$('#paletteOverlay').addEventListener('pointerdown', e => { if (e.target.id === 'paletteOverlay') closePalette(); });
$('#paletteResults').addEventListener('click', e => {
const it = e.target.closest('.pal-item');
if (it){ const item = palItems[+it.dataset.i]; closePalette(); item && item.run(); }
});
/* global keys */
document.addEventListener('keydown', e => {
const mod = e.metaKey || e.ctrlKey;
if (mod && e.key.toLowerCase() === 'k'){ e.preventDefault(); $('#paletteOverlay').classList.contains('show') ? closePalette() : openPalette(); return; }
if (mod && e.key.toLowerCase() === 'n'){ e.preventDefault(); createNote(); return; }
if (mod && e.shiftKey && e.key.toLowerCase() === 'l'){ e.preventDefault(); toggleTheme(); return; }
if (mod && e.shiftKey && e.key.toLowerCase() === 'f'){ e.preventDefault(); $('#searchInput').focus(); return; }
if (mod && e.key.toLowerCase() === 's'){ e.preventDefault(); save(); toast('All changes saved', 'check'); return; }
if (e.key === 'Escape'){
if (ctxEl) closeMenu();
else if (document.body.classList.contains('sb-open')) document.body.classList.remove('sb-open');
else if (state.focus) toggleFocus();
}
});
}
/* ---------- init ---------- */
function init(){
load();
buildToolbar();
applyPrefs();
renderAll();
renderEditor();
initEvents();
initDnD();
initResizers();
try { document.execCommand('defaultParagraphSeparator', false, 'p'); } catch(e){}
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"k3","provider":"kimi","persona":"full","sessionId":"8a295e3f-51c8-4345-ac4c-9fd0b9e8ea61","tokensIn":1874411,"tokensOut":48020,"tokensTotal":1922431,"turns":32,"toolCalls":31,"failedToolCalls":0,"timestamp":"2026-07-20T20:39:34.980Z"} -->