Notes
Qwen 3.6 35b a3b · 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 61,835 bytes · SHA-256 5da70ed2dfd6
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Noteflow</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--bg-tertiary: #11111b;
--bg-hover: #2a2a3d;
--bg-active: #313244;
--bg-input: #1e1e2e;
--text-primary: #cdd6f4;
--text-secondary: #a6adc8;
--text-muted: #6c7086;
--accent: #89b4fa;
--accent-hover: #74c7ec;
--accent-subtle: rgba(137, 180, 250, 0.1);
--border: #313244;
--red: #f38ba8;
--green: #a6e3a1;
--yellow: #f9e2af;
--purple: #cba6f7;
--orange: #fab387;
--pink: #f5c2e7;
--teal: #94e2d5;
--scrollbar-thumb: #45475a;
--shadow: rgba(0,0,0,0.3);
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
--font-serif: 'Georgia', 'Times New Roman', serif;
--radius: 8px;
--radius-sm: 4px;
--transition: 150ms ease;
}
html, body {
height: 100%;
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
font-size: 14px;
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
.app {
display: flex;
height: 100vh;
width: 100vw;
}
/* ===== SIDEBAR ===== */
.sidebar {
width: 260px;
min-width: 260px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: width var(--transition), min-width var(--transition), opacity var(--transition);
overflow: hidden;
position: relative;
}
.sidebar.collapsed {
width: 0;
min-width: 0;
opacity: 0;
border-right: none;
}
.sidebar-header {
padding: 16px 16px 8px;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-header .logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
flex-shrink: 0;
}
.sidebar-header .logo svg { flex-shrink: 0; }
.sidebar-search {
padding: 4px 12px 12px;
flex-shrink: 0;
}
.sidebar-search input {
width: 100%;
padding: 8px 12px 8px 34px;
background: var(--bg-input);
border: 1px solid transparent;
border-radius: var(--radius);
color: var(--text-primary);
font-size: 13px;
font-family: var(--font-sans);
outline: none;
transition: border-color var(--transition), background var(--transition);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c7086' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 10px center;
background-size: 14px;
}
.sidebar-search input:focus {
border-color: var(--accent);
background-color: var(--bg-hover);
}
.sidebar-search input::placeholder { color: var(--text-muted); }
.sidebar-section {
flex-shrink: 0;
padding: 0 8px;
}
.sidebar-section-title {
padding: 8px 8px 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
}
.sidebar-section-title:hover { color: var(--text-secondary); }
.sidebar-section-title .chevron {
transition: transform var(--transition);
display: inline-block;
}
.sidebar-section-title .chevron.collapsed { transform: rotate(-90deg); }
.sidebar-list {
overflow-y: auto;
max-height: 0;
transition: max-height 300ms ease;
}
.sidebar-list.expanded { max-height: 600px; }
.sidebar-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--text-secondary);
font-size: 13px;
transition: background var(--transition), color var(--transition);
user-select: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-item:hover { background: var(--bg-hover); color: var(--text-primary); }
.sidebar-item.active { background: var(--accent-subtle); color: var(--accent); }
.sidebar-item .icon { flex-shrink: 0; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; }
.sidebar-item .count {
margin-left: auto;
font-size: 11px;
color: var(--text-muted);
}
.folder-children {
padding-left: 16px;
max-height: 0;
overflow: hidden;
transition: max-height 300ms ease;
}
.folder-children.expanded { max-height: 200px; }
/* ===== NOTE LIST ===== */
.note-list-panel {
width: 280px;
min-width: 200px;
max-width: 400px;
background: var(--bg-primary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
}
.note-list-header {
padding: 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.note-list-header h2 {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.note-list-header-actions {
display: flex;
gap: 2px;
}
.note-list-header-actions button {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: color var(--transition), background var(--transition);
}
.note-list-header-actions button:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.note-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.note-card {
padding: 12px;
border-radius: var(--radius);
cursor: pointer;
margin-bottom: 2px;
transition: background var(--transition);
border: 1px solid transparent;
}
.note-card:hover { background: var(--bg-hover); }
.note-card.active { background: var(--bg-active); border-color: var(--border); }
.note-card-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-card-preview {
font-size: 12px;
color: var(--text-muted);
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-muted);
}
.note-card-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-top: 4px;
}
.note-card-tag {
font-size: 10px;
padding: 1px 6px;
border-radius: 3px;
background: var(--accent-subtle);
color: var(--accent);
}
/* ===== EDITOR ===== */
.editor-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.editor-toolbar {
padding: 8px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
background: var(--bg-primary);
}
.editor-toolbar button {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: var(--radius-sm);
transition: color var(--transition), background var(--transition);
font-size: 13px;
}
.editor-toolbar button:hover { background: var(--bg-hover); color: var(--text-primary); }
.editor-toolbar button.active { background: var(--accent-subtle); color: var(--accent); }
.toolbar-divider {
width: 1px;
height: 20px;
background: var(--border);
margin: 0 4px;
}
.editor-breadcrumb {
margin-left: auto;
font-size: 12px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 4px;
}
.editor-content {
flex: 1;
overflow-y: auto;
padding: 32px 48px;
display: flex;
flex-direction: column;
}
.editor-title {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
border: none;
background: none;
outline: none;
width: 100%;
margin-bottom: 8px;
font-family: var(--font-sans);
line-height: 1.3;
}
.editor-title::placeholder { color: var(--text-muted); }
.editor-meta {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 24px;
display: flex;
gap: 16px;
}
.editor-body {
flex: 1;
outline: none;
font-size: 15px;
line-height: 1.75;
color: var(--text-primary);
min-height: 300px;
font-family: var(--font-sans);
}
.editor-body:empty::before {
content: 'Start writing...';
color: var(--text-muted);
pointer-events: none;
}
.editor-body h1 { font-size: 28px; font-weight: 700; margin: 24px 0 12px; color: var(--text-primary); }
.editor-body h2 { font-size: 22px; font-weight: 600; margin: 20px 0 10px; color: var(--text-primary); }
.editor-body h3 { font-size: 18px; font-weight: 600; margin: 16px 0 8px; color: var(--text-primary); }
.editor-body p { margin-bottom: 12px; }
.editor-body blockquote {
border-left: 3px solid var(--accent);
padding: 8px 16px;
margin: 12px 0;
background: var(--accent-subtle);
border-radius: 0 var(--radius) var(--radius) 0;
color: var(--text-secondary);
}
.editor-body code {
font-family: var(--font-mono);
background: var(--bg-hover);
padding: 2px 6px;
border-radius: var(--radius-sm);
font-size: 0.9em;
color: var(--pink);
}
.editor-body pre {
background: var(--bg-tertiary);
padding: 16px;
border-radius: var(--radius);
overflow-x: auto;
margin: 12px 0;
border: 1px solid var(--border);
}
.editor-body pre code {
background: none;
padding: 0;
color: var(--text-primary);
}
.editor-body ul, .editor-body ol { padding-left: 24px; margin: 8px 0; }
.editor-body li { margin-bottom: 4px; }
.editor-body hr {
border: none;
border-top: 1px solid var(--border);
margin: 24px 0;
}
.editor-body a { color: var(--accent); text-decoration: underline; }
.editor-body strong { font-weight: 600; color: var(--text-primary); }
.editor-body em { font-style: italic; }
.editor-body img { max-width: 100%; border-radius: var(--radius); margin: 8px 0; }
.editor-body table { border-collapse: collapse; width: 100%; margin: 12px 0; }
.editor-body th, .editor-body td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; }
.editor-body th { background: var(--bg-hover); font-weight: 600; }
.editor-body .task-list { list-style: none; padding-left: 0; }
.editor-body .task-list li { display: flex; align-items: flex-start; gap: 8px; }
.editor-body .task-list input[type="checkbox"] { margin-top: 4px; accent-color: var(--accent); }
/* ===== STATUS BAR ===== */
.status-bar {
padding: 4px 16px;
font-size: 11px;
color: var(--text-muted);
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 16px;
background: var(--bg-secondary);
flex-shrink: 0;
}
.status-bar .status-item { display: flex; align-items: center; gap: 4px; }
/* ===== CONTEXT MENU ===== */
.context-menu {
position: fixed;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 4px;
min-width: 180px;
box-shadow: 0 8px 32px var(--shadow);
z-index: 1000;
display: none;
}
.context-menu.visible { display: block; }
.context-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
transition: background var(--transition), color var(--transition);
}
.context-menu-item:hover { background: var(--bg-hover); color: var(--text-primary); }
.context-menu-item.danger { color: var(--red); }
.context-menu-item.danger:hover { background: rgba(243, 139, 168, 0.1); }
.context-menu-divider {
height: 1px;
background: var(--border);
margin: 4px 0;
}
.context-menu-shortcut {
margin-left: auto;
font-size: 11px;
color: var(--text-muted);
}
/* ===== MODAL ===== */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-overlay.visible { display: flex; }
.modal {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 16px 64px var(--shadow);
}
.modal h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
}
.modal input {
width: 100%;
padding: 10px 14px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 14px;
font-family: var(--font-sans);
outline: none;
margin-bottom: 16px;
}
.modal input:focus { border-color: var(--accent); }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.modal-btn {
padding: 8px 16px;
border-radius: var(--radius);
border: none;
cursor: pointer;
font-size: 13px;
font-family: var(--font-sans);
transition: background var(--transition);
}
.modal-btn.cancel {
background: var(--bg-hover);
color: var(--text-secondary);
}
.modal-btn.cancel:hover { background: var(--bg-active); }
.modal-btn.primary {
background: var(--accent);
color: var(--bg-tertiary);
font-weight: 600;
}
.modal-btn.primary:hover { background: var(--accent-hover); }
/* ===== EMPTY STATE ===== */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-muted);
gap: 12px;
}
.empty-state svg { opacity: 0.3; }
.empty-state p { font-size: 14px; }
.empty-state .hint { font-size: 12px; color: var(--text-muted); opacity: 0.6; }
/* ===== RESIZE HANDLE ===== */
.resize-handle {
width: 4px;
cursor: col-resize;
background: transparent;
flex-shrink: 0;
transition: background var(--transition);
position: relative;
}
.resize-handle:hover, .resize-handle.active { background: var(--accent); }
/* ===== WELCOME ===== */
.welcome-screen {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 40px;
}
.welcome-screen .welcome-icon {
width: 80px;
height: 80px;
background: var(--accent-subtle);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
}
.welcome-screen h1 {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
}
.welcome-screen p {
color: var(--text-muted);
text-align: center;
max-width: 400px;
line-height: 1.6;
}
.welcome-shortcuts {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.welcome-shortcut {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
color: var(--text-secondary);
}
.welcome-shortcut kbd {
background: var(--bg-hover);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
}
/* ===== ANIMATIONS ===== */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.note-card { animation: fadeIn 200ms ease; }
/* ===== FIND BAR ===== */
.find-bar {
position: absolute;
top: 0;
right: 24px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 var(--radius) var(--radius);
padding: 8px 12px;
display: none;
align-items: center;
gap: 8px;
box-shadow: 0 4px 16px var(--shadow);
z-index: 100;
}
.find-bar.visible { display: flex; }
.find-bar input {
padding: 6px 10px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 13px;
outline: none;
width: 240px;
font-family: var(--font-sans);
}
.find-bar input:focus { border-color: var(--accent); }
.find-bar button {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
}
.find-bar button:hover { color: var(--text-primary); background: var(--bg-hover); }
/* ===== TAG PANEL ===== */
.tag-panel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 12px 16px;
display: none;
gap: 8px;
flex-wrap: wrap;
z-index: 50;
}
.tag-panel.visible { display: flex; }
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: background var(--transition), transform var(--transition);
}
.tag:hover { transform: scale(1.05); }
.tag-red { background: rgba(243,139,168,0.15); color: var(--red); }
.tag-green { background: rgba(166,227,161,0.15); color: var(--green); }
.tag-yellow { background: rgba(249,226,175,0.15); color: var(--yellow); }
.tag-blue { background: rgba(137,180,250,0.15); color: var(--accent); }
.tag-purple { background: rgba(203,166,247,0.15); color: var(--purple); }
.tag-orange { background: rgba(250,179,135,0.15); color: var(--orange); }
.tag-teal { background: rgba(148,226,213,0.15); color: var(--teal); }
.tag-pink { background: rgba(245,194,231,0.15); color: var(--pink); }
/* ===== TOOLTIP ===== */
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
padding: 4px 8px;
background: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 11px;
border-radius: var(--radius-sm);
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity var(--transition);
z-index: 100;
}
[data-tooltip]:hover::after { opacity: 1; }
/* ===== NOTIFICATION ===== */
.notification {
position: fixed;
bottom: 48px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 20px;
color: var(--text-primary);
font-size: 13px;
box-shadow: 0 8px 32px var(--shadow);
opacity: 0;
transition: all 300ms ease;
z-index: 3000;
pointer-events: none;
}
.notification.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ===== VIEW MODES ===== */
.app.view-welcome .note-list-panel,
.app.view-welcome .sidebar { display: none; }
.app.view-welcome .editor-panel { background: var(--bg-primary); }
/* ===== PRINT ===== */
@media print {
.sidebar, .note-list-panel, .editor-toolbar, .status-bar { display: none !important; }
.editor-panel { width: 100% !important; }
.editor-content { padding: 20px; }
}
</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" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
Noteflow
</div>
</div>
<div class="sidebar-search">
<input type="text" id="sidebarSearch" placeholder="Search notes..." autocomplete="off">
</div>
<div class="sidebar-section">
<div class="sidebar-section-title" data-toggle="unsorted">
<span>All Notes</span>
<span class="chevron">▾</span>
</div>
<div class="sidebar-list expanded" id="unsortedList"></div>
</div>
<div class="sidebar-section">
<div class="sidebar-section-title" data-toggle="folders">
<span>Folders</span>
<span class="chevron">▾</span>
</div>
<div class="sidebar-list expanded" id="foldersList"></div>
</div>
<div class="sidebar-section">
<div class="sidebar-section-title" data-toggle="tags">
<span>Tags</span>
<span class="chevron">▾</span>
</div>
<div class="sidebar-list expanded" id="tagsList"></div>
</div>
<div class="sidebar-section">
<div class="sidebar-section-title" data-toggle="recent">
<span>Recent</span>
<span class="chevron">▾</span>
</div>
<div class="sidebar-list expanded" id="recentList"></div>
</div>
</aside>
<!-- Note List Panel -->
<div class="note-list-panel" id="noteListPanel">
<div class="note-list-header">
<h2 id="noteListTitle">All Notes</h2>
<div class="note-list-header-actions">
<button id="btnNewNote" data-tooltip="New Note (Ctrl+N)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
<button id="btnCollapseList" data-tooltip="Collapse">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg>
</button>
</div>
</div>
<div class="note-list" id="noteList"></div>
</div>
<!-- Resize Handle -->
<div class="resize-handle" id="resizeHandle"></div>
<!-- Editor Panel -->
<div class="editor-panel" id="editorPanel">
<div class="editor-toolbar" id="editorToolbar">
<button id="btnToggleSidebar" data-tooltip="Toggle Sidebar (Ctrl+B)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
</button>
<div class="toolbar-divider"></div>
<button id="btnBold" data-tooltip="Bold (Ctrl+B)" onclick="execCmd('bold')"><strong>B</strong></button>
<button id="btnItalic" data-tooltip="Italic (Ctrl+I)" onclick="execCmd('italic')"><em>I</em></button>
<button id="btnUnderline" data-tooltip="Underline (Ctrl+U)" onclick="execCmd('underline')"><u>U</u></button>
<button id="btnStrike" data-tooltip="Strikethrough" onclick="execCmd('strikeThrough')"><s>S</s></button>
<div class="toolbar-divider"></div>
<button id="btnH1" data-tooltip="Heading 1" onclick="execCmd('formatBlock','<h1>')">H1</button>
<button id="btnH2" data-tooltip="Heading 2" onclick="execCmd('formatBlock','<h2>')">H2</button>
<button id="btnH3" data-tooltip="Heading 3" onclick="execCmd('formatBlock','<h3>')">H3</button>
<div class="toolbar-divider"></div>
<button data-tooltip="Unordered List" onclick="execCmd('insertUnorderedList')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor"/><circle cx="4" cy="12" r="1" fill="currentColor"/><circle cx="4" cy="18" r="1" fill="currentColor"/></svg>
</button>
<button data-tooltip="Ordered List" onclick="execCmd('insertOrderedList')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><text x="2" y="8" font-size="7" fill="currentColor" stroke="none">1</text><text x="2" y="14" font-size="7" fill="currentColor" stroke="none">2</text><text x="2" y="20" font-size="7" fill="currentColor" stroke="none">3</text></svg>
</button>
<button data-tooltip="Blockquote" onclick="execCmd('formatBlock','<blockquote>')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>
</button>
<div class="toolbar-divider"></div>
<button data-tooltip="Insert Code Block" onclick="insertCodeBlock()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
</button>
<button data-tooltip="Insert Horizontal Rule" onclick="execCmd('insertHorizontalRule')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="2" y1="12" x2="22" y2="12"/></svg>
</button>
<button data-tooltip="Insert Link" onclick="insertLink()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="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>
<div class="toolbar-divider"></div>
<button data-tooltip="Find (Ctrl+F)" onclick="toggleFindBar()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</button>
<div class="editor-breadcrumb" id="breadcrumb"></div>
</div>
<!-- Find Bar -->
<div class="find-bar" id="findBar">
<input type="text" id="findInput" placeholder="Find in note...">
<span id="findCount" style="font-size:12px;color:var(--text-muted);min-width:40px;">0/0</span>
<button onclick="findPrev()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
</button>
<button onclick="findNext()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<button onclick="toggleFindBar()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<!-- Editor Content -->
<div class="editor-content" id="editorContent">
<div class="welcome-screen" id="welcomeScreen">
<div class="welcome-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
</div>
<h1>Welcome to Noteflow</h1>
<p>A beautiful, distraction-free notes app. Create your first note to get started.</p>
<div class="welcome-shortcuts">
<div class="welcome-shortcut"><kbd>Ctrl+N</kbd> New note</div>
<div class="welcome-shortcut"><kbd>Ctrl+S</kbd> Save note</div>
<div class="welcome-shortcut"><kbd>Ctrl+F</kbd> Find</div>
<div class="welcome-shortcut"><kbd>Ctrl+B</kbd> Toggle sidebar</div>
<div class="welcome-shortcut"><kbd>Ctrl+Shift+T</kbd> New folder</div>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="status-bar" id="statusBar">
<span class="status-item" id="statusWords">0 words</span>
<span class="status-item" id="statusChars">0 characters</span>
<span class="status-item" id="statusLines">0 lines</span>
<span class="status-item" style="margin-left:auto" id="statusSaved">Saved</span>
</div>
</div>
</div>
<!-- Context Menu -->
<div class="context-menu" id="contextMenu">
<div class="context-menu-item" onclick="contextAction('rename')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
Rename
</div>
<div class="context-menu-item" onclick="contextAction('duplicate')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
Duplicate
</div>
<div class="context-menu-item" onclick="contextAction('moveToFolder')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
Move to folder...
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick="contextAction('addTag')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
Add tag
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item danger" onclick="contextAction('delete')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Delete
<span class="context-menu-shortcut">Del</span>
</div>
</div>
<!-- Modal -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal" id="modal">
<h3 id="modalTitle">New Folder</h3>
<input type="text" id="modalInput" placeholder="Enter name...">
<div class="modal-actions">
<button class="modal-btn cancel" onclick="closeModal()">Cancel</button>
<button class="modal-btn primary" id="modalConfirm">Create</button>
</div>
</div>
</div>
<!-- Notification -->
<div class="notification" id="notification"></div>
<script>
// ===== STATE =====
let state = {
notes: [],
folders: [],
tags: {},
activeNoteId: null,
activeFolder: null,
activeTag: null,
sidebarCollapsed: false,
searchQuery: '',
contextNoteId: null,
};
let saveTimeout = null;
// ===== DATA =====
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
}
function getTagColors() {
return ['tag-red', 'tag-green', 'tag-yellow', 'tag-blue', 'tag-purple', 'tag-orange', 'tag-teal', 'tag-pink'];
}
function loadState() {
try {
const saved = localStorage.getItem('noteflow_state');
if (saved) {
const parsed = JSON.parse(saved);
state.notes = parsed.notes || [];
state.folders = parsed.folders || [];
state.tags = parsed.tags || {};
state.activeNoteId = parsed.activeNoteId || null;
}
} catch(e) { console.error('Failed to load state:', e); }
if (state.notes.length === 0) {
createDefaultNotes();
}
}
function createDefaultNotes() {
state.folders = [
{ id: generateId(), name: 'Personal', icon: '📁' },
{ id: generateId(), name: 'Work', icon: '💼' },
{ id: generateId(), name: 'Ideas', icon: '💡' },
];
const note1Id = generateId();
const note2Id = generateId();
const note3Id = generateId();
state.notes = [
{
id: note1Id,
title: 'Welcome to Noteflow',
content: '<h1>Welcome to Noteflow</h1><p>Your beautiful, distraction-free notes app. Here are some things you can do:</p><ul><li><strong>Create new notes</strong> with Ctrl+N</li><li><strong>Format text</strong> using the toolbar or keyboard shortcuts</li><li><strong>Organize</strong> notes into folders</li><li><strong>Tag your notes</strong> for easy filtering</li><li><strong>Search</strong> across all your notes</li></ul><blockquote>Everything is saved automatically to your browser.</blockquote><hr><p>Try editing this note or create a new one to get started!</p>',
folderId: state.folders[0]?.id,
tags: ['welcome', 'getting-started'],
created: Date.now(),
modified: Date.now(),
},
{
id: note2Id,
title: 'Meeting Notes',
content: '<h2>Project Kickoff Meeting</h2><p><strong>Date:</strong> Today</p><p><strong>Attendees:</strong> Team leads</p><h3>Agenda</h3><ol><li>Project overview</li><li>Timeline discussion</li><li>Role assignments</li><li>Next steps</ol><h3>Action Items</h3><ul><li>Review project requirements</li><li>Set up development environment</li><li>Prepare initial design mockups</ul>',
folderId: state.folders[1]?.id,
tags: ['work', 'meetings'],
created: Date.now() - 86400000,
modified: Date.now() - 3600000,
},
{
id: note3Id,
content: '',
folderId: state.folders[2]?.id,
tags: ['ideas'],
created: Date.now() - 172800000,
modified: Date.now() - 7200000,
},
];
state.tags = {
'welcome': { color: getTagColors()[3], notes: [note1Id] },
'getting-started': { color: getTagColors()[4], notes: [note1Id] },
'work': { color: getTagColors()[0], notes: [note2Id] },
'meetings': { color: getTagColors()[5], notes: [note2Id] },
'ideas': { color: getTagColors()[6], notes: [note1Id, note3Id] },
};
saveState();
}
function saveState() {
try {
localStorage.setItem('noteflow_state', JSON.stringify({
notes: state.notes,
folders: state.folders,
tags: state.tags,
activeNoteId: state.activeNoteId,
}));
} catch(e) { console.error('Failed to save state:', e); }
}
function saveNote() {
if (!state.activeNoteId) return;
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) return;
const titleEl = document.getElementById('editorTitle');
const bodyEl = document.getElementById('editorBody');
note.title = titleEl.value || 'Untitled';
note.content = bodyEl.innerHTML;
note.modified = Date.now();
saveState();
updateStatusBar();
document.getElementById('statusSaved').textContent = 'Saved';
document.getElementById('statusSaved').style.color = 'var(--green)';
setTimeout(() => {
document.getElementById('statusSaved').style.color = '';
}, 2000);
}
function autoSave() {
clearTimeout(saveTimeout);
document.getElementById('statusSaved').textContent = 'Saving...';
saveTimeout = setTimeout(saveNote, 500);
}
// ===== RENDERING =====
function getFilteredNotes() {
let notes = [...state.notes];
if (state.activeFolder) {
notes = notes.filter(n => n.folderId === state.activeFolder);
}
if (state.activeTag) {
const tagNotes = state.tags[state.activeTag]?.notes || [];
notes = notes.filter(n => tagNotes.includes(n.id));
}
if (state.searchQuery) {
const q = state.searchQuery.toLowerCase();
notes = notes.filter(n =>
n.title.toLowerCase().includes(q) ||
stripHtml(n.content).toLowerCase().includes(q)
);
}
notes.sort((a, b) => b.modified - a.modified);
return notes;
}
function stripHtml(html) {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
}
function formatDate(ts) {
const now = Date.now();
const diff = now - ts;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
if (diff < 604800000) return `${Math.floor(diff / 86400000)}d ago`;
return new Date(ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function renderNoteList() {
const list = document.getElementById('noteList');
const title = document.getElementById('noteListTitle');
const notes = getFilteredNotes();
if (state.activeTag) {
title.textContent = `#${state.activeTag}`;
} else if (state.activeFolder) {
const folder = state.folders.find(f => f.id === state.activeFolder);
title.textContent = folder ? folder.name : 'Folder';
} else if (state.searchQuery) {
title.textContent = `Search: "${state.searchQuery}"`;
} else {
title.textContent = 'All Notes';
}
if (notes.length === 0) {
list.innerHTML = `
<div class="empty-state" style="padding:40px 20px;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="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"/><polyline points="14 2 14 8 20 8"/>
</svg>
<p style="font-size:13px;">No notes yet</p>
<button class="modal-btn primary" onclick="createNote()" style="margin-top:8px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;margin-right:4px"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
New Note
</button>
</div>
`;
return;
}
list.innerHTML = notes.map(note => {
const isActive = note.id === state.activeNoteId;
const preview = stripHtml(note.content).substring(0, 100);
const noteTags = note.tags || [];
return `
<div class="note-card ${isActive ? 'active' : ''}" data-note-id="${note.id}" onclick="selectNote('${note.id}')">
<div class="note-card-title">${note.title || 'Untitled'}</div>
<div class="note-card-preview">${preview || 'Empty note'}</div>
<div class="note-card-meta">
<span>${formatDate(note.modified)}</span>
</div>
${noteTags.length ? `<div class="note-card-tags">${noteTags.map(t => `<span class="note-card-tag">#${t}</span>`).join('')}</div>` : ''}
</div>
`;
}).join('');
}
function renderSidebar() {
// Folders
const foldersList = document.getElementById('foldersList');
foldersList.innerHTML = state.folders.map(folder => {
const count = state.notes.filter(n => n.folderId === folder.id).length;
const isActive = state.activeFolder === folder.id;
return `
<div class="sidebar-item ${isActive ? 'active' : ''}" onclick="selectFolder('${folder.id}')">
<span class="icon">📁</span>
<span>${folder.name}</span>
<span class="count">${count}</span>
</div>
`;
}).join('');
// Tags
const tagsList = document.getElementById('tagsList');
const allTags = Object.keys(state.tags);
if (allTags.length === 0) {
tagsList.innerHTML = '<div style="padding:8px;font-size:12px;color:var(--text-muted)">No tags yet</div>';
} else {
tagsList.innerHTML = allTags.map(tag => {
const colorClass = state.tags[tag].color;
const count = state.tags[tag].notes.length;
const isActive = state.activeTag === tag;
return `
<div class="sidebar-item ${isActive ? 'active' : ''}" onclick="selectTag('${tag}')">
<span class="icon"><span class="tag ${colorClass}" style="font-size:10px;padding:1px 5px;">#${tag}</span></span>
<span>${tag}</span>
<span class="count">${count}</span>
</div>
`;
}).join('');
}
// Recent
const recentList = document.getElementById('recentList');
const recent = [...state.notes].sort((a, b) => b.modified - a.modified).slice(0, 8);
recentList.innerHTML = recent.map(note => {
const isActive = note.id === state.activeNoteId;
return `
<div class="sidebar-item ${isActive ? 'active' : ''}" onclick="selectNote('${note.id}')">
<span class="icon">📄</span>
<span>${note.title || 'Untitled'}</span>
</div>
`;
}).join('');
}
function updateStatusBar() {
if (!state.activeNoteId) return;
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) return;
const text = stripHtml(note.content);
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
const chars = text.length;
const lines = text.split('\n').length;
document.getElementById('statusWords').textContent = `${words} word${words !== 1 ? 's' : ''}`;
document.getElementById('statusChars').textContent = `${chars} character${chars !== 1 ? 's' : ''}`;
document.getElementById('statusLines').textContent = `${lines} line${lines !== 1 ? 's' : ''}`;
}
function renderBreadcrumb() {
const bc = document.getElementById('breadcrumb');
if (!state.activeNoteId) { bc.innerHTML = ''; return; }
const note = state.notes.find(n => n.id === state.activeNoteId);
if (!note) { bc.innerHTML = ''; return; }
let parts = [];
if (note.folderId) {
const folder = state.folders.find(f => f.id === note.folderId);
if (folder) parts.push(folder.name);
}
parts.push(note.title || 'Untitled');
bc.innerHTML = parts.map((p, i) => {
if (i < parts.length - 1) return `<span>${p}</span><span style="opacity:0.3">/</span>`;
return `<span style="color:var(--text-primary);font-weight:500">${p}</span>`;
}).join('');
}
function renderAll() {
renderNoteList();
renderSidebar();
renderBreadcrumb();
}
// ===== NOTE OPERATIONS =====
function createNote(folderId) {
const note = {
id: generateId(),
title: '',
content: '',
folderId: folderId || state.activeFolder,
tags: [],
created: Date.now(),
modified: Date.now(),
};
state.notes.unshift(note);
state.activeNoteId = note.id;
saveState();
renderAll();
openNote(note.id);
notify('New note created');
setTimeout(() => {
document.getElementById('editorTitle')?.focus();
}, 100);
}
function selectNote(id) {
if (state.activeNoteId === id) return;
state.activeNoteId = id;
state.activeTag = null;
saveState();
renderAll();
openNote(id);
}
function openNote(id) {
const note = state.notes.find(n => n.id === id);
if (!note) return;
const welcome = document.getElementById('welcomeScreen');
if (welcome) welcome.style.display = 'none';
const editorContent = document.getElementById('editorContent');
// Remove welcome if present
const existing = editorContent.querySelector('.welcome-screen');
if (existing) existing.remove();
// Remove editor body if exists
const existingBody = editorContent.querySelector('#editorBody');
if (existingBody) existingBody.remove();
// Remove title if exists
const existingTitle = editorContent.querySelector('#editorTitle');
if (existingTitle) existingTitle.remove();
const titleHtml = `<input class="editor-title" id="editorTitle" value="${escapeHtml(note.title)}" placeholder="Untitled" oninput="autoSave()">`;
const metaHtml = `<div class="editor-meta"><span>Created ${formatDate(note.created)}</span><span>Modified ${formatDate(note.modified)}</span></div>`;
const bodyHtml = `<div class="editor-body" id="editorBody" contenteditable="true" oninput="autoSave()" onkeydown="handleEditorKey(event)">${note.content}</div>`;
editorContent.insertAdjacentHTML('beforeend', titleHtml + metaHtml + bodyHtml);
renderAll();
updateStatusBar();
updateToolbarState();
}
function escapeHtml(str) {
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
}
function deleteNote(id) {
state.notes = state.notes.filter(n => n.id !== id);
if (state.activeNoteId === id) {
state.activeNoteId = state.notes.length > 0 ? state.notes[0].id : null;
}
saveState();
renderAll();
if (state.activeNoteId) {
openNote(state.activeNoteId);
} else {
showWelcome();
}
notify('Note deleted');
}
function showWelcome() {
const editorContent = document.getElementById('editorContent');
editorContent.innerHTML = `
<div class="welcome-screen" id="welcomeScreen">
<div class="welcome-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
</div>
<h1>Welcome to Noteflow</h1>
<p>A beautiful, distraction-free notes app. Create your first note to get started.</p>
<div class="welcome-shortcuts">
<div class="welcome-shortcut"><kbd>Ctrl+N</kbd> New note</div>
<div class="welcome-shortcut"><kbd>Ctrl+S</kbd> Save note</div>
<div class="welcome-shortcut"><kbd>Ctrl+F</kbd> Find</div>
<div class="welcome-shortcut"><kbd>Ctrl+B</kbd> Toggle sidebar</div>
<div class="welcome-shortcut"><kbd>Ctrl+Shift+T</kbd> New folder</div>
</div>
</div>
`;
}
// ===== FOLDER OPERATIONS =====
function selectFolder(id) {
state.activeFolder = state.activeFolder === id ? null : id;
state.activeTag = null;
saveState();
renderAll();
}
function createFolder() {
showModal('New Folder', 'Enter folder name...', (name) => {
if (!name.trim()) return;
state.folders.push({
id: generateId(),
name: name.trim(),
icon: '📁',
});
saveState();
renderAll();
notify(`Folder "${name.trim()}" created`);
});
}
function deleteFolder(id) {
state.folders = state.folders.filter(f => f.id !== id);
state.notes.forEach(n => { if (n.folderId === id) n.folderId = null; });
if (state.activeFolder === id) state.activeFolder = null;
saveState();
renderAll();
notify('Folder deleted');
}
// ===== TAG OPERATIONS =====
function selectTag(tag) {
state.activeTag = state.activeTag === tag ? null : tag;
state.activeFolder = null;
saveState();
renderAll();
}
function addTagToNote(noteId) {
const note = state.notes.find(n => n.id === noteId);
if (!note) return;
showModal('Add Tag', 'Enter tag name (comma separated)...', (value) => {
if (!value.trim()) return;
const newTags = value.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);
const colorMap = getTagColors();
newTags.forEach(tag => {
if (!note.tags.includes(tag)) {
note.tags.push(tag);
}
if (!state.tags[tag]) {
state.tags[tag] = {
color: colorMap[Object.keys(state.tags).length % colorMap.length],
notes: [],
};
}
if (!state.tags[tag].notes.includes(noteId)) {
state.tags[tag].notes.push(noteId);
}
});
saveState();
renderAll();
if (state.activeNoteId === noteId) openNote(noteId);
notify(`Tag${newTags.length > 1 ? 's' : ''} added`);
});
}
// ===== CONTEXT MENU =====
function showContextMenu(e, noteId) {
e.preventDefault();
state.contextNoteId = noteId;
const menu = document.getElementById('contextMenu');
menu.classList.add('visible');
let x = e.clientX;
let y = e.clientY;
menu.style.left = x + 'px';
menu.style.top = y + 'px';
requestAnimationFrame(() => {
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth) menu.style.left = (x - rect.width) + 'px';
if (rect.bottom > window.innerHeight) menu.style.top = (y - rect.height) + 'px';
});
}
function hideContextMenu() {
document.getElementById('contextMenu').classList.remove('visible');
}
function contextAction(action) {
hideContextMenu();
const noteId = state.contextNoteId;
const note = state.notes.find(n => n.id === noteId);
if (!note) return;
switch(action) {
case 'rename':
showModal('Rename Note', 'Enter new name...', (name) => {
if (name.trim()) {
note.title = name.trim();
saveState();
renderAll();
if (state.activeNoteId === noteId) openNote(noteId);
}
});
break;
case 'duplicate':
const dup = {
...note,
id: generateId(),
title: note.title + ' (copy)',
created: Date.now(),
modified: Date.now(),
};
state.notes.unshift(dup);
state.activeNoteId = dup.id;
saveState();
renderAll();
openNote(dup.id);
notify('Note duplicated');
break;
case 'moveToFolder':
showModal('Move to Folder', '', (value) => {
if (state.folders.length === 0) {
notify('Create a folder first');
return;
}
if (value.trim()) {
const folder = state.folders.find(f => f.name.toLowerCase() === value.trim().toLowerCase());
if (folder) {
note.folderId = folder.id;
saveState();
renderAll();
if (state.activeNoteId === noteId) openNote(noteId);
notify(`Moved to ${folder.name}`);
} else {
notify('Folder not found');
}
}
});
break;
case 'addTag':
addTagToNote(noteId);
break;
case 'delete':
deleteNote(noteId);
break;
}
}
// ===== EDITOR COMMANDS =====
function execCmd(command, value) {
document.getElementById('editorBody')?.focus();
document.execCommand(command, false, value || null);
autoSave();
}
function insertCodeBlock() {
const body = document.getElementById('editorBody');
if (!body) return;
body.focus();
document.execCommand('insertHTML', false, '<pre><code>// Your code here\n</code></pre><p></p>');
autoSave();
}
function insertLink() {
const url = prompt('Enter URL:');
if (url) {
document.execCommand('createLink', false, url);
autoSave();
}
}
function handleEditorKey(e) {
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertHTML', false, ' ');
}
}
function updateToolbarState() {
// Update button states based on current selection
const commands = ['bold', 'italic', 'underline', 'strikeThrough'];
commands.forEach(cmd => {
const btn = document.getElementById('btn' + cmd.charAt(0).toUpperCase() + cmd.slice(1));
if (btn) {
btn.classList.toggle('active', document.queryCommandState(cmd));
}
});
}
// ===== FIND BAR =====
let findIndex = -1;
let findMatches = [];
function toggleFindBar() {
const bar = document.getElementById('findBar');
bar.classList.toggle('visible');
if (bar.classList.contains('visible')) {
document.getElementById('findInput').focus();
}
}
function findInNote(query) {
if (!query) { findMatches = []; findIndex = -1; updateFindCount(); return; }
const body = document.getElementById('editorBody');
if (!body) return;
// Remove old highlights
document.querySelectorAll('.find-highlight').forEach(el => {
el.replaceWith(document.createTextNode(el.textContent));
});
document.body.normalize();
const text = body.textContent;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
findMatches = [];
let idx = 0;
while ((idx = lowerText.indexOf(lowerQuery, idx)) !== -1) {
findMatches.push(idx);
idx += query.length;
}
findIndex = findMatches.length > 0 ? 0 : -1;
updateFindCount();
if (findMatches.length > 0) {
highlightMatch(findIndex);
}
}
function highlightMatch(index) {
if (index < 0 || index >= findMatches.length) return;
const body = document.getElementById('editorBody');
if (!body) return;
const matchPos = findMatches[index];
const query = document.getElementById('findInput').value;
if (!query) return;
// Use TreeWalker to find the text node
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false);
let textNode = null;
let charCount = 0;
while (walker.nextNode()) {
const node = walker.currentNode;
const nodeLen = node.textContent.length;
if (matchPos >= charCount && matchPos < charCount + nodeLen) {
textNode = node;
break;
}
charCount += nodeLen;
}
if (!textNode) return;
const offset = matchPos - charCount;
// Remove old highlights
document.querySelectorAll('.find-highlight').forEach(el => {
el.replaceWith(document.createTextNode(el.textContent));
});
document.body.normalize();
// Re-find and highlight
const walker2 = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false);
let c = 0;
while (walker2.nextNode()) {
const n = walker2.currentNode;
if (c === charCount) {
const range = document.createRange();
range.setStart(n, offset);
range.setEnd(n, offset + query.length);
const span = document.createElement('span');
span.className = 'find-highlight';
span.style.background = 'rgba(137, 180, 250, 0.3)';
span.style.borderRadius = '2px';
range.surroundContents(span);
break;
}
c += n.textContent.length;
}
// Scroll into view
const highlight = document.querySelector('.find-highlight');
if (highlight) highlight.scrollIntoView({ block: 'center', behavior: 'smooth' });
updateFindCount();
}
function findNext() {
if (findMatches.length === 0) return;
findIndex = (findIndex + 1) % findMatches.length;
highlightMatch(findIndex);
}
function findPrev() {
if (findMatches.length === 0) return;
findIndex = (findIndex - 1 + findMatches.length) % findMatches.length;
highlightMatch(findIndex);
}
function updateFindCount() {
const countEl = document.getElementById('findCount');
if (findMatches.length === 0) {
countEl.textContent = '0/0';
} else {
countEl.textContent = `${findIndex + 1}/${findMatches.length}`;
}
}
// ===== MODAL =====
let modalCallback = null;
function showModal(title, placeholder, callback) {
modalCallback = callback;
document.getElementById('modalTitle').textContent = title;
const input = document.getElementById('modalInput');
input.value = '';
input.placeholder = placeholder;
document.getElementById('modalOverlay').classList.add('visible');
setTimeout(() => input.focus(), 100);
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('visible');
modalCallback = null;
}
function confirmModal() {
const value = document.getElementById('modalInput').value;
if (modalCallback) modalCallback(value);
closeModal();
}
// ===== NOTIFICATIONS =====
let notifyTimeout = null;
function notify(message) {
const el = document.getElementById('notification');
el.textContent = message;
el.classList.add('visible');
clearTimeout(notifyTimeout);
notifyTimeout = setTimeout(() => el.classList.remove('visible'), 2500);
}
// ===== SIDEBAR TOGGLE =====
function toggleSidebar() {
state.sidebarCollapsed = !state.sidebarCollapsed;
document.getElementById('sidebar').classList.toggle('collapsed', state.sidebarCollapsed);
}
// ===== RESIZE HANDLE =====
function initResize() {
const handle = document.getElementById('resizeHandle');
const panel = document.getElementById('noteListPanel');
let startX, startWidth;
handle.addEventListener('mousedown', (e) => {
startX = e.clientX;
startWidth = panel.offsetWidth;
handle.classList.add('active');
document.addEventListener('mousemove', onResize);
document.addEventListener('mouseup', stopResize);
e.preventDefault();
});
function onResize(e) {
const diff = e.clientX - startX;
const newWidth = Math.max(180, Math.min(500, startWidth + diff));
panel.style.width = newWidth + 'px';
}
function stopResize() {
handle.classList.remove('active');
document.removeEventListener('mousemove', onResize);
document.removeEventListener('mouseup', stopResize);
}
}
// ===== KEYBOARD SHORTCUTS =====
function initKeyboard() {
document.addEventListener('keydown', (e) => {
// Ctrl+N: New note
if (e.ctrlKey || e.metaKey) {
if (e.key === 'n' || e.key === 'N') {
e.preventDefault();
createNote();
}
if (e.key === 's' || e.key === 'S') {
e.preventDefault();
saveNote();
notify('Note saved');
}
if (e.key === 'b' || e.key === 'B') {
if (e.shiftKey) return; // Let browser handle bold
e.preventDefault();
toggleSidebar();
}
if (e.key === 'f' || e.key === 'F') {
e.preventDefault();
toggleFindBar();
}
if (e.key === 't' || e.key === 'T') {
if (e.shiftKey) {
e.preventDefault();
createFolder();
}
}
}
// Escape
if (e.key === 'Escape') {
const findBar = document.getElementById('findBar');
if (findBar.classList.contains('visible')) {
toggleFindBar();
}
hideContextMenu();
closeModal();
}
// Delete note
if (e.key === 'Delete' && state.activeNoteId) {
const active = document.activeElement;
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)) return;
deleteNote(state.activeNoteId);
}
});
}
// ===== EVENT LISTENERS =====
function initEvents() {
// New note button
document.getElementById('btnNewNote').addEventListener('click', () => createNote());
// Collapse list button
document.getElementById('btnCollapseList').addEventListener('click', () => {
const panel = document.getElementById('noteListPanel');
panel.style.display = panel.style.display === 'none' ? 'flex' : 'none';
});
// Sidebar toggle
document.getElementById('btnToggleSidebar').addEventListener('click', toggleSidebar);
// Sidebar search
document.getElementById('sidebarSearch').addEventListener('input', (e) => {
state.searchQuery = e.target.value;
renderAll();
});
// Folder section toggle
document.querySelectorAll('.sidebar-section-title').forEach(title => {
title.addEventListener('click', () => {
const listId = title.getAttribute('data-toggle') + 'List';
const list = document.getElementById(listId);
const chevron = title.querySelector('.chevron');
list.classList.toggle('expanded');
chevron.classList.toggle('collapsed');
});
});
// Context menu
document.addEventListener('contextmenu', (e) => {
const card = e.target.closest('.note-card');
if (card) {
showContextMenu(e, card.dataset.noteId);
} else {
hideContextMenu();
}
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.context-menu')) {
hideContextMenu();
}
});
// Modal
document.getElementById('modalConfirm').addEventListener('click', confirmModal);
document.getElementById('modalInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') confirmModal();
if (e.key === 'Escape') closeModal();
});
document.getElementById('modalOverlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeModal();
});
// Find bar
document.getElementById('findInput').addEventListener('input', (e) => {
findInNote(e.target.value);
});
document.getElementById('findInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.shiftKey ? findPrev() : findNext();
}
if (e.key === 'Escape') toggleFindBar();
});
// Editor focus
document.getElementById('editorContent').addEventListener('click', (e) => {
const body = document.getElementById('editorBody');
if (body && !e.target.closest('.editor-title')) {
body.focus();
}
});
// Right-click on notes for context menu
document.addEventListener('mousedown', (e) => {
const card = e.target.closest('.note-card');
if (card && e.button === 2) {
showContextMenu(e, card.dataset.noteId);
}
});
// Init
initResize();
initKeyboard();
}
// ===== INIT =====
function init() {
loadState();
renderAll();
if (state.activeNoteId) {
const note = state.notes.find(n => n.id === state.activeNoteId);
if (note) {
openNote(state.activeNoteId);
} else {
state.activeNoteId = null;
showWelcome();
}
} else {
showWelcome();
}
initEvents();
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"full","sessionId":"ca1ebfad-5bac-428b-b659-4b8bd4fb5e5d","tokensIn":82288,"tokensOut":20788,"tokensTotal":103076,"turns":4,"toolCalls":3,"failedToolCalls":0,"timestamp":"2026-07-21T01:12:20.444Z"} -->