Notes
muse spark 1.2 contributor · 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 63,520 bytes · SHA-256 3d735bc6697f
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inkwell — Notes</title>
<style>
/* ========== TUNABLE CONSTANTS ========== */
:root{
--bg:#fdfbf7;
--bg-sidebar:#f6f1e8;
--bg-notelist:#fcf9f4;
--bg-editor:#ffffff;
--bg-hover:#efe8d8;
--bg-active:#e8ddd0;
--border:#e8ddd0;
--border-strong:#d6c9b6;
--text:#1a1a1a;
--text-secondary:#6b6560;
--text-muted:#9a9590;
--accent:#c07a3a;
--accent-hover:#a8682e;
--accent-light:#fdf0e0;
--accent-bg:#f5e6d3;
--radius:8px;
--radius-sm:6px;
--shadow:0 1px 3px rgba(0,0,0,0.06), 0 4px 12px rgba(0,0,0,0.04);
--shadow-lg:0 8px 24px rgba(0,0,0,0.08);
--font-sans:'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
--font-serif:'Georgia', 'Times New Roman', serif;
--font-mono:'SF Mono', 'Cascadia Code', monospace;
--sidebar-w:260px;
--notelist-w:340px;
}
[data-theme="dark"]{
--bg:#1a1a1e;
--bg-sidebar:#232326;
--bg-notelist:#1e1e21;
--bg-editor:#252529;
--bg-hover:#2e2e32;
--bg-active:#36363c;
--border:#2e2e32;
--border-strong:#3a3a40;
--text:#e8e6e3;
--text-secondary:#9a9590;
--text-muted:#6b6560;
--accent:#d49a5a;
--accent-hover:#e0a86a;
--accent-light:#2a2318;
--accent-bg:#2e2618;
--shadow:0 1px 3px rgba(0,0,0,0.3);
--shadow-lg:0 8px 24px rgba(0,0,0,0.4);
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font-sans);background:var(--bg);color:var(--text);height:100vh;overflow:hidden;display:flex;flex-direction:column;-webkit-font-smoothing:antialiased}
button{font-family:inherit;cursor:pointer}
input{font-family:inherit}
/* Header */
.app-header{height:44px;display:flex;align-items:center;gap:12px;padding:0 16px;border-bottom:1px solid var(--border);background:var(--bg-sidebar);flex-shrink:0}
.app-logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;letter-spacing:-0.02em}
.app-logo-mark{width:28px;height:28px;background:var(--accent);border-radius:7px;display:grid;place-items:center;color:white;font-size:14px}
.header-actions{margin-left:auto;display:flex;align-items:center;gap:8px}
.icon-btn{width:32px;height:32px;border:1px solid var(--border);background:var(--bg-editor);border-radius:8px;display:grid;place-items:center;color:var(--text-secondary);transition:all .15s}
.icon-btn:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-strong)}
.search-wrap{position:relative}
.search-wrap input{width:220px;height:32px;padding:0 32px 0 32px;border:1px solid var(--border);border-radius:20px;background:var(--bg-editor);font-size:13px;outline:none;transition:border-color .15s}
.search-wrap input:focus{border-color:var(--accent)}
.search-wrap svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-muted);pointer-events:none}
.search-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:20px;height:20px;border:none;background:var(--bg-hover);border-radius:50%;display:grid;place-items:center;font-size:12px;color:var(--text-muted);cursor:pointer}
.theme-toggle{width:32px;height:32px;border-radius:50%;border:1px solid var(--border);background:var(--bg-editor);display:grid;place-items:center;font-size:14px}
/* Main layout */
.main{flex:1;display:flex;overflow:hidden;min-height:0}
.pane{flex-shrink:0;overflow:hidden;display:flex;flex-direction:column;min-width:0}
#sidebar{width:var(--sidebar-w);background:var(--bg-sidebar);border-right:1px solid var(--border)}
#notelist{width:var(--notelist-w);background:var(--bg-notelist);border-right:1px solid var(--border)}
#editor{flex:1;background:var(--bg-editor);min-width:320px}
.drag-handle{width:4px;flex-shrink:0;background:transparent;cursor:col-resize;position:relative;z-index:5;transition:background .15s}
.drag-handle:hover,.drag-handle.dragging{background:var(--accent);opacity:0.6}
.drag-handle::after{content:'';position:absolute;inset:0 -4px 0 -4px}
/* Sidebar */
.sidebar-top{padding:16px 12px 8px;display:flex;align-items:center;justify-content:space-between}
.sidebar-title{font-size:11px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--text-muted)}
.btn-new-nb{font-size:12px;padding:5px 10px;border-radius:20px;border:1px solid var(--border);background:var(--bg-editor);color:var(--text-secondary);display:flex;align-items:center;gap:4px}
.btn-new-nb:hover{border-color:var(--accent);color:var(--accent)}
.sidebar-nav{padding:8px;flex:1;overflow-y:auto}
.nav-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;cursor:pointer;font-size:13.5px;color:var(--text-secondary);transition:all .12s;user-select:none;position:relative}
.nav-item:hover{background:var(--bg-hover);color:var(--text)}
.nav-item.active{background:var(--accent-bg);color:var(--text);font-weight:600}
.nav-item.active .nav-icon{color:var(--accent)}
.nav-icon{width:20px;height:20px;display:grid;place-items:center;font-size:13px;flex-shrink:0}
.nav-count{margin-left:auto;font-size:11px;background:var(--bg-editor);border:1px solid var(--border);padding:1px 6px;border-radius:10px;color:var(--text-muted);min-width:20px;text-align:center}
.nav-item.active .nav-count{background:var(--bg-editor)}
.notebook-row{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:8px;cursor:pointer;font-size:13.5px;color:var(--text-secondary);transition:all .12s}
.notebook-row:hover{background:var(--bg-hover)}
.notebook-row.active{background:var(--bg-active);color:var(--text);font-weight:600}
.nb-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.nb-actions{margin-left:auto;display:flex;gap:2px;opacity:0;transition:opacity .12s}
.notebook-row:hover .nb-actions{opacity:1}
.nb-actions button{width:22px;height:22px;border:none;background:transparent;border-radius:5px;display:grid;place-items:center;color:var(--text-muted);font-size:12px}
.nb-actions button:hover{background:var(--bg-editor);color:var(--text)}
.sidebar-bottom{padding:12px;border-top:1px solid var(--border);display:flex;align-items:center;gap:8px}
.collapse-btn{flex:1;font-size:12px;padding:7px;border:1px solid var(--border);background:var(--bg-editor);border-radius:8px;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;gap:6px}
.storage-info{font-size:11px;color:var(--text-muted);text-align:center;padding:8px}
/* Collapsed sidebar */
.main.sidebar-collapsed #sidebar{width:0 !important;overflow:hidden;border:none;opacity:0;pointer-events:none}
.main.sidebar-collapsed #sidebar *{pointer-events:none}
/* Note list */
.notelist-header{padding:14px 14px 10px;border-bottom:1px solid var(--border);background:var(--bg-notelist);flex-shrink:0}
.notelist-title-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.notelist-title{font-size:15px;font-weight:700;letter-spacing:-0.02em}
.notelist-count{font-size:12px;color:var(--text-muted);background:var(--bg-editor);border:1px solid var(--border);padding:2px 8px;border-radius:10px}
.notelist-controls{display:flex;gap:8px;align-items:center}
.btn-new-note{flex:1;height:36px;background:var(--accent);color:white;border:none;border-radius:8px;font-weight:600;font-size:13px;display:flex;align-items:center;justify-content:center;gap:6px;transition:background .15s}
.btn-new-note:hover{background:var(--accent-hover)}
.sort-select{height:36px;padding:0 8px;border:1px solid var(--border);border-radius:8px;background:var(--bg-editor);font-size:12px;color:var(--text-secondary);outline:none;cursor:pointer}
.note-list{flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:6px}
.note-card{padding:12px 12px 10px;background:var(--bg-editor);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;position:relative}
.note-card:hover{border-color:var(--border-strong);box-shadow:var(--shadow)}
.note-card.selected{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-light);background:var(--bg-editor)}
.note-card.pinned{border-left:3px solid var(--accent)}
.note-card.dragging{opacity:0.5}
.note-card-top{display:flex;align-items:center;gap:6px;margin-bottom:4px}
.pin-icon{color:var(--accent);font-size:11px}
.note-card-title{font-size:13.5px;font-weight:600;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.note-card-snippet{font-size:12.5px;color:var(--text-secondary);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:1.4em;margin-bottom:6px;word-break:break-word}
.note-card-snippet mark{background:#fde68a;color:inherit;padding:0 2px;border-radius:3px}
[data-theme="dark"] .note-card-snippet mark{background:#854d0e;color:#fef3c7}
.note-card-meta{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-muted)}
.note-card-tags{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}
.mini-tag{font-size:10px;padding:2px 6px;background:var(--bg-hover);border:1px solid var(--border);border-radius:10px;color:var(--text-secondary)}
.note-card-actions{position:absolute;top:8px;right:8px;display:flex;gap:2px;opacity:0;transition:opacity .12s}
.note-card:hover .note-card-actions{opacity:1}
.note-card-actions button{width:24px;height:24px;border:none;background:var(--bg-hover);border-radius:6px;display:grid;place-items:center;font-size:11px;color:var(--text-muted)}
.note-card-actions button:hover{background:var(--bg-active);color:var(--text)}
.empty-list{padding:40px 20px;text-align:center;color:var(--text-muted)}
.empty-list p{font-size:13px;margin-top:8px}
/* Editor */
.editor-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--text-muted);text-align:center}
.editor-empty-icon{width:64px;height:64px;border:1px dashed var(--border-strong);border-radius:16px;display:grid;place-items:center;font-size:24px;margin-bottom:16px;background:var(--bg-notelist)}
.editor-empty h3{font-size:16px;font-weight:600;color:var(--text);margin-bottom:6px}
.editor-empty p{font-size:13px;max-width:280px;line-height:1.5}
.editor-empty button{margin-top:16px;padding:8px 16px;background:var(--accent);color:white;border:none;border-radius:8px;font-weight:600;font-size:13px}
.editor-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}
.editor-toolbar{height:44px;display:flex;align-items:center;gap:2px;padding:0 16px;border-bottom:1px solid var(--border);background:var(--bg-editor);flex-shrink:0;overflow-x:auto;scrollbar-width:none}
.editor-toolbar::-webkit-scrollbar{display:none}
.tb-btn{width:30px;height:30px;border:none;background:transparent;border-radius:6px;display:grid;place-items:center;font-size:13px;color:var(--text-secondary);flex-shrink:0;transition:all .12s}
.tb-btn:hover{background:var(--bg-hover);color:var(--text)}
.tb-btn.active{background:var(--accent-bg);color:var(--accent);font-weight:700}
.tb-sep{width:1px;height:20px;background:var(--border);margin:0 4px;flex-shrink:0}
.editor-meta{padding:14px 32px 0;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.editor-title{flex:1;min-width:200px;font-size:22px;font-weight:700;border:none;outline:none;background:transparent;color:var(--text);letter-spacing:-0.02em;line-height:1.2;padding:4px 0}
.editor-title::placeholder{color:var(--text-muted)}
.editor-actions{display:flex;gap:6px;align-items:center}
.editor-actions button{height:28px;padding:0 10px;border:1px solid var(--border);background:var(--bg-editor);border-radius:6px;font-size:12px;color:var(--text-secondary);display:flex;align-items:center;gap:4px}
.editor-actions button:hover{border-color:var(--border-strong);color:var(--text)}
.editor-actions .btn-danger{color:#b45309;border-color:#fde68a;background:#fffbeb}
[data-theme="dark"] .editor-actions .btn-danger{background:#2a1f0f;color:#fbbf24;border-color:#78350f}
.saved-ind{font-size:11px;color:var(--text-muted);display:flex;align-items:center;gap:4px;transition:opacity .3s}
.saved-ind.dot::before{content:'';width:6px;height:6px;background:#22c55e;border-radius:50%;display:inline-block}
.tag-bar{display:flex;align-items:center;gap:6px;padding:8px 32px;flex-wrap:wrap}
.tag-pill{font-size:11px;padding:4px 8px;background:var(--accent-light);border:1px solid var(--border);border-radius:20px;display:flex;align-items:center;gap:4px;color:var(--text-secondary)}
.tag-pill button{border:none;background:transparent;color:var(--text-muted);cursor:pointer;font-size:12px;line-height:1}
.tag-input{border:none;outline:none;background:transparent;font-size:12px;min-width:80px;color:var(--text)}
.tag-input::placeholder{color:var(--text-muted)}
.editor-content{flex:1;overflow-y:auto;padding:8px 32px 40px}
#editorEl{outline:none;min-height:200px;font-family:var(--font-serif);font-size:15.5px;line-height:1.7;color:var(--text);max-width:720px}
#editorEl:empty:before{content:attr(data-placeholder);color:var(--text-muted);pointer-events:none}
#editorEl h1{font-family:var(--font-sans);font-size:28px;font-weight:800;letter-spacing:-0.03em;margin:20px 0 8px;line-height:1.2}
#editorEl h2{font-family:var(--font-sans);font-size:22px;font-weight:700;letter-spacing:-0.02em;margin:18px 0 6px;line-height:1.3}
#editorEl h3{font-family:var(--font-sans);font-size:17px;font-weight:700;margin:16px 0 6px;line-height:1.3;color:var(--text-secondary)}
#editorEl p{margin:8px 0}
#editorEl blockquote{border-left:3px solid var(--accent);padding:6px 16px;margin:12px 0;background:var(--accent-light);border-radius:0 8px 8px 0;color:var(--text-secondary);font-style:italic}
#editorEl ul, #editorEl ol{margin:8px 0 8px 24px}
#editorEl li{margin:4px 0}
#editorEl code{background:var(--bg-hover);border:1px solid var(--border);padding:1px 5px;border-radius:4px;font-family:var(--font-mono);font-size:13px}
#editorEl pre{background:var(--bg-hover);border:1px solid var(--border);padding:12px 16px;border-radius:8px;overflow-x:auto;margin:12px 0}
#editorEl pre code{background:none;border:none;padding:0}
#editorEl hr{border:none;border-top:1px solid var(--border);margin:16px 0}
#editorEl .checklist{list-style:none;margin-left:0;padding:0}
#editorEl .checklist li{display:flex;align-items:flex-start;gap:8px;padding:3px 0}
#editorEl .checklist li input[type="checkbox"]{margin-top:6px;width:16px;height:16px;accent-color:var(--accent);flex-shrink:0;cursor:pointer}
#editorEl .checklist li.checked span{ text-decoration:line-through;color:var(--text-muted)}
/* Dropdown / modal */
.dropdown{position:absolute;top:100%;right:0;margin-top:6px;background:var(--bg-editor);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-lg);padding:6px;min-width:180px;z-index:50;display:none}
.dropdown.open{display:block}
.dropdown button{width:100%;text-align:left;padding:8px 10px;border:none;background:transparent;border-radius:6px;font-size:13px;color:var(--text);display:flex;align-items:center;gap:8px}
.dropdown button:hover{background:var(--bg-hover)}
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.35);display:none;align-items:center;justify-content:center;z-index:100}
.modal-overlay.open{display:flex}
.modal{background:var(--bg-editor);border-radius:12px;box-shadow:var(--shadow-lg);padding:20px;min-width:360px;max-width:90vw}
.modal h3{font-size:15px;margin-bottom:12px}
.modal input{width:100%;height:36px;padding:0 12px;border:1px solid var(--border);border-radius:8px;outline:none;font-size:13px;background:var(--bg);margin-bottom:12px}
.modal input:focus{border-color:var(--accent)}
.modal-actions{display:flex;gap:8px;justify-content:flex-end}
.btn-primary{padding:7px 14px;background:var(--accent);color:white;border:none;border-radius:8px;font-weight:600;font-size:13px}
.btn-ghost{padding:7px 14px;background:transparent;border:1px solid var(--border);border-radius:8px;font-size:13px;color:var(--text-secondary)}
/* Context menu */
.ctx-menu{position:fixed;background:var(--bg-editor);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-lg);padding:6px;min-width:180px;z-index:90;display:none}
.ctx-menu.open{display:block}
.ctx-menu button{width:100%;text-align:left;padding:7px 10px;border:none;background:transparent;border-radius:6px;font-size:12.5px;color:var(--text);display:flex;align-items:center;gap:8px}
.ctx-menu button:hover{background:var(--bg-hover)}
.ctx-menu .sep{height:1px;background:var(--border);margin:4px 0}
/* Responsive */
@media(max-width:900px){
#sidebar{position:absolute;z-index:20;height:100%;box-shadow:var(--shadow-lg)}
.main.sidebar-collapsed #sidebar{transform:translateX(-100%)}
#notelist{width:280px}
.search-wrap input{width:140px}
}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:10px}
::-webkit-scrollbar-track{background:transparent}
</style>
</head>
<body>
<header class="app-header">
<div class="app-logo"><div class="app-logo-mark">✦</div> Inkwell</div>
<button class="icon-btn" id="toggleSidebar" title="Toggle sidebar">☰</button>
<div class="header-actions">
<div class="search-wrap">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input id="searchInput" placeholder="Search notes..." autocomplete="off">
<button class="search-clear" id="searchClear" style="display:none">×</button>
</div>
<button class="theme-toggle" id="themeToggle" title="Toggle theme">◐</button>
</div>
</header>
<div class="main" id="mainLayout">
<div class="pane" id="sidebar">
<div class="sidebar-top">
<span class="sidebar-title">Notebooks</span>
<button class="btn-new-nb" id="btnNewNotebook">+ New</button>
</div>
<div class="sidebar-nav" id="sidebarNav"></div>
<div class="sidebar-bottom">
<button class="collapse-btn" id="btnCollapse">‹ Collapse</button>
</div>
<div class="storage-info" id="storageInfo"></div>
</div>
<div class="drag-handle" id="handle1"></div>
<div class="pane" id="notelist">
<div class="notelist-header">
<div class="notelist-title-row">
<span class="notelist-title" id="notelistTitle">All Notes</span>
<span class="notelist-count" id="notelistCount">0</span>
</div>
<div class="notelist-controls">
<button class="btn-new-note" id="btnNewNote">+ New note</button>
<select class="sort-select" id="sortSelect">
<option value="updated">Modified</option>
<option value="created">Created</option>
<option value="alpha">A — Z</option>
</select>
</div>
</div>
<div class="note-list" id="noteList"></div>
</div>
<div class="drag-handle" id="handle2"></div>
<div class="pane" id="editor">
<div class="editor-empty" id="editorEmpty">
<div class="editor-empty-icon">📝</div>
<h3>Select or create a note</h3>
<p>Choose a note from the list or create a new one to start writing. Your notes auto-save as you type.</p>
<button id="btnEmptyNew">Create new note</button>
</div>
<div class="editor-wrap" id="editorWrap" style="display:none">
<div class="editor-toolbar" id="toolbar">
<button class="tb-btn" data-cmd="bold" title="Bold (Ctrl+B)"><b>B</b></button>
<button class="tb-btn" data-cmd="italic" title="Italic (Ctrl+I)"><i>I</i></button>
<button class="tb-btn" data-cmd="underline" title="Underline (Ctrl+U)"><u>U</u></button>
<button class="tb-btn" data-cmd="strikeThrough" title="Strikethrough"><s>S</s></button>
<button class="tb-btn" data-cmd="code" title="Inline code">{ }</button>
<div class="tb-sep"></div>
<button class="tb-btn" data-cmd="h1" title="Heading 1">H1</button>
<button class="tb-btn" data-cmd="h2" title="Heading 2">H2</button>
<button class="tb-btn" data-cmd="h3" title="Heading 3">H3</button>
<div class="tb-sep"></div>
<button class="tb-btn" data-cmd="insertUnorderedList" title="Bullet list">•</button>
<button class="tb-btn" data-cmd="insertOrderedList" title="Numbered list">1.</button>
<button class="tb-btn" data-cmd="checklist" title="Checklist">☑</button>
<button class="tb-btn" data-cmd="blockquote" title="Quote">❝</button>
<button class="tb-btn" data-cmd="hr" title="Horizontal rule">—</button>
</div>
<div class="editor-meta">
<input class="editor-title" id="editorTitle" placeholder="Untitled">
<div class="editor-actions">
<span class="saved-ind dot" id="savedInd">Saved</span>
<div style="position:relative">
<button id="btnMoveNote">Move ▾</button>
<div class="dropdown" id="moveDropdown"></div>
</div>
<button id="btnPinNote" title="Pin">📌</button>
<button id="btnDupNote" title="Duplicate">⧉</button>
<button id="btnDelNote" class="btn-danger" title="Delete">Delete</button>
</div>
</div>
<div class="tag-bar" id="tagBar">
<span style="font-size:11px;color:var(--text-muted)">Tags:</span>
<div id="tagPills" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center"></div>
<input class="tag-input" id="tagInput" placeholder="Add tag + Enter">
</div>
<div class="editor-content"><div id="editorEl" contenteditable="true" data-placeholder="Start writing… Type # for heading, - for list, [] for checklist, > for quote"></div></div>
</div>
</div>
</div>
<!-- Notebook modal -->
<div class="modal-overlay" id="nbModal">
<div class="modal">
<h3 id="nbModalTitle">New Notebook</h3>
<input id="nbModalInput" placeholder="Notebook name">
<div class="modal-actions">
<button class="btn-ghost" id="nbModalCancel">Cancel</button>
<button class="btn-primary" id="nbModalOk">Create</button>
</div>
</div>
</div>
<div class="ctx-menu" id="ctxMenu"></div>
<script>
/* =========================================================
INKWELL — Single-file Notes App
========================================================= */
/* ---------- Tunable constants ---------- */
const CONFIG = {
STORAGE_KEY: 'inkwell_v2',
AUTOSAVE_DEBOUNCE: 350,
SNIPPET_LENGTH: 120,
DEFAULT_NOTEBOOK_COLORS: ['#c07a3a','#5b8fb9','#6a9a6a','#9a6ab5','#d45d5d','#4a9a9a','#b58a3a'],
FONTS: { sans: 'Inter, system-ui, sans-serif', serif: 'Georgia, serif', mono: 'SF Mono, monospace' }
};
/* ---------- Helpers ---------- */
const $ = s => document.querySelector(s);
const uid = () => Date.now().toString(36)+Math.random().toString(36).slice(2,6);
const now = () => Date.now();
const fmtDate = ts => {
const d=new Date(ts); const diff=Date.now()-ts;
if(diff<60000) return 'Just now';
if(diff<3600000) return Math.floor(diff/60000)+'m ago';
if(diff<86400000) return Math.floor(diff/3600000)+'h ago';
if(diff<604800000) return Math.floor(diff/86400000)+'d ago';
return d.toLocaleDateString(undefined,{month:'short',day:'numeric',year: d.getFullYear()!==new Date().getFullYear()?'numeric':undefined});
};
function escapeHtml(s){ const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
/* ---------- Text extraction & sanitization ---------- */
function extractText(html){
const tmp=document.createElement('div');
tmp.innerHTML=html||'';
// remove script/style
tmp.querySelectorAll('script,style').forEach(e=>e.remove());
return (tmp.textContent||tmp.innerText||'').replace(/\s+/g,' ').trim();
}
function snippetFromHtml(html){
const t=extractText(html);
return t.length>CONFIG.SNIPPET_LENGTH ? t.slice(0,CONFIG.SNIPPET_LENGTH)+'…' : t;
}
function sanitizeHtml(html){
const tmp=document.createElement('div');
tmp.innerHTML=html;
// strip disallowed
tmp.querySelectorAll('script,style,iframe,object,embed,link,meta,form').forEach(e=>e.remove());
// remove event handlers & javascript: urls
const walker=document.createTreeWalker(tmp, NodeFilter.SHOW_ELEMENT);
const els=[];
while(walker.nextNode()) els.push(walker.currentNode);
const ALLOWED_TAGS=new Set(['P','BR','B','STRONG','I','EM','U','S','STRIKE','DEL','CODE','PRE','H1','H2','H3','UL','OL','LI','BLOCKQUOTE','HR','DIV','SPAN','A','INPUT','LABEL']);
els.forEach(el=>{
// strip attributes
[...el.attributes].forEach(attr=>{
const n=attr.name.toLowerCase();
if(n.startsWith('on')) el.removeAttribute(attr.name);
else if(n==='style') el.removeAttribute(attr.name);
else if(n==='href' && attr.value.trim().toLowerCase().startsWith('javascript:')) el.removeAttribute(attr.name);
else if(!['href','type','checked','class','data-checked','data-checklist'].includes(n)) {
// keep class only for checklist
if(n==='class' && el.classList.contains('checklist')) return;
if(n!=='class') el.removeAttribute(attr.name);
}
});
if(!ALLOWED_TAGS.has(el.tagName)){
// unwrap
const parent=el.parentNode;
while(el.firstChild) parent.insertBefore(el.firstChild, el);
parent.removeChild(el);
}
});
// ensure checklist inputs are checkbox
tmp.querySelectorAll('input').forEach(inp=>{
if(inp.type!=='checkbox'){ inp.type='checkbox'; }
});
return tmp.innerHTML;
}
/* ---------- Data Model ---------- */
let store = {
notebooks: [],
notes: [],
ui: {
selectedNotebookId: 'all',
selectedNoteId: null,
sortOrder: 'updated',
searchQuery: '',
tagFilter: null,
sidebarCollapsed: false,
paneWidths: { sidebar: 260, notelist: 340 },
theme: 'light'
}
};
function defaultData(){
const nb1={id:uid(), name:'Personal', color:CONFIG.DEFAULT_NOTEBOOK_COLORS[0]};
const nb2={id:uid(), name:'Work', color:CONFIG.DEFAULT_NOTEBOOK_COLORS[1]};
const nb3={id:uid(), name:'Ideas', color:CONFIG.DEFAULT_NOTEBOOK_COLORS[2]};
const n1={id:uid(), notebookId:nb1.id, title:'Welcome to Inkwell', content:'<h1>Welcome to Inkwell ✦</h1><p>A calm, focused place for your thoughts. This editor supports <b>bold</b>, <i>italic</i>, <u>underline</u>, <code>inline code</code>, and more.</p><blockquote>“Good notes are a garden, not a filing cabinet.”</blockquote><p>Try the shortcuts:</p><ul><li>Type <code># </code> at line start for a heading</li><li>Type <code>- </code> for a bullet list</li><li>Type <code>[] </code> for a checklist</li><li>Type <code>> </code> for a quote</li></ul><ul class="checklist"><li><input type="checkbox"> <span>Explore the app</span></li><li><input type="checkbox" checked> <span>Create your first note</span></li></ul><p>Everything saves automatically.</p>', tags:['welcome','getting-started'], pinned:true, createdAt:now()-86400000, updatedAt:now()-3600000, deleted:false};
const n2={id:uid(), notebookId:nb2.id, title:'Meeting notes — Q3 planning', content:'<h2>Q3 Planning — 12 Aug</h2><p><b>Attendees:</b> Alex, Sam, Priya</p><ul><li>Ship onboarding revamp by Sep 15</li><li>Research <i>user retention</i> patterns</li><li>Budget review next Friday</li></ul><hr><p>Follow-ups: send deck to team.</p>', tags:['work','planning'], pinned:false, createdAt:now()-172800000, updatedAt:now()-7200000, deleted:false};
const n3={id:uid(), notebookId:nb3.id, title:'Book ideas', content:'<h3>Novel seeds</h3><ol><li>The library that remembers every book never written</li><li>A translator who discovers languages change the past</li></ol><p>Tags help you find things later — try clicking a tag below.</p>', tags:['writing','ideas'], pinned:false, createdAt:now()-259200000, updatedAt:now()-10000000, deleted:false};
return { notebooks:[nb1,nb2,nb3], notes:[n1,n2,n3], ui:{ selectedNotebookId:'all', selectedNoteId:n1.id, sortOrder:'updated', searchQuery:'', tagFilter:null, sidebarCollapsed:false, paneWidths:{sidebar:260, notelist:340}, theme:'light'} };
}
function loadStore(){
try{
const raw=localStorage.getItem(CONFIG.STORAGE_KEY);
if(raw){
const parsed=JSON.parse(raw);
// migrate: ensure fields
store.notebooks=parsed.notebooks||[];
store.notes=parsed.notes||[];
store.ui={...defaultData().ui, ...(parsed.ui||{})};
// ensure deleted flag
store.notes.forEach(n=>{ if(n.deleted===undefined) n.deleted=false; if(!n.tags) n.tags=[]; });
if(store.notebooks.length===0 && store.notes.length===0) store=defaultData();
} else {
store=defaultData();
saveStore();
}
}catch(e){ store=defaultData(); }
applyTheme();
// restore pane widths
if(store.ui.paneWidths){
$('#sidebar').style.width=store.ui.paneWidths.sidebar+'px';
$('#notelist').style.width=store.ui.paneWidths.notelist+'px';
}
if(store.ui.sidebarCollapsed) $('#mainLayout').classList.add('sidebar-collapsed');
}
function saveStore(){
localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(store));
}
let saveTimer=null;
function debouncedSave(){
$('#savedInd').textContent='Saving…'; $('#savedInd').style.opacity='1';
clearTimeout(saveTimer);
saveTimer=setTimeout(()=>{ saveStore(); $('#savedInd').textContent='Saved'; setTimeout(()=>$('#savedInd').style.opacity='0.7',800); }, CONFIG.AUTOSAVE_DEBOUNCE);
}
function applyTheme(){
document.documentElement.setAttribute('data-theme', store.ui.theme);
$('#themeToggle').textContent = store.ui.theme==='dark' ? '☀' : '◐';
}
/* ---------- Rendering ---------- */
function renderAll(){
renderSidebar();
renderNoteList();
renderEditor();
updateStorageInfo();
}
function renderSidebar(){
const nav=$('#sidebarNav');
const allCount=store.notes.filter(n=>!n.deleted).length;
const trashCount=store.notes.filter(n=>n.deleted).length;
let html='';
html+=`<div class="nav-item ${store.ui.selectedNotebookId==='all'?'active':''}" data-nb="all"><span class="nav-icon">◧</span> All Notes <span class="nav-count">${allCount}</span></div>`;
store.notebooks.forEach(nb=>{
const cnt=store.notes.filter(n=>!n.deleted && n.notebookId===nb.id).length;
const active=store.ui.selectedNotebookId===nb.id?'active':'';
html+=`<div class="notebook-row ${active}" data-nb="${nb.id}" draggable="true">
<span class="nb-dot" style="background:${nb.color}"></span>
<span style="flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(nb.name)}</span>
<span class="nav-count">${cnt}</span>
<span class="nb-actions">
<button data-act="rename" data-id="${nb.id}" title="Rename">✎</button>
<button data-act="delete" data-id="${nb.id}" title="Delete">×</button>
</span>
</div>`;
});
html+=`<div class="nav-item ${store.ui.selectedNotebookId==='trash'?'active':''}" data-nb="trash" style="margin-top:8px"><span class="nav-icon">🗑</span> Trash <span class="nav-count">${trashCount}</span></div>`;
if(store.ui.tagFilter){
html+=`<div style="margin-top:12px;padding:8px 10px;background:var(--accent-light);border:1px solid var(--border);border-radius:8px;display:flex;align-items:center;gap:8px;font-size:12px"><span>Tag: <b>${escapeHtml(store.ui.tagFilter)}</b></span><button id="clearTagFilter" style="margin-left:auto;border:none;background:var(--bg-editor);border-radius:50%;width:20px;height:20px;display:grid;place-items:center">×</button></div>`;
}
nav.innerHTML=html;
// events
nav.querySelectorAll('[data-nb]').forEach(el=>{
el.addEventListener('click', e=>{
if(e.target.closest('[data-act]')) return;
store.ui.selectedNotebookId=el.dataset.nb;
store.ui.selectedNoteId=null;
// auto select first note in that view
const list=getFilteredNotes();
if(list.length) store.ui.selectedNoteId=list[0].id;
saveStore(); renderAll();
});
// drag over for moving notes
el.addEventListener('dragover', e=>{ e.preventDefault(); el.style.background='var(--accent-light)'; });
el.addEventListener('dragleave', ()=> el.style.background='');
el.addEventListener('drop', e=>{
e.preventDefault(); el.style.background='';
const nid=e.dataTransfer.getData('text/plain');
const note=store.notes.find(n=>n.id===nid);
if(note && el.dataset.nb!=='all' && el.dataset.nb!=='trash'){
note.notebookId=el.dataset.nb; note.updatedAt=now(); saveStore(); renderAll();
}
});
});
nav.querySelectorAll('[data-act="rename"]').forEach(b=> b.addEventListener('click', e=>{ e.stopPropagation(); openNbModal('rename', b.dataset.id); }));
nav.querySelectorAll('[data-act="delete"]').forEach(b=> b.addEventListener('click', e=>{ e.stopPropagation(); deleteNotebook(b.dataset.id); }));
const clearBtn=$('#clearTagFilter');
if(clearBtn) clearBtn.addEventListener('click', ()=>{ store.ui.tagFilter=null; saveStore(); renderAll(); });
}
function getFilteredNotes(){
let notes=store.notes.filter(n=>{
if(store.ui.selectedNotebookId==='trash') return n.deleted;
if(n.deleted) return false;
if(store.ui.selectedNotebookId!=='all' && n.notebookId!==store.ui.selectedNotebookId) return false;
if(store.ui.tagFilter && !n.tags.includes(store.ui.tagFilter)) return false;
if(store.ui.searchQuery){
const q=store.ui.searchQuery.toLowerCase();
const hay=(n.title+' '+extractText(n.content)).toLowerCase();
if(!hay.includes(q)) return false;
}
return true;
});
// sort: pinned first, then by sortOrder
const order=store.ui.sortOrder;
notes.sort((a,b)=>{
if(a.pinned!==b.pinned) return b.pinned - a.pinned;
if(order==='alpha') return a.title.localeCompare(b.title);
if(order==='created') return b.createdAt - a.createdAt;
return b.updatedAt - a.updatedAt;
});
return notes;
}
function highlightText(text, query){
if(!query) return escapeHtml(text);
const idx=text.toLowerCase().indexOf(query.toLowerCase());
if(idx===-1) return escapeHtml(text);
return escapeHtml(text.slice(0,idx))+'<mark>'+escapeHtml(text.slice(idx, idx+query.length))+'</mark>'+escapeHtml(text.slice(idx+query.length));
}
function renderNoteList(){
const list=getFilteredNotes();
const titleMap={all:'All Notes', trash:'Trash'};
const nb=store.notebooks.find(x=>x.id===store.ui.selectedNotebookId);
$('#notelistTitle').textContent = titleMap[store.ui.selectedNotebookId] || (nb? nb.name : 'Notes');
$('#notelistCount').textContent=list.length;
$('#sortSelect').value=store.ui.sortOrder;
const container=$('#noteList');
if(list.length===0){
const msg=store.ui.searchQuery? 'No matches for "'+escapeHtml(store.ui.searchQuery)+'"' : (store.ui.selectedNotebookId==='trash'?'Trash is empty':'No notes yet');
container.innerHTML=`<div class="empty-list"><div style="font-size:24px">📄</div><p>${msg}</p>${store.ui.selectedNotebookId!=='trash'?'<button class="btn-primary" style="margin-top:12px" id="emptyNewBtn">New note</button>':''}</div>`;
const b=$('#emptyNewBtn'); if(b) b.addEventListener('click', ()=> createNote());
return;
}
container.innerHTML=list.map(n=>{
const snippet=snippetFromHtml(n.content);
const isSelected=n.id===store.ui.selectedNoteId;
const q=store.ui.searchQuery;
let titleHtml=escapeHtml(n.title||'Untitled');
let snippetHtml=escapeHtml(snippet);
if(q){
// highlight in title/snippet using plain text
const t=n.title||'Untitled';
const low=q.toLowerCase();
if(t.toLowerCase().includes(low)) titleHtml=highlightText(t,q);
if(snippet.toLowerCase().includes(low)) snippetHtml=highlightText(snippet,q);
}
const tagsHtml=n.tags.slice(0,3).map(t=>`<span class="mini-tag">#${escapeHtml(t)}</span>`).join('');
return `<div class="note-card ${isSelected?'selected':''} ${n.pinned?'pinned':''}" data-id="${n.id}" draggable="true">
<div class="note-card-actions">
<button data-card-act="pin" title="${n.pinned?'Unpin':'Pin'}">${n.pinned?'📌':'📍'}</button>
<button data-card-act="dup" title="Duplicate">⧉</button>
<button data-card-act="del" title="${store.ui.selectedNotebookId==='trash'?'Delete forever':'Delete'}">🗑</button>
</div>
<div class="note-card-top">${n.pinned?'<span class="pin-icon">📌</span>':''}<div class="note-card-title">${titleHtml}</div></div>
<div class="note-card-snippet">${snippetHtml||'<span style="color:var(--text-muted);font-style:italic">No content</span>'}</div>
<div class="note-card-meta"><span>${fmtDate(n.updatedAt)}</span> ${n.tags.length? '· <span>'+n.tags.length+' tags</span>':''}</div>
${tagsHtml?'<div class="note-card-tags">'+tagsHtml+'</div>':''}
</div>`;
}).join('');
container.querySelectorAll('.note-card').forEach(card=>{
card.addEventListener('click', e=>{
if(e.target.closest('[data-card-act]')) return;
store.ui.selectedNoteId=card.dataset.id;
saveStore(); renderAll();
});
card.addEventListener('dragstart', e=>{ e.dataTransfer.setData('text/plain', card.dataset.id); card.classList.add('dragging'); });
card.addEventListener('dragend', ()=> card.classList.remove('dragging'));
card.querySelectorAll('[data-card-act]').forEach(btn=>{
btn.addEventListener('click', e=>{
e.stopPropagation();
const id=card.dataset.id;
if(btn.dataset.cardAct==='pin') togglePin(id);
if(btn.dataset.cardAct==='dup') duplicateNote(id);
if(btn.dataset.cardAct==='del') deleteNote(id);
});
});
// right click
card.addEventListener('contextmenu', e=>{
e.preventDefault();
showCtxMenu(e.clientX, e.clientY, card.dataset.id);
});
});
}
let suppressEditorInput=false;
let historyStack=[]; let historyIndex=-1;
function pushHistory(html){
// trim future
historyStack=historyStack.slice(0, historyIndex+1);
historyStack.push(html);
if(historyStack.length>80) historyStack.shift();
else historyIndex++;
if(historyStack.length>80) historyIndex=historyStack.length-1;
}
function renderEditor(){
const note=store.notes.find(n=>n.id===store.ui.selectedNoteId);
const empty=$('#editorEmpty');
const wrap=$('#editorWrap');
if(!note || (store.ui.selectedNotebookId==='trash' && !note.deleted) ){
empty.style.display='flex'; wrap.style.display='none';
return;
}
// if trash view but note not deleted, shouldn't happen; but handle
empty.style.display='none'; wrap.style.display='flex';
suppressEditorInput=true;
$('#editorTitle').value=note.title;
$('#editorTitle').disabled=note.deleted;
$('#editorEl').contentEditable = note.deleted ? 'false' : 'true';
$('#editorEl').innerHTML=note.content || '';
if(note.deleted) $('#editorEl').style.opacity='0.6'; else $('#editorEl').style.opacity='1';
// tags
const pills=$('#tagPills');
pills.innerHTML=note.tags.map(t=>`<span class="tag-pill">#${escapeHtml(t)} <button data-tag="${escapeHtml(t)}">×</button></span>`).join('');
pills.querySelectorAll('button').forEach(b=> b.addEventListener('click', ()=>{
note.tags=note.tags.filter(x=>x!==b.dataset.tag);
note.updatedAt=now(); saveStore(); renderEditor(); renderNoteList();
}));
$('#tagInput').style.display= note.deleted? 'none':'block';
// pin button state
$('#btnPinNote').textContent=note.pinned?'Unpin 📌':'Pin 📍';
$('#btnPinNote').style.background=note.pinned?'var(--accent-bg)':'';
// move dropdown
const dd=$('#moveDropdown');
dd.innerHTML=store.notebooks.map(nb=>`<button data-move="${nb.id}"><span class="nb-dot" style="background:${nb.color}"></span> ${escapeHtml(nb.name)} ${nb.id===note.notebookId?'✓':''}</button>`).join('');
dd.querySelectorAll('[data-move]').forEach(b=> b.addEventListener('click', ()=>{
note.notebookId=b.dataset.move; note.updatedAt=now(); dd.classList.remove('open'); saveStore(); renderAll();
}));
// delete button label
$('#btnDelNote').textContent= note.deleted ? 'Delete forever' : 'Delete';
$('#btnDupNote').style.display= note.deleted? 'none':'';
$('#btnMoveNote').style.display= note.deleted? 'none':'';
$('#btnPinNote').style.display= note.deleted? 'none':'';
// show restore button if deleted
let restoreBtn=$('#btnRestore');
if(note.deleted){
if(!restoreBtn){
const btn=document.createElement('button');
btn.id='btnRestore'; btn.textContent='Restore'; btn.style.background='var(--accent)'; btn.style.color='white'; btn.style.border='none';
btn.addEventListener('click', ()=> restoreNote(note.id));
$('.editor-actions').prepend(btn);
}
} else {
if(restoreBtn) restoreBtn.remove();
}
// history init
historyStack=[note.content||'']; historyIndex=0;
suppressEditorInput=false;
updateToolbarState();
// focus if new note empty
if(!note.content || note.content==='<p><br></p>'){
setTimeout(()=> $('#editorEl').focus(), 50);
}
}
function updateStorageInfo(){
try{
const s=JSON.stringify(store).length;
$('#storageInfo').textContent=(s/1024).toFixed(1)+' KB stored · '+store.notes.filter(n=>!n.deleted).length+' notes';
}catch(e){}
}
/* ---------- Actions ---------- */
function createNote(){
const nbId= store.ui.selectedNotebookId==='all' || store.ui.selectedNotebookId==='trash' ? (store.notebooks[0]?.id || null) : store.ui.selectedNotebookId;
if(!nbId){ alert('Create a notebook first'); return; }
const n={id:uid(), notebookId:nbId, title:'Untitled', content:'<p><br></p>', tags:[], pinned:false, createdAt:now(), updatedAt:now(), deleted:false, originalNotebookId:null};
store.notes.unshift(n);
store.ui.selectedNotebookId = nbId==='trash' ? 'all' : store.ui.selectedNotebookId;
// if we were in trash, switch to all
if(store.ui.selectedNotebookId==='trash') store.ui.selectedNotebookId='all';
store.ui.selectedNoteId=n.id;
saveStore(); renderAll();
setTimeout(()=>{ $('#editorTitle').focus(); $('#editorTitle').select(); }, 80);
}
function duplicateNote(id){
const src=store.notes.find(n=>n.id===id); if(!src) return;
const copy={...src, id:uid(), title:src.title+' (copy)', createdAt:now(), updatedAt:now(), deleted:false};
copy.tags=[...src.tags];
store.notes.unshift(copy);
store.ui.selectedNoteId=copy.id;
saveStore(); renderAll();
}
function deleteNote(id){
const n=store.notes.find(x=>x.id===id); if(!n) return;
if(n.deleted){
if(!confirm('Permanently delete this note?')) return;
store.notes=store.notes.filter(x=>x.id!==id);
if(store.ui.selectedNoteId===id) store.ui.selectedNoteId=null;
} else {
n.deleted=true; n.originalNotebookId=n.notebookId; n.updatedAt=now();
if(store.ui.selectedNoteId===id) store.ui.selectedNoteId=null;
}
saveStore(); renderAll();
}
function restoreNote(id){
const n=store.notes.find(x=>x.id===id); if(!n) return;
n.deleted=false;
if(n.originalNotebookId && store.notebooks.some(b=>b.id===n.originalNotebookId)) n.notebookId=n.originalNotebookId;
n.updatedAt=now();
saveStore(); renderAll();
}
function togglePin(id){
const n=store.notes.find(x=>x.id===id); if(!n) return;
n.pinned=!n.pinned; n.updatedAt=now(); saveStore(); renderAll();
}
function deleteNotebook(id){
const cnt=store.notes.filter(n=>!n.deleted && n.notebookId===id).length;
if(cnt>0 && !confirm(`Delete notebook and move ${cnt} note(s) to Trash?`)) return;
// move notes to trash or delete notebook
store.notes.forEach(n=>{ if(n.notebookId===id && !n.deleted){ n.deleted=true; n.originalNotebookId=id; }});
store.notebooks=store.notebooks.filter(b=>b.id!==id);
if(store.ui.selectedNotebookId===id) store.ui.selectedNotebookId='all';
saveStore(); renderAll();
}
/* ---------- Notebook modal ---------- */
let nbModalMode='create', nbModalId=null;
function openNbModal(mode, id){
nbModalMode=mode; nbModalId=id;
$('#nbModalTitle').textContent= mode==='rename' ? 'Rename Notebook' : 'New Notebook';
$('#nbModalOk').textContent= mode==='rename' ? 'Save' : 'Create';
$('#nbModalInput').value= mode==='rename' ? (store.notebooks.find(b=>b.id===id)?.name||'') : '';
$('#nbModal').classList.add('open');
setTimeout(()=> $('#nbModalInput').focus(), 50);
}
function closeNbModal(){ $('#nbModal').classList.remove('open'); }
$('#nbModalOk').addEventListener('click', ()=>{
const name=$('#nbModalInput').value.trim();
if(!name) return;
if(nbModalMode==='create'){
const nb={id:uid(), name, color:CONFIG.DEFAULT_NOTEBOOK_COLORS[store.notebooks.length % CONFIG.DEFAULT_NOTEBOOK_COLORS.length]};
store.notebooks.push(nb);
store.ui.selectedNotebookId=nb.id; store.ui.selectedNoteId=null;
} else {
const nb=store.notebooks.find(b=>b.id===nbModalId);
if(nb) nb.name=name;
}
saveStore(); renderAll(); closeNbModal();
});
$('#nbModalCancel').addEventListener('click', closeNbModal);
$('#nbModal').addEventListener('click', e=>{ if(e.target===$('#nbModal')) closeNbModal(); });
$('#nbModalInput').addEventListener('keydown', e=>{ if(e.key==='Enter') $('#nbModalOk').click(); if(e.key==='Escape') closeNbModal(); });
/* ---------- Context menu ---------- */
function showCtxMenu(x,y, noteId){
const n=store.notes.find(v=>v.id===noteId); if(!n) return;
const m=$('#ctxMenu');
m.innerHTML=`
<button data-ctx="pin">${n.pinned?'Unpin':'Pin'}</button>
<button data-ctx="dup">Duplicate</button>
<button data-ctx="move">Move to…</button>
<div class="sep"></div>
<button data-ctx="del" style="color:#b45309">${n.deleted?'Delete forever':'Delete'}</button>
${n.deleted?'<button data-ctx="restore">Restore</button>':''}
`;
m.style.left=x+'px'; m.style.top=y+'px'; m.classList.add('open');
m.querySelectorAll('[data-ctx]').forEach(b=> b.addEventListener('click', ()=>{
m.classList.remove('open');
if(b.dataset.ctx==='pin') togglePin(noteId);
if(b.dataset.ctx==='dup') duplicateNote(noteId);
if(b.dataset.ctx==='del') deleteNote(noteId);
if(b.dataset.ctx==='restore') restoreNote(noteId);
if(b.dataset.ctx==='move'){
// show move dropdown near
store.ui.selectedNoteId=noteId; renderEditor();
$('#moveDropdown').classList.add('open');
}
}));
}
document.addEventListener('click', ()=> $('#ctxMenu').classList.remove('open'));
/* ---------- Editor events ---------- */
const editorEl=$('#editorEl');
const titleEl=$('#editorTitle');
titleEl.addEventListener('input', ()=>{
const n=store.notes.find(x=>x.id===store.ui.selectedNoteId); if(!n) return;
n.title=titleEl.value; n.updatedAt=now();
debouncedSave(); renderNoteList();
});
titleEl.addEventListener('keydown', e=>{
if(e.key==='Enter'){ e.preventDefault(); editorEl.focus(); }
});
editorEl.addEventListener('input', ()=>{
if(suppressEditorInput) return;
const n=store.notes.find(x=>x.id===store.ui.selectedNoteId); if(!n) return;
// markdown shortcuts check
handleMarkdownShortcuts();
// sanitize checklist clicks handled separately
const html=sanitizeHtml(editorEl.innerHTML);
// avoid loop if sanitize changed nothing significant
n.content=editorEl.innerHTML; // keep as is but sanitized on save
n.updatedAt=now();
// history
if(historyStack[historyIndex]!==editorEl.innerHTML) pushHistory(editorEl.innerHTML);
debouncedSave();
// update snippet live
renderNoteList();
updateToolbarState();
});
editorEl.addEventListener('keydown', e=>{
// undo/redo
if((e.ctrlKey||e.metaKey) && e.key.toLowerCase()==='z'){
e.preventDefault();
if(e.shiftKey) redo(); else undo();
return;
}
if((e.ctrlKey||e.metaKey) && e.key.toLowerCase()==='y'){ e.preventDefault(); redo(); return; }
// bold/italic/underline
if((e.ctrlKey||e.metaKey) && ['b','i','u'].includes(e.key.toLowerCase())){
e.preventDefault();
const map={b:'bold',i:'italic',u:'underline'};
document.execCommand(map[e.key.toLowerCase()], false, null);
updateToolbarState();
triggerSaveFromEditor();
return;
}
// Enter handling for lists
if(e.key==='Enter'){
const sel=window.getSelection();
if(!sel.rangeCount) return;
const range=sel.getRangeAt(0);
let li=range.startContainer;
while(li && li!==editorEl && li.tagName!=='LI') li=li.parentNode;
if(li && li.tagName==='LI'){
const text=li.textContent.trim();
const ul=li.parentNode;
const isChecklist=ul.classList.contains('checklist');
if(text===''){
e.preventDefault();
// exit list
const p=document.createElement('p');
p.innerHTML='<br>';
if(isChecklist){
// remove empty li
li.remove();
if(ul.children.length===0) ul.remove();
} else {
// unwrap
const newP=document.createElement('p');
newP.innerHTML='<br>';
ul.parentNode.insertBefore(newP, ul.nextSibling);
li.remove();
if(ul.children.length===0) ul.remove();
placeCaretAt(newP,0);
triggerSaveFromEditor();
return;
}
// place after ul
const after=document.createElement('p');
after.innerHTML='<br>';
const parent=ul.parentNode;
parent.insertBefore(after, ul.nextSibling);
placeCaretAt(after,0);
triggerSaveFromEditor();
return;
} else {
// let browser handle continuation, but for checklist ensure new li has checkbox
if(isChecklist){
// allow default then fix next li
setTimeout(()=>{
const sel2=window.getSelection();
let newLi=sel2.anchorNode;
while(newLi && newLi.tagName!=='LI') newLi=newLi.parentNode;
if(newLi && !newLi.querySelector('input')){
const cb=document.createElement('input'); cb.type='checkbox';
cb.addEventListener('change', onCheckToggle);
newLi.insertBefore(cb, newLi.firstChild);
// ensure span wrapper
if(!newLi.querySelector('span')){
const span=document.createElement('span');
// move text nodes into span
const nodes=[...newLi.childNodes].filter(n=> n!==cb);
nodes.forEach(n=> span.appendChild(n));
newLi.appendChild(span);
}
}
triggerSaveFromEditor();
}, 0);
}
}
}
}
// Escape handling already via browser
});
function triggerSaveFromEditor(){
const n=store.notes.find(x=>x.id===store.ui.selectedNoteId); if(!n) return;
n.content=editorEl.innerHTML; n.updatedAt=now();
pushHistory(editorEl.innerHTML);
debouncedSave(); renderNoteList();
}
function placeCaretAt(el, offset){
const range=document.createRange(); const sel=window.getSelection();
range.setStart(el, offset); range.collapse(true); sel.removeAllRanges(); sel.addRange(range);
}
function undo(){
if(historyIndex<=0) return;
historyIndex--;
const html=historyStack[historyIndex];
editorEl.innerHTML=html;
const n=store.notes.find(x=>x.id===store.ui.selectedNoteId); if(n) n.content=html;
saveStore(); renderNoteList();
}
function redo(){
if(historyIndex>=historyStack.length-1) return;
historyIndex++;
const html=historyStack[historyIndex];
editorEl.innerHTML=html;
const n=store.notes.find(x=>x.id===store.ui.selectedNoteId); if(n) n.content=html;
saveStore(); renderNoteList();
}
/* Markdown shortcuts: only at line start */
function handleMarkdownShortcuts(){
const sel=window.getSelection();
if(!sel.rangeCount) return;
const range=sel.getRangeAt(0);
if(!range.collapsed) return;
// find current block
let node=range.startContainer;
// if text node, get its parent block
let block=node;
while(block && block!==editorEl && !['P','DIV','H1','H2','H3','LI','BLOCKQUOTE'].includes(block.tagName)) block=block.parentNode;
if(!block || block===editorEl) {
// try to find containing p/div
block=node.parentNode;
while(block && block!==editorEl && block.tagName!=='DIV' && block.tagName!=='P') block=block.parentNode;
}
if(!block || block===editorEl) return;
// get text up to cursor
const textBefore = getTextBeforeCursor(block, range);
// Only trigger if at line start: textBefore is exactly the shortcut + space and block starts with it
// Check patterns
const patterns=[
{ regex:/^# $/, action:()=> convertBlock(block,'H1') },
{ regex:/^## $/, action:()=> convertBlock(block,'H2') },
{ regex:/^### $/, action:()=> convertBlock(block,'H3') },
{ regex:/^- $/, action:()=> convertToList(block,'UL',false) },
{ regex:/^\* $/, action:()=> convertToList(block,'UL',false) },
{ regex:/^\[\] $/, action:()=> convertToList(block,'UL',true) },
{ regex:/^\[ \] $/, action:()=> convertToList(block,'UL',true) },
{ regex:/^> $/, action:()=> convertToBlockquote(block) },
{ regex:/^1\. $/, action:()=> convertToList(block,'OL',false) },
];
for(const p of patterns){
if(p.regex.test(textBefore)){
// ensure it's at start of block (block text starts with that)
const blockText=(block.textContent||'');
if(blockText.startsWith(textBefore.trimEnd()) || blockText.trimStart().startsWith(textBefore.trim())){
p.action();
break;
}
}
}
}
function getTextBeforeCursor(block, range){
const r=document.createRange();
r.selectNodeContents(block);
r.setEnd(range.startContainer, range.startOffset);
const frag=r.cloneContents();
const div=document.createElement('div'); div.appendChild(frag);
return div.textContent;
}
function convertBlock(block, tag){
const newEl=document.createElement(tag);
newEl.innerHTML='<br>';
block.parentNode.replaceChild(newEl, block);
placeCaretAt(newEl,0);
triggerSaveFromEditor();
}
function convertToList(block, listTag, isChecklist){
const list=document.createElement(listTag);
if(isChecklist) list.className='checklist';
const li=document.createElement('li');
if(isChecklist){
const cb=document.createElement('input'); cb.type='checkbox';
cb.addEventListener('change', onCheckToggle);
const span=document.createElement('span'); span.innerHTML='<br>';
li.appendChild(cb); li.appendChild(span);
} else {
li.innerHTML='<br>';
}
list.appendChild(li);
block.parentNode.replaceChild(list, block);
// focus li span or li
if(isChecklist) placeCaretAt(li.querySelector('span'),0);
else placeCaretAt(li,0);
triggerSaveFromEditor();
}
function convertToBlockquote(block){
const bq=document.createElement('blockquote');
bq.innerHTML='<br>';
block.parentNode.replaceChild(bq, block);
placeCaretAt(bq,0);
triggerSaveFromEditor();
}
function onCheckToggle(e){
const li=e.target.closest('li');
if(li){
if(e.target.checked) li.classList.add('checked'); else li.classList.remove('checked');
triggerSaveFromEditor();
}
}
// delegate checkbox changes
editorEl.addEventListener('change', e=>{
if(e.target.type==='checkbox') onCheckToggle(e);
});
editorEl.addEventListener('click', e=>{
if(e.target.type==='checkbox'){
// toggle class already handled via change, but ensure
setTimeout(()=> onCheckToggle({target:e.target}),0);
}
});
/* Toolbar */
function updateToolbarState(){
const cmds=['bold','italic','underline','strikeThrough'];
document.querySelectorAll('.tb-btn[data-cmd]').forEach(btn=>{
const cmd=btn.dataset.cmd;
let active=false;
try{
if(['bold','italic','underline','strikeThrough'].includes(cmd)) active=document.queryCommandState(cmd);
else if(cmd==='h1') active=isCursorIn('H1');
else if(cmd==='h2') active=isCursorIn('H2');
else if(cmd==='h3') active=isCursorIn('H3');
}catch(e){}
btn.classList.toggle('active', active);
});
}
function isCursorIn(tag){
const sel=window.getSelection(); if(!sel.rangeCount) return false;
let n=sel.anchorNode;
while(n && n!==editorEl){ if(n.tagName===tag) return true; n=n.parentNode; }
return false;
}
document.addEventListener('selectionchange', ()=>{ if(document.activeElement===editorEl) updateToolbarState(); });
$('#toolbar').addEventListener('click', e=>{
const btn=e.target.closest('.tb-btn'); if(!btn) return;
const cmd=btn.dataset.cmd;
editorEl.focus();
if(cmd==='code'){
// wrap selection in code
const sel=window.getSelection();
if(!sel.rangeCount) return;
const range=sel.getRangeAt(0);
if(range.collapsed){
document.execCommand('insertHTML', false, '<code>code</code>');
} else {
const content=range.extractContents();
const code=document.createElement('code');
code.appendChild(content);
range.insertNode(code);
// select after
range.setStartAfter(code); range.collapse(true);
sel.removeAllRanges(); sel.addRange(range);
}
triggerSaveFromEditor();
} else if(cmd==='h1' || cmd==='h2' || cmd==='h3'){
const tag=cmd.toUpperCase();
// toggle: if already in that heading, convert to p
if(isCursorIn(tag)){
document.execCommand('formatBlock', false, 'p');
} else {
document.execCommand('formatBlock', false, tag);
}
triggerSaveFromEditor();
} else if(cmd==='checklist'){
convertToListAtCursor(true);
} else if(cmd==='blockquote'){
document.execCommand('formatBlock', false, 'blockquote');
triggerSaveFromEditor();
} else if(cmd==='hr'){
document.execCommand('insertHorizontalRule', false, null);
triggerSaveFromEditor();
} else {
document.execCommand(cmd, false, null);
triggerSaveFromEditor();
}
updateToolbarState();
});
function convertToListAtCursor(isChecklist){
const sel=window.getSelection(); if(!sel.rangeCount) return;
let block=sel.anchorNode;
while(block && block!==editorEl && !['P','DIV','H1','H2','H3'].includes(block.tagName)) block=block.parentNode;
if(!block || block===editorEl) {
document.execCommand(isChecklist?'insertUnorderedList': 'insertUnorderedList', false, null);
if(isChecklist){
const ul=editorEl.querySelector('ul:not(.checklist)');
if(ul) ul.classList.add('checklist');
ul?.querySelectorAll('li').forEach(li=>{
if(!li.querySelector('input')){
const cb=document.createElement('input'); cb.type='checkbox';
li.insertBefore(cb, li.firstChild);
}
});
}
} else {
convertToList(block, 'UL', isChecklist);
}
triggerSaveFromEditor();
}
/* Paste sanitization */
editorEl.addEventListener('paste', e=>{
e.preventDefault();
const text=(e.clipboardData||window.clipboardData).getData('text/html') || (e.clipboardData||window.clipboardData).getData('text/plain');
// if html contains tags, sanitize; else plain text
let html=text;
// if plain text without html tags, convert newlines to <br> or paragraphs
const hasHtml = /<[a-z][\s\S]*>/i.test(text) && (e.clipboardData.getData('text/html'));
if(!hasHtml){
const plain=(e.clipboardData||window.clipboardData).getData('text/plain');
html=escapeHtml(plain).replace(/\n/g,'<br>');
}
const clean=sanitizeHtml(html);
document.execCommand('insertHTML', false, clean);
triggerSaveFromEditor();
});
/* Search */
$('#searchInput').addEventListener('input', e=>{
store.ui.searchQuery=e.target.value;
$('#searchClear').style.display= store.ui.searchQuery ? 'grid' : 'none';
saveStore(); renderNoteList();
});
$('#searchClear').addEventListener('click', ()=>{
store.ui.searchQuery=''; $('#searchInput').value=''; $('#searchClear').style.display='none';
saveStore(); renderNoteList();
});
/* Sort */
$('#sortSelect').addEventListener('change', e=>{
store.ui.sortOrder=e.target.value; saveStore(); renderNoteList();
});
/* Tags */
$('#tagInput').addEventListener('keydown', e=>{
if(e.key==='Enter'){
e.preventDefault();
const val=e.target.value.trim().replace(/^#/,'').toLowerCase();
if(!val) return;
const n=store.notes.find(x=>x.id===store.ui.selectedNoteId); if(!n) return;
if(!n.tags.includes(val)){ n.tags.push(val); n.updatedAt=now(); saveStore(); renderEditor(); renderNoteList(); }
e.target.value='';
}
if(e.key==='Backspace' && !e.target.value){
const n=store.notes.find(x=>x.id===store.ui.selectedNoteId); if(n && n.tags.length){ n.tags.pop(); saveStore(); renderEditor(); renderNoteList(); }
}
});
// clicking tag in note list filters
document.addEventListener('click', e=>{
const pill=e.target.closest('.mini-tag');
if(pill){
const tag=pill.textContent.replace('#','').trim();
store.ui.tagFilter=tag; saveStore(); renderAll();
}
const tp=e.target.closest('.tag-pill');
if(tp && !e.target.closest('button')){
const tag=tp.textContent.replace('#','').replace('×','').trim();
store.ui.tagFilter=tag; saveStore(); renderAll();
}
});
/* Move dropdown toggle */
$('#btnMoveNote').addEventListener('click', e=>{
e.stopPropagation();
$('#moveDropdown').classList.toggle('open');
});
document.addEventListener('click', ()=> $('#moveDropdown').classList.remove('open'));
/* Pin / dup / del in editor */
$('#btnPinNote').addEventListener('click', ()=>{ if(store.ui.selectedNoteId) togglePin(store.ui.selectedNoteId); });
$('#btnDupNote').addEventListener('click', ()=>{ if(store.ui.selectedNoteId) duplicateNote(store.ui.selectedNoteId); });
$('#btnDelNote').addEventListener('click', ()=>{ if(store.ui.selectedNoteId) deleteNote(store.ui.selectedNoteId); });
/* New note buttons */
$('#btnNewNote').addEventListener('click', createNote);
$('#btnEmptyNew').addEventListener('click', createNote);
$('#btnNewNotebook').addEventListener('click', ()=> openNbModal('create'));
/* Sidebar collapse */
$('#toggleSidebar').addEventListener('click', ()=>{
$('#mainLayout').classList.toggle('sidebar-collapsed');
store.ui.sidebarCollapsed=$('#mainLayout').classList.contains('sidebar-collapsed');
saveStore();
});
$('#btnCollapse').addEventListener('click', ()=>{
$('#mainLayout').classList.add('sidebar-collapsed');
store.ui.sidebarCollapsed=true; saveStore();
});
/* Theme */
$('#themeToggle').addEventListener('click', ()=>{
store.ui.theme= store.ui.theme==='light' ? 'dark' : 'light';
applyTheme(); saveStore();
});
/* Resizable panes */
function makeResizable(handle, pane, key){
let startX, startW;
handle.addEventListener('mousedown', e=>{
e.preventDefault();
startX=e.clientX; startW=pane.getBoundingClientRect().width;
handle.classList.add('dragging');
function onMove(ev){
const dx=ev.clientX - startX;
let nw=startW + dx;
nw=Math.max(180, Math.min(420, nw));
pane.style.width=nw+'px';
store.ui.paneWidths[key]=nw;
}
function onUp(){
handle.classList.remove('dragging');
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
saveStore();
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
makeResizable($('#handle1'), $('#sidebar'), 'sidebar');
makeResizable($('#handle2'), $('#notelist'), 'notelist');
/* Keyboard: Ctrl+N new note */
document.addEventListener('keydown', e=>{
if((e.ctrlKey||e.metaKey) && e.key.toLowerCase()==='n'){
e.preventDefault(); createNote();
}
});
/* Init */
loadStore();
renderAll();
// restore search input
$('#searchInput').value=store.ui.searchQuery;
$('#searchClear').style.display= store.ui.searchQuery ? 'grid' : 'none';
// expose for probe tests
window.__inkwell={ store, extractText, sanitizeHtml, getFilteredNotes, saveStore, loadStore, CONFIG };
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"954d0d94-26d5-4945-a113-ba4e58818a30","tokensIn":162038,"tokensOut":21092,"tokensTotal":183130,"cost":0.007316268,"turns":7,"toolCalls":6,"failedToolCalls":0,"timestamp":"2026-08-23T00:55:50.640Z"} -->