Notes
Qwen 3.6 27b · 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 108,555 bytes · SHA-256 1acdcd57512e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notes — Your Personal Workspace</title>
<style>
/* ── css: reset ── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
overflow: hidden;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input, button, textarea {
font-family: inherit;
font-size: inherit;
}
button {
cursor: pointer;
border: none;
background: none;
outline: none;
}
input {
outline: none;
border: none;
background: none;
}
::selection {
background: rgba(108, 92, 231, 0.25);
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.12);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.2);
}
/* ── css: layout ── */
#app {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
}
/* Sidebar */
#sidebar {
width: 240px;
min-width: 160px;
max-width: 320px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
background: var(--bg-secondary);
transition: width 0.25s ease, min-width 0.25s ease;
overflow: hidden;
}
#sidebar.collapsed {
width: 0 !important;
min-width: 0 !important;
border-right: none;
}
#sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 16px 8px;
flex-shrink: 0;
}
#app-logo {
font-size: 20px;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.5px;
}
#sidebar-actions {
padding: 4px 16px;
flex-shrink: 0;
}
#sidebar-list {
flex: 1;
overflow-y: auto;
padding: 4px 8px;
}
#sidebar-footer {
padding: 8px;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
/* Resize handles */
.resize-handle {
width: 4px;
cursor: col-resize;
background: transparent;
transition: background 0.15s;
flex-shrink: 0;
position: relative;
}
.resize-handle:hover,
.resize-handle.active {
background: var(--accent);
}
/* Note list pane */
#note-list-pane {
width: 300px;
min-width: 200px;
max-width: 450px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
background: var(--bg);
overflow: hidden;
}
#note-list-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#note-list-title-area {
display: flex;
align-items: baseline;
gap: 8px;
}
#note-list-title {
font-size: 14px;
font-weight: 600;
color: var(--text);
}
#note-list-count {
font-size: 12px;
color: var(--text-tertiary);
}
#note-list-actions {
display: flex;
gap: 2px;
}
#search-area {
padding: 8px 12px;
flex-shrink: 0;
}
#search-input {
width: 100%;
padding: 8px 12px;
border-radius: 8px;
background: var(--search-bg);
color: var(--text);
font-size: 13px;
border: 1px solid transparent;
transition: border-color 0.15s, box-shadow 0.15s;
}
#search-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-bg);
}
#search-input::placeholder {
color: var(--text-tertiary);
}
#note-list-container {
flex: 1;
overflow-y: auto;
padding: 4px 8px;
}
/* Editor pane */
#editor-pane {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg-editor);
min-width: 0;
overflow: hidden;
}
#editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#editor-title-area {
flex: 1;
min-width: 0;
}
#editor-title {
width: 100%;
font-size: 18px;
font-weight: 600;
color: var(--text);
background: transparent;
padding: 4px 0;
}
#editor-title:focus {
outline: none;
}
#editor-title::placeholder {
color: var(--text-tertiary);
}
#editor-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
#saved-indicator {
font-size: 11px;
color: var(--text-tertiary);
margin-right: 8px;
opacity: 0;
transition: opacity 0.3s;
}
#saved-indicator.show {
opacity: 1;
color: var(--success);
}
#editor-toolbar {
display: flex;
align-items: center;
padding: 6px 24px;
border-bottom: 1px solid var(--border);
gap: 2px;
flex-shrink: 0;
background: var(--toolbar-bg);
overflow-x: auto;
}
.toolbar-btn {
padding: 6px 8px;
border-radius: 4px;
font-size: 13px;
color: var(--text-secondary);
transition: background 0.1s, color 0.1s;
min-width: 30px;
text-align: center;
}
.toolbar-btn:hover {
background: var(--toolbar-hover);
color: var(--text);
}
.toolbar-btn.active {
background: var(--toolbar-active);
color: var(--accent);
}
.toolbar-sep {
width: 1px;
height: 20px;
background: var(--toolbar-border);
margin: 0 4px;
flex-shrink: 0;
}
#editor-body {
flex: 1;
overflow-y: auto;
padding: 24px 48px;
}
#editor-content {
max-width: 720px;
margin: 0 auto;
font-family: 'Georgia', 'Noto Serif', 'Source Serif Pro', serif;
font-size: 16px;
line-height: 1.7;
color: var(--text);
outline: none;
min-height: 100%;
}
#editor-content:empty::before {
content: 'Start writing…';
color: var(--text-tertiary);
pointer-events: none;
}
#editor-tags-area {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 48px;
border-top: 1px solid var(--border);
flex-shrink: 0;
flex-wrap: wrap;
}
#tags-container {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
background: var(--tag-bg);
color: var(--tag-text);
cursor: pointer;
transition: background 0.1s;
}
.tag-chip:hover {
background: var(--bg-hover);
}
#tag-input {
font-size: 12px;
color: var(--text);
min-width: 80px;
padding: 4px 8px;
}
#tag-input::placeholder {
color: var(--text-tertiary);
}
/* Empty state */
#editor-empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.empty-state-content {
text-align: center;
color: var(--text-tertiary);
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state-text {
font-size: 16px;
}
/* Icon buttons */
.icon-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
font-size: 16px;
color: var(--text-secondary);
transition: background 0.1s, color 0.1s;
}
.icon-btn:hover {
background: var(--bg-hover);
color: var(--text);
}
.icon-btn.active {
color: var(--accent);
background: var(--accent-bg);
}
/* Dropdowns */
.dropdown {
position: fixed;
background: var(--bg-editor);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 24px var(--shadow-medium);
padding: 4px;
z-index: 1000;
min-width: 160px;
}
.dropdown-item {
padding: 8px 12px;
font-size: 13px;
color: var(--text);
border-radius: 4px;
cursor: pointer;
transition: background 0.1s;
}
.dropdown-item:hover {
background: var(--bg-hover);
}
.dropdown-item.danger {
color: var(--danger);
}
.dropdown-item.danger:hover {
background: var(--danger-bg);
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
background: var(--bg-editor);
border-radius: 12px;
padding: 24px;
min-width: 320px;
box-shadow: 0 16px 48px var(--shadow-medium);
}
.modal-content h3 {
margin-bottom: 16px;
font-size: 16px;
color: var(--text);
}
.modal-content input {
width: 100%;
padding: 10px 14px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
font-size: 14px;
margin-bottom: 16px;
}
.modal-content input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-bg);
}
.modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.btn-primary {
padding: 8px 20px;
border-radius: 6px;
background: var(--accent);
color: white;
font-size: 13px;
font-weight: 500;
transition: opacity 0.1s;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
padding: 8px 20px;
border-radius: 6px;
background: var(--bg-tertiary);
color: var(--text);
font-size: 13px;
font-weight: 500;
transition: background 0.1s;
}
.btn-secondary:hover {
background: var(--bg-hover);
}
/* ── css: sidebar ── */
/* Sidebar items */
.sidebar-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.1s;
margin-bottom: 2px;
position: relative;
user-select: none;
}
.sidebar-item:hover {
background: var(--bg-hover);
}
.sidebar-item.active {
background: var(--accent-bg);
color: var(--accent);
}
.sidebar-item-icon {
font-size: 16px;
flex-shrink: 0;
width: 20px;
text-align: center;
}
.sidebar-item-name {
flex: 1;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-item-count {
font-size: 11px;
color: var(--text-tertiary);
flex-shrink: 0;
}
.sidebar-item-actions {
display: none;
gap: 2px;
flex-shrink: 0;
}
.sidebar-item:hover .sidebar-item-actions {
display: flex;
}
.sidebar-item-action {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 12px;
color: var(--text-tertiary);
transition: background 0.1s, color 0.1s;
}
.sidebar-item-action:hover {
background: var(--bg-tertiary);
color: var(--text);
}
.sidebar-item-action.delete:hover {
color: var(--danger);
background: var(--danger-bg);
}
/* Notebook color dot */
.notebook-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
/* Collapsible notebook header */
.notebook-header {
cursor: pointer;
}
.notebook-header .sidebar-item-name {
font-weight: 600;
}
.notebook-notes {
padding-left: 12px;
overflow: hidden;
transition: max-height 0.2s ease;
}
.notebook-notes.collapsed {
max-height: 0 !important;
}
/* Note items in sidebar */
.sidebar-note-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.1s;
margin-bottom: 1px;
font-size: 12px;
color: var(--text-secondary);
}
.sidebar-note-item:hover {
background: var(--bg-hover);
}
.sidebar-note-item.active {
background: var(--accent-bg);
color: var(--accent);
}
/* Note list items */
.note-item {
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: background 0.1s, box-shadow 0.1s;
margin-bottom: 4px;
border: 1px solid transparent;
position: relative;
}
.note-item:hover {
background: var(--bg-secondary);
}
.note-item.active {
background: var(--accent-bg);
border-color: var(--accent);
box-shadow: 0 2px 8px var(--accent-bg);
}
.note-item-title {
font-size: 14px;
font-weight: 500;
color: var(--text);
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.note-item-snippet {
font-size: 12px;
color: var(--text-tertiary);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 6px;
}
.note-item-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--text-tertiary);
}
.note-item-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.note-item-tag {
padding: 1px 6px;
border-radius: 4px;
background: var(--tag-bg);
color: var(--tag-text);
font-size: 10px;
cursor: pointer;
transition: background 0.1s;
}
.note-item-tag:hover {
background: var(--bg-hover);
}
.pin-indicator {
font-size: 10px;
color: var(--accent);
}
/* Search highlight */
.search-highlight {
background: var(--highlight-bg);
border-radius: 2px;
padding: 0 1px;
}
/* Editor content styles */
#editor-content h1 {
font-size: 28px;
font-weight: 700;
margin: 24px 0 12px;
line-height: 1.3;
}
#editor-content h2 {
font-size: 22px;
font-weight: 600;
margin: 20px 0 10px;
line-height: 1.35;
}
#editor-content h3 {
font-size: 18px;
font-weight: 600;
margin: 16px 0 8px;
line-height: 1.4;
}
#editor-content p {
margin: 8px 0;
}
#editor-content blockquote {
border-left: 3px solid var(--accent);
padding-left: 16px;
margin: 12px 0;
color: var(--text-secondary);
font-style: italic;
}
#editor-content code {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
#editor-content hr {
border: none;
border-top: 1px solid var(--border);
margin: 20px 0;
}
#editor-content ul, #editor-content ol {
padding-left: 24px;
margin: 8px 0;
}
#editor-content li {
margin: 4px 0;
}
/* Checklist items */
.checklist-item {
list-style: none;
margin-left: -20px;
padding-left: 28px;
position: relative;
}
.checklist-checkbox {
position: absolute;
left: 0;
top: 3px;
width: 16px;
height: 16px;
border: 2px solid var(--checkbox-unchecked);
border-radius: 3px;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.checklist-checkbox.checked {
background: var(--checkbox-checked);
border-color: var(--checkbox-checked);
}
.checklist-checkbox.checked::after {
content: '✓';
color: white;
font-size: 10px;
font-weight: bold;
}
.checklist-item.checked-text {
text-decoration: line-through;
color: var(--text-tertiary);
}
/* Sidebar collapse button */
#btn-collapse-sidebar {
width: 100%;
height: 32px;
border-radius: 6px;
font-size: 12px;
color: var(--text-tertiary);
transition: background 0.1s, color 0.1s;
}
#btn-collapse-sidebar:hover {
background: var(--bg-hover);
color: var(--text);
}
/* Sidebar expand button (shown when collapsed) */
#sidebar-expand {
position: fixed;
top: 12px;
left: 8px;
width: 32px;
height: 32px;
display: none;
align-items: center;
justify-content: center;
border-radius: 6px;
background: var(--bg-secondary);
border: 1px solid var(--border);
color: var(--text-secondary);
cursor: pointer;
z-index: 100;
font-size: 16px;
transition: background 0.1s;
}
#sidebar-expand:hover {
background: var(--bg-hover);
}
/* Drop target highlight */
.drop-target {
outline: 2px solid var(--accent);
outline-offset: -2px;
border-radius: 8px;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.dropdown {
animation: fadeIn 0.12s ease;
}
/* ── css: theme ── */
/* Light theme (default) */
:root {
--bg: #FAFAF9;
--bg-secondary: #F3F2F0;
--bg-tertiary: #EBEAE7;
--bg-hover: #E8E7E4;
--bg-active: #DFDED9;
--bg-editor: #FFFFFF;
--text: #2C2C2C;
--text-secondary: #6B6B6B;
--text-tertiary: #9A9A9A;
--border: #E5E4E1;
--border-light: #EDECE9;
--accent: #6C5CE7;
--accent-light: #A29BFE;
--accent-bg: rgba(108, 92, 231, 0.08);
--danger: #E74C3C;
--danger-bg: rgba(231, 76, 60, 0.08);
--success: #27AE60;
--success-bg: rgba(39, 174, 96, 0.08);
--shadow: rgba(0, 0, 0, 0.06);
--shadow-medium: rgba(0, 0, 0, 0.1);
--tag-bg: #E8E7E4;
--tag-text: #5A5A5A;
--checkbox-unchecked: #C4C4C4;
--checkbox-checked: #6C5CE7;
--toolbar-bg: #FFFFFF;
--toolbar-border: #E5E4E1;
--toolbar-hover: #F3F2F0;
--toolbar-active: #DFDED9;
--search-bg: #F3F2F0;
--highlight-bg: rgba(255, 235, 59, 0.3);
}
/* Dark theme */
body.dark-mode {
--bg: #1A1A1A;
--bg-secondary: #232323;
--bg-tertiary: #2A2A2A;
--bg-hover: #333333;
--bg-active: #3A3A3A;
--bg-editor: #1E1E1E;
--text: #E8E8E8;
--text-secondary: #A0A0A0;
--text-tertiary: #6B6B6B;
--border: #333333;
--border-light: #2A2A2A;
--accent: #A29BFE;
--accent-light: #6C5CE7;
--accent-bg: rgba(162, 155, 254, 0.12);
--danger: #FF6B6B;
--danger-bg: rgba(255, 107, 107, 0.12);
--success: #69DB7C;
--success-bg: rgba(105, 219, 124, 0.12);
--shadow: rgba(0, 0, 0, 0.3);
--shadow-medium: rgba(0, 0, 0, 0.5);
--tag-bg: #333333;
--tag-text: #B0B0B0;
--checkbox-unchecked: #555555;
--checkbox-checked: #A29BFE;
--toolbar-bg: #232323;
--toolbar-border: #333333;
--toolbar-hover: #2A2A2A;
--toolbar-active: #3A3A3A;
--search-bg: #2A2A2A;
--highlight-bg: rgba(255, 235, 59, 0.2);
}
body.dark-mode ::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.1);
}
body.dark-mode ::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.2);
}
</style>
</head>
<body>
<div id="app">
<!-- Sidebar -->
<div id="sidebar">
<div id="sidebar-header">
<div id="app-logo">Notes</div>
<button id="btn-dark-mode" class="icon-btn" title="Toggle dark mode">◐</button>
</div>
<div id="sidebar-actions">
<button id="btn-new-notebook" class="icon-btn" title="New Notebook">+</button>
</div>
<div id="sidebar-list">
<div id="sidebar-virtual-items"></div>
<div id="sidebar-notebooks"></div>
</div>
<div id="sidebar-footer">
<button id="btn-collapse-sidebar" class="icon-btn" title="Toggle sidebar">◀</button>
</div>
</div>
<!-- Resize handle 1 -->
<div id="handle1" class="resize-handle"></div>
<!-- Note list -->
<div id="note-list-pane">
<div id="note-list-header">
<div id="note-list-title-area">
<span id="note-list-title">All Notes</span>
<span id="note-list-count"></span>
</div>
<div id="note-list-actions">
<button id="btn-sort" class="icon-btn" title="Sort order">⇅</button>
<button id="btn-new-note" class="icon-btn" title="New note">+</button>
</div>
</div>
<div id="search-area">
<input id="search-input" type="text" placeholder="Search notes…">
</div>
<div id="note-list-container"></div>
</div>
<!-- Resize handle 2 -->
<div id="handle2" class="resize-handle"></div>
<!-- Editor -->
<div id="editor-pane">
<div id="editor-header">
<div id="editor-title-area">
<input id="editor-title" type="text" placeholder="Note title">
</div>
<div id="editor-actions">
<span id="saved-indicator">Saved</span>
<button id="btn-move-note" class="icon-btn" title="Move note">↗</button>
<button id="btn-duplicate-note" class="icon-btn" title="Duplicate">⧉</button>
<button id="btn-pin-note" class="icon-btn" title="Pin">📌</button>
<button id="btn-delete-note" class="icon-btn" title="Delete">🗑</button>
</div>
</div>
<div id="editor-toolbar">
<button data-cmd="bold" class="toolbar-btn" title="Bold (Ctrl+B)"><b>B</b></button>
<button data-cmd="italic" class="toolbar-btn" title="Italic (Ctrl+I)"><i>I</i></button>
<button data-cmd="underline" class="toolbar-btn" title="Underline (Ctrl+U)"><u>U</u></button>
<button data-cmd="strikethrough" class="toolbar-btn" title="Strikethrough"><s>S</s></button>
<button data-cmd="code" class="toolbar-btn" title="Inline Code" style="font-family: monospace;"><></button>
<span class="toolbar-sep"></span>
<button data-cmd="h1" class="toolbar-btn" title="Heading 1">H1</button>
<button data-cmd="h2" class="toolbar-btn" title="Heading 2">H2</button>
<button data-cmd="h3" class="toolbar-btn" title="Heading 3">H3</button>
<span class="toolbar-sep"></span>
<button data-cmd="ul" class="toolbar-btn" title="Bullet List">•≡</button>
<button data-cmd="ol" class="toolbar-btn" title="Numbered List">1≡</button>
<button data-cmd="checklist" class="toolbar-btn" title="Checklist">☑</button>
<button data-cmd="blockquote" class="toolbar-btn" title="Quote">❝</button>
<button data-cmd="hr" class="toolbar-btn" title="Horizontal Rule">—</button>
<span class="toolbar-sep"></span>
<button data-cmd="undo" class="toolbar-btn" title="Undo (Ctrl+Z)">↶</button>
<button data-cmd="redo" class="toolbar-btn" title="Redo (Ctrl+Shift+Z)">↷</button>
</div>
<div id="editor-body">
<div id="editor-content" contenteditable="true" spellcheck="true"></div>
</div>
<div id="editor-tags-area">
<div id="tags-container"></div>
<input id="tag-input" type="text" placeholder="Add tag…">
</div>
<div id="editor-empty-state" style="display:none;">
<div class="empty-state-content">
<div class="empty-state-icon">📝</div>
<div class="empty-state-text">Select or create a note</div>
</div>
</div>
</div>
<!-- Move note dropdown -->
<div id="move-dropdown" class="dropdown" style="display:none;"></div>
<!-- Sort dropdown -->
<div id="sort-dropdown" class="dropdown" style="display:none;">
<div class="dropdown-item" data-sort="modified">Last Modified</div>
<div class="dropdown-item" data-sort="created">Date Created</div>
<div class="dropdown-item" data-sort="alphabetical">Alphabetical</div>
</div>
<!-- Context menu for trash actions -->
<div id="trash-menu" class="dropdown" style="display:none;">
<div class="dropdown-item" data-action="restore">Restore</div>
<div class="dropdown-item danger" data-action="permanent-delete">Delete Permanently</div>
</div>
<!-- New notebook dialog -->
<div id="new-notebook-dialog" class="modal-overlay" style="display:none;">
<div class="modal-content">
<h3>New Notebook</h3>
<input id="new-notebook-input" type="text" placeholder="Notebook name">
<div class="modal-actions">
<button id="btn-cancel-notebook" class="btn-secondary">Cancel</button>
<button id="btn-confirm-notebook" class="btn-primary">Create</button>
</div>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./model":"src/model.ts","./types":"src/types.ts","./renderer":"src/renderer.ts","./editor":"src/editor.ts"},"src/model.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/renderer.ts":{"./types":"src/types.ts","./model":"src/model.ts"},"src/editor.ts":{"./constants":"src/constants.ts","./model":"src/model.ts","./renderer":"src/renderer.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 });
exports.start = start;
// ── Entry point: wires model, renderer, and editor together ────────────
const model_1 = require("./model");
const types_1 = require("./types");
const renderer_1 = require("./renderer");
const editor_1 = require("./editor");
// ── Start ──────────────────────────────────────────────────────────────
function start() {
// Initialize model
const state = (0, model_1.initModel)();
// Cache DOM references
(0, renderer_1.cacheDom)();
// Subscribe to model changes
(0, model_1.subscribe)(() => {
(0, renderer_1.render)((0, model_1.getState)());
});
// Initial render
(0, renderer_1.render)(state);
// Initialize editor
(0, editor_1.initEditor)();
// Setup drag and drop
(0, renderer_1.setupDragDrop)();
// Wire up event handlers
wireEvents();
// Restore active note content
if (state.activeNoteId) {
(0, editor_1.loadNoteContent)(state.activeNoteId);
}
// Restore dark mode
if (state.darkMode) {
document.body.classList.add('dark-mode');
}
}
// ── Event wiring ───────────────────────────────────────────────────────
function wireEvents() {
const dom = getDomRefs();
// Sidebar toggle
dom.btnCollapse.addEventListener('click', () => {
(0, model_1.toggleSidebar)();
});
// Dark mode toggle
dom.btnDarkMode.addEventListener('click', () => {
(0, model_1.toggleDarkMode)();
});
// New note
dom.btnNewNote.addEventListener('click', () => {
const state = (0, model_1.getState)();
let notebookId = state.activeNotebookId;
if (!notebookId || notebookId === types_1.NOTEBOOK_TRASH) {
// Default to first notebook
const firstNb = state.notebooks[0];
if (firstNb)
notebookId = firstNb.id;
}
if (notebookId) {
const note = (0, model_1.createNote)(notebookId);
(0, model_1.setActiveNote)(note.id);
setTimeout(() => {
(0, editor_1.loadNoteContent)(note.id);
(0, renderer_1.focusEditor)();
}, 50);
}
});
// New notebook
dom.btnNewNotebook.addEventListener('click', () => {
(0, renderer_1.showNewNotebookDialog)();
});
dom.btnConfirmNotebook.addEventListener('click', () => {
const name = dom.newNotebookInput.value.trim();
if (name) {
(0, model_1.createNotebook)(name);
(0, renderer_1.hideNewNotebookDialog)();
}
});
dom.btnCancelNotebook.addEventListener('click', () => {
(0, renderer_1.hideNewNotebookDialog)();
});
dom.newNotebookInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const name = dom.newNotebookInput.value.trim();
if (name) {
(0, model_1.createNotebook)(name);
(0, renderer_1.hideNewNotebookDialog)();
}
}
if (e.key === 'Escape') {
(0, renderer_1.hideNewNotebookDialog)();
}
});
// Sort toggle
dom.btnSort.addEventListener('click', (e) => {
e.stopPropagation();
(0, renderer_1.showSortDropdown)();
});
// Sort dropdown selection
dom.sortDropdown.addEventListener('click', (e) => {
const item = e.target.closest('.dropdown-item');
if (item) {
const sort = item.dataset.sort;
if (sort) {
(0, model_1.setSortOrder)(sort);
(0, renderer_1.hideAllDropdowns)();
}
}
});
// Pin note
dom.btnPin.addEventListener('click', () => {
const state = (0, model_1.getState)();
if (state.activeNoteId)
(0, model_1.togglePin)(state.activeNoteId);
});
// Duplicate note
dom.btnDuplicate.addEventListener('click', () => {
const state = (0, model_1.getState)();
if (state.activeNoteId) {
const dup = (0, model_1.duplicateNote)(state.activeNoteId);
if (dup) {
(0, model_1.setActiveNote)(dup.id);
setTimeout(() => (0, editor_1.loadNoteContent)(dup.id), 50);
}
}
});
// Delete note
dom.btnDelete.addEventListener('click', () => {
const state = (0, model_1.getState)();
if (state.activeNoteId) {
const note = (0, model_1.getNote)(state.activeNoteId);
if (note && note.trashed) {
// Already in trash: permanent delete
(0, model_1.permanentlyDeleteNote)(state.activeNoteId);
}
else {
(0, model_1.trashNote)(state.activeNoteId);
}
}
});
// Move note
dom.btnMove.addEventListener('click', (e) => {
e.stopPropagation();
(0, renderer_1.showMoveDropdown)();
});
dom.moveDropdown.addEventListener('click', (e) => {
const item = e.target.closest('.dropdown-item');
if (item) {
const nbId = item.dataset.moveTo;
if (nbId) {
const state = (0, model_1.getState)();
if (state.activeNoteId)
(0, model_1.moveNote)(state.activeNoteId, nbId);
(0, renderer_1.hideAllDropdowns)();
}
}
});
// Search input
dom.searchInput.addEventListener('input', (e) => {
(0, model_1.setSearchQuery)(e.target.value);
});
// Toolbar buttons
dom.editorToolbar.addEventListener('click', (e) => {
const btn = e.target.closest('.toolbar-btn');
if (btn && btn.dataset.cmd) {
(0, editor_1.handleToolbarCommand)(btn.dataset.cmd);
}
});
// Close dropdowns on outside click
document.addEventListener('click', (e) => {
const target = e.target;
// Close move dropdown
if (!target.closest('#move-dropdown') && !target.closest('#btn-move-note')) {
dom.moveDropdown.style.display = 'none';
}
// Close sort dropdown
if (!target.closest('#sort-dropdown') && !target.closest('#btn-sort')) {
dom.sortDropdown.style.display = 'none';
}
// Close trash menu
if (!target.closest('#trash-menu')) {
dom.trashMenu.style.display = 'none';
}
// Close notebook dialog on overlay click
if (target.closest('#new-notebook-dialog') && !target.closest('.modal-content')) {
(0, renderer_1.hideNewNotebookDialog)();
}
});
// Note list: right-click for trash actions
dom.noteListContainer.addEventListener('contextmenu', (e) => {
const state = (0, model_1.getState)();
if (state.activeNotebookId === types_1.NOTEBOOK_TRASH) {
e.preventDefault();
const noteItem = e.target.closest('.note-item');
if (noteItem) {
(0, renderer_1.showTrashMenu)(e.clientX, e.clientY);
}
}
});
dom.trashMenu.addEventListener('click', (e) => {
const item = e.target.closest('.dropdown-item');
if (item) {
const action = item.dataset.action;
const state = (0, model_1.getState)();
if (state.activeNoteId) {
if (action === 'restore')
(0, model_1.restoreNote)(state.activeNoteId);
if (action === 'permanent-delete')
(0, model_1.permanentlyDeleteNote)(state.activeNoteId);
}
(0, renderer_1.hideAllDropdowns)();
}
});
// Pane resizing
setupResizeHandle(dom.handle1, (delta) => {
const state = (0, model_1.getState)();
const newWidth = Math.max(160, Math.min(320, state.layout.sidebarWidth + delta));
state.layout.sidebarWidth = newWidth;
// Force re-render for width change
(0, renderer_1.render)(state);
});
setupResizeHandle(dom.handle2, (delta) => {
const state = (0, model_1.getState)();
const newWidth = Math.max(200, Math.min(450, state.layout.noteListWidth + delta));
state.layout.noteListWidth = newWidth;
(0, renderer_1.render)(state);
});
// Note selection handler (called from renderer dispatchers)
window.__onNoteSelect = (noteId) => {
(0, model_1.setActiveNote)(noteId);
setTimeout(() => {
const note = (0, model_1.getNote)(noteId);
if (note) {
(0, editor_1.loadNoteContent)(noteId);
}
}, 10);
};
// Notebook selection handler
window.__onNotebookSelect = (nbId) => {
(0, model_1.setActiveNotebook)(nbId);
(0, model_1.setActiveNote)(null);
(0, editor_1.clearEditor)();
};
// Collapse handler
window.__onCollapse = (nbId) => {
(0, model_1.toggleNotebookCollapse)(nbId);
};
// Rename notebook handler
window.__onRenameNotebook = (nbId) => {
const nb = (0, model_1.getState)().notebooks.find(n => n.id === nbId);
if (!nb)
return;
const newName = prompt('Rename notebook:', nb.name);
if (newName && newName.trim()) {
(0, model_1.renameNotebook)(nbId, newName.trim());
}
};
// Delete notebook handler
window.__onDeleteNotebook = (nbId) => {
const nb = (0, model_1.getState)().notebooks.find(n => n.id === nbId);
if (!nb)
return;
if (confirm(`Delete "${nb.name}"? Notes will be moved to Trash.`)) {
(0, model_1.deleteNotebook)(nbId);
}
};
// Tag click handler
window.__onTagClick = (tag) => {
(0, model_1.setTagFilter)(tag);
};
// Tag remove handler
window.__onTagRemove = (tag) => {
const state = (0, model_1.getState)();
if (state.activeNoteId) {
const note = (0, model_1.getNote)(state.activeNoteId);
if (note) {
(0, model_1.updateNoteTags)(state.activeNoteId, note.tags.filter(t => t !== tag));
}
}
};
// Tag add handler
window.__onTagAdd = () => {
const state = (0, model_1.getState)();
if (state.activeNoteId) {
const input = document.getElementById('tag-input');
const tag = input.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
if (tag) {
const note = (0, model_1.getNote)(state.activeNoteId);
if (note && !note.tags.includes(tag)) {
(0, model_1.updateNoteTags)(state.activeNoteId, [...note.tags, tag]);
}
input.value = '';
}
}
};
// Sort select handler
window.__onSortSelect = (sort) => {
(0, model_1.setSortOrder)(sort);
};
// Move to notebook handler
window.__onMoveToNotebook = (nbId) => {
const state = (0, model_1.getState)();
if (state.activeNoteId) {
(0, model_1.moveNote)(state.activeNoteId, nbId);
}
};
// Notebook created handler
window.__onNotebookCreated = (name) => {
(0, model_1.createNotebook)(name);
};
// Notebook cancelled handler
window.__onNotebookCancelled = () => {
(0, renderer_1.hideNewNotebookDialog)();
};
// Search input handler
window.__onSearchInput = (value) => {
(0, model_1.setSearchQuery)(value);
};
// Pin handler
window.__onPinNote = () => {
const state = (0, model_1.getState)();
if (state.activeNoteId)
(0, model_1.togglePin)(state.activeNoteId);
};
// Duplicate handler
window.__onDuplicateNote = () => {
const state = (0, model_1.getState)();
if (state.activeNoteId) {
const dup = (0, model_1.duplicateNote)(state.activeNoteId);
if (dup) {
(0, model_1.setActiveNote)(dup.id);
setTimeout(() => (0, editor_1.loadNoteContent)(dup.id), 50);
}
}
};
// Delete handler
window.__onDeleteNote = () => {
const state = (0, model_1.getState)();
if (state.activeNoteId) {
const note = (0, model_1.getNote)(state.activeNoteId);
if (note && note.trashed) {
(0, model_1.permanentlyDeleteNote)(state.activeNoteId);
}
else {
(0, model_1.trashNote)(state.activeNoteId);
}
}
};
// Move handler
window.__onMoveNote = () => {
(0, renderer_1.showMoveDropdown)();
};
// Restore handler
window.__onRestoreNote = () => {
const state = (0, model_1.getState)();
if (state.activeNoteId)
(0, model_1.restoreNote)(state.activeNoteId);
};
// Permanent delete handler
window.__onPermanentDeleteNote = () => {
const state = (0, model_1.getState)();
if (state.activeNoteId)
(0, model_1.permanentlyDeleteNote)(state.activeNoteId);
};
}
// ── DOM reference shortcut ─────────────────────────────────────────────
function getDomRefs() {
return {
btnCollapse: document.getElementById('btn-collapse-sidebar'),
btnDarkMode: document.getElementById('btn-dark-mode'),
btnNewNote: document.getElementById('btn-new-note'),
btnNewNotebook: document.getElementById('btn-new-notebook'),
btnSort: document.getElementById('btn-sort'),
btnPin: document.getElementById('btn-pin-note'),
btnDuplicate: document.getElementById('btn-duplicate-note'),
btnDelete: document.getElementById('btn-delete-note'),
btnMove: document.getElementById('btn-move-note'),
searchInput: document.getElementById('search-input'),
editorToolbar: document.getElementById('editor-toolbar'),
noteListContainer: document.getElementById('note-list-container'),
moveDropdown: document.getElementById('move-dropdown'),
sortDropdown: document.getElementById('sort-dropdown'),
trashMenu: document.getElementById('trash-menu'),
newNotebookDialog: document.getElementById('new-notebook-dialog'),
newNotebookInput: document.getElementById('new-notebook-input'),
btnConfirmNotebook: document.getElementById('btn-confirm-notebook'),
btnCancelNotebook: document.getElementById('btn-cancel-notebook'),
handle1: document.getElementById('handle1'),
handle2: document.getElementById('handle2'),
};
}
// ── Resize handle logic ────────────────────────────────────────────────
function setupResizeHandle(handle, onResize) {
let startX = 0;
let isResizing = false;
handle.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
handle.classList.add('active');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing)
return;
const delta = e.clientX - startX;
startX = e.clientX;
onResize(delta);
});
document.addEventListener('mouseup', () => {
if (!isResizing)
return;
isResizing = false;
handle.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
}
// ── Auto-start ─────────────────────────────────────────────────────────
start();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ── Core data model types ──────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.NOTEBOOK_TRASH = exports.NOTEBOOK_ALL = void 0;
exports.isVirtualNotebook = isVirtualNotebook;
// Virtual notebook IDs
exports.NOTEBOOK_ALL = 'all';
exports.NOTEBOOK_TRASH = 'trash';
function isVirtualNotebook(id) {
return id === exports.NOTEBOOK_ALL || id === exports.NOTEBOOK_TRASH;
}
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ── Tunable constants ──────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_SORT_ORDER = exports.DEFAULT_LAYOUT = exports.FONTS = exports.COLORS = exports.NOTE_LIST_MAX_WIDTH = exports.NOTE_LIST_MIN_WIDTH = exports.SIDEBAR_MAX_WIDTH = exports.SIDEBAR_MIN_WIDTH = exports.MAX_PANE_WIDTH = exports.MIN_PANE_WIDTH = exports.SNIPPET_LENGTH = exports.AUTOSAVE_DEBOUNCE = exports.STORAGE_KEY = void 0;
exports.STORAGE_KEY = 'notes-app-state';
exports.AUTOSAVE_DEBOUNCE = 500;
exports.SNIPPET_LENGTH = 80;
exports.MIN_PANE_WIDTH = 180;
exports.MAX_PANE_WIDTH = 600;
exports.SIDEBAR_MIN_WIDTH = 160;
exports.SIDEBAR_MAX_WIDTH = 320;
exports.NOTE_LIST_MIN_WIDTH = 200;
exports.NOTE_LIST_MAX_WIDTH = 450;
// ── Color palette ──────────────────────────────────────────────────────
exports.COLORS = {
// Light theme
light: {
bg: '#FAFAF9',
bgSecondary: '#F3F2F0',
bgTertiary: '#EBEAE7',
bgHover: '#E8E7E4',
bgActive: '#DFDED9',
bgEditor: '#FFFFFF',
text: '#2C2C2C',
textSecondary: '#6B6B6B',
textTertiary: '#9A9A9A',
border: '#E5E4E1',
borderLight: '#EDECE9',
accent: '#6C5CE7',
accentLight: '#A29BFE',
accentBg: 'rgba(108, 92, 231, 0.08)',
danger: '#E74C3C',
dangerBg: 'rgba(231, 76, 60, 0.08)',
success: '#27AE60',
successBg: 'rgba(39, 174, 96, 0.08)',
shadow: 'rgba(0, 0, 0, 0.06)',
shadowMedium: 'rgba(0, 0, 0, 0.1)',
tagBg: '#E8E7E4',
tagText: '#5A5A5A',
checkboxUnchecked: '#C4C4C4',
checkboxChecked: '#6C5CE7',
toolbarBg: '#FFFFFF',
toolbarBorder: '#E5E4E1',
toolbarHover: '#F3F2F0',
toolbarActive: '#DFDED9',
searchBg: '#F3F2F0',
highlightBg: 'rgba(255, 235, 59, 0.3)',
},
// Dark theme
dark: {
bg: '#1A1A1A',
bgSecondary: '#232323',
bgTertiary: '#2A2A2A',
bgHover: '#333333',
bgActive: '#3A3A3A',
bgEditor: '#1E1E1E',
text: '#E8E8E8',
textSecondary: '#A0A0A0',
textTertiary: '#6B6B6B',
border: '#333333',
borderLight: '#2A2A2A',
accent: '#A29BFE',
accentLight: '#6C5CE7',
accentBg: 'rgba(162, 155, 254, 0.12)',
danger: '#FF6B6B',
dangerBg: 'rgba(255, 107, 107, 0.12)',
success: '#69DB7C',
successBg: 'rgba(105, 219, 124, 0.12)',
shadow: 'rgba(0, 0, 0, 0.3)',
shadowMedium: 'rgba(0, 0, 0, 0.5)',
tagBg: '#333333',
tagText: '#B0B0B0',
checkboxUnchecked: '#555555',
checkboxChecked: '#A29BFE',
toolbarBg: '#232323',
toolbarBorder: '#333333',
toolbarHover: '#2A2A2A',
toolbarActive: '#3A3A3A',
searchBg: '#2A2A2A',
highlightBg: 'rgba(255, 235, 59, 0.2)',
},
// Notebook accent colors
notebookColors: [
'#6C5CE7', '#00B894', '#FDCB6E', '#E17055',
'#0984E3', '#D63031', '#00CEC9', '#E84393',
'#55A3F5', '#FF7675', '#74B9FF', '#A8E6CF',
],
};
// ── Font settings ──────────────────────────────────────────────────────
exports.FONTS = {
ui: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
editor: "'Georgia', 'Noto Serif', 'Source Serif Pro', 'Liberation Serif', 'Times New Roman', serif",
editorSans: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
mono: "'JetBrains Mono', 'Fira Code', 'SF Mono', 'Cascadia Code', 'Consolas', monospace",
};
// ── Default state ──────────────────────────────────────────────────────
exports.DEFAULT_LAYOUT = {
sidebarWidth: 240,
noteListWidth: 300,
sidebarCollapsed: false,
};
exports.DEFAULT_SORT_ORDER = 'modified';
};
// ── module: src/model.ts ──
__mods["src/model.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.genId = genId;
exports.initModel = initModel;
exports.saveState = saveState;
exports.getState = getState;
exports.getNote = getNote;
exports.getNotebook = getNotebook;
exports.getNotesForNotebook = getNotesForNotebook;
exports.createNotebook = createNotebook;
exports.renameNotebook = renameNotebook;
exports.deleteNotebook = deleteNotebook;
exports.toggleNotebookCollapse = toggleNotebookCollapse;
exports.createNote = createNote;
exports.updateNote = updateNote;
exports.updateNoteTags = updateNoteTags;
exports.togglePin = togglePin;
exports.duplicateNote = duplicateNote;
exports.trashNote = trashNote;
exports.restoreNote = restoreNote;
exports.permanentlyDeleteNote = permanentlyDeleteNote;
exports.moveNote = moveNote;
exports.setActiveNotebook = setActiveNotebook;
exports.setActiveNote = setActiveNote;
exports.setSortOrder = setSortOrder;
exports.setSearchQuery = setSearchQuery;
exports.setTagFilter = setTagFilter;
exports.getFilteredAndSortedNotes = getFilteredAndSortedNotes;
exports.setSidebarWidth = setSidebarWidth;
exports.setNoteListWidth = setNoteListWidth;
exports.toggleSidebar = toggleSidebar;
exports.toggleDarkMode = toggleDarkMode;
exports.extractPlainText = extractPlainText;
exports.getSnippet = getSnippet;
exports.sanitizeHtml = sanitizeHtml;
exports.subscribe = subscribe;
exports.formatTimestamp = formatTimestamp;
const types_1 = require("./types");
const constants_1 = require("./constants");
// ── ID generation ──────────────────────────────────────────────────────
let _idCounter = Date.now();
function genId() {
return `id_${_idCounter++}_${Math.random().toString(36).slice(2, 8)}`;
}
// ── Model ──────────────────────────────────────────────────────────────
let state;
let listeners = new Set();
function initModel() {
const loaded = loadState();
if (loaded) {
state = loaded;
}
else {
state = createDefaultState();
}
return state;
}
function createDefaultState() {
const notebook1 = genId();
const notebook2 = genId();
const now = Date.now();
return {
notebooks: [
{ id: notebook1, name: 'Personal', collapsed: false, color: constants_1.COLORS.notebookColors[0] },
{ id: notebook2, name: 'Work', collapsed: false, color: constants_1.COLORS.notebookColors[1] },
],
notes: [
{
id: genId(), notebookId: notebook1, title: 'Welcome to Notes',
content: '<h1>Welcome to Notes</h1><p>This is a <strong>rich text editor</strong> with markdown-style shortcuts.</p><ul><li>Type <code># </code> for headings</li><li>Type <code>- </code> for lists</li><li>Type <code>[] </code> for checklists</li></ul><p>Try it out!</p>',
tags: ['welcome', 'getting-started'], pinned: true,
createdAt: now, updatedAt: now, trashed: false,
},
{
id: genId(), notebookId: notebook2, title: 'Meeting Notes',
content: '<p>Discuss project timeline and deliverables.</p>',
tags: ['work', 'meetings'], pinned: false,
createdAt: now - 1000, updatedAt: now - 1000, trashed: false,
},
],
tags: [],
activeNotebookId: types_1.NOTEBOOK_ALL,
activeNoteId: null,
sortOrder: constants_1.DEFAULT_SORT_ORDER,
searchQuery: '',
tagFilter: null,
layout: constants_1.DEFAULT_LAYOUT,
darkMode: false,
};
}
// ── Persistence ────────────────────────────────────────────────────────
function saveState() {
try {
localStorage.setItem(constants_1.STORAGE_KEY, JSON.stringify(state));
}
catch (e) {
console.warn('Failed to save state:', e);
}
}
function loadState() {
try {
const raw = localStorage.getItem(constants_1.STORAGE_KEY);
if (!raw)
return null;
const parsed = JSON.parse(raw);
// Basic validation
if (!parsed.notebooks || !parsed.notes)
return null;
return parsed;
}
catch {
return null;
}
}
// ── Getters ────────────────────────────────────────────────────────────
function getState() {
return state;
}
function getNote(id) {
return state.notes.find(n => n.id === id);
}
function getNotebook(id) {
return state.notebooks.find(n => n.id === id);
}
function getNotesForNotebook(notebookId) {
if (notebookId === types_1.NOTEBOOK_ALL)
return state.notes.filter(n => !n.trashed);
if (notebookId === types_1.NOTEBOOK_TRASH)
return state.notes.filter(n => n.trashed);
return state.notes.filter(n => n.notebookId === notebookId && !n.trashed);
}
// ── Notebook operations ────────────────────────────────────────────────
function createNotebook(name) {
const color = constants_1.COLORS.notebookColors[state.notebooks.length % constants_1.COLORS.notebookColors.length];
const nb = { id: genId(), name, collapsed: false, color };
state.notebooks.push(nb);
notify();
return nb;
}
function renameNotebook(id, name) {
const nb = getNotebook(id);
if (nb) {
nb.name = name;
notify();
}
}
function deleteNotebook(id) {
const nb = getNotebook(id);
if (!nb)
return;
// Move all notes to trash
state.notes
.filter(n => n.notebookId === id && !n.trashed)
.forEach(n => { n.trashed = true; n.trashedAt = Date.now(); });
state.notebooks = state.notebooks.filter(n => n.id !== id);
if (state.activeNotebookId === id)
state.activeNotebookId = types_1.NOTEBOOK_ALL;
notify();
}
function toggleNotebookCollapse(id) {
const nb = getNotebook(id);
if (nb) {
nb.collapsed = !nb.collapsed;
notify();
}
}
// ── Note operations ────────────────────────────────────────────────────
function createNote(notebookId, title = 'Untitled') {
const now = Date.now();
const note = {
id: genId(), notebookId, title,
content: '', tags: [], pinned: false,
createdAt: now, updatedAt: now, trashed: false,
};
state.notes.push(note);
notify();
return note;
}
function updateNote(id, updates) {
const note = getNote(id);
if (note) {
if (updates.title !== undefined)
note.title = updates.title;
if (updates.content !== undefined)
note.content = updates.content;
note.updatedAt = Date.now();
notify();
}
}
function updateNoteTags(id, tags) {
const note = getNote(id);
if (note) {
note.tags = tags;
note.updatedAt = Date.now();
notify();
}
}
function togglePin(id) {
const note = getNote(id);
if (note) {
note.pinned = !note.pinned;
note.updatedAt = Date.now();
notify();
}
}
function duplicateNote(id) {
const note = getNote(id);
if (!note)
return undefined;
const now = Date.now();
const dup = {
...note,
id: genId(),
title: note.title + ' (copy)',
createdAt: now,
updatedAt: now,
pinned: false,
trashed: false,
trashedAt: undefined,
};
state.notes.push(dup);
notify();
return dup;
}
function trashNote(id) {
const note = getNote(id);
if (note && !note.trashed) {
note.trashed = true;
note.trashedAt = Date.now();
note.updatedAt = Date.now();
if (state.activeNoteId === id)
state.activeNoteId = null;
notify();
}
}
function restoreNote(id) {
const note = getNote(id);
if (note && note.trashed) {
note.trashed = false;
note.trashedAt = undefined;
note.updatedAt = Date.now();
notify();
}
}
function permanentlyDeleteNote(id) {
state.notes = state.notes.filter(n => n.id !== id);
if (state.activeNoteId === id)
state.activeNoteId = null;
notify();
}
function moveNote(id, targetNotebookId) {
if ((0, types_1.isVirtualNotebook)(targetNotebookId))
return;
const note = getNote(id);
if (note) {
note.notebookId = targetNotebookId;
note.updatedAt = Date.now();
notify();
}
}
// ── Selection ──────────────────────────────────────────────────────────
function setActiveNotebook(id) {
state.activeNotebookId = id;
state.tagFilter = null;
notify();
}
function setActiveNote(id) {
state.activeNoteId = id;
notify();
}
// ── Sort & filter ──────────────────────────────────────────────────────
function setSortOrder(order) {
state.sortOrder = order;
notify();
}
function setSearchQuery(query) {
state.searchQuery = query;
notify();
}
function setTagFilter(tag) {
state.tagFilter = tag;
notify();
}
function getFilteredAndSortedNotes() {
const nbId = state.activeNotebookId;
let notes = nbId ? getNotesForNotebook(nbId) : [];
// 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 => {
const titleMatch = n.title.toLowerCase().includes(q);
const contentMatch = extractPlainText(n.content).toLowerCase().includes(q);
return titleMatch || contentMatch;
});
}
// Sort: pinned first, then by sort order
notes.sort((a, b) => {
if (a.pinned !== b.pinned)
return a.pinned ? -1 : 1;
switch (state.sortOrder) {
case 'modified': return b.updatedAt - a.updatedAt;
case 'created': return b.createdAt - a.createdAt;
case 'alphabetical': return a.title.localeCompare(b.title);
default: return 0;
}
});
return notes;
}
// ── Layout ─────────────────────────────────────────────────────────────
function setSidebarWidth(width) {
state.layout.sidebarWidth = width;
notify();
}
function setNoteListWidth(width) {
state.layout.noteListWidth = width;
notify();
}
function toggleSidebar() {
state.layout.sidebarCollapsed = !state.layout.sidebarCollapsed;
notify();
}
function toggleDarkMode() {
state.darkMode = !state.darkMode;
notify();
}
// ── Text extraction (pure, no HTML tags) ───────────────────────────────
function extractPlainText(html) {
if (!html)
return '';
const div = document.createElement('div');
div.innerHTML = html;
return (div.textContent || div.innerText || '').trim();
}
// ── Snippet generation ─────────────────────────────────────────────────
function getSnippet(html, maxLength = 80) {
const text = extractPlainText(html);
if (text.length <= maxLength)
return text;
return text.slice(0, maxLength).trimEnd() + '…';
}
// ── Sanitization ───────────────────────────────────────────────────────
const ALLOWED_TAGS = new Set([
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'del', 'code',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'hr',
'div', 'span', 'a',
]);
function sanitizeHtml(html) {
const div = document.createElement('div');
div.innerHTML = html;
// Remove disallowed tags (keep their children)
const walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT);
const toRemove = [];
while (walker.nextNode()) {
const el = walker.currentNode;
if (!ALLOWED_TAGS.has(el.tagName.toLowerCase())) {
toRemove.push(el);
}
}
for (const el of toRemove) {
// Replace with text content
const text = document.createTextNode(el.textContent || '');
el.parentNode?.replaceChild(text, el);
}
// Remove all attributes except those needed for checklist state
const allEls = div.querySelectorAll('*');
for (const el of allEls) {
const attrs = Array.from(el.attributes);
for (const attr of attrs) {
// Preserve class on checklist items and checkboxes
if (attr.name === 'class' && (el.tagName.toLowerCase() === 'li' ||
el.tagName.toLowerCase() === 'span'))
continue;
// Preserve data-* on li elements
if (el.tagName.toLowerCase() === 'li' && attr.name.startsWith('data-'))
continue;
el.removeAttribute(attr.name);
}
}
return div.innerHTML;
}
// ── Reactive notifications ─────────────────────────────────────────────
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function notify() {
saveState();
for (const fn of listeners)
fn();
}
// ── Timestamp formatting ───────────────────────────────────────────────
function formatTimestamp(ts) {
const now = Date.now();
const diff = now - ts;
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
const week = 7 * day;
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 < week)
return `${Math.floor(diff / day)}d ago`;
const date = new Date(ts);
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined });
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cacheDom = cacheDom;
exports.render = render;
exports.renderTheme = renderTheme;
exports.renderLayout = renderLayout;
exports.renderSidebar = renderSidebar;
exports.renderNoteList = renderNoteList;
exports.renderEditorVisibility = renderEditorVisibility;
exports.renderEditorState = renderEditorState;
exports.showSavedIndicator = showSavedIndicator;
exports.dispatchNotebookSelect = dispatchNotebookSelect;
exports.dispatchNoteSelect = dispatchNoteSelect;
exports.dispatchCollapse = dispatchCollapse;
exports.dispatchRenameNotebook = dispatchRenameNotebook;
exports.dispatchDeleteNotebook = dispatchDeleteNotebook;
exports.dispatchNewNote = dispatchNewNote;
exports.dispatchNewNotebook = dispatchNewNotebook;
exports.dispatchToggleSidebar = dispatchToggleSidebar;
exports.dispatchToggleDarkMode = dispatchToggleDarkMode;
exports.dispatchSortToggle = dispatchSortToggle;
exports.dispatchPinNote = dispatchPinNote;
exports.dispatchDuplicateNote = dispatchDuplicateNote;
exports.dispatchDeleteNote = dispatchDeleteNote;
exports.dispatchMoveNote = dispatchMoveNote;
exports.dispatchRestoreNote = dispatchRestoreNote;
exports.dispatchPermanentDeleteNote = dispatchPermanentDeleteNote;
exports.dispatchSearchInput = dispatchSearchInput;
exports.dispatchTagClick = dispatchTagClick;
exports.dispatchTagRemove = dispatchTagRemove;
exports.dispatchTagAdd = dispatchTagAdd;
exports.dispatchSortSelect = dispatchSortSelect;
exports.dispatchMoveToNotebook = dispatchMoveToNotebook;
exports.dispatchNotebookCreated = dispatchNotebookCreated;
exports.dispatchNotebookCancelled = dispatchNotebookCancelled;
exports.getEditorContent = getEditorContent;
exports.setEditorContent = setEditorContent;
exports.focusEditor = focusEditor;
exports.showMoveDropdown = showMoveDropdown;
exports.showSortDropdown = showSortDropdown;
exports.showTrashMenu = showTrashMenu;
exports.hideAllDropdowns = hideAllDropdowns;
exports.showNewNotebookDialog = showNewNotebookDialog;
exports.hideNewNotebookDialog = hideNewNotebookDialog;
exports.updateToolbarState = updateToolbarState;
exports.setupDragDrop = setupDragDrop;
const types_1 = require("./types");
const model_1 = require("./model");
// ── DOM element references ─────────────────────────────────────────────
let dom = {};
let searchInput;
let editorContent;
let editorTitle;
let savedIndicator;
let editorBody;
let editorEmptyState;
let tagInput;
function cacheDom() {
dom = {
sidebar: document.getElementById('sidebar'),
sidebarList: document.getElementById('sidebar-list'),
sidebarVirtual: document.getElementById('sidebar-virtual-items'),
sidebarNotebooks: document.getElementById('sidebar-notebooks'),
noteListPane: document.getElementById('note-list-pane'),
noteListTitle: document.getElementById('note-list-title'),
noteListCount: document.getElementById('note-list-count'),
noteListContainer: document.getElementById('note-list-container'),
editorPane: document.getElementById('editor-pane'),
editorBody: document.getElementById('editor-body'),
editorContent: document.getElementById('editor-content'),
editorTitle: document.getElementById('editor-title'),
savedIndicator: document.getElementById('saved-indicator'),
editorEmptyState: document.getElementById('editor-empty-state'),
tagsContainer: document.getElementById('tags-container'),
tagInput: document.getElementById('tag-input'),
editorHeader: document.getElementById('editor-header'),
editorToolbar: document.getElementById('editor-toolbar'),
editorTagsArea: document.getElementById('editor-tags-area'),
btnCollapse: document.getElementById('btn-collapse-sidebar'),
btnNewNote: document.getElementById('btn-new-note'),
btnNewNotebook: document.getElementById('btn-new-notebook'),
btnSort: document.getElementById('btn-sort'),
btnMove: document.getElementById('btn-move-note'),
btnDuplicate: document.getElementById('btn-duplicate-note'),
btnPin: document.getElementById('btn-pin-note'),
btnDelete: document.getElementById('btn-delete-note'),
btnDarkMode: document.getElementById('btn-dark-mode'),
moveDropdown: document.getElementById('move-dropdown'),
sortDropdown: document.getElementById('sort-dropdown'),
trashMenu: document.getElementById('trash-menu'),
newNotebookDialog: document.getElementById('new-notebook-dialog'),
newNotebookInput: document.getElementById('new-notebook-input'),
btnConfirmNotebook: document.getElementById('btn-confirm-notebook'),
btnCancelNotebook: document.getElementById('btn-cancel-notebook'),
handle1: document.getElementById('handle1'),
handle2: document.getElementById('handle2'),
};
searchInput = document.getElementById('search-input');
editorContent = dom.editorContent;
editorTitle = dom.editorTitle;
savedIndicator = dom.savedIndicator;
editorBody = dom.editorBody;
editorEmptyState = dom.editorEmptyState;
tagInput = dom.tagInput;
}
// ── Full re-render ─────────────────────────────────────────────────────
function render(state) {
renderTheme(state);
renderLayout(state.layout);
renderSidebar(state);
renderNoteList(state);
renderEditorState(state);
renderEditorVisibility(state);
updateSavedIndicator(false);
}
// ── Theme ──────────────────────────────────────────────────────────────
let lastDarkMode = null;
function renderTheme(state) {
if (state.darkMode === lastDarkMode)
return;
lastDarkMode = state.darkMode;
document.body.classList.toggle('dark-mode', state.darkMode);
}
// ── Layout ─────────────────────────────────────────────────────────────
let lastSidebarCollapsed = null;
function renderLayout(layout) {
// Sidebar collapse
if (layout.sidebarCollapsed !== lastSidebarCollapsed) {
lastSidebarCollapsed = layout.sidebarCollapsed;
dom.sidebar.classList.toggle('collapsed', layout.sidebarCollapsed);
dom.btnCollapse.textContent = layout.sidebarCollapsed ? '▶' : '◀';
dom.handle1.style.display = layout.sidebarCollapsed ? 'none' : '';
}
// Widths
dom.sidebar.style.width = layout.sidebarWidth + 'px';
dom.noteListPane.style.width = layout.noteListWidth + 'px';
}
// ── Sidebar ────────────────────────────────────────────────────────────
let lastSidebarKey = '';
function renderSidebar(state) {
const key = JSON.stringify({
nbCount: state.notebooks.length,
nbNames: state.notebooks.map(n => n.name),
nbCollapsed: state.notebooks.map(n => n.collapsed),
noteCounts: state.notebooks.map(nb => state.notes.filter(n => n.notebookId === nb.id && !n.trashed).length),
trashCount: state.notes.filter(n => n.trashed).length,
active: state.activeNotebookId,
});
if (key === lastSidebarKey)
return;
lastSidebarKey = key;
// Virtual items: All Notes + Trash
const allCount = state.notes.filter(n => !n.trashed).length;
const trashCount = state.notes.filter(n => n.trashed).length;
dom.sidebarVirtual.innerHTML = `
${renderVirtualItem('📋', 'All Notes', types_1.NOTEBOOK_ALL, allCount, state.activeNotebookId === types_1.NOTEBOOK_ALL)}
${renderVirtualItem('🗑', 'Trash', types_1.NOTEBOOK_TRASH, trashCount, state.activeNotebookId === types_1.NOTEBOOK_TRASH)}
`;
// Notebooks
let html = '';
for (const nb of state.notebooks) {
const notes = state.notes.filter(n => n.notebookId === nb.id && !n.trashed);
const isActive = state.activeNotebookId === nb.id;
html += renderNotebookItem(nb, notes.length, isActive, state);
}
dom.sidebarNotebooks.innerHTML = html;
// Bind events
bindSidebarEvents(state);
}
function renderVirtualItem(icon, name, id, count, active) {
return `<div class="sidebar-item ${active ? 'active' : ''}" data-notebook="${id}">
<span class="sidebar-item-icon">${icon}</span>
<span class="sidebar-item-name">${name}</span>
<span class="sidebar-item-count">${count}</span>
</div>`;
}
function renderNotebookItem(nb, count, active, state) {
const notes = state.notes.filter(n => n.notebookId === nb.id && !n.trashed);
let notesHtml = '';
if (!nb.collapsed) {
for (const note of notes.slice(0, 10)) {
notesHtml += `<div class="sidebar-note-item ${state.activeNoteId === note.id ? 'active' : ''}" data-note="${note.id}" data-notebook="${nb.id}">
${escapeHtml(note.title || 'Untitled')}
</div>`;
}
}
return `<div class="notebook-group">
<div class="sidebar-item notebook-header ${active ? 'active' : ''}" data-notebook="${nb.id}">
<span class="notebook-dot" style="background:${nb.color}"></span>
<span class="sidebar-item-name">${escapeHtml(nb.name)}</span>
<span class="sidebar-item-count">${count}</span>
<div class="sidebar-item-actions">
<button class="sidebar-item-action" data-action="collapse" data-notebook="${nb.id}" title="Collapse">▾</button>
<button class="sidebar-item-action" data-action="rename" data-notebook="${nb.id}" title="Rename">✎</button>
<button class="sidebar-item-action delete" data-action="delete-nb" data-notebook="${nb.id}" title="Delete">✕</button>
</div>
</div>
<div class="notebook-notes ${nb.collapsed ? 'collapsed' : ''}" data-notebook-notes="${nb.id}">
${notesHtml}
</div>
</div>`;
}
function bindSidebarEvents(state) {
// Notebook selection
dom.sidebarVirtual.addEventListener('click', (e) => {
const item = e.target.closest('[data-notebook]');
if (!item)
return;
const id = item.dataset.notebook;
if (id)
dispatchNotebookSelect(id);
});
dom.sidebarNotebooks.addEventListener('click', (e) => {
const btn = e.target;
// Action buttons
if (btn.dataset.action === 'collapse') {
e.stopPropagation();
const nbId = btn.dataset.notebook;
if (nbId)
dispatchCollapse(nbId);
return;
}
if (btn.dataset.action === 'rename') {
e.stopPropagation();
const nbId = btn.dataset.notebook;
if (nbId)
dispatchRenameNotebook(nbId);
return;
}
if (btn.dataset.action === 'delete-nb') {
e.stopPropagation();
const nbId = btn.dataset.notebook;
if (nbId)
dispatchDeleteNotebook(nbId);
return;
}
// Notebook or note selection
const item = e.target.closest('[data-notebook]') || e.target.closest('[data-note]');
if (item) {
const nbId = item.dataset.notebook;
if (nbId)
dispatchNotebookSelect(nbId);
const noteId = item.dataset.note;
if (noteId)
dispatchNoteSelect(noteId);
}
});
}
// ── Note List ──────────────────────────────────────────────────────────
let lastNoteListKey = '';
function renderNoteList(state) {
const notes = (0, model_1.getFilteredAndSortedNotes)();
const nbId = state.activeNotebookId;
// Title
let titleText = 'All Notes';
if (nbId === types_1.NOTEBOOK_TRASH)
titleText = 'Trash';
else {
const nb = state.notebooks.find(n => n.id === nbId);
if (nb)
titleText = nb.name;
}
dom.noteListTitle.textContent = titleText;
dom.noteListCount.textContent = `${notes.length} note${notes.length !== 1 ? 's' : ''}`;
// Search input sync
if (searchInput.value !== state.searchQuery) {
searchInput.value = state.searchQuery;
}
// Tag filter indicator
if (state.tagFilter) {
dom.noteListTitle.textContent = `#${state.tagFilter}`;
}
// Render notes
const key = JSON.stringify(notes.map(n => ({
id: n.id, title: n.title, pinned: n.pinned, updatedAt: n.updatedAt,
tags: n.tags, contentLen: n.content.length,
})));
if (key === lastNoteListKey && notes.length === dom.noteListContainer.children.length)
return;
lastNoteListKey = key;
let html = '';
for (const note of notes) {
html += renderNoteItem(note, state);
}
dom.noteListContainer.innerHTML = html;
bindNoteListEvents();
}
function bindNoteListEvents() {
dom.noteListContainer.onclick = (e) => {
// Tag click
const tagEl = e.target.closest('.note-item-tag');
if (tagEl) {
const tag = tagEl.dataset.tag;
if (tag)
dispatchTagClick(tag);
return;
}
// Note item click
const noteItem = e.target.closest('.note-item');
if (noteItem) {
const noteId = noteItem.dataset.noteId;
if (noteId)
dispatchNoteSelect(noteId);
}
};
// Right-click for trash actions
dom.noteListContainer.oncontextmenu = (e) => {
const state = (0, model_1.getState)();
if (state.activeNotebookId === types_1.NOTEBOOK_TRASH) {
e.preventDefault();
const noteItem = e.target.closest('.note-item');
if (noteItem) {
// Select this note first
const noteId = noteItem.dataset.noteId;
if (noteId)
dispatchNoteSelect(noteId);
showTrashMenu(e.clientX, e.clientY);
}
}
};
}
function renderNoteItem(note, state) {
const snippet = (0, model_1.getSnippet)(note.content, 80);
const timestamp = (0, model_1.formatTimestamp)(note.updatedAt);
const isActive = state.activeNoteId === note.id;
const title = note.title || 'Untitled';
const searchQuery = state.searchQuery;
const highlightedTitle = searchQuery ? highlightText(title, searchQuery) : escapeHtml(title);
const highlightedSnippet = searchQuery ? highlightText(snippet, searchQuery) : escapeHtml(snippet);
let tagsHtml = '';
if (note.tags.length > 0) {
tagsHtml = `<div class="note-item-tags">
${note.tags.map(t => `<span class="note-item-tag" data-tag="${escapeHtml(t)}">${escapeHtml(t)}</span>`).join('')}
</div>`;
}
const pinIcon = note.pinned ? '<span class="pin-indicator">📌</span>' : '';
return `<div class="note-item ${isActive ? 'active' : ''}" data-note-id="${note.id}">
<div class="note-item-title">${pinIcon}${highlightedTitle}</div>
<div class="note-item-snippet">${highlightedSnippet}</div>
<div class="note-item-meta">
<span>${timestamp}</span>
${tagsHtml}
</div>
</div>`;
}
function highlightText(text, query) {
if (!query)
return escapeHtml(text);
const escaped = escapeHtml(text);
const escapedQuery = escapeHtml(query);
const regex = new RegExp(`(${escapeRegex(escapedQuery)})`, 'gi');
return escaped.replace(regex, '<mark class="search-highlight">$1</mark>');
}
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
// ── Editor visibility ──────────────────────────────────────────────────
let lastActiveNoteId = null;
function renderEditorVisibility(state) {
if (state.activeNoteId === lastActiveNoteId)
return;
lastActiveNoteId = state.activeNoteId;
const hasNote = !!state.activeNoteId;
editorBody.style.display = hasNote ? '' : 'none';
dom.editorHeader.style.display = hasNote ? '' : 'none';
dom.editorToolbar.style.display = hasNote ? '' : 'none';
dom.editorTagsArea.style.display = hasNote ? '' : 'none';
editorEmptyState.style.display = hasNote ? 'none' : 'flex';
}
// ── Editor state ───────────────────────────────────────────────────────
function renderEditorState(state) {
const note = state.activeNoteId ? (0, model_1.getNote)(state.activeNoteId) : null;
if (!note)
return;
// Title
if (editorTitle.value !== note.title) {
editorTitle.value = note.title;
}
// Tags
renderTags(note);
}
function renderTags(note) {
const container = dom.tagsContainer;
const existingTags = Array.from(container.querySelectorAll('.tag-chip')).map(el => el.dataset.tag);
const newTags = note.tags;
// Simple diff: rebuild if different
const key = JSON.stringify(newTags);
if (container._tagKey === key)
return;
container._tagKey = key;
container.innerHTML = note.tags.map(t => `<span class="tag-chip" data-tag="${escapeHtml(t)}">${escapeHtml(t)}<span class="tag-remove" data-tag="${escapeHtml(t)}">×</span></span>`).join('');
}
// ── Saved indicator ────────────────────────────────────────────────────
let savedTimeout = null;
function showSavedIndicator() {
savedIndicator.classList.add('show');
if (savedTimeout)
clearTimeout(savedTimeout);
savedTimeout = setTimeout(() => {
savedIndicator.classList.remove('show');
}, 1500);
}
function updateSavedIndicator(show) {
savedIndicator.classList.toggle('show', show);
}
// ── Event dispatchers (bridge to model) ────────────────────────────────
function dispatchNotebookSelect(id) {
const handler = window;
if (handler.__onNotebookSelect)
handler.__onNotebookSelect(id);
}
function dispatchNoteSelect(id) {
const handler = window;
if (handler.__onNoteSelect)
handler.__onNoteSelect(id);
}
function dispatchCollapse(nbId) {
const handler = window;
if (handler.__onCollapse)
handler.__onCollapse(nbId);
}
function dispatchRenameNotebook(nbId) {
const handler = window;
if (handler.__onRenameNotebook)
handler.__onRenameNotebook(nbId);
}
function dispatchDeleteNotebook(nbId) {
const handler = window;
if (handler.__onDeleteNotebook)
handler.__onDeleteNotebook(nbId);
}
function dispatchNewNote() {
const handler = window;
if (handler.__onNewNote)
handler.__onNewNote();
}
function dispatchNewNotebook() {
const handler = window;
if (handler.__onNewNotebook)
handler.__onNewNotebook();
}
function dispatchToggleSidebar() {
const handler = window;
if (handler.__onToggleSidebar)
handler.__onToggleSidebar();
}
function dispatchToggleDarkMode() {
const handler = window;
if (handler.__onToggleDarkMode)
handler.__onToggleDarkMode();
}
function dispatchSortToggle() {
const handler = window;
if (handler.__onSortToggle)
handler.__onSortToggle();
}
function dispatchPinNote() {
const handler = window;
if (handler.__onPinNote)
handler.__onPinNote();
}
function dispatchDuplicateNote() {
const handler = window;
if (handler.__onDuplicateNote)
handler.__onDuplicateNote();
}
function dispatchDeleteNote() {
const handler = window;
if (handler.__onDeleteNote)
handler.__onDeleteNote();
}
function dispatchMoveNote() {
const handler = window;
if (handler.__onMoveNote)
handler.__onMoveNote();
}
function dispatchRestoreNote() {
const handler = window;
if (handler.__onRestoreNote)
handler.__onRestoreNote();
}
function dispatchPermanentDeleteNote() {
const handler = window;
if (handler.__onPermanentDeleteNote)
handler.__onPermanentDeleteNote();
}
function dispatchSearchInput(value) {
const handler = window;
if (handler.__onSearchInput)
handler.__onSearchInput(value);
}
function dispatchTagClick(tag) {
const handler = window;
if (handler.__onTagClick)
handler.__onTagClick(tag);
}
function dispatchTagRemove(tag) {
const handler = window;
if (handler.__onTagRemove)
handler.__onTagRemove(tag);
}
function dispatchTagAdd() {
const handler = window;
if (handler.__onTagAdd)
handler.__onTagAdd();
}
function dispatchSortSelect(sort) {
const handler = window;
if (handler.__onSortSelect)
handler.__onSortSelect(sort);
}
function dispatchMoveToNotebook(nbId) {
const handler = window;
if (handler.__onMoveToNotebook)
handler.__onMoveToNotebook(nbId);
}
function dispatchNotebookCreated(name) {
const handler = window;
if (handler.__onNotebookCreated)
handler.__onNotebookCreated(name);
}
function dispatchNotebookCancelled() {
const handler = window;
if (handler.__onNotebookCancelled)
handler.__onNotebookCancelled();
}
// ── Editor content helpers ─────────────────────────────────────────────
function getEditorContent() {
return editorContent.innerHTML;
}
function setEditorContent(html) {
editorContent.innerHTML = html;
}
function focusEditor() {
editorContent.focus();
}
// ── Dropdown management ────────────────────────────────────────────────
function showMoveDropdown() {
const state = (0, model_1.getState)();
const btn = dom.btnMove;
const rect = btn.getBoundingClientRect();
dom.moveDropdown.style.top = (rect.bottom + 4) + 'px';
dom.moveDropdown.style.left = rect.left + 'px';
dom.moveDropdown.style.display = 'block';
let html = '';
for (const nb of state.notebooks) {
html += `<div class="dropdown-item" data-move-to="${nb.id}">
<span class="notebook-dot" style="background:${nb.color};display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;"></span>
${escapeHtml(nb.name)}
</div>`;
}
dom.moveDropdown.innerHTML = html;
}
function showSortDropdown() {
const btn = dom.btnSort;
const rect = btn.getBoundingClientRect();
dom.sortDropdown.style.top = (rect.bottom + 4) + 'px';
dom.sortDropdown.style.left = rect.left + 'px';
dom.sortDropdown.style.display = 'block';
}
function showTrashMenu(x, y) {
dom.trashMenu.style.top = y + 'px';
dom.trashMenu.style.left = x + 'px';
dom.trashMenu.style.display = 'block';
}
function hideAllDropdowns() {
dom.moveDropdown.style.display = 'none';
dom.sortDropdown.style.display = 'none';
dom.trashMenu.style.display = 'none';
}
function showNewNotebookDialog() {
dom.newNotebookDialog.style.display = 'flex';
dom.newNotebookInput.value = '';
dom.newNotebookInput.focus();
}
function hideNewNotebookDialog() {
dom.newNotebookDialog.style.display = 'none';
}
// ── Toolbar state ──────────────────────────────────────────────────────
function updateToolbarState() {
const toolbar = dom.editorToolbar;
const buttons = toolbar.querySelectorAll('.toolbar-btn[data-cmd]');
for (const btn of buttons) {
const cmd = btn.dataset.cmd;
if (!cmd)
continue;
let isActive = false;
switch (cmd) {
case 'bold':
isActive = document.queryCommandState('bold');
break;
case 'italic':
isActive = document.queryCommandState('italic');
break;
case 'underline':
isActive = document.queryCommandState('underline');
break;
case 'strikethrough':
isActive = document.queryCommandState('strikeThrough');
break;
case 'h1':
isActive = document.queryCommandState('formatBlock') && getCurrentBlock() === 'H1';
break;
case 'h2':
isActive = document.queryCommandState('formatBlock') && getCurrentBlock() === 'H2';
break;
case 'h3':
isActive = document.queryCommandState('formatBlock') && getCurrentBlock() === 'H3';
break;
case 'ul':
isActive = document.queryCommandState('insertUnorderedList');
break;
case 'ol':
isActive = document.queryCommandState('insertOrderedList');
break;
case 'blockquote':
isActive = getCurrentBlock() === 'BLOCKQUOTE';
break;
case 'code': {
const sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
let node = sel.anchorNode;
while (node && node !== editorContent) {
if (node instanceof HTMLElement && node.tagName === 'CODE') {
isActive = true;
break;
}
node = node.parentNode;
}
}
break;
}
}
btn.classList.toggle('active', isActive);
}
}
function getCurrentBlock() {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return '';
let node = sel.anchorNode;
while (node && node !== editorContent) {
if (node instanceof HTMLElement && /^H[1-6]|BLOCKQUOTE$/.test(node.tagName)) {
return node.tagName;
}
node = node.parentNode;
}
return '';
}
// ── Drag and drop for notes ────────────────────────────────────────────
function setupDragDrop() {
// Note items are draggable
dom.noteListContainer.addEventListener('dragstart', (e) => {
const noteItem = e.target.closest('.note-item');
if (!noteItem)
return;
const noteId = noteItem.dataset.noteId;
if (noteId) {
e.dataTransfer.setData('text/plain', noteId);
noteItem.style.opacity = '0.5';
}
});
dom.noteListContainer.addEventListener('dragend', (e) => {
const noteItem = e.target.closest('.note-item');
if (noteItem)
noteItem.style.opacity = '';
});
// Sidebar notebooks as drop targets
dom.sidebarNotebooks.addEventListener('dragover', (e) => {
e.preventDefault();
const item = e.target.closest('.sidebar-item');
if (item)
item.classList.add('drop-target');
});
dom.sidebarNotebooks.addEventListener('dragleave', (e) => {
const item = e.target.closest('.sidebar-item');
if (item)
item.classList.remove('drop-target');
});
dom.sidebarNotebooks.addEventListener('drop', (e) => {
e.preventDefault();
const item = e.target.closest('.sidebar-item');
if (item)
item.classList.remove('drop-target');
const noteId = e.dataTransfer.getData('text/plain');
const nbId = item?.dataset.notebook;
if (noteId && nbId) {
dispatchMoveToNotebook(nbId);
}
});
}
};
// ── module: src/editor.ts ──
__mods["src/editor.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initEditor = initEditor;
exports.handleEditorKeydown = handleEditorKeydown;
exports.handleToolbarCommand = handleToolbarCommand;
exports.loadNoteContent = loadNoteContent;
exports.clearEditor = clearEditor;
// ── Editor: contenteditable management, toolbar, keyboard shortcuts ─────
const constants_1 = require("./constants");
const model_1 = require("./model");
const renderer_1 = require("./renderer");
let saveTimeout = null;
let isComposing = false; // IME composition flag
let lastContent = '';
// ── Editor initialization ──────────────────────────────────────────────
function initEditor() {
const editor = document.getElementById('editor-content');
const titleInput = document.getElementById('editor-title');
const tagInput = document.getElementById('tag-input');
// Title input
titleInput.addEventListener('input', () => {
const state = (0, model_1.getState)();
if (!state.activeNoteId)
return;
const note = (0, model_1.getNote)(state.activeNoteId);
if (note && note.title !== titleInput.value) {
(0, model_1.updateNote)(state.activeNoteId, { title: titleInput.value });
scheduleSave();
}
});
// Content input
editor.addEventListener('input', () => {
if (isComposing)
return;
const state = (0, model_1.getState)();
if (!state.activeNoteId)
return;
const content = (0, model_1.sanitizeHtml)(editor.innerHTML);
if (content !== lastContent) {
(0, model_1.updateNote)(state.activeNoteId, { content });
lastContent = content;
scheduleSave();
}
});
// Selection change → update toolbar
editor.addEventListener('selectionchange', () => {
// Use a microtask to let the selection settle
requestAnimationFrame(() => (0, renderer_1.updateToolbarState)());
});
// Keyboard shortcuts
editor.addEventListener('keydown', handleEditorKeydown);
// Paste sanitization
editor.addEventListener('paste', (e) => {
e.preventDefault();
const text = (e.clipboardData || window.clipboardData)?.getData('text/plain') || '';
document.execCommand('insertText', false, text);
});
// Drag prevention (don't let files be dropped)
editor.addEventListener('dragover', (e) => e.preventDefault());
editor.addEventListener('drop', (e) => e.preventDefault());
// IME composition
editor.addEventListener('compositionstart', () => { isComposing = true; });
editor.addEventListener('compositionend', () => {
isComposing = false;
// Trigger input after composition ends
const state = (0, model_1.getState)();
if (state.activeNoteId) {
const content = (0, model_1.sanitizeHtml)(editor.innerHTML);
if (content !== lastContent) {
(0, model_1.updateNote)(state.activeNoteId, { content });
lastContent = content;
scheduleSave();
}
}
});
// Tag input
tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTagFromInput(tagInput);
}
});
// Tag chip clicks
document.getElementById('tags-container').addEventListener('click', (e) => {
const removeBtn = e.target.closest('.tag-remove');
if (removeBtn) {
const tag = removeBtn.dataset.tag;
if (tag)
removeTag(tag);
return;
}
const chip = e.target.closest('.tag-chip');
if (chip) {
const tag = chip.dataset.tag;
if (tag)
filterByTag(tag);
}
});
}
// ── Autosave ───────────────────────────────────────────────────────────
function scheduleSave() {
if (saveTimeout)
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
(0, renderer_1.showSavedIndicator)();
}, constants_1.AUTOSAVE_DEBOUNCE);
}
// ── Markdown shortcuts ─────────────────────────────────────────────────
function handleEditorKeydown(e) {
const editor = document.getElementById('editor-content');
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
const key = e.key;
// Handle Enter key for list continuation and markdown shortcuts
if (key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.altKey) {
if (handleEnterKey(editor, sel)) {
e.preventDefault();
}
return;
}
// Handle Tab for indent/outdent
if (key === 'Tab') {
e.preventDefault();
if (e.shiftKey) {
document.execCommand('outdent', false);
}
else {
document.execCommand('indent', false);
}
return;
}
// Markdown shortcuts: check on space key
if (key === ' ' || key === 'Spacebar') {
if (handleMarkdownShortcut(editor, sel)) {
e.preventDefault();
}
}
}
function handleEnterKey(editor, sel) {
const range = sel.getRangeAt(0);
const anchorNode = range.startContainer;
// Find the current block element
let block = findBlockElement(anchorNode);
if (!block)
return false;
// Check if we're in a list
const listParent = findListParent(block);
if (listParent) {
const isEmpty = isListItemEmpty(block);
if (isEmpty) {
// Exit the list: remove the empty item
const parentList = listParent;
const prevSibling = block.previousElementSibling;
parentList.removeChild(block);
// If list is now empty, remove it too
if (parentList.children.length === 0) {
const p = document.createElement('p');
p.textContent = '';
parentList.parentNode?.replaceChild(p, parentList);
// Place cursor in the new paragraph
const newRange = document.createRange();
newRange.setStart(p, 0);
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
}
else {
// Place cursor after the previous sibling
const newRange = document.createRange();
if (prevSibling) {
newRange.setStartAfter(prevSibling);
}
else {
newRange.setStartBefore(parentList);
}
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
}
return true; // Prevent default
}
}
return false;
}
function findBlockElement(node) {
const editor = document.getElementById('editor-content');
while (node && node !== document.body && node !== editor) {
if (node instanceof HTMLElement) {
const tag = node.tagName;
if (['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI', 'DIV'].includes(tag)) {
return node;
}
}
node = node.parentNode;
}
return null;
}
function findListParent(element) {
let parent = element.parentElement;
while (parent) {
if (parent.tagName === 'UL' || parent.tagName === 'OL')
return parent;
parent = parent.parentElement;
}
return null;
}
function isListItemEmpty(li) {
// A list item is empty if it has no text content (ignoring whitespace)
// and no child elements other than checkboxes
const text = li.textContent?.trim() || '';
if (text)
return false;
// Check for checklist checkbox - that's OK
const children = li.children;
for (let i = 0; i < children.length; i++) {
if (children[i].classList?.contains('checklist-checkbox'))
continue;
return false;
}
return true;
}
// ── Markdown shortcuts on space ────────────────────────────────────────
function handleMarkdownShortcut(editor, sel) {
const range = sel.getRangeAt(0);
const anchorNode = range.startContainer;
// Find the text node and its position
if (anchorNode.nodeType !== Node.TEXT_NODE)
return false;
const text = anchorNode.textContent || '';
const offset = range.startOffset;
// Get the text before the cursor (the space hasn't been inserted yet on keydown)
const beforeCursor = text.substring(0, offset);
// Find the current line (text before the last newline)
const lines = beforeCursor.split('\n');
const currentLine = lines[lines.length - 1];
// Check markdown patterns - the space triggers the conversion
// At keydown time, the space hasn't been inserted yet
// The line may have leading whitespace from the paragraph, so check trimmed
const trimmedLine = currentLine.trim();
let deleteLen = 0;
let format = '';
if (trimmedLine === '#') {
format = 'H1';
deleteLen = 1;
}
else if (trimmedLine === '##') {
format = 'H2';
deleteLen = 2;
}
else if (trimmedLine === '###') {
format = 'H3';
deleteLen = 3;
}
else if (trimmedLine === '-') {
format = 'ul';
deleteLen = 1;
}
else if (trimmedLine === '1.') {
format = 'ol';
deleteLen = 2;
}
else if (trimmedLine === '[]') {
// Delete the "[]" prefix first, then apply checklist
if (anchorNode instanceof Text && offset >= 2) {
anchorNode.deleteData(offset - 2, 2);
}
// Re-establish selection after deletion
const newRange = document.createRange();
newRange.setStart(anchorNode, Math.max(0, offset - 2));
newRange.collapse(true);
sel.removeAllRanges();
sel.addRange(newRange);
applyChecklistItem(editor, sel);
return true;
}
else if (trimmedLine === '>') {
format = 'BLOCKQUOTE';
deleteLen = 1;
}
if (format) {
// Select and delete the prefix
const newRange = document.createRange();
newRange.setStart(anchorNode, offset - deleteLen);
newRange.setEnd(anchorNode, offset);
sel.removeAllRanges();
sel.addRange(newRange);
document.execCommand('delete', false);
// Re-establish selection at the start of the text node
const restoreRange = document.createRange();
if (anchorNode.parentNode) {
restoreRange.setStart(anchorNode, 0);
}
else {
restoreRange.setStart(editor, 0);
}
restoreRange.collapse(true);
sel.removeAllRanges();
sel.addRange(restoreRange);
editor.focus();
applyBlockFormat(format, editor, sel);
return true;
}
return false;
}
function deleteSelectedText(textNode, start, len) {
if (textNode instanceof Text && start >= 0 && start + len <= textNode.length) {
textNode.deleteData(start, len);
}
}
function applyBlockFormat(format, editor, sel) {
// Apply the format
if (format === 'ul') {
document.execCommand('insertUnorderedList', false);
}
else if (format === 'ol') {
document.execCommand('insertOrderedList', false);
}
else {
document.execCommand('formatBlock', false, format);
}
// Re-focus editor
editor.focus();
}
function applyChecklistItem(editor, sel) {
// Get the current block element
let range;
try {
range = sel.getRangeAt(0);
}
catch {
// Selection is invalid, create a new one at the start of editor
range = document.createRange();
range.setStart(editor, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
let block = findBlockElement(range.startContainer);
if (!block) {
// Create a paragraph first
document.execCommand('formatBlock', false, 'P');
try {
range = sel.getRangeAt(0);
}
catch {
range = document.createRange();
range.setStart(editor, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
block = findBlockElement(range.startContainer);
}
if (!block)
return;
// Convert to list item with checklist
const parentList = findListParent(block);
if (parentList) {
// Already in a list, convert to checklist
convertListToChecklist(parentList);
}
else {
// Create a new ul and move the block into it
const ul = document.createElement('ul');
const li = document.createElement('li');
li.className = 'checklist-item';
// Add checkbox
const checkbox = document.createElement('span');
checkbox.className = 'checklist-checkbox';
checkbox.addEventListener('click', (e) => {
e.stopPropagation();
checkbox.classList.toggle('checked');
li.classList.toggle('checked-text');
saveChecklistState();
});
li.appendChild(checkbox);
// Move block content into li
while (block.firstChild) {
li.appendChild(block.firstChild);
}
ul.appendChild(li);
// Replace the block with the ul
const parent = block.parentNode;
if (parent) {
parent.replaceChild(ul, block);
}
else {
// If no parent, append to editor
editor.appendChild(ul);
}
// Restore cursor
const newRange = document.createRange();
newRange.selectNodeContents(li);
newRange.collapse(false);
sel.removeAllRanges();
sel.addRange(newRange);
}
}
function convertListToChecklist(ul) {
const items = ul.querySelectorAll('li');
items.forEach(li => {
if (!li.classList.contains('checklist-item')) {
li.className = 'checklist-item';
const checkbox = document.createElement('span');
checkbox.className = 'checklist-checkbox';
checkbox.addEventListener('click', (e) => {
e.stopPropagation();
checkbox.classList.toggle('checked');
li.classList.toggle('checked-text');
saveChecklistState();
});
li.insertBefore(checkbox, li.firstChild);
}
});
}
function saveChecklistState() {
const state = (0, model_1.getState)();
if (!state.activeNoteId)
return;
const content = (0, renderer_1.getEditorContent)();
(0, model_1.updateNote)(state.activeNoteId, { content });
scheduleSave();
}
// ── Toolbar command handler ────────────────────────────────────────────
function handleToolbarCommand(cmd) {
const editor = document.getElementById('editor-content');
editor.focus();
switch (cmd) {
case 'bold':
document.execCommand('bold', false);
break;
case 'italic':
document.execCommand('italic', false);
break;
case 'underline':
document.execCommand('underline', false);
break;
case 'strikethrough':
document.execCommand('strikeThrough', false);
break;
case 'code':
insertInlineCode();
break;
case 'h1':
document.execCommand('formatBlock', false, 'H1');
break;
case 'h2':
document.execCommand('formatBlock', false, 'H2');
break;
case 'h3':
document.execCommand('formatBlock', false, 'H3');
break;
case 'ul':
document.execCommand('insertUnorderedList', false);
break;
case 'ol':
document.execCommand('insertOrderedList', false);
break;
case 'checklist':
insertChecklistItem();
break;
case 'blockquote':
document.execCommand('formatBlock', false, 'BLOCKQUOTE');
break;
case 'hr':
document.execCommand('insertHorizontalRule', false);
break;
case 'undo':
document.execCommand('undo', false);
break;
case 'redo':
document.execCommand('redo', false);
break;
}
(0, renderer_1.updateToolbarState)();
// Save after command
const state = (0, model_1.getState)();
if (state.activeNoteId) {
const content = (0, renderer_1.getEditorContent)();
(0, model_1.updateNote)(state.activeNoteId, { content });
scheduleSave();
}
}
function insertInlineCode() {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
const range = sel.getRangeAt(0);
const selectedText = range.toString();
if (selectedText) {
// Wrap selection in code tags
const code = document.createElement('code');
code.textContent = selectedText;
range.deleteContents();
range.insertNode(code);
range.setStartAfter(code);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
else {
// Insert code placeholder
document.execCommand('insertText', false, '`code`');
}
}
function insertChecklistItem() {
const sel = window.getSelection();
if (!sel || sel.rangeCount === 0)
return;
const editor = document.getElementById('editor-content');
applyChecklistItem(editor, sel);
}
// ── Tag management ─────────────────────────────────────────────────────
function addTagFromInput(input) {
const state = (0, model_1.getState)();
if (!state.activeNoteId)
return;
const tag = input.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
if (!tag)
return;
const note = (0, model_1.getNote)(state.activeNoteId);
if (!note)
return;
if (!note.tags.includes(tag)) {
(0, model_1.updateNoteTags)(state.activeNoteId, [...note.tags, tag]);
}
input.value = '';
}
function removeTag(tag) {
const state = (0, model_1.getState)();
if (!state.activeNoteId)
return;
const note = (0, model_1.getNote)(state.activeNoteId);
if (!note)
return;
(0, model_1.updateNoteTags)(state.activeNoteId, note.tags.filter(t => t !== tag));
}
function filterByTag(tag) {
const handler = window;
if (handler.__onTagClick)
handler.__onTagClick(tag);
}
// ── Load note content into editor ──────────────────────────────────────
function loadNoteContent(noteId) {
const note = (0, model_1.getNote)(noteId);
if (!note)
return;
lastContent = note.content;
(0, renderer_1.setEditorContent)(note.content);
// Re-bind checklist checkboxes
rebindCheckboxes();
}
function rebindCheckboxes() {
const editor = document.getElementById('editor-content');
const checkboxes = editor.querySelectorAll('.checklist-checkbox');
checkboxes.forEach(cb => {
// Remove old listeners by cloning
const newCb = cb.cloneNode(true);
cb.parentNode?.replaceChild(newCb, cb);
newCb.addEventListener('click', (e) => {
e.stopPropagation();
newCb.classList.toggle('checked');
const li = newCb.closest('li');
if (li)
li.classList.toggle('checked-text');
saveChecklistState();
});
});
}
// ── Clear editor ───────────────────────────────────────────────────────
function clearEditor() {
const editor = document.getElementById('editor-content');
editor.innerHTML = '';
lastContent = '';
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>