Notes
Qwen 3.6 27B MTP GGUF · typescript

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 76,536 bytes · SHA-256 7c11f25dce73
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notes — Personal Knowledge Manager</title>
<style>
/* ── css: layout ── */
:root {
--bg-primary: #faf9f7;
--bg-secondary: #f3f1ed;
--bg-tertiary: #ebe8e2;
--bg-hover: #e2ded7;
--bg-active: #d5cfc6;
--bg-selection: rgba(196, 114, 47, 0.08);
--text-primary: #1a1714;
--text-secondary: #5c564f;
--text-tertiary: #8a8279;
--text-placeholder: #b5afa8;
--border: #ddd8d1;
--border-light: #e8e4de;
--accent: #c4722f;
--accent-light: #f5e6d6;
--accent-hover: #a9601f;
--danger: #c0392b;
--danger-light: #f8d7d4;
--success: #27ae60;
--shadow-sm: 0 1px 3px rgba(44, 40, 37, 0.05);
--shadow-md: 0 4px 16px rgba(44, 40, 37, 0.08);
--shadow-lg: 0 8px 30px rgba(44, 40, 37, 0.12);
--radius: 8px;
--radius-sm: 5px;
--radius-lg: 12px;
--radius-xl: 16px;
--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-editor: "Georgia", "Iowan Old Style", "Palatino Linotype", "Palatino", "Times New Roman", serif;
--font-mono: "SF Mono", "Cascadia Code", "Fira Code", "Consolas", "Liberation Mono", monospace;
--sidebar-width: 250px;
--notelist-width: 300px;
--toolbar-height: 44px;
--transition-fast: 120ms cubic-bezier(0.2, 0, 0, 1);
--transition-normal: 200ms cubic-bezier(0.2, 0, 0, 1);
--transition-slow: 300ms cubic-bezier(0.2, 0, 0, 1);
}
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #222222;
--bg-tertiary: #2c2c2c;
--bg-hover: #333333;
--bg-active: #3c3c3c;
--bg-selection: rgba(224, 138, 62, 0.12);
--text-primary: #e8e4e0;
--text-secondary: #a09a93;
--text-tertiary: #6e6962;
--text-placeholder: #4a4742;
--border: #333333;
--border-light: #2c2c2c;
--accent: #e08a3e;
--accent-light: #3a2a18;
--accent-hover: #f09a4e;
--danger: #e74c3c;
--danger-light: #3a1a1a;
--success: #2ecc71;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 16px 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: var(--font-ui);
font-size: 13px;
line-height: 1.5;
color: var(--text-primary);
background: var(--bg-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
}
/* ── Toolbar ── */
.toolbar-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 0 10px;
height: var(--toolbar-height);
min-height: var(--toolbar-height);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
z-index: 100;
user-select: none;
}
.app-title {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.3px;
margin-right: auto;
padding-left: 2px;
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: var(--radius-sm);
font-size: 15px;
transition: background var(--transition-fast), color var(--transition-fast);
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.icon-btn:active { background: var(--bg-active); }
.search-container {
display: flex;
align-items: center;
width: 220px;
max-width: 35vw;
}
.search-input {
flex: 1;
height: 30px;
padding: 0 10px 0 30px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-primary) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='%238a8279' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85zm-5.242.656a5 5 0 1 1 0-10 5 5 0 0 1 0 10z'/%3E%3C/svg%3E") no-repeat 10px center;
color: var(--text-primary);
font-size: 12px;
font-family: var(--font-ui);
outline: none;
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.search-input::placeholder { color: var(--text-placeholder); }
.search-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-light);
}
/* ── Main Layout ── */
.main-layout { display: flex; flex: 1; overflow: hidden; }
.pane { display: flex; flex-direction: column; overflow: hidden; }
.sidebar-pane {
width: var(--sidebar-width);
min-width: 160px;
max-width: 400px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
flex-shrink: 0;
transition: width var(--transition-slow);
overflow-y: auto;
overflow-x: hidden;
}
.sidebar-pane.collapsed { width: 0 !important; min-width: 0 !important; border-right: none; }
.notelist-pane {
width: var(--notelist-width);
min-width: 200px;
max-width: 500px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
flex-shrink: 0;
}
.editor-pane {
flex: 1;
min-width: 300px;
background: var(--bg-primary);
overflow: hidden;
}
/* ── Resizer ── */
.resizer {
width: 3px;
cursor: col-resize;
background: transparent;
position: relative;
z-index: 10;
flex-shrink: 0;
transition: background var(--transition-fast);
}
.resizer::after {
content: '';
position: absolute;
inset: 0;
width: 6px;
margin-left: -2px;
}
.resizer:hover, .resizer.active { background: var(--accent); }
/* ── Sidebar Items ── */
.sidebar-section { padding: 0 6px; }
.sidebar-item {
display: flex;
align-items: center;
gap: 7px;
padding: 5px 8px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12.5px;
color: var(--text-secondary);
transition: background var(--transition-fast), color var(--transition-fast);
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-light); color: var(--accent); font-weight: 600; }
.sidebar-item .icon { flex-shrink: 0; width: 18px; text-align: center; font-size: 14px; }
.sidebar-item .count {
margin-left: auto;
font-size: 11px;
color: var(--text-tertiary);
flex-shrink: 0;
padding: 0 4px;
}
.sidebar-add-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 8px;
margin: 2px 0;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
font-size: 12px;
border-radius: var(--radius-sm);
width: calc(100% - 12px);
text-align: left;
transition: color var(--transition-fast), background var(--transition-fast);
}
.sidebar-add-btn:hover { background: var(--bg-hover); color: var(--text-secondary); }
/* Notebook groups */
.notebook-group { margin-bottom: 1px; }
.notebook-header {
display: flex;
align-items: center;
gap: 3px;
padding: 2px 4px;
cursor: pointer;
border-radius: var(--radius-sm);
transition: background var(--transition-fast);
}
.notebook-header:hover { background: var(--bg-hover); }
.notebook-header .chevron {
font-size: 9px;
transition: transform var(--transition-fast);
width: 14px;
text-align: center;
color: var(--text-tertiary);
flex-shrink: 0;
}
.notebook-header .chevron.collapsed { transform: rotate(-90deg); }
.notebook-notes { padding-left: 6px; overflow: hidden; }
.notebook-notes.collapsed { max-height: 0 !important; }
/* ── Note List ── */
.notelist-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px 10px;
border-bottom: 1px solid var(--border);
min-height: 52px;
}
.notelist-header h2 {
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.2px;
}
.notelist-actions { display: flex; gap: 1px; }
.notelist-actions button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
border-radius: var(--radius-sm);
font-size: 13px;
transition: color var(--transition-fast), background var(--transition-fast);
}
.notelist-actions button:hover { background: var(--bg-hover); color: var(--text-primary); }
.note-list { flex: 1; overflow-y: auto; padding: 4px 8px 16px; }
.note-item {
display: flex;
flex-direction: column;
gap: 3px;
padding: 10px 10px;
border-radius: var(--radius);
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast);
border: 1px solid transparent;
margin-bottom: 2px;
}
.note-item:hover { background: var(--bg-hover); }
.note-item.active {
background: var(--bg-tertiary);
border-color: var(--border);
box-shadow: var(--shadow-sm);
}
.note-item.pinned .note-item-title::before {
content: '📌 ';
font-size: 10px;
}
.note-item-title {
font-size: 12.5px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.35;
}
.note-item-snippet {
font-size: 11.5px;
color: var(--text-tertiary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
}
.note-item-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 10.5px;
color: var(--text-tertiary);
}
.note-item-tags { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 2px; }
.note-item-tag {
font-size: 10px;
padding: 1px 7px;
border-radius: 10px;
background: var(--bg-tertiary);
color: var(--text-tertiary);
cursor: pointer;
transition: background var(--transition-fast), color var(--transition-fast);
}
.note-item-tag:hover { background: var(--accent-light); color: var(--accent); }
.note-item-tag.active { background: var(--accent-light); color: var(--accent); }
/* ── Editor ── */
.editor-content {
display: none;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.editor-content.visible { display: flex; }
.empty-state {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 16px;
color: var(--text-tertiary);
}
.empty-state.visible { display: flex; }
.empty-state-icon {
font-size: 48px;
opacity: 0.35;
filter: grayscale(0.5);
}
.empty-state-text {
font-size: 15px;
font-weight: 500;
color: var(--text-tertiary);
}
/* Toolbar */
.editor-toolbar {
display: none;
align-items: center;
gap: 1px;
padding: 4px 16px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
min-height: 38px;
}
.editor-toolbar.visible { display: flex; }
.toolbar-group { display: flex; align-items: center; gap: 1px; }
.toolbar-group + .toolbar-group { margin-left: 6px; padding-left: 6px; border-left: 1px solid var(--border); }
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 26px;
padding: 0 5px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 4px;
font-size: 12px;
font-family: var(--font-ui);
transition: background var(--transition-fast), color var(--transition-fast);
user-select: none;
}
.toolbar-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.toolbar-btn.active { background: var(--accent-light); color: var(--accent); }
.toolbar-select {
height: 26px;
padding: 0 6px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 11px;
font-family: var(--font-ui);
cursor: pointer;
outline: none;
transition: border-color var(--transition-fast);
}
.toolbar-select:hover { border-color: var(--accent); }
.toolbar-select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
/* Editor content area */
.editor-wrapper {
flex: 1;
overflow-y: auto;
padding: 32px 48px 60px;
max-width: 720px;
margin: 0 auto;
width: 100%;
}
.note-title {
font-size: 26px;
font-weight: 700;
color: var(--text-primary);
border: none;
outline: none;
padding: 4px 0 6px;
margin-bottom: 2px;
background: transparent;
font-family: var(--font-ui);
line-height: 1.3;
letter-spacing: -0.3px;
}
.note-title:empty::before {
content: attr(data-placeholder);
color: var(--text-placeholder);
pointer-events: none;
}
.note-tags {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 0 14px;
flex-wrap: wrap;
}
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 10px;
border-radius: 12px;
background: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 11px;
cursor: pointer;
transition: background var(--transition-fast), color var(--transition-fast);
}
.tag:hover { background: var(--accent-light); color: var(--accent); }
.tag .remove-tag {
display: inline-flex;
align-items: center;
justify-content: center;
width: 13px;
height: 13px;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
border-radius: 50%;
font-size: 10px;
line-height: 1;
opacity: 0.5;
transition: opacity var(--transition-fast);
}
.tag .remove-tag:hover { opacity: 1; }
.add-tag-btn {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 8px;
border: 1px dashed var(--border);
border-radius: 12px;
background: transparent;
color: var(--text-tertiary);
font-size: 11px;
cursor: pointer;
transition: border-color var(--transition-fast), color var(--transition-fast);
}
.add-tag-btn:hover { border-color: var(--accent); color: var(--accent); }
/* Note body */
.note-body {
flex: 1;
outline: none;
font-family: var(--font-editor);
font-size: 15.5px;
line-height: 1.8;
color: var(--text-primary);
min-height: 150px;
padding-bottom: 40px;
}
.note-body:empty::before {
content: attr(data-placeholder);
color: var(--text-placeholder);
pointer-events: none;
font-style: italic;
}
.note-body h1 { font-size: 1.7em; font-weight: 700; margin: 0.6em 0 0.3em; line-height: 1.3; letter-spacing: -0.5px; }
.note-body h2 { font-size: 1.35em; font-weight: 700; margin: 0.5em 0 0.25em; line-height: 1.35; }
.note-body h3 { font-size: 1.15em; font-weight: 600; margin: 0.4em 0 0.15em; line-height: 1.4; }
.note-body blockquote {
margin: 0.4em 0;
padding: 0.3em 1em;
border-left: 3px solid var(--accent);
color: var(--text-secondary);
font-style: italic;
background: var(--bg-tertiary);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.note-body ul, .note-body ol { padding-left: 1.5em; margin: 0.2em 0; }
.note-body li { margin: 0.12em 0; }
.note-body code {
font-family: var(--font-mono);
font-size: 0.88em;
padding: 1px 5px;
background: var(--bg-tertiary);
border-radius: 3px;
color: var(--accent);
}
.note-body hr { margin: 1.2em 0; border: none; height: 1px; background: var(--border); }
/* Checklist */
.checklist-item {
list-style: none;
display: flex;
align-items: flex-start;
gap: 8px;
margin: 0.15em 0;
padding-left: 0 !important;
}
.checklist-item .check-box {
flex-shrink: 0;
width: 17px;
height: 17px;
border: 2px solid var(--border);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
transition: all var(--transition-fast);
font-size: 10px;
background: var(--bg-primary);
}
.checklist-item .check-box:hover { border-color: var(--accent); }
.checklist-item .check-box.checked {
background: var(--accent);
border-color: var(--accent);
}
.checklist-item .check-box.checked::after { content: '✓'; color: #fff; font-weight: 700; }
.checklist-item.checked-item .check-content { text-decoration: line-through; color: var(--text-tertiary); }
/* Saved indicator */
.saved-indicator {
position: fixed;
bottom: 16px;
right: 20px;
padding: 5px 14px;
border-radius: 20px;
background: var(--bg-secondary);
color: var(--text-tertiary);
font-size: 11px;
font-family: var(--font-ui);
box-shadow: var(--shadow-md);
opacity: 0;
transform: translateY(8px);
transition: opacity var(--transition-normal), transform var(--transition-normal);
pointer-events: none;
z-index: 1000;
}
.saved-indicator.visible { opacity: 1; transform: translateY(0); }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--bg-hover); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--bg-active); }
/* Context menu */
.context-menu {
position: fixed;
z-index: 1000;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
padding: 4px;
min-width: 180px;
}
.context-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12.5px;
color: var(--text-primary);
transition: background var(--transition-fast);
border: none;
background: none;
width: 100%;
text-align: left;
font-family: var(--font-ui);
}
.context-menu-item:hover { background: var(--bg-hover); }
.context-menu-item.danger { color: var(--danger); }
.context-menu-item.danger:hover { background: var(--danger-light); }
.context-menu-separator { height: 1px; background: var(--border); margin: 3px 0; }
.hidden { display: none !important; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* Move dialog */
.move-dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn var(--transition-fast) ease;
backdrop-filter: blur(2px);
}
.move-dialog {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: 20px;
width: 280px;
}
.move-dialog h3 { font-size: 15px; margin-bottom: 10px; }
.move-dialog-item {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
color: var(--text-primary);
transition: background var(--transition-fast);
}
.move-dialog-item:hover { background: var(--bg-hover); }
</style>
</head>
<body>
<div id="app" class="app">
<div id="toolbar-bar" class="toolbar-bar">
<button id="toggle-sidebar" class="icon-btn" title="Toggle Sidebar">☰</button>
<div class="app-title">Notes</div>
<div id="search-container" class="search-container">
<input id="search-input" type="text" placeholder="Search notes…" class="search-input">
<button id="clear-search" class="icon-btn hidden">✕</button>
</div>
<button id="theme-toggle" class="icon-btn" title="Toggle Theme">🌙</button>
</div>
<div id="main-layout" class="main-layout">
<div id="sidebar-pane" class="pane sidebar-pane"></div>
<div id="sidebar-resizer" class="resizer"></div>
<div id="notelist-pane" class="pane notelist-pane"></div>
<div id="notelist-resizer" class="resizer"></div>
<div id="editor-pane" class="pane editor-pane">
<div id="editor-toolbar" class="editor-toolbar"></div>
<div id="editor-content" class="editor-content">
<div class="editor-wrapper">
<div id="note-title" class="note-title" contenteditable="true" data-placeholder="Untitled"></div>
<div id="note-tags" class="note-tags"></div>
<div id="note-body" class="note-body" contenteditable="true" data-placeholder="Start writing…"></div>
</div>
</div>
<div id="empty-state" class="empty-state">
<div class="empty-state-icon">📝</div>
<div class="empty-state-text">Select or create a note</div>
</div>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./store":"src/store.ts","./sidebar":"src/sidebar.ts","./notelist":"src/notelist.ts","./editor":"src/editor.ts"},"src/store.ts":{"./types":"src/types.ts"},"src/utils.ts":{"./types":"src/types.ts"},"src/sidebar.ts":{"./types":"src/types.ts","./utils":"src/utils.ts"},"src/notelist.ts":{"./types":"src/types.ts","./utils":"src/utils.ts"},"src/editor.ts":{"./types":"src/types.ts","./utils":"src/utils.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("./types");
const store_1 = require("./store");
const sidebar_1 = require("./sidebar");
const notelist_1 = require("./notelist");
const editor_1 = require("./editor");
// Initialize
const state = (0, store_1.initState)();
applyTheme(state.theme);
applyPaneState(state.paneState);
// Reactive render function
let renderLock = false;
function renderAll() {
if (renderLock)
return;
renderLock = true;
const st = (0, store_1.getState)();
(0, sidebar_1.renderSidebar)(st);
(0, notelist_1.renderNoteList)(st);
renderLock = false;
}
// Wire editor save callback
(0, editor_1.setSaveCallback)((noteId, updates) => {
const st = (0, store_1.getState)();
const note = st.notes.find(n => n.id === noteId);
if (!note)
return;
Object.assign(note, updates);
(0, store_1.setState)(st);
renderAll();
});
// Initial render
renderAll();
(0, editor_1.initEditor)();
if (state.selectedNoteId) {
const note = state.notes.find(n => n.id === state.selectedNoteId);
if (note && !note.trashed) {
(0, editor_1.setActiveNote)(note);
(0, editor_1.setNote)(note);
}
}
// ============ EVENT HANDLERS ============
// Global action handler
document.addEventListener('app-action', (e) => {
const detail = e.detail;
handleAppAction(detail.action, detail);
});
// Context menu handler
document.addEventListener('note-context-menu', (e) => {
const detail = e.detail;
showNoteContextMenu(detail.noteId, detail.x, detail.y);
});
// Search
const searchInput = document.getElementById('search-input');
const clearSearch = document.getElementById('clear-search');
searchInput.addEventListener('input', () => {
const st = (0, store_1.getState)();
(0, store_1.setState)({ ...st, searchQuery: searchInput.value });
clearSearch.classList.toggle('hidden', !searchInput.value);
renderAll();
});
clearSearch.addEventListener('click', () => {
searchInput.value = '';
const st = (0, store_1.getState)();
(0, store_1.setState)({ ...st, searchQuery: '' });
clearSearch.classList.add('hidden');
renderAll();
searchInput.focus();
});
// Theme toggle
document.getElementById('theme-toggle').addEventListener('click', () => {
const st = (0, store_1.getState)();
const newTheme = st.theme === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
(0, store_1.setState)({ ...st, theme: newTheme });
});
// Sidebar toggle
document.getElementById('toggle-sidebar').addEventListener('click', () => {
const st = (0, store_1.getState)();
const collapsed = !st.paneState.sidebarCollapsed;
(0, store_1.setState)({ ...st, paneState: { ...st.paneState, sidebarCollapsed: collapsed } });
applyPaneState(st.paneState);
});
// Resizers
initResizer('sidebar-resizer', 'sidebar-pane', (delta) => {
const st = (0, store_1.getState)();
const newWidth = Math.max(160, Math.min(400, st.paneState.sidebarWidth + delta));
(0, store_1.setState)({ ...st, paneState: { ...st.paneState, sidebarWidth: newWidth } });
applyPaneState(st.paneState);
});
initResizer('notelist-resizer', 'notelist-pane', (delta) => {
const st = (0, store_1.getState)();
const newWidth = Math.max(180, Math.min(500, st.paneState.notelistWidth + delta));
(0, store_1.setState)({ ...st, paneState: { ...st.paneState, notelistWidth: newWidth } });
applyPaneState(st.paneState);
});
// ============ HELPER FUNCTIONS ============
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
const btn = document.getElementById('theme-toggle');
btn.textContent = theme === 'light' ? '🌙' : '☀️';
}
function applyPaneState(ps) {
const sidebar = document.getElementById('sidebar-pane');
const notelist = document.getElementById('notelist-pane');
sidebar.style.width = ps.sidebarWidth + 'px';
sidebar.classList.toggle('collapsed', ps.sidebarCollapsed);
notelist.style.width = ps.notelistWidth + 'px';
}
function handleAppAction(action, detail) {
const st = (0, store_1.getState)();
switch (action) {
case 'select-notebook': {
(0, store_1.setState)({ ...st, selectedNotebookId: detail.notebookId, selectedNoteId: null, activeTagFilter: null });
(0, editor_1.setActiveNote)(null);
(0, editor_1.setNote)(null);
renderAll();
break;
}
case 'toggle-notebook': {
const nb = st.notebooks.find(n => n.id === detail.notebookId);
if (nb)
nb.collapsed = !nb.collapsed;
(0, store_1.setState)({ ...st, notebooks: [...st.notebooks] });
renderAll();
break;
}
case 'add-notebook': {
const name = prompt('Notebook name:');
if (!name?.trim())
return;
const nb = (0, types_1.createNotebook)(name.trim());
(0, store_1.setState)({ ...st, notebooks: [...st.notebooks, nb] });
renderAll();
break;
}
case 'rename-notebook': {
const nb = st.notebooks.find(n => n.id === detail.notebookId);
if (!nb)
return;
const name = prompt('Rename notebook:', nb.name);
if (!name?.trim())
return;
nb.name = name.trim();
(0, store_1.setState)({ ...st, notebooks: [...st.notebooks] });
renderAll();
break;
}
case 'delete-notebook': {
const nb = st.notebooks.find(n => n.id === detail.notebookId);
if (!nb)
return;
if (!confirm(`Delete notebook "${nb.name}" and all its notes?`))
return;
const noteIds = st.notes.filter(n => n.notebookId === detail.notebookId).map(n => n.id);
(0, store_1.setState)({
...st,
notebooks: st.notebooks.filter(n => n.id !== detail.notebookId),
notes: st.notes.filter(n => !noteIds.includes(n.id)),
selectedNotebookId: st.selectedNotebookId === detail.notebookId ? null : st.selectedNotebookId,
selectedNoteId: noteIds.includes(st.selectedNoteId || '') ? null : st.selectedNoteId,
});
(0, editor_1.setActiveNote)(null);
(0, editor_1.setNote)(null);
renderAll();
break;
}
case 'add-note': {
const notebookId = detail.notebookId || st.selectedNotebookId || st.notebooks[0]?.id || null;
if (!notebookId)
return;
const note = (0, types_1.createNote)(notebookId);
(0, store_1.setState)({ ...st, notes: [...st.notes, note], selectedNotebookId: notebookId, selectedNoteId: note.id });
(0, editor_1.setActiveNote)(note);
(0, editor_1.setNote)(note);
renderAll();
setTimeout(() => document.getElementById('note-title')?.focus(), 50);
break;
}
case 'select-note': {
const note = st.notes.find(n => n.id === detail.noteId);
if (!note)
return;
(0, store_1.setState)({ ...st, selectedNoteId: note.id });
(0, editor_1.setActiveNote)(note);
(0, editor_1.setNote)(note);
renderAll();
break;
}
case 'sort-toggle': {
const orders = ['modified', 'created', 'alphabetical'];
const idx = orders.indexOf(st.sortOrder);
const nextOrder = orders[(idx + 1) % orders.length];
(0, store_1.setState)({ ...st, sortOrder: nextOrder });
renderAll();
break;
}
case 'filter-tag': {
(0, store_1.setState)({ ...st, activeTagFilter: detail.tag });
renderAll();
break;
}
case 'clear-tag-filter': {
(0, store_1.setState)({ ...st, activeTagFilter: null });
renderAll();
break;
}
case 'move-note': {
const note = st.notes.find(n => n.id === detail.noteId);
if (!note)
return;
note.notebookId = detail.targetNotebookId;
note.updatedAt = Date.now();
(0, store_1.setState)({ ...st, notes: [...st.notes] });
renderAll();
break;
}
case 'pin': {
const note = st.notes.find(n => n.id === detail.noteId);
if (!note)
return;
note.pinned = !note.pinned;
note.updatedAt = Date.now();
(0, store_1.setState)({ ...st, notes: [...st.notes] });
renderAll();
break;
}
case 'duplicate': {
const note = st.notes.find(n => n.id === detail.noteId);
if (!note)
return;
const dup = structuredClone(note);
dup.id = `id_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
dup.title = note.title + ' (copy)';
dup.pinned = false;
dup.createdAt = Date.now();
dup.updatedAt = Date.now();
dup.trashed = false;
dup.trashedAt = null;
dup.previousNotebookId = null;
(0, store_1.setState)({ ...st, notes: [...st.notes, dup] });
renderAll();
break;
}
case 'delete': {
const note = st.notes.find(n => n.id === detail.noteId);
if (!note)
return;
note.trashed = true;
note.trashedAt = Date.now();
note.previousNotebookId = note.notebookId;
(0, store_1.setState)({ ...st, notes: [...st.notes], selectedNoteId: null });
(0, editor_1.setActiveNote)(null);
(0, editor_1.setNote)(null);
renderAll();
break;
}
case 'restore': {
const note = st.notes.find(n => n.id === detail.noteId);
if (!note)
return;
note.trashed = false;
note.trashedAt = null;
note.notebookId = note.previousNotebookId || st.notebooks[0]?.id;
note.previousNotebookId = null;
note.updatedAt = Date.now();
(0, store_1.setState)({ ...st, notes: [...st.notes] });
renderAll();
break;
}
case 'permanently-delete': {
const note = st.notes.find(n => n.id === detail.noteId);
if (!note)
return;
(0, store_1.setState)({
...st,
notes: st.notes.filter(n => n.id !== detail.noteId),
selectedNoteId: st.selectedNoteId === detail.noteId ? null : st.selectedNoteId,
});
renderAll();
break;
}
case 'move': {
const note = st.notes.find(n => n.id === detail.noteId);
if (note)
showMoveDialog(note);
break;
}
default:
break;
}
}
// Context menu
function showNoteContextMenu(noteId, x, y) {
removeContextMenu();
const st = (0, store_1.getState)();
const note = st.notes.find(n => n.id === noteId);
if (!note)
return;
const isTrashed = st.selectedNotebookId === 'trash';
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.id = 'context-menu';
let items = '';
if (!isTrashed) {
items += `<button class="context-menu-item" data-ctx-action="pin" data-note-id="${noteId}">${note.pinned ? 'Unpin' : '📌 Pin'}</button>`;
items += `<button class="context-menu-item" data-ctx-action="duplicate" data-note-id="${noteId}">📋 Duplicate</button>`;
items += `<div class="context-menu-separator"></div>`;
items += `<button class="context-menu-item" data-ctx-action="move" data-note-id="${noteId}">📂 Move to…</button>`;
items += `<div class="context-menu-separator"></div>`;
items += `<button class="context-menu-item danger" data-ctx-action="delete" data-note-id="${noteId}">🗑️ Delete</button>`;
}
else {
items += `<button class="context-menu-item" data-ctx-action="restore" data-note-id="${noteId}">↩ Restore</button>`;
items += `<div class="context-menu-separator"></div>`;
items += `<button class="context-menu-item danger" data-ctx-action="permanently-delete" data-note-id="${noteId}">🗑️ Delete Permanently</button>`;
}
menu.innerHTML = items;
menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
menu.style.top = Math.min(y, window.innerHeight - 150) + 'px';
document.body.appendChild(menu);
menu.addEventListener('click', (e) => {
const btn = e.target.closest('.context-menu-item');
if (!btn)
return;
const ctxAction = btn.getAttribute('data-ctx-action');
const ctxNoteId = btn.getAttribute('data-note-id');
if (ctxAction === 'permanently-delete') {
if (!confirm('Permanently delete this note?'))
return;
}
document.dispatchEvent(new CustomEvent('app-action', {
detail: { action: ctxAction, noteId: ctxNoteId }
}));
removeContextMenu();
});
setTimeout(() => {
const closeMenu = (ev) => {
if (!menu.contains(ev.target)) {
removeContextMenu();
document.removeEventListener('mousedown', closeMenu);
}
};
document.addEventListener('mousedown', closeMenu);
}, 0);
}
function removeContextMenu() {
const existing = document.getElementById('context-menu');
if (existing)
existing.remove();
}
function showMoveDialog(note) {
const st = (0, store_1.getState)();
const overlay = document.createElement('div');
overlay.className = 'move-dialog-overlay';
const dialog = document.createElement('div');
dialog.className = 'move-dialog';
dialog.innerHTML = `<h3>Move to notebook</h3>`;
st.notebooks.forEach(nb => {
const item = document.createElement('div');
item.className = 'move-dialog-item';
item.innerHTML = `<span>📁</span><span>${nb.name}</span>`;
if (nb.id === note.notebookId)
item.style.opacity = '0.5';
item.addEventListener('click', () => {
if (nb.id !== note.notebookId) {
document.dispatchEvent(new CustomEvent('app-action', {
detail: { action: 'move-note', noteId: note.id, targetNotebookId: nb.id }
}));
}
overlay.remove();
});
dialog.appendChild(item);
});
overlay.appendChild(dialog);
overlay.addEventListener('click', (e) => {
if (e.target === overlay)
overlay.remove();
});
document.body.appendChild(overlay);
}
// Resizer
function initResizer(resizerId, paneId, onResize) {
const resizer = document.getElementById(resizerId);
const pane = document.getElementById(paneId);
if (!resizer || !pane)
return;
resizer.addEventListener('mousedown', (e) => {
const startX = e.clientX;
const startWidth = pane.offsetWidth;
resizer.classList.add('active');
const onMouseMove = (e) => {
onResize(e.clientX - startX);
};
const onMouseUp = () => {
resizer.classList.remove('active');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
}
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSTANTS = void 0;
exports.createNotebook = createNotebook;
exports.createNote = createNote;
exports.genId = genId;
exports.emptyState = emptyState;
// Tunable constants
exports.CONSTANTS = {
// Colors (matching CSS vars, duplicated for JS use)
colors: {
accent: '#c4722f',
},
// Fonts
fonts: {
ui: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
editor: 'Georgia, "Iowan Old Style", "Palatino Linotype", serif',
},
// Autosave
autosaveDebounceMs: 500,
// Snippet
snippetLength: 100,
// Persistence
storageKey: 'notes-app-data',
};
function createNotebook(name) {
return {
id: genId(),
name,
collapsed: false,
createdAt: Date.now(),
};
}
function createNote(notebookId, title = '', body = '') {
const now = Date.now();
return {
id: genId(),
notebookId,
title,
body,
tags: [],
pinned: false,
createdAt: now,
updatedAt: now,
trashed: false,
trashedAt: null,
previousNotebookId: null,
};
}
let idCounter = 0;
function genId() {
idCounter++;
return `id_${Date.now()}_${idCounter}_${Math.random().toString(36).slice(2, 8)}`;
}
function emptyState() {
return {
notebooks: [createNotebook('General')],
notes: [],
selectedNotebookId: null,
selectedNoteId: null,
sortOrder: 'modified',
searchQuery: '',
activeTagFilter: null,
theme: 'light',
paneState: {
sidebarWidth: 240,
notelistWidth: 280,
sidebarCollapsed: false,
},
};
}
};
// ── module: src/store.ts ──
__mods["src/store.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initState = initState;
exports.getState = getState;
exports.setState = setState;
exports.scheduleSave = scheduleSave;
exports.saveImmediately = saveImmediately;
const types_1 = require("./types");
let state;
let saveTimeout = null;
function initState() {
const loaded = loadState();
state = loaded || (0, types_1.emptyState)();
// Migrations
if (!state.theme)
state.theme = 'light';
if (!state.paneState)
state.paneState = { sidebarWidth: 240, notelistWidth: 280, sidebarCollapsed: false };
// Ensure note notebookId defaults
state.notes.forEach(n => { if (!n.notebookId)
n.notebookId = state.notebooks[0]?.id || ''; });
return state;
}
function getState() {
return state;
}
function setState(s) {
state = s;
// Persist immediately for state mutations (delete, restore, etc.)
// Editor input uses scheduleSave for debounce
saveState();
}
function loadState() {
try {
const raw = localStorage.getItem(types_1.CONSTANTS.storageKey);
if (!raw)
return null;
return JSON.parse(raw);
}
catch {
return null;
}
}
function saveState() {
try {
localStorage.setItem(types_1.CONSTANTS.storageKey, JSON.stringify(state));
}
catch (e) {
console.error('Failed to save state:', e);
}
}
function scheduleSave(onSaved) {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
if (onSaved) {
// Store callback
saveTimeout.__onSaved = onSaved;
}
saveTimeout = setTimeout(() => {
saveState();
const cb = saveTimeout.__onSaved;
delete saveTimeout.__onSaved;
if (cb)
cb();
}, types_1.CONSTANTS.autosaveDebounceMs);
}
function saveImmediately() {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveState();
}
};
// ── module: src/utils.ts ──
__mods["src/utils.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripHtml = stripHtml;
exports.getSnippet = getSnippet;
exports.sanitizeHtml = sanitizeHtml;
exports.searchNotes = searchNotes;
exports.filterByTag = filterByTag;
exports.sortNotes = sortNotes;
exports.formatTime = formatTime;
exports.getNotesForNotebook = getNotesForNotebook;
exports.countNotes = countNotes;
const types_1 = require("./types");
// Strip HTML tags to get plain text for search/snippets
function stripHtml(html) {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
}
// Extract text snippet
function getSnippet(note) {
const text = stripHtml(note.body);
return text.length > types_1.CONSTANTS.snippetLength
? text.slice(0, types_1.CONSTANTS.snippetLength) + '…'
: text;
}
// Sanitize HTML - remove scripts, event handlers, foreign markup
function sanitizeHtml(html) {
const tmp = document.createElement('div');
tmp.innerHTML = html;
// Remove script and other dangerous elements
const dangerous = tmp.querySelectorAll('script,style,link,meta,iframe,object,embed,form,button,input,textarea,select');
dangerous.forEach(el => el.remove());
// Remove event handlers from all elements
const all = tmp.querySelectorAll('*');
all.forEach(el => {
const attrs = Array.from(el.attributes);
attrs.forEach(attr => {
if (attr.name.startsWith('on')) {
el.removeAttribute(attr.name);
}
});
});
return tmp.innerHTML;
}
// Search in plain text content
function searchNotes(notes, query) {
if (!query.trim())
return notes;
const q = query.toLowerCase();
return notes.filter(n => {
const title = n.title.toLowerCase();
const text = stripHtml(n.body).toLowerCase();
return title.includes(q) || text.includes(q);
});
}
// Filter by tag
function filterByTag(notes, tag) {
if (!tag)
return notes;
return notes.filter(n => n.tags.includes(tag));
}
// Sort notes
function sortNotes(notes, order) {
const pinned = notes.filter(n => n.pinned);
const rest = notes.filter(n => !n.pinned);
const sortFn = (a, b) => {
switch (order) {
case 'modified':
return b.updatedAt - a.updatedAt;
case 'created':
return b.createdAt - a.createdAt;
case 'alphabetical':
return a.title.localeCompare(b.title) || a.updatedAt - b.updatedAt;
default:
return 0;
}
};
return [...pinned.sort(sortFn), ...rest.sort(sortFn)];
}
// Format timestamp
function formatTime(ts) {
const d = new Date(ts);
const now = new Date();
const diff = now.getTime() - ts;
if (diff < 60 * 1000)
return 'Just now';
if (diff < 60 * 60 * 1000)
return `${Math.floor(diff / 60000)}m ago`;
if (diff < 24 * 60 * 60 * 1000)
return `${Math.floor(diff / 3600000)}h ago`;
if (d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear()) {
return d.toLocaleDateString('en', { month: 'short', day: 'numeric' });
}
return d.toLocaleDateString('en', { month: 'short', day: 'numeric', year: 'numeric' });
}
// Get notes for a notebook
function getNotesForNotebook(state) {
let notes;
if (state.selectedNotebookId === 'trash') {
notes = state.notes.filter(n => n.trashed);
}
else if (state.selectedNotebookId === null) {
notes = state.notes.filter(n => !n.trashed);
}
else {
notes = state.notes.filter(n => n.notebookId === state.selectedNotebookId && !n.trashed);
}
if (state.activeTagFilter) {
notes = filterByTag(notes, state.activeTagFilter);
}
if (state.searchQuery.trim()) {
notes = searchNotes(notes, state.searchQuery);
}
return sortNotes(notes, state.sortOrder);
}
// Count notes per notebook
function countNotes(notebookId, state) {
if (notebookId === 'trash')
return state.notes.filter(n => n.trashed).length;
if (notebookId === null)
return state.notes.filter(n => !n.trashed).length;
return state.notes.filter(n => n.notebookId === notebookId && !n.trashed).length;
}
};
// ── module: src/sidebar.ts ──
__mods["src/sidebar.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderSidebar = renderSidebar;
const utils_1 = require("./utils");
function renderSidebar(state) {
const pane = document.getElementById('sidebar-pane');
let html = '';
// Special sections: All Notes, Trash
html += renderSpecialItem(state, null, '📋', 'All Notes');
html += renderSpecialItem(state, 'trash', '🗑️', 'Trash');
html += '<div class="sidebar-section" style="margin-top: 8px">';
html += '<div class="sidebar-header"><h2>Notebooks</h2></div>';
html += '<button class="sidebar-add-btn" data-action="add-notebook">+ New Notebook</button>';
html += '</div>';
// Notebook groups
for (const nb of state.notebooks) {
html += renderNotebookGroup(state, nb);
}
pane.innerHTML = html;
bindSidebarEvents(state, pane);
}
function renderSpecialItem(state, id, icon, label) {
const active = state.selectedNotebookId === id ? 'active' : '';
const count = (0, utils_1.countNotes)(id, state);
const countStr = count > 0 ? `<span class="count">${count}</span>` : '';
return `<div class="sidebar-item ${active}" data-notebook-id="${id}">
<span class="icon">${icon}</span>
<span>${label}</span>
${countStr}
</div>`;
}
function renderNotebookGroup(state, nb) {
const active = state.selectedNotebookId === nb.id ? 'active' : '';
const collapsed = nb.collapsed ? 'collapsed' : '';
const count = (0, utils_1.countNotes)(nb.id, state);
const chevronClass = nb.collapsed ? 'chevron collapsed' : 'chevron';
let html = `<div class="notebook-group" data-notebook-id="${nb.id}">`;
html += `<div class="notebook-header" data-toggle-notebook="${nb.id}">
<span class="${chevronClass}">▼</span>
<span class="sidebar-item ${active}" data-notebook-id="${nb.id}" style="flex:1">
<span class="icon">📁</span>
<span>${nb.name}</span>
<span class="count">${count}</span>
</span>
<div class="actions">
<button data-action="rename-notebook" data-notebook-id="${nb.id}" title="Rename">✎</button>
<button data-action="delete-notebook" data-notebook-id="${nb.id}" title="Delete">✕</button>
</div>
</div>`;
html += `<div class="notebook-notes ${collapsed}">`;
html += `<button class="sidebar-add-btn" data-action="add-note" data-notebook-id="${nb.id}">+ New Note</button>`;
html += '</div>';
html += '</div>';
return html;
}
function bindSidebarEvents(_state, pane) {
// Notebook selection clicks
pane.addEventListener('click', (e) => {
const action = e.target.getAttribute('data-action');
const notebookId = e.target.getAttribute('data-notebook-id');
const toggleId = e.target.getAttribute('data-toggle-notebook');
if (action === 'add-notebook') {
dispatchCustomEvent('app-action', { action: 'add-notebook' });
return;
}
if (action === 'add-note') {
dispatchCustomEvent('app-action', { action: 'add-note', notebookId });
return;
}
if (action === 'rename-notebook') {
dispatchCustomEvent('app-action', { action: 'rename-notebook', notebookId });
return;
}
if (action === 'delete-notebook') {
dispatchCustomEvent('app-action', { action: 'delete-notebook', notebookId });
return;
}
if (toggleId) {
dispatchCustomEvent('app-action', { action: 'toggle-notebook', notebookId: toggleId });
return;
}
if (notebookId !== null && !action) {
dispatchCustomEvent('app-action', { action: 'select-notebook', notebookId });
}
});
// Drag and drop for moving notes
pane.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
pane.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
const notebookId = getClosestAttr(e.target, 'data-notebook-id');
if (notebookId && notebookId !== 'trash') {
const noteId = (e.dataTransfer?.getData('note-id') || '');
if (noteId) {
dispatchCustomEvent('app-action', { action: 'move-note', noteId, targetNotebookId: notebookId });
}
}
});
}
function getClosestAttr(el, attr) {
let current = el;
while (current) {
const val = current.getAttribute(attr);
if (val !== null)
return val;
current = current.parentElement;
}
return null;
}
function dispatchCustomEvent(type, detail) {
document.dispatchEvent(new CustomEvent(type, { detail }));
}
};
// ── module: src/notelist.ts ──
__mods["src/notelist.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderNoteList = renderNoteList;
const utils_1 = require("./utils");
function renderNoteList(state) {
const pane = document.getElementById('notelist-pane');
const notes = (0, utils_1.getNotesForNotebook)(state);
const notebookName = getNotebookName(state);
const tagFilterText = state.activeTagFilter
? `<span style="font-size:12px;color:var(--accent)">Filtering by: #${state.activeTagFilter}</span>`
: '';
pane.innerHTML = `
<div class="notelist-header">
<div>
<h2>${notebookName}</h2>
${tagFilterText}
</div>
<div class="notelist-actions">
${state.selectedNotebookId !== 'trash' ? `<button data-action="add-note" title="New Note">+</button>` : ''}
<button data-action="sort-toggle" title="Sort Order" class="${state.sortOrder}">${sortIcon(state.sortOrder)}</button>
</div>
</div>
<div class="note-list" id="note-list">
${notes.length === 0 ? renderEmptyNoteList(state) : notes.map(n => renderNoteItem(n, state)).join('')}
</div>
`;
bindNoteListEvents(state, pane);
}
function getNotebookName(state) {
if (state.selectedNotebookId === null)
return 'All Notes';
if (state.selectedNotebookId === 'trash')
return 'Trash';
const nb = state.notebooks.find(n => n.id === state.selectedNotebookId);
return nb ? nb.name : 'Unknown';
}
function sortIcon(order) {
switch (order) {
case 'modified': return '↕';
case 'created': return '↑';
case 'alphabetical': return 'A→Z';
default: return '↕';
}
}
function renderEmptyNoteList(state) {
if (state.searchQuery.trim()) {
return `<div style="padding:24px;text-align:center;color:var(--text-tertiary);font-size:13px">No notes match your search</div>`;
}
return `<div style="padding:24px;text-align:center;color:var(--text-tertiary);font-size:13px">No notes yet</div>`;
}
function renderNoteItem(note, state) {
const active = state.selectedNoteId === note.id ? 'active' : '';
const pinned = note.pinned ? 'pinned' : '';
const snippet = (0, utils_1.getSnippet)(note) || (state.selectedNotebookId === 'trash' ? `Trashed ${(0, utils_1.formatTime)(note.trashedAt || 0)}` : 'Empty note');
const time = (0, utils_1.formatTime)(state.selectedNotebookId === 'trash' && note.trashedAt ? note.trashedAt : note.updatedAt);
const isTrash = state.selectedNotebookId === 'trash';
const tagsHtml = note.tags.length > 0
? `<div class="note-item-tags">${note.tags.map(t => `<span class="note-item-tag ${state.activeTagFilter === t ? 'active' : ''}" data-tag="${t}">${t}</span>`).join('')}</div>`
: '';
return `<div class="note-item ${active} ${pinned}" data-note-id="${note.id}" draggable="${!isTrash}"
data-note-id="${note.id}">
<div class="note-item-title">${escapeHtml(note.title) || 'Untitled'}</div>
<div class="note-item-snippet">${escapeHtml(snippet)}</div>
<div class="note-item-meta">
<span>${time}</span>
</div>
${tagsHtml}
</div>`;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function bindNoteListEvents(state, pane) {
pane.addEventListener('click', (e) => {
const actionEl = e.target.closest('[data-action]');
const tagEl = e.target.closest('[data-tag]');
const noteEl = e.target.closest('[data-note-id]');
if (actionEl) {
const action = actionEl.getAttribute('data-action');
dispatchCustomEvent('app-action', { action });
return;
}
if (tagEl) {
const tag = tagEl.getAttribute('data-tag');
if (state.activeTagFilter === tag) {
dispatchCustomEvent('app-action', { action: 'clear-tag-filter' });
}
else {
dispatchCustomEvent('app-action', { action: 'filter-tag', tag });
}
return;
}
if (noteEl) {
const noteId = noteEl.getAttribute('data-note-id');
dispatchCustomEvent('app-action', { action: 'select-note', noteId });
}
});
// Right-click context menu
const noteList = pane.querySelector('.note-list');
if (noteList) {
noteList.addEventListener('contextmenu', (e) => {
const me = e;
const noteEl = e.target.closest('.note-item');
if (noteEl) {
e.preventDefault();
const noteId = noteEl.getAttribute('data-note-id');
dispatchCustomEvent('note-context-menu', { noteId, x: me.clientX, y: me.clientY });
}
});
}
// Drag start
pane.addEventListener('dragstart', (e) => {
const noteItem = e.target.closest('.note-item');
if (noteItem && noteItem.draggable) {
const noteId = noteItem.getAttribute('data-note-id');
e.dataTransfer?.setData('note-id', noteId);
e.dataTransfer.effectAllowed = 'move';
}
});
pane.addEventListener('dragend', (e) => {
const noteItem = e.target.closest('.note-item');
if (noteItem) {
noteItem.style.opacity = '';
}
});
}
function dispatchCustomEvent(type, detail) {
document.dispatchEvent(new CustomEvent(type, { detail }));
}
};
// ── module: src/editor.ts ──
__mods["src/editor.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setSaveCallback = setSaveCallback;
exports.setActiveNote = setActiveNote;
exports.initEditor = initEditor;
exports.setNote = setNote;
exports.handleMarkdownShortcut = handleMarkdownShortcut;
const utils_1 = require("./utils");
let saveCallback = null;
let titleSaveTimer = null;
let bodySaveTimer = null;
let _activeNote = null;
function setSaveCallback(cb) {
saveCallback = cb;
}
function setActiveNote(note) {
_activeNote = note;
}
function getActiveNote() {
return _activeNote;
}
function onSave(updates) {
if (!saveCallback)
return;
const note = getActiveNote();
if (!note)
return;
saveCallback(note.id, updates);
}
function initEditor() {
bindEditorEvents();
buildToolbar();
}
function setNote(note) {
const titleEl = document.getElementById('note-title');
const bodyEl = document.getElementById('note-body');
const contentEl = document.getElementById('editor-content');
const emptyEl = document.getElementById('empty-state');
const toolbarEl = document.getElementById('editor-toolbar');
const tagsEl = document.getElementById('note-tags');
if (!note) {
contentEl.classList.remove('visible');
toolbarEl.classList.remove('visible');
emptyEl.classList.add('visible');
emptyEl.classList.remove('hidden');
return;
}
contentEl.classList.add('visible');
toolbarEl.classList.add('visible');
emptyEl.classList.remove('visible');
emptyEl.classList.add('hidden');
if (document.activeElement !== titleEl) {
titleEl.textContent = note.title;
}
if (document.activeElement !== bodyEl) {
bodyEl.innerHTML = note.body || '';
}
tagsEl.innerHTML = note.tags.map(t => `<span class="tag" data-tag="${t}">#${escapeHtml(t)}<button class="remove-tag" data-action="remove-tag" data-tag="${escapeHtml(t)}">×</button></span>`).join('') + `<button class="add-tag-btn" data-action="add-tag">+ tag</button>`;
bindTagEvents(tagsEl);
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function bindTagEvents(container) {
container.addEventListener('click', (e) => {
const removeBtn = e.target.closest('[data-action="remove-tag"]');
if (removeBtn) {
e.preventDefault();
const tag = removeBtn.getAttribute('data-tag');
const note = getActiveNote();
if (note) {
onSave({ tags: note.tags.filter(t => t !== tag) });
}
removeBtn.closest('.tag')?.remove();
return;
}
const addBtn = e.target.closest('[data-action="add-tag"]');
if (addBtn) {
const tag = prompt('Enter tag name:');
if (tag && tag.trim()) {
const note = getActiveNote();
if (note && !note.tags.includes(tag.trim())) {
onSave({ tags: [...note.tags, tag.trim()] });
const span = document.createElement('span');
span.className = 'tag';
span.dataset.tag = tag.trim();
span.innerHTML = `#${escapeHtml(tag.trim())}<button class="remove-tag" data-action="remove-tag" data-tag="${escapeHtml(tag.trim())}">×</button>`;
addBtn.before(span);
}
}
}
});
}
function bindEditorEvents() {
const titleEl = document.getElementById('note-title');
const bodyEl = document.getElementById('note-body');
titleEl.addEventListener('input', () => {
const note = getActiveNote();
if (!note)
return;
if (titleSaveTimer)
clearTimeout(titleSaveTimer);
titleSaveTimer = setTimeout(() => {
const val = titleEl.textContent || '';
if (note.title !== val) {
onSave({ title: val, updatedAt: Date.now() });
showSaved();
}
}, 300);
});
bodyEl.addEventListener('input', () => {
const note = getActiveNote();
if (!note)
return;
if (bodySaveTimer)
clearTimeout(bodySaveTimer);
bodySaveTimer = setTimeout(() => {
const val = bodyEl.innerHTML || '';
if (note.body !== val) {
onSave({ body: val, updatedAt: Date.now() });
showSaved();
}
}, 300);
});
// Markdown shortcuts on keydown before default
bodyEl.addEventListener('keydown', (e) => {
// Handle markdown shortcuts (space key)
if (handleMarkdownShortcut(e, bodyEl))
return;
// Ctrl/Cmd+B/I/U
if ((e.ctrlKey || e.metaKey) && !e.altKey) {
if (e.key === 'b') {
e.preventDefault();
document.execCommand('bold');
updateToolbarState();
return;
}
if (e.key === 'i') {
e.preventDefault();
document.execCommand('italic');
updateToolbarState();
return;
}
if (e.key === 'u') {
e.preventDefault();
document.execCommand('underline');
updateToolbarState();
return;
}
}
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertText', false, ' ');
return;
}
if (e.key === 'Enter' && !e.shiftKey) {
handleEnterKey(e, bodyEl);
}
});
// Paste sanitization
bodyEl.addEventListener('paste', (e) => {
e.preventDefault();
const clipboardData = e.clipboardData;
if (!clipboardData)
return;
let html = clipboardData.getData('text/html');
if (html) {
html = (0, utils_1.sanitizeHtml)(html);
html = filterSupportedHtml(html);
document.execCommand('insertHTML', false, html);
}
else {
const text = clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
}
updateToolbarState();
});
document.addEventListener('selectionchange', () => {
if (document.activeElement === bodyEl || bodyEl.contains(document.activeElement)) {
updateToolbarState();
}
});
}
function filterSupportedHtml(html) {
const div = document.createElement('div');
div.innerHTML = html;
const unsupported = div.querySelectorAll('table,tbody,thead,tfoot,tr,td,th,svg,canvas,audio,video,map,area,figure,figcaption,details,summary,datalist,optgroup,option,dialog,fieldset,legend,picture,source,track,noscript,template,slot');
unsupported.forEach(el => {
el.replaceWith(document.createTextNode(el.textContent || ''));
});
return div.innerHTML;
}
function handleEnterKey(e, bodyEl) {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
const node = sel.anchorNode;
const element = node?.nodeType === Node.TEXT_NODE ? node.parentElement : node;
if (!element)
return;
const li = element.closest('li');
if (li) {
const parentList = li.closest('ul,ol');
if (parentList) {
if (li.classList.contains('checklist-item')) {
const contentSpan = li.querySelector('.check-content');
const text = contentSpan?.textContent?.trim() || '';
if (text === '' || text === '<br>') {
e.preventDefault();
li.remove();
if (parentList.children.length === 0) {
const p = document.createElement('p');
p.innerHTML = '<br>';
parentList.replaceWith(p);
const range = document.createRange();
range.setStart(p, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
return;
}
e.preventDefault();
insertChecklistItem(bodyEl);
return;
}
// Regular list: if empty, exit
const liText = li.textContent?.trim() || '';
if (liText === '' || liText === '•' || liText === '-') {
e.preventDefault();
li.remove();
if (parentList.children.length === 0) {
const p = document.createElement('p');
p.innerHTML = '<br>';
parentList.replaceWith(p);
const range = document.createRange();
range.setStart(p, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
return;
}
// Browser handles list continuation by default
}
}
}
function insertChecklistItem(bodyEl) {
const sel = window.getSelection();
let ul = null;
let currentLi = null;
if (sel && sel.rangeCount > 0) {
const anchorNode = sel.anchorNode;
const anchorEl = anchorNode?.nodeType === Node.TEXT_NODE ? anchorNode.parentElement : anchorNode;
if (anchorEl) {
ul = anchorEl.closest('ul');
currentLi = anchorEl?.closest('li') || null;
}
}
const li = document.createElement('li');
li.className = 'checklist-item';
const checkbox = document.createElement('span');
checkbox.className = 'check-box';
checkbox.contentEditable = 'false';
checkbox.addEventListener('click', () => {
checkbox.classList.toggle('checked');
li.classList.toggle('checked-item');
// Trigger save
const note = getActiveNote();
if (note) {
onSave({ body: bodyEl.innerHTML, updatedAt: Date.now() });
}
});
const content = document.createElement('span');
content.className = 'check-content';
content.contentEditable = 'true';
content.innerHTML = '';
li.appendChild(checkbox);
li.appendChild(content);
if (ul) {
if (currentLi) {
ul.insertBefore(li, currentLi.nextSibling);
}
else {
ul.appendChild(li);
}
}
else {
ul = document.createElement('ul');
ul.style.listStyle = 'none';
ul.appendChild(li);
bodyEl.appendChild(ul);
}
// Also mark parent ul as checklist container
if (ul && !ul.classList.contains('checklist')) {
ul.classList.add('checklist');
// Convert any sibling non-checklist lis
ul.querySelectorAll('li:not(.checklist-item)').forEach(sibLi => {
const newLi = document.createElement('li');
newLi.className = 'checklist-item';
const cb = document.createElement('span');
cb.className = 'check-box';
cb.contentEditable = 'false';
cb.addEventListener('click', () => {
cb.classList.toggle('checked');
newLi.classList.toggle('checked-item');
const note = getActiveNote();
if (note)
onSave({ body: bodyEl.innerHTML, updatedAt: Date.now() });
});
const ct = document.createElement('span');
ct.className = 'check-content';
ct.contentEditable = 'true';
ct.innerHTML = sibLi.innerHTML;
newLi.appendChild(cb);
newLi.appendChild(ct);
sibLi.replaceWith(newLi);
});
}
setTimeout(() => {
content.focus();
const range = document.createRange();
range.setStart(content, 0);
range.collapse(true);
if (sel) {
sel.removeAllRanges();
sel.addRange(range);
}
}, 0);
}
function buildToolbar() {
const toolbar = document.getElementById('editor-toolbar');
const buttons = [
{ cmd: 'bold', icon: 'B', label: 'Bold', style: 'font-weight:700' },
{ cmd: 'italic', icon: 'I', label: 'Italic', style: 'font-style:italic' },
{ cmd: 'underline', icon: 'U', label: 'Underline', style: 'text-decoration:underline' },
{ cmd: 'strikeThrough', icon: 'S̶', label: 'Strikethrough', style: 'text-decoration:line-through' },
{ cmd: null, icon: '⟨⟩', label: 'Inline Code', action: 'code', style: '' },
];
toolbar.innerHTML = `
<div class="toolbar-group">
${buttons.map(b => `<button class="toolbar-btn" data-cmd="${b.cmd || ''}" data-action="${b.action || ''}" title="${b.label}" ${b.style ? `style="${b.style}"` : ''}>${b.icon}</button>`).join('')}
</div>
<div class="toolbar-group">
<select class="toolbar-select" id="heading-select">
<option value="p">Text</option>
<option value="h1">H1</option>
<option value="h2">H2</option>
<option value="h3">H3</option>
</select>
</div>
<div class="toolbar-group">
<button class="toolbar-btn" data-cmd="insertUnorderedList" title="Bullet List">•≡</button>
<button class="toolbar-btn" data-cmd="insertOrderedList" title="Numbered List">1.</button>
<button class="toolbar-btn" data-action="checklist" title="Checklist">☑</button>
</div>
<div class="toolbar-group">
<button class="toolbar-btn" data-action="blockquote" title="Blockquote">❝</button>
<button class="toolbar-btn" data-cmd="insertHorizontalRule" title="Horizontal Rule">—</button>
</div>
<div class="toolbar-group">
<button class="toolbar-btn" data-cmd="undo" title="Undo (Ctrl+Z)">↩</button>
<button class="toolbar-btn" data-cmd="redo" title="Redo (Ctrl+Shift+Z)">↪</button>
</div>
`;
toolbar.addEventListener('mousedown', (e) => {
const btn = e.target.closest('.toolbar-btn');
if (!btn)
return;
e.preventDefault();
const cmd = btn.getAttribute('data-cmd');
const action = btn.getAttribute('data-action');
if (cmd) {
document.execCommand(cmd, false, undefined);
updateToolbarState();
document.getElementById('note-body')?.focus();
}
if (action === 'code') {
wrapSelection('code');
updateToolbarState();
document.getElementById('note-body')?.focus();
}
if (action === 'checklist') {
const bodyEl = document.getElementById('note-body');
insertChecklistItem(bodyEl);
}
if (action === 'blockquote') {
const sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
const anchorNode = sel.anchorNode;
const block = findBlockElement(anchorNode);
if (block && block.tagName === 'BLOCKQUOTE') {
unwrapElement(block);
}
else {
wrapSelection('blockquote');
}
}
updateToolbarState();
document.getElementById('note-body')?.focus();
}
});
const headingSelect = toolbar.querySelector('#heading-select');
headingSelect.addEventListener('change', () => {
const val = headingSelect.value;
document.execCommand('formatBlock', false, val);
updateToolbarState();
});
}
function findBlockElement(node) {
if (!node)
return null;
let el = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
while (el && el !== document.body) {
const display = window.getComputedStyle(el).display;
if (display === 'block' || display === 'list-item' || display === 'contents')
return el;
el = el.parentElement;
}
return null;
}
function unwrapElement(el) {
const parent = el.parentElement;
if (!parent)
return;
while (el.firstChild)
parent.insertBefore(el.firstChild, el);
el.remove();
}
function wrapSelection(tag) {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
const range = sel.getRangeAt(0);
if (range.collapsed)
return;
const el = document.createElement(tag);
try {
range.surroundContents(el);
}
catch {
const html = `<${tag}>${range.toString()}</${tag}>`;
document.execCommand('insertHTML', false, html);
}
}
function updateToolbarState() {
const toolbar = document.getElementById('editor-toolbar');
const cmds = ['bold', 'italic', 'underline', 'strikeThrough'];
cmds.forEach(cmd => {
const btn = toolbar.querySelector(`[data-cmd="${cmd}"]`);
if (btn)
btn.classList.toggle('active', document.queryCommandState(cmd));
});
const headingSel = toolbar.querySelector('#heading-select');
if (headingSel) {
const blockFormat = document.queryCommandValue('formatBlock');
const tag = blockFormat ? blockFormat.replace(/<(.*)>/, '$1').toLowerCase() : 'p';
headingSel.value = tag === 'div' ? 'p' : tag;
}
}
function showSaved() {
let indicator = document.getElementById('saved-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'saved-indicator';
indicator.className = 'saved-indicator';
indicator.textContent = 'Saved';
document.body.appendChild(indicator);
}
indicator.textContent = 'Saved';
indicator.classList.add('visible');
setTimeout(() => indicator.classList.remove('visible'), 1500);
}
// Markdown shortcut handler
function handleMarkdownShortcut(e, bodyEl) {
if (e.key !== ' ')
return false;
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return false;
const anchorNode = sel.anchorNode;
if (!anchorNode || anchorNode.nodeType !== Node.TEXT_NODE)
return false;
const range = sel.getRangeAt(0);
if (!range.collapsed)
return false;
const textNode = anchorNode;
const cursorOffset = range.startOffset;
const lineStart = findLineStart(textNode, cursorOffset);
// Must be at start of a line
if (lineStart !== 0 && textNode.textContent[lineStart - 1] !== '\n') {
// Not at start of line — but actually we check if it's at the start of the text node
// or after a newline
if (lineStart !== 0) {
// Check if there's a newline before
}
}
const prefix = textNode.textContent?.slice(lineStart, cursorOffset) || '';
let newBlock = null;
if (prefix === '# ')
newBlock = 'h1';
else if (prefix === '## ')
newBlock = 'h2';
else if (prefix === '### ')
newBlock = 'h3';
else if (prefix === '- ' || prefix === '* ')
newBlock = 'ul';
else if (prefix === '1. ')
newBlock = 'ol';
else if (prefix === '> ')
newBlock = 'blockquote';
else if (prefix === '[] ' || prefix === '[ ]')
newBlock = 'checklist';
if (newBlock) {
e.preventDefault();
// Remove prefix
const before = textNode.textContent?.slice(0, lineStart) || '';
const after = textNode.textContent?.slice(cursorOffset) || '';
textNode.textContent = before + after;
const newRange = document.createRange();
newRange.setStart(textNode, lineStart);
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
if (newBlock === 'checklist') {
insertChecklistItem(bodyEl);
}
else if (newBlock === 'ul') {
document.execCommand('insertUnorderedList');
}
else if (newBlock === 'ol') {
document.execCommand('insertOrderedList');
}
else if (newBlock === 'blockquote') {
document.execCommand('formatBlock', false, 'blockquote');
}
else {
document.execCommand('formatBlock', false, newBlock);
}
return true;
}
return false;
}
function findLineStart(textNode, offset) {
const before = textNode.textContent?.slice(0, offset) || '';
const lastNewline = before.lastIndexOf('\n');
return lastNewline === -1 ? 0 : lastNewline + 1;
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"78ce5e96-e5cb-4173-ac46-7561e4e09ee7","tokensIn":4202274,"tokensOut":65430,"tokensTotal":4267704,"cost":0,"turns":81,"toolCalls":80,"failedToolCalls":2,"timestamp":"2026-08-02T05:55:35.803Z"} -->