Notes
thinkingcap qwen3.6 27b · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 34,204 bytes · SHA-256 b7d01670369f
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NoteFlow — Notes App</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg-primary:#ffffff;--bg-secondary:#f8f9fa;--bg-tertiary:#eef1f5;
--bg-hover:#e2e6ea;--bg-active:#d4d9df;--bg-sidebar:#fafbfc;
--text-primary:#1a1a2e;--text-secondary:#555770;--text-tertiary:#8b8da3;
--accent:#6c5ce7;--accent-light:#a29bfe;--accent-bg:rgba(108,92,231,.08);
--border:#e4e7ec;--shadow:0 2px 12px rgba(0,0,0,.06);
--radius:8px;--radius-lg:12px;
--font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
--font-mono:'JetBrains Mono','Fira Code',monospace;
--sidebar-w:260px;--note-list-w:280px;
--transition:180ms ease;
}
[data-theme="dark"]{
--bg-primary:#1a1b2e;--bg-secondary:#232439;--bg-tertiary:#2c2d45;
--bg-hover:#363750;--bg-active:#404160;--bg-sidebar:#18192a;
--text-primary:#e8e8f0;--text-secondary:#a0a0b8;--text-tertiary:#6e6e88;
--accent:#a29bfe;--accent-light:#c4bfff;--accent-bg:rgba(162,155,254,.1);
--border:#333450;--shadow:0 2px 12px rgba(0,0,0,.25);
}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
html,body{height:100%;font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);overflow:hidden;font-size:14px}
/* ── Layout ── */
.app{display:flex;height:100vh;width:100vw;position:relative}
/* ── Sidebar (Folders) ── */
.sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--bg-sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;transition:min-width .25s ease,width .25s ease,opacity .2s;z-index:20}
.sidebar.collapsed{width:0;min-width:0;overflow:hidden;border:none}
.sidebar-header{padding:16px;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
.logo{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px;color:var(--accent);white-space:nowrap}
.logo svg{flex-shrink:0}
.sidebar-toggle{margin-left:auto;background:none;border:none;color:var(--text-tertiary);cursor:pointer;padding:4px;border-radius:4px;display:flex;align-items:center}
.sidebar-toggle:hover{background:var(--bg-hover)}
.search-box{padding:12px 16px}
.search-input{width:100%;padding:8px 12px 8px 34px;border-radius:var(--radius);border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:13px;outline:none;transition:border var(--transition)}
.search-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)}
.search-wrap{position:relative}
.search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-tertiary);pointer-events:none}
.folder-list{flex:1;overflow-y:auto;padding:4px 8px}
.folder-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--radius);cursor:pointer;font-size:13px;color:var(--text-secondary);transition:background var(--transition),color var(--transition);user-select:none;white-space:nowrap}
.folder-item:hover{background:var(--bg-hover)}
.folder-item.active{background:var(--accent-bg);color:var(--accent);font-weight:600}
.folder-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:14px}
.folder-count{margin-left:auto;font-size:11px;color:var(--text-tertiary);background:var(--bg-tertiary);padding:1px 7px;border-radius:10px}
.sidebar-footer{padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:4px}
.footer-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:8px;border:none;background:var(--bg-secondary);color:var(--text-secondary);border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:500;transition:all var(--transition)}
.footer-btn:hover{background:var(--bg-hover);color:var(--text-primary)}
/* ── Note List Panel ── */
.note-list-panel{width:var(--note-list-w);min-width:var(--note-list-w);border-right:1px solid var(--border);display:flex;flex-direction:column;background:var(--bg-secondary);transition:min-width .25s ease,width .25s ease,opacity .2s;z-index:10}
.note-list-panel.collapsed{width:0;min-width:0;overflow:hidden;border:none}
.note-list-header{padding:16px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border)}
.note-list-title{font-weight:600;font-size:14px;color:var(--text-primary)}
.new-note-btn{background:var(--accent);color:#fff;border:none;padding:6px 14px;border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:600;display:flex;align-items:center;gap:5px;transition:all var(--transition)}
.new-note-btn:hover{background:var(--accent-light);transform:translateY(-1px);box-shadow:0 4px 12px rgba(108,92,231,.3)}
.note-list{flex:1;overflow-y:auto;padding:6px}
.note-card{padding:12px;border-radius:var(--radius-lg);cursor:pointer;margin-bottom:4px;transition:all var(--transition);border:1px solid transparent}
.note-card:hover{background:var(--bg-hover)}
.note-card.active{background:var(--accent-bg);border-color:var(--accent);box-shadow:0 2px 8px rgba(108,92,231,.1)}
.note-card-title{font-weight:600;font-size:13px;margin-bottom:4px;color:var(--text-primary);line-height:1.3;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
.note-card-preview{font-size:12px;color:var(--text-tertiary);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-bottom:6px}
.note-card-meta{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text-tertiary)}
.note-tag{background:var(--bg-tertiary);padding:1px 6px;border-radius:4px;font-size:10px}
/* ── Editor Area ── */
.editor-area{flex:1;display:flex;flex-direction:column;background:var(--bg-primary);min-width:0}
.toolbar{padding:8px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:4px;background:var(--bg-primary);flex-shrink:0;overflow-x:auto}
.tool-btn{background:none;border:none;color:var(--text-secondary);cursor:pointer;padding:6px 8px;border-radius:6px;display:flex;align-items:center;justify-content:center;transition:all var(--transition);font-size:13px;min-width:32px;height:32px}
.tool-btn:hover{background:var(--bg-hover);color:var(--text-primary)}
.tool-btn.active{background:var(--accent-bg);color:var(--accent)}
.tool-sep{width:1px;height:20px;background:var(--border);margin:0 4px;flex-shrink:0}
.toolbar-spacer{flex:1}
.editor-title-area{padding:28px 40px 8px;flex-shrink:0}
.editor-title{font-size:28px;font-weight:700;color:var(--text-primary);border:none;background:none;width:100%;outline:none;line-height:1.3;resize:none;font-family:var(--font-sans)}
.editor-title::placeholder{color:var(--text-tertiary)}
.editor-meta{padding:0 40px 8px;display:flex;gap:12px;font-size:12px;color:var(--text-tertiary);align-items:center}
.meta-tag{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;background:var(--bg-tertiary);border-radius:4px;cursor:pointer;transition:all var(--transition)}
.meta-tag:hover{background:var(--bg-hover)}
.editor-content-wrap{flex:1;overflow-y:auto;padding:0 40px 60px}
.editor-content{outline:none;font-size:15px;line-height:1.75;color:var(--text-primary);min-height:200px}
.editor-content h1{font-size:24px;margin:16px 0 8px;font-weight:700}
.editor-content h2{font-size:20px;margin:14px 0 6px;font-weight:600}
.editor-content h3{font-size:17px;margin:12px 0 4px;font-weight:600}
.editor-content p{margin-bottom:8px}
.editor-content ul,.editor-content ol{padding-left:24px;margin-bottom:8px}
.editor-content blockquote{border-left:3px solid var(--accent);padding-left:16px;color:var(--text-secondary);margin:8px 0;font-style:italic}
.editor-content code{background:var(--bg-tertiary);padding:2px 6px;border-radius:4px;font-family:var(--font-mono);font-size:13px}
.editor-content pre{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:12px;margin:8px 0;overflow-x:auto}
.editor-content pre code{background:none;padding:0}
.editor-content a{color:var(--accent)}
.editor-content hr{border:none;border-top:1px solid var(--border);margin:16px 0}
/* ── Empty State ── */
.empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text-tertiary);gap:12px;padding:40px}
.empty-icon{font-size:64px;opacity:.3}
.empty-title{font-size:18px;font-weight:600;color:var(--text-secondary)}
.empty-desc{font-size:13px;text-align:center;max-width:280px;line-height:1.5}
/* ── Modal / Dialog ── */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
.modal-overlay.show{opacity:1;pointer-events:auto}
.modal{background:var(--bg-primary);border-radius:var(--radius-lg);padding:24px;width:90%;max-width:400px;box-shadow:0 20px 60px rgba(0,0,0,.2);transform:scale(.95);transition:transform .2s}
.modal-overlay.show .modal{transform:scale(1)}
.modal h3{margin-bottom:16px;font-size:16px}
.modal input[type="text"]{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-secondary);color:var(--text-primary);font-size:14px;outline:none;margin-bottom:16px}
.modal input:focus{border-color:var(--accent)}
.modal-actions{display:flex;gap:8px;justify-content:flex-end}
.modal-btn{padding:8px 20px;border-radius:var(--radius);border:none;cursor:pointer;font-size:13px;font-weight:600;transition:all var(--transition)}
.modal-btn.primary{background:var(--accent);color:#fff}
.modal-btn.primary:hover{background:var(--accent-light)}
.modal-btn.secondary{background:var(--bg-tertiary);color:var(--text-secondary)}
/* ── Context Menu ── */
.context-menu{position:fixed;background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 8px 30px rgba(0,0,0,.15);padding:4px;z-index:200;min-width:160px;display:none}
.context-menu.show{display:block}
.ctx-item{padding:7px 12px;font-size:13px;color:var(--text-secondary);cursor:pointer;border-radius:4px;display:flex;align-items:center;gap:8px;transition:background var(--transition)}
.ctx-item:hover{background:var(--bg-hover);color:var(--text-primary)}
.ctx-item.danger{color:#e74c3c}
.ctx-sep{height:1px;background:var(--border);margin:4px 0}
/* ── Toast ── */
.toast-container{position:fixed;bottom:20px;right:20px;z-index:300;display:flex;flex-direction:column;gap:8px}
.toast{padding:12px 20px;background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);font-size:13px;color:var(--text-primary);display:flex;align-items:center;gap:8px;animation:toastIn .3s ease,toastOut .3s ease 2.7s forwards}
@keyframes toastIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes toastOut{to{opacity:0;transform:translateX(20px)}}
/* ── Scrollbar ── */
::-webkit-scrollbar{width:6px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--bg-active);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--text-tertiary)}
/* ── Mobile Toggle ── */
.mobile-toggle{display:none;position:fixed;top:12px;left:12px;z-index:50;background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);padding:8px;cursor:pointer;color:var(--text-secondary)}
@media(max-width:768px){
.sidebar{position:absolute;left:0;top:0;height:100%;box-shadow:none}
.note-list-panel{position:absolute;left:var(--sidebar-w);top:0;height:100%}
.mobile-toggle{display:flex}
}
/* ── Animations ── */
@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
.note-card{animation:fadeIn .2s ease}
</style>
</head>
<body>
<div class="app" id="app">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><rect x="3" y="2" width="18" height="20" rx="3" stroke="currentColor" stroke-width="2"/><path d="M8 7h8M8 11h5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
NoteFlow
</div>
<button class="sidebar-toggle" onclick="toggleSidebar()" title="Toggle sidebar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
</div>
<div class="search-box">
<div class="search-wrap">
<span class="search-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg></span>
<input class="search-input" type="text" placeholder="Search notes…" id="searchInput" oninput="filterNotes()">
</div>
</div>
<div class="folder-list" id="folderList"></div>
<div class="sidebar-footer">
<button class="footer-btn" onclick="openNewFolderModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
New Folder
</button>
<button class="footer-btn" onclick="toggleTheme()" title="Toggle theme">
<span id="themeIcon">🌙</span>
</button>
</div>
</aside>
<!-- Note List -->
<div class="note-list-panel" id="noteListPanel">
<div class="note-list-header">
<span class="note-list-title" id="folderTitle">All Notes</span>
<button class="new-note-btn" onclick="createNote()">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M12 5v14M5 12h14"/></svg>
New Note
</button>
</div>
<div class="note-list" id="noteList"></div>
</div>
<!-- Editor -->
<main class="editor-area" id="editorArea">
<div class="empty-state" id="emptyState">
<div class="empty-icon">📝</div>
<div class="empty-title">No note selected</div>
<div class="empty-desc">Select a note from the sidebar or create a new one to get started.</div>
</div>
<div id="editorContainer" style="display:none;flex:1;display:none;flex-direction:column;">
<div class="toolbar" id="toolbar">
<button class="tool-btn" onclick="execCmd('bold')" title="Bold (Ctrl+B)"><b>B</b></button>
<button class="tool-btn" onclick="execCmd('italic')" title="Italic (Ctrl+I)"><i>I</i></button>
<button class="tool-btn" onclick="execCmd('underline')" title="Underline (Ctrl+U)" style="text-decoration:underline">U</button>
<button class="tool-btn" onclick="execCmd('strikeThrough')" title="Strikethrough" style="text-decoration:line-through">S</button>
<div class="tool-sep"></div>
<button class="tool-btn" onclick="execHeading('h1')" title="Heading 1">H1</button>
<button class="tool-btn" onclick="execHeading('h2')" title="Heading 2">H2</button>
<button class="tool-btn" onclick="execHeading('h3')" title="Heading 3">H3</button>
<div class="tool-sep"></div>
<button class="tool-btn" onclick="execCmd('insertUnorderedList')" title="Bullet list">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="5" cy="6" r="1.5" fill="currentColor"/><circle cx="5" cy="12" r="1.5" fill="currentColor"/><circle cx="5" cy="18" r="1.5" fill="currentColor"/></svg>
</button>
<button class="tool-btn" onclick="execCmd('insertOrderedList')" title="Numbered list">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="10" y1="6" x2="20" y2="6"/><line x1="10" y1="12" x2="20" y2="12"/><line x1="10" y1="18" x2="20" y2="18"/></svg>
</button>
<div class="tool-sep"></div>
<button class="tool-btn" onclick="execCmd('formatBlock','<blockquote>')" title="Quote">❝</button>
<button class="tool-btn" onclick="insertCode()" title="Inline code"></></button>
<button class="tool-btn" onclick="execCmd('insertHorizontalRule')" title="Divider">—</button>
<div class="toolbar-spacer"></div>
<span id="wordCount" style="font-size:11px;color:var(--text-tertiary)"></span>
</div>
<div class="editor-title-area">
<input class="editor-title" type="text" id="noteTitleInput" placeholder="Untitled note…" oninput="updateNoteTitle()">
</div>
<div class="editor-meta" id="editorMeta"></div>
<div class="editor-content-wrap">
<div class="editor-content" id="editorContent" contenteditable="true" spellcheck="true" oninput="onEditorInput()" onkeydown="handleEditorKeys(event)"></div>
</div>
</div>
</main>
</div>
<!-- Context Menu -->
<div class="context-menu" id="contextMenu">
<div class="ctx-item" onclick="duplicateNote()">📋 Duplicate</div>
<div class="ctx-item" onclick="moveNoteFolder()">📁 Move to…</div>
<div class="ctx-sep"></div>
<div class="ctx-item danger" onclick="deleteCurrentNote()">🗑 Delete</div>
</div>
<!-- New Folder Modal -->
<div class="modal-overlay" id="folderModal">
<div class="modal">
<h3>New Folder</h3>
<input type="text" id="newFolderName" placeholder="Folder name…" onkeydown="if(event.key==='Enter')saveNewFolder()">
<div class="modal-actions">
<button class="modal-btn secondary" onclick="closeModal('folderModal')">Cancel</button>
<button class="modal-btn primary" onclick="saveNewFolder()">Create</button>
</div>
</div>
</div>
<!-- Move Note Modal -->
<div class="modal-overlay" id="moveNoteModal">
<div class="modal">
<h3>Move to Folder</h3>
<div id="moveFolderList" style="max-height:200px;overflow-y:auto;margin-bottom:16px"></div>
<div class="modal-actions">
<button class="modal-btn secondary" onclick="closeModal('moveNoteModal')">Cancel</button>
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<script>
// ── Data Layer ──
const STORAGE_KEY = 'noteflow_data';
let state = {
folders: [
{ id: 'personal', name: 'Personal', icon: '📁' },
{ id: 'work', name: 'Work', icon: '💼' },
{ id: 'ideas', name: 'Ideas', icon: '💡' },
],
notes: [
{
id: 'n1', title: 'Welcome to NoteFlow ✨', folderId: 'personal',
content: '<h2>Welcome!</h2><p>NoteFlow is a beautiful, minimal notes application. Here are some things you can do:</p><ul><li>Create and organize notes into folders</li><li>Format text with the toolbar above</li><li>Search through all your notes</li><li>Toggle dark mode for comfortable reading</li></ul><blockquote>Your data is saved automatically in your browser.</blockquote><h3>Getting Started</h3><p>Click <strong>New Note</strong> to create your first note, or select one from the sidebar. Everything you type is auto-saved!</p>',
tags: ['welcome', 'getting-started'], created: Date.now() - 86400000, updated: Date.now() - 3600000
},
{
id: 'n2', title: 'Meeting Notes — Q4 Planning', folderId: 'work',
content: '<h2>Q4 Planning Meeting</h2><p><strong>Date:</strong> October 15, 2024</p><ol><li>Review Q3 results and key metrics</li><li>Set OKRs for Q4</li><li>Budget allocation discussion</li></ol><p><strong>Action items:</strong></p><ul><li>Draft proposal by Friday</li><li>Schedule follow-up with design team</li><li>Prepare budget spreadsheet</li></ul>',
tags: ['meeting', 'planning'], created: Date.now() - 172800000, updated: Date.now() - 7200000
},
{
id: 'n3', title: 'App Ideas Backlog', folderId: 'ideas',
content: '<h2>App Ideas</h2><p>A collection of ideas to explore:</p><h3>1. Habit Tracker</h3><p>A minimal habit tracking app with streaks and gentle reminders.</p><h3>2. Recipe Manager</h3><p>Organize recipes by cuisine, difficulty, and ingredients. Auto-generate shopping lists.</p><h3>3. Reading List</h3><p>Track books you want to read, currently reading, and finished. Rate and review.</p>',
tags: ['ideas', 'apps'], created: Date.now() - 259200000, updated: Date.now() - 86400000
},
{
id: 'n4', title: 'Weekly Journal — Week 42', folderId: 'personal',
content: '<p>Great week overall. Finished the project presentation and got positive feedback from the team.</p><p>Started reading "Atomic Habits" — really enjoying it so far. Key takeaway: small changes compound over time.</p><hr><p><em>Gratitude:</em></p><ul><li>Good health this week</li><li>Creative flow on Thursday</li><li>Nice conversation with an old friend</li></ul>',
tags: ['journal', 'weekly'], created: Date.now() - 345600000, updated: Date.now() - 172800000
},
{
id: 'n5', title: 'Design System Notes', folderId: 'work',
content: '<h2>Design System</h2><p>Key principles for our design system:</p><ul><li><strong>Consistency</strong> — Same patterns across all components</li><li><strong>Accessibility</strong> — WCAG 2.1 AA minimum</li><li><strong>Flexibility</strong> — Adaptable to different contexts</li></ul><h3>Color Palette</h3><p>Primary: <code>#6c5ce7</code>, Secondary: <code>#00b894</code>, Accent: <code>#fdcb6e</code></p>',
tags: ['design', 'system'], created: Date.now() - 432000000, updated: Date.now() - 259200000
}
],
activeFolder: null, // null = all notes
activeNoteId: null,
theme: 'light'
};
function loadState() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
state = { ...state, ...parsed };
}
} catch(e) {}
}
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch(e) {}
}
// ── Rendering ──
function renderFolders() {
const list = document.getElementById('folderList');
let html = `<div class="folder-item ${state.activeFolder===null?'active':''}" onclick="selectFolder(null)">
<span class="folder-icon">📋</span> All Notes
<span class="folder-count">${state.notes.length}</span>
</div>`;
state.folders.forEach(f => {
const count = state.notes.filter(n => n.folderId === f.id).length;
html += `<div class="folder-item ${state.activeFolder===f.id?'active':''}" onclick="selectFolder('${f.id}')">
<span class="folder-icon">${f.icon}</span> ${escHtml(f.name)}
<span class="folder-count">${count}</span>
</div>`;
});
list.innerHTML = html;
}
function renderNoteList() {
const list = document.getElementById('noteList');
let notes = state.notes;
if (state.activeFolder !== null) {
notes = notes.filter(n => n.folderId === state.activeFolder);
}
const query = document.getElementById('searchInput').value.toLowerCase().trim();
if (query) {
notes = notes.filter(n =>
n.title.toLowerCase().includes(query) ||
stripHtml(n.content).toLowerCase().includes(query) ||
(n.tags||[]).some(t => t.toLowerCase().includes(query))
);
}
// Sort by updated desc
notes.sort((a, b) => b.updated - a.updated);
if (!notes.length) {
list.innerHTML = `<div style="text-align:center;padding:40px 20px;color:var(--text-tertiary)">
<div style="font-size:32px;margin-bottom:8px">📭</div>
<div style="font-size:13px">${query ? 'No notes match your search' : 'No notes in this folder'}</div>
</div>`;
return;
}
list.innerHTML = notes.map(n => {
const preview = stripHtml(n.content).substring(0, 80);
const date = formatDate(n.updated);
const tags = (n.tags || []).slice(0, 2).map(t => `<span class="note-tag">${escHtml(t)}</span>`).join('');
return `<div class="note-card ${state.activeNoteId===n.id?'active':''}" onclick="selectNote('${n.id}')" oncontextmenu="showContextMenu(event,'${n.id}')">
<div class="note-card-title">${escHtml(n.title || 'Untitled')}</div>
<div class="note-card-preview">${escHtml(preview)}</div>
<div class="note-card-meta"><span>${date}</span>${tags}</div>
</div>`;
}).join('');
const folder = state.folders.find(f => f.id === state.activeFolder);
document.getElementById('folderTitle').textContent = folder ? folder.name : 'All Notes';
}
function renderEditor() {
const emptyState = document.getElementById('emptyState');
const editorContainer = document.getElementById('editorContainer');
if (!state.activeNoteId) {
emptyState.style.display = 'flex';
editorContainer.style.display = 'none';
return;
}
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) {
state.activeNoteId = null;
renderEditor();
return;
}
emptyState.style.display = 'none';
editorContainer.style.display = 'flex';
document.getElementById('noteTitleInput').value = note.title || '';
const editorContent = document.getElementById('editorContent');
// Only update content if it differs (preserve cursor position when possible)
if (editorContent.innerHTML !== note.content) {
editorContent.innerHTML = note.content;
}
// Meta info
const folder = state.folders.find(f => f.id === note.folderId);
document.getElementById('editorMeta').innerHTML = `
<span>📁 ${escHtml(folder ? folder.name : 'Unknown')}</span>
<span>🕐 ${formatDate(note.updated)}</span>
${(note.tags||[]).map(t => `<span class="meta-tag" onclick="toggleTag('${escHtml(t)}')">✏️ ${escHtml(t)}</span>`).join('')}
`;
updateWordCount();
}
// ── Actions ──
function selectFolder(folderId) {
state.activeFolder = folderId;
renderFolders();
renderNoteList();
}
function selectNote(noteId) {
saveCurrentContent();
state.activeNoteId = noteId;
renderNoteList();
renderEditor();
}
function createNote() {
const id = 'n' + Date.now();
const note = {
id, title: '', folderId: state.activeFolder || 'personal',
content: '<p>Start writing…</p>', tags: [],
created: Date.now(), updated: Date.now()
};
state.notes.unshift(note);
saveState();
selectNote(id);
setTimeout(() => document.getElementById('noteTitleInput').focus(), 100);
showToast('✨ New note created');
}
function updateNoteTitle() {
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) return;
note.title = document.getElementById('noteTitleInput').value;
note.updated = Date.now();
saveState();
renderNoteList(); // update preview in list
}
function onEditorInput() {
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) return;
note.content = document.getElementById('editorContent').innerHTML;
note.updated = Date.now();
saveState();
updateWordCount();
}
function handleEditorKeys(e) {
// Tab key inserts spaces
if (e.key === 'Tab') {
e.preventDefault();
execCmd('insertText', ' ');
}
}
function saveCurrentContent() {
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) return;
note.content = document.getElementById('editorContent').innerHTML;
note.title = document.getElementById('noteTitleInput').value;
note.updated = Date.now();
saveState();
}
function deleteCurrentNote() {
hideContextMenu();
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) return;
state.notes = state.notes.filter(n => n.id !== state.activeNoteId);
state.activeNoteId = null;
saveState();
renderFolders();
renderNoteList();
renderEditor();
showToast('🗑 Note deleted');
}
function duplicateNote() {
hideContextMenu();
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) return;
const id = 'n' + Date.now();
const dup = { ...note, id, title: note.title + ' (copy)', created: Date.now(), updated: Date.now() };
state.notes.unshift(dup);
saveState();
selectNote(id);
showToast('📋 Note duplicated');
}
function moveNoteFolder() {
hideContextMenu();
const list = document.getElementById('moveFolderList');
list.innerHTML = state.folders.map(f =>
`<div class="folder-item" onclick="doMoveNote('${f.id}')">${f.icon} ${escHtml(f.name)}</div>`
).join('');
openModal('moveNoteModal');
}
function doMoveNote(folderId) {
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) return;
note.folderId = folderId;
note.updated = Date.now();
saveState();
closeModal('moveNoteModal');
renderFolders();
renderNoteList();
renderEditor();
showToast('📁 Note moved');
}
function toggleTag(tag) {
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note || !note.tags) return;
const idx = note.tags.indexOf(tag);
if (idx >= 0) note.tags.splice(idx, 1);
else note.tags.push(tag);
note.updated = Date.now();
saveState();
renderEditor();
renderNoteList();
}
function filterNotes() {
renderNoteList();
}
// ── Toolbar Commands ──
function execCmd(cmd, val) {
document.execCommand(cmd, false, val || null);
document.getElementById('editorContent').focus();
onEditorInput();
}
function execHeading(tag) {
document.execCommand('formatBlock', false, `<${tag}>`);
document.getElementById('editorContent').focus();
onEditorInput();
}
function insertCode() {
const sel = window.getSelection();
if (sel.rangeCount && !sel.isCollapsed) {
const range = sel.getRangeAt(0);
const code = document.createElement('code');
code.textContent = range.toString();
range.deleteContents();
range.insertNode(code);
onEditorInput();
} else {
execCmd('insertText', '`code`');
}
}
function updateWordCount() {
const text = stripHtml(document.getElementById('editorContent').innerHTML).trim();
const words = text ? text.split(/\s+/).length : 0;
document.getElementById('wordCount').textContent = `${words} word${words!==1?'s':''}`;
}
// ── Folders ──
function openNewFolderModal() {
document.getElementById('newFolderName').value = '';
openModal('folderModal');
setTimeout(() => document.getElementById('newFolderName').focus(), 100);
}
function saveNewFolder() {
const name = document.getElementById('newFolderName').value.trim();
if (!name) return;
const icons = ['📁','📂','🗂️','💼','🎨','🔬','📚','🏠','⭐','🎯'];
state.folders.push({
id: 'f' + Date.now(), name, icon: icons[Math.floor(Math.random()*icons.length)]
});
saveState();
closeModal('folderModal');
renderFolders();
showToast(`📁 Folder "${name}" created`);
}
// ── Theme ──
function toggleTheme() {
state.theme = state.theme === 'dark' ? 'light' : 'dark';
applyTheme();
saveState();
}
function applyTheme() {
document.documentElement.setAttribute('data-theme', state.theme);
document.getElementById('themeIcon').textContent = state.theme === 'dark' ? '☀️' : '🌙';
}
// ── Sidebar Toggle ──
function toggleSidebar() {
const sb = document.getElementById('sidebar');
sb.classList.toggle('collapsed');
}
// ── Context Menu ──
let ctxNoteId = null;
function showContextMenu(e, noteId) {
e.preventDefault();
e.stopPropagation();
if (state.activeNoteId !== noteId) selectNote(noteId);
const menu = document.getElementById('contextMenu');
menu.style.left = e.clientX + 'px';
menu.style.top = e.clientY + 'px';
menu.classList.add('show');
}
function hideContextMenu() {
document.getElementById('contextMenu').classList.remove('show');
}
document.addEventListener('click', () => hideContextMenu());
// ── Modal Helpers ──
function openModal(id) { document.getElementById(id).classList.add('show'); }
function closeModal(id) { document.getElementById(id).classList.remove('show'); }
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(el => {
el.addEventListener('click', e => { if (e.target === el) el.classList.remove('show'); });
});
// ── Toast ──
function showToast(msg) {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = msg;
container.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// ── Utilities ──
function escHtml(s) { const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
function stripHtml(html) { const d=document.createElement('div'); d.innerHTML=html; return d.textContent||''; }
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 < 604800000) return Math.floor(diff/86400000) + 'd ago';
return d.toLocaleDateString('en-US', { month:'short', day:'numeric' });
}
// ── Keyboard Shortcuts ──
document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault(); createNote();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault(); document.getElementById('searchInput').focus();
}
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.show').forEach(m => m.classList.remove('show'));
}
});
// ── Init ──
loadState();
applyTheme();
renderFolders();
renderNoteList();
renderEditor();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"full","sessionId":"aa49c281-625a-49f8-b5fe-3628188af482","tokensIn":65765,"tokensOut":11799,"tokensTotal":77564,"turns":5,"toolCalls":4,"failedToolCalls":0,"timestamp":"2026-07-18T20:13:10.838Z"} -->