Notes
Grok 4.5 · legacy

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 83,886 bytes · SHA-256 90a808669c6b
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lumen 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&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #f6f5f2;
--bg-elevated: #ffffff;
--bg-sidebar: #efeee9;
--bg-hover: rgba(0,0,0,0.05);
--bg-active: rgba(0,0,0,0.08);
--bg-input: #ffffff;
--border: rgba(0,0,0,0.08);
--border-strong: rgba(0,0,0,0.12);
--text: #1a1a1a;
--text-secondary: #6b6b6b;
--text-muted: #9a9a9a;
--accent: #5b6cfa;
--accent-soft: rgba(91,108,250,0.12);
--accent-hover: #4a5ae8;
--danger: #e05454;
--danger-soft: rgba(224,84,84,0.12);
--success: #3d9a6a;
--warning: #d4a017;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow: 0 4px 16px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.04);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.1), 0 4px 12px rgba(0,0,0,0.05);
--radius: 10px;
--radius-sm: 6px;
--radius-lg: 14px;
--sidebar-w: 280px;
--list-w: 300px;
--font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--transition: 160ms cubic-bezier(0.4, 0, 0.2, 1);
}
[data-theme="dark"] {
--bg: #141416;
--bg-elevated: #1c1c1f;
--bg-sidebar: #18181b;
--bg-hover: rgba(255,255,255,0.05);
--bg-active: rgba(255,255,255,0.08);
--bg-input: #252528;
--border: rgba(255,255,255,0.08);
--border-strong: rgba(255,255,255,0.12);
--text: #f0f0f2;
--text-secondary: #a0a0a8;
--text-muted: #6a6a72;
--accent: #7b8cff;
--accent-soft: rgba(123,140,255,0.15);
--accent-hover: #8f9eff;
--danger: #f07070;
--danger-soft: rgba(240,112,112,0.15);
--shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
--shadow: 0 4px 16px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.4), 0 4px 12px rgba(0,0,0,0.25);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
overflow: hidden;
font-family: var(--font);
font-size: 14px;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button, input, textarea { font-family: inherit; font-size: inherit; color: inherit; }
button { cursor: pointer; border: none; background: none; }
::selection { background: var(--accent-soft); color: var(--text); }
/* ─── App Shell ─── */
.app {
display: grid;
grid-template-columns: var(--sidebar-w) var(--list-w) 1fr;
height: 100%;
transition: grid-template-columns var(--transition);
}
.app.sidebar-collapsed { grid-template-columns: 0 var(--list-w) 1fr; }
.app.list-collapsed { grid-template-columns: var(--sidebar-w) 0 1fr; }
.app.sidebar-collapsed.list-collapsed { grid-template-columns: 0 0 1fr; }
/* ─── Sidebar ─── */
.sidebar {
background: var(--bg-sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
transition: opacity var(--transition);
}
.app.sidebar-collapsed .sidebar { opacity: 0; pointer-events: none; }
.sidebar-header {
padding: 16px 14px 12px;
display: flex;
align-items: center;
gap: 10px;
}
.logo {
display: flex;
align-items: center;
gap: 9px;
flex: 1;
min-width: 0;
}
.logo-mark {
width: 28px;
height: 28px;
border-radius: 8px;
background: linear-gradient(135deg, var(--accent), #a78bfa);
display: grid;
place-items: center;
color: white;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(91,108,250,0.35);
}
.logo-mark svg { width: 15px; height: 15px; }
.logo-text {
font-weight: 700;
font-size: 15px;
letter-spacing: -0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.icon-btn {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
display: grid;
place-items: center;
color: var(--text-secondary);
transition: background var(--transition), color var(--transition);
flex-shrink: 0;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text); }
.icon-btn:active { background: var(--bg-active); }
.icon-btn svg { width: 16px; height: 16px; stroke-width: 1.75; }
.icon-btn.sm { width: 28px; height: 28px; }
.icon-btn.sm svg { width: 14px; height: 14px; }
.icon-btn.danger:hover { background: var(--danger-soft); color: var(--danger); }
.search-wrap {
padding: 0 12px 12px;
}
.search-box {
display: flex;
align-items: center;
gap: 8px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0 10px;
height: 36px;
transition: border-color var(--transition), box-shadow var(--transition);
}
.search-box:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.search-box svg { width: 15px; height: 15px; color: var(--text-muted); flex-shrink: 0; }
.search-box input {
flex: 1;
border: none;
outline: none;
background: transparent;
min-width: 0;
}
.search-box input::placeholder { color: var(--text-muted); }
.search-box .clear-search {
display: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--bg-hover);
color: var(--text-secondary);
place-items: center;
}
.search-box .clear-search.visible { display: grid; }
.search-box .clear-search svg { width: 10px; height: 10px; }
.nav-section {
padding: 4px 10px 8px;
}
.nav-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
padding: 8px 8px 6px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-item {
display: flex;
align-items: center;
gap: 9px;
padding: 7px 10px;
border-radius: var(--radius-sm);
color: var(--text-secondary);
transition: background var(--transition), color var(--transition);
user-select: none;
width: 100%;
text-align: left;
}
.nav-item:hover { background: var(--bg-hover); color: var(--text); }
.nav-item.active {
background: var(--accent-soft);
color: var(--accent);
font-weight: 500;
}
.nav-item svg { width: 16px; height: 16px; flex-shrink: 0; stroke-width: 1.75; }
.nav-item .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item .count {
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
background: var(--bg-hover);
padding: 1px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
.nav-item.active .count { background: var(--accent-soft); color: var(--accent); }
.notebook-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.nav-item .item-actions {
display: none;
gap: 2px;
}
.nav-item:hover .item-actions { display: flex; }
.nav-item:hover .count { display: none; }
.sidebar-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.sidebar-footer {
padding: 10px 12px;
border-top: 1px solid var(--border);
display: flex;
gap: 4px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
height: 34px;
padding: 0 14px;
border-radius: var(--radius-sm);
font-weight: 500;
font-size: 13px;
transition: background var(--transition), color var(--transition), box-shadow var(--transition);
white-space: nowrap;
}
.btn svg { width: 15px; height: 15px; stroke-width: 1.75; }
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:active { transform: scale(0.98); }
.btn-ghost {
background: transparent;
color: var(--text-secondary);
}
.btn-ghost:hover { background: var(--bg-hover); color: var(--text); }
.btn-soft {
background: var(--accent-soft);
color: var(--accent);
}
.btn-soft:hover { background: var(--accent); color: white; }
.btn-block { width: 100%; }
.btn-danger {
background: var(--danger-soft);
color: var(--danger);
}
.btn-danger:hover { background: var(--danger); color: white; }
/* ─── Note List ─── */
.note-list-panel {
background: var(--bg-elevated);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
transition: opacity var(--transition);
}
.app.list-collapsed .note-list-panel { opacity: 0; pointer-events: none; }
.list-header {
padding: 14px 14px 10px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--border);
min-height: 56px;
}
.list-header h2 {
flex: 1;
font-size: 15px;
font-weight: 600;
letter-spacing: -0.02em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-meta {
padding: 0 14px 10px;
font-size: 12px;
color: var(--text-muted);
}
.sort-row {
padding: 0 10px 8px;
display: flex;
gap: 4px;
}
.chip {
height: 26px;
padding: 0 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
background: transparent;
transition: background var(--transition), color var(--transition);
}
.chip:hover { background: var(--bg-hover); color: var(--text); }
.chip.active { background: var(--accent-soft); color: var(--accent); }
.notes-scroll {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
padding: 4px 8px 16px;
}
.note-card {
padding: 12px 12px;
border-radius: var(--radius);
cursor: pointer;
transition: background var(--transition);
border: 1px solid transparent;
margin-bottom: 2px;
position: relative;
}
.note-card:hover { background: var(--bg-hover); }
.note-card.active {
background: var(--accent-soft);
border-color: transparent;
}
.note-card-top {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 4px;
}
.note-card-title {
flex: 1;
font-weight: 600;
font-size: 13.5px;
letter-spacing: -0.01em;
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.note-card .pin-icon {
color: var(--warning);
flex-shrink: 0;
margin-top: 1px;
}
.note-card .pin-icon svg { width: 12px; height: 12px; fill: currentColor; }
.note-card-preview {
font-size: 12.5px;
color: var(--text-secondary);
line-height: 1.45;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 8px;
}
.note-card-footer {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.note-card-date {
font-size: 11px;
color: var(--text-muted);
}
.tag {
font-size: 11px;
font-weight: 500;
padding: 1px 7px;
border-radius: 4px;
background: var(--bg-hover);
color: var(--text-secondary);
}
.note-card.active .tag { background: rgba(91,108,250,0.1); color: var(--accent); }
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
color: var(--text-muted);
gap: 8px;
}
.empty-list svg { width: 40px; height: 40px; opacity: 0.4; margin-bottom: 4px; }
.empty-list p { font-size: 13px; line-height: 1.5; max-width: 180px; }
/* ─── Editor ─── */
.editor-panel {
background: var(--bg-elevated);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.editor-toolbar {
display: flex;
align-items: center;
gap: 2px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
min-height: 52px;
flex-wrap: wrap;
}
.editor-toolbar .spacer { flex: 1; }
.toolbar-group {
display: flex;
align-items: center;
gap: 1px;
padding: 0 4px;
}
.toolbar-group + .toolbar-group {
border-left: 1px solid var(--border);
padding-left: 8px;
margin-left: 4px;
}
.tb-btn {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
display: grid;
place-items: center;
color: var(--text-secondary);
transition: background var(--transition), color var(--transition);
}
.tb-btn:hover { background: var(--bg-hover); color: var(--text); }
.tb-btn.active { background: var(--accent-soft); color: var(--accent); }
.tb-btn svg { width: 15px; height: 15px; stroke-width: 1.75; }
.tb-btn[data-cmd="bold"] { font-weight: 700; font-size: 14px; }
.tb-btn[data-cmd="italic"] { font-style: italic; font-family: Georgia, serif; font-size: 15px; }
.tb-sep {
width: 1px;
height: 18px;
background: var(--border);
margin: 0 6px;
}
.editor-body {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.editor-inner {
max-width: 720px;
margin: 0 auto;
padding: 36px 48px 120px;
}
.editor-meta-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.editor-meta-bar .meta-date {
font-size: 12px;
color: var(--text-muted);
}
.editor-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.editor-tag {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 500;
padding: 3px 8px;
border-radius: 5px;
background: var(--accent-soft);
color: var(--accent);
cursor: default;
}
.editor-tag button {
display: grid;
place-items: center;
width: 14px;
height: 14px;
border-radius: 50%;
color: var(--accent);
opacity: 0.6;
}
.editor-tag button:hover { opacity: 1; background: rgba(91,108,250,0.15); }
.editor-tag button svg { width: 9px; height: 9px; }
.add-tag-btn {
font-size: 12px;
color: var(--text-muted);
padding: 3px 8px;
border-radius: 5px;
border: 1px dashed var(--border-strong);
transition: color var(--transition), border-color var(--transition);
}
.add-tag-btn:hover { color: var(--accent); border-color: var(--accent); }
.note-title {
width: 100%;
border: none;
outline: none;
background: transparent;
font-size: 32px;
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.25;
color: var(--text);
margin-bottom: 8px;
resize: none;
overflow: hidden;
min-height: 40px;
}
.note-title::placeholder { color: var(--text-muted); }
.note-content {
outline: none;
min-height: 300px;
font-size: 15.5px;
line-height: 1.7;
color: var(--text);
letter-spacing: -0.01em;
}
.note-content:empty::before {
content: attr(data-placeholder);
color: var(--text-muted);
pointer-events: none;
}
.note-content h1 { font-size: 1.6em; font-weight: 700; margin: 1.2em 0 0.4em; letter-spacing: -0.02em; }
.note-content h2 { font-size: 1.3em; font-weight: 650; margin: 1.1em 0 0.35em; letter-spacing: -0.02em; }
.note-content h3 { font-size: 1.1em; font-weight: 600; margin: 1em 0 0.3em; }
.note-content p { margin: 0.4em 0; }
.note-content ul, .note-content ol { padding-left: 1.5em; margin: 0.5em 0; }
.note-content li { margin: 0.25em 0; }
.note-content blockquote {
border-left: 3px solid var(--accent);
padding: 4px 16px;
margin: 0.8em 0;
color: var(--text-secondary);
background: var(--accent-soft);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.note-content code {
font-family: var(--font-mono);
font-size: 0.88em;
background: var(--bg-hover);
padding: 2px 6px;
border-radius: 4px;
}
.note-content pre {
font-family: var(--font-mono);
font-size: 0.88em;
background: var(--bg-sidebar);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
overflow-x: auto;
margin: 0.8em 0;
line-height: 1.55;
}
.note-content pre code { background: none; padding: 0; }
.note-content a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.note-content hr {
border: none;
border-top: 1px solid var(--border-strong);
margin: 1.5em 0;
}
.note-content img { max-width: 100%; border-radius: var(--radius); margin: 0.5em 0; }
.editor-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-muted);
gap: 12px;
padding: 40px;
text-align: center;
}
.editor-empty .empty-icon {
width: 72px;
height: 72px;
border-radius: 20px;
background: var(--bg-sidebar);
display: grid;
place-items: center;
margin-bottom: 8px;
}
.editor-empty .empty-icon svg { width: 32px; height: 32px; opacity: 0.5; }
.editor-empty h3 { font-size: 17px; font-weight: 600; color: var(--text-secondary); letter-spacing: -0.02em; }
.editor-empty p { font-size: 13.5px; max-width: 280px; line-height: 1.55; }
.editor-empty .shortcuts {
display: flex;
gap: 16px;
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
}
.shortcut {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-muted);
}
kbd {
font-family: var(--font);
font-size: 11px;
font-weight: 500;
background: var(--bg-sidebar);
border: 1px solid var(--border-strong);
border-radius: 4px;
padding: 2px 6px;
color: var(--text-secondary);
box-shadow: 0 1px 0 var(--border-strong);
}
/* ─── Modals ─── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 200ms ease;
}
.modal-overlay.open { opacity: 1; pointer-events: auto; }
.modal {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
width: min(400px, calc(100vw - 32px));
padding: 24px;
transform: scale(0.96) translateY(8px);
transition: transform 200ms cubic-bezier(0.34, 1.2, 0.64, 1);
}
.modal-overlay.open .modal { transform: scale(1) translateY(0); }
.modal h3 {
font-size: 17px;
font-weight: 650;
letter-spacing: -0.02em;
margin-bottom: 6px;
}
.modal .modal-desc {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 18px;
line-height: 1.5;
}
.modal-field { margin-bottom: 14px; }
.modal-field label {
display: block;
font-size: 12px;
font-weight: 550;
color: var(--text-secondary);
margin-bottom: 6px;
}
.modal-field input, .modal-field select {
width: 100%;
height: 38px;
padding: 0 12px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--bg-input);
outline: none;
transition: border-color var(--transition), box-shadow var(--transition);
}
.modal-field input:focus, .modal-field select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.color-picks {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.color-pick {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid transparent;
transition: border-color var(--transition), transform var(--transition);
cursor: pointer;
}
.color-pick:hover { transform: scale(1.1); }
.color-pick.selected { border-color: var(--text); box-shadow: 0 0 0 2px var(--bg-elevated); }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
}
/* Command palette */
.cmd-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.35);
backdrop-filter: blur(3px);
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 15vh;
z-index: 1100;
opacity: 0;
pointer-events: none;
transition: opacity 150ms ease;
}
.cmd-overlay.open { opacity: 1; pointer-events: auto; }
.cmd-palette {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
width: min(520px, calc(100vw - 32px));
overflow: hidden;
transform: scale(0.97) translateY(-6px);
transition: transform 180ms cubic-bezier(0.34, 1.2, 0.64, 1);
}
.cmd-overlay.open .cmd-palette { transform: scale(1) translateY(0); }
.cmd-input-wrap {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.cmd-input-wrap svg { width: 18px; height: 18px; color: var(--text-muted); flex-shrink: 0; }
.cmd-input-wrap input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 15px;
}
.cmd-input-wrap input::placeholder { color: var(--text-muted); }
.cmd-results {
max-height: 320px;
overflow-y: auto;
padding: 6px;
}
.cmd-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background var(--transition);
}
.cmd-item:hover, .cmd-item.highlighted { background: var(--bg-hover); }
.cmd-item .cmd-icon {
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--bg-sidebar);
display: grid;
place-items: center;
color: var(--text-secondary);
flex-shrink: 0;
}
.cmd-item .cmd-icon svg { width: 14px; height: 14px; }
.cmd-item .cmd-label { flex: 1; font-weight: 500; font-size: 13.5px; }
.cmd-item .cmd-hint { font-size: 11px; color: var(--text-muted); }
.cmd-empty {
padding: 24px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
/* Toast */
.toasts {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 2000;
display: flex;
flex-direction: column-reverse;
gap: 8px;
}
.toast {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 12px 16px;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
animation: toastIn 250ms cubic-bezier(0.34, 1.2, 0.64, 1);
min-width: 200px;
}
.toast.out { animation: toastOut 200ms ease forwards; }
@keyframes toastIn {
from { opacity: 0; transform: translateY(12px) scale(0.96); }
to { opacity: 1; transform: none; }
}
@keyframes toastOut {
to { opacity: 0; transform: translateY(8px) scale(0.96); }
}
.toast svg { width: 16px; height: 16px; flex-shrink: 0; }
.toast.success svg { color: var(--success); }
.toast.info svg { color: var(--accent); }
/* Context menu */
.context-menu {
position: fixed;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
padding: 4px;
z-index: 1500;
min-width: 180px;
display: none;
}
.context-menu.open { display: block; animation: toastIn 120ms ease; }
.ctx-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 8px 10px;
border-radius: var(--radius-sm);
font-size: 13px;
color: var(--text);
transition: background var(--transition);
text-align: left;
}
.ctx-item:hover { background: var(--bg-hover); }
.ctx-item.danger { color: var(--danger); }
.ctx-item.danger:hover { background: var(--danger-soft); }
.ctx-item svg { width: 14px; height: 14px; opacity: 0.7; }
.ctx-sep { height: 1px; background: var(--border); margin: 4px 6px; }
/* Status bar */
.status-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 28px;
display: flex;
align-items: center;
padding: 0 16px;
font-size: 11px;
color: var(--text-muted);
border-top: 1px solid var(--border);
background: var(--bg-elevated);
gap: 16px;
}
.status-bar .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--success);
}
.status-bar .stat-right { margin-left: auto; display: flex; gap: 16px; }
/* Responsive */
@media (max-width: 900px) {
.app { grid-template-columns: 0 var(--list-w) 1fr; }
.sidebar { position: fixed; left: 0; top: 0; bottom: 0; width: var(--sidebar-w); z-index: 100; box-shadow: var(--shadow-lg); transform: translateX(-100%); transition: transform 200ms ease; }
.app.mobile-sidebar .sidebar { transform: translateX(0); opacity: 1; pointer-events: auto; }
.app.sidebar-collapsed { grid-template-columns: 0 var(--list-w) 1fr; }
}
@media (max-width: 640px) {
.app, .app.sidebar-collapsed, .app.list-collapsed { grid-template-columns: 1fr; }
.note-list-panel { position: fixed; inset: 0; z-index: 50; width: 100%; transition: transform 200ms ease; }
.app.mobile-editor .note-list-panel { transform: translateX(-100%); }
.editor-inner { padding: 24px 20px 100px; }
.note-title { font-size: 24px; }
}
/* Focus mode */
.app.focus-mode { grid-template-columns: 0 0 1fr; }
.app.focus-mode .sidebar,
.app.focus-mode .note-list-panel { opacity: 0; pointer-events: none; }
</style>
</head>
<body>
<div class="app" id="app">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo">
<div class="logo-mark">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
</div>
<span class="logo-text">Lumen Notes</span>
</div>
<button class="icon-btn" id="btnTheme" title="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>
</button>
</div>
<div class="search-wrap">
<div class="search-box">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="searchInput" placeholder="Search notes…" autocomplete="off" />
<button class="clear-search" id="clearSearch" title="Clear">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="sidebar-scroll">
<div class="nav-section">
<button class="nav-item" data-view="all" id="navAll">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></svg>
<span class="label">All Notes</span>
<span class="count" id="countAll">0</span>
</button>
<button class="nav-item" data-view="pinned" id="navPinned">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>
<span class="label">Pinned</span>
<span class="count" id="countPinned">0</span>
</button>
<button class="nav-item" data-view="recent" id="navRecent">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span class="label">Recent</span>
</button>
</div>
<div class="nav-section">
<div class="nav-label">
Notebooks
<button class="icon-btn sm" id="btnAddNotebook" title="New notebook">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5v14"/></svg>
</button>
</div>
<div id="notebookList"></div>
</div>
<div class="nav-section" id="tagsSection" style="display:none">
<div class="nav-label">Tags</div>
<div id="tagsList"></div>
</div>
</div>
<div class="sidebar-footer">
<button class="btn btn-soft btn-block" id="btnNewNote">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5v14"/></svg>
New Note
</button>
</div>
</aside>
<!-- Note list -->
<div class="note-list-panel" id="noteListPanel">
<div class="list-header">
<button class="icon-btn" id="btnToggleSidebar" title="Toggle sidebar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>
</button>
<h2 id="listTitle">All Notes</h2>
<button class="icon-btn" id="btnNewNoteHeader" title="New note (⌘N)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5v14"/></svg>
</button>
</div>
<div class="sort-row">
<button class="chip active" data-sort="updated">Recent</button>
<button class="chip" data-sort="created">Created</button>
<button class="chip" data-sort="title">Title</button>
</div>
<div class="list-meta" id="listMeta">0 notes</div>
<div class="notes-scroll" id="notesScroll"></div>
</div>
<!-- Editor -->
<main class="editor-panel" id="editorPanel">
<div id="editorToolbar" class="editor-toolbar" style="display:none">
<button class="icon-btn" id="btnBackList" title="Back to list" style="display:none">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</button>
<div class="toolbar-group">
<button class="tb-btn" data-cmd="bold" title="Bold (⌘B)"><b>B</b></button>
<button class="tb-btn" data-cmd="italic" title="Italic (⌘I)"><i>I</i></button>
<button class="tb-btn" data-cmd="underline" title="Underline (⌘U)"><u style="font-size:13px;font-weight:500">U</u></button>
<button class="tb-btn" data-cmd="strikeThrough" title="Strikethrough"><s style="font-size:13px">S</s></button>
</div>
<div class="toolbar-group">
<button class="tb-btn" data-cmd="formatBlock" data-value="h1" title="Heading 1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h8M4 18V6M12 18V6M17 12l3-2v8"/></svg>
</button>
<button class="tb-btn" data-cmd="formatBlock" data-value="h2" title="Heading 2">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4 12h8M4 18V6M12 18V6M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"/></svg>
</button>
<button class="tb-btn" data-cmd="formatBlock" data-value="p" title="Paragraph">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M13 4v16M17 4v16M19 4H9.5a4.5 4.5 0 0 0 0 9H13"/></svg>
</button>
</div>
<div class="toolbar-group">
<button class="tb-btn" data-cmd="insertUnorderedList" title="Bullet list">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>
</button>
<button class="tb-btn" data-cmd="insertOrderedList" title="Numbered list">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4M4 10h2M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>
</button>
<button class="tb-btn" data-cmd="formatBlock" data-value="blockquote" title="Quote">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><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.031V21"/><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 4v3"/></svg>
</button>
<button class="tb-btn" data-cmd="formatBlock" data-value="pre" title="Code block">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
</button>
</div>
<div class="toolbar-group">
<button class="tb-btn" id="btnInsertLink" title="Insert link">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>
</button>
<button class="tb-btn" data-cmd="removeFormat" title="Clear formatting">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h16v3M9 20h6M12 4v16"/></svg>
</button>
</div>
<div class="spacer"></div>
<div class="toolbar-group">
<button class="tb-btn" id="btnPin" title="Pin note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>
</button>
<button class="tb-btn" id="btnMoveNote" title="Move to notebook">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>
</button>
<button class="tb-btn" id="btnFocusMode" title="Focus mode (⌘.)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
</button>
<button class="tb-btn danger" id="btnDeleteNote" title="Delete note" style="color:var(--text-secondary)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
</button>
</div>
</div>
<div class="editor-body" id="editorBody">
<div class="editor-empty" id="editorEmpty">
<div class="empty-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
</div>
<h3>Select a note or create one</h3>
<p>Your thoughts, ideas, and knowledge — beautifully organized in one place.</p>
<button class="btn btn-primary" id="btnEmptyNew" style="margin-top:8px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="15" height="15"><path d="M5 12h14M12 5v14"/></svg>
Create note
</button>
<div class="shortcuts">
<div class="shortcut"><kbd>⌘</kbd><kbd>N</kbd> New note</div>
<div class="shortcut"><kbd>⌘</kbd><kbd>K</kbd> Search</div>
<div class="shortcut"><kbd>⌘</kbd><kbd>.</kbd> Focus</div>
</div>
</div>
<div class="editor-inner" id="editorInner" style="display:none">
<div class="editor-meta-bar">
<span class="meta-date" id="editorDate"></span>
<div class="editor-tags" id="editorTags"></div>
</div>
<textarea class="note-title" id="noteTitle" placeholder="Untitled" rows="1"></textarea>
<div class="note-content" id="noteContent" contenteditable="true" data-placeholder="Start writing… Type / for commands or just begin." spellcheck="true"></div>
</div>
</div>
<div class="status-bar" id="statusBar" style="display:none">
<span class="dot"></span>
<span id="statusSaved">Saved</span>
<div class="stat-right">
<span id="statusWords">0 words</span>
<span id="statusChars">0 characters</span>
</div>
</div>
</main>
</div>
<!-- Notebook modal -->
<div class="modal-overlay" id="notebookModal">
<div class="modal">
<h3 id="nbModalTitle">New Notebook</h3>
<p class="modal-desc">Organize notes into notebooks by project, topic, or purpose.</p>
<div class="modal-field">
<label for="nbName">Name</label>
<input type="text" id="nbName" placeholder="e.g. Work, Personal, Research" maxlength="40" />
</div>
<div class="modal-field">
<label>Color</label>
<div class="color-picks" id="colorPicks"></div>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" id="nbCancel">Cancel</button>
<button class="btn btn-primary" id="nbSave">Create</button>
</div>
</div>
</div>
<!-- Move note modal -->
<div class="modal-overlay" id="moveModal">
<div class="modal">
<h3>Move to Notebook</h3>
<p class="modal-desc">Choose a notebook for this note.</p>
<div class="modal-field">
<label for="moveSelect">Notebook</label>
<select id="moveSelect"></select>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" id="moveCancel">Cancel</button>
<button class="btn btn-primary" id="moveSave">Move</button>
</div>
</div>
</div>
<!-- Confirm delete -->
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<h3>Delete note?</h3>
<p class="modal-desc">This action cannot be undone. The note will be permanently removed.</p>
<div class="modal-actions">
<button class="btn btn-ghost" id="delCancel">Cancel</button>
<button class="btn btn-danger" id="delConfirm">Delete</button>
</div>
</div>
</div>
<!-- Tag input modal -->
<div class="modal-overlay" id="tagModal">
<div class="modal">
<h3>Add Tag</h3>
<p class="modal-desc">Tags help you find related notes across notebooks.</p>
<div class="modal-field">
<label for="tagInput">Tag name</label>
<input type="text" id="tagInput" placeholder="e.g. ideas, todo, reference" maxlength="24" />
</div>
<div class="modal-actions">
<button class="btn btn-ghost" id="tagCancel">Cancel</button>
<button class="btn btn-primary" id="tagSave">Add</button>
</div>
</div>
</div>
<!-- Command palette -->
<div class="cmd-overlay" id="cmdOverlay">
<div class="cmd-palette">
<div class="cmd-input-wrap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="cmdInput" placeholder="Search notes and commands…" autocomplete="off" />
<kbd style="flex-shrink:0">ESC</kbd>
</div>
<div class="cmd-results" id="cmdResults"></div>
</div>
</div>
<!-- Context menu -->
<div class="context-menu" id="contextMenu"></div>
<div class="toasts" id="toasts"></div>
<script>
(function () {
'use strict';
// ─── Icons (inline SVG snippets) ───
const I = {
pin: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>',
trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>',
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>',
folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>',
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>',
note: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/></svg>',
moon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>',
sun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>',
x: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>',
tag: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z"/><path d="M7 7h.01"/></svg>',
plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5v14"/></svg>',
};
const COLORS = ['#5b6cfa','#a78bfa','#f472b6','#fb7185','#fbbf24','#34d399','#22d3ee','#60a5fa','#fb923c','#94a3b8'];
const STORAGE_KEY = 'lumen-notes-v1';
// ─── State ───
let state = {
theme: 'light',
notebooks: [],
notes: [],
view: { type: 'all' }, // all | pinned | recent | notebook | tag | search
selectedNoteId: null,
sort: 'updated',
searchQuery: '',
focusMode: false,
};
let saveTimer = null;
let editingNotebookId = null;
let selectedColor = COLORS[0];
let ctxTarget = null;
// ─── Sample data ───
function seedData() {
const now = Date.now();
const nb = [
{ id: uid(), name: 'Personal', color: COLORS[0] },
{ id: uid(), name: 'Work', color: COLORS[5] },
{ id: uid(), name: 'Ideas', color: COLORS[1] },
];
const notes = [
{
id: uid(),
title: 'Welcome to Lumen Notes',
content: `<p>Lumen is a lightweight notes app inspired by Obsidian, UpNote, and Notion. Everything lives in your browser — no account required.</p>
<h2>What you can do</h2>
<ul>
<li><b>Create notebooks</b> to organize by project or topic</li>
<li><b>Pin important notes</b> so they stay at the top</li>
<li><b>Tag notes</b> for cross-cutting labels</li>
<li><b>Rich text editing</b> — bold, headings, lists, quotes, code</li>
<li><b>Search everything</b> with ⌘K</li>
<li><b>Focus mode</b> with ⌘. for distraction-free writing</li>
</ul>
<blockquote>All notes are saved automatically to local storage.</blockquote>
<h2>Keyboard shortcuts</h2>
<ul>
<li><code>⌘/Ctrl + N</code> — New note</li>
<li><code>⌘/Ctrl + K</code> — Command palette / search</li>
<li><code>⌘/Ctrl + .</code> — Toggle focus mode</li>
<li><code>⌘/Ctrl + B / I / U</code> — Bold, italic, underline</li>
</ul>
<p>Select a different notebook, create a new note, and make this space yours.</p>`,
notebookId: nb[0].id,
tags: ['getting-started', 'tips'],
pinned: true,
createdAt: now - 86400000 * 3,
updatedAt: now - 3600000,
},
{
id: uid(),
title: 'Weekly planning',
content: `<h2>This week</h2>
<ul>
<li>Review Q1 goals</li>
<li>Ship the onboarding flow</li>
<li>Write design doc for search</li>
</ul>
<h3>Later</h3>
<ul>
<li>Team retro notes</li>
<li>UpdateHire plan</li>
</ul>
<p>Focus theme: <b>depth over breadth</b>.</p>`,
notebookId: nb[1].id,
tags: ['planning', 'work'],
pinned: false,
createdAt: now - 86400000 * 2,
updatedAt: now - 7200000,
},
{
id: uid(),
title: 'Product ideas dump',
content: `<p>Random sparks — filter later.</p>
<ol>
<li>Smart notebook suggestions based on content</li>
<li>Bi-directional links between notes</li>
<li>Daily note auto-create at midnight</li>
<li>Export as Markdown / PDF</li>
<li>Canvas view for spatial thinking</li>
</ol>
<blockquote>Capture first. Organize second. Perfect never.</blockquote>`,
notebookId: nb[2].id,
tags: ['ideas', 'product'],
pinned: true,
createdAt: now - 86400000,
updatedAt: now - 1800000,
},
{
id: uid(),
title: 'Meeting notes — design sync',
content: `<p><b>Attendees:</b> Alex, Sam, Jordan</p>
<p><b>Date:</b> Today</p>
<hr>
<h3>Decisions</h3>
<ul>
<li>Ship MVP with three core flows only</li>
<li>Postpone dark theme polish until next sprint</li>
</ul>
<h3>Action items</h3>
<ul>
<li>Alex — Figma passes by Friday</li>
<li>Sam — API contracts drafted</li>
<li>Jordan — User testing script</li>
</ul>`,
notebookId: nb[1].id,
tags: ['meetings'],
pinned: false,
createdAt: now - 86400000 * 0.5,
updatedAt: now - 900000,
},
{
id: uid(),
title: 'Books to read',
content: `<ul>
<li>Thinking, Fast and Slow — Kahneman</li>
<li>The Design of Everyday Things — Norman</li>
<li>Staff Engineer — Larson</li>
<li>Atomic Habits — Clear</li>
</ul>
<p>Currently reading: <i>Range</i> by David Epstein.</p>`,
notebookId: nb[0].id,
tags: ['reading'],
pinned: false,
createdAt: now - 86400000 * 5,
updatedAt: now - 86400000 * 4,
},
];
state.notebooks = nb;
state.notes = notes;
state.selectedNoteId = notes[0].id;
}
// ─── Utils ───
function uid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
function stripHtml(html) {
const d = document.createElement('div');
d.innerHTML = html || '';
return (d.textContent || d.innerText || '').replace(/\s+/g, ' ').trim();
}
function formatDate(ts) {
const d = new Date(ts);
const now = new Date();
const diff = now - d;
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 < 86400000 * 7) return Math.floor(diff / 86400000) + 'd ago';
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: d.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
}
function formatFullDate(ts) {
return new Date(ts).toLocaleString(undefined, {
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
hour: 'numeric', minute: '2-digit'
});
}
function wordCount(html) {
const t = stripHtml(html);
if (!t) return 0;
return t.split(/\s+/).filter(Boolean).length;
}
function debounce(fn, ms) {
let t;
return function (...args) {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), ms);
};
}
function isMod(e) {
return e.metaKey || e.ctrlKey;
}
// ─── Persistence ───
function load() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const data = JSON.parse(raw);
state.theme = data.theme || 'light';
state.notebooks = data.notebooks || [];
state.notes = data.notes || [];
state.selectedNoteId = data.selectedNoteId || null;
state.sort = data.sort || 'updated';
if (!state.notebooks.length && !state.notes.length) seedData();
} else {
seedData();
}
} catch {
seedData();
}
}
function persist() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
theme: state.theme,
notebooks: state.notebooks,
notes: state.notes,
selectedNoteId: state.selectedNoteId,
sort: state.sort,
}));
} catch (e) {
console.warn('Storage full or unavailable', e);
}
}
// ─── Filtering / Sorting ───
function getFilteredNotes() {
let notes = [...state.notes];
const v = state.view;
if (v.type === 'pinned') notes = notes.filter(n => n.pinned);
else if (v.type === 'recent') {
notes = notes.sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 20);
} else if (v.type === 'notebook') notes = notes.filter(n => n.notebookId === v.id);
else if (v.type === 'tag') notes = notes.filter(n => (n.tags || []).includes(v.tag));
else if (v.type === 'search') {
const q = (state.searchQuery || '').toLowerCase();
notes = notes.filter(n =>
(n.title || '').toLowerCase().includes(q) ||
stripHtml(n.content).toLowerCase().includes(q) ||
(n.tags || []).some(t => t.toLowerCase().includes(q))
);
}
if (v.type !== 'recent') {
notes.sort((a, b) => {
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
if (state.sort === 'title') return (a.title || '').localeCompare(b.title || '');
if (state.sort === 'created') return b.createdAt - a.createdAt;
return b.updatedAt - a.updatedAt;
});
}
return notes;
}
function getSelectedNote() {
return state.notes.find(n => n.id === state.selectedNoteId) || null;
}
function allTags() {
const map = {};
state.notes.forEach(n => (n.tags || []).forEach(t => { map[t] = (map[t] || 0) + 1; }));
return Object.entries(map).sort((a, b) => b[1] - a[1]);
}
// ─── Rendering ───
function applyTheme() {
document.documentElement.setAttribute('data-theme', state.theme);
const btn = document.getElementById('btnTheme');
btn.innerHTML = state.theme === 'dark' ? I.sun : I.moon;
btn.title = state.theme === 'dark' ? 'Light mode' : 'Dark mode';
}
function renderNotebooks() {
const el = document.getElementById('notebookList');
el.innerHTML = state.notebooks.map(nb => {
const count = state.notes.filter(n => n.notebookId === nb.id).length;
const active = state.view.type === 'notebook' && state.view.id === nb.id;
return `
<button class="nav-item ${active ? 'active' : ''}" data-notebook="${nb.id}">
<span class="notebook-dot" style="background:${nb.color}"></span>
<span class="label">${esc(nb.name)}</span>
<span class="count">${count}</span>
<span class="item-actions">
<span class="icon-btn sm" data-edit-nb="${nb.id}" title="Rename"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></span>
<span class="icon-btn sm danger" data-del-nb="${nb.id}" title="Delete"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/></svg></span>
</span>
</button>`;
}).join('');
}
function renderTags() {
const tags = allTags();
const section = document.getElementById('tagsSection');
const list = document.getElementById('tagsList');
if (!tags.length) { section.style.display = 'none'; return; }
section.style.display = '';
list.innerHTML = tags.map(([tag, count]) => {
const active = state.view.type === 'tag' && state.view.tag === tag;
return `
<button class="nav-item ${active ? 'active' : ''}" data-tag="${esc(tag)}">
${I.tag}
<span class="label">${esc(tag)}</span>
<span class="count">${count}</span>
</button>`;
}).join('');
}
function renderNavCounts() {
document.getElementById('countAll').textContent = state.notes.length;
document.getElementById('countPinned').textContent = state.notes.filter(n => n.pinned).length;
document.querySelectorAll('.nav-item[data-view]').forEach(el => {
el.classList.toggle('active', state.view.type === el.dataset.view);
});
}
function renderNoteList() {
const notes = getFilteredNotes();
const scroll = document.getElementById('notesScroll');
const meta = document.getElementById('listMeta');
const title = document.getElementById('listTitle');
let titleText = 'All Notes';
if (state.view.type === 'pinned') titleText = 'Pinned';
else if (state.view.type === 'recent') titleText = 'Recent';
else if (state.view.type === 'search') titleText = `Search: “${state.searchQuery}”`;
else if (state.view.type === 'notebook') {
const nb = state.notebooks.find(n => n.id === state.view.id);
titleText = nb ? nb.name : 'Notebook';
} else if (state.view.type === 'tag') titleText = `#${state.view.tag}`;
title.textContent = titleText;
meta.textContent = notes.length + (notes.length === 1 ? ' note' : ' notes');
document.querySelectorAll('.chip[data-sort]').forEach(c => {
c.classList.toggle('active', c.dataset.sort === state.sort);
});
if (!notes.length) {
scroll.innerHTML = `
<div class="empty-list">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/></svg>
<p>${state.searchQuery ? 'No notes match your search.' : 'No notes here yet. Create one!'}</p>
</div>`;
return;
}
scroll.innerHTML = notes.map(n => {
const preview = stripHtml(n.content).slice(0, 120);
const active = n.id === state.selectedNoteId;
const tags = (n.tags || []).slice(0, 3).map(t => `<span class="tag">${esc(t)}</span>`).join('');
return `
<div class="note-card ${active ? 'active' : ''}" data-note-id="${n.id}" role="button" tabindex="0">
<div class="note-card-top">
<div class="note-card-title">${esc(n.title || 'Untitled')}</div>
${n.pinned ? `<span class="pin-icon">${I.pin}</span>` : ''}
</div>
${preview ? `<div class="note-card-preview">${esc(preview)}</div>` : ''}
<div class="note-card-footer">
<span class="note-card-date">${formatDate(n.updatedAt)}</span>
${tags}
</div>
</div>`;
}).join('');
}
function renderEditor() {
const note = getSelectedNote();
const empty = document.getElementById('editorEmpty');
const inner = document.getElementById('editorInner');
const toolbar = document.getElementById('editorToolbar');
const status = document.getElementById('statusBar');
if (!note) {
empty.style.display = '';
inner.style.display = 'none';
toolbar.style.display = 'none';
status.style.display = 'none';
return;
}
empty.style.display = 'none';
inner.style.display = '';
toolbar.style.display = 'flex';
status.style.display = 'flex';
const titleEl = document.getElementById('noteTitle');
const contentEl = document.getElementById('noteContent');
if (titleEl.dataset.noteId !== note.id) {
titleEl.value = note.title || '';
titleEl.dataset.noteId = note.id;
autoResizeTitle();
contentEl.innerHTML = note.content || '';
contentEl.dataset.noteId = note.id;
}
document.getElementById('editorDate').textContent =
`Updated ${formatFullDate(note.updatedAt)} · Created ${formatDate(note.createdAt)}`;
renderEditorTags(note);
updateWordCount(note.content);
document.getElementById('btnPin').classList.toggle('active', !!note.pinned);
document.getElementById('statusSaved').textContent = 'Saved';
}
function renderEditorTags(note) {
const el = document.getElementById('editorTags');
const tags = note.tags || [];
el.innerHTML = tags.map(t => `
<span class="editor-tag">
${esc(t)}
<button data-remove-tag="${esc(t)}" title="Remove tag">${I.x}</button>
</span>
`).join('') + `<button class="add-tag-btn" id="btnAddTag">+ Tag</button>`;
}
function updateWordCount(html) {
const text = stripHtml(html);
const words = wordCount(html);
document.getElementById('statusWords').textContent = words + (words === 1 ? ' word' : ' words');
document.getElementById('statusChars').textContent = text.length + ' characters';
}
function autoResizeTitle() {
const el = document.getElementById('noteTitle');
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}
function fullRender() {
applyTheme();
renderNotebooks();
renderTags();
renderNavCounts();
renderNoteList();
renderEditor();
}
function esc(s) {
const d = document.createElement('div');
d.textContent = String(s == null ? '' : s);
return d.innerHTML;
}
// ─── Actions ───
function createNote(opts = {}) {
const notebookId = opts.notebookId ||
(state.view.type === 'notebook' ? state.view.id : null) ||
(state.notebooks[0] && state.notebooks[0].id) || null;
const note = {
id: uid(),
title: '',
content: '',
notebookId,
tags: opts.tags || [],
pinned: false,
createdAt: Date.now(),
updatedAt: Date.now(),
};
state.notes.unshift(note);
state.selectedNoteId = note.id;
persist();
fullRender();
// focus title
requestAnimationFrame(() => {
const t = document.getElementById('noteTitle');
t.focus();
document.getElementById('app').classList.add('mobile-editor');
});
toast('Note created', 'success');
return note;
}
function deleteNote(id) {
const idx = state.notes.findIndex(n => n.id === id);
if (idx === -1) return;
state.notes.splice(idx, 1);
if (state.selectedNoteId === id) {
state.selectedNoteId = state.notes[0] ? state.notes[0].id : null;
}
persist();
fullRender();
toast('Note deleted', 'info');
}
function updateNote(id, patch) {
const note = state.notes.find(n => n.id === id);
if (!note) return;
Object.assign(note, patch, { updatedAt: Date.now() });
document.getElementById('statusSaved').textContent = 'Saving…';
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
persist();
document.getElementById('statusSaved').textContent = 'Saved';
// light refresh list without resetting editor focus
renderNoteList();
renderNavCounts();
renderNotebooks();
renderTags();
const n = getSelectedNote();
if (n) {
document.getElementById('editorDate').textContent =
`Updated ${formatFullDate(n.updatedAt)} · Created ${formatDate(n.createdAt)}`;
}
}, 400);
}
function togglePin(id) {
const note = state.notes.find(n => n.id === id);
if (!note) return;
note.pinned = !note.pinned;
note.updatedAt = Date.now();
persist();
fullRender();
toast(note.pinned ? 'Pinned' : 'Unpinned', 'success');
}
function createNotebook(name, color) {
const nb = { id: uid(), name: name.trim(), color };
state.notebooks.push(nb);
persist();
fullRender();
toast('Notebook created', 'success');
return nb;
}
function updateNotebook(id, name, color) {
const nb = state.notebooks.find(n => n.id === id);
if (!nb) return;
nb.name = name.trim();
nb.color = color;
persist();
fullRender();
toast('Notebook updated', 'success');
}
function deleteNotebook(id) {
state.notebooks = state.notebooks.filter(n => n.id !== id);
state.notes.forEach(n => {
if (n.notebookId === id) n.notebookId = state.notebooks[0] ? state.notebooks[0].id : null;
});
if (state.view.type === 'notebook' && state.view.id === id) {
state.view = { type: 'all' };
}
persist();
fullRender();
toast('Notebook deleted', 'info');
}
// ─── Toasts ───
function toast(msg, type = 'info') {
const el = document.createElement('div');
el.className = `toast ${type}`;
el.innerHTML = (type === 'success' ? I.check : I.info) + `<span>${esc(msg)}</span>`;
document.getElementById('toasts').appendChild(el);
setTimeout(() => {
el.classList.add('out');
setTimeout(() => el.remove(), 200);
}, 2200);
}
// ─── Modals ───
function openModal(id) {
document.getElementById(id).classList.add('open');
}
function closeModal(id) {
document.getElementById(id).classList.remove('open');
}
function openNotebookModal(editId) {
editingNotebookId = editId || null;
selectedColor = COLORS[0];
const title = document.getElementById('nbModalTitle');
const save = document.getElementById('nbSave');
const input = document.getElementById('nbName');
if (editId) {
const nb = state.notebooks.find(n => n.id === editId);
title.textContent = 'Edit Notebook';
save.textContent = 'Save';
input.value = nb ? nb.name : '';
selectedColor = nb ? nb.color : COLORS[0];
} else {
title.textContent = 'New Notebook';
save.textContent = 'Create';
input.value = '';
}
const picks = document.getElementById('colorPicks');
picks.innerHTML = COLORS.map(c =>
`<button type="button" class="color-pick ${c === selectedColor ? 'selected' : ''}" data-color="${c}" style="background:${c}"></button>`
).join('');
openModal('notebookModal');
setTimeout(() => input.focus(), 50);
}
// ─── Command palette ───
function openCmd() {
const overlay = document.getElementById('cmdOverlay');
overlay.classList.add('open');
const input = document.getElementById('cmdInput');
input.value = '';
renderCmdResults('');
setTimeout(() => input.focus(), 30);
}
function closeCmd() {
document.getElementById('cmdOverlay').classList.remove('open');
}
function renderCmdResults(q) {
const results = document.getElementById('cmdResults');
const query = (q || '').toLowerCase().trim();
const items = [];
// Commands
const commands = [
{ id: 'new', label: 'New note', hint: '⌘N', icon: I.plus, action: () => { closeCmd(); createNote(); } },
{ id: 'new-nb', label: 'New notebook', hint: '', icon: I.folder, action: () => { closeCmd(); openNotebookModal(); } },
{ id: 'theme', label: 'Toggle theme', hint: '', icon: state.theme === 'dark' ? I.sun : I.moon, action: () => { closeCmd(); toggleTheme(); } },
{ id: 'focus', label: 'Toggle focus mode', hint: '⌘.', icon: I.note, action: () => { closeCmd(); toggleFocus(); } },
{ id: 'all', label: 'Go to All Notes', hint: '', icon: I.note, action: () => { closeCmd(); setView({ type: 'all' }); } },
{ id: 'pinned', label: 'Go to Pinned', hint: '', icon: I.pin, action: () => { closeCmd(); setView({ type: 'pinned' }); } },
];
if (!query || 'new note create'.includes(query)) items.push(commands[0]);
if (!query || 'notebook folder'.includes(query) || commands[1].label.toLowerCase().includes(query)) items.push(commands[1]);
if (!query || commands[2].label.toLowerCase().includes(query) || 'dark light'.includes(query)) items.push(commands[2]);
if (!query || commands[3].label.toLowerCase().includes(query) || 'focus'.includes(query)) items.push(commands[3]);
if (!query || 'all notes'.includes(query)) items.push(commands[4]);
if (!query || 'pinned'.includes(query)) items.push(commands[5]);
// Notes match
if (query) {
state.notes.forEach(n => {
const hay = ((n.title || '') + ' ' + stripHtml(n.content) + ' ' + (n.tags || []).join(' ')).toLowerCase();
if (hay.includes(query)) {
items.push({
id: 'note-' + n.id,
label: n.title || 'Untitled',
hint: formatDate(n.updatedAt),
icon: I.note,
action: () => {
closeCmd();
state.selectedNoteId = n.id;
fullRender();
document.getElementById('app').classList.add('mobile-editor');
},
});
}
});
}
if (!items.length) {
results.innerHTML = `<div class="cmd-empty">No results</div>`;
return;
}
results.innerHTML = items.map((item, i) => `
<div class="cmd-item ${i === 0 ? 'highlighted' : ''}" data-cmd-idx="${i}">
<div class="cmd-icon">${item.icon}</div>
<span class="cmd-label">${esc(item.label)}</span>
${item.hint ? `<span class="cmd-hint">${esc(item.hint)}</span>` : ''}
</div>
`).join('');
results._items = items;
results._hi = 0;
}
function runHighlightedCmd() {
const results = document.getElementById('cmdResults');
const items = results._items || [];
const hi = results._hi || 0;
if (items[hi]) items[hi].action();
}
// ─── View helpers ───
function setView(view) {
state.view = view;
if (view.type !== 'search') {
state.searchQuery = '';
document.getElementById('searchInput').value = '';
document.getElementById('clearSearch').classList.remove('visible');
}
fullRender();
}
function toggleTheme() {
state.theme = state.theme === 'dark' ? 'light' : 'dark';
persist();
applyTheme();
}
function toggleFocus() {
state.focusMode = !state.focusMode;
document.getElementById('app').classList.toggle('focus-mode', state.focusMode);
document.getElementById('btnFocusMode').classList.toggle('active', state.focusMode);
if (state.focusMode) toast('Focus mode on — press ⌘. to exit', 'info');
}
// ─── Context menu ───
function showContextMenu(x, y, items) {
const menu = document.getElementById('contextMenu');
menu.innerHTML = items.map(it => {
if (it.sep) return '<div class="ctx-sep"></div>';
return `<button class="ctx-item ${it.danger ? 'danger' : ''}" data-ctx="${it.id}">${it.icon || ''}<span>${esc(it.label)}</span></button>`;
}).join('');
menu._actions = {};
items.forEach(it => { if (it.id) menu._actions[it.id] = it.action; });
menu.style.display = 'block';
menu.classList.add('open');
const rect = menu.getBoundingClientRect();
let left = x, top = y;
if (left + rect.width > window.innerWidth - 8) left = window.innerWidth - rect.width - 8;
if (top + rect.height > window.innerHeight - 8) top = window.innerHeight - rect.height - 8;
menu.style.left = left + 'px';
menu.style.top = top + 'px';
}
function hideContextMenu() {
const menu = document.getElementById('contextMenu');
menu.classList.remove('open');
menu.style.display = 'none';
}
// ─── Event wiring ───
function bindEvents() {
// Theme
document.getElementById('btnTheme').addEventListener('click', toggleTheme);
// New note
document.getElementById('btnNewNote').addEventListener('click', () => createNote());
document.getElementById('btnNewNoteHeader').addEventListener('click', () => createNote());
document.getElementById('btnEmptyNew').addEventListener('click', () => createNote());
// Sidebar toggle
document.getElementById('btnToggleSidebar').addEventListener('click', () => {
document.getElementById('app').classList.toggle('sidebar-collapsed');
});
// Nav views
document.getElementById('sidebar').addEventListener('click', (e) => {
const viewBtn = e.target.closest('[data-view]');
if (viewBtn) {
setView({ type: viewBtn.dataset.view });
return;
}
const nbBtn = e.target.closest('[data-notebook]');
if (nbBtn && !e.target.closest('[data-edit-nb]') && !e.target.closest('[data-del-nb]')) {
setView({ type: 'notebook', id: nbBtn.dataset.notebook });
return;
}
const editNb = e.target.closest('[data-edit-nb]');
if (editNb) {
e.stopPropagation();
openNotebookModal(editNb.dataset.editNb);
return;
}
const delNb = e.target.closest('[data-del-nb]');
if (delNb) {
e.stopPropagation();
const id = delNb.dataset.delNb;
const nb = state.notebooks.find(n => n.id === id);
if (nb && confirm(`Delete notebook “${nb.name}”? Notes will be moved to another notebook.`)) {
deleteNotebook(id);
}
return;
}
const tagBtn = e.target.closest('[data-tag]');
if (tagBtn) {
setView({ type: 'tag', tag: tagBtn.dataset.tag });
}
});
document.getElementById('btnAddNotebook').addEventListener('click', () => openNotebookModal());
// Search
const searchInput = document.getElementById('searchInput');
const clearSearch = document.getElementById('clearSearch');
searchInput.addEventListener('input', () => {
const q = searchInput.value;
clearSearch.classList.toggle('visible', !!q);
state.searchQuery = q;
if (q.trim()) {
state.view = { type: 'search' };
} else {
state.view = { type: 'all' };
}
renderNoteList();
renderNavCounts();
});
clearSearch.addEventListener('click', () => {
searchInput.value = '';
clearSearch.classList.remove('visible');
state.searchQuery = '';
setView({ type: 'all' });
});
// Sort chips
document.querySelector('.sort-row').addEventListener('click', (e) => {
const chip = e.target.closest('[data-sort]');
if (!chip) return;
state.sort = chip.dataset.sort;
persist();
renderNoteList();
});
// Note list selection
document.getElementById('notesScroll').addEventListener('click', (e) => {
const card = e.target.closest('[data-note-id]');
if (!card) return;
state.selectedNoteId = card.dataset.noteId;
// force editor swap
document.getElementById('noteTitle').dataset.noteId = '';
renderEditor();
renderNoteList();
document.getElementById('app').classList.add('mobile-editor');
});
document.getElementById('notesScroll').addEventListener('contextmenu', (e) => {
const card = e.target.closest('[data-note-id]');
if (!card) return;
e.preventDefault();
const id = card.dataset.noteId;
const note = state.notes.find(n => n.id === id);
if (!note) return;
showContextMenu(e.clientX, e.clientY, [
{ id: 'open', label: 'Open', icon: I.note, action: () => {
state.selectedNoteId = id;
document.getElementById('noteTitle').dataset.noteId = '';
fullRender();
}},
{ id: 'pin', label: note.pinned ? 'Unpin' : 'Pin', icon: I.pin, action: () => togglePin(id) },
{ sep: true },
{ id: 'del', label: 'Delete', icon: I.trash, danger: true, action: () => {
state.selectedNoteId = id;
openModal('deleteModal');
}},
]);
});
// Title / content editing
const titleEl = document.getElementById('noteTitle');
titleEl.addEventListener('input', () => {
autoResizeTitle();
const id = titleEl.dataset.noteId;
if (id) updateNote(id, { title: titleEl.value });
});
const contentEl = document.getElementById('noteContent');
contentEl.addEventListener('input', () => {
const id = contentEl.dataset.noteId;
if (id) {
updateNote(id, { content: contentEl.innerHTML });
updateWordCount(contentEl.innerHTML);
}
});
// Toolbar formatting
document.getElementById('editorToolbar').addEventListener('click', (e) => {
const btn = e.target.closest('[data-cmd]');
if (!btn) return;
e.preventDefault();
const cmd = btn.dataset.cmd;
const val = btn.dataset.value || null;
document.getElementById('noteContent').focus();
if (cmd === 'formatBlock') {
document.execCommand('formatBlock', false, val === 'pre' ? 'pre' : val);
} else {
document.execCommand(cmd, false, val);
}
const id = contentEl.dataset.noteId;
if (id) {
updateNote(id, { content: contentEl.innerHTML });
updateWordCount(contentEl.innerHTML);
}
});
document.getElementById('btnInsertLink').addEventListener('click', () => {
const url = prompt('Link URL:', 'https://');
if (!url) return;
document.getElementById('noteContent').focus();
document.execCommand('createLink', false, url);
const id = contentEl.dataset.noteId;
if (id) updateNote(id, { content: contentEl.innerHTML });
});
document.getElementById('btnPin').addEventListener('click', () => {
if (state.selectedNoteId) togglePin(state.selectedNoteId);
});
document.getElementById('btnFocusMode').addEventListener('click', toggleFocus);
document.getElementById('btnDeleteNote').addEventListener('click', () => {
if (state.selectedNoteId) openModal('deleteModal');
});
document.getElementById('btnMoveNote').addEventListener('click', () => {
const sel = document.getElementById('moveSelect');
sel.innerHTML = state.notebooks.map(nb =>
`<option value="${nb.id}">${esc(nb.name)}</option>`
).join('') + `<option value="">(No notebook)</option>`;
const note = getSelectedNote();
if (note) sel.value = note.notebookId || '';
openModal('moveModal');
});
document.getElementById('btnBackList').addEventListener('click', () => {
document.getElementById('app').classList.remove('mobile-editor');
});
// Tags in editor
document.getElementById('editorTags').addEventListener('click', (e) => {
if (e.target.closest('#btnAddTag') || e.target.id === 'btnAddTag') {
document.getElementById('tagInput').value = '';
openModal('tagModal');
setTimeout(() => document.getElementById('tagInput').focus(), 50);
return;
}
const rm = e.target.closest('[data-remove-tag]');
if (rm) {
const tag = rm.dataset.removeTag;
const note = getSelectedNote();
if (!note) return;
note.tags = (note.tags || []).filter(t => t !== tag);
note.updatedAt = Date.now();
persist();
renderEditorTags(note);
renderNoteList();
renderTags();
}
});
// Modal: notebook
document.getElementById('colorPicks').addEventListener('click', (e) => {
const p = e.target.closest('[data-color]');
if (!p) return;
selectedColor = p.dataset.color;
document.querySelectorAll('.color-pick').forEach(el => {
el.classList.toggle('selected', el.dataset.color === selectedColor);
});
});
document.getElementById('nbCancel').addEventListener('click', () => closeModal('notebookModal'));
document.getElementById('nbSave').addEventListener('click', () => {
const name = document.getElementById('nbName').value.trim();
if (!name) { document.getElementById('nbName').focus(); return; }
if (editingNotebookId) updateNotebook(editingNotebookId, name, selectedColor);
else createNotebook(name, selectedColor);
closeModal('notebookModal');
});
document.getElementById('nbName').addEventListener('keydown', (e) => {
if (e.key === 'Enter') document.getElementById('nbSave').click();
});
// Modal: move
document.getElementById('moveCancel').addEventListener('click', () => closeModal('moveModal'));
document.getElementById('moveSave').addEventListener('click', () => {
const note = getSelectedNote();
if (note) {
note.notebookId = document.getElementById('moveSelect').value || null;
note.updatedAt = Date.now();
persist();
fullRender();
toast('Note moved', 'success');
}
closeModal('moveModal');
});
// Modal: delete
document.getElementById('delCancel').addEventListener('click', () => closeModal('deleteModal'));
document.getElementById('delConfirm').addEventListener('click', () => {
if (state.selectedNoteId) deleteNote(state.selectedNoteId);
closeModal('deleteModal');
});
// Modal: tag
document.getElementById('tagCancel').addEventListener('click', () => closeModal('tagModal'));
document.getElementById('tagSave').addEventListener('click', () => {
const t = document.getElementById('tagInput').value.trim().toLowerCase().replace(/\s+/g, '-');
if (!t) return;
const note = getSelectedNote();
if (note) {
note.tags = note.tags || [];
if (!note.tags.includes(t)) {
note.tags.push(t);
note.updatedAt = Date.now();
persist();
renderEditorTags(note);
renderNoteList();
renderTags();
toast('Tag added', 'success');
}
}
closeModal('tagModal');
});
document.getElementById('tagInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') document.getElementById('tagSave').click();
});
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(ov => {
ov.addEventListener('click', (e) => {
if (e.target === ov) ov.classList.remove('open');
});
});
// Command palette
document.getElementById('cmdOverlay').addEventListener('click', (e) => {
if (e.target.id === 'cmdOverlay') closeCmd();
});
document.getElementById('cmdInput').addEventListener('input', (e) => {
renderCmdResults(e.target.value);
});
document.getElementById('cmdInput').addEventListener('keydown', (e) => {
const results = document.getElementById('cmdResults');
const items = results._items || [];
if (e.key === 'ArrowDown') {
e.preventDefault();
results._hi = Math.min((results._hi || 0) + 1, items.length - 1);
results.querySelectorAll('.cmd-item').forEach((el, i) => el.classList.toggle('highlighted', i === results._hi));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
results._hi = Math.max((results._hi || 0) - 1, 0);
results.querySelectorAll('.cmd-item').forEach((el, i) => el.classList.toggle('highlighted', i === results._hi));
} else if (e.key === 'Enter') {
e.preventDefault();
runHighlightedCmd();
} else if (e.key === 'Escape') {
closeCmd();
}
});
document.getElementById('cmdResults').addEventListener('click', (e) => {
const item = e.target.closest('[data-cmd-idx]');
if (!item) return;
const items = document.getElementById('cmdResults')._items || [];
const idx = +item.dataset.cmdIdx;
if (items[idx]) items[idx].action();
});
// Context menu actions
document.getElementById('contextMenu').addEventListener('click', (e) => {
const item = e.target.closest('[data-ctx]');
if (!item) return;
const menu = document.getElementById('contextMenu');
const fn = menu._actions && menu._actions[item.dataset.ctx];
hideContextMenu();
if (fn) fn();
});
document.addEventListener('click', hideContextMenu);
document.addEventListener('scroll', hideContextMenu, true);
// Global shortcuts
document.addEventListener('keydown', (e) => {
// Don't capture when typing in inputs except our intentional shortcuts
const inField = e.target.matches('input, textarea') || e.target.isContentEditable;
if (e.key === 'Escape') {
closeCmd();
document.querySelectorAll('.modal-overlay.open').forEach(m => m.classList.remove('open'));
if (state.focusMode) toggleFocus();
return;
}
if (isMod(e) && e.key.toLowerCase() === 'k') {
e.preventDefault();
openCmd();
return;
}
if (isMod(e) && e.key.toLowerCase() === 'n' && !e.shiftKey) {
e.preventDefault();
createNote();
return;
}
if (isMod(e) && e.key === '.') {
e.preventDefault();
toggleFocus();
return;
}
// Formatting shortcuts when in content
if (inField && e.target.id === 'noteContent') {
if (isMod(e) && e.key.toLowerCase() === 'b') {
e.preventDefault();
document.execCommand('bold');
updateNote(contentEl.dataset.noteId, { content: contentEl.innerHTML });
}
if (isMod(e) && e.key.toLowerCase() === 'i') {
e.preventDefault();
document.execCommand('italic');
updateNote(contentEl.dataset.noteId, { content: contentEl.innerHTML });
}
if (isMod(e) && e.key.toLowerCase() === 'u') {
e.preventDefault();
document.execCommand('underline');
updateNote(contentEl.dataset.noteId, { content: contentEl.innerHTML });
}
}
});
// Responsive back button visibility
function updateMobileUI() {
const back = document.getElementById('btnBackList');
back.style.display = window.innerWidth <= 640 ? '' : 'none';
}
window.addEventListener('resize', updateMobileUI);
updateMobileUI();
// Prefer dark if system wants it and no stored preference on first run
if (!localStorage.getItem(STORAGE_KEY) && window.matchMedia('(prefers-color-scheme: dark)').matches) {
state.theme = 'dark';
}
}
// ─── Init ───
load();
bindEvents();
fullRender();
})();
</script>
</body>
</html>