Notes
bonsai 27b · composer

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 41,039 bytes · SHA-256 22b7ab15e951
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notes — Beautiful Note Taking App</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: #f0f2f5; color: #1a1a2e; }
.app-wrapper { display: flex; min-height: 100vh; }
/* Sidebar */
.sidebar { width: 240px; background: #ffffff; border-right: 1px solid #e2e8f0; display: flex; flex-direction: column; position: relative; z-index: 10; }
.sidebar-header { padding: 16px 20px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: space-between; }
.logo { display: flex; align-items: center; gap: 10px; }
.logo-icon { font-size: 24px; }
.logo-text { font-size: 22px; font-weight: 700; color: #1a1a2e; }
.sidebar-actions { display: flex; gap: 8px; }
.sidebar-actions button { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.sidebar-header .btn-primary { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; }
.sidebar-header .btn-secondary { background: #f1f5f9; color: #334155; }
.sidebar-body { flex: 1; overflow-y: auto; padding: 12px 20px; }
.sidebar-footer { padding: 12px 20px; border-top: 1px solid #e2e8f0; }
/* Search */
.search-bar { display: flex; gap: 8px; padding: 12px 0; margin-bottom: 4px; }
.search-input-wrap { flex: 1; position: relative; }
.search-input-wrap svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #94a3b8; }
.search-input { width: 100%; height: 36px; padding: 10px 12px 10px 38px; border: 1px solid #d1d5db; border-radius: 10px; font-size: 14px; background: #ffffff; color: #1a1a2e; outline: none; transition: border-color 0.2s; }
.search-input:focus { border-color: #6366f1; }
.search-filters { display: flex; gap: 4px; }
.filter-btn { padding: 6px 12px; border: none; border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.2s; background: #f1f5f9; color: #64748b; }
.filter-btn.active { background: #6366f1; color: white; }
.filter-btn:hover:not(.active) { background: #e2e8f0; color: #334155; }
/* Note List */
.section-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.section-label { font-size: 13px; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.section-count { font-size: 13px; color: #94a3b8; }
.note-list { list-style: none; }
.note-list li { display: flex; align-items: flex-start; gap: 12px; padding: 10px 0; cursor: pointer; transition: background 0.15s; }
.note-list li:hover { background: #f1f5f9; }
.note-list li .note-color { width: 4px; height: 24px; background: #6366f1; border-radius: 2px; flex-shrink: 0; margin-top: 1px; }
.note-list li .note-info { flex: 1; min-width: 0; }
.note-list li .note-title { font-size: 14px; font-weight: 600; color: #1a1a2e; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.note-list li .note-preview { font-size: 13px; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.note-list li .note-actions { display: flex; align-items: center; gap: 4px; }
.note-list li .note-star { color: #f59e0b; }
.note-list li .note-date { font-size: 11px; color: #94a3b8; margin-left: auto; }
/* Main Area */
.main-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.editor-header { padding: 20px 24px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-shrink: 0; }
.editor-header .btn { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid #d1d5db; border-radius: 8px; background: #ffffff; color: #334155; cursor: pointer; font-size: 13px; transition: all 0.2s; }
.editor-header .btn:hover { background: #f1f5f9; border-color: #94a3b8; }
.editor-header .btn-star { border-color: #f59e0b; color: #f59e0b; }
.editor-header .btn-star:hover { background: #fef3c7; }
.editor-header .btn-star .btn-icon svg { fill: #f59e0b; stroke: #f59e0b; }
.editor-header .btn-danger { border-color: #dc2626; color: #dc2626; }
.editor-header .btn-danger:hover { background: #fee2e2; }
.editor-header .btn-icon { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
.editor-header .btn-icon svg { width: 18px; height: 18px; }
.header-content h1 { margin: 0; font-size: 28px; font-weight: 700; color: #1a1a2e; line-height: 1.2; }
.header-content { flex: 1; display: flex; flex-direction: column; justify-content: center; }
.note-actions { display: flex; align-items: center; gap: 8px; }
/* Editor Content */
.editor-content { flex: 1; overflow-y: auto; padding: 24px; position: relative; }
.editor-content .markdown { word-break: break-word; }
.editor-content h1 { font-size: 28px; font-weight: 700; margin-bottom: 16px; color: #1a1a2e; border-bottom: 2px solid #e2e8f0; padding-bottom: 8px; }
.editor-content h2 { font-size: 24px; font-weight: 600; margin: 24px 0 12px; color: #1a1a2e; }
.editor-content h3 { font-size: 20px; font-weight: 600; margin: 20px 0 12px; color: #1a1a2e; }
.editor-content h4 { font-size: 18px; font-weight: 600; margin: 16px 0 12px; color: #1a1a2e; }
.editor-content h5 { font-size: 16px; font-weight: 600; margin: 12px 0 8px; color: #1a1a2e; }
.editor-content h6 { font-size: 15px; font-weight: 600; margin: 10px 0 8px; color: #1a1a2e; }
.editor-content p { line-height: 1.7; margin-bottom: 12px; color: #334155; }
.editor-content blockquote { border-left: 4px solid #6366f1; padding: 0 16px; margin: 16px 0; color: #64748b; font-style: italic; }
.editor-content ul { margin: 12px 0; }
.editor-content ol { margin: 12px 0; }
.editor-content li { margin-bottom: 4px; }
.editor-content strong { font-weight: 600; }
.editor-content em { font-style: italic; }
.editor-content a { color: #6366f1; text-decoration: none; }
.editor-content a:hover { text-decoration: underline; }
.editor-content pre { background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; overflow-x: auto; margin: 12px 0; font-family: 'Fira Code', 'SF Mono', monospace; font-size: 13px; }
.editor-content code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
.editor-content table { border-collapse: collapse; margin: 12px 0; }
.editor-content th, .editor-content td { border: 1px solid #e2e8f0; padding: 8px 12px; text-align: left; }
.editor-content th { background: #f1f5f9; font-weight: 600; }
.editor-content tr:hover td { background: #f1f5f9; }
.editor-content .task { display: flex; align-items: center; gap: 8px; margin: 4px 0; cursor: pointer; }
.editor-content .task input[type="checkbox"] { width: 20px; height: 20px; accent-color: #6366f1; }
.editor-content .task:checked + span { text-decoration: line-through; color: #94a3b8; }
/* Footer */
.editor-footer { border-top: 1px solid #e2e8f0; padding: 12px 24px; background: #ffffff; display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
.editor-footer .btn { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid #d1d5db; border-radius: 8px; background: #ffffff; color: #334155; cursor: pointer; font-size: 13px; transition: all 0.2s; }
.editor-footer .btn-icon { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
.editor-footer .btn-icon svg { width: 18px; height: 18px; }
.editor-footer .btn:hover { background: #f1f5f9; border-color: #94a3b8; }
.editor-footer #footerSeparator { flex: 1; }
.editor-footer span { font-size: 13px; color: #64748b; }
.editor-footer #footerWordCount { font-weight: 600; color: #334155; }
/* Modal */
.modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 100; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); opacity: 0; pointer-events: none; transition: opacity 0.2s; }
.modal.hidden { display: none; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); }
.modal-dialog { background: #ffffff; border-radius: 16px; padding: 24px; width: 90%; max-width: 400px; transform: scale(0.9); transition: transform 0.2s; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); }
.modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.modal-title { font-size: 20px; font-weight: 700; color: #1a1a2e; }
.modal-body { margin-bottom: 20px; }
.modal-body p { margin-bottom: 8px; color: #334155; }
.modal-subtext { font-size: 13px; color: #64748b; }
.modal-footer { display: flex; gap: 8px; justify-content: flex-end; }
.modal .btn { padding: 10px 20px; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.modal .btn-close { background: #f1f5f9; color: #64748b; }
.modal .btn-close:hover { background: #e2e8f0; }
.modal .btn-primary { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; }
.modal .btn-secondary { background: #f1f5f9; color: #334155; }
.modal .btn-danger { background: #dc2626; color: white; }
.modal .btn:hover { opacity: 0.9; }
/* Form */
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 13px; font-weight: 600; color: #334155; margin-bottom: 6px; }
.form-input { width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 10px; font-size: 14px; background: #ffffff; color: #1a1a2e; outline: none; transition: border-color 0.2s; }
.form-input:focus { border-color: #6366f1; }
.color-picker { width: 36px; height: 36px; border-radius: 8px; border: 2px solid #d1d5db; cursor: pointer; }
/* Responsive */
@media (max-width: 768px) {
.sidebar { display: none; }
.main-area { flex-direction: column; }
}
</style>
</head>
<body>
<div id="app" class="app-wrapper">
<!-- Sidebar -->
<aside id="sidebar" class="sidebar" aria-label="Navigation">
<div class="sidebar-header">
<div class="logo">
<div class="logo-icon">✏</div>
<span class="logo-text">Notes</span>
</div>
<div class="sidebar-actions">
<button id="newNoteBtn" class="btn btn-primary" aria-label="Create new note">
<span class="btn-icon">+</span>
New Note
</button>
<button id="showSidebarBtn" class="btn btn-secondary btn-icon" aria-label="Toggle sidebar">
<span>☰</span>
</button>
</div>
</div>
<div class="sidebar-body">
<div class="search-bar">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input id="searchInput" type="text" placeholder="Search notes..." class="search-input" aria-label="Search notes">
</div>
<div class="search-filters">
<button id="filterAll" class="filter-btn active" data-filter="all">All</button>
<button id="filterStarred" class="filter-btn" data-filter="starred">★ Starred</button>
<button id="filterArchive" class="filter-btn" data-filter="archive">📁 Archive</button>
</div>
</div>
<div class="section-header">
<span class="section-label">Your Notes</span>
<span class="section-count" id="sectionCount">0 notes</span>
</div>
<ul id="noteList" class="note-list" role="list">
<!-- Notes rendered by JS -->
</ul>
</div>
<div class="sidebar-footer">
<div class="sidebar-footer-info">
<span class="footer-label">Storage</span>
<div class="footer-stats">
<div class="stat" data-stat="total">
<span class="stat-value" id="statTotal">0</span>
<span class="stat-unit">notes</span>
</div>
<div class="stat" data-stat="starred">
<span class="stat-value" id="statStarred">0</span>
<span class="stat-unit">starred</span>
</div>
</div>
</div>
</div>
</aside>
<!-- Main Editor Area -->
<div id="mainArea" class="main-area">
<div class="editor-header">
<button id="backBtn" class="btn btn-secondary btn-icon" aria-label="Go back">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
<div class="header-content">
<h1 id="noteTitle" class="note-title">Untitled Note</h1>
<div class="note-actions">
<button id="toggleStarBtn" class="btn btn-star" aria-label="Toggle star">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2.082m0 13.838A9 9 0 0122 11.918V22a2 2 0 11-4 0v-7.082c0-5.509-4.452-10.827-10-10.827S2.548 16.471 0 22v-7.082a9.003 9.003 0 0112-10.827z"></path>
</svg>
</button>
<button id="editNoteBtn" class="btn btn-secondary btn-icon" aria-label="Edit note">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7.082c0-5.509-4.452-10.827-10-10.827S2.548 16.471 0 22v-7.082a9.003 9.003 0 0112-10.827z"></path>
</svg>
</button>
<button id="deleteNoteBtn" class="btn btn-danger btn-icon" aria-label="Delete note">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 7l-.863.863A2 2 0 0016 12h-2v-6a2 2 0 002-2z"></path>
<path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</button>
</div>
</div>
</div>
<div id="editorContent" class="editor-content">
<!-- Content rendered by JS -->
</div>
<div class="editor-footer">
<div class="footer-bar">
<button id="insertListBtn" class="btn btn-secondary btn-icon" aria-label="Insert unordered list">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 19l-1-1 5.5-5.5 1 1-4.5 4.5M18 10l1 1-5.5 5.5-1-1 4.5-4.5"></path>
<path d="M9 19l-1-1 5.5-5.5 1 1-4.5 4.5M18 10l1 1-5.5 5.5-1-1 4.5-4.5"></path>
</svg>
</button>
<button id="insertHeadingBtn" class="btn btn-secondary btn-icon" aria-label="Insert heading">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="1" x2="12" y2="21"></line>
<line x1="5.5" y1="1" x2="5.5" y2="21"></line>
<line x1="12" y1="1" x2="12" y2="21"></line>
<line x1="18.5" y1="1" x2="18.5" y2="21"></line>
</svg>
</button>
<button id="insertImageBtn" class="btn btn-secondary btn-icon" aria-label="Insert image">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
<circle cx="8" cy="8" r="2"></circle>
<circle cx="16" cy="16" r="2"></circle>
<path d="M19 9H5m14 0v6M5 9v6"></path>
</svg>
</button>
<button id="insertCodeBtn" class="btn btn-secondary btn-icon" aria-label="Insert code block">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 10v4a2 2 0 002 2h8a2 2 0 002-2v-4M9 10l-1-1v4l-1 1M16 10l1-1v4l1-1"></path>
</svg>
</button>
<button id="insertTaskBtn" class="btn btn-secondary btn-icon" aria-label="Insert task">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="16.35" y2="11.65"></line>
<line x1="12" y1="16" x2="16.35" y2="19.35"></line>
</svg>
</button>
<button id="insertTableBtn" class="btn btn-secondary btn-icon" aria-label="Insert table">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
<line x1="9" y1="3" x2="9" y2="21"></line>
<line x1="15" y1="3" x2="15" y2="21"></line>
<line x1="3" y1="9" x2="21" y2="9"></line>
<line x1="3" y1="15" x2="21" y2="15"></line>
</svg>
</button>
<div id="footerSeparator"></div>
<span id="footerStatus">Ready</span>
<span id="footerWordCount">0 words</span>
</div>
</div>
</div>
<!-- Modal: New Note -->
<div id="newNoteModal" class="modal hidden">
<div class="modal-overlay" data-modal="newNoteModal"></div>
<div class="modal-dialog" role="dialog" aria-modal="true">
<div class="modal-header">
<span class="modal-title">Create New Note</span>
<button id="closeNewNoteModal" class="btn btn-close" aria-label="Close">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<form id="newNoteForm">
<div class="form-group">
<label for="newNoteTitle">Title</label>
<input id="newNoteTitle" type="text" placeholder="Enter note title..." class="form-input" required="">
</div>
<div class="form-group">
<label for="newNoteColor">Cover Color</label>
<input id="newNoteColor" type="color" class="form-input color-picker" value="#6366f1" style="width:36px;height:36px;border:none;height:36px;padding:0;">
</div>
<div class="form-group">
<label for="newNoteDescription">Description</label>
<textarea id="newNoteDescription" class="form-input" placeholder="Add a brief description..."></textarea>
</div>
</form>
<div class="modal-footer">
<button id="createNoteBtn" class="btn btn-primary">Create Note</button>
<button id="cancelNewNoteBtn" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Modal: Delete Confirmation -->
<div id="deleteModal" class="modal hidden">
<div class="modal-overlay" data-modal="deleteModal"></div>
<div class="modal-dialog" role="dialog" aria-modal="true">
<div class="modal-header">
<span class="modal-title">Delete Note</span>
<button id="closeDeleteModal" class="btn btn-close" aria-label="Close">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<p class="modal-message" id="deleteModalMessage">Are you sure you want to delete this note?</p>
<p class="modal-subtext" id="deleteModalSubtext">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button id="confirmDeleteBtn" class="btn btn-danger">Delete</button>
<button id="cancelDeleteBtn" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
</div>
<div id="app"></div>
<script>
'use strict';
// ── const: STORAGE_KEY ──
const STORAGE_KEY = 'notes_app_data';
// ── const: DEFAULT_NOTE ──
const DEFAULT_NOTE = {
id: null,
title: '',
content: '',
createdAt: Date.now(),
updatedAt: Date.now(),
isStarred: false,
isArchive: false,
color: '#6366f1'
};
// ── let: nextId ──
let nextId = 1;
// ── let: currentNoteId ──
let currentNoteId = null;
// ── let: notes ──
let notes = [];
// ── let: editingNote ──
let editingNote = null;
// ── function: loadNotes ──
function loadNotes() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
notes = JSON.parse(saved);
nextId = Math.max(...notes.map(n => n.id), 0) + 1;
}
} catch (e) {
console.error('Failed to load notes:', e);
}
}
// ── function: saveNotes ──
function saveNotes() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
} catch (e) {
console.error('Failed to save notes:', e);
}
}
// ── function: createNote ──
function createNote(note = DEFAULT_NOTE) {
const id = nextId++;
note.id = id;
note.createdAt = Date.now();
note.updatedAt = Date.now();
notes.push(note);
saveNotes();
return note;
}
// ── function: getNote ──
function getNote(id) {
return notes.find(n => n.id === id);
}
// ── function: updateNote ──
function updateNote(id, updates) {
const note = getNote(id);
if (note) {
Object.assign(note, updates, { updatedAt: Date.now() });
saveNotes();
return note;
}
return null;
}
// ── function: deleteNote ──
function deleteNote(id) {
notes = notes.filter(n => n.id !== id);
saveNotes();
}
// ── function: getFilteredNotes ──
function getFilteredNotes(filter = 'all') {
let result = [...notes];
if (filter === 'starred') {
result = result.filter(n => n.isStarred);
} else if (filter === 'archive') {
result = result.filter(n => n.isArchive);
}
return result;
}
// ── function: getSearchResults ──
function getSearchResults(query) {
if (!query) return getFilteredNotes();
const q = query.toLowerCase();
return notes.filter(n =>
n.title.toLowerCase().includes(q) || n.content.toLowerCase().includes(q)
);
}
// ── const: editor ──
// Simplified editor using contentEditable
const editor = {
init() {
const el = document.getElementById('editorContent');
el.innerHTML = '<p class="markdown">Start writing your first note here...</p>';
el.setAttribute('contenteditable', 'true');
el.setAttribute('spellcheck', 'false');
el.focus();
this.bindEvents(el);
},
bindEvents(el) {
el.addEventListener('keydown', (e) => this.handleInput(e));
el.addEventListener('input', () => this.save());
el.addEventListener('click', (e) => this.handleClick(e));
},
handleInput(e) {
if (e.key === 'Enter') {
if (!e.shiftKey) {
// Double enter creates new paragraph
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const parent = range.parentContainer;
const before = range.startContainer;
const after = range.endContainer;
if (before === after) {
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
if (before && before.classList.contains('markdown')) {
newDiv = document.createElement('div');
newDiv.className = 'markdown';
}
if (before && before.classList.contains('markdown')) {
before.parentNode.insertBefore(newDiv, after);
}
selection.setRange(newDiv, 0, 0);
range.collapse();
} else {
const newLine = document.createElement('div');
newLine.className = 'markdown';
if (before && before.classList.contains('markdown')) {
before.parentNode.insertBefore(newLine, after);
}
selection.setRange(newLine, 0, 0);
range.collapse();
}
}
}
},
handleClick(e) {
const target = e.target;
if (target.classList.contains('block')) {
editor.activeBlock = target;
}
},
save() {
if (currentNoteId) {
const el = document.getElementById('editorContent');
const content = el.innerHTML.trim();
updateNote(currentNoteId, { content });
}
},
insertBlock(type) {
const el = document.getElementById('editorContent');
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const parent = range.parentContainer;
if (type === 'paragraph') {
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
range.setStart(newDiv, 0);
range.collapse();
} else if (type === 'list') {
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const li = document.createElement('li');
li.textContent = '';
newDiv.appendChild(li);
range.setStart(li, 0);
range.collapse();
} else if (type === 'heading') {
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const h1 = document.createElement('h1');
h1.textContent = 'New Heading';
newDiv.appendChild(h1);
range.setStart(h1, 0);
range.collapse();
} else if (type === 'image') {
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const img = document.createElement('img');
img.src = 'https://via.placeholder.com/400x200?text=Image';
img.alt = 'Image';
newDiv.appendChild(img);
range.setStart(img, 0);
range.collapse();
} else if (type === 'code') {
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const pre = document.createElement('pre');
pre.className = 'editor-content code-block';
const code = document.createElement('code');
code.textContent = 'console.log("Hello, World!");';
pre.appendChild(code);
newDiv.appendChild(pre);
range.setStart(code, 0);
range.collapse();
} else if (type === 'task') {
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const li = document.createElement('li');
li.className = 'editor-content task';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'task-checkbox';
li.appendChild(cb);
const span = document.createElement('span');
span.textContent = '';
li.appendChild(span);
newDiv.appendChild(li);
range.setStart(span, 0);
range.collapse();
} else if (type === 'table') {
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const table = document.createElement('table');
table.className = 'editor-content';
const tr = document.createElement('tr');
const th1 = document.createElement('th');
th1.textContent = 'Column 1';
const th2 = document.createElement('th');
th2.textContent = 'Column 2';
tr.appendChild(th1);
tr.appendChild(th2);
table.appendChild(tr);
newDiv.appendChild(table);
range.setStart(th1, 0);
range.collapse();
}
},
insertHeading(level) {
const el = document.getElementById('editorContent');
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const parent = range.parentContainer;
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const h = document.createElement('h' + Math.min(Math.max(level, 1), 6));
h.textContent = 'New Heading';
newDiv.appendChild(h);
range.setStart(h, 0);
range.collapse();
},
insertImage() {
const el = document.getElementById('editorContent');
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const parent = range.parentContainer;
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const img = document.createElement('img');
img.src = 'https://via.placeholder.com/400x200?text=Image';
img.alt = 'Image';
newDiv.appendChild(img);
range.setStart(img, 0);
range.collapse();
},
insertCode() {
const el = document.getElementById('editorContent');
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const parent = range.parentContainer;
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const pre = document.createElement('pre');
pre.className = 'editor-content code-block';
const code = document.createElement('code');
code.textContent = 'console.log("Hello, World!");';
pre.appendChild(code);
newDiv.appendChild(pre);
range.setStart(code, 0);
range.collapse();
},
insertTask() {
const el = document.getElementById('editorContent');
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const parent = range.parentContainer;
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const li = document.createElement('li');
li.className = 'editor-content task';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'task-checkbox';
li.appendChild(cb);
const span = document.createElement('span');
span.textContent = '';
li.appendChild(span);
newDiv.appendChild(li);
range.setStart(span, 0);
range.collapse();
},
insertTable() {
const el = document.getElementById('editorContent');
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const parent = range.parentContainer;
const newDiv = document.createElement('div');
newDiv.className = 'markdown';
parent.insertBefore(newDiv, range.startContainer);
const table = document.createElement('table');
table.className = 'editor-content';
const tr = document.createElement('tr');
const th1 = document.createElement('th');
th1.textContent = 'Column 1';
const th2 = document.createElement('th');
th2.textContent = 'Column 2';
tr.appendChild(th1);
tr.appendChild(th2);
table.appendChild(tr);
newDiv.appendChild(table);
range.setStart(th1, 0);
range.collapse();
}
};
// ── const: ui ──
const ui = {
// Sidebar
showNotes() {
const filtered = getSearchResults(document.getElementById('searchInput').value);
const list = document.getElementById('noteList');
const count = document.getElementById('sectionCount');
list.innerHTML = '';
filtered.forEach(note => {
const li = document.createElement('li');
li.className = 'note-item';
li.dataset.noteId = note.id;
li.innerHTML = `
<div class="note-color" style="background:${note.color}"></div>
<div class="note-info">
<div class="note-title">${this.escapeHtml(note.title || 'Untitled')}</div>
<div class="note-preview">${this.truncate(note.content, 50)}</div>
</div>
<div class="note-actions">
${note.isStarred ? '<svg class="note-star" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.082m0 13.838A9 9 0 0122 11.918V22a2 2 0 11-4 0v-7.082c0-5.509-4.452-10.827-10-10.827S2.548 16.471 0 22v-7.082a9.003 9.003 0 0112-10.827z"/></svg>' : ''}
<span class="note-date">${this.formatDate(note.updatedAt)}</span>
</div>
`;
li.addEventListener('click', () => this.openNote(note));
li.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showDeleteModal(note);
});
list.appendChild(li);
});
count.textContent = `${filtered.length} ${filtered.length === 1 ? 'note' : 'notes'}`;
updateStats();
},
openNote(note) {
currentNoteId = note.id;
const titleEl = document.getElementById('noteTitle');
const contentEl = document.getElementById('editorContent');
titleEl.textContent = note.title || 'Untitled';
editor.init();
// Set note title in the editor
editor.content = note.content;
editor.render();
},
showNewNoteModal() {
document.getElementById('newNoteModal').classList.remove('hidden');
document.getElementById('newNoteTitle').focus();
},
closeNewNoteModal() {
document.getElementById('newNoteModal').classList.add('hidden');
document.getElementById('newNoteTitle').value = '';
document.getElementById('newNoteColor').value = '#6366f1';
document.getElementById('newNoteDescription').value = '';
},
createNewNote() {
const title = document.getElementById('newNoteTitle').value.trim();
const color = document.getElementById('newNoteColor').value;
const desc = document.getElementById('newNoteDescription').value.trim();
const note = createNote({ title, color, content: desc || '' });
this.closeNewNoteModal();
this.openNote(note);
},
// Delete
showDeleteModal(note) {
document.getElementById('deleteModalMessage').textContent = `Delete "${this.escapeHtml(note.title || 'Untitled')}"?`;
document.getElementById('deleteModal').classList.remove('hidden');
},
closeDeleteModal() {
document.getElementById('deleteModal').classList.add('hidden');
},
deleteNote() {
if (currentNoteId) {
deleteNote(currentNoteId);
this.closeDeleteModal();
this.showNotes();
if (currentNoteId === editingNote.id) {
editor.init();
}
}
},
// Star
toggleStar(note) {
note.isStarred = !note.isStarred;
updateNote(note.id, { isStarred: note.isStarred });
this.showNotes();
},
// Stats
updateStats() {
const total = notes.length;
const starred = notes.filter(n => n.isStarred).length;
document.getElementById('statTotal').textContent = total;
document.getElementById('statStarred').textContent = starred;
},
// Utilities
formatDate(timestamp) {
const date = new Date(timestamp);
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return date.toLocaleDateString('en-US', options);
},
truncate(text, maxChars) {
if (text.length <= maxChars) return text;
return text.substring(0, maxChars - 1) + '…';
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// ── const: wiring ──
const wiring = {
init() {
this.bindAll();
this.initSidebar();
this.initMain();
this.initModals();
this.initKeyboard();
this.initResize();
},
bindAll() {
// Sidebar
document.getElementById('newNoteBtn').addEventListener('click', () => ui.showNewNoteModal());
document.getElementById('showSidebarBtn').addEventListener('click', () => {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('collapsed');
});
document.getElementById('filterAll').addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
document.getElementById('filterAll').classList.add('active');
ui.showNotes();
});
document.getElementById('filterStarred').addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
document.getElementById('filterStarred').classList.add('active');
ui.showNotes();
});
document.getElementById('filterArchive').addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
document.getElementById('filterArchive').classList.add('active');
ui.showNotes();
});
// Search
document.getElementById('searchInput').addEventListener('input', () => {
ui.showNotes();
});
// Main area
document.getElementById('backBtn').addEventListener('click', () => {
if (currentNoteId) {
const note = getNote(currentNoteId);
editor.init();
editor.content = note.content;
editor.render();
document.getElementById('noteTitle').textContent = note.title || 'Untitled';
}
});
document.getElementById('toggleStarBtn').addEventListener('click', () => {
if (currentNoteId) {
const note = getNote(currentNoteId);
ui.toggleStar(note);
}
});
document.getElementById('deleteNoteBtn').addEventListener('click', () => {
if (currentNoteId) {
ui.showDeleteModal(getNote(currentNoteId));
}
});
// Editor content buttons
document.getElementById('insertListBtn').addEventListener('click', () => editor.insertBlock('• '));
document.getElementById('insertHeadingBtn').addEventListener('click', () => editor.insertBlock('# Note'));
document.getElementById('insertImageBtn').addEventListener('click', () => editor.insertBlock(');
document.getElementById('insertCodeBtn').addEventListener('click', () => editor.insertBlock('```python\nconsole.log("Hello, World!");\n```'));
document.getElementById('insertTaskBtn').addEventListener('click', () => editor.insertBlock('- Task Item'));
document.getElementById('insertTableBtn').addEventListener('click', () => editor.insertBlock('| Column 1 | Column 2 |\n|---|---|\n| Data 1 | Data 2 |'));
},
initSidebar() {
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
const sidebar = document.getElementById('sidebar');
if (window.innerWidth < 768) {
sidebar.classList.add('collapsed');
}
}, 100);
});
},
initMain() {
if (!currentNoteId) {
const note = createNote({ title: 'Untitled Note', content: 'Start writing your first note here...' });
currentNoteId = note.id;
ui.openNote(note);
} else {
ui.showNotes();
}
},
initModals() {
document.getElementById('closeNewNoteModal').addEventListener('click', ui.closeNewNoteModal);
document.getElementById('cancelNewNoteBtn').addEventListener('click', ui.closeNewNoteModal);
document.getElementById('createNoteBtn').addEventListener('click', ui.createNewNote);
document.getElementById('closeDeleteModal').addEventListener('click', ui.closeDeleteModal);
document.getElementById('cancelDeleteBtn').addEventListener('click', ui.closeDeleteModal);
document.getElementById('confirmDeleteBtn').addEventListener('click', ui.deleteNote);
},
initKeyboard() {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal:not(.hidden)').forEach(m => m.classList.add('hidden'));
}
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
ui.showNewNoteModal();
}
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveNotes();
document.getElementById('footerStatus').textContent = 'Saved ✓';
setTimeout(() => { document.getElementById('footerStatus').textContent = 'Ready'; }, 1500);
}
});
},
initResize() {
window.addEventListener('resize', () => {
if (window.innerWidth < 768) {
document.getElementById('sidebar').classList.add('collapsed');
}
});
}
};
// ── main ──
document.addEventListener('DOMContentLoaded', () => {
loadNotes();
wiring.init();
});
</script>
</body>
</html>