Notes
Qwen 3.6 27b MTP · 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 90,955 bytes · SHA-256 bfe672bb2c49
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notes — Your Personal Knowledge Base</title>
<style>
/* ── css: layout ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background: var(--bg);
}
#app {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
}
/* ─── CSS Custom Properties (Light Theme) ─── */
:root, [data-theme="light"] {
--bg: #FAF9F7;
--bg-secondary: #FFFFFF;
--bg-tertiary: #F3F1EE;
--bg-hover: #EDEBE8;
--bg-active: #E5E2DD;
--text-primary: #1C1B1A;
--text-secondary: #6B6760;
--text-tertiary: #9B9790;
--accent: #D4883E;
--accent-hover: #C47A32;
--accent-light: rgba(212, 136, 62, 0.1);
--border: #E5E2DD;
--border-light: #EDEBE8;
--danger: #D94F4F;
--danger-hover: #C74444;
--success: #4CAF50;
--tag-bg: rgba(212, 136, 62, 0.1);
--tag-text: #B8722E;
--editor-bg: #FFFFFF;
--shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
--shadow-lg: 0 4px 12px rgba(0,0,0,0.08);
}
[data-theme="dark"] {
--bg: #1A1917;
--bg-secondary: #232220;
--bg-tertiary: #2C2B28;
--bg-hover: #353430;
--bg-active: #3D3C38;
--text-primary: #E8E6E1;
--text-secondary: #9B9790;
--text-tertiary: #6B6760;
--accent: #E8A54F;
--accent-hover: #D4953F;
--accent-light: rgba(232, 165, 79, 0.12);
--border: #353430;
--border-light: #2C2B28;
--danger: #E06060;
--danger-hover: #D04F4F;
--success: #6BC970;
--tag-bg: rgba(232, 165, 79, 0.12);
--tag-text: #E8A54F;
--editor-bg: #232220;
--shadow: 0 1px 3px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.15);
--shadow-lg: 0 4px 12px rgba(0,0,0,0.3);
}
/* ─── Resize Handles ─── */
.resize-handle {
width: 4px;
cursor: col-resize;
background: transparent;
transition: background 0.15s;
flex-shrink: 0;
z-index: 10;
}
.resize-handle:hover, .resize-handle.active {
background: var(--accent);
}
/* ─── Scrollbar Styling ─── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--bg-active); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
/* ─── Selection ─── */
::selection { background: var(--accent-light); }
/* ─── Focus Ring ─── */
:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
/* ─── Animations ─── */
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slideIn { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: translateX(0); } }
/* ─── Utility Classes ─── */
.hidden { display: none !important; }
.fade-in { animation: fadeIn 0.2s ease-out; }
/* ── css: sidebar ── */
/* ─── Sidebar Pane ─── */
#sidebar-pane {
width: var(--sidebar-width, 240px);
min-width: 180px;
max-width: 400px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
transition: width 0.2s ease, min-width 0.2s ease;
}
#sidebar-pane.collapsed {
width: 0 !important;
min-width: 0 !important;
border-right: none;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 14px 8px;
flex-shrink: 0;
}
.sidebar-title {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
}
.sidebar-actions {
display: flex;
gap: 2px;
}
.sidebar-btn {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.15s;
}
.sidebar-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* ─── Sidebar Search ─── */
.sidebar-search {
padding: 4px 12px 8px;
flex-shrink: 0;
}
.sidebar-search input {
width: 100%;
height: 32px;
border: 1px solid var(--border);
border-radius: 6px;
padding: 0 10px 0 32px;
font-size: 13px;
background: var(--bg-tertiary) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239B9790' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") 10px center no-repeat;
color: var(--text-primary);
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.sidebar-search input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-light);
}
.sidebar-search input::placeholder {
color: var(--text-tertiary);
}
/* ─── Sidebar Navigation ─── */
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 4px 8px;
}
.nav-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
transition: all 0.12s;
user-select: none;
position: relative;
}
.nav-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.nav-item.active {
background: var(--accent-light);
color: var(--accent);
font-weight: 600;
}
.nav-icon {
width: 18px;
text-align: center;
flex-shrink: 0;
font-size: 15px;
}
.nav-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nav-count {
font-size: 11px;
color: var(--text-tertiary);
min-width: 20px;
text-align: right;
}
/* ─── Notebook Items ─── */
.notebook-item {
margin-top: 4px;
}
.notebook-header {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
transition: all 0.12s;
user-select: none;
}
.notebook-header:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.notebook-header.active {
background: var(--accent-light);
color: var(--accent);
font-weight: 600;
}
.notebook-chevron {
width: 14px;
text-align: center;
flex-shrink: 0;
transition: transform 0.2s;
font-size: 10px;
}
.notebook-header.collapsed .notebook-chevron {
transform: rotate(-90deg);
}
.notebook-notes {
overflow: hidden;
max-height: 1000px;
transition: max-height 0.25s ease, opacity 0.2s;
opacity: 1;
}
.notebook-notes.collapsed {
max-height: 0 !important;
opacity: 0;
}
.notebook-note-item {
padding-left: 34px;
}
/* ─── Sidebar Footer ─── */
.sidebar-footer {
padding: 8px 12px;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.add-notebook-btn, .add-note-btn {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 7px 10px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 6px;
font-size: 13px;
transition: all 0.12s;
}
.add-notebook-btn:hover, .add-note-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* ─── Context Menu ─── */
.context-menu {
position: fixed;
z-index: 1000;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow-lg);
padding: 4px;
min-width: 160px;
animation: fadeIn 0.12s ease-out;
}
.context-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
color: var(--text-primary);
transition: background 0.1s;
}
.context-menu-item:hover {
background: var(--bg-hover);
}
.context-menu-item.danger {
color: var(--danger);
}
.context-menu-divider {
height: 1px;
background: var(--border-light);
margin: 4px 8px;
}
/* ─── Tag Filter Bar ─── */
.tag-filter-bar {
display: flex;
gap: 6px;
padding: 6px 12px;
flex-wrap: wrap;
border-bottom: 1px solid var(--border-light);
}
.tag-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: var(--tag-bg);
color: var(--tag-text);
border-radius: 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.tag-chip:hover {
opacity: 0.8;
}
.tag-chip.active {
background: var(--accent);
color: white;
}
/* ── css: note-list ── */
/* ─── Note List Pane ─── */
#note-list-pane {
width: var(--notelist-width, 300px);
min-width: 200px;
max-width: 500px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.notelist-header {
padding: 14px 16px 8px;
flex-shrink: 0;
border-bottom: 1px solid var(--border-light);
}
.notelist-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
}
.notelist-title {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
}
.notelist-actions {
display: flex;
gap: 2px;
}
.sort-btn {
padding: 4px 8px;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
border-radius: 4px;
font-size: 12px;
transition: all 0.15s;
}
.sort-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
/* ─── Note List Items ─── */
.note-list-container {
flex: 1;
overflow-y: auto;
padding: 4px 8px;
}
.note-item {
display: flex;
flex-direction: column;
gap: 3px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
position: relative;
}
.note-item:hover {
background: var(--bg-hover);
}
.note-item.active {
background: var(--accent-light);
}
.note-item-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.note-item.active .note-item-title {
color: var(--accent);
}
.note-item-snippet {
font-size: 12px;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
}
.note-item-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-tertiary);
}
.pin-indicator {
color: var(--accent);
font-size: 12px;
}
.note-item-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.note-tag {
padding: 1px 6px;
background: var(--tag-bg);
color: var(--tag-text);
border-radius: 8px;
font-size: 10px;
cursor: pointer;
transition: opacity 0.15s;
}
.note-tag:hover {
opacity: 0.7;
}
/* ─── Search Highlight ─── */
.search-highlight {
background: rgba(212, 136, 62, 0.25);
border-radius: 2px;
padding: 0 1px;
}
[data-theme="dark"] .search-highlight {
background: rgba(232, 165, 79, 0.3);
}
/* ─── Empty State ─── */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
color: var(--text-tertiary);
}
.empty-state-icon {
font-size: 36px;
margin-bottom: 12px;
opacity: 0.5;
}
.empty-state-text {
font-size: 14px;
line-height: 1.5;
}
/* ─── Note Actions Dropdown ─── */
.note-actions-btn {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.15s;
}
.note-item:hover .note-actions-btn {
opacity: 1;
}
.note-actions-btn:hover {
background: var(--bg-active);
color: var(--text-primary);
}
/* ── css: editor ── */
/* ─── Editor Pane ─── */
#editor-pane {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
}
/* ─── Toolbar ─── */
#toolbar {
display: flex;
align-items: center;
gap: 2px;
padding: 6px 12px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-secondary);
flex-shrink: 0;
overflow-x: auto;
}
.toolbar-btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.12s;
flex-shrink: 0;
}
.toolbar-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.toolbar-btn.active {
background: var(--accent-light);
color: var(--accent);
}
.toolbar-separator {
width: 1px;
height: 20px;
background: var(--border);
margin: 0 4px;
flex-shrink: 0;
}
/* ─── Editor Header (Title + Tags) ─── */
#editor-header {
padding: 16px 32px 8px;
flex-shrink: 0;
}
.editor-title-input {
width: 100%;
border: none;
background: transparent;
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
outline: none;
font-family: inherit;
line-height: 1.3;
}
.editor-title-input::placeholder {
color: var(--text-tertiary);
}
.editor-tags-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
flex-wrap: wrap;
}
.editor-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: var(--tag-bg);
color: var(--tag-text);
border-radius: 10px;
font-size: 12px;
}
.editor-tag-remove {
cursor: pointer;
opacity: 0.6;
font-size: 14px;
line-height: 1;
}
.editor-tag-remove:hover {
opacity: 1;
}
.add-tag-input {
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 12px;
outline: none;
width: 80px;
padding: 2px 4px;
}
.add-tag-input::placeholder {
color: var(--text-tertiary);
}
/* ─── Editor Content ─── */
#editor-content {
flex: 1;
overflow-y: auto;
padding: 8px 32px 60px;
font-family: 'Georgia', 'Noto Serif', 'Source Serif Pro', serif;
font-size: 16px;
line-height: 1.75;
color: var(--text-primary);
outline: none;
}
#editor-content:empty::before {
content: attr(data-placeholder);
color: var(--text-tertiary);
pointer-events: none;
}
/* ─── Editor Content Typography ─── */
#editor-content h1 { font-size: 2em; font-weight: 700; margin: 0.6em 0 0.3em; line-height: 1.3; }
#editor-content h2 { font-size: 1.5em; font-weight: 700; margin: 0.5em 0 0.25em; line-height: 1.35; }
#editor-content h3 { font-size: 1.25em; font-weight: 600; margin: 0.4em 0 0.2em; line-height: 1.4; }
#editor-content blockquote {
border-left: 3px solid var(--accent);
padding-left: 16px;
margin: 0.5em 0;
color: var(--text-secondary);
font-style: italic;
}
#editor-content code {
background: var(--bg-tertiary);
padding: 2px 5px;
border-radius: 3px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.9em;
}
#editor-content hr {
border: none;
height: 1px;
background: var(--border);
margin: 1.5em 0;
}
/* ─── Lists in Editor ─── */
#editor-content ul, #editor-content ol {
padding-left: 24px;
margin: 0.3em 0;
}
#editor-content li {
margin-bottom: 2px;
}
/* ─── Checklist Items ─── */
.checklist-item {
list-style: none !important;
display: flex;
align-items: flex-start;
gap: 8px;
padding-left: 0 !important;
}
.checklist-checkbox {
width: 16px;
height: 16px;
border: 2px solid var(--border);
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
margin-top: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.checklist-checkbox.checked {
background: var(--accent);
border-color: var(--accent);
}
.checklist-checkbox.checked::after {
content: '✓';
color: white;
font-size: 11px;
line-height: 1;
}
.checklist-item.checked .checklist-text {
text-decoration: line-through;
color: var(--text-tertiary);
}
/* ─── Saved Indicator ─── */
#saved-indicator {
position: fixed;
bottom: 16px;
right: 24px;
padding: 6px 14px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 20px;
font-size: 12px;
color: var(--text-tertiary);
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(8px);
transition: all 0.3s ease;
pointer-events: none;
z-index: 100;
}
#saved-indicator.visible {
opacity: 1;
transform: translateY(0);
}
/* ─── Editor Empty State ─── */
.editor-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-tertiary);
}
.editor-empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.3;
}
.editor-empty-text {
font-size: 16px;
color: var(--text-tertiary);
}
/* ─── Move Menu ─── */
.move-menu {
position: fixed;
z-index: 1000;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow-lg);
padding: 4px;
min-width: 180px;
animation: fadeIn 0.12s ease-out;
}
.move-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
color: var(--text-primary);
transition: background 0.1s;
}
.move-menu-item:hover {
background: var(--bg-hover);
}
/* ─── Drag Over Highlight ─── */
.drag-over {
outline: 2px dashed var(--accent) !important;
outline-offset: -2px;
}
</style>
</head>
<body>
<div id="sidebar-pane"></div>
<div id="sidebar-resize-handle" class="resize-handle vertical"></div>
<div id="note-list-pane"></div>
<div id="notelist-resize-handle" class="resize-handle vertical"></div>
<div id="editor-pane">
<div id="toolbar"></div>
<div id="editor-header"></div>
<div id="editor-content" contenteditable="true" spellcheck="true"></div>
<div id="saved-indicator"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./state":"src/state.ts","./editor":"src/editor.ts","./ui":"src/ui.ts"},"src/state.ts":{"./types":"src/types.ts"},"src/editor.ts":{"./types":"src/types.ts","./state":"src/state.ts"},"src/ui.ts":{"./types":"src/types.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";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const State = __importStar(require("./state"));
const editor_1 = require("./editor");
const ui_1 = require("./ui");
// ─── Initialize ──────────────────────────────────────────────────────
State.initState();
const editor = new editor_1.Editor({
onTitleChange: () => { }, // handled inline via updateNote
onSave: () => ui.renderSidebar(), // re-render sidebar counts after save
});
const ui = new ui_1.AppUI();
// ─── Apply Initial Layout & Theme ────────────────────────────────────
ui.applyLayout();
// ─── Load Selected Note ──────────────────────────────────────────────
function loadSelectedNote() {
const note = State.getSelectedNote();
if (note) {
editor.loadNote(note);
}
else {
editor.clear();
ui.showEditorEmptyState();
}
}
// ─── Subscribe to State Changes ──────────────────────────────────────
State.subscribe(() => {
const s = State.getState();
// Apply layout changes (theme, pane sizes)
ui.applyLayout();
// Re-render sidebar (notebook counts change)
ui.renderSidebar();
// Re-render note list when filters/sort/view change
ui.renderNoteList();
// Load selected note into editor
loadSelectedNote();
});
// ─── Wire Up Toolbar Actions in Editor Pane ──────────────────────────
const editorPane = document.getElementById('editor-pane');
editorPane.addEventListener('toolbar-action', (e) => {
const detail = e.detail;
if (detail.heading) {
const level = parseInt(detail.heading.replace('h', ''), 10);
editor.applyHeading(level);
}
else if (detail.cmd === 'b') {
document.execCommand('bold');
}
else if (detail.cmd === 'i') {
document.execCommand('italic');
}
else if (detail.cmd === 'u') {
document.execCommand('underline');
}
else if (detail.cmd === 's') {
document.execCommand('strikethrough');
}
else if (detail.cmd === 'code') {
editor.applyInlineCode();
}
else if (detail.cmd === 'blockquote') {
editor.applyBlockquote();
}
else if (detail.cmd === 'hr') {
editor.applyHorizontalRule();
}
// Re-focus editor after toolbar action
const contentEl = document.getElementById('editor-content');
if (contentEl)
contentEl.focus();
});
// ─── Sidebar Drag-and-Drop for Notebooks ──────────────────────────────
const sidebarPane = document.getElementById('sidebar-pane');
sidebarPane.addEventListener('dragover', (e) => {
e.preventDefault();
const nbItem = e.target.closest('.notebook-item, .nav-item[data-id="all"]');
if (nbItem) {
nbItem.classList.add('drag-over');
}
});
sidebarPane.addEventListener('dragleave', (e) => {
const nbItem = e.target.closest('.notebook-item, .nav-item[data-id="all"]');
if (nbItem)
nbItem.classList.remove('drag-over');
});
sidebarPane.addEventListener('drop', (e) => {
e.preventDefault();
const noteId = e.dataTransfer.getData('text/plain');
if (!noteId)
return;
// Find target notebook
const nbHeader = e.target.closest('.notebook-header');
if (nbHeader) {
const targetNbId = nbHeader.getAttribute('data-id');
if (targetNbId && targetNbId !== 'trash' && targetNbId !== 'all') {
State.moveNoteToNotebook(noteId, targetNbId);
}
}
// Clean up drag-over highlights
sidebarPane.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
});
// ─── Keyboard Shortcuts (Global) ──────────────────────────────────────
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + N — new note
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'n') {
e.preventDefault();
const s = State.getState();
let targetNb;
if (s.selectedNotebookId !== 'all' && s.selectedNotebookId !== 'trash') {
targetNb = s.selectedNotebookId;
}
else if (s.notebooks.length > 0) {
targetNb = s.notebooks[0].id;
}
State.createNote(targetNb);
}
// Ctrl/Cmd + F — focus search
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'f') {
const contentEl = document.getElementById('editor-content');
const sel = window.getSelection();
// Only prevent default if there's no text selection in the editor
if (!sel || sel.isCollapsed || !contentEl?.contains(sel.anchorNode)) {
e.preventDefault();
const searchInput = document.getElementById('search-input');
if (searchInput)
searchInput.focus();
}
}
// Escape — clear tag filter or deselect note
if (e.key === 'Escape') {
const s = State.getState();
if (s.tagFilter) {
State.setTagFilter(null);
}
else if (s.searchQuery) {
State.setSearchQuery('');
}
}
});
// ─── Initial Render ──────────────────────────────────────────────────
ui.renderSidebar();
ui.renderNoteList();
loadSelectedNote();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSTANTS = void 0;
// ─── Tunable Constants ────────────────────────────────────────────────
exports.CONSTANTS = {
// Colors — warm light theme
colors: {
bg: '#FAF9F7',
bgSecondary: '#FFFFFF',
bgTertiary: '#F3F1EE',
bgHover: '#EDEBE8',
bgActive: '#E5E2DD',
textPrimary: '#1C1B1A',
textSecondary: '#6B6760',
textTertiary: '#9B9790',
accent: '#D4883E',
accentHover: '#C47A32',
accentLight: 'rgba(212, 136, 62, 0.1)',
border: '#E5E2DD',
borderLight: '#EDEBE8',
danger: '#D94F4F',
dangerHover: '#C74444',
success: '#4CAF50',
tagBg: 'rgba(212, 136, 62, 0.1)',
tagText: '#B8722E',
editorBg: '#FFFFFF',
shadow: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
shadowLg: '0 4px 12px rgba(0,0,0,0.08)',
},
// Dark theme colors
darkColors: {
bg: '#1A1917',
bgSecondary: '#232220',
bgTertiary: '#2C2B28',
bgHover: '#353430',
bgActive: '#3D3C38',
textPrimary: '#E8E6E1',
textSecondary: '#9B9790',
textTertiary: '#6B6760',
accent: '#E8A54F',
accentHover: '#D4953F',
accentLight: 'rgba(232, 165, 79, 0.12)',
border: '#353430',
borderLight: '#2C2B28',
danger: '#E06060',
dangerHover: '#D04F4F',
success: '#6BC970',
tagBg: 'rgba(232, 165, 79, 0.12)',
tagText: '#E8A54F',
editorBg: '#232220',
shadow: '0 1px 3px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.15)',
shadowLg: '0 4px 12px rgba(0,0,0,0.3)',
},
// Fonts
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
editorFontFamily: "'Georgia', 'Noto Serif', 'Source Serif Pro', serif",
fontSizeSm: '12px',
fontSizeBase: '14px',
fontSizeLg: '16px',
// Layout
sidebarMinWidth: 200,
sidebarMaxWidth: 400,
noteListMinWidth: 220,
noteListMaxWidth: 500,
defaultSidebarWidth: 240,
defaultNoteListWidth: 300,
// Behavior
autosaveDebounceMs: 800,
snippetLength: 120,
maxTitleLength: 200,
};
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadState = loadState;
exports.saveState = saveState;
exports.getState = getState;
exports.setState = setState;
exports.updateState = updateState;
exports.subscribe = subscribe;
exports.genId = genId;
exports.createNotebook = createNotebook;
exports.renameNotebook = renameNotebook;
exports.deleteNotebook = deleteNotebook;
exports.toggleNotebookCollapse = toggleNotebookCollapse;
exports.createNote = createNote;
exports.updateNote = updateNote;
exports.togglePin = togglePin;
exports.duplicateNote = duplicateNote;
exports.moveToTrash = moveToTrash;
exports.restoreFromTrash = restoreFromTrash;
exports.permanentlyDelete = permanentlyDelete;
exports.moveNoteToNotebook = moveNoteToNotebook;
exports.selectNotebook = selectNotebook;
exports.selectNote = selectNote;
exports.setSortMode = setSortMode;
exports.setSearchQuery = setSearchQuery;
exports.setTagFilter = setTagFilter;
exports.toggleDarkTheme = toggleDarkTheme;
exports.setPaneLayout = setPaneLayout;
exports.getFilteredNotes = getFilteredNotes;
exports.getNoteCountForNotebook = getNoteCountForNotebook;
exports.getSelectedNote = getSelectedNote;
exports.extractPlainText = extractPlainText;
exports.extractSnippet = extractSnippet;
exports.sanitizeHTML = sanitizeHTML;
exports.initState = initState;
const types_1 = require("./types");
const STORAGE_KEY = 'notes-app-state-v1';
// ─── Default State ────────────────────────────────────────────────────
function createDefaultState() {
const now = Date.now();
return {
notebooks: [
{ id: 'nb-personal', name: 'Personal', collapsed: false },
{ id: 'nb-work', name: 'Work', collapsed: false },
{ id: 'nb-ideas', name: 'Ideas', collapsed: false },
],
notes: [
{
id: 'note-welcome',
title: 'Welcome to Notes',
content: '<p>This is your new <b>notes application</b>. Here are some things you can try:</p><ul><li data-list="bullet"><span>Create a checklist item by typing <code>[] </code></span></li><li data-list="bullet"><span>Format text with <b>bold</b>, <i>italic</i>, or <u>underline</u></span></li><li data-list="bullet"><span>Type <code># </code> for headings, <code>> </code> for blockquotes</span></li></ul><p>Your notes are saved automatically to your browser.</p>',
notebookId: 'nb-personal',
tags: ['welcome', 'getting-started'],
pinned: true,
createdAt: now,
updatedAt: now,
},
],
selectedNotebookId: 'all',
selectedNoteId: 'note-welcome',
sortMode: 'modified',
searchQuery: '',
tagFilter: null,
darkTheme: false,
paneLayout: {
sidebarWidth: types_1.CONSTANTS.defaultSidebarWidth,
noteListWidth: types_1.CONSTANTS.defaultNoteListWidth,
sidebarCollapsed: false,
},
};
}
// ─── Persistence ──────────────────────────────────────────────────────
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
// Merge with defaults for forward compatibility
return { ...createDefaultState(), ...parsed };
}
}
catch {
// Corrupted storage — start fresh
}
return createDefaultState();
}
function saveState(state) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
catch (e) {
console.warn('Failed to save state:', e);
}
}
// ─── State Mutators ───────────────────────────────────────────────────
let _state = null;
const listeners = new Set();
function getState() {
if (!_state)
throw new Error('State not initialized');
return _state;
}
function setState(state) {
_state = state;
notifyListeners();
}
function updateState(updater) {
if (!_state)
throw new Error('State not initialized');
const next = updater(_state);
_state = next;
saveState(next);
notifyListeners();
}
function subscribe(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
function notifyListeners() {
for (const fn of listeners) {
try {
fn();
}
catch (e) {
console.error('Listener error:', e);
}
}
}
// ─── ID Generation ────────────────────────────────────────────────────
let _idCounter = Date.now();
function genId() {
return `id-${++_idCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
// ─── Notebook Operations ──────────────────────────────────────────────
function createNotebook(name) {
updateState(s => ({
...s,
notebooks: [...s.notebooks, { id: genId(), name, collapsed: false }],
}));
}
function renameNotebook(id, name) {
updateState(s => ({
...s,
notebooks: s.notebooks.map(nb => nb.id === id ? { ...nb, name } : nb),
}));
}
function deleteNotebook(id) {
updateState(s => {
const notesInNotebook = s.notes.filter(n => n.notebookId === id);
return {
...s,
notebooks: s.notebooks.filter(nb => nb.id !== id),
notes: [
...notesInNotebook.map(n => ({
...n,
notebookId: 'trash',
originalNotebookId: n.notebookId,
updatedAt: Date.now(),
})),
...s.notes.filter(n => n.notebookId !== id),
],
selectedNotebookId: s.selectedNotebookId === id ? 'all' : s.selectedNotebookId,
};
});
}
function toggleNotebookCollapse(id) {
updateState(s => ({
...s,
notebooks: s.notebooks.map(nb => nb.id === id ? { ...nb, collapsed: !nb.collapsed } : nb),
}));
}
// ─── Note Operations ──────────────────────────────────────────────────
function createNote(notebookId) {
const now = Date.now();
const note = {
id: genId(),
title: '',
content: '<p><br></p>',
notebookId: (notebookId || 'nb-personal'),
tags: [],
pinned: false,
createdAt: now,
updatedAt: now,
};
updateState(s => ({
...s,
notes: [note, ...s.notes],
selectedNotebookId: s.selectedNotebookId === 'trash' ? note.notebookId : s.selectedNotebookId,
selectedNoteId: note.id,
}));
return note;
}
function updateNote(id, updates) {
const now = Date.now();
updateState(s => ({
...s,
notes: s.notes.map(n => n.id === id ? { ...n, ...updates, updatedAt: now } : n),
}));
}
function togglePin(id) {
updateState(s => ({
...s,
notes: s.notes.map(n => n.id === id ? { ...n, pinned: !n.pinned, updatedAt: Date.now() } : n),
}));
}
function duplicateNote(id) {
const now = Date.now();
updateState(s => {
const orig = s.notes.find(n => n.id === id);
if (!orig)
return s;
const dup = {
...orig,
id: genId(),
title: `${orig.title} (copy)`,
pinned: false,
createdAt: now,
updatedAt: now,
};
return { ...s, notes: [dup, ...s.notes], selectedNoteId: dup.id };
});
}
function moveToTrash(id) {
updateState(s => ({
...s,
notes: s.notes.map(n => n.id === id ? {
...n, notebookId: 'trash', originalNotebookId: (n.notebookId !== 'trash' ? n.notebookId : undefined), updatedAt: Date.now()
} : n),
}));
}
function restoreFromTrash(id) {
updateState(s => ({
...s,
notes: s.notes.map(n => n.id === id ? {
...n, notebookId: (n.originalNotebookId || 'nb-personal'), updatedAt: Date.now()
} : n),
}));
}
function permanentlyDelete(id) {
updateState(s => ({
...s,
notes: s.notes.filter(n => n.id !== id),
selectedNoteId: s.selectedNoteId === id ? null : s.selectedNoteId,
}));
}
function moveNoteToNotebook(noteId, targetNotebookId) {
updateState(s => ({
...s,
notes: s.notes.map(n => n.id === noteId ? {
...n, notebookId: targetNotebookId, updatedAt: Date.now()
} : n),
}));
}
// ─── Selection & UI State ─────────────────────────────────────────────
function selectNotebook(id) {
updateState(s => ({ ...s, selectedNotebookId: id }));
}
function selectNote(id) {
updateState(s => ({ ...s, selectedNoteId: id }));
}
function setSortMode(mode) {
updateState(s => ({ ...s, sortMode: mode }));
}
function setSearchQuery(query) {
updateState(s => ({ ...s, searchQuery: query }));
}
function setTagFilter(tag) {
updateState(s => ({ ...s, tagFilter: tag }));
}
function toggleDarkTheme() {
updateState(s => ({ ...s, darkTheme: !s.darkTheme }));
}
// ─── Pane Layout ──────────────────────────────────────────────────────
function setPaneLayout(layout) {
updateState(s => ({ ...s, paneLayout: { ...s.paneLayout, ...layout } }));
}
// ─── Derived Data ─────────────────────────────────────────────────────
function getFilteredNotes() {
const s = getState();
let notes = s.notes;
// Filter by notebook view
if (s.selectedNotebookId === 'trash') {
notes = notes.filter(n => n.notebookId === 'trash');
}
else if (s.selectedNotebookId !== 'all') {
notes = notes.filter(n => n.notebookId === s.selectedNotebookId);
}
// Filter by tag
if (s.tagFilter) {
const tf = s.tagFilter;
notes = notes.filter(n => n.tags.includes(tf));
}
// Search filter
if (s.searchQuery.trim()) {
const q = s.searchQuery.toLowerCase().trim();
notes = notes.filter(n => {
const titleMatch = n.title.toLowerCase().includes(q);
const bodyMatch = extractPlainText(n.content).toLowerCase().includes(q);
return titleMatch || bodyMatch;
});
}
// Sort: pinned first, then by sort mode
notes.sort((a, b) => {
if (a.pinned !== b.pinned)
return a.pinned ? -1 : 1;
switch (s.sortMode) {
case 'created': return b.createdAt - a.createdAt;
case 'alphabetical': return a.title.localeCompare(b.title);
default: return b.updatedAt - a.updatedAt;
}
});
return notes;
}
function getNoteCountForNotebook(notebookId) {
const s = getState();
if (notebookId === 'all')
return s.notes.filter(n => n.notebookId !== 'trash').length;
if (notebookId === 'trash')
return s.notes.filter(n => n.notebookId === 'trash').length;
return s.notes.filter(n => n.notebookId === notebookId).length;
}
function getSelectedNote() {
const s = getState();
if (!s.selectedNoteId)
return null;
return s.notes.find(n => n.id === s.selectedNoteId) || null;
}
// ─── Text Extraction (for search & snippets) ──────────────────────────
function extractPlainText(html) {
const div = document.createElement('div');
div.innerHTML = html;
// Remove script/style tags entirely before extracting text
div.querySelectorAll('script, style').forEach(el => el.remove());
return (div.textContent || '').trim();
}
function extractSnippet(html, maxLength) {
const plain = extractPlainText(html);
if (!plain)
return '';
if (plain.length <= maxLength)
return plain;
// Try to break at a word boundary
let truncated = plain.slice(0, maxLength);
const lastSpace = truncated.lastIndexOf(' ');
if (lastSpace > maxLength * 0.6) {
truncated = truncated.slice(0, lastSpace);
}
return truncated + '…';
}
// ─── HTML Sanitization ────────────────────────────────────────────────
const ALLOWED_TAGS = new Set([
'p', 'br', 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'code',
'h1', 'h2', 'h3', 'blockquote', 'hr', 'ul', 'ol', 'li', 'span', 'div',
]);
const DANGEROUS_TAGS = new Set([
'script', 'style', 'iframe', 'object', 'embed', 'form', 'input',
'button', 'select', 'textarea', 'link', 'meta', 'base',
]);
const ALLOWED_ATTRS = new Set([
'data-list', 'data-checked', 'class', 'style',
]);
function sanitizeHTML(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
for (const node of Array.from(doc.body.childNodes)) {
sanitizeNode(node);
}
return doc.body.innerHTML;
}
function sanitizeNode(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node;
const tag = el.tagName.toLowerCase();
// Remove dangerous tags entirely (including children)
if (DANGEROUS_TAGS.has(tag)) {
el.remove();
return;
}
// Remove disallowed tags but keep children
if (!ALLOWED_TAGS.has(tag)) {
while (el.firstChild) {
el.parentNode?.insertBefore(el.firstChild, el);
}
el.remove();
return;
}
// Clean attributes
for (const attr of Array.from(el.attributes)) {
if (!ALLOWED_ATTRS.has(attr.name.toLowerCase())) {
el.removeAttribute(attr.name);
}
}
// Remove style properties that could be dangerous
const style = el.getAttribute('style') || '';
const cleanStyle = style.replace(/expression|javascript|vbscript|behavior/gi, '');
if (cleanStyle !== style) {
el.setAttribute('style', cleanStyle);
}
// Recurse into children
sanitizeNode(node.firstChild);
}
else if (node.nodeType === Node.TEXT_NODE) {
// Keep text nodes as-is
}
else {
// Remove comment and other node types
node.parentNode?.removeChild(node);
}
}
// ─── Initialize State ─────────────────────────────────────────────────
function initState() {
_state = loadState();
}
};
// ── module: src/editor.ts ──
__mods["src/editor.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Editor = void 0;
// ─── Rich Text Editor Module ──────────────────────────────────────────
const types_1 = require("./types");
const state_1 = require("./state");
class Editor {
constructor(callbacks) {
this.currentNoteId = null;
this.saveTimer = null;
this.isComposing = false;
this.contentEl = document.getElementById('editor-content');
this.callbacks = callbacks;
this.setupTitleInput();
this.setupContentEditable();
this.setupPasteSanitization();
this.savedIndicator = document.getElementById('saved-indicator');
}
setupTitleInput() {
const headerEl = document.getElementById('editor-header');
this.titleInput = document.createElement('input');
this.titleInput.type = 'text';
this.titleInput.className = 'editor-title-input';
this.titleInput.placeholder = 'Untitled Note';
this.titleInput.maxLength = types_1.CONSTANTS.maxTitleLength;
let titleTimer = null;
this.titleInput.addEventListener('input', () => {
if (!this.currentNoteId)
return;
const val = this.titleInput.value.trim();
(0, state_1.updateNote)(this.currentNoteId, { title: val });
clearTimeout(titleTimer);
titleTimer = setTimeout(() => this.scheduleSave(), 300);
});
headerEl.innerHTML = '';
headerEl.appendChild(this.titleInput);
}
setupContentEditable() {
this.contentEl.addEventListener('keydown', (e) => this.handleKeyDown(e));
this.contentEl.addEventListener('input', () => this.onEditorInput());
this.contentEl.addEventListener('mouseup', () => this.updateToolbarState());
this.contentEl.addEventListener('keyup', () => this.updateToolbarState());
// Handle checklist checkbox clicks
this.contentEl.addEventListener('click', (e) => {
const cb = e.target.closest('.checklist-checkbox');
if (cb) {
e.preventDefault();
this.toggleChecklistCheckbox(cb);
}
});
// Prevent drag-and-drop of text from messing up content
this.contentEl.addEventListener('dragover', (e) => e.preventDefault());
// IME composition events
this.contentEl.addEventListener('compositionstart', () => { this.isComposing = true; });
this.contentEl.addEventListener('compositionend', () => {
this.isComposing = false;
this.onEditorInput();
});
}
setupPasteSanitization() {
this.contentEl.addEventListener('paste', (e) => {
e.preventDefault();
const clipboardData = e.clipboardData;
if (!clipboardData)
return;
let html = clipboardData.getData('text/html');
if (!html) {
const text = clipboardData.getData('text/plain');
if (text) {
document.execCommand('insertText', false, text);
this.scheduleSave();
}
return;
}
html = (0, state_1.sanitizeHTML)(html);
document.execCommand('insertHTML', false, html);
this.scheduleSave();
});
}
onEditorInput() {
if (!this.isComposing) {
this.updateToolbarState();
this.scheduleSave();
}
}
// ─── Key Handling ────────────────────────────────────────────────
handleKeyDown(e) {
if (this.isComposing)
return;
const sel = window.getSelection();
if (!sel || !sel.rangeCount)
return;
// Ctrl/Cmd shortcuts
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
switch (e.key.toLowerCase()) {
case 'b':
e.preventDefault();
this.execFormat('bold');
break;
case 'i':
e.preventDefault();
this.execFormat('italic');
break;
case 'u':
e.preventDefault();
this.execFormat('underline');
break;
}
}
// Enter key — list continuation and empty item exit
if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey) {
const blockEl = this.getCurrentBlockElement();
if (!blockEl)
return;
const li = this.getListItem(blockEl);
if (li) {
const textContent = this.getBlockTextContent(li);
if (textContent.trim() === '' || this.isChecklistEmpty(li)) {
e.preventDefault();
this.exitList(li);
return;
}
// Non-empty: let browser continue the list, then clean up structure
setTimeout(() => this.cleanUpNewListItem(), 0);
}
}
// Space — markdown shortcuts at line start
if (e.key === ' ') {
const blockEl = this.getCurrentBlockElement();
if (!blockEl)
return;
const textBeforeCursor = this.getTextBeforeCursor(blockEl);
this.applyMarkdownShortcut(textBeforeCursor, e);
}
}
getTextBeforeCursor(block) {
const sel = window.getSelection();
if (!sel || !sel.rangeCount)
return '';
const range = sel.getRangeAt(0);
const preRange = document.createRange();
preRange.selectNodeContents(block);
preRange.setEnd(range.startContainer, range.startOffset);
return preRange.toString();
}
applyMarkdownShortcut(textBefore, e) {
if (!textBefore)
return;
const trimmed = textBefore.trimStart();
let replacementText = '';
let tag = null;
if (trimmed === '# ') {
replacementText = textBefore.slice(textBefore.length - 2);
tag = 'h1';
}
else if (trimmed === '## ') {
replacementText = textBefore.slice(textBefore.length - 3);
tag = 'h2';
}
else if (trimmed === '### ') {
replacementText = textBefore.slice(textBefore.length - 4);
tag = 'h3';
}
else if (trimmed === '- ' || trimmed === '* ') {
e.preventDefault();
this.convertToBulletList();
return;
}
else if (trimmed === '[] ') {
e.preventDefault();
this.convertToChecklistItem();
return;
}
else if (trimmed === '> ') {
replacementText = textBefore.slice(textBefore.length - 2);
tag = 'blockquote';
}
if (!tag)
return;
const blockEl = this.getCurrentBlockElement();
if (!blockEl)
return;
e.preventDefault();
// Remove the shortcut prefix and wrap in the target tag
blockEl.innerHTML = `<${tag}>${replacementText}<br></${tag}>`;
// Place cursor at end of content
const sel = window.getSelection();
if (!sel)
return;
const range = document.createRange();
range.selectNodeContents(blockEl);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
this.scheduleSave();
}
convertToBulletList() {
const blockEl = this.getCurrentBlockElement();
if (!blockEl)
return;
const textBeforeCursor = this.getTextBeforeCursor(blockEl);
const contentAfterPrefix = textBeforeCursor.slice(textBeforeCursor.length - 2);
const ul = document.createElement('ul');
const li = document.createElement('li');
li.setAttribute('data-list', 'bullet');
const span = document.createElement('span');
span.textContent = contentAfterPrefix;
li.appendChild(span);
ul.appendChild(li);
blockEl.replaceWith(ul);
const sel = window.getSelection();
if (!sel)
return;
const range = document.createRange();
range.selectNodeContents(span);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
this.scheduleSave();
}
convertToChecklistItem() {
const blockEl = this.getCurrentBlockElement();
if (!blockEl)
return;
const textBeforeCursor = this.getTextBeforeCursor(blockEl);
const contentAfterPrefix = textBeforeCursor.slice(textBeforeCursor.length - 3);
const ul = document.createElement('ul');
const li = document.createElement('li');
li.setAttribute('data-list', 'checklist');
li.className = 'checklist-item';
const checkbox = document.createElement('span');
checkbox.className = 'checklist-checkbox';
checkbox.setAttribute('contenteditable', 'false');
li.appendChild(checkbox);
const textSpan = document.createElement('span');
textSpan.className = 'checklist-text';
textSpan.textContent = contentAfterPrefix;
li.appendChild(textSpan);
ul.appendChild(li);
blockEl.replaceWith(ul);
const sel = window.getSelection();
if (!sel)
return;
const range = document.createRange();
range.selectNodeContents(textSpan);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
this.scheduleSave();
}
// ─── Block Helpers ────────────────────────────────────────────────
getCurrentBlockElement() {
const sel = window.getSelection();
if (!sel || !sel.rangeCount)
return null;
let node = sel.anchorNode;
while (node && node !== this.contentEl) {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node;
const display = window.getComputedStyle(el).display;
if (el.parentElement === this.contentEl ||
['block', 'list-item'].includes(display)) {
return el;
}
}
node = node.parentNode;
}
return null;
}
getListItem(block) {
if (block.tagName === 'LI')
return block;
const li = block.closest('li');
return li || null;
}
getBlockTextContent(el) {
if (el.getAttribute('data-list') === 'checklist') {
const textSpan = el.querySelector('.checklist-text');
return textSpan ? textSpan.textContent || '' : '';
}
if (el.getAttribute('data-list') === 'bullet') {
const span = el.querySelector(':scope > span');
return span ? span.textContent || '' : el.textContent || '';
}
return el.textContent || '';
}
isChecklistEmpty(li) {
if (li.getAttribute('data-list') !== 'checklist')
return false;
const textSpan = li.querySelector('.checklist-text');
return !textSpan || textSpan.textContent?.trim() === '';
}
exitList(li) {
const ul = li.parentElement;
if (!ul)
return;
const p = document.createElement('p');
p.innerHTML = '<br>';
ul.removeChild(li);
if (ul.children.length === 0) {
ul.replaceWith(p);
}
else {
ul.after(p);
}
const sel = window.getSelection();
if (!sel)
return;
const range = document.createRange();
range.selectNodeContents(p);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
this.scheduleSave();
}
cleanUpNewListItem() {
const sel = window.getSelection();
if (!sel || !sel.rangeCount)
return;
let node = sel.anchorNode;
while (node && node !== this.contentEl) {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'LI') {
const li = node;
const ul = li.parentElement;
if (!ul)
return;
const prevLi = li.previousElementSibling;
const listType = prevLi?.getAttribute('data-list') || 'bullet';
if (listType === 'checklist' && !li.getAttribute('data-list')) {
li.setAttribute('data-list', 'checklist');
li.className = 'checklist-item';
const checkbox = document.createElement('span');
checkbox.className = 'checklist-checkbox';
checkbox.setAttribute('contenteditable', 'false');
const textSpan = document.createElement('span');
textSpan.className = 'checklist-text';
while (li.firstChild) {
textSpan.appendChild(li.firstChild);
}
li.insertBefore(checkbox, li.firstChild);
li.appendChild(textSpan);
this.scheduleSave();
}
else if (listType === 'bullet' && !li.getAttribute('data-list')) {
li.setAttribute('data-list', 'bullet');
const span = document.createElement('span');
while (li.firstChild) {
span.appendChild(li.firstChild);
}
li.appendChild(span);
this.scheduleSave();
}
return;
}
node = node.parentNode;
}
}
toggleChecklistCheckbox(checkbox) {
const isChecked = checkbox.classList.toggle('checked');
const li = checkbox.parentElement;
if (li) {
li.setAttribute('data-checked', String(isChecked));
li.classList.toggle('checked', isChecked);
}
this.scheduleSave();
}
// ─── Formatting Commands ──────────────────────────────────────────
execFormat(command) {
document.execCommand(command, false, undefined);
this.contentEl.focus();
this.updateToolbarState();
this.scheduleSave();
}
applyHeading(level) {
const block = this.getCurrentBlockElement();
if (!block)
return;
const tag = `h${level}`;
if (block.tagName.toLowerCase() === tag) {
this.unwrapToParagraph(block);
return;
}
const newBlock = document.createElement(tag);
while (block.firstChild) {
newBlock.appendChild(block.firstChild);
}
block.replaceWith(newBlock);
this.contentEl.focus();
this.updateToolbarState();
this.scheduleSave();
}
unwrapToParagraph(block) {
const p = document.createElement('p');
while (block.firstChild) {
p.appendChild(block.firstChild);
}
block.replaceWith(p);
}
applyInlineCode() {
const sel = window.getSelection();
if (!sel || !sel.rangeCount || sel.isCollapsed)
return;
const range = sel.getRangeAt(0);
const codeEl = document.createElement('code');
codeEl.textContent = range.toString();
range.deleteContents();
range.insertNode(codeEl);
range.setStartAfter(codeEl);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
this.updateToolbarState();
this.scheduleSave();
}
applyBlockquote() {
const block = this.getCurrentBlockElement();
if (!block)
return;
if (block.tagName.toLowerCase() === 'blockquote') {
this.unwrapToParagraph(block);
return;
}
const bq = document.createElement('blockquote');
while (block.firstChild) {
bq.appendChild(block.firstChild);
}
block.replaceWith(bq);
this.contentEl.focus();
this.updateToolbarState();
this.scheduleSave();
}
applyHorizontalRule() {
const block = this.getCurrentBlockElement();
if (!block)
return;
const hr = document.createElement('hr');
const p = document.createElement('p');
p.innerHTML = '<br>';
block.replaceWith(hr, p);
const sel = window.getSelection();
if (!sel)
return;
const range = document.createRange();
range.selectNodeContents(p);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
this.contentEl.focus();
this.scheduleSave();
}
// ─── Toolbar State ────────────────────────────────────────────────
updateToolbarState() {
const toolbar = document.getElementById('toolbar');
if (!toolbar)
return;
const commands = {
bold: 'b', italic: 'i', underline: 'u', strikethrough: 's',
};
for (const [cmd, cls] of Object.entries(commands)) {
const btn = toolbar.querySelector(`[data-cmd="${cls}"]`);
if (btn) {
try {
const isActive = document.queryCommandState(cmd);
btn.classList.toggle('active', isActive);
}
catch { /* ignore */ }
}
}
const block = this.getCurrentBlockElement();
if (block) {
const tag = block.tagName.toLowerCase();
toolbar.querySelectorAll('[data-heading]').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-heading') === tag);
});
const bqBtn = toolbar.querySelector('[data-cmd="blockquote"]');
if (bqBtn)
bqBtn.classList.toggle('active', tag === 'blockquote');
}
// Check for inline code at cursor
const sel = window.getSelection();
if (sel && !sel.isCollapsed) {
let node = sel.anchorNode;
while (node) {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'CODE') {
const codeBtn = toolbar.querySelector('[data-cmd="code"]');
if (codeBtn)
codeBtn.classList.add('active');
break;
}
node = node.parentNode;
}
}
}
// ─── Save / Load ──────────────────────────────────────────────────
scheduleSave() {
if (!this.currentNoteId)
return;
clearTimeout(this.saveTimer);
this.saveTimer = setTimeout(() => this.doSave(), types_1.CONSTANTS.autosaveDebounceMs);
}
doSave() {
if (!this.currentNoteId)
return;
const html = (0, state_1.sanitizeHTML)(this.contentEl.innerHTML);
(0, state_1.updateNote)(this.currentNoteId, { content: html });
this.callbacks.onSave();
this.showSavedIndicator();
}
forceSave() {
clearTimeout(this.saveTimer);
if (this.currentNoteId) {
const html = (0, state_1.sanitizeHTML)(this.contentEl.innerHTML);
(0, state_1.updateNote)(this.currentNoteId, { content: html });
}
}
showSavedIndicator() {
this.savedIndicator.textContent = '✓ Saved';
this.savedIndicator.classList.add('visible');
setTimeout(() => this.savedIndicator.classList.remove('visible'), 2000);
}
loadNote(note) {
this.currentNoteId = note.id;
this.titleInput.value = note.title || '';
this.contentEl.innerHTML = note.content || '<p><br></p>';
this.restoreChecklistStates();
if (!note.content || note.content === '<p><br></p>') {
setTimeout(() => this.contentEl.focus(), 50);
}
this.updateToolbarState();
}
restoreChecklistStates() {
const items = this.contentEl.querySelectorAll('li[data-list="checklist"]');
for (const li of Array.from(items)) {
const checkedAttr = li.getAttribute('data-checked');
if (checkedAttr === 'true') {
const checkbox = li.querySelector('.checklist-checkbox');
if (checkbox)
checkbox.classList.add('checked');
li.classList.add('checked');
}
}
}
clear() {
this.currentNoteId = null;
this.titleInput.value = '';
this.contentEl.innerHTML = '';
}
}
exports.Editor = Editor;
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppUI = void 0;
// ─── UI Module — Sidebar, Note List, Toolbar ──────────────────────────
const types_1 = require("./types");
const State = __importStar(require("./state"));
class AppUI {
constructor() {
this.sidebarPane = document.getElementById('sidebar-pane');
this.noteListPane = document.getElementById('note-list-pane');
this.editorPane = document.getElementById('editor-pane');
this.toolbarEl = document.getElementById('toolbar');
this.buildToolbar();
this.setupResizeHandles();
}
// ─── Toolbar ──────────────────────────────────────────────────────
buildToolbar() {
const buttons = [
{ cmd: 'b', icon: '<b>B</b>', title: 'Bold (Ctrl+B)' },
{ cmd: 'i', icon: '<i>I</i>', title: 'Italic (Ctrl+I)' },
{ cmd: 'u', icon: '<u>U</u>', title: 'Underline (Ctrl+U)' },
{ cmd: 's', icon: '<s>S</s>', title: 'Strikethrough' },
{ cmd: 'code', icon: '</>', title: 'Inline Code' },
];
let html = '';
for (const btn of buttons) {
html += `<button class="toolbar-btn" data-cmd="${btn.cmd}" title="${btn.title}">${btn.icon}</button>`;
}
html += '<div class="toolbar-separator"></div>';
// Headings
for (let i = 1; i <= 3; i++) {
html += `<button class="toolbar-btn" data-heading="h${i}" title="Heading ${i}">H${i}</button>`;
}
html += '<div class="toolbar-separator"></div>';
// Block elements
html += `<button class="toolbar-btn" data-cmd="blockquote" title="Blockquote (>)">❝</button>`;
html += `<button class="toolbar-btn" data-cmd="hr" title="Horizontal Rule">—</button>`;
this.toolbarEl.innerHTML = html;
// Wire up toolbar buttons
this.toolbarEl.addEventListener('click', (e) => {
const btn = e.target.closest('.toolbar-btn');
if (!btn)
return;
const cmd = btn.getAttribute('data-cmd');
const heading = btn.getAttribute('data-heading');
// Dispatch custom event for the editor to handle
this.editorPane.dispatchEvent(new CustomEvent('toolbar-action', {
detail: { cmd, heading },
bubbles: true,
}));
});
}
// ─── Sidebar Rendering ────────────────────────────────────────────
renderSidebar() {
const s = State.getState();
const notebooks = s.notebooks;
const selectedNb = s.selectedNotebookId;
let html = `
<div class="sidebar-header">
<span class="sidebar-title">Notes</span>
<div class="sidebar-actions">
<button class="sidebar-btn" id="theme-toggle" title="Toggle theme">${s.darkTheme ? '☀️' : '🌙'}</button>
</div>
</div>
<div class="sidebar-search">
<input type="text" id="search-input" placeholder="Search notes…" value="${escapeAttr(s.searchQuery)}" />
</div>
<nav class="sidebar-nav">
`;
// All Notes
const allCount = State.getNoteCountForNotebook('all');
html += this.renderNavItem('📋', 'All Notes', 'all', selectedNb === 'all', allCount);
// Notebooks
for (const nb of notebooks) {
const count = State.getNoteCountForNotebook(nb.id);
const isActive = selectedNb === nb.id;
html += `
<div class="notebook-item" data-notebook-id="${nb.id}">
<div class="notebook-header ${isActive ? 'active' : ''} ${nb.collapsed ? 'collapsed' : ''}"
data-action="select-notebook" data-id="${nb.id}">
<span class="notebook-chevron">${nb.collapsed ? '▶' : '▼'}</span>
<span class="nav-icon">📁</span>
<span class="nav-label">${escapeHtml(nb.name)}</span>
<span class="nav-count">${count}</span>
</div>
<div class="notebook-notes ${nb.collapsed ? 'collapsed' : ''}">
`;
// Notes in this notebook (only show when not viewing "all")
if (!nb.collapsed && selectedNb !== 'trash') {
const notes = State.getFilteredNotes().filter(n => n.notebookId === nb.id);
for (const note of notes.slice(0, 10)) { // limit preview
html += this.renderNoteItem(note, false);
}
}
html += `</div></div>`;
}
// Trash
const trashCount = State.getNoteCountForNotebook('trash');
html += this.renderNavItem('🗑️', 'Trash', 'trash', selectedNb === 'trash', trashCount);
html += '</nav>';
// Footer buttons
html += `
<div class="sidebar-footer">
<button class="add-note-btn" id="new-note-btn">+ New Note</button>
<button class="add-notebook-btn" id="new-notebook-btn">+ New Notebook</button>
</div>
`;
this.sidebarPane.innerHTML = html;
this.bindSidebarEvents();
}
renderNavItem(icon, label, id, active, count) {
return `<div class="nav-item ${active ? 'active' : ''}" data-action="select-notebook" data-id="${id}">
<span class="nav-icon">${icon}</span>
<span class="nav-label">${escapeHtml(label)}</span>
<span class="nav-count">${count}</span>
</div>`;
}
renderNoteItem(note, showActions) {
const snippet = State.extractSnippet(note.content, types_1.CONSTANTS.snippetLength);
const timeStr = formatTime(note.updatedAt);
const isActive = State.getState().selectedNoteId === note.id;
let tagsHtml = '';
if (note.tags.length > 0) {
tagsHtml = '<div class="note-item-tags">';
for (const tag of note.tags.slice(0, 3)) {
tagsHtml += `<span class="note-tag" data-action="filter-tag" data-tag="${escapeAttr(tag)}">#${escapeHtml(tag)}</span>`;
}
if (note.tags.length > 3)
tagsHtml += `<span class="note-tag">+${note.tags.length - 3}</span>`;
tagsHtml += '</div>';
}
return `
<div class="note-item ${isActive ? 'active' : ''}" data-action="select-note" data-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">
${note.pinned ? '<span class="pin-indicator">📌</span>' : ''}
<span>${timeStr}</span>
</div>
${tagsHtml}
</div>`;
}
bindSidebarEvents() {
// Search input
const searchInput = document.getElementById('search-input');
if (searchInput) {
let timer;
searchInput.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => State.setSearchQuery(searchInput.value), 200);
});
}
// Theme toggle
const themeBtn = document.getElementById('theme-toggle');
if (themeBtn) {
themeBtn.addEventListener('click', () => State.toggleDarkTheme());
}
// New note button
const newNoteBtn = document.getElementById('new-note-btn');
if (newNoteBtn) {
newNoteBtn.addEventListener('click', () => {
const s = State.getState();
let targetNb;
if (s.selectedNotebookId === 'trash') {
// Create in first available notebook
targetNb = s.notebooks.length > 0 ? s.notebooks[0].id : undefined;
}
else if (s.selectedNotebookId !== 'all') {
targetNb = s.selectedNotebookId;
}
State.createNote(targetNb);
});
}
// New notebook button
const newNtBtn = document.getElementById('new-notebook-btn');
if (newNtBtn) {
newNtBtn.addEventListener('click', () => {
const name = prompt('Notebook name:');
if (name?.trim())
State.createNotebook(name.trim());
});
}
// Delegate clicks in sidebar nav
this.sidebarPane.addEventListener('click', (e) => {
const target = e.target;
// Tag filter click
const tagEl = target.closest('[data-action="filter-tag"]');
if (tagEl) {
const tag = tagEl.getAttribute('data-tag');
State.setTagFilter(tag);
return;
}
// Notebook / view selection
const navItem = target.closest('[data-action="select-notebook"]');
if (navItem) {
const id = navItem.getAttribute('data-id');
State.selectNotebook(id);
return;
}
// Note selection from sidebar preview
const noteItem = target.closest('[data-action="select-note"]');
if (noteItem) {
const id = noteItem.getAttribute('data-id');
State.selectNote(id);
return;
}
});
// Notebook header click — toggle collapse or select
this.sidebarPane.addEventListener('click', (e) => {
const nbHeader = e.target.closest('.notebook-header');
if (!nbHeader)
return;
e.stopPropagation();
const id = nbHeader.getAttribute('data-id');
// Check if clicking the chevron area — toggle collapse
const rect = nbHeader.getBoundingClientRect();
const x = e.clientX - rect.left;
if (x < 24) {
State.toggleNotebookCollapse(id);
}
else {
State.selectNotebook(id);
}
});
// Right-click on notebooks for context menu
this.sidebarPane.addEventListener('contextmenu', (e) => {
const nbHeader = e.target.closest('.notebook-header');
if (!nbHeader)
return;
e.preventDefault();
const id = nbHeader.getAttribute('data-id');
showNotebookContextMenu(e.clientX, e.clientY, id);
});
}
// ─── Note List Rendering ──────────────────────────────────────────
renderNoteList() {
const s = State.getState();
const notes = State.getFilteredNotes();
const viewTitle = getViewTitle(s.selectedNotebookId, s.notebooks);
let html = `
<div class="notelist-header">
<div class="notelist-title-row">
<span class="notelist-title">${escapeHtml(viewTitle)}</span>
<div class="notelist-actions">
<button class="sort-btn" id="sort-toggle" title="Sort order">${getSortLabel(s.sortMode)}</button>
</div>
</div>
</div>
`;
// Tag filter bar if active
if (s.tagFilter) {
html += `<div class="tag-filter-bar">
<span class="tag-chip active" data-action="clear-tag-filter">#${escapeHtml(s.tagFilter)}</span>
</div>`;
}
html += '<div class="note-list-container">';
if (notes.length === 0) {
const isTrash = s.selectedNotebookId === 'trash';
html += `
<div class="empty-state">
<div class="empty-state-icon">${isTrash ? '🗑️' : '📝'}</div>
<div class="empty-state-text">${isTrash ? 'Trash is empty' : s.searchQuery ? 'No notes match your search' : 'No notes yet. Create one!'}</div>
</div>`;
}
else {
for (const note of notes) {
html += this.renderNoteItem(note, true);
}
}
html += '</div>';
this.noteListPane.innerHTML = html;
this.bindNoteListEvents();
}
bindNoteListEvents() {
// Sort toggle
const sortBtn = document.getElementById('sort-toggle');
if (sortBtn) {
sortBtn.addEventListener('click', () => {
const s = State.getState();
const modes = ['modified', 'created', 'alphabetical'];
const idx = modes.indexOf(s.sortMode);
State.setSortMode(modes[(idx + 1) % modes.length]);
});
}
// Tag filter clear
this.noteListPane.addEventListener('click', (e) => {
const tagChip = e.target.closest('[data-action="clear-tag-filter"]');
if (tagChip) {
State.setTagFilter(null);
return;
}
// Tag filter from note list
const tagEl = e.target.closest('[data-action="filter-tag"]');
if (tagEl) {
const tag = tagEl.getAttribute('data-tag');
State.setTagFilter(tag);
return;
}
// Note selection
const noteItem = e.target.closest('[data-action="select-note"]');
if (noteItem) {
const id = noteItem.getAttribute('data-id');
State.selectNote(id);
return;
}
});
// Right-click on notes for context menu
this.noteListPane.addEventListener('contextmenu', (e) => {
const noteItem = e.target.closest('[data-action="select-note"]');
if (!noteItem)
return;
e.preventDefault();
const id = noteItem.getAttribute('data-id');
showNoteContextMenu(e.clientX, e.clientY, id);
});
// Drag start for notes (move between notebooks)
this.noteListPane.addEventListener('dragstart', (e) => {
const noteItem = e.target.closest('[data-action="select-note"]');
if (!noteItem)
return;
e.dataTransfer.setData('text/plain', noteItem.getAttribute('data-id'));
e.dataTransfer.effectAllowed = 'move';
});
// Make note items draggable
this.noteListPane.querySelectorAll('.note-item').forEach(el => {
el.setAttribute('draggable', 'true');
});
}
// ─── Editor Empty State ───────────────────────────────────────────
showEditorEmptyState() {
const headerEl = document.getElementById('editor-header');
const contentEl = document.getElementById('editor-content');
headerEl.innerHTML = '';
contentEl.innerHTML = `
<div class="editor-empty-state">
<div class="editor-empty-icon">📝</div>
<div class="editor-empty-text">Select or create a note to get started</div>
</div>`;
}
// ─── Resize Handles ───────────────────────────────────────────────
setupResizeHandles() {
this.setupResizeHandle('sidebar-resize-handle', 'sidebar-pane', (width) => {
State.setPaneLayout({ sidebarWidth: width });
}, types_1.CONSTANTS.sidebarMinWidth, types_1.CONSTANTS.sidebarMaxWidth);
this.setupResizeHandle('notelist-resize-handle', 'note-list-pane', (width) => {
State.setPaneLayout({ noteListWidth: width });
}, types_1.CONSTANTS.noteListMinWidth, types_1.CONSTANTS.noteListMaxWidth);
}
setupResizeHandle(handleId, paneId, onResize, minW, maxW) {
const handle = document.getElementById(handleId);
const pane = document.getElementById(paneId);
let startX = 0;
let startWidth = 0;
const onMouseDown = (e) => {
e.preventDefault();
startX = e.clientX;
startWidth = pane.offsetWidth;
handle.classList.add('active');
const onMouseMove = (e) => {
const diff = e.clientX - startX;
let newWidth = startWidth + diff;
newWidth = Math.max(minW, Math.min(maxW, newWidth));
pane.style.width = newWidth + 'px';
onResize(newWidth);
};
const onMouseUp = () => {
handle.classList.remove('active');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
handle.addEventListener('mousedown', onMouseDown);
}
// ─── Apply Layout State ───────────────────────────────────────────
applyLayout() {
const s = State.getState();
const pl = s.paneLayout;
this.sidebarPane.style.width = pl.sidebarWidth + 'px';
this.noteListPane.style.width = pl.noteListWidth + 'px';
if (pl.sidebarCollapsed) {
this.sidebarPane.classList.add('collapsed');
}
else {
this.sidebarPane.classList.remove('collapsed');
}
// Theme
document.documentElement.setAttribute('data-theme', s.darkTheme ? 'dark' : 'light');
}
}
exports.AppUI = AppUI;
// ─── Context Menus ────────────────────────────────────────────────────
function showNoteContextMenu(x, y, noteId) {
removeContextMenus();
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.style.left = x + 'px';
menu.style.top = y + 'px';
const s = State.getState();
const isTrash = s.selectedNotebookId === 'trash';
let html = '';
if (isTrash) {
html += `<div class="context-menu-item" data-action="restore-note" data-id="${noteId}">↩️ Restore</div>`;
html += '<div class="context-menu-divider"></div>';
html += `<div class="context-menu-item danger" data-action="permanent-delete" data-id="${noteId}">🗑️ Delete Permanently</div>`;
}
else {
html += `<div class="context-menu-item" data-action="toggle-pin" data-id="${noteId}">${s.notes.find(n => n.id === noteId)?.pinned ? '📌 Unpin' : '📌 Pin to Top'}</div>`;
html += `<div class="context-menu-item" data-action="duplicate-note" data-id="${noteId}">📋 Duplicate</div>`;
html += '<div class="context-menu-divider"></div>';
// Move to notebook options
for (const nb of s.notebooks) {
if (nb.id !== 'trash') {
html += `<div class="context-menu-item" data-action="move-to-notebook" data-id="${noteId}" data-target="${nb.id}">📁 → ${escapeHtml(nb.name)}</div>`;
}
}
html += '<div class="context-menu-divider"></div>';
html += `<div class="context-menu-item danger" data-action="delete-note" data-id="${noteId}">🗑️ Move to Trash</div>`;
}
menu.innerHTML = html;
document.body.appendChild(menu);
// Adjust position if off-screen
const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth)
menu.style.left = (x - rect.width) + 'px';
if (rect.bottom > window.innerHeight)
menu.style.top = (y - rect.height) + 'px';
menu.addEventListener('click', (e) => {
const item = e.target.closest('.context-menu-item');
if (!item)
return;
const action = item.getAttribute('data-action');
const id = item.getAttribute('data-id');
switch (action) {
case 'toggle-pin':
State.togglePin(id);
break;
case 'duplicate-note':
State.duplicateNote(id);
break;
case 'delete-note':
State.moveToTrash(id);
break;
case 'restore-note':
State.restoreFromTrash(id);
break;
case 'permanent-delete':
State.permanentlyDelete(id);
break;
case 'move-to-notebook':
const target = item.getAttribute('data-target');
State.moveNoteToNotebook(id, target);
break;
}
removeContextMenus();
});
// Close on outside click
setTimeout(() => {
document.addEventListener('click', () => removeContextMenus(), { once: true });
}, 0);
}
function showNotebookContextMenu(x, y, notebookId) {
removeContextMenus();
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.style.left = x + 'px';
menu.style.top = y + 'px';
menu.innerHTML = `
<div class="context-menu-item" data-action="rename-notebook" data-id="${notebookId}">✏️ Rename</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item danger" data-action="delete-notebook" data-id="${notebookId}">🗑️ Delete Notebook</div>
`;
document.body.appendChild(menu);
menu.addEventListener('click', (e) => {
const item = e.target.closest('.context-menu-item');
if (!item)
return;
const action = item.getAttribute('data-action');
const id = item.getAttribute('data-id');
switch (action) {
case 'rename-notebook': {
const name = prompt('Rename notebook:');
if (name?.trim())
State.renameNotebook(id, name.trim());
break;
}
case 'delete-notebook': {
if (confirm('Delete this notebook? Notes will be moved to Trash.')) {
State.deleteNotebook(id);
}
break;
}
}
removeContextMenus();
});
setTimeout(() => {
document.addEventListener('click', () => removeContextMenus(), { once: true });
}, 0);
}
function removeContextMenus() {
document.querySelectorAll('.context-menu, .move-menu').forEach(el => el.remove());
}
// ─── Utility Functions ────────────────────────────────────────────────
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function escapeAttr(str) {
return str.replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
}
function formatTime(ts) {
const now = Date.now();
const diff = now - ts;
if (diff < 60000)
return 'Just now';
if (diff < 3600000)
return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}h ago`;
const d = new Date(ts);
const month = d.toLocaleString('default', { month: 'short' });
return `${month} ${d.getDate()}`;
}
function getViewTitle(viewId, notebooks) {
if (viewId === 'all')
return 'All Notes';
if (viewId === 'trash')
return 'Trash';
const nb = notebooks.find(n => n.id === viewId);
return nb?.name || 'Notes';
}
function getSortLabel(mode) {
switch (mode) {
case 'modified': return '↕ Modified';
case 'created': return '↕ Created';
case 'alphabetical': return 'A→Z Alpha';
default: return '↕ Modified';
}
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"f54883c3-c792-4f89-b6b1-6217c612cd7f","tokensIn":4641537,"tokensOut":53493,"tokensTotal":4695030,"turns":85,"toolCalls":84,"failedToolCalls":6,"timestamp":"2026-07-31T00:46:46.745Z"} -->