Notes
Qwen 3.6 27B MTP GGUF UD Q5 K XL · 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 88,651 bytes · SHA-256 cdf93a9fb11f
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notes — A Polished Notes Application</title>
<style>
/* ── css: layout ── */
/* Base & Reset */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
body {
background: var(--bg, #ffffff);
color: var(--text, #1a1a2e);
transition: background 0.3s, color 0.3s;
}
#app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
/* Header */
#app-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 16px; background: var(--bg-secondary, #f8f9fa);
border-bottom: 1px solid var(--border, #e0e2e6); height: 44px;
flex-shrink: 0; z-index: 100;
}
.app-title { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
#dark-toggle {
background: none; border: 1px solid var(--border, #e0e2e6);
border-radius: 8px; padding: 4px 10px; cursor: pointer; font-size: 15px;
transition: background 0.15s, transform 0.15s;
}
#dark-toggle:hover { background: var(--bg-hover, #e8eaed); transform: scale(1.05); }
/* Main layout */
#main-content { display: flex; flex: 1; overflow: hidden; }
/* Sidebar */
#sidebar {
background: var(--bg-secondary, #f8f9fa);
border-right: 1px solid var(--border, #e0e2e6);
overflow: hidden;
transition: width 0.25s cubic-bezier(0.4,0,0.2,1), min-width 0.25s cubic-bezier(0.4,0,0.2,1), max-width 0.25s;
position: relative;
}
.sidebar-header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 14px 10px; border-bottom: 1px solid var(--border, #e0e2e6); flex-shrink: 0;
}
.sidebar-logo { font-size: 14px; font-weight: 700; letter-spacing: -0.02em; }
.sidebar-toggle {
background: none; border: none; cursor: pointer; font-size: 11px;
color: var(--text-tertiary, #9aa0a6); padding: 2px 4px; border-radius: 4px;
transition: background 0.15s, color 0.15s;
}
.sidebar-toggle:hover { background: var(--bg-hover, #e8eaed); color: var(--text, #1a1a2e); }
.sidebar-btn {
width: 100%; padding: 6px 14px; margin: 6px 0 0; background: none; border: none;
text-align: left; font-size: 12px; color: var(--text-secondary, #5f6368);
cursor: pointer; border-radius: 6px; transition: background 0.15s, color 0.15s; font-weight: 500;
}
.sidebar-btn:hover { background: var(--bg-hover, #e8eaed); color: var(--text, #1a1a2e); }
.sidebar-scroll { flex: 1; overflow-y: auto; padding: 8px 0; }
.sidebar-scroll::-webkit-scrollbar { width: 4px; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: var(--border, #e0e2e6); border-radius: 2px; }
.nav-group-item {
display: flex; align-items: center; padding: 6px 14px; cursor: pointer;
font-size: 13px; border-left: 3px solid transparent;
transition: background 0.12s, border-color 0.12s; gap: 8px; user-select: none;
}
.nav-group-item:hover { background: var(--bg-hover, #e8eaed); }
.nav-group-item.active { background: var(--bg-active, #dde1e7); font-weight: 500; }
.nav-group-icon { font-size: 15px; width: 20px; text-align: center; }
.nav-group-label { flex: 1; }
.nav-group-count { font-size: 11px; color: var(--text-tertiary, #9aa0a6); min-width: 16px; text-align: right; }
.sidebar-separator { height: 1px; background: var(--border, #e0e2e6); margin: 8px 14px; }
/* Notebooks */
.notebook-group { margin: 0; }
.notebook-header {
display: flex; align-items: center; padding: 5px 14px; cursor: pointer;
font-size: 13px; border-left: 3px solid transparent;
transition: background 0.12s, border-color 0.12s; gap: 6px; user-select: none;
}
.notebook-header:hover { background: var(--bg-hover, #e8eaed); }
.notebook-header.active { font-weight: 500; }
.notebook-arrow { font-size: 8px; color: var(--text-tertiary, #9aa0a6); width: 14px; text-align: center; }
.notebook-icon { font-size: 14px; }
.notebook-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.notebook-count { font-size: 11px; color: var(--text-tertiary, #9aa0a6); min-width: 16px; text-align: right; }
.notebook-more {
background: none; border: none; cursor: pointer; font-size: 14px;
color: var(--text-tertiary, #9aa0a6); padding: 2px 4px; border-radius: 4px;
opacity: 0; transition: opacity 0.15s, background 0.15s;
}
.notebook-header:hover .notebook-more { opacity: 1; }
.notebook-more:hover { background: var(--bg-active, #dde1e7); }
/* Sidebar expand button */
.sidebar-expand-btn {
position: absolute; left: 0; top: 50%; transform: translateY(-50%);
width: 20px; height: 60px; border: none; background: var(--accent, #5b6abf);
color: white; cursor: pointer; border-radius: 0 4px 4px 0; z-index: 11;
font-size: 10px; transition: opacity 0.2s;
}
/* Resize handles */
.resize-handle { transition: background 0.2s; }
/* Note list */
#notelist {
background: var(--bg, #ffffff);
border-right: 1px solid var(--border, #e0e2e6);
overflow: hidden;
transition: width 0.25s cubic-bezier(0.4,0,0.2,1);
}
.notelist-header { padding: 12px 14px; border-bottom: 1px solid var(--border, #e0e2e6); flex-shrink: 0; }
.notelist-title-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.notelist-title { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.new-note-btn {
width: 28px; height: 28px; border-radius: 50%; border: none;
background: var(--accent, #5b6abf); color: white; font-size: 18px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: background 0.15s, transform 0.15s; line-height: 1;
}
.new-note-btn:hover { background: var(--accent-hover, #4a59a8); transform: scale(1.1); }
.notelist-controls { display: flex; flex-direction: column; gap: 6px; }
.search-box {
display: flex; align-items: center; background: var(--bg-secondary, #f8f9fa);
border: 1px solid var(--border, #e0e2e6); border-radius: 8px;
padding: 0 8px; transition: border-color 0.15s, box-shadow 0.15s;
}
.search-box:focus-within {
border-color: var(--accent, #5b6abf);
box-shadow: 0 0 0 2px var(--accent-light, #e8eaff);
}
.search-icon { font-size: 13px; opacity: 0.5; margin-right: 6px; }
.search-input {
border: none; background: none; outline: none; font-size: 13px;
padding: 6px 0; width: 100%; color: var(--text, #1a1a2e); font-family: inherit;
}
.search-input::placeholder { color: var(--text-tertiary, #9aa0a6); }
.active-tag-filter {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; background: var(--tag-bg, #e8eaff);
color: var(--tag-text, #5b6abf); border-radius: 12px; font-size: 11px;
}
.clear-tag-filter {
background: none; border: none; cursor: pointer; font-size: 13px;
color: var(--tag-text, #5b6abf); padding: 0 2px;
}
.sort-controls { display: flex; gap: 2px; }
.sort-btn {
padding: 3px 8px; border: none; background: none; font-size: 11px;
color: var(--text-tertiary, #9aa0a6); cursor: pointer; border-radius: 4px;
transition: background 0.15s, color 0.15s;
}
.sort-btn:hover { background: var(--bg-hover, #e8eaed); color: var(--text, #1a1a2e); }
.sort-btn.active { color: var(--accent, #5b6abf); font-weight: 500; }
/* Note items */
.notelist-items { flex: 1; overflow-y: auto; }
.notelist-items::-webkit-scrollbar { width: 4px; }
.notelist-items::-webkit-scrollbar-thumb { background: var(--border, #e0e2e6); border-radius: 2px; }
.note-item {
padding: 10px 14px; cursor: pointer; border-left: 3px solid transparent;
border-bottom: 1px solid var(--border-light, #eef0f2);
transition: background 0.12s, border-color 0.12s;
}
.note-item:hover { background: var(--bg-hover, #e8eaed); }
.note-item.active { font-weight: 500; }
.note-item-header { display: flex; align-items: center; gap: 4px; margin-bottom: 3px; }
.note-item-title {
font-size: 13px; font-weight: 500; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; flex: 1;
}
.pin-badge { font-size: 11px; opacity: 0.7; }
.note-item-menu {
background: none; border: none; cursor: pointer; font-size: 14px;
color: var(--text-tertiary, #9aa0a6); padding: 2px 4px; border-radius: 4px;
opacity: 0; transition: opacity 0.15s, background 0.15s; flex-shrink: 0;
}
.note-item:hover .note-item-menu { opacity: 1; }
.note-item-menu:hover { background: var(--bg-active, #dde1e7); }
.note-item-snippet {
font-size: 12px; color: var(--text-secondary, #5f6368); line-height: 1.4;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 4px;
}
.note-item-footer { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.note-item-time { font-size: 11px; color: var(--text-tertiary, #9aa0a6); }
.note-item-tags { display: flex; gap: 4px; flex-wrap: wrap; }
.note-tag {
display: inline-block; padding: 1px 6px; background: var(--tag-bg, #e8eaff);
color: var(--tag-text, #5b6abf); border-radius: 8px; font-size: 10px;
cursor: pointer; transition: opacity 0.15s;
}
.note-tag:hover { opacity: 0.7; }
/* Empty state */
.notelist-empty {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 40px 20px; text-align: center;
}
.empty-icon { font-size: 36px; margin-bottom: 12px; opacity: 0.5; }
.notelist-empty p { font-size: 13px; color: var(--text-secondary, #5f6368); line-height: 1.5; }
.empty-hint { font-size: 12px !important; color: var(--text-tertiary, #9aa0a6) !important; margin-top: 4px; }
/* Editor */
#editor {
flex: 1; display: flex; flex-direction: column; overflow: hidden;
background: var(--bg-editor, #ffffff); position: relative;
}
.editor-toolbar {
display: flex; align-items: center; padding: 4px 12px; gap: 1px;
flex-wrap: wrap; border-bottom: 1px solid var(--border, #e0e2e6);
flex-shrink: 0; min-height: 38px;
}
.toolbar-btn {
min-width: 28px; height: 28px; display: flex; align-items: center;
justify-content: center; padding: 0 6px; border: none; border-radius: 4px;
cursor: pointer; font-size: 12px; transition: background 0.12s, color 0.12s;
user-select: none;
}
.toolbar-btn:hover { opacity: 0.85; }
.toolbar-btn.active { box-shadow: inset 0 0 0 1px currentColor; }
.editor-title {
border: none; outline: none; font-size: 24px; font-weight: 600;
padding: 16px 28px 4px; width: 100%; flex-shrink: 0;
background: transparent; letter-spacing: -0.02em;
}
.editor-title::placeholder { color: var(--text-tertiary, #9aa0a6); }
.editor-tags {
display: flex; align-items: center; gap: 6px;
padding: 4px 28px 8px; flex-wrap: wrap;
}
.tag-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; border-radius: 12px; font-size: 12px; cursor: default;
transition: opacity 0.15s;
}
.tag-remove { cursor: pointer; font-size: 14px; line-height: 1; opacity: 0.6; transition: opacity 0.15s; }
.tag-remove:hover { opacity: 1; }
.tag-input { border: none; background: transparent; outline: none; font-size: 12px; }
.tag-input::placeholder { color: var(--text-tertiary, #9aa0a6); }
.editor-content {
flex: 1; overflow-y: auto; padding: 8px 28px 60px; outline: none;
font-family: Georgia, Cambria, 'Times New Roman', serif;
font-size: 15px; line-height: 1.7; color: var(--text, #1a1a2e);
}
.editor-content::-webkit-scrollbar { width: 6px; }
.editor-content::-webkit-scrollbar-thumb { background: var(--border, #e0e2e6); border-radius: 3px; }
.editor-content::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary, #9aa0a6); }
/* Editor typography */
.editor-content h1 { font-size: 1.8em; font-weight: 700; margin: 0.5em 0 0.3em; letter-spacing: -0.02em; line-height: 1.3; }
.editor-content h2 { font-size: 1.4em; font-weight: 600; margin: 0.5em 0 0.3em; letter-spacing: -0.01em; line-height: 1.3; }
.editor-content h3 { font-size: 1.15em; font-weight: 600; margin: 0.4em 0 0.2em; line-height: 1.4; }
.editor-content p { margin: 0.4em 0; }
.editor-content ul, .editor-content ol { padding-left: 1.5em; margin: 0.3em 0; }
.editor-content li { margin: 0.2em 0; }
.editor-content blockquote {
border-left: 3px solid var(--accent, #5b6abf); margin: 0.5em 0;
padding: 0.3em 0 0.3em 1em; color: var(--text-secondary, #5f6368); font-style: italic;
}
.editor-content hr { border: none; border-top: 1px solid var(--border, #e0e2e6); margin: 1em 0; }
.editor-content code {
background: var(--bg-tertiary, #f0f1f3); padding: 2px 5px; border-radius: 4px;
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; font-size: 0.88em;
color: var(--danger, #d93025);
}
.editor-content pre {
background: var(--bg-tertiary, #f0f1f3); padding: 12px 16px; border-radius: 8px;
overflow-x: auto; font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
font-size: 0.88em; line-height: 1.5; margin: 0.5em 0;
}
.editor-content:empty::before {
content: attr(data-placeholder); color: var(--text-tertiary, #9aa0a6);
pointer-events: none; font-style: italic;
}
.editor-empty-state {
display: none; flex-direction: column; align-items: center; justify-content: center;
flex: 1; padding: 40px; text-align: center;
}
.empty-state-icon { font-size: 56px; margin-bottom: 16px; opacity: 0.3; }
.editor-empty-state h3 { font-size: 18px; font-weight: 500; color: var(--text-secondary, #5f6368); margin-bottom: 8px; }
.editor-empty-state p { font-size: 14px; color: var(--text-tertiary, #9aa0a6); line-height: 1.5; }
/* Save indicator */
.save-indicator {
position: absolute; bottom: 8px; right: 20px; font-size: 11px;
color: var(--text-tertiary, #9aa0a6); transition: opacity 0.3s;
opacity: 0; pointer-events: none; z-index: 10;
}
/* Context menu */
.context-menu { animation: contextMenuFadeIn 0.12s ease-out; }
@keyframes contextMenuFadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.context-menu-item:hover { filter: brightness(0.95); }
/* Checklist checkboxes */
.checklist-checkbox {
margin-right: 6px; cursor: pointer; user-select: none;
display: inline-block; vertical-align: middle; font-size: 15px; line-height: 1;
}
.checklist-checkbox.checked { color: var(--accent, #5b6abf); }
/* Search highlight */
.editor-content mark { background: var(--search-highlight, #fff3cd); color: inherit; padding: 0 2px; border-radius: 2px; }
/* Dark theme overrides */
.dark-theme .editor-content blockquote { border-left-color: var(--accent, #7c8cf8); }
.dark-theme .editor-content code { background: #2a2a40; color: #f25555; }
.dark-theme .editor-content pre { background: #2a2a40; }
.dark-theme .editor-empty-state h3 { color: var(--text-secondary, #a0a0b8); }
.dark-theme .editor-empty-state p { color: var(--text-tertiary, #6e6e88); }
/* Smooth transitions */
#app-inner, #sidebar, #notelist, #editor, .notelist-header,
.sidebar-header, .editor-toolbar, .search-box {
transition: background-color 0.3s, border-color 0.3s;
}
/* Selection */
.editor-content ::selection { background: var(--accent, #5b6abf); color: white; opacity: 0.3; }
/* Scrollbar for note list and sidebar */
.notelist-items::-webkit-scrollbar { width: 4px; }
.notelist-items::-webkit-scrollbar-thumb { background: var(--border, #e0e2e6); border-radius: 2px; }
</style>
</head>
<body>
<div id="app-inner">
<header id="app-header">
<h1 class="app-title">📝 Notes</h1>
<button id="dark-toggle" title="Toggle dark mode">🌙</button>
</header>
<div id="main-content">
<div id="sidebar"></div>
<div id="notelist"></div>
<div id="editor"></div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./sidebar":"src/sidebar.ts","./notelist":"src/notelist.ts","./editor":"src/editor.ts","./state":"src/state.ts","./resizer":"src/resizer.ts","./constants":"src/constants.ts"},"src/model.ts":{"./constants":"src/constants.ts"},"src/utils.ts":{"./constants":"src/constants.ts"},"src/state.ts":{"./model":"src/model.ts","./utils":"src/utils.ts","./constants":"src/constants.ts"},"src/sidebar.ts":{"./state":"src/state.ts","./constants":"src/constants.ts"},"src/notelist.ts":{"./state":"src/state.ts","./utils":"src/utils.ts","./constants":"src/constants.ts","./model":"src/model.ts"},"src/editor.ts":{"./state":"src/state.ts","./utils":"src/utils.ts","./constants":"src/constants.ts"},"src/resizer.ts":{"./state":"src/state.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 });
// Entry module — wires everything together
const sidebar_1 = require("./sidebar");
const notelist_1 = require("./notelist");
const editor_1 = require("./editor");
const state_1 = require("./state");
const resizer_1 = require("./resizer");
const constants_1 = require("./constants");
// Expose actions to window for inline handlers and testing
window.setSidebarCollapsed = state_1.setSidebarCollapsed;
window.createNewNote = state_1.createNewNote;
window.renameNotebook = state_1.renameNotebook;
window.togglePin = state_1.togglePin;
window.restoreNote = state_1.restoreNote;
window.permanentDelete = state_1.permanentDelete;
window.moveNoteTo = state_1.moveNote;
window.forceSave = state_1.forceSave;
window.setDarkMode = state_1.setDarkMode;
window.trashNote = state_1.trashNote;
window.directUpdateNoteContent = state_1.directUpdateNoteContent;
// Wait for DOM to be ready
function init() {
const sidebar = document.getElementById('sidebar');
const notelist = document.getElementById('notelist');
const editor = document.getElementById('editor');
const darkToggle = document.getElementById('dark-toggle');
// Init panels
(0, sidebar_1.initSidebar)(sidebar);
(0, notelist_1.initNoteList)(notelist);
(0, editor_1.initEditor)(editor);
// Resizable panes
(0, resizer_1.initResizable)();
// Drag and drop for notes
(0, resizer_1.initDragDrop)();
// Dark mode toggle
darkToggle.addEventListener('click', () => {
const state = (0, state_1.getState)();
window.setDarkMode?.(!state.darkMode);
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
(0, state_1.createNewNote)();
setTimeout(editor_1.focusTitle, 50);
}
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchInput = document.querySelector('.search-input');
if (searchInput) {
searchInput.focus();
searchInput.select();
}
}
});
// Apply initial theme
applyTheme();
(0, state_1.subscribe)(applyTheme);
}
function applyTheme() {
const state = (0, state_1.getState)();
const dark = state.darkMode;
document.body.className = dark ? 'dark-theme' : 'light-theme';
const colors = dark ? constants_1.COLORS.dark : constants_1.COLORS.light;
document.documentElement.style.setProperty('--bg', colors.bg);
document.documentElement.style.setProperty('--bg-secondary', colors.bgSecondary);
document.documentElement.style.setProperty('--text', colors.text);
document.documentElement.style.setProperty('--text-secondary', colors.textSecondary);
document.documentElement.style.setProperty('--border', colors.border);
document.documentElement.style.setProperty('--accent', colors.accent);
const toggle = document.getElementById('dark-toggle');
if (toggle) {
toggle.textContent = dark ? '☀️' : '🌙';
}
}
// Start
init();
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ALLOWED_ATTRS = exports.SUPPORTED_HTML_TAGS = exports.DEFAULT_NOTEBOOKS = exports.STORAGE_KEY = exports.TRASH_AUTO_DELETE_DAYS = exports.SNIPPET_LENGTH = exports.AUTOSAVE_DELAY = exports.FONTS = exports.COLORS = void 0;
// Tunable constants
exports.COLORS = {
light: {
bg: '#ffffff',
bgSecondary: '#f8f9fa',
bgTertiary: '#f0f1f3',
bgHover: '#e8eaed',
bgActive: '#dde1e7',
bgEditor: '#ffffff',
text: '#1a1a2e',
textSecondary: '#5f6368',
textTertiary: '#9aa0a6',
border: '#e0e2e6',
borderLight: '#eef0f2',
accent: '#5b6abf',
accentHover: '#4a59a8',
accentLight: '#e8eaff',
danger: '#d93025',
dangerHover: '#c5221f',
success: '#1e8e3e',
tagBg: '#e8eaff',
tagText: '#5b6abf',
pinColor: '#f9a825',
shadow: 'rgba(0,0,0,0.08)',
toolbarBg: '#fafbfc',
checkboxUnchecked: '#dadce0',
checkboxChecked: '#5b6abf',
searchHighlight: '#fff3cd',
sidebarWidth: 240,
noteListWidth: 280,
editorWidth: 'auto',
},
dark: {
bg: '#1e1e2e',
bgSecondary: '#252537',
bgTertiary: '#2a2a3d',
bgHover: '#33334d',
bgActive: '#3a3a55',
bgEditor: '#1e1e2e',
text: '#e0e0f0',
textSecondary: '#a0a0b8',
textTertiary: '#6e6e88',
border: '#363650',
borderLight: '#2e2e45',
accent: '#7c8cf8',
accentHover: '#6b7bef',
accentLight: '#2a2a50',
danger: '#f25555',
dangerHover: '#e04040',
success: '#4ade80',
tagBg: '#2a2a50',
tagText: '#7c8cf8',
pinColor: '#fbbf24',
shadow: 'rgba(0,0,0,0.3)',
toolbarBg: '#252537',
checkboxUnchecked: '#444460',
checkboxChecked: '#7c8cf8',
searchHighlight: '#4a4a20',
sidebarWidth: 240,
noteListWidth: 280,
editorWidth: 'auto',
},
};
exports.FONTS = {
ui: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
editor: "'Georgia', 'Cambria', 'Times New Roman', serif",
editorSans: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
mono: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
};
exports.AUTOSAVE_DELAY = 500;
exports.SNIPPET_LENGTH = 120;
exports.TRASH_AUTO_DELETE_DAYS = 30;
exports.STORAGE_KEY = 'notes-app-data';
exports.DEFAULT_NOTEBOOKS = [
{ id: 'nb-default', name: 'My Notes', icon: '📝' },
{ id: 'nb-personal', name: 'Personal', icon: '📓' },
{ id: 'nb-work', name: 'Work', icon: '💼' },
];
exports.SUPPORTED_HTML_TAGS = new Set([
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'del',
'code', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'blockquote', 'hr', 'div', 'span',
'a', 'sub', 'sup',
]);
exports.ALLOWED_ATTRS = new Set([
'href', 'title', 'target', 'rel', 'checked',
'class', 'contenteditable', 'data-handler', 'data-placeholder',
]);
};
// ── module: src/model.ts ──
__mods["src/model.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createId = createId;
exports.createNote = createNote;
exports.createNotebook = createNotebook;
exports.buildDefaultState = buildDefaultState;
exports.saveState = saveState;
exports.loadState = loadState;
// Data model — single source of truth
const constants_1 = require("./constants");
function createId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 9);
}
function createNote(notebookId) {
const now = Date.now();
return {
id: createId(),
notebookId,
originalNotebookId: notebookId,
title: '',
content: '',
tags: [],
pinned: false,
created: now,
updated: now,
trashed: false,
};
}
function createNotebook(name, icon) {
return {
id: createId(),
name,
icon,
collapsed: false,
};
}
function buildDefaultState() {
const nb = constants_1.DEFAULT_NOTEBOOKS.map(n => ({ ...n, collapsed: false }));
const defaultNbId = nb[0]?.id ?? '';
const welcomeNote = {
id: createId(),
notebookId: defaultNbId,
originalNotebookId: defaultNbId,
title: 'Welcome to Notes',
content: '<h2>Welcome! 🎉</h2><p>This is a <strong>rich text editor</strong>. Try the formatting toolbar above.</p><p>Some things you can do:</p><ul><li>Write <strong>bold</strong>, <em>italic</em>, and <u>underlined</u> text</li><li>Create bulleted and numbered lists</li><li>Use <code>inline code</code></li><li>Add checklists with checkboxes</li></ul><p><strong>Markdown shortcuts:</strong></p><ul><li>Type <code># </code> for a heading</li><li>Type <code>- </code> for a bullet</li><li>Type <code>[] </code> for a checklist</li><li>Type <code>> </code> for a blockquote</li></ul><hr><p><em>Start typing to create your first note!</em></p>',
tags: ['welcome', 'getting-started'],
pinned: true,
created: Date.now(),
updated: Date.now(),
trashed: false,
};
return {
notebooks: nb,
notes: [welcomeNote],
activeNotebookId: 'all',
activeNoteId: welcomeNote.id,
tagFilter: null,
sortBy: 'modified',
darkMode: false,
sidebarCollapsed: false,
paneSizes: { sidebar: 240, noteList: 280 },
searchQuery: '',
};
}
function saveState(state) {
try {
localStorage.setItem(constants_1.STORAGE_KEY, JSON.stringify(state));
}
catch {
// Storage full or unavailable
}
}
function loadState() {
try {
const raw = localStorage.getItem(constants_1.STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
// Ensure all fields exist (migration safety)
return {
...buildDefaultState(),
...parsed,
paneSizes: {
...buildDefaultState().paneSizes,
...(parsed.paneSizes ?? {}),
},
};
}
}
catch {
// Invalid data — start fresh
}
return buildDefaultState();
}
};
// ── module: src/utils.ts ──
__mods["src/utils.ts"] = function (exports, require, module) {
"use strict";
// Pure text extraction and sanitization utilities
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripHtml = stripHtml;
exports.getSnippet = getSnippet;
exports.sanitizeHtml = sanitizeHtml;
exports.searchText = searchText;
exports.highlightText = highlightText;
exports.formatTime = formatTime;
exports.formatFullDate = formatFullDate;
const constants_1 = require("./constants");
// Strip all HTML tags, decode entities — pure text only
function stripHtml(html) {
// Create a temporary element to parse HTML
const div = document.createElement('div');
div.innerHTML = html;
return (div.textContent ?? div.innerText ?? '') || '';
}
// Extract text snippet for the note list
function getSnippet(html, length = constants_1.SNIPPET_LENGTH) {
const text = stripHtml(html);
if (text.length <= length)
return text;
const end = text.lastIndexOf(' ', length);
const cut = end > 0 ? end : length;
return text.slice(0, cut) + '…';
}
// Sanitize HTML — remove unsupported tags and attributes
function sanitizeHtml(html) {
if (!html || typeof html !== 'string')
return '';
try {
// Use a div approach instead of DOMParser to avoid issues with body being null
const div = document.createElement('div');
div.innerHTML = html;
// Sanitize in place
sanitizeNodeInPlace(div);
return div.innerHTML;
}
catch {
// Fallback: strip all HTML
return html.replace(/<[^>]*>/g, '');
}
}
function sanitizeNodeInPlace(parent) {
const children = Array.from(parent.childNodes);
for (let i = children.length - 1; i >= 0; i--) {
const node = children[i];
if (node.nodeType !== Node.ELEMENT_NODE)
continue;
const element = node;
const tag = element.tagName.toLowerCase();
// Remove dangerous/unsupported tags
if (DANGEROUS_TAGS.has(tag)) {
// Replace with text content
const text = element.textContent;
if (text) {
parent.insertBefore(document.createTextNode(text), element);
}
parent.removeChild(element);
continue;
}
if (!constants_1.SUPPORTED_HTML_TAGS.has(tag)) {
// Unwrap: move children before the element
const nodeChildren = Array.from(element.childNodes);
for (let j = nodeChildren.length - 1; j >= 0; j--) {
parent.insertBefore(nodeChildren[j], element);
}
parent.removeChild(element);
// Re-process the inserted children (they're now siblings)
// Since we go backwards, they're already handled above
continue;
}
// Sanitize attributes
const attrs = Array.from(element.attributes);
for (const attr of attrs) {
if (!constants_1.ALLOWED_ATTRS.has(attr.name)) {
element.removeAttribute(attr.name);
}
else if (attr.name === 'href' || attr.name === 'src') {
if (/^(javascript:|data:|vbscript:)/i.test(attr.value)) {
element.removeAttribute(attr.name);
}
}
}
// Recurse
sanitizeNodeInPlace(element);
}
}
const DANGEROUS_TAGS = new Set([
'script', 'style', 'iframe', 'object', 'embed', 'form',
'input', 'textarea', 'select', 'link', 'meta', 'base', 'head', 'html',
]);
function sanitizeNode(doc, node) {
if (node.nodeType !== Node.ELEMENT_NODE) {
// For text nodes and comments, nothing to sanitize
return;
}
const element = node;
const tag = element.tagName.toLowerCase();
if (tag === 'script' || tag === 'style' || tag === 'iframe' || tag === 'object' || tag === 'embed' || tag === 'form' || tag === 'input' || tag === 'textarea' || tag === 'select' || tag === 'link' || tag === 'meta' || tag === 'base' || tag === 'head' || tag === 'html') {
// Remove dangerous tags entirely — replace with text content
const parent = element.parentNode;
if (!parent)
return; // Already detached
const text = element.textContent;
if (text) {
parent.insertBefore(doc.createTextNode(text), element);
}
parent.removeChild(element);
return;
}
if (!constants_1.SUPPORTED_HTML_TAGS.has(tag)) {
// Unsupported tag — unwrap (keep children, discard tag)
const parent = element.parentNode;
if (!parent)
return;
const children = Array.from(element.childNodes);
for (let i = children.length - 1; i >= 0; i--) {
parent.insertBefore(children[i], element);
}
parent.removeChild(element);
return;
}
// Sanitize attributes
const attrs = Array.from(element.attributes);
for (const attr of attrs) {
if (!constants_1.ALLOWED_ATTRS.has(attr.name)) {
element.removeAttribute(attr.name);
}
else if (attr.name === 'href' || attr.name === 'src') {
const val = attr.value;
if (/^(javascript:|data:|vbscript:)/i.test(val)) {
element.removeAttribute(attr.name);
}
}
}
// Recursively process children (iterate in reverse to handle mutations safely)
const children = Array.from(element.childNodes);
for (let i = children.length - 1; i >= 0; i--) {
sanitizeNode(doc, children[i]);
}
}
// Extract plain text from HTML for search — ignores tag names
function searchText(html) {
return stripHtml(html).toLowerCase();
}
// Highlight matched text in a string for display
function highlightText(text, query) {
if (!query)
return escapeHtml(text);
const escaped = escapeHtml(text);
const escapedQuery = escapeHtml(query);
const regex = new RegExp(`(${escapedQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return escaped.replace(regex, '<mark>$1</mark>');
}
function escapeHtml(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
// Format a timestamp nicely
function formatTime(timestamp) {
const now = Date.now();
const diff = now - timestamp;
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
if (diff < minute)
return 'Just now';
if (diff < hour)
return `${Math.floor(diff / minute)}m ago`;
if (diff < day)
return `${Math.floor(diff / hour)}h ago`;
if (diff < 7 * day)
return `${Math.floor(diff / day)}d ago`;
const d = new Date(timestamp);
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: d.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined });
}
function formatFullDate(timestamp) {
return new Date(timestamp).toLocaleDateString(undefined, {
year: 'numeric', month: 'long', day: 'numeric',
hour: '2-digit', minute: '2-digit',
});
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.subscribe = subscribe;
exports.getState = getState;
exports.forceSave = forceSave;
exports.directUpdateNoteContent = directUpdateNoteContent;
exports.getNoteById = getNoteById;
exports.setActiveNotebook = setActiveNotebook;
exports.setActiveNote = setActiveNote;
exports.setSortBy = setSortBy;
exports.setDarkMode = setDarkMode;
exports.setSidebarCollapsed = setSidebarCollapsed;
exports.setPaneSizes = setPaneSizes;
exports.setSearchQuery = setSearchQuery;
exports.setTagFilter = setTagFilter;
exports.createNewNote = createNewNote;
exports.updateNote = updateNote;
exports.renameNotebook = renameNotebook;
exports.deleteNotebook = deleteNotebook;
exports.toggleNotebookCollapse = toggleNotebookCollapse;
exports.trashNote = trashNote;
exports.restoreNote = restoreNote;
exports.permanentDelete = permanentDelete;
exports.duplicateNote = duplicateNote;
exports.moveNote = moveNote;
exports.getFilteredNotes = getFilteredNotes;
exports.getActiveNote = getActiveNote;
exports.getNoteCount = getNoteCount;
exports.getTrashCount = getTrashCount;
exports.togglePin = togglePin;
exports.addNotebook = addNotebook;
// State management — actions, subscriptions, and state mutations
const model_1 = require("./model");
const utils_1 = require("./utils");
let state = (0, model_1.loadState)();
let autosaveTimer = null;
const subscribers = [];
function subscribe(fn) {
subscribers.push(fn);
return () => {
const idx = subscribers.indexOf(fn);
if (idx >= 0)
subscribers.splice(idx, 1);
};
}
function getState() {
return state;
}
function notify() {
for (const fn of subscribers)
fn();
}
function scheduleSave() {
if (autosaveTimer !== null)
clearTimeout(autosaveTimer);
autosaveTimer = setTimeout(() => {
(0, model_1.saveState)(state);
autosaveTimer = null;
}, 200);
}
function forceSave() {
if (autosaveTimer !== null)
clearTimeout(autosaveTimer);
(0, model_1.saveState)(state);
autosaveTimer = null;
}
// Debug: expose raw state mutation for testing
function directUpdateNoteContent(noteId, content) {
const note = state.notes.find(n => n.id === noteId);
if (!note)
return;
note.content = (0, utils_1.sanitizeHtml)(content);
note.updated = Date.now();
notify();
scheduleSave();
}
function getNoteById(noteId) {
return state.notes.find(n => n.id === noteId);
}
// --- Actions ---
function setActiveNotebook(notebookId) {
state.activeNotebookId = notebookId;
state.activeNoteId = null;
state.tagFilter = null;
notify();
scheduleSave();
}
function setActiveNote(noteId) {
state.activeNoteId = noteId;
notify();
}
function setSortBy(sortBy) {
state.sortBy = sortBy;
notify();
scheduleSave();
}
function setDarkMode(dark) {
state.darkMode = dark;
notify();
scheduleSave();
}
function setSidebarCollapsed(collapsed) {
state.sidebarCollapsed = collapsed;
notify();
scheduleSave();
}
function setPaneSizes(paneSizes) {
state.paneSizes = { ...paneSizes };
notify();
scheduleSave();
}
function setSearchQuery(query) {
state.searchQuery = query;
notify();
}
function setTagFilter(tag) {
state.tagFilter = tag;
notify();
}
function createNewNote(notebookId) {
const targetNotebook = notebookId ?? state.activeNotebookId;
if (targetNotebook === 'all' || targetNotebook === 'trash') {
// Create in the first real notebook
const firstReal = state.notebooks[0];
if (!firstReal)
return (0, model_1.createNote)('');
state.activeNotebookId = firstReal.id;
}
const note = (0, model_1.createNote)(state.activeNotebookId);
state.notes.push(note);
state.activeNoteId = note.id;
notify();
scheduleSave();
return note;
}
function updateNote(noteId, updates) {
const note = state.notes.find(n => n.id === noteId);
if (!note)
return;
if (updates.title !== undefined)
note.title = updates.title;
if (updates.content !== undefined) {
note.content = (0, utils_1.sanitizeHtml)(updates.content);
}
if (updates.tags !== undefined)
note.tags = updates.tags;
if (updates.pinned !== undefined)
note.pinned = updates.pinned;
note.updated = Date.now();
notify();
scheduleSave();
}
function renameNotebook(notebookId, name) {
const nb = state.notebooks.find(n => n.id === notebookId);
if (!nb)
return;
nb.name = name;
notify();
scheduleSave();
}
function deleteNotebook(notebookId) {
const idx = state.notebooks.findIndex(n => n.id === notebookId);
if (idx < 0)
return;
state.notebooks.splice(idx, 1);
// Move notes to the first remaining notebook, or trash them
const remaining = state.notebooks[0];
const notes = state.notes.filter(n => n.notebookId === notebookId && !n.trashed);
for (const note of notes) {
if (remaining) {
note.notebookId = remaining.id;
}
else {
trashNote(note.id);
}
}
if (state.activeNotebookId === notebookId) {
state.activeNotebookId = remaining?.id ?? 'all';
state.activeNoteId = null;
}
notify();
scheduleSave();
}
function toggleNotebookCollapse(notebookId) {
const nb = state.notebooks.find(n => n.id === notebookId);
if (!nb)
return;
nb.collapsed = !nb.collapsed;
notify();
scheduleSave();
}
function trashNote(noteId) {
const note = state.notes.find(n => n.id === noteId);
if (!note)
return;
note.trashed = true;
if (state.activeNoteId === noteId)
state.activeNoteId = null;
notify();
scheduleSave();
}
function restoreNote(noteId) {
const note = state.notes.find(n => n.id === noteId);
if (!note)
return;
note.trashed = false;
// Restore to original notebook if it exists, otherwise first notebook
if (!state.notebooks.find(n => n.id === note.originalNotebookId)) {
const first = state.notebooks[0];
if (first)
note.notebookId = first.id;
}
else {
note.notebookId = note.originalNotebookId;
}
notify();
scheduleSave();
}
function permanentDelete(noteId) {
const idx = state.notes.findIndex(n => n.id === noteId);
if (idx < 0)
return;
state.notes.splice(idx, 1);
if (state.activeNoteId === noteId)
state.activeNoteId = null;
notify();
scheduleSave();
}
function duplicateNote(noteId) {
const note = state.notes.find(n => n.id === noteId);
if (!note)
return;
const copy = {
...note,
id: (0, model_1.createId)(),
title: note.title + ' (copy)',
tags: [...note.tags],
pinned: false,
trashed: false,
created: Date.now(),
updated: Date.now(),
};
state.notes.push(copy);
state.activeNoteId = copy.id;
notify();
scheduleSave();
}
function moveNote(noteId, targetNotebookId) {
const note = state.notes.find(n => n.id === noteId);
if (!note)
return;
note.notebookId = targetNotebookId;
note.updated = Date.now();
notify();
scheduleSave();
}
function getFilteredNotes() {
let notes;
if (state.activeNotebookId === 'all') {
notes = state.notes.filter(n => !n.trashed);
}
else if (state.activeNotebookId === 'trash') {
notes = state.notes.filter(n => n.trashed);
}
else {
notes = state.notes.filter(n => n.notebookId === state.activeNotebookId && !n.trashed);
}
// Tag filter
if (state.tagFilter) {
const tag = state.tagFilter;
notes = notes.filter(n => n.tags.includes(tag));
}
// Search
if (state.searchQuery) {
const q = state.searchQuery.toLowerCase();
notes = notes.filter(n => n.title.toLowerCase().includes(q) ||
(n.content && stripHtmlForSearch(n.content).includes(q)));
}
// Sort: pinned first, then by sort order
const pinned = notes.filter(n => n.pinned);
const unpinned = notes.filter(n => !n.pinned);
const sortFn = (a, b) => {
switch (state.sortBy) {
case 'modified': return b.updated - a.updated;
case 'created': return b.created - a.created;
case 'alphabetical': return a.title.localeCompare(b.title);
default: return b.updated - a.updated;
}
};
pinned.sort(sortFn);
unpinned.sort(sortFn);
return [...pinned, ...unpinned];
}
function stripHtmlForSearch(html) {
const div = document.createElement('div');
div.innerHTML = html;
return (div.textContent ?? '').toLowerCase();
}
function getActiveNote() {
if (!state.activeNoteId)
return null;
return state.notes.find(n => n.id === state.activeNoteId) ?? null;
}
function getNoteCount(notebookId) {
return state.notes.filter(n => n.notebookId === notebookId && !n.trashed).length;
}
function getTrashCount() {
return state.notes.filter(n => n.trashed).length;
}
function togglePin(noteId) {
const note = state.notes.find(n => n.id === noteId);
if (!note)
return;
note.pinned = !note.pinned;
note.updated = Date.now();
notify();
scheduleSave();
}
function addNotebook(name, icon) {
const nb = (0, model_1.createNotebook)(name, icon);
state.notebooks.push(nb);
state.activeNotebookId = nb.id;
notify();
scheduleSave();
return nb;
}
};
// ── module: src/sidebar.ts ──
__mods["src/sidebar.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initSidebar = initSidebar;
exports.destroySidebar = destroySidebar;
// Sidebar rendering — notebooks list, All Notes, Trash
const state_1 = require("./state");
const constants_1 = require("./constants");
let container;
let unsubscribe;
function initSidebar(el) {
container = el;
render();
unsubscribe = (0, state_1.subscribe)(render);
}
function destroySidebar() {
unsubscribe();
}
function render() {
const state = (0, state_1.getState)();
const collapsed = state.sidebarCollapsed;
container.innerHTML = '';
container.style.width = collapsed ? '0' : `${state.paneSizes.sidebar}px`;
container.style.minWidth = collapsed ? '0' : `${state.paneSizes.sidebar}px`;
container.style.maxWidth = collapsed ? '0' : `${state.paneSizes.sidebar}px`;
container.style.overflow = 'hidden';
if (collapsed)
return;
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.height = '100%';
// Header
const header = document.createElement('div');
header.className = 'sidebar-header';
header.innerHTML = `
<div class="sidebar-logo">📝 Notes</div>
<button class="sidebar-toggle" title="Collapse sidebar">◀</button>
`;
header.querySelector('.sidebar-toggle')?.addEventListener('click', () => {
window.setSidebarCollapsed?.(true);
});
container.append(header);
// New notebook button
const newNbBtn = document.createElement('button');
newNbBtn.className = 'sidebar-btn new-notebook-btn';
newNbBtn.textContent = '+ New Notebook';
newNbBtn.addEventListener('click', handleNewNotebook);
container.append(newNbBtn);
// Scrollable area
const scrollArea = document.createElement('div');
scrollArea.className = 'sidebar-scroll';
// All Notes
scrollArea.append(makeNavGroupItem('all', '📋', 'All Notes', state.notes.filter(n => !n.trashed).length));
scrollArea.append(makeNavGroupItem('trash', '🗑️', 'Trash', (0, state_1.getTrashCount)()));
// Separator
const sep = document.createElement('div');
sep.className = 'sidebar-separator';
scrollArea.append(sep);
// Notebooks
for (const nb of state.notebooks) {
scrollArea.append(makeNotebookItem(nb));
}
container.append(scrollArea);
}
function makeNavGroupItem(id, icon, label, count) {
const state = (0, state_1.getState)();
const colors = state.darkMode ? constants_1.COLORS.dark : constants_1.COLORS.light;
const active = state.activeNotebookId === id;
const item = document.createElement('div');
item.className = `nav-group-item${active ? ' active' : ''}`;
if (active)
item.style.borderLeftColor = colors.accent;
item.innerHTML = `<span class="nav-group-icon">${icon}</span><span class="nav-group-label">${label}</span><span class="nav-group-count">${count}</span>`;
item.addEventListener('click', () => (0, state_1.setActiveNotebook)(id));
return item;
}
function makeNotebookItem(nb) {
const state = (0, state_1.getState)();
const colors = state.darkMode ? constants_1.COLORS.dark : constants_1.COLORS.light;
const active = state.activeNotebookId === nb.id;
const count = (0, state_1.getNoteCount)(nb.id);
const wrapper = document.createElement('div');
wrapper.className = 'notebook-group';
const header = document.createElement('div');
header.className = `notebook-header${active ? ' active' : ''}`;
if (active) {
header.style.backgroundColor = colors.bgActive;
header.style.borderLeftColor = colors.accent;
}
header.innerHTML = `
<span class="notebook-arrow">${nb.collapsed ? '▶' : '▼'}</span>
<span class="notebook-icon">${nb.icon}</span>
<span class="notebook-name">${escapeHtml(nb.name)}</span>
<span class="notebook-count">${count}</span>
<button class="notebook-more" title="More options">⋯</button>
`;
header.addEventListener('click', () => (0, state_1.setActiveNotebook)(nb.id));
header.querySelector('.notebook-more')?.addEventListener('click', (e) => {
e.stopPropagation();
const me = e;
handleNotebookMenu(nb.id, me);
});
wrapper.append(header);
return wrapper;
}
function handleNotebookMenu(notebookId, e) {
const state = (0, state_1.getState)();
const nb = state.notebooks.find(n => n.id === notebookId);
if (!nb)
return;
// Remove existing
document.querySelector('.context-menu')?.remove();
const menu = document.createElement('div');
menu.className = 'context-menu';
const colors = state.darkMode ? constants_1.COLORS.dark : constants_1.COLORS.light;
Object.assign(menu.style, {
position: 'fixed', zIndex: '1000', minWidth: '160px',
backgroundColor: colors.bgSecondary,
border: `1px solid ${colors.border}`,
borderRadius: '8px',
boxShadow: `0 4px 12px ${colors.shadow}`,
fontFamily: constants_1.FONTS.ui, fontSize: '13px',
top: `${e.clientY}px`, left: `${e.clientX}px`,
});
const items = [
{ label: 'Rename', action: () => renameNotebookDialog(notebookId) },
{ label: 'New Note', action: () => { (0, state_1.setActiveNotebook)(notebookId); window.createNewNote?.(); } },
{ label: 'Delete', danger: true, action: () => { if (confirm(`Delete "${nb.name}"? Notes will be moved to the first notebook.`))
(0, state_1.deleteNotebook)(notebookId); } },
];
for (const item of items) {
const btn = document.createElement('div');
btn.className = 'context-menu-item';
btn.textContent = item.label;
btn.style.cssText = `padding:8px 12px;cursor:pointer;color:${item.danger ? colors.danger : 'inherit'}`;
btn.addEventListener('mouseenter', () => btn.style.backgroundColor = colors.bgHover);
btn.addEventListener('mouseleave', () => btn.style.backgroundColor = 'transparent');
btn.addEventListener('click', () => { item.action(); menu.remove(); });
menu.append(btn);
}
document.body.append(menu);
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
}
function renameNotebookDialog(notebookId) {
const state = (0, state_1.getState)();
const nb = state.notebooks.find(n => n.id === notebookId);
if (!nb)
return;
const name = prompt('Notebook name:', nb.name);
if (name && name.trim()) {
// Need to import renameNotebook
window.renameNotebook?.(notebookId, name.trim());
}
}
function handleNewNotebook() {
const name = prompt('Notebook name:', 'New Notebook');
if (name && name.trim()) {
(0, state_1.addNotebook)(name.trim(), '📒');
}
}
function escapeHtml(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
};
// ── module: src/notelist.ts ──
__mods["src/notelist.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initNoteList = initNoteList;
exports.destroyNoteList = destroyNoteList;
exports.getContainer = getContainer;
// Note list pane — displays notes in the selected notebook
const state_1 = require("./state");
const utils_1 = require("./utils");
const constants_1 = require("./constants");
let container;
let unsubscribe;
function initNoteList(el) {
container = el;
render();
unsubscribe = (0, state_1.subscribe)(render);
}
function destroyNoteList() {
unsubscribe();
}
function getContainer() {
return container;
}
function render() {
const state = (0, state_1.getState)();
const colors = state.darkMode ? constants_1.COLORS.dark : constants_1.COLORS.light;
const collapsed = state.sidebarCollapsed;
const notes = (0, state_1.getFilteredNotes)();
const activeNoteId = state.activeNoteId;
container.innerHTML = '';
container.style.width = `${state.paneSizes.noteList}px`;
container.style.minWidth = '180px';
container.style.maxWidth = '600px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.height = '100%';
// Header
const header = document.createElement('div');
header.className = 'notelist-header';
const activeNb = state.notebooks.find(n => n.id === state.activeNotebookId);
let headerTitle = 'All Notes';
if (state.activeNotebookId === 'trash')
headerTitle = 'Trash';
else if (activeNb)
headerTitle = activeNb.name;
header.innerHTML = `
<div class="notelist-title-row">
<h2 class="notelist-title">${escapeHtml(headerTitle)}</h2>
${state.activeNotebookId !== 'trash' && state.activeNotebookId !== 'all' ? `<button class="new-note-btn" title="New Note">+</button>` : ''}
</div>
<div class="notelist-controls">
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" class="search-input" placeholder="Search notes..." value="${escapeHtml(state.searchQuery)}" />
</div>
${state.tagFilter ? `<span class="active-tag-filter">Tag: ${escapeHtml(state.tagFilter)} <button class="clear-tag-filter">×</button></span>` : ''}
<div class="sort-controls">
<button class="sort-btn${state.sortBy === 'modified' ? ' active' : ''}" data-sort="modified">Modified</button>
<button class="sort-btn${state.sortBy === 'created' ? ' active' : ''}" data-sort="created">Created</button>
<button class="sort-btn${state.sortBy === 'alphabetical' ? ' active' : ''}" data-sort="alphabetical">A-Z</button>
</div>
</div>
`;
// New note button
header.querySelector('.new-note-btn')?.addEventListener('click', () => {
window.createNewNote?.();
});
// Search input
const searchInput = header.querySelector('.search-input');
searchInput.addEventListener('input', () => (0, state_1.setSearchQuery)(searchInput.value));
// Clear tag filter
header.querySelector('.clear-tag-filter')?.addEventListener('click', () => (0, state_1.setTagFilter)(null));
// Sort buttons
header.querySelectorAll('.sort-btn').forEach(btn => {
btn.addEventListener('click', () => {
const sort = btn.dataset.sort;
if (sort)
(0, state_1.setSortBy)(sort);
});
});
container.append(header);
// Notes
const notesContainer = document.createElement('div');
notesContainer.className = 'notelist-items';
if (notes.length === 0) {
const empty = document.createElement('div');
empty.className = 'notelist-empty';
empty.innerHTML = state.searchQuery
? '<div class="empty-icon">🔍</div><p>No notes found</p>'
: state.activeNotebookId === 'trash'
? '<div class="empty-icon">🗑️</div><p>Trash is empty</p>'
: '<div class="empty-icon">📝</div><p>No notes yet</p><p class="empty-hint">Click + to create one</p>';
notesContainer.append(empty);
}
else {
for (const note of notes) {
notesContainer.append(makeNoteItem(note, activeNoteId ?? null, colors));
}
}
container.append(notesContainer);
}
function makeNoteItem(note, activeNoteId, colors) {
const state = (0, state_1.getState)();
const item = document.createElement('div');
item.className = `note-item${note.id === activeNoteId ? ' active' : ''}`;
if (note.id === activeNoteId) {
item.style.backgroundColor = colors.accentLight;
item.style.borderLeftColor = colors.accent;
}
const displayTitle = note.title || 'Untitled';
const snippet = (0, utils_1.getSnippet)(note.content, constants_1.SNIPPET_LENGTH);
let tagsHtml = '';
if (note.tags.length > 0 && state.activeNotebookId !== 'trash') {
tagsHtml = note.tags.map(t => `<span class="note-tag" data-tag="${escapeHtml(t)}">#${escapeHtml(t)}</span>`).join('');
}
item.innerHTML = `
<div class="note-item-header">
<span class="note-item-title">${escapeHtml(displayTitle)}</span>
${note.pinned ? '<span class="pin-badge" title="Pinned">📌</span>' : ''}
${note.id !== activeNoteId ? `<button class="note-item-menu" title="More options">⋯</button>` : ''}
</div>
<div class="note-item-snippet">${escapeHtml(snippet)}</div>
<div class="note-item-footer">
<span class="note-item-time">${(0, utils_1.formatTime)(note.updated)}</span>
${tagsHtml ? `<div class="note-item-tags">${tagsHtml}</div>` : ''}
</div>
`;
// Click to select
item.addEventListener('click', (e) => {
if (e.target.closest('.note-item-menu'))
return;
if (e.target.closest('.note-tag'))
return;
(0, state_1.setActiveNote)(note.id);
});
// Tag click
item.querySelectorAll('.note-tag').forEach(tagEl => {
tagEl.addEventListener('click', (e) => {
e.stopPropagation();
const tag = e.target.dataset.tag;
if (tag)
(0, state_1.setTagFilter)(tag);
});
});
// Menu button
item.querySelector('.note-item-menu')?.addEventListener('click', (e) => {
e.stopPropagation();
showNoteMenu(note, e);
});
// Drag support
item.draggable = true;
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', note.id);
item.style.opacity = '0.5';
});
item.addEventListener('dragend', () => { item.style.opacity = '1'; });
return item;
}
function showNoteMenu(note, e) {
const state = (0, state_1.getState)();
const colors = state.darkMode ? constants_1.COLORS.dark : constants_1.COLORS.light;
document.querySelector('.context-menu')?.remove();
const menu = document.createElement('div');
menu.className = 'context-menu';
Object.assign(menu.style, {
position: 'fixed', zIndex: '1000', minWidth: '160px',
backgroundColor: colors.bgSecondary,
border: `1px solid ${colors.border}`,
borderRadius: '8px',
boxShadow: `0 4px 12px ${colors.shadow}`,
fontFamily: constants_1.FONTS.ui, fontSize: '13px',
top: `${e.clientY}px`, left: `${e.clientX}px`,
});
const actions = [
{ label: note.pinned ? 'Unpin' : 'Pin', action: () => window.togglePin?.(note.id) },
{ label: 'Duplicate', action: () => (0, state_1.duplicateNote)(note.id) },
{ label: 'Delete', danger: true, action: () => (0, state_1.trashNote)(note.id) },
];
if (state.activeNotebookId !== 'trash') {
// Add move options
for (const nb of state.notebooks) {
if (nb.id !== note.notebookId) {
actions.push({ label: `Move to ${nb.name}`, action: () => { window.moveNoteTo?.(note.id, nb.id); } });
}
}
}
else {
actions.push({ label: 'Restore', action: () => window.restoreNote?.(note.id) }, { label: 'Delete Forever', danger: true, action: () => window.permanentDelete?.(note.id) });
}
for (const item of actions) {
const btn = document.createElement('div');
btn.className = 'context-menu-item';
btn.textContent = item.label;
btn.style.cssText = `padding:8px 12px;cursor:pointer;color:${item.danger ? colors.danger : 'inherit'}`;
btn.addEventListener('mouseenter', () => btn.style.backgroundColor = colors.bgHover);
btn.addEventListener('mouseleave', () => btn.style.backgroundColor = 'transparent');
btn.addEventListener('click', () => { item.action(); menu.remove(); });
menu.append(btn);
}
document.body.append(menu);
setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10);
}
function escapeHtml(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
};
// ── module: src/editor.ts ──
__mods["src/editor.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initEditor = initEditor;
exports.destroyEditor = destroyEditor;
exports.focusEditor = focusEditor;
exports.focusTitle = focusTitle;
// Rich text editor with contenteditable, toolbar, markdown shortcuts, paste sanitization
const state_1 = require("./state");
const utils_1 = require("./utils");
const constants_1 = require("./constants");
let container;
let editorEl;
let toolbarEl;
let titleEl;
let tagsContainer;
let saveIndicator;
let unsubscribe;
let saveTimeout = null;
let savedContent = '';
let suppressNextInput = false;
let isSaving = false; // Prevent re-render from overwriting during save
function initEditor(el) {
container = el;
buildEditorDOM();
attachEditorEvents();
render();
unsubscribe = (0, state_1.subscribe)(render);
}
function destroyEditor() {
unsubscribe();
}
function buildEditorDOM() {
container.innerHTML = '';
container.className = 'editor-pane';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.height = '100%';
// Toolbar
toolbarEl = document.createElement('div');
toolbarEl.className = 'editor-toolbar';
container.append(toolbarEl);
// Title input
titleEl = document.createElement('input');
titleEl.type = 'text';
titleEl.className = 'editor-title';
titleEl.placeholder = 'Note title...';
container.append(titleEl);
// Tags row
tagsContainer = document.createElement('div');
tagsContainer.className = 'editor-tags';
container.append(tagsContainer);
// Empty state overlay
const emptyState = document.createElement('div');
emptyState.className = 'editor-empty-state';
emptyState.innerHTML = `
<div class="empty-state-icon">📝</div>
<h3>Select or create a note</h3>
<p>Choose a note from the sidebar or create a new one to get started.</p>
`;
container.append(emptyState);
// Editor area
editorEl = document.createElement('div');
editorEl.className = 'editor-content';
editorEl.contentEditable = 'true';
editorEl.spellcheck = true;
editorEl.setAttribute('data-placeholder', 'Start writing...');
container.append(editorEl);
// Save indicator
saveIndicator = document.createElement('div');
saveIndicator.className = 'save-indicator';
saveIndicator.textContent = 'Saved';
container.append(saveIndicator);
}
function render() {
const state = (0, state_1.getState)();
const colors = state.darkMode ? constants_1.COLORS.dark : constants_1.COLORS.light;
const note = (0, state_1.getActiveNote)();
// Apply theme
container.style.backgroundColor = colors.bgEditor;
container.style.color = colors.text;
// Title input
titleEl.style.backgroundColor = colors.bgEditor;
titleEl.style.color = colors.text;
titleEl.style.border = `1px solid transparent`;
titleEl.style.fontFamily = constants_1.FONTS.ui;
// Show/hide elements
const hasNote = note !== null;
titleEl.hidden = !hasNote;
tagsContainer.hidden = !hasNote;
editorEl.hidden = !hasNote;
toolbarEl.hidden = !hasNote;
saveIndicator.hidden = !hasNote;
container.querySelector('.editor-empty-state').style.display = hasNote ? 'none' : 'flex';
if (!hasNote)
return;
// Update title
const oldTitle = titleEl.value;
titleEl.value = note.title;
// Don't steal focus if title hasn't changed
if (oldTitle === note.title) {
// Keep cursor position
}
// Only update editor content if user isn't currently typing (isSaving = false)
// and the content differs from what was last saved
if (!isSaving) {
const currentContent = editorEl.innerHTML;
if (currentContent === '' && note.content) {
// Empty editor but note has content — load it
editorEl.innerHTML = note.content;
savedContent = note.content;
}
else if (currentContent !== note.content && currentContent !== savedContent) {
// Content diverged (external change) — sync
editorEl.innerHTML = note.content;
savedContent = note.content;
}
}
// Restore checklist checkbox handlers after any content change
restoreChecklistHandlers();
// Tags
renderTags(note);
// Toolbar
renderToolbar();
}
function renderToolbar() {
const state = (0, state_1.getState)();
const colors = state.darkMode ? constants_1.COLORS.dark : constants_1.COLORS.light;
const currentBlock = getCurrentBlockFormat();
const currentInline = getCurrentInlineFormats();
toolbarEl.style.backgroundColor = colors.toolbarBg;
toolbarEl.style.borderBottom = `1px solid ${colors.border}`;
const buttons = [
{ cmd: 'bold', icon: 'B', title: 'Bold (Ctrl+B)', active: currentInline.has('strong') || currentInline.has('b') },
{ cmd: 'italic', icon: 'I', title: 'Italic (Ctrl+I)', active: currentInline.has('em') || currentInline.has('i') },
{ cmd: 'underline', icon: 'U', title: 'Underline (Ctrl+U)', active: currentInline.has('u') },
{ cmd: 'strikeThrough', icon: 'S̶', title: 'Strikethrough', active: currentInline.has('s') || currentInline.has('strike') || currentInline.has('del') },
{ separator: true },
{ cmd: 'formatBlock', value: 'H1', icon: 'H1', title: 'Heading 1', active: currentBlock === 'H1' },
{ cmd: 'formatBlock', value: 'H2', icon: 'H2', title: 'Heading 2', active: currentBlock === 'H2' },
{ cmd: 'formatBlock', value: 'H3', icon: 'H3', title: 'Heading 3', active: currentBlock === 'H3' },
{ cmd: 'formatBlock', value: 'P', icon: '¶', title: 'Paragraph', active: currentBlock === 'P' },
{ separator: true },
{ cmd: 'insertUnorderedList', icon: '•≡', title: 'Bulleted List', active: false }, // We handle list detection below
{ cmd: 'insertOrderedList', icon: '1≡', title: 'Numbered List', active: false },
{ separator: true },
{ cmd: 'formatBlock', value: 'BLOCKQUOTE', icon: '❝', title: 'Blockquote', active: currentBlock === 'BLOCKQUOTE' },
{ cmd: 'insertHorizontalRule', icon: '—', title: 'Horizontal Rule', active: false },
{ cmd: 'code', icon: '<>', title: 'Inline Code', active: isInCode() },
{ separator: true },
{ cmd: 'undo', icon: '↩', title: 'Undo (Ctrl+Z)', active: false },
{ cmd: 'redo', icon: '↪', title: 'Redo (Ctrl+Shift+Z)', active: false },
];
toolbarEl.innerHTML = '';
for (const btn of buttons) {
if (btn.separator) {
const sep = document.createElement('div');
sep.className = 'toolbar-separator';
sep.style.cssText = `width:1px;height:20px;background:${colors.border};margin:0 4px;align-self:center;`;
toolbarEl.append(sep);
continue;
}
const b = btn;
const button = document.createElement('button');
button.className = `toolbar-btn${b.active ? ' active' : ''}`;
button.textContent = b.icon;
button.title = b.title;
button.style.cssText = `
padding: 6px 8px;
border: none;
background: ${b.active ? colors.accentLight : 'transparent'};
color: ${b.active ? colors.accent : colors.textSecondary};
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: ${b.cmd === 'bold' ? 'bold' : 'normal'};
font-style: ${b.cmd === 'italic' ? 'italic' : 'normal'};
text-decoration: ${b.cmd === 'underline' ? 'underline' : 'none'};
transition: all 0.15s;
line-height: 1;
`;
button.addEventListener('mouseenter', () => {
if (!b.active)
button.style.backgroundColor = colors.bgHover;
});
button.addEventListener('mouseleave', () => {
if (!b.active)
button.style.backgroundColor = 'transparent';
});
button.addEventListener('click', () => {
editorEl.focus();
if (b.cmd === 'code') {
toggleInlineCode();
}
else if (b.cmd === 'insertHorizontalRule') {
document.execCommand('insertHorizontalRule', false, undefined);
onEditorChange();
}
else if (b.cmd === 'formatBlock' && b.value) {
document.execCommand('formatBlock', false, b.value.toLowerCase());
onEditorChange();
}
else {
document.execCommand(b.cmd, false, undefined);
onEditorChange();
}
});
toolbarEl.append(button);
}
}
function getCurrentBlockFormat() {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return 'P';
let node = sel.anchorNode;
while (node && node !== editorEl) {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node;
const tag = el.tagName;
if (tag === 'H1' || tag === 'H2' || tag === 'H3' || tag === 'H4' || tag === 'H5' || tag === 'H6' || tag === 'BLOCKQUOTE' || tag === 'P' || tag === 'DIV') {
return tag;
}
}
node = node.parentNode;
}
return 'P';
}
function getCurrentInlineFormats() {
const formats = new Set();
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return formats;
let node = sel.anchorNode;
while (node && node !== editorEl) {
if (node.nodeType === Node.ELEMENT_NODE) {
const tag = node.tagName.toLowerCase();
formats.add(tag);
}
node = node.parentNode;
}
return formats;
}
function isInCode() {
const sel = window.getSelection();
if (!sel)
return false;
let node = sel.anchorNode;
while (node && node !== editorEl) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName.toLowerCase() === 'code')
return true;
}
node = node.parentNode;
}
return false;
}
function toggleInlineCode() {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
const range = sel.getRangeAt(0);
// Check if selection is already in a code element
let node = range.startContainer;
while (node && node !== editorEl) {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'code') {
// Remove code wrapper
const parent = node.parentNode;
const fc = node.firstChild;
while (fc) {
parent?.insertBefore(fc, node);
}
node.remove();
onEditorChange();
return;
}
node = node.parentNode;
}
document.execCommand('wrapCodeToCode', false, 'code');
// Fallback: manual wrapping
const selectedNode = range.cloneContents();
const codeEl = document.createElement('code');
codeEl.style.cssText = 'background:#f0f0f0;padding:2px 4px;border-radius:3px;font-family:monospace;font-size:0.9em;';
if (selectedNode.childNodes.length > 0) {
for (let i = 0; i < selectedNode.childNodes.length; i++) {
codeEl.appendChild(selectedNode.childNodes[i].cloneNode(true));
}
}
else {
codeEl.textContent = sel.toString() || '';
}
range.deleteContents();
range.insertNode(codeEl);
// Move cursor after the code
const newRange = document.createRange();
newRange.setStartAfter(codeEl);
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
onEditorChange();
}
function renderTags(note) {
tagsContainer.innerHTML = '';
if (note.tags.length > 0) {
for (const tag of note.tags) {
const tagEl = document.createElement('span');
tagEl.className = 'tag-chip';
tagEl.textContent = `#${tag}`;
tagEl.style.cssText = `
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; border-radius: 12px;
background: ${getColorState().tagBg};
color: ${getColorState().tagText};
font-size: 12px; cursor: pointer;
`;
tagsContainer.append(tagEl);
const removeBtn = document.createElement('span');
removeBtn.className = 'tag-remove';
removeBtn.textContent = '×';
removeBtn.style.cursor = 'pointer';
removeBtn.addEventListener('click', () => {
(0, state_1.updateNote)(note.id, { tags: note.tags.filter(t => t !== tag) });
});
tagEl.append(removeBtn);
}
}
// Add tag input
const input = document.createElement('input');
input.type = 'text';
input.className = 'tag-input';
input.placeholder = '+ Add tag';
input.style.cssText = `
border: none; background: transparent; outline: none;
font-size: 12px; color: ${getColorState().textSecondary};
font-family: ${constants_1.FONTS.ui}; width: 80px;
`;
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const val = input.value.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
if (val && !note.tags.includes(val)) {
(0, state_1.updateNote)(note.id, { tags: [...note.tags, val] });
}
input.value = '';
}
});
tagsContainer.append(input);
}
function getColorState() {
const s = (0, state_1.getState)();
return s.darkMode ? constants_1.COLORS.dark : constants_1.COLORS.light;
}
function attachEditorEvents() {
// Title change
titleEl.addEventListener('input', () => {
const note = (0, state_1.getActiveNote)();
if (!note)
return;
(0, state_1.updateNote)(note.id, { title: titleEl.value });
});
// Editor input
editorEl.addEventListener('input', () => {
if (suppressNextInput) {
suppressNextInput = false;
// Still save the content — suppress was only to prevent double-save
onEditorChange();
return;
}
onEditorChange();
});
// Keydown for markdown shortcuts and list handling
editorEl.addEventListener('keydown', handleEditorKeydown);
// Selection change for toolbar update
document.addEventListener('selectionchange', () => {
const sel = window.getSelection();
if (sel && editorEl.contains(sel.anchorNode)) {
renderToolbar();
}
});
// Click in editor to focus
editorEl.addEventListener('focus', () => {
renderToolbar();
});
// Paste sanitization
editorEl.addEventListener('paste', (e) => {
e.preventDefault();
const html = e.clipboardData?.getData('text/html');
const text = e.clipboardData?.getData('text/plain');
let content;
if (html) {
content = (0, utils_1.sanitizeHtml)(html);
}
else if (text) {
content = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
else {
return;
}
document.execCommand('insertHTML', false, content);
onEditorChange();
});
// Title focus when no note
titleEl.addEventListener('focus', () => {
const note = (0, state_1.getActiveNote)();
if (!note) {
titleEl.blur();
}
});
}
function onEditorChange() {
const note = (0, state_1.getActiveNote)();
if (!note)
return;
isSaving = true;
const content = editorEl.innerHTML;
savedContent = content;
(0, state_1.updateNote)(note.id, { content });
isSaving = false;
// Show saving indicator
saveIndicator.textContent = 'Saving...';
saveIndicator.style.opacity = '0.6';
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
saveIndicator.textContent = 'Saved';
saveIndicator.style.opacity = '0';
}, 1000);
}
function handleEditorKeydown(e) {
const note = (0, state_1.getActiveNote)();
if (!note)
return;
// Ctrl/Cmd+B/I/U shortcuts
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
if (e.key === 'b' || e.key === 'B') {
e.preventDefault();
document.execCommand('bold');
onEditorChange();
return;
}
if (e.key === 'i' || e.key === 'I') {
e.preventDefault();
document.execCommand('italic');
onEditorChange();
return;
}
if (e.key === 'u' || e.key === 'U') {
e.preventDefault();
document.execCommand('underline');
onEditorChange();
return;
}
}
// Markdown shortcuts: detect patterns at line start on space key
if (e.key === ' ') {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
const range = sel.getRangeAt(0);
// Get the text node content before the cursor
// At keydown, the space hasn't been inserted yet, so we check for patterns WITHOUT the trailing space
let textBefore = '';
if (range.startContainer.nodeType === Node.TEXT_NODE) {
const textNode = range.startContainer;
textBefore = textNode.textContent?.slice(0, range.startOffset) ?? '';
}
// Check for line start patterns (without trailing space — the space is what the user is typing now)
// Patterns: # , ## , ### , - , * , [] , >
// We need to check the text ends with the pattern prefix and a space is being typed
const lineMatch = textBefore.match(/(^|\n)(#{1,3}|-|\*|\[\]|\[|>)$/);
if (lineMatch) {
const prefix = lineMatch[1];
const pattern = lineMatch[2];
// Get the text node and remove the pattern
const textNode = range.startContainer;
const textContent = textNode.textContent ?? '';
const removeLen = pattern.length;
const newText = textContent.slice(0, textContent.length - removeLen);
textNode.textContent = newText;
// If there was a newline prefix, handle it
if (prefix === '\n') {
const prevNode = textNode.previousSibling;
if (prevNode && prevNode.nodeType === Node.TEXT_NODE) {
const prevText = prevNode.textContent ?? '';
prevNode.textContent = prevText.slice(0, -1);
}
}
e.preventDefault();
// Apply formatting based on pattern
switch (pattern) {
case '#':
document.execCommand('formatBlock', false, 'h1');
break;
case '##':
document.execCommand('formatBlock', false, 'h2');
break;
case '###':
document.execCommand('formatBlock', false, 'h3');
break;
case '-':
case '*':
document.execCommand('insertUnorderedList');
break;
case '[]':
insertChecklistItem();
onEditorChange();
return;
case '>':
document.execCommand('formatBlock', false, 'blockquote');
break;
case '[':
// Half-typed [], ignore
break;
}
onEditorChange();
return;
}
}
// Enter key: list continuation / exit
if (e.key === 'Enter') {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
let node = sel.anchorNode;
let inList = false;
let parentLi = null;
while (node && node !== editorEl) {
if (node.nodeType === Node.ELEMENT_NODE) {
const tag = node.tagName;
if (tag === 'LI') {
inList = true;
parentLi = node;
}
}
node = node.parentNode;
}
if (inList && parentLi) {
// Check if list item is empty
const isEmpty = parentLi.textContent?.trim() === '' || parentLi.textContent?.trim() === '•' || parentLi.textContent?.trim() === '☐';
if (isEmpty) {
// Exit the list
e.preventDefault();
const parentUl = parentLi.parentElement;
// Remove the empty list item
const p = document.createElement('p');
p.innerHTML = '<br>';
if (parentLi.nextSibling) {
parentLi.parentNode?.insertBefore(p, parentLi.nextSibling);
}
else {
parentLi.parentNode?.appendChild(p);
}
parentLi.remove();
// If UL is empty, remove it too
if (parentUl && !parentUl.children.length) {
parentUl.remove();
}
// Place cursor in the new paragraph
const newRange = document.createRange();
newRange.setStart(p, 0);
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
onEditorChange();
return;
}
// Let browser handle list continuation normally
}
}
}
function insertChecklistItem(checked = false) {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
// Use span-based checkboxes for proper serialization
const checkbox = document.createElement('span');
checkbox.className = 'checklist-checkbox';
checkbox.setAttribute('contenteditable', 'false');
checkbox.textContent = checked ? '☑' : '☐';
checkbox.style.cssText = 'margin-right:6px;cursor:pointer;user-select:none;';
checkbox.addEventListener('click', () => {
checkbox.textContent = checkbox.textContent === '☑' ? '☐' : '☑';
checkbox.className = checkbox.textContent === '☑' ? 'checklist-checkbox checked' : 'checklist-checkbox';
onEditorChange();
});
const br = document.createElement('br');
const fragment = document.createDocumentFragment();
fragment.appendChild(checkbox);
fragment.appendChild(br);
sel.getRangeAt(0).insertNode(fragment);
// Move cursor after the checkbox
const newRange = document.createRange();
newRange.setStartAfter(br);
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
suppressNextInput = true;
onEditorChange();
restoreChecklistHandlers();
}
function restoreChecklistHandlers() {
editorEl.querySelectorAll('.checklist-checkbox').forEach(cb => {
const span = cb;
if (span.dataset.handler)
return;
span.dataset.handler = '1';
span.style.cursor = 'pointer';
span.style.userSelect = 'none';
span.addEventListener('click', () => {
span.textContent = span.textContent === '☑' ? '☐' : '☑';
span.className = span.textContent === '☑' ? 'checklist-checkbox checked' : 'checklist-checkbox';
onEditorChange();
});
});
}
function focusEditor() {
editorEl.focus();
}
function focusTitle() {
titleEl.focus();
titleEl.select();
}
};
// ── module: src/resizer.ts ──
__mods["src/resizer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initResizable = initResizable;
exports.initDragDrop = initDragDrop;
// Resizable panes and drag-drop for notes
const state_1 = require("./state");
function initResizable() {
// Create resize handles dynamically
const mainContent = document.getElementById('main-content');
if (!mainContent)
return;
// Sidebar resize handle
const sidebarHandle = document.createElement('div');
sidebarHandle.className = 'resize-handle sidebar-handle';
sidebarHandle.style.cssText = `
width:4px;cursor:col-resize;background:transparent;
transition:background 0.2s;position:relative;z-index:10;
flex-shrink:0;
`;
sidebarHandle.addEventListener('mouseenter', () => { sidebarHandle.style.background = 'var(--accent, #5b6abf)'; });
sidebarHandle.addEventListener('mouseleave', () => { sidebarHandle.style.background = 'transparent'; });
// Note list resize handle
const noteListHandle = document.createElement('div');
noteListHandle.className = 'resize-handle notelist-handle';
noteListHandle.style.cssText = `
width:4px;cursor:col-resize;background:transparent;
transition:background 0.2s;position:relative;z-index:10;
flex-shrink:0;
`;
noteListHandle.addEventListener('mouseenter', () => { noteListHandle.style.background = 'var(--accent, #5b6abf)'; });
noteListHandle.addEventListener('mouseleave', () => { noteListHandle.style.background = 'transparent'; });
mainContent.insertBefore(sidebarHandle, mainContent.children[1]);
mainContent.insertBefore(noteListHandle, mainContent.children[2]);
// Sidebar collapse/expand button
const sidebar = document.getElementById('sidebar');
if (sidebar) {
const collapseBtn = document.createElement('button');
collapseBtn.className = 'sidebar-expand-btn';
collapseBtn.style.cssText = `
position:absolute;left:0;top:50%;transform:translateY(-50%);
width:20px;height:60px;border:none;background:var(--accent, #5b6abf);
color:white;cursor:pointer;border-radius:0 4px 4px 0;z-index:11;
font-size:10px;display:none;
`;
collapseBtn.textContent = '▶';
collapseBtn.addEventListener('click', () => {
const state = (0, state_1.getState)();
(0, state_1.setSidebarCollapsed)(!state.sidebarCollapsed);
});
sidebar.appendChild(collapseBtn);
// Subscribe to sidebar state
const unsub = (0, state_1.subscribe)(() => {
const s = (0, state_1.getState)();
collapseBtn.style.display = s.sidebarCollapsed ? 'block' : 'none';
});
}
// Make sidebar handle draggable
makeDraggable(sidebarHandle, 'sidebar');
// Make notelist handle draggable
makeDraggable(noteListHandle, 'noteList');
}
function makeDraggable(handle, target) {
let startX = 0;
let startWidth = 0;
handle.addEventListener('mousedown', (e) => {
e.preventDefault();
startX = e.clientX;
const state = (0, state_1.getState)();
startWidth = target === 'sidebar' ? state.paneSizes.sidebar : state.paneSizes.noteList;
const onMove = (ev) => {
const dx = ev.clientX - startX;
const newWidth = Math.max(150, Math.min(500, startWidth + dx));
(0, state_1.setPaneSizes)({
sidebar: target === 'sidebar' ? newWidth : state.paneSizes.sidebar,
noteList: target === 'noteList' ? newWidth : state.paneSizes.noteList,
});
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
function initDragDrop() {
// Drop targets: sidebar notebooks
const sidebar = document.getElementById('sidebar');
if (!sidebar)
return;
sidebar.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
sidebar.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
const noteId = e.dataTransfer.getData('text/plain');
if (!noteId)
return;
// Find which notebook was dropped on
const notebooks = Array.from(sidebar.querySelectorAll('.notebook-header'));
const target = e.target;
const nbEl = target.closest('.notebook-header');
if (nbEl) {
// Find notebook by name matching
const nameEl = nbEl.querySelector('.notebook-name');
if (nameEl) {
const state = (0, state_1.getState)();
const nb = state.notebooks.find(n => n.name === nameEl.textContent);
if (nb) {
(0, state_1.moveNote)(noteId, nb.id);
}
}
}
});
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-UD-Q5_K_XL","provider":"brigid","persona":"typescript","sessionId":"90b65969-adb4-4d9c-9d11-3fef20e2093a","tokensIn":8935989,"tokensOut":93858,"tokensTotal":9029847,"cost":0,"turns":147,"toolCalls":149,"failedToolCalls":5,"timestamp":"2026-08-04T13:35:44.219Z"} -->