Notes
Qwen 3.6 35b a3b · composer

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 39,590 bytes · SHA-256 23a1c911e95f
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Noteflow — Beautiful Notes</title>
<style>
/* ── css: layout ── */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #f0f1f3;
--bg-sidebar: #1e1e2e;
--bg-sidebar-hover: #2a2a3e;
--bg-sidebar-active: #3b82f6;
--bg-toolbar: #ffffff;
--bg-editor: #ffffff;
--bg-empty: #f8f9fa;
--bg-context: #ffffff;
--bg-context-hover: #f0f1f3;
--bg-notebook: #2a2a3e;
--bg-notebook-hover: #353550;
--text-primary: #1a1a2e;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--text-sidebar: #c4c4d4;
--text-sidebar-active: #ffffff;
--text-toolbar: #374151;
--text-empty: #9ca3af;
--border-color: #e5e7eb;
--border-sidebar: #2a2a3e;
--border-toolbar: #e5e7eb;
--accent: #3b82f6;
--accent-hover: #2563eb;
--accent-light: #dbeafe;
--danger: #ef4444;
--danger-hover: #dc2626;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 12px rgba(0,0,0,0.1);
--shadow-lg: 0 8px 30px rgba(0,0,0,0.12);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--transition: 0.2s ease;
}
[data-theme="dark"] {
--bg-primary: #0f0f1a;
--bg-secondary: #1a1a2e;
--bg-tertiary: #252540;
--bg-sidebar: #0a0a14;
--bg-sidebar-hover: #1a1a2e;
--bg-sidebar-active: #3b82f6;
--bg-toolbar: #1a1a2e;
--bg-editor: #0f0f1a;
--bg-empty: #1a1a2e;
--bg-context: #1e1e2e;
--bg-context-hover: #2a2a3e;
--bg-notebook: #1a1a2e;
--bg-notebook-hover: #252540;
--text-primary: #e5e7eb;
--text-secondary: #9ca3af;
--text-tertiary: #6b7280;
--text-sidebar: #9ca3af;
--text-sidebar-active: #ffffff;
--text-toolbar: #d1d5db;
--text-empty: #6b7280;
--border-color: #2a2a3e;
--border-sidebar: #1a1a2e;
--border-toolbar: #2a2a3e;
--accent: #3b82f6;
--accent-hover: #60a5fa;
--accent-light: #1e3a5f;
--danger: #ef4444;
--danger-hover: #f87171;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
--shadow-md: 0 4px 12px rgba(0,0,0,0.3);
--shadow-lg: 0 8px 30px rgba(0,0,0,0.4);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
display: flex;
height: 100vh;
width: 100vw;
background: var(--bg-primary);
color: var(--text-primary);
}
/* ── Sidebar ── */
#sidebar {
width: 260px;
min-width: 260px;
background: var(--bg-sidebar);
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-sidebar);
overflow: hidden;
user-select: none;
}
#sidebar-header {
padding: 16px 16px 12px;
}
#logo {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-sidebar-active);
font-size: 18px;
font-weight: 700;
letter-spacing: -0.3px;
}
#logo svg {
opacity: 0.9;
}
#sidebar-search {
padding: 0 12px 12px;
}
#sidebar-search svg {
position: absolute;
left: 24px;
top: 50%;
transform: translateY(-50%);
color: var(--text-tertiary);
pointer-events: none;
}
#search-input {
width: 100%;
padding: 8px 12px 8px 34px;
background: var(--bg-notebook);
border: none;
border-radius: var(--radius-sm);
color: var(--text-sidebar);
font-size: 13px;
outline: none;
transition: background var(--transition);
}
#search-input::placeholder {
color: var(--text-tertiary);
}
#search-input:focus {
background: var(--bg-notebook-hover);
}
#notebooks-section, #notes-section {
flex-shrink: 0;
}
#notebooks-section {
padding: 0 12px 8px;
}
#notes-section {
padding: 0 12px 8px;
flex: 1;
overflow-y: auto;
min-height: 0;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
margin-bottom: 2px;
}
.section-header span {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-tertiary);
}
.icon-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
border-radius: var(--radius-sm);
color: var(--text-tertiary);
cursor: pointer;
transition: all var(--transition);
}
.icon-btn:hover {
background: var(--bg-notebook-hover);
color: var(--text-sidebar-active);
}
#notebook-list, #note-list {
display: flex;
flex-direction: column;
gap: 1px;
}
.notebook-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition);
color: var(--text-sidebar);
font-size: 13px;
}
.notebook-item:hover {
background: var(--bg-notebook-hover);
}
.notebook-item.active {
background: var(--bg-sidebar-active);
color: var(--text-sidebar-active);
}
.notebook-item.active svg {
opacity: 1;
}
.notebook-count {
margin-left: auto;
font-size: 11px;
opacity: 0.5;
}
.note-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition);
border-left: 3px solid transparent;
}
.note-item:hover {
background: var(--bg-notebook-hover);
}
.note-item.active {
background: var(--bg-sidebar-active);
}
.note-item.active .note-item-title,
.note-item.active .note-item-preview,
.note-item.active .note-item-date {
color: var(--text-sidebar-active);
}
.note-item.active .note-item-date {
color: rgba(255,255,255,0.6);
}
.note-item-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
margin-top: 2px;
background: var(--bg-notebook);
}
.note-item.active .note-item-icon {
background: rgba(255,255,255,0.15);
}
.note-item-content {
flex: 1;
min-width: 0;
}
.note-item-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
.note-item-preview {
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 3px;
}
.note-item-date {
font-size: 11px;
color: var(--text-tertiary);
}
#sidebar-footer {
padding: 12px;
border-top: 1px solid var(--border-sidebar);
display: flex;
justify-content: center;
}
/* ── Main area ── */
#main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
background: var(--bg-primary);
}
/* ── Toolbar ── */
#toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
background: var(--bg-toolbar);
border-bottom: 1px solid var(--border-toolbar);
flex-wrap: wrap;
box-shadow: var(--shadow-sm);
z-index: 10;
}
#toolbar.hidden {
display: none;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 2px;
}
.toolbar-divider {
width: 1px;
height: 24px;
background: var(--border-toolbar);
margin: 0 6px;
}
.toolbar-btn {
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
border-radius: var(--radius-sm);
color: var(--text-toolbar);
cursor: pointer;
transition: all var(--transition);
position: relative;
}
.toolbar-btn:hover {
background: var(--bg-tertiary);
}
.toolbar-btn:active {
transform: scale(0.95);
}
.toolbar-btn.active {
background: var(--accent-light);
color: var(--accent);
}
.toolbar-select {
padding: 6px 10px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-toolbar);
font-size: 13px;
cursor: pointer;
outline: none;
transition: all var(--transition);
}
.toolbar-select:hover {
border-color: var(--accent);
}
/* ── Editor container ── */
#editor-container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
#empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
color: var(--text-empty);
padding: 40px;
}
#empty-state svg {
opacity: 0.15;
}
#empty-state p {
font-size: 20px;
font-weight: 600;
color: var(--text-secondary);
}
#empty-state span {
font-size: 14px;
color: var(--text-tertiary);
}
/* ── Editor wrapper ── */
#editor-wrapper {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 0;
}
#editor-wrapper.hidden {
display: none;
}
#note-title {
padding: 40px 80px 0;
font-size: 32px;
font-weight: 700;
outline: none;
border: none;
background: transparent;
color: var(--text-primary);
letter-spacing: -0.5px;
line-height: 1.2;
width: 100%;
max-width: 800px;
margin: 0 auto;
}
#note-title::placeholder {
color: var(--text-tertiary);
}
#note-title:focus {
color: var(--text-primary);
}
#note-meta {
padding: 8px 80px 0;
display: flex;
gap: 16px;
font-size: 12px;
color: var(--text-tertiary);
max-width: 800px;
margin: 0 auto;
}
#editor {
flex: 1;
padding: 20px 80px 100px;
outline: none;
border: none;
background: transparent;
color: var(--text-primary);
font-size: 15px;
line-height: 1.75;
max-width: 800px;
margin: 0 auto;
width: 100%;
overflow-wrap: break-word;
}
#editor::placeholder {
color: var(--text-tertiary);
}
#editor:focus {
outline: none;
}
#editor h1 { font-size: 2em; font-weight: 700; margin: 0.5em 0 0.3em; letter-spacing: -0.5px; }
#editor h2 { font-size: 1.5em; font-weight: 600; margin: 0.5em 0 0.3em; letter-spacing: -0.3px; }
#editor h3 { font-size: 1.25em; font-weight: 600; margin: 0.4em 0 0.2em; }
#editor p { margin: 0.3em 0; }
#editor ul, #editor ol { padding-left: 1.5em; margin: 0.3em 0; }
#editor blockquote {
border-left: 3px solid var(--accent);
padding-left: 16px;
margin: 0.5em 0;
color: var(--text-secondary);
font-style: italic;
}
#editor code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
font-family: 'SF Mono', 'Fira Code', monospace;
}
#editor pre {
background: var(--bg-tertiary);
padding: 16px;
border-radius: var(--radius-sm);
overflow-x: auto;
margin: 0.5em 0;
}
#editor pre code {
background: none;
padding: 0;
}
#editor a { color: var(--accent); }
#editor hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 1.5em 0;
}
/* ── Context menu ── */
#context-menu {
position: fixed;
background: var(--bg-context);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
padding: 4px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 160px;
}
#context-menu.hidden {
display: none;
}
#context-menu button {
padding: 8px 12px;
background: none;
border: none;
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 13px;
cursor: pointer;
text-align: left;
transition: background var(--transition);
display: flex;
align-items: center;
gap: 8px;
}
#context-menu button:hover {
background: var(--bg-context-hover);
}
#context-menu button.danger {
color: var(--danger);
}
#context-menu button.danger:hover {
background: rgba(239, 68, 68, 0.1);
}
/* ── Scrollbar ── */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* ── Animations ── */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
#note-list .note-item {
animation: slideIn 0.2s ease forwards;
}
</style>
</head>
<body>
<div></div>
<div id="app">
<div id="sidebar">
<div id="sidebar-header">
<div id="logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
<span>Noteflow</span>
</div>
</div>
<div id="sidebar-search">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="search-input" placeholder="Search notes...">
</div>
<div id="notebooks-section">
<div class="section-header">
<span>Notebooks</span>
<button id="add-notebook-btn" class="icon-btn" title="New notebook">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</button>
</div>
<div id="notebook-list"></div>
</div>
<div id="notes-section">
<div class="section-header">
<span>Notes</span>
<button id="add-note-btn" class="icon-btn" title="New note">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</button>
</div>
<div id="note-list"></div>
</div>
<div id="sidebar-footer">
<button id="theme-toggle" class="icon-btn" title="Toggle theme">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
</button>
</div>
</div>
<div id="main">
<div id="toolbar" class="hidden">
<div class="toolbar-group">
<button class="toolbar-btn" data-action="bold" title="Bold (Ctrl+B)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path></svg></button>
<button class="toolbar-btn" data-action="italic" title="Italic (Ctrl+I)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="4" x2="10" y2="4"></line><line x1="14" y1="20" x2="5" y2="20"></line><line x1="15" y1="4" x2="9" y2="20"></line></svg></button>
<button class="toolbar-btn" data-action="underline" title="Underline (Ctrl+U)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"></path><line x1="4" y1="21" x2="20" y2="21"></line></svg></button>
<button class="toolbar-btn" data-action="strikeThrough" title="Strikethrough"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 4H9a3 3 0 0 0-3 3v0a3 3 0 0 0 3 3h6a3 3 0 0 1 3 3v0a3 3 0 0 1-3 3H8"></path><line x1="4" y1="12" x2="20" y2="12"></line></svg></button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-group">
<button class="toolbar-btn" data-action="insertUnorderedList" title="Bullet list"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><circle cx="4" cy="6" r="1" fill="currentColor"></circle><circle cx="4" cy="12" r="1" fill="currentColor"></circle><circle cx="4" cy="18" r="1" fill="currentColor"></circle></svg></button>
<button class="toolbar-btn" data-action="insertOrderedList" title="Numbered list"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="10" y1="6" x2="21" y2="6"></line><line x1="10" y1="12" x2="21" y2="12"></line><line x1="10" y1="18" x2="21" y2="18"></line><text x="3" y="8" font-size="7" fill="currentColor" stroke="none">1</text><text x="3" y="14" font-size="7" fill="currentColor" stroke="none">2</text><text x="3" y="20" font-size="7" fill="currentColor" stroke="none">3</text></svg></button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-group">
<select class="toolbar-select" id="heading-select" title="Heading level">
<option value="p">Paragraph</option>
<option value="h1">Heading 1</option>
<option value="h2">Heading 2</option>
<option value="h3">Heading 3</option>
</select>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-group">
<button class="toolbar-btn" data-action="foreColor" title="Text color" id="color-btn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3L3 21h18L12 3z" fill="#3B82F6" stroke="#2563EB"></path><path d="M9 9l3 3 3-3"></path></svg></button>
<input type="color" id="color-picker" value="#3B82F6" style="display:none">
<button class="toolbar-btn" data-action="hiliteColor" title="Highlight" id="hilite-btn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9" fill="#FBBF24" stroke="#F59E0B"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" fill="#FBBF24" stroke="#F59E0B"></path></svg></button>
<input type="color" id="hilite-picker" value="#FBBF24" style="display:none">
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-group">
<button class="toolbar-btn" data-action="removeFormat" title="Clear formatting"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M7 7v10M17 7v10"></path><line x1="18" y1="4" x2="6" y2="20" stroke-width="3"></line></svg></button>
</div>
</div>
<div id="editor-container">
<div id="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" opacity="0.3"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
<p>No note selected</p>
<span>Create a new note or select one from the sidebar</span>
</div>
<div id="editor-wrapper" class="hidden">
<div id="note-title" contenteditable="true" spellcheck="true"></div>
<div id="note-meta">
<span id="note-date"></span>
<span id="note-word-count"></span>
</div>
<div id="editor" contenteditable="true" spellcheck="true"></div>
</div>
</div>
</div>
<div id="context-menu" class="hidden">
<button data-action="rename">Rename</button>
<button data-action="duplicate">Duplicate</button>
<button data-action="delete" class="danger">Delete</button>
</div>
</div>
<script>
'use strict';
// ── function: generateId ──
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}
// ── function: formatDate ──
function formatDate(dateStr) {
const d = new Date(dateStr);
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 < 604800000) return Math.floor(diff / 86400000) + 'd ago';
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: d.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
}
// ── function: stripHtml ──
function stripHtml(html) {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
}
// ── function: getNotebookIcon ──
function getNotebookIcon(name) {
const icons = ['📓', '📔', '📒', '📕', '📗', '📘', '📙', '📚', '📁', '📂'];
let hash = 0;
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
return icons[Math.abs(hash) % icons.length];
}
// ── function: getWordCount ──
function getWordCount(html) {
const text = stripHtml(html).trim();
if (!text) return 0;
return text.split(/\s+/).length;
}
// ── const: STORAGE_KEY ──
const STORAGE_KEY = 'noteflow_data';
// ── function: loadStore ──
function loadStore() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return JSON.parse(raw);
} catch(e) {}
// Seed initial data
const notebooks = [
{ id: 'nb_personal', name: 'Personal' },
{ id: 'nb_work', name: 'Work' },
{ id: 'nb_ideas', name: 'Ideas' }
];
const notes = [
{
id: 'welcome_note',
title: 'Welcome to Noteflow ✨',
content: '<h1>Welcome to Noteflow</h1><p>Your beautiful, minimal notes app. Here are some things you can do:</p><ul><li><b>Create new notes</b> with the + button</li><li><b>Format text</b> using the toolbar</li><li><b>Organize</b> notes into notebooks</li><li><b>Search</b> across all your notes</li><li><b>Toggle theme</b> with the sun icon</li></ul><p><b>Keyboard shortcuts:</b></p><ul><li><b>Ctrl+N</b> — New note</li><li><b>Ctrl+F</b> — Search</li><li><b>Ctrl+B</b> — Bold</li><li><b>Ctrl+I</b> — Italic</li><li><b>Ctrl+U</b> — Underline</li></ul><p>Right-click any note for more options. All your data is saved locally in your browser.</p>',
notebookId: 'nb_personal',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
{
id: 'meeting_notes',
title: 'Meeting Notes — Q4 Planning',
content: '<h2>Agenda</h2><ol><li>Review Q3 results</li><li>Set Q4 objectives</li><li>Resource allocation</li></ol><h3>Key Decisions</h3><ul><li>Focus on user retention</li><li>Launch new feature set by November</li><li>Hire 2 additional engineers</li></ul><p><b>Action items:</b></p><ul><li>Prepare financial report — Sarah</li><li>Draft product roadmap — Mike</li><li>Update hiring plan — HR</li></ul>',
notebookId: 'nb_work',
createdAt: new Date(Date.now() - 3600000).toISOString(),
updatedAt: new Date(Date.now() - 3600000).toISOString()
},
{
id: 'recipe_note',
title: 'Recipe: Homemade Pasta',
content: '<h2>Ingredients</h2><ul><li>2 cups all-purpose flour</li><li>3 large eggs</li><li>1 tbsp olive oil</li><li>Semolina for dusting</li></ul><h2>Instructions</h2><p>1. Make a well with the flour on a clean surface.</p><p>2. Crack eggs into the well, add olive oil.</p><p>3. Gradually incorporate flour into eggs.</p><p>4. Knead for 10 minutes until smooth.</p><p>5. Rest for 30 minutes, then roll thin.</p><blockquote>Pro tip: Let the pasta dry for 15 minutes before cooking for better texture.</blockquote>',
notebookId: 'nb_personal',
createdAt: new Date(Date.now() - 86400000).toISOString(),
updatedAt: new Date(Date.now() - 86400000).toISOString()
},
{
id: 'app_idea',
title: 'App Idea: Habit Tracker',
content: '<h2>Concept</h2><p>A minimal habit tracker that focuses on <b>consistency over intensity</b>.</p><h2>Features</h2><ul><li>📊 Visual streak calendar</li><li>🔔 Gentle reminders</li><li>📈 Weekly/monthly analytics</li><li>🎯 Focus mode for deep work</li><li>🤝 Group challenges</li></ul><h2>Design Principles</h2><p>Keep it simple. No social features unless they add real value. Dark mode first. Offline capable.</p>',
notebookId: 'nb_ideas',
createdAt: new Date(Date.now() - 172800000).toISOString(),
updatedAt: new Date(Date.now() - 172800000).toISOString()
}
];
return { notes, notebooks, activeNoteId: null, activeNotebookId: null, theme: 'light' };
}
// ── function: saveStore ──
function saveStore(store) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
} catch(e) {}
}
// ── function: createNote ──
function createNote(title = 'Untitled Note', notebookId = null) {
const store = loadStore();
const note = {
id: generateId(),
title: title,
content: '',
notebookId: notebookId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
store.notes.unshift(note);
saveStore(store);
return note;
}
// ── function: updateNote ──
function updateNote(id, updates) {
const store = loadStore();
const idx = store.notes.findIndex(n => n.id === id);
if (idx === -1) return null;
store.notes[idx] = { ...store.notes[idx], ...updates, updatedAt: new Date().toISOString() };
saveStore(store);
return store.notes[idx];
}
// ── function: deleteNote ──
function deleteNote(id) {
const store = loadStore();
store.notes = store.notes.filter(n => n.id !== id);
saveStore(store);
}
// ── function: createNotebook ──
function createNotebook(name = 'New Notebook') {
const store = loadStore();
const notebook = { id: generateId(), name };
store.notebooks.push(notebook);
saveStore(store);
return notebook;
}
// ── function: deleteNotebook ──
function deleteNotebook(id) {
const store = loadStore();
store.notebooks = store.notebooks.filter(n => n.id !== id);
store.notes.forEach(n => { if (n.notebookId === id) n.notebookId = null; });
saveStore(store);
}
// ── function: duplicateNote ──
function duplicateNote(id) {
const store = loadStore();
const note = store.notes.find(n => n.id === id);
if (!note) return null;
const dup = { ...note, id: generateId(), title: note.title + ' (copy)', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
store.notes.unshift(dup);
saveStore(store);
return dup;
}
// ── function: getNotesByNotebook ──
function getNotesByNotebook(store, notebookId) {
if (!notebookId) return store.notes;
return store.notes.filter(n => n.notebookId === notebookId);
}
// ── function: searchNotes ──
function searchNotes(store, query) {
if (!query.trim()) return store.notes;
const q = query.toLowerCase();
return store.notes.filter(n => n.title.toLowerCase().includes(q) || stripHtml(n.content).toLowerCase().includes(q));
}
// ── let: currentNoteId ──
let currentNoteId = null;
// ── let: currentNotebookId ──
let currentNotebookId = null;
// ── let: searchQuery ──
let searchQuery = '';
// ── let: debounceTimer ──
let debounceTimer = null;
// ── function: renderNotebooks ──
function renderNotebooks(store) {
const list = document.getElementById('notebook-list');
const allLabel = currentNotebookId === null ? 'All Notes' : 'All';
const allActive = currentNotebookId === null ? 'active' : '';
list.innerHTML = `
<div class="notebook-item ${allActive}" data-notebook="all">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
<span>${allLabel}</span>
<span class="notebook-count">${store.notes.length}</span>
</div>
${store.notebooks.map(nb => `
<div class="notebook-item ${currentNotebookId === nb.id ? 'active' : ''}" data-notebook="${nb.id}">
<span>${getNotebookIcon(nb.name)}</span>
<span>${escapeHtml(nb.name)}</span>
<span class="notebook-count">${store.notes.filter(n => n.notebookId === nb.id).length}</span>
</div>
`).join('')}
`;
}
// ── function: renderNoteList ──
function renderNoteList(store) {
const list = document.getElementById('note-list');
let notes = currentNotebookId !== null ? getNotesByNotebook(store, currentNotebookId) : store.notes;
if (searchQuery) notes = searchNotes(store, searchQuery);
if (notes.length === 0) {
list.innerHTML = `<div style="padding: 20px; text-align: center; color: var(--text-tertiary); font-size: 13px;">No notes yet</div>`;
return;
}
list.innerHTML = notes.map(note => {
const preview = stripHtml(note.content).slice(0, 60) || 'Empty note';
const date = formatDate(note.updatedAt);
return `
<div class="note-item ${currentNoteId === note.id ? 'active' : ''}" data-note-id="${note.id}">
<div class="note-item-icon">📝</div>
<div class="note-item-content">
<div class="note-item-title">${escapeHtml(note.title) || 'Untitled'}</div>
<div class="note-item-preview">${escapeHtml(preview)}</div>
<div class="note-item-date">${date}</div>
</div>
</div>
`;
}).join('');
}
// ── function: selectNote ──
function selectNote(id) {
currentNoteId = id;
const store = loadStore();
const note = store.notes.find(n => n.id === id);
if (!note) return;
document.getElementById('empty-state').classList.add('hidden');
document.getElementById('editor-wrapper').classList.remove('hidden');
document.getElementById('toolbar').classList.remove('hidden');
document.getElementById('note-title').textContent = note.title;
document.getElementById('editor').innerHTML = note.content;
updateMeta(note);
renderNoteList(store);
}
// ── function: updateMeta ──
function updateMeta(note) {
const date = new Date(note.updatedAt).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
const words = getWordCount(note.content);
document.getElementById('note-date').textContent = date;
document.getElementById('note-word-count').textContent = words + ' word' + (words !== 1 ? 's' : '');
}
// ── function: escapeHtml ──
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ── function: deselectNote ──
function deselectNote() {
currentNoteId = null;
document.getElementById('empty-state').classList.remove('hidden');
document.getElementById('editor-wrapper').classList.add('hidden');
document.getElementById('toolbar').classList.add('hidden');
const store = loadStore();
renderNoteList(store);
}
// ── function: setTheme ──
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
const store = loadStore();
store.theme = theme;
saveStore(store);
}
// ── function: execCmd ──
function execCmd(command, value = null) {
document.execCommand(command, false, value);
document.getElementById('editor').focus();
}
// ── function: applyHeading ──
function applyHeading(tag) {
if (tag === 'p') {
document.execCommand('formatBlock', false, 'p');
} else {
document.execCommand('formatBlock', false, tag);
}
document.getElementById('editor').focus();
}
// ── function: updateToolbarState ──
function updateToolbarState() {
const actions = ['bold', 'italic', 'underline', 'strikeThrough', 'insertUnorderedList', 'insertOrderedList'];
actions.forEach(action => {
const btn = document.querySelector(`.toolbar-btn[data-action="${action}"]`);
if (btn) {
if (document.queryCommandState(action)) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
}
});
const block = document.queryCommandValue('formatBlock');
const select = document.getElementById('heading-select');
if (block) {
const tag = block.toLowerCase().replace(/[<>]/g, '');
if (['p', 'h1', 'h2', 'h3'].includes(tag)) {
select.value = tag;
}
}
}
// ── function: autoSave ──
function autoSave() {
if (!currentNoteId) return;
const title = document.getElementById('note-title').textContent.trim();
const content = document.getElementById('editor').innerHTML;
updateNote(currentNoteId, { title: title || 'Untitled Note', content });
const store = loadStore();
updateMeta(store.notes.find(n => n.id === currentNoteId));
renderNoteList(store);
}
// ── function: debouncedSave ──
function debouncedSave() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(autoSave, 300);
}
// ── main ──
// ── Initialize ──
let store = loadStore();
setTheme(store.theme || 'light');
renderNotebooks(store);
renderNoteList(store);
// ── Search ──
document.getElementById('search-input').addEventListener('input', (e) => {
searchQuery = e.target.value;
const s = loadStore();
renderNoteList(s);
});
// ── Add note ──
document.getElementById('add-note-btn').addEventListener('click', () => {
const s = loadStore();
const note = createNote('Untitled Note', currentNotebookId);
selectNote(note.id);
document.getElementById('note-title').focus();
});
// ── Add notebook ──
document.getElementById('add-notebook-btn').addEventListener('click', () => {
createNotebook('New Notebook');
const s = loadStore();
renderNotebooks(s);
renderNoteList(s);
});
// ── Notebook selection ──
document.getElementById('notebook-list').addEventListener('click', (e) => {
const item = e.target.closest('.notebook-item');
if (!item) return;
const notebookId = item.dataset.notebook;
currentNotebookId = notebookId === 'all' ? null : notebookId;
const s = loadStore();
renderNotebooks(s);
renderNoteList(s);
});
// ── Note selection ──
document.getElementById('note-list').addEventListener('click', (e) => {
const item = e.target.closest('.note-item');
if (!item) return;
selectNote(item.dataset.noteId);
});
// ── Note title edit ──
document.getElementById('note-title').addEventListener('input', debouncedSave);
// ── Editor content edit ──
document.getElementById('editor').addEventListener('input', () => {
debouncedSave();
updateToolbarState();
});
document.getElementById('editor').addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertText', false, ' ');
}
});
// ── Toolbar buttons ──
document.getElementById('toolbar').addEventListener('click', (e) => {
const btn = e.target.closest('.toolbar-btn');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'foreColor' || action === 'hiliteColor') {
const pickerId = action === 'foreColor' ? 'color-picker' : 'hilite-picker';
document.getElementById(pickerId).click();
return;
}
execCmd(action);
});
document.getElementById('color-picker').addEventListener('input', (e) => {
execCmd('foreColor', e.target.value);
});
document.getElementById('hilite-picker').addEventListener('change', (e) => {
execCmd('hiliteColor', e.target.value);
});
document.getElementById('heading-select').addEventListener('change', (e) => {
applyHeading(e.target.value);
updateToolbarState();
});
// ── Keyboard shortcuts ──
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
const s = loadStore();
const note = createNote('Untitled Note', currentNotebookId);
selectNote(note.id);
document.getElementById('note-title').focus();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
document.getElementById('search-input').focus();
}
if (e.key === 'Escape') {
hideContextMenu();
document.getElementById('search-input').blur();
}
});
// ── Context menu ──
let contextTarget = null;
document.getElementById('note-list').addEventListener('contextmenu', (e) => {
const item = e.target.closest('.note-item');
if (!item) return;
e.preventDefault();
contextTarget = item.dataset.noteId;
showContextMenu(e.clientX, e.clientY);
});
document.getElementById('notebook-list').addEventListener('contextmenu', (e) => {
const item = e.target.closest('.notebook-item');
if (!item || item.dataset.notebook === 'all') return;
e.preventDefault();
contextTarget = 'nb_' + item.dataset.notebook;
showContextMenu(e.clientX, e.clientY);
});
function showContextMenu(x, y) {
const menu = document.getElementById('context-menu');
menu.classList.remove('hidden');
menu.style.left = Math.min(x, window.innerWidth - 180) + 'px';
menu.style.top = Math.min(y, window.innerHeight - 120) + 'px';
}
function hideContextMenu() {
document.getElementById('context-menu').classList.add('hidden');
}
document.addEventListener('click', (e) => {
if (!e.target.closest('#context-menu')) hideContextMenu();
});
document.getElementById('context-menu').addEventListener('click', (e) => {
const btn = e.target.closest('button');
if (!btn) return;
const action = btn.dataset.action;
if (contextTarget && !contextTarget.startsWith('nb_')) {
const s = loadStore();
if (action === 'delete') {
deleteNote(contextTarget);
if (currentNoteId === contextTarget) deselectNote();
renderNoteList(s);
}
if (action === 'duplicate') {
const dup = duplicateNote(contextTarget);
if (dup) selectNote(dup.id);
renderNoteList(s);
}
if (action === 'rename') {
const note = s.notes.find(n => n.id === contextTarget);
if (note) {
const newName = prompt('Rename note:', note.title);
if (newName !== null) {
updateNote(contextTarget, { title: newName });
renderNoteList(s);
}
}
}
}
if (contextTarget && contextTarget.startsWith('nb_')) {
const nbId = contextTarget.replace('nb_', '');
const s = loadStore();
if (action === 'delete') {
deleteNotebook(nbId);
currentNotebookId = null;
renderNotebooks(s);
renderNoteList(s);
}
if (action === 'rename') {
const nb = s.notebooks.find(n => n.id === nbId);
if (nb) {
const newName = prompt('Rename notebook:', nb.name);
if (newName !== null) {
const idx = s.notebooks.findIndex(n => n.id === nbId);
if (idx !== -1) {
s.notebooks[idx].name = newName;
saveStore(s);
}
renderNotebooks(s);
renderNoteList(s);
}
}
}
}
hideContextMenu();
});
// ── Theme toggle ──
document.getElementById('theme-toggle').addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
setTheme(current === 'dark' ? 'light' : 'dark');
const s = loadStore();
renderNotebooks(s);
});
// ── Init with first note or empty state ──
const s = loadStore();
if (s.notes.length > 0) {
selectNote(s.notes[0].id);
} else {
deselectNote();
}
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"7b52134e-144e-42bf-bbcb-6bb58cbd63dc","tokensIn":771443,"tokensOut":29862,"tokensTotal":801305,"turns":31,"toolCalls":30,"failedToolCalls":2,"timestamp":"2026-07-20T22:14:36.761Z"} -->