<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Capture</title>
<style>
html, body { height:100%; margin:0; padding:0; font-family: Inter, Roboto, Arial, sans-serif; }
body { display:flex; flex-direction:column; background:#f8f9fa; padding:20px; box-sizing:border-box; }
#top-bar { position: fixed; top: 10px; right: 10px; z-index:1000; }
#mode-toggle { font-size:22px; background:none; border:none; cursor:pointer; }
#gpc-display { font-weight:bold; margin-bottom:12px; font-size:18px; }
#gpc-buttons {
display: flex;
align-items: center;
gap: 8px; /* spacing between buttons */
margin-bottom: 8px;
}
#gpc-buttons button {
font-size: 1em;
padding: 8px 12px;
cursor: pointer;
border-radius: 6px;
border: 1px solid #ccc;
background: #f9f9f9;
transition: background 0.2s ease;
}
#gpc-buttons button:hover {
background: #eee;
}
#gpc-buttons button.active { background:#111; color:#fff; border-color:#111; }
#editor-container { flex:1; display:flex; flex-direction:column; margin-bottom:60px; }
#editor, #preview {
flex:1; width:100%; box-sizing:border-box;
border:1px solid #ddd;
font-family:"Courier New", monospace; font-size:16px; line-height:1.4; background:#fff; overflow:auto; white-space: pre-wrap;
}
#editor {
display:block;
padding:20px;
border-radius:8px;
}
#preview {
display:none;
padding:20px;
margin-left: -20px;
margin-right: -20px;
width: calc(100% + 40px);
border-left: none;
border-right: none;
border-radius: 8px 8px 0 0;
}
#preview pre {
position: relative;
background: #f5f5f5;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
font-family: "Courier New", monospace;
}
#preview .copy-btn {
z-index: 10;
}
#floating-t-dropdown {
display: none;
position: fixed;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
background:#fff;
border:1px solid #ccc;
border-radius:8px;
box-shadow: 0 -8px 20px rgba(0,0,0,0.2);
z-index: 999999999;
min-width: 120px; /* same as H */
padding: 0; /* remove extra padding inside container */
}
#floating-t-dropdown button {
width: 100%;
padding: 8px 0; /* match H button vertical padding */
border: none;
background: none;
cursor: pointer;
font-size: 16px; /* same as H */
border-bottom: 1px solid #eee;
text-align: center;
}
#floating-t-dropdown button:last-child {
border-bottom: none;
}
#floating-t-dropdown.open { display: block; }
#floating-t-dropdown button:hover { background:#f0f0f0; }
#floating-table-dropdown {
display:none;
position:fixed;
bottom:60px;
left:50%;
transform:translateX(-50%);
background:#fff;
border:1px solid #ccc;
border-radius:8px;
box-shadow:0 -8px 20px rgba(0,0,0,0.2);
z-index:999999999;
min-width:160px;
}
#floating-table-dropdown button {
width:100%;
padding:12px;
border:none;
background:none;
cursor:pointer;
border-bottom:1px solid #eee;
}
#floating-table-dropdown button:last-child { border-bottom:none; }
#floating-table-dropdown.open { display:block; }
/* Highlight */
#editor-container .highlight {
background-color: yellow;
}
#preview .highlight {
background-color: yellow;
}
/* Colored highlights */
#preview .highlight.pink { background-color: #ffc0cb !important; }
#preview .highlight.orange { background-color: #ffdab9 !important; }
#preview .highlight.yellow { background-color: #ffff99 !important; }
#preview .highlight.green { background-color: #90ee90 !important; }
#preview .highlight.blue { background-color: #add8e6 !important; }
#preview .highlight.purple { background-color: #e6ccff !important; }
#preview .highlight.gray { background-color: #d3d3d3 !important; }
#preview .highlight.brown { background-color: #d2b48c !important; }
/* Table styling */
#preview table {
border-collapse: collapse;
width: 100%;
margin: 10px 0;
}
#preview th, #preview td {
border: 1px solid #999;
padding: 6px 10px;
text-align: left;
}
#preview th {
background-color: #f2f2f2;
}
#floating-highlight-dropdown {
display: none; position: fixed; bottom: 60px; left: 50%;
transform: translateX(-50%); background:#fff; border:1px solid #ccc;
border-radius:8px; box-shadow: 0 -8px 20px rgba(0,0,0,0.2);
z-index: 999999999; min-width: 120px;
}
#floating-highlight-dropdown button {
display: block; width:100%; text-align:center; padding:12px;
border:none; background:none; cursor:pointer; font-size:15px;
border-bottom: 1px solid #eee;
}
#floating-highlight-dropdown button:last-child { border-bottom: none; }
#floating-highlight-dropdown button:hover { background:#f0f0f0; }
#floating-highlight-dropdown.open { display: block; }
/* Tighter bullets and checkboxes */
#preview ul, #preview ol {
padding-left: 1.6em;
margin: 0.4em 0;
}
#preview li {
margin-bottom: 0.15em;
line-height: 1.35;
}
/* Draggable unchecked tasks */
#preview li.task-unchecked {
cursor: grab;
padding: 3px 5px;
border-radius: 5px;
transition: background 0.2s;
}
#preview li.task-unchecked:active {
cursor: grabbing;
}
#preview li.task-unchecked.dragging {
opacity: 0.7;
background: rgba(100, 149, 237, 0.4) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
z-index: 10;
}
#preview li.task-unchecked.drag-over {
background: rgba(100, 149, 237, 0.15);
border-top: 2px solid cornflowerblue;
}
/* GPC stamps (DOY, DOS, DOM, Heptad) in Times New Roman */
#preview .gpc-stamp {
font-family: "Times New Roman", Times, serif !important;
text-decoration: none !important;
}
/* Gregorian date in Helvetica */
#preview .gregorian-stamp {
font-family: Helvetica, Arial, sans-serif !important;
text-decoration: none !important;
}
/* Block underline globally */
#preview u, #preview em, #preview strong {
text-decoration: none !important;
}
#toolbar {
display:none;
position:fixed;
bottom:0;
left:0;
width:100%;
background:#eee;
padding:6px 0;
border-top:1px solid #ccc;
z-index:1000;
overflow-x:auto;
-webkit-overflow-scrolling:touch;
}
#toolbar-inner {
display:inline-flex;
gap:8px;
padding:0 10px;
padding-right:140px;
}
#toolbar button {
flex-shrink:0;
padding:8px 12px;
border-radius:6px;
border:1px solid #aaa;
cursor:pointer;
background:#fff;
font-size:16px;
}
/* Shared floating dropdown style */
#floating-h-dropdown, #floating-date-dropdown {
display: none;
position: fixed;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
background:#fff;
border:1px solid #ccc;
border-radius:8px;
box-shadow: 0 -8px 20px rgba(0,0,0,0.2);
z-index: 999999999;
min-width: 120px;
}
#floating-h-dropdown button, #floating-date-dropdown button {
display: block;
width:100%;
text-align:center;
padding:12px;
border:none;
background:none;
cursor:pointer;
font-size:15px;
border-bottom: 1px solid #eee;
}
#floating-h-dropdown button:last-child, #floating-date-dropdown button:last-child {
border-bottom: none;
}
#floating-h-dropdown button:hover, #floating-date-dropdown button:hover { background:#f0f0f0; }
#floating-h-dropdown.open, #floating-date-dropdown.open { display: block; }
/* Cursor pointer for headings */
#preview h1, #preview h2, #preview h3, #preview h4, #preview h5, #preview h6 {
cursor: pointer;
user-select: none;
position: relative;
}
/* Indent subheadings */
#preview h2 { margin-left: 1.2em; }
#preview h3 { margin-left: 2.4em; }
#preview h4 { margin-left: 3.6em; }
#preview h5 { margin-left: 4.8em; }
#preview h6 { margin-left: 6em; }
/* Hide nested content */
.collapsed + .collapsible-content {
display: none;
}
/* Arrow indicator for collapsible headings */
h1, h2, h3, h4, h5, h6 {
cursor: pointer;
user-select: none;
position: relative;
padding-left: 1.2em; /* space for the arrow */
}
h1::before,
h2::before,
h3::before,
h4::before,
h5::before,
h6::before {
content: '▶'; /* arrow symbol */
position: absolute;
left: 0;
transition: transform 0.2s ease;
font-size: 0.9em;
top: 0.05em; /* adjust vertical alignment */
}
h1.collapsed::before,
h2.collapsed::before,
h3.collapsed::before,
h4.collapsed::before,
h5.collapsed::before,
h6.collapsed::before {
transform: rotate(0deg); /* point right when collapsed */
}
h1:not(.collapsed)::before,
h2:not(.collapsed)::before,
h3:not(.collapsed)::before,
h4:not(.collapsed)::before,
h5:not(.collapsed)::before,
h6:not(.collapsed)::before {
transform: rotate(90deg); /* point down when expanded */
}
/* Zen backlinks - subtle underline + hover */
#preview .backlinks {
margin: 2em 0 1em 0;
padding: 0.5em 0;
border-top: 1px solid #e5e5e5;
}
#preview .backlinks h4 {
margin: 0 0 0.8em 0;
font-size: 1em;
color: #666;
font-weight: 400;
}
#preview .backlinks ul {
margin: 0;
padding-left: 0;
list-style: none;
}
#preview .backlinks li {
margin: 0.4em 0;
}
#preview .backlinks a.backlink {
color: #1a73e8; /* Subtle blue */
text-decoration: none; /* No underline */
font-weight: 500;
padding: 0.1em 0.3em;
border-radius: 3px;
border-bottom: 1px solid transparent; /* ← CLICKABLE CUE */
transition: all 0.15s ease;
}
#preview .backlinks a.backlink:hover {
background: rgba(26,115,232,0.08);
border-bottom-color: #1a73e8;
transform: translateX(1px);
}
/* Sidebar slides over content, invisible when closed */
#sidebar {
width: 250px;
background: #f0f0f0;
position: fixed; /* float over content */
top: 0; /* all the way to the top now */
bottom: 0;
left: -250px; /* completely off-screen when closed */
padding: 1em 1em 1em 1em; /* optional: top/right/bottom/left padding */
overflow-y: auto;
z-index: 100; /* above content when open */
box-shadow: 2px 0 6px rgba(0,0,0,0.2);
transition: left 0.25s ease;
pointer-events: none; /* ignore clicks when closed */
opacity: 0; /* invisible when closed */
}
/* Sidebar open */
#sidebar.open {
left: 0;
pointer-events: auto; /* interactable */
opacity: 1; /* visible */
}
/* Main content stays full width */
#main-content {
flex: 1;
display: flex;
flex-direction: column;
}
/* Sidebar items styling */
#sidebar .sidebar-item {
font-size: 1.2em; /* bigger text */
padding: 14px 20px; /* bigger clickable area */
margin-bottom: 12px; /* spacing between buttons */
border-radius: 8px; /* subtle rounded corners */
cursor: pointer;
transition: background 0.2s ease, transform 0.1s ease;
user-select: none; /* prevent text selection on click */
}
/* Hover effect */
#sidebar .sidebar-item:hover {
background: #e0e0e0; /* subtle hover background */
transform: translateX(2px); /* slight movement for feel */
}
/* Active / focused state */
#sidebar .sidebar-item.active {
background: #d0d0d0;
font-weight: 600;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #ddd;
background: #f8f8f8;
}
.sidebar-header h3 {
margin: 0;
font-size: 1.1em;
color: #333;
}
/* Recent notes container */
#recent-list {
margin-top: 10px;
max-height: 350px; /* scroll if too many */
overflow-y: auto;
padding-left: 0;
font-family: 'Inter', 'Roboto', sans-serif;
}
/* Each note item */
#recent-list .note-item {
position: relative;
padding: 10px 14px 10px 24px; /* space for bullet */
margin: 4px 0;
border-radius: 8px;
cursor: pointer;
font-size: 1em; /* slightly bigger */
color: #1f2937; /* dark gray */
background-color: #f9fafb; /* very light gray */
transition: background 0.25s, transform 0.15s;
}
/* Zen-style bullet */
#recent-list .note-item::before {
content: "•"; /* simple bullet */
position: absolute;
left: 10px;
color: #6366f1; /* soft purple/blue */
font-size: 1.2em;
line-height: 1;
}
/* Hover effect */
#recent-list .note-item:hover {
background-color: #eef2ff; /* light lavender */
transform: translateX(2px);
}
/* Active note highlight */
#recent-list .note-item.active {
background-color: #dbeafe; /* soft blue */
color: #1e40af;
font-weight: 500;
}
/* Scrollbar subtle */
#recent-list::-webkit-scrollbar {
width: 5px;
}
#recent-list::-webkit-scrollbar-thumb {
background-color: #c7d2fe;
border-radius: 3px;
}
.note-item.active {
background: #ffeb3b !important;
font-weight: bold;
}
/* ===== Sidebar note items ===== */
.note-item {
display: flex;
justify-content: space-between; /* title left, trash right */
align-items: center;
padding: 4px 8px;
cursor: pointer;
border-bottom: 1px solid #e5e5e5; /* optional separator */
transition: background 0.2s;
}
/* Note title: truncate if too long */
.note-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1; /* take all available space */
margin-right: 8px; /* spacing between title and trash */
}
/* Trash icon */
.note-item .delete-note {
cursor: pointer;
color: #c00; /* dark red */
font-size: 0.9em; /* slightly smaller */
user-select: none; /* prevent accidental selection */
margin-left: 8px;
}
/* Hover effects */
.note-item:hover {
background: #f0f0f0; /* subtle background hover */
}
.note-item:hover .delete-note {
color: #f00; /* bright red on hover */
}
/* Common flex layout for all note & folder items */
.note-item {
display: flex;
align-items: center; /* vertically center everything */
justify-content: space-between; /* title left, icons right */
padding: 4px 8px;
gap: 6px;
cursor: pointer;
}
/* Note title / folder name */
.note-item .title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Icons (pencil, trash) */
.note-item .icons {
display: flex;
align-items: center;
gap: 4px;
}
/* Optional: small size for icons */
.note-item .icons span {
font-size: 0.9em;
cursor: pointer;
}
</style>
</head>
<body>
</script>
<div id="top-bar">
<button id="mode-toggle" title="View / Edit">✏️</button>
</div>
<div id="gpc-display"></div>
<div id="gpc-buttons">
<button data-mode="DOY" class="active">DOY</button>
<button data-mode="DOS">DOS</button>
<button data-mode="DOM">DOM</button>
<button data-mode="HEPTAD">Heptad</button>
<button id="sidebar-toggle" title="Toggle Sidebar">📁</button> <!-- moved here -->
</div>
<div id="editor-container">
<textarea id="editor" placeholder="Capture…"></textarea>
<div id="preview"></div>
</div>
<!-- Keep your other items if you want, or move them below -->
<div id="sidebar">
<div class="sidebar-section">
<div class="sidebar-item">↔ Forward / Back</div>
<div class="sidebar-item">🔍 Search</div>
<!-- Recent button -->
<div class="sidebar-item" id="recent-btn">🗒️ Notes</div>
<div id="recent-list" style="margin-top:8px;"></div>
<div class="sidebar-item">⚙ System</div>
</div>
</div>
<div id="toolbar">
<div id="toolbar-inner">
<button data-action="undo">↶</button>
<button data-action="redo">↷</button>
<button id="t-button">T ▼</button>
<button data-action="highlight">🖍</button>
<button id="highlight-button">🎨 ▼</button>
<button data-action="quote">❝</button> <!-- Quote block -->
<button data-action="code">📋</button> <!-- Inline code / backtick -->
<button id="h-button">H ▼</button>
<button id="date-button">📅 ▼</button>
<button id="table-button">▦ ▼</button>
<button data-action="ul">•</button>
<button data-action="ol">1.</button>
<button data-action="checkbox">☑</button>
<button data-action="link">🔗</button>
<button data-action="wikilink">📄</button>
<button id="comment-btn">💬</button>
</div>
</div>
<!-- H Dropdown -->
<div id="floating-h-dropdown">
<button data-action="h1">H1</button>
<button data-action="h2">H2</button>
<button data-action="h3">H3</button>
<button data-action="h4">H4</button>
<button data-action="h5">H5</button>
<button data-action="h6">H6</button>
</div>
<!-- Text Formatting Dropdown -->
<div id="floating-t-dropdown">
<button data-action="bold"><b>B</b></button>
<button data-action="italic"><i>I</i></button>
<button data-action="underline"><u>U</u></button>
<button data-action="strike">S</button>
<button data-action="sub">X₂</button>
<button data-action="sup">X²</button>
</div>
<!-- Date Stamp Dropdown -->
<div id="floating-date-dropdown">
<button data-stamp="doy">DOY</button>
<button data-stamp="dos">DOS</button>
<button data-stamp="dom">DOM</button>
<button data-stamp="heptad">Heptad</button>
<button data-stamp="gregorian">Civil Date</button>
</div>
<div id="floating-table-dropdown">
<button data-action="add-table">Add table</button>
<button data-action="add-row">+ Row</button>
<button data-action="del-row">- Row</button>
<button data-action="add-col">+ Col</button>
<button data-action="del-col">- Col</button>
<button data-action="add-calc">🧮 Calculator</button>
<button data-action="row-up">Row ↑</button>
<button data-action="row-down">Row ↓</button>
<button data-action="col-left">Col ←</button>
<button data-action="col-right">Col →</button>
</div>
<div id="floating-highlight-dropdown">
<button data-color="pink">pink</button>
<button data-color="orange">orange</button>
<button data-color="yellow">yellow</button>
<button data-color="green">green</button>
<button data-color="blue">blue</button>
<button data-color="purple">purple</button>
<button data-color="gray">gray</button>
<button data-color="brown">brown</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
// Safety guard: make sure VaultDB gets defined before we use it
if (typeof VaultDB === 'undefined') {
console.error("VaultDB definition missing — check script order!");
}
</script>
<script>
/* =========================
VAULT (IndexedDB)
========================= */
const VaultDB = (() => {
const DB_NAME = "capture-vault";
const STORE = "notes";
const VERSION = 1;
let db = null; // Start as null (explicitly)
// Internal helper: wait until db is ready
async function waitForDb() {
if (db) return; // Already open → good
if (!openPromise) {
throw new Error("Database not initialized");
}
await openPromise; // Wait for open to finish
}
let openPromise = null; // Shared promise for opening
function open() {
if (openPromise) return openPromise; // Don't open twice
openPromise = new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, VERSION);
req.onupgradeneeded = e => {
const database = e.target.result;
if (!database.objectStoreNames.contains(STORE)) {
database.createObjectStore(STORE, { keyPath: "name" });
}
};
req.onsuccess = () => {
db = req.result;
resolve();
};
req.onerror = () => reject(req.error);
});
return openPromise;
}
async function get(name) {
await waitForDb(); // ← Safety: wait here
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, "readonly");
const store = tx.objectStore(STORE);
const req = store.get(name);
req.onsuccess = () => resolve(req.result?.content || null);
req.onerror = () => reject(req.error);
});
}
async function set(name, content) {
await waitForDb(); // ← Safety: wait here
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, "readwrite");
tx.objectStore(STORE).put({ name, content });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
async function list() {
await waitForDb(); // ← Safety: wait here
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE, "readonly");
const req = tx.objectStore(STORE).getAllKeys();
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
return { open, get, set, list };
})();
// === BASIC REFERENCES ===
const editor = document.querySelector("#editor");
const preview = document.querySelector("#preview");
let currentNote = null;
// === BACKLINKS SYSTEM ===
const backlinksMap = {}; // Stores which notes link to which
function updateBacklinks(currentNote, text) {
for (const note in backlinksMap) {
backlinksMap[note]?.delete(currentNote);
}
const matches = [...text.matchAll(/\[\[([^\|\]]+)(?:\|[^\]]+)?\]\]/g)];
matches.forEach(m => {
const linkedNote = m[1].trim();
if (!backlinksMap[linkedNote]) backlinksMap[linkedNote] = new Set();
backlinksMap[linkedNote].add(currentNote);
});
}
function renderBacklinks(note) {
const links = backlinksMap[note] ? Array.from(backlinksMap[note]) : [];
if (links.length === 0) return '';
return `
<div class="backlinks">
<h4>Backlinks</h4>
<ul>
${links.map(n => `<li><a href="#" class="backlink" data-note="${n}">${n}</a></li>`).join('')}
</ul>
</div>
`;
}
// === YOUR OTHER FUNCTIONS BELOW ===
</script>
<script>
document.addEventListener("DOMContentLoaded", function(){
console.log("DOM loaded");
(async () => {
try {
console.log("Opening vault...");
await VaultDB.open(); // ← this now returns a promise that waits
await SidebarManager.init();
console.log("Vault opened successfully");
let content = await VaultDB.get("Home");
if (!content) {
content = "# Home\n";
await VaultDB.set("Home", content);
}
editor.value = content;
pushHistory(true);
updatePreviewAndSave();
console.log("Vault ready:", await VaultDB.list());
} catch (err) {
console.error("Vault startup failed:", err);
}
})();
const editor = document.getElementById("editor");
const preview = document.getElementById("preview");
// === Stable Undo / Redo System ===
const history = [];
let historyIndex = -1;
const HISTORY_LIMIT = 100;
let suppressHistory = false;
function pushHistory(force = false) {
if (suppressHistory) return;
const value = editor.value;
if (!force && history[historyIndex] === value) return;
history.splice(historyIndex + 1);
history.push(value);
if (history.length > HISTORY_LIMIT) {
history.shift();
} else {
historyIndex++;
}
}
function undo() {
if (historyIndex <= 0) return;
suppressHistory = true;
historyIndex--;
editor.value = history[historyIndex];
suppressHistory = false;
updatePreviewAndSave();
}
function redo() {
if (historyIndex >= history.length - 1) return;
suppressHistory = true;
historyIndex++;
editor.value = history[historyIndex];
suppressHistory = false;
updatePreviewAndSave();
}
editor.addEventListener("input", () => {
pushHistory(); // typing
updatePreviewAndSave();
});
const toggleBtn = document.getElementById("mode-toggle");
const toolbar = document.getElementById("toolbar");
const hButton = document.getElementById("h-button");
const dateButton = document.getElementById("date-button");
const hDropdown = document.getElementById("floating-h-dropdown");
const dateDropdown = document.getElementById("floating-date-dropdown");
const tButton = document.getElementById("t-button");
const tDropdown = document.getElementById("floating-t-dropdown");
let editMode = true;
const highlightButton = document.getElementById("highlight-button");
const highlightDropdown = document.getElementById("floating-highlight-dropdown");
const tableButton = document.getElementById("table-button");
const tableDropdown = document.getElementById("floating-table-dropdown");
tableDropdown.addEventListener("mousedown", e => {
e.preventDefault(); // keeps editor selection inside the table
});
const tableNavActions = [
"row-up",
"row-down",
"col-left",
"col-right"
];
function closeAllDropdowns() {
hDropdown.classList.remove("open");
tDropdown.classList.remove("open");
dateDropdown.classList.remove("open");
highlightDropdown.classList.remove("open");
tableDropdown.classList.remove("open");
}
tableButton.addEventListener("click", e => {
e.stopPropagation();
hDropdown.classList.remove("open");
tDropdown.classList.remove("open");
dateDropdown.classList.remove("open");
highlightDropdown.classList.remove("open");
tableDropdown.classList.toggle("open");
});
tableDropdown.addEventListener("click", e => {
const btn = e.target.closest("button[data-action]");
if (!btn) return;
const action = btn.dataset.action;
({
"add-table": insertTable,
"add-row": addRow,
"del-row": deleteRow,
"add-col": addColumn,
"del-col": deleteColumn,
"add-calc": addCalculator,
"row-up": rowUp,
"row-down": rowDown,
"col-left": () => moveColumn("left"),
"col-right": () => moveColumn("right")
})[action]?.();
editor.focus();
updatePreviewAndSave();
tableDropdown.classList.remove("open");
});
let currentNote = "Home";
pushHistory(true);
const html = marked.parse(editor.value);
preview.innerHTML = renderWikilinks(html);
updatePreviewAndSave();
function updateMode() {
closeAllDropdowns(); // ← ADD THIS LINE
if (editMode) {
editor.style.display = "block";
preview.style.display = "none";
toolbar.style.display = "block";
toggleBtn.textContent = "✏️";
} else {
editor.style.display = "none";
preview.style.display = "block";
toolbar.style.display = "none";
toggleBtn.textContent = "👁";
}
}
updateMode();
toggleBtn.addEventListener("click",()=>{
editMode = !editMode;
updateMode();
});
preview.addEventListener("click", async (e) => {
const wikiLink = e.target.closest(".wikilink, .backlink");
if (!wikiLink) return;
e.preventDefault();
const name = wikiLink.dataset.note?.trim();
if (!name) return;
// Safety check: VaultDB must be ready before we try to use it
if (!VaultDB || typeof VaultDB.get !== 'function' || typeof VaultDB.set !== 'function') {
console.error("VaultDB not ready yet when clicking wikilink");
alert("Cannot switch note yet — storage is still loading. Wait 2–3 seconds and try again.");
return;
}
try {
// Save current note safely
if (currentNote) {
await VaultDB.set(currentNote, editor.value);
}
// Switch to new note
currentNote = name;
let content = await VaultDB.get(name);
if (content === null || content === undefined) {
content = `# ${name}\n\n`;
await VaultDB.set(name, content);
}
editor.value = content;
pushHistory(true);
updatePreviewAndSave();
} catch (err) {
console.error("Error during note switch:", err);
alert("Could not load the note — check console (F12) for details");
}
});
function updatePreviewAndSave() {
// 1️⃣ Start with the raw Markdown
const raw = editor.value;
// 2️⃣ Apply wikilinks
let html = renderWikilinks(raw);
// 3️⃣ Obsidian-style highlights (FIXED)
html = html.replace(/==(\w+):([\s\S]+?)==/g, '<span class="highlight $1">$2</span>');
html = html.replace(/==([\s\S]+?)==/g, '<span class="highlight">$1</span>');
// 4️⃣ Subscript / Superscript BEFORE Markdown
html = html.replace(/~([\s\S]+?)~/g, '<sub>$1</sub>');
html = html.replace(/\^([\s\S]+?)\^/g, '<sup>$1</sup>');
// 5️⃣ Parse Markdown
html = marked.parse(html);
// 6️⃣ GPC stamps → Times New Roman
const gpcPattern = /(🟠|🟤|🌺|⚫|🟢|🔵|🟣)\s\d{3}|Q\d \d{2}|\d{2}\.\d{2}|(♣️|♦️|♥️|♠️)\s*\d+/g;
html = html.replace(gpcPattern, '<span class="gpc-stamp">$&</span>');
// 7️⃣ Gregorian dates → Helvetica
const gregPattern = /\d{4}-\d{2}-\d{2}/g;
html = html.replace(gregPattern, '<span class="gregorian-stamp">$&</span>');
// 8️⃣ Update preview
// Update backlinks for current note
updateBacklinks(currentNote, raw);
// Show content + backlinks
preview.innerHTML = html + renderBacklinks(currentNote);
// Reapply collapsible headings
wrapHeadingsForCollapse();
function wrapHeadingsForCollapse() {
const headings = preview.querySelectorAll("h1, h2, h3, h4, h5, h6");
headings.forEach((h) => {
if (h.dataset.processed) return; // already processed
const level = parseInt(h.tagName.substring(1));
const contentWrapper = document.createElement("div");
contentWrapper.className = "collapsible-content";
// Move following siblings that are deeper or same nested
let next = h.nextElementSibling;
while (next) {
if (next.tagName && next.tagName.startsWith("H")) {
const nextLevel = parseInt(next.tagName.substring(1));
if (nextLevel <= level) break;
}
const temp = next.nextElementSibling;
contentWrapper.appendChild(next);
next = temp;
}
h.after(contentWrapper);
// Click toggle
h.addEventListener("click", () => {
h.classList.toggle("collapsed");
});
h.dataset.processed = true;
});
}
// ✅ FINAL ENHANCED TABLE CALCULATOR – Σ+ fully works, supports *, per-column ops, Remaining, smart formatting
preview.querySelectorAll("table").forEach((table) => {
const rows = Array.from(table.rows);
const lastRow = rows[rows.length - 1];
// Utility: format numbers (whole if possible)
function formatNumber(num) {
return Number.isInteger(num) ? num : +num.toFixed(1);
}
// 🔹 Handle Σ row
const labelCellText = lastRow.cells[0].innerText.trim();
const match = labelCellText.match(/^Σ([+\-*])?$/);
if (!match) return;
const opSymbol = match[1];
const globalOp = opSymbol || "+";
let startCol = 1;
let changeLabelToTotal = true;
if (opSymbol) {
startCol = 0;
changeLabelToTotal = false;
}
const perColumnOps = Array.from(lastRow.cells).slice(1).map(cell => {
const text = cell.innerText.trim();
return (text === "+" || text === "-" || text === "*") ? text : globalOp;
});
const fullOps = startCol === 0 ? [globalOp, ...perColumnOps] : perColumnOps;
// 🔹 Calculate Σ ignoring Remaining rows
let sigmaTotal = null;
for (let c = startCol; c < lastRow.cells.length; c++) {
const values = [];
for (let r = 1; r < rows.length - 1; r++) {
const rowLabel = rows[r].cells[0]?.innerText.trim().toLowerCase();
if (rowLabel === "remaining") continue;
const val = parseFloat(rows[r].cells[c]?.innerText) || 0;
values.push(val);
}
const op = fullOps[c - startCol];
let result;
if (op === "*") {
result = values.length > 0 ? values.reduce((a, b) => a * b, 1) : 1;
} else if (op === "-") {
if (values.length === 0) result = 0;
else if (values.length === 1) result = values[0];
else result = values.reduce((a, b) => a - b);
} else { // "+"
result = values.reduce((a, b) => a + b, 0);
}
lastRow.cells[c].innerHTML = `<strong style="color: green;">${formatNumber(result)}</strong>`;
if (sigmaTotal === null) sigmaTotal = result;
}
if (changeLabelToTotal) lastRow.cells[0].innerHTML = `<strong>total</strong>`;
// 🔹 Handle Remaining rows with formulas like "44-@Σ", "44+@Σ", "44* @Σ" or "44@Σ"
rows.forEach(row => {
const label = row.cells[0]?.innerText.trim().toLowerCase();
if (label === "remaining") {
for (let c = 1; c < row.cells.length; c++) {
const cell = row.cells[c];
if (!cell) continue;
const formula = cell.innerText.trim();
const formulaMatch = formula.match(/([\d.]+)\s*([\+\-\*]?)\s*@Σ/);
if (formulaMatch) {
const num = parseFloat(formulaMatch[1]);
const operator = formulaMatch[2] || "+";
let remaining;
if (operator === "+") remaining = num + sigmaTotal;
else if (operator === "-") remaining = num - sigmaTotal;
else if (operator === "*") remaining = num * sigmaTotal;
cell.innerHTML = `<strong style="color: green;">${formatNumber(remaining)}</strong>`;
}
}
}
});
});
// === STEP 2: Enable task list checkboxes ===
// Find marked-generated task checkboxes
const boxes = preview.querySelectorAll(
'input[type="checkbox"][disabled]'
);
let taskIndex = 0;
boxes.forEach(box => {
box.disabled = false;
box.dataset.taskIndex = taskIndex++;
});
// Sync checkbox changes back to markdown
boxes.forEach(box => {
box.addEventListener('change', () => {
const index = Number(box.dataset.taskIndex);
const lines = editor.value.split('\n');
let current = -1;
for (let i = 0; i < lines.length; i++) {
if (/^- \[[ xX]\]/.test(lines[i])) {
current++;
if (current === index) {
lines[i] = box.checked ?
lines[i].replace('- [ ]', '- [x]') :
lines[i].replace(/- \[[xX]\]/, '- [ ]');
break;
}
}
}
editor.value = lines.join('\n');
// 🔽 SAFE inline reorder (no new functions)
const reordered = editor.value.split('\n');
let out = [], open = [], done = [];
for (let l of reordered) {
if (/^\s*- \[[ xX]\]/.test(l)) {
(/\[[xX]\]/.test(l) ? done : open).push(l);
} else {
out.push(...open, ...done);
open = []; done = [];
out.push(l);
}
}
out.push(...open, ...done);
editor.value = out.join('\n');
updatePreviewAndSave();
});
});
// 🔑 RTL: Fix Hebrew text (SPARING REGULAR COLORS) - COMPLETE VERSION
const rtlClass = 'custom-rtl-hebrew';
const style = document.createElement('style');
style.textContent = `
.${rtlClass} {
direction: rtl !important;
text-align: right !important;
unicode-bidi: isolate !important;
}
.${rtlClass} * {
unicode-bidi: isolate;
}
/* Checkbox fix: keep checkbox on left even in RTL */
.${rtlClass} li {
display: flex;
flex-direction: row-reverse;
align-items: flex-start;
}
.${rtlClass} li input[type="checkbox"] {
order: 2;
margin-right: 0;
margin-left: 8px;
flex-shrink: 0;
}
`;
document.head.appendChild(style);
function applyRTL(el) {
const hasHebrew = /[-]/u.test(el.textContent || el.innerText);
if (hasHebrew) {
el.classList.add(rtlClass);
} else {
el.classList.remove(rtlClass);
}
}
function refreshRTL() {
const rtlElements = preview.querySelectorAll('p, li:not(.task-unchecked), td, th');
rtlElements.forEach(applyRTL);
}
// Initial application
refreshRTL();
// Targeted observer - ONLY checkbox lists
const checkboxLists = preview.querySelectorAll('ul, ol');
const observer = new MutationObserver((mutations) => {
let hasListChange = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && (
node.matches('li') ||
node.matches('ul, ol') ||
node.querySelector('li')
)) {
hasListChange = true;
}
});
}
});
if (hasListChange) {
// Small delay to let DOM settle after move
setTimeout(refreshRTL, 10);
}
});
// Observe only lists, not entire preview
checkboxLists.forEach(list => {
observer.observe(list, { childList: true, subtree: true });
});
// Safety net: clean up non-Hebrew items every 500ms
setInterval(() => {
preview.querySelectorAll('.custom-rtl-hebrew').forEach(el => {
if (!/[-]/u.test(el.textContent || el.innerText)) {
el.classList.remove('custom-rtl-hebrew');
}
});
}, 500);
// 9️⃣ Save raw Markdown to localStorage
VaultDB.set(currentNote, editor.value);
// 10️⃣ Add copy buttons for code blocks
preview.querySelectorAll('pre').forEach(block => {
// Avoid duplicating buttons
if (block.querySelector('.copy-btn')) return;
const btn = document.createElement('button');
btn.textContent = '📋';
btn.className = 'copy-btn';
btn.style.position = 'absolute';
btn.style.top = '4px';
btn.style.right = '4px';
btn.style.padding = '4px 6px';
btn.style.fontSize = '14px';
btn.style.cursor = 'pointer';
btn.style.border = 'none';
btn.style.background = '#eee';
btn.style.borderRadius = '4px';
btn.addEventListener('click', () => {
navigator.clipboard.writeText(block.innerText).then(() => {
btn.textContent = '✅';
setTimeout(() => btn.textContent = '📋', 1000);
});
});
block.style.position = 'relative';
block.appendChild(btn);
});
// === MAKE UNCHECKED TASKS DRAGGABLE IN VIEW MODE (FIXED - NO DUPLICATES) ===
const taskLis = preview.querySelectorAll('li');
taskLis.forEach(li => {
const checkbox = li.querySelector('input[type="checkbox"]');
if (checkbox && !checkbox.checked) {
li.classList.add('task-unchecked');
li.draggable = true;
} else {
li.classList.remove('task-unchecked');
li.draggable = false;
}
});
// Remove old listeners to prevent duplicates on every refresh
preview.querySelectorAll('li.task-unchecked').forEach(item => {
item.removeEventListener('dragstart', handleDragStart);
item.removeEventListener('dragover', handleDragOver);
item.removeEventListener('dragleave', handleDragLeave);
item.removeEventListener('dragend', handleDragEnd);
item.removeEventListener('drop', handleDrop);
});
// Shared dragged reference
let dragged = null;
// Event handlers (defined once)
function handleDragStart(e) {
dragged = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function handleDragOver(e) {
if (dragged && dragged !== this && dragged.parentNode === this.parentNode) {
e.preventDefault();
this.classList.add('drag-over');
}
}
function handleDragLeave() {
this.classList.remove('drag-over');
}
function handleDragEnd() {
if (dragged) dragged.classList.remove('dragging');
preview.querySelectorAll('li.drag-over').forEach(el => el.classList.remove('drag-over'));
}
function handleDrop(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (!dragged || dragged === this || dragged.parentNode !== this.parentNode) return;
const listContainer = this.parentNode;
const allUncheckedLis = Array.from(listContainer.querySelectorAll('li.task-unchecked'));
const fromIndex = allUncheckedLis.indexOf(dragged);
const toIndex = allUncheckedLis.indexOf(this);
// Move in DOM
if (fromIndex < toIndex) {
listContainer.insertBefore(dragged, this.nextSibling);
} else {
listContainer.insertBefore(dragged, this);
}
// === ACCURATELY REBUILD MARKDOWN FOR THIS LIST ONLY ===
const lines = editor.value.split('\n');
const newLines = [];
let currentTaskBlock = [];
let inTaskBlock = false;
for (const line of lines) {
if (/^\s*- \[[ xX]\] /.test(line)) {
if (!inTaskBlock) {
if (currentTaskBlock.length > 0) {
newLines.push(...currentTaskBlock);
currentTaskBlock = [];
}
inTaskBlock = true;
}
currentTaskBlock.push(line);
} else {
if (inTaskBlock) {
// Process the completed task block
const newOrder = Array.from(listContainer.children).map(li => {
const checkbox = li.querySelector('input[type="checkbox"]');
const isChecked = checkbox ? checkbox.checked : false;
const rawText = li.textContent.trim();
return currentTaskBlock.find(l => {
const checkedInMd = /\[[xX]\]/.test(l);
let taskText = l.replace(/^\s*- \[[ xX]\] /, '');
let normalized = taskText
.replace(/==(?:\w+:)?([\s\S]*?)==/g, '$1') // highlights (colored + plain)
.replace(/~([\s\S]*?)~/g, '$1') // subscript
.replace(/\^([\s\S]*?)\^/g, '$1') // superscript
.replace(/\*\*([\s\S]*?)\*\*/g, '$1') // bold **
.replace(/__([\s\S]*?)__/g, '$1') // bold __
.replace(/\*([\s\S]*?)\*/g, '$1') // italic *
.replace(/_([\s\S]*?)_/g, '$1') // italic _
.replace(/~~([\s\S]*?)~~/g, '$1') // strikethrough
.replace(/`([^`]*?)`/g, '$1') // inline code
.replace(/\[([^\]]*?)\]\([^\)]*?\)/g, '$1') // links [text](url)
.trim();
return checkedInMd === isChecked && normalized === rawText;
});
}).filter(Boolean);
newLines.push(...newOrder);
currentTaskBlock = [];
inTaskBlock = false;
}
newLines.push(line);
}
}
// Handle last block if file ends with tasks
if (currentTaskBlock.length > 0) {
const newOrder = Array.from(listContainer.children).map(li => {
const checkbox = li.querySelector('input[type="checkbox"]');
const isChecked = checkbox ? checkbox.checked : false;
const rawText = li.textContent.trim();
return currentTaskBlock.find(l => {
const checkedInMd = /\[[xX]\]/.test(l);
const textInMd = l.replace(/^\s*- \[[ xX]\] /, '').trim();
return checkedInMd === isChecked && textInMd === rawText;
});
}).filter(Boolean);
newLines.push(...newOrder);
}
editor.value = newLines.join('\n');
updatePreviewAndSave(); // recursive but safe — only refreshes once
}
// Attach fresh listeners
preview.querySelectorAll('li.task-unchecked').forEach(item => {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('dragleave', handleDragLeave);
item.addEventListener('dragend', handleDragEnd);
item.addEventListener('drop', handleDrop);
});
}
function renderWikilinks(text) {
return text.replace(
/\[\[([^\|\]]+)(?:\|([^\]]+))?\]\]/g,
(m, page, alias) => {
const note = page.trim();
const label = alias ? alias.trim() : note;
return `<a href="#" class="wikilink" data-note="${note}">${label}</a>`;
}
);
}
function insertTable() {
const tpl =
`| | |
|---|---|
| | |`;
const pos = editor.selectionStart;
editor.value =
editor.value.slice(0, pos) +
"\n" + tpl + "\n" +
editor.value.slice(pos);
editor.selectionStart = editor.selectionEnd = pos + tpl.length + 2;
}
// --- Table Utilities ---
function getTableAtCursor() {
const lines = editor.value.split("\n");
let row = editor.value.substr(0, editor.selectionStart).split("\n").length - 1;
// Find nearest table line
while (row >= 0 && !lines[row].includes("|")) row--;
if (row < 0) return null;
// Table start
let start = row;
while (start > 0 && lines[start - 1].includes("|")) start--;
// Table end
let end = row;
while (end < lines.length - 1 && lines[end + 1].includes("|")) end++;
return { lines, start, end };
}
function insertTable() {
const tpl = "| | |\n|---|---|\n| | |";
const pos = editor.selectionStart;
editor.value = editor.value.slice(0, pos) + "\n" + tpl + "\n" + editor.value.slice(pos);
editor.selectionStart = editor.selectionEnd = pos + 1;
updatePreviewAndSave();
}
function addRow() {
const t = getTableAtCursor();
if (!t) return;
// Find which row the cursor is currently on
const cursorLine = editor.value.substring(0, editor.selectionStart).split('\n').length - 1;
let insertAt = cursorLine; // default to current row
// If cursor is on or below the separator row, insert after current row
if (cursorLine <= t.start + 1) {
insertAt = t.start + 2; // insert as first data row (after header + separator)
}
const headerCols = t.lines[t.start].split("|").length - 2;
const newRow = "| " + Array(headerCols).fill(" ").join(" | ") + " |";
modifyTableWithCursor(() => {
t.lines.splice(insertAt + 1, 0, newRow); // insert AFTER the current row
editor.value = t.lines.join("\n");
updatePreviewAndSave();
});
}
function deleteRow() {
const t = getTableAtCursor();
if (!t) return;
if (t.end - t.start < 2) return; // prevent deleting header + separator
const cursorLine = editor.value.substring(0, editor.selectionStart).split('\n').length - 1;
// Don't allow deleting header or separator row
if (cursorLine <= t.start + 1) return;
modifyTableWithCursor(() => {
t.lines.splice(cursorLine, 1);
editor.value = t.lines.join("\n");
updatePreviewAndSave();
});
}
function addColumn() {
const t = getTableAtCursor();
if (!t) return;
// Determine which column the cursor is in
const beforeCursor = editor.value.substring(0, editor.selectionStart);
const currentLineIndex = beforeCursor.split('\n').length - 1;
const currentLine = t.lines[currentLineIndex];
const offsetInLine = editor.selectionStart - (beforeCursor.lastIndexOf('\n') + 1);
const colIndex = currentLine.substring(0, offsetInLine).split("|").length - 2;
modifyTableWithCursor(() => {
for (let i = t.start; i <= t.end; i++) {
let parts = t.lines[i].split("|");
const insertPos = colIndex + 1; // insert AFTER current column
if (i === t.start + 1) {
// Separator row
parts.splice(insertPos + 1, 0, " --- ");
} else {
// Normal row
parts.splice(insertPos + 1, 0, " ");
}
t.lines[i] = parts.join("|");
}
editor.value = t.lines.join("\n");
updatePreviewAndSave();
});
}
function deleteColumn() {
const t = getTableAtCursor();
if (!t) return;
const beforeCursor = editor.value.substring(0, editor.selectionStart);
const currentLineIndex = beforeCursor.split('\n').length - 1;
const currentLine = t.lines[currentLineIndex];
const offsetInLine = editor.selectionStart - (beforeCursor.lastIndexOf('\n') + 1);
const colIndex = currentLine.substring(0, offsetInLine).split("|").length - 2;
// Prevent deleting if only 1 data column left
const totalCols = t.lines[t.start].split("|").length - 2;
if (totalCols <= 1) return;
modifyTableWithCursor(() => {
for (let i = t.start; i <= t.end; i++) {
let parts = t.lines[i].split("|");
const deletePos = colIndex + 1; // +1 because split("|") gives empty at ends
parts.splice(deletePos, 1);
t.lines[i] = parts.join("|");
}
editor.value = t.lines.join("\n");
updatePreviewAndSave();
});
}
function rowUp() {
const t = getTableAtCursor();
if (!t) return;
const rowIndex = editor.value.substr(0, editor.selectionStart).split("\n").length - 1;
if (rowIndex <= t.start + 1) return;
modifyTableWithCursor(() => {
const lines = t.lines;
[lines[rowIndex - 1], lines[rowIndex]] = [lines[rowIndex], lines[rowIndex - 1]];
editor.value = lines.join("\n");
updatePreviewAndSave();
});
}
function rowDown() {
const t = getTableAtCursor();
if (!t) return;
const rowIndex = editor.value.substr(0, editor.selectionStart).split("\n").length - 1;
if (rowIndex >= t.end) return;
modifyTableWithCursor(() => {
const lines = t.lines;
[lines[rowIndex], lines[rowIndex + 1]] = [lines[rowIndex + 1], lines[rowIndex]];
editor.value = lines.join("\n");
updatePreviewAndSave();
});
}
function moveColumn(dir) {
const t = getTableAtCursor();
if (!t) return;
const beforeCursor = editor.value.substring(0, editor.selectionStart);
const lineIndex = beforeCursor.split("\n").length - 1;
const cursorInLine = editor.selectionStart - (beforeCursor.lastIndexOf("\n") + 1);
const line = t.lines[lineIndex];
const col = line.substring(0, cursorInLine).split("|").length - 2;
const target = dir === "left" ? col - 1 : col + 1;
if (target < 0) return;
modifyTableWithCursor(() => {
for (let i = t.start; i <= t.end; i++) {
const cells = t.lines[i].split("|");
if (!cells[col + 1] || !cells[target + 1]) continue;
[cells[col + 1], cells[target + 1]] = [cells[target + 1], cells[col + 1]];
t.lines[i] = cells.join("|");
}
editor.value = t.lines.join("\n");
updatePreviewAndSave();
});
}
function addCalculator() {
const t = getTableAtCursor();
if (!t) return;
const headerCols = t.lines[t.start].split("|").length - 2;
const ops = Array(headerCols).fill("+").join(" | ");
const calcRow = "| Σ | " + ops + " |";
modifyTableWithCursor(() => {
t.lines.splice(t.end + 1, 0, calcRow);
editor.value = t.lines.join("\n");
updatePreviewAndSave();
});
}
// ✅ In preview, sum/multiply columns if last row starts with Σ
function recalcTables() {
preview.querySelectorAll("table").forEach(table => {
const rows = Array.from(table.rows);
const lastRow = rows[rows.length - 1];
if (!lastRow.cells[0].innerText.trim().startsWith("Σ")) return;
const ops = Array.from(lastRow.cells).slice(1).map(td => td.innerText.trim());
for (let c = 1; c < ops.length + 1; c++) {
const values = [];
for (let r = 1; r < rows.length - 1; r++) {
const cellText = rows[r].cells[c]?.innerText.trim();
// Skip formula rows like "44-@Σ"
if (cellText.includes('@Σ')) return;
const val = parseFloat(cellText);
if (!isNaN(val)) values.push(val);
}
lastRow.cells[c].innerText = ops[c - 1] === "*" ? values.reduce((a, b) => a * b, 1) :
ops[c - 1] === "-" ? values.reduce((a, b) => a - b) :
values.reduce((a, b) => a + b, 0);
}
// === Handle @Σ references (e.g. 44-@Σ) ===
rows.forEach((row, rIndex) => {
if (rIndex === 0 || rIndex === rows.length - 1) return; // skip header & Σ row
const cell = row.cells[startCol];
if (!cell) return;
const text = cell.innerText.trim();
// Match: number - @Σ
const match = text.match(/^([\d.]+)\s*-\s*@Σ$/);
if (!match || sigmaTotal === null) return;
const base = parseFloat(match[1]);
if (isNaN(base)) return;
const remaining = base - sigmaTotal;
cell.innerHTML = `<strong style="color: green;">${remaining}</strong>`;
});
});
}
// Helper: Run table modifications while preserving cursor position
function modifyTableWithCursor(callback) {
const cursorPos = editor.selectionStart;
const linesBefore = editor.value.substring(0, cursorPos).split('\n');
const lineNumber = linesBefore.length - 1; // 0-based line index
const columnInLine = cursorPos - (linesBefore.join('\n').length + (lineNumber > 0 ? 1 : 0));
// Run the modification
callback();
// After update, try to restore cursor to same line and approximate column
const newLines = editor.value.split('\n');
if (lineNumber < newLines.length) {
const newLineStart = newLines.slice(0, lineNumber).join('\n').length + (lineNumber > 0 ? 1 : 0);
const targetPos = newLineStart + columnInLine;
const clampedPos = Math.min(targetPos, newLines[lineNumber].length + newLineStart);
editor.selectionStart = editor.selectionEnd = clampedPos;
} else {
// If lines were removed and cursor would be beyond end, go to end of document
editor.selectionStart = editor.selectionEnd = editor.value.length;
}
editor.focus();
}
// Auto-continue lists
editor.addEventListener("keydown", (e) => {
if (e.key !== "Enter" || e.shiftKey || e.ctrlKey || e.metaKey) return;
const start = editor.selectionStart;
const textBefore = editor.value.substring(0, start);
const lineStart = textBefore.lastIndexOf("\n") + 1;
const line = textBefore.substring(lineStart);
const ulMatch = line.match(/^(\s*)- /);
const olMatch = line.match(/^(\s*)\d+\. /);
const taskMatch = line.match(/^(\s*)- \[[ xX]\] /);
// 1. YOUR ORIGINAL SPECIAL EXIT for bullets/numbers with (
if ((ulMatch || olMatch) && line.trim().match(/^-\s*\( |^\d+\.\s* \)/)) {
e.preventDefault();
const prefixLen = ulMatch ? "- ".length : olMatch[0].length;
editor.value = editor.value.substring(0, lineStart) + editor.value.substring(lineStart + prefixLen);
editor.selectionStart = editor.selectionEnd = lineStart;
updatePreviewAndSave();
return;
}
// 2. TASK LIST: exit if the task is empty (nothing after checkbox)
if (taskMatch && line.substring(taskMatch[0].length).trim() === "") {
e.preventDefault();
editor.value = editor.value.substring(0, lineStart) + editor.value.substring(lineStart + taskMatch[0].length);
editor.selectionStart = editor.selectionEnd = lineStart;
updatePreviewAndSave();
return;
}
// 3. TASK LIST: continue with new blank task
if (taskMatch) {
e.preventDefault();
const indent = taskMatch[1];
editor.value = editor.value.substring(0, start) + "\n" + indent + "- [ ] " + editor.value.substring(start);
editor.selectionStart = editor.selectionEnd = start + indent.length + "- [ ] ".length + 1;
updatePreviewAndSave();
return;
}
// 4. YOUR ORIGINAL CONTINUE for normal bullets/numbers
if (ulMatch || olMatch) {
e.preventDefault();
const indent = ulMatch ? ulMatch[1] : olMatch[1];
const prefix = ulMatch ? "- " : "1. ";
editor.value = editor.value.substring(0, start) + "\n" + indent + prefix + editor.value.substring(start);
editor.selectionStart = editor.selectionEnd = start + indent.length + prefix.length + 1;
updatePreviewAndSave();
}
});
// Toolbar formatting
toolbar.addEventListener("click", (e) => {
let btn = e.target.closest("button[data-action]");
if (!btn || !btn.dataset.action || ["h-button", "date-button", "highlight-button", "t-button"].includes(btn.id)) return;
const action = btn.dataset.action;
e.preventDefault();
if (action === "undo") {
undo();
return;
}
if (action === "redo") {
redo();
return;
}
if (action === "checkbox") {
const cursor = editor.selectionStart;
// Find start of current line
let lineStart = editor.value.lastIndexOf('\n', cursor - 1) + 1;
if (lineStart < 0) lineStart = 0;
// Find end of current line
let lineEnd = editor.value.indexOf('\n', cursor);
if (lineEnd === -1) lineEnd = editor.value.length;
// Get current line text (with leading whitespace preserved)
let lineText = editor.value.substring(lineStart, lineEnd);
// Check if this line is already a task item (unchecked or checked)
const taskRegex = /^\s*- \[[ xX]\] /;
if (taskRegex.test(lineText)) {
// It's a task → remove the checkbox prefix (toggle off)
const newLine = lineText.replace(taskRegex, '');
editor.value =
editor.value.substring(0, lineStart) +
newLine +
editor.value.substring(lineEnd);
// Place cursor at end of the new plain text
editor.selectionStart = editor.selectionEnd = lineStart + newLine.length;
} else {
// Not a task → add checkbox at start
const trimmedLine = lineText.trimStart(); // remove leading whitespace for clean insert
const indent = lineText.substring(0, lineText.length - trimmedLine.length); // preserve indent
const newLine = indent + `- [ ] ${trimmedLine}`;
editor.value =
editor.value.substring(0, lineStart) +
newLine +
editor.value.substring(lineEnd);
// Place cursor right after the checkbox, ready to type
editor.selectionStart = editor.selectionEnd = lineStart + indent.length + `- [ ] `.length;
}
editor.focus();
updatePreviewAndSave();
return;
}
let start = editor.selectionStart;
let end = editor.selectionEnd;
const fullText = editor.value;
if (start === end && ['bold','italic','underline','link','wikilink'].includes(action)) {
const left = fullText.lastIndexOf(' ', start - 1) + 1;
const right = fullText.indexOf(' ', start);
start = left;
end = right === -1 ? fullText.length : right;
}
const selected = fullText.substring(start, end);
if (action === 'ul' || action === 'ol') {
const prefix = action === 'ul' ? '- ' : '1. ';
const lineStart = fullText.lastIndexOf('\n', start - 1) + 1;
const lineEnd = fullText.indexOf('\n', end);
const lineText = fullText.substring(lineStart, lineEnd === -1 ? undefined : lineEnd);
const newLine = lineText.startsWith(prefix) ? lineText.substring(prefix.length) : prefix + lineText;
editor.value = fullText.substring(0, lineStart) + newLine + fullText.substring(lineEnd === -1 ? fullText.length : lineEnd);
editor.selectionStart = editor.selectionEnd = lineStart + newLine.length;
editor.focus();
updatePreviewAndSave();
return;
}
let before = '', after = '';
switch(action) {
case 'bold': before = '**'; after = '**'; break;
case 'italic': before = '*'; after = '*'; break;
case 'underline': before = '<u>'; after = '</u>'; break;
case 'link': before = '['; after = '](https://)'; break;
case 'wikilink': before = '[['; after = ']]'; break;
case 'highlight': before = '=='; after = '=='; break;
case 'sub': before = '~'; after = '~'; break;
case 'sup': before = '^'; after = '^'; break;
case 'strike': before = '~~'; after = '~~'; break;
case 'quote':
{
// Add "> " at the start of the line
const lineStart = fullText.lastIndexOf('\n', start - 1) + 1;
const lineEnd = fullText.indexOf('\n', end);
const lineText = fullText.substring(lineStart, lineEnd === -1 ? undefined : lineEnd);
const newLine = lineText.startsWith('> ') ? lineText.substring(2) : '> ' + lineText;
editor.value = fullText.substring(0, lineStart) + newLine + fullText.substring(lineEnd === -1 ? fullText.length : lineEnd);
editor.selectionStart = editor.selectionEnd = lineStart + newLine.length;
editor.focus();
updatePreviewAndSave();
return;
}
case 'code':
{
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selected = editor.value.substring(start, end) || "code here";
const codeBlock = `\`\`\`\n${selected}\n\`\`\`\n`;
editor.value = editor.value.substring(0, start) + codeBlock + editor.value.substring(end);
editor.selectionStart = start + 4;
editor.selectionEnd = start + 4 + selected.length;
editor.focus();
updatePreviewAndSave();
return;
}
}
const isWrapped = selected.startsWith(before) && selected.endsWith(after);
const newSelected = isWrapped ? selected.slice(before.length, -after.length) : before + selected + after;
editor.value = fullText.substring(0, start) + newSelected + fullText.substring(end);
editor.selectionStart = editor.selectionEnd = start + (isWrapped ? 0 : before.length);
editor.focus();
updatePreviewAndSave();
});
// H dropdown
hButton.addEventListener('click', (e) => {
e.stopPropagation(); // prevent document click from closing immediately
// Close all other dropdowns
tDropdown.classList.remove('open');
highlightDropdown.classList.remove('open');
dateDropdown.classList.remove('open');
tableDropdown.classList.remove('open');
// Toggle H dropdown
hDropdown.classList.toggle('open');
});
tButton.addEventListener("click", (e) => {
e.stopPropagation();
hDropdown.classList.remove("open");
highlightDropdown.classList.remove("open");
dateDropdown.classList.remove("open");
tableDropdown.classList.remove("open"); // ← ADD THIS
tDropdown.classList.toggle("open");
});
// --- T dropdown formatting ---
tDropdown.addEventListener("click", (e) => {
const btn = e.target.closest("button[data-action]");
if (!btn) return;
const action = btn.dataset.action;
e.preventDefault();
e.stopPropagation();
const start = editor.selectionStart;
const end = editor.selectionEnd;
const fullText = editor.value;
const selected = fullText.substring(start, end);
let before = '', after = '';
switch(action) {
case 'bold': before='**'; after='**'; break;
case 'italic': before='*'; after='*'; break;
case 'underline': before='<u>'; after='</u>'; break;
case 'strike': before='~~'; after='~~'; break;
case 'sub': before='~'; after='~'; break;
case 'sup': before='^'; after='^'; break;
}
const isWrapped = selected.startsWith(before) && selected.endsWith(after);
const newSelected = isWrapped ? selected.slice(before.length,-after.length) : before + selected + after;
editor.value = fullText.substring(0,start) + newSelected + fullText.substring(end);
editor.selectionStart = editor.selectionEnd = start + (isWrapped ? 0 : before.length);
editor.focus();
updatePreviewAndSave();
tDropdown.classList.remove("open");
});
hDropdown.addEventListener('click', (e) => {
let btn = e.target.closest("button[data-action]");
if (!btn) return;
if (!btn.dataset.action.startsWith('h')) return;
e.preventDefault();
e.stopPropagation();
const level = btn.dataset.action.slice(1);
const prefix = '#'.repeat(level) + ' ';
const start = editor.selectionStart;
const fullText = editor.value;
const lineStart = fullText.lastIndexOf('\n', start - 1) + 1;
const lineEnd = fullText.indexOf('\n', start);
const lineText = fullText.substring(lineStart, lineEnd === -1 ? undefined : lineEnd);
const newLine = lineText.startsWith(prefix) ? lineText.substring(prefix.length) : prefix + lineText;
editor.value = fullText.substring(0, lineStart) + newLine + fullText.substring(lineEnd === -1 ? fullText.length : lineEnd);
editor.selectionStart = editor.selectionEnd = lineStart + newLine.length;
editor.focus();
updatePreviewAndSave();
hDropdown.classList.remove('open');
});
// Date Stamp dropdown - FIXED
dateButton.addEventListener('click', (e) => {
e.stopPropagation();
tDropdown.classList.remove('open');
hDropdown.classList.remove('open');
highlightDropdown.classList.remove('open');
tableDropdown.classList.remove('open'); // ← ADD THIS
dateDropdown.classList.toggle('open');
});
const commentBtn = document.getElementById("comment-btn");
commentBtn.addEventListener("click", () => {
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selectedText = editor.value.slice(start, end);
const commentText = `%%${selectedText}%%`;
// Insert commentText at cursor, replacing selection
editor.value = editor.value.slice(0, start) + commentText + editor.value.slice(end);
// Move cursor inside the %%
const cursorPos = start + 2; // between the %%
editor.selectionStart = editor.selectionEnd = cursorPos + selectedText.length;
editor.focus();
// Trigger preview update if you have one
updatePreviewAndSave();
});
// Colored highlight dropdown
highlightButton.addEventListener('click', (e) => {
e.stopPropagation();
tDropdown.classList.remove('open');
hDropdown.classList.remove('open');
dateDropdown.classList.remove('open');
tableDropdown.classList.remove('open'); // ← ADD THIS
highlightDropdown.classList.toggle('open');
});
highlightDropdown.addEventListener('click', (e) => {
const btn = e.target.closest("button[data-color]");
if (!btn) return;
const color = btn.dataset.color;
e.preventDefault(); e.stopPropagation();
const start = editor.selectionStart, end = editor.selectionEnd;
const fullText = editor.value;
let selected = fullText.substring(start, end) || "text";
const wrapper = `==${color}:${selected}==`;
editor.value = fullText.substring(0, start) + wrapper + fullText.substring(end);
editor.selectionStart = start + 3 + color.length + 1;
editor.selectionEnd = start + 3 + color.length + 1 + selected.length;
editor.focus();
updatePreviewAndSave();
highlightDropdown.classList.remove('open');
});
dateDropdown.addEventListener('click', (e) => {
let btn = e.target.closest("button[data-stamp]");
if (!btn) return;
const type = btn.dataset.stamp;
e.preventDefault();
e.stopPropagation();
const {doyToday} = calculateGPC();
const weekEmoji = weekEmojis[(doyToday-1)%7] || '';
let stamp = '';
switch (type) {
case 'doy':
stamp = (weekEmoji ? weekEmoji + ' ' : '') + doyToday;
if (heptadEnabled) {
const heptad = heptadMap[doyToday] || '';
if (heptad) stamp += ' ' + heptad;
}
break;
case 'dos':
const quarter = Math.floor((doyToday - 1) / 91) + 1;
const dayInSeason = ((doyToday - 1) % 91) + 1;
stamp = (weekEmoji ? weekEmoji + ' ' : '') + 'Q' + quarter + ' ' + String(dayInSeason).padStart(2, '0');
if (heptadEnabled) {
const heptad = heptadMap[doyToday] || '';
if (heptad) stamp += ' ' + heptad;
}
break;
case 'dom':
let remaining = doyToday;
let monthIndex = 0;
for (let i = 0; i < gpcMonths.length; i++) {
if (remaining <= gpcMonths[i].days) {
monthIndex = i;
break;
}
remaining -= gpcMonths[i].days;
}
const month = monthIndex + 1;
const day = remaining;
stamp = (weekEmoji ? weekEmoji + ' ' : '') + String(month).padStart(2, '0') + '.' + String(day).padStart(2, '0');
if (heptadEnabled) {
const heptad = heptadMap[doyToday] || '';
if (heptad) stamp += ' ' + heptad;
}
break;
case 'heptad':
// ALWAYS insert the current heptad, even if toggle is off
stamp = heptadMap[doyToday] || '';
break;
case 'gregorian':
const today = new Date();
const iso = today.toISOString().split('T')[0];
stamp = iso;
if (heptadEnabled) {
const heptad = heptadMap[doyToday] || '';
if (heptad) stamp += ' ' + heptad;
}
break;
}
if (stamp) {
const start = editor.selectionStart;
const end = editor.selectionEnd;
editor.value = editor.value.substring(0, start) + stamp + editor.value.substring(end);
editor.selectionStart = editor.selectionEnd = start + stamp.length;
editor.focus();
updatePreviewAndSave();
}
dateDropdown.classList.remove('open');
});
// Close dropdowns on outside click
document.addEventListener('click', (e) => {
if (!hDropdown.contains(e.target) && e.target !== hButton) {
hDropdown.classList.remove('open');
}
if (!dateDropdown.contains(e.target) && e.target !== dateButton) {
dateDropdown.classList.remove('open');
}
if (!tDropdown.contains(e.target) && e.target !== tButton) {
tDropdown.classList.remove('open');
}
if (!highlightDropdown.contains(e.target) && e.target !== highlightButton) {
highlightDropdown.classList.remove('open');
}
if (!tableDropdown.contains(e.target) && e.target !== tableButton) { // ← ADD THIS
tableDropdown.classList.remove('open');
}
});
// === YOUR FULL ORIGINAL GPC CALENDAR LOGIC - 100% UNTOUCHED ===
const gpcMonths = [
{ name: 'Unspring', days: 30 },{ name: 'Duspring', days: 30 },{ name: 'Trispring', days: 31 },
{ name: 'Quadsum', days: 30 },{ name: 'Fivesum', days: 30 },{ name: 'Sixsum', days: 31 },
{ name: 'Sepafall', days: 30 },{ name: 'Oktafall', days: 30 },{ name: 'Novafall', days: 31 },
{ name: 'Dekawint', days: 30 },{ name: 'Elvawint', days: 30 },{ name: 'Dozawint', days: 31 }
];
let baseMode = "DOY";
let heptadEnabled = false;
const gpcDisplayEl = document.getElementById("gpc-display");
const gpcBtns = document.querySelectorAll("#gpc-buttons button[data-mode]");
const epochDate = new Date(Date.UTC(1982,2,24));
const epochYear = 5979;
const weekEmojis = ['🟠','🟤','🌺','⚫','🟢','🔵','🟣'];
const heptadMap = {};
const suitEmojis = ['♣️','♦️','♥️','♠️'];
const baseStartDOYs = [
1,8,15,22,31,38,45,52,61,68,75,82,89,
92,99,106,113,122,129,136,143,152,159,166,173,180,
183,190,197,204,213,220,227,234,243,250,257,264,271,
274,281,288,295,304,311,318,325,334,341,348,355,362
];
let weekNum = 1;
for(let s=0;s<4;s++){
const suit = suitEmojis[s];
for(let w=0; w<13; w++){
let startDOY = baseStartDOYs[s*13 + w];
const isLongWeek = (w===3||w===7||w===11);
const daysInWeek = isLongWeek?9:7;
for(let d=0;d<daysInWeek;d++){
const doy=startDOY+d;
if(doy<=364) heptadMap[doy]=suit+' '+weekNum;
}
weekNum++;
}
weekNum=1;
}
function calculateGPC(){
const now=new Date();
const todayUTC=Date.UTC(now.getUTCFullYear(),now.getUTCMonth(),now.getUTCDate());
let remainingDays=Math.floor((todayUTC-epochDate.getTime())/86400000);
let gpcYear=epochYear;
while(true){
const rel=gpcYear-epochYear;
let leapDays=0;
if(rel%7===6) leapDays+=7;
if(rel%49===48) leapDays+=7;
if((rel+29)%70===0) leapDays+=7;
const yearLength=364+leapDays;
if(remainingDays<yearLength) break;
remainingDays-=yearLength;
gpcYear++;
}
const doyToday=Math.max(1,Math.min(364,remainingDays+1));
return {gpcYear,doyToday};
}
function updateGPC(){
const {doyToday}=calculateGPC();
const emoji=weekEmojis[(doyToday-1)%7]||'';
let mainText='';
if(baseMode==='DOY') mainText=String(doyToday).padStart(3,'0');
else if(baseMode==='DOM'){
let remaining=doyToday, monthIndex=0;
for(let i=0;i<gpcMonths.length;i++){
if(remaining<=gpcMonths[i].days){ monthIndex=i; break; }
remaining-=gpcMonths[i].days;
}
const month=monthIndex+1, day=remaining;
mainText=String(month).padStart(2,'0') + '.' + String(day).padStart(2,'0');
}
else if(baseMode==='DOS'){
const quarter=Math.floor((doyToday-1)/91)+1;
const dayInSeason=((doyToday-1)%91)+1;
mainText='Q'+quarter + ' ' + String(dayInSeason).padStart(2,'0');
}
let heptadText = heptadEnabled ? ' ' + (heptadMap[doyToday]||'') : '';
gpcDisplayEl.textContent = emoji + ' ' + mainText + heptadText;
}
updateGPC();
setInterval(updateGPC,60000);
gpcBtns.forEach(btn=>{
btn.addEventListener("click",()=>{
const mode=btn.dataset.mode;
if(mode==='HEPTAD'){
heptadEnabled=!heptadEnabled;
btn.classList.toggle('active',heptadEnabled);
} else {
baseMode=mode;
gpcBtns.forEach(b=>{
if(b.dataset.mode!=='HEPTAD') b.classList.toggle('active',b===btn);
});
}
updateGPC();
});
});
});
// ===== PERFECT HOMEPROOF SIDEBAR (FIXED) =====
document.addEventListener('DOMContentLoaded', () => {
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebar-toggle');
const HOME_NOTE = "Home";
if (!sidebar || !sidebarToggle) {
console.error("❌ Sidebar elements missing");
return;
}
const recentBtn = document.getElementById('recent-btn') || (() => {
const btn = document.createElement('div');
btn.id = 'recent-btn';
btn.className = 'sidebar-item';
btn.textContent = '🕘 Recent';
sidebar.appendChild(btn);
return btn;
})();
const recentList = document.getElementById('recent-list') || (() => {
const list = document.createElement('div');
list.id = 'recent-list';
list.style.marginTop = '8px';
sidebar.appendChild(list);
return list;
})();
let recentVisible = false;
let lastRequestedNote = null;
let noteCache = {};
// ===== VaultDB tombstone delete helper =====
async function vaultDelete(noteName) {
// Set to null to "delete" in your VaultDB
return VaultDB.set(noteName, null);
}
// ===== Sidebar toggle =====
sidebarToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
});
// ===== Recent toggle =====
recentBtn.addEventListener('click', async (e) => {
e.stopPropagation();
recentVisible = !recentVisible;
recentBtn.textContent = recentVisible ? '🗒️ Hide' : '🗒️ Notes';
recentList.innerHTML = recentVisible ? '⏳ Loading...' : '';
if (recentVisible) await loadRecent();
});
// ===== Load recent notes =====
async function loadRecent() {
try {
const notes = await VaultDB.list();
if (!notes?.length) {
recentList.innerHTML = 'No notes yet';
return;
}
const notesWithTime = [];
for (const name of notes) {
const data = await VaultDB.get(name);
if (!data) continue; // skip deleted notes
notesWithTime.push({ name, timestamp: data.timestamp || 0 });
}
recentList.innerHTML = notesWithTime
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, 10)
.map(item => `
<div class="note-item" data-note="${item.name}">
<span class="note-title">${item.name}</span>
${item.name !== HOME_NOTE ? `<span class="delete-note" title="Delete">🗑</span>` : ''}
</div>
`)
.join('');
} catch (err) {
console.error(err);
recentList.innerHTML = '❌ Error loading';
}
}
// ===== Delegated click handling (OPEN vs DELETE) =====
recentList.addEventListener('click', (e) => {
const deleteBtn = e.target.closest('.delete-note');
const item = e.target.closest('.note-item');
if (!item) return;
const noteName = item.dataset.note;
if (deleteBtn) {
e.stopPropagation();
deleteNote(noteName);
} else {
switchToNote(noteName);
}
});
// ===== Delete note (Home-safe, cache-safe) =====
async function deleteNote(noteName) {
if (!noteName || noteName === HOME_NOTE) return;
if (!confirm(`Delete "${noteName}" permanently?`)) return;
try {
await vaultDelete(noteName); // ✅ now exists
delete noteCache[noteName];
if (currentNote === noteName) {
currentNote = null;
editor.value = '';
await switchToNote(HOME_NOTE);
}
await loadRecent();
console.log(`🗑 Deleted "${noteName}"`);
} catch (err) {
console.error("❌ Delete failed", err);
alert("Delete failed — see console");
}
}
// ===== Home-proof note switching =====
async function switchToNote(noteName) {
noteName = noteName?.trim();
if (!noteName || noteName === currentNote) return;
lastRequestedNote = noteName;
if (
currentNote &&
currentNote !== HOME_NOTE &&
noteCache[currentNote] !== undefined
) {
await VaultDB.set(currentNote, noteCache[currentNote]);
}
let content = noteCache[noteName];
if (content === undefined || content === null) {
content = await VaultDB.get(noteName);
if (!content) content = `# ${noteName}\n\n`;
noteCache[noteName] = content;
}
if (lastRequestedNote !== noteName) return;
currentNote = noteName;
editor.value = content;
suppressHistory = true;
editor.dispatchEvent(new Event('input', { bubbles: true }));
suppressHistory = false;
document.querySelectorAll('.note-item.active')
.forEach(el => el.classList.remove('active'));
document.querySelectorAll(`.note-item[data-note="${currentNote}"]`)
.forEach(el => el.classList.add('active'));
}
// ===== Keep cache in sync =====
editor.addEventListener('input', () => {
if (currentNote) noteCache[currentNote] = editor.value;
});
});
const SidebarManager = {
sidebar: null,
listEl: null,
folders: {},
/* =====================
INIT
===================== */
async init() {
this.sidebar = document.getElementById("sidebar");
this.listEl = document.getElementById("recent-list");
if (!this.sidebar || !this.listEl) {
console.error("Sidebar elements missing");
return;
}
await this.load();
await this.syncNotes();
this.render();
this.injectControls();
},
/* =====================
STORAGE
===================== */
async load() {
const raw = await VaultDB.get("__folders__");
this.folders = raw ? JSON.parse(raw) : { "__root__": [] };
},
async save() {
await VaultDB.set("__folders__", JSON.stringify(this.folders));
},
/* =====================
SYNC NOTES
===================== */
async syncNotes() {
const notes = (await VaultDB.list()).filter(n => n !== "__folders__");
const known = new Set(Object.values(this.folders).flat());
for (const n of notes) {
if (known.has(n)) continue;
const data = await VaultDB.get(n);
if (!data) continue; // skip deleted notes
this.folders.__root__.push(n);
}
this.sortAll();
await this.save();
},
sortAll() {
Object.values(this.folders).forEach(arr =>
arr.sort((a, b) => a.localeCompare(b))
);
},
/* =====================
UI CONTROLS
===================== */
injectControls() {
const btn = document.createElement("div");
btn.className = "sidebar-item";
btn.textContent = "📁 New Folder";
btn.onclick = () => this.addFolder();
this.sidebar.appendChild(btn);
},
/* =====================
FOLDER ACTIONS
===================== */
async addFolder() {
const name = prompt("Folder name?");
if (!name || this.folders[name]) return;
this.folders[name] = [];
await this.save();
this.render();
},
async renameFolder(oldName) {
const name = prompt("Rename folder:", oldName);
if (!name || name === oldName || this.folders[name]) return;
this.folders[name] = this.folders[oldName];
delete this.folders[oldName];
await this.save();
this.render();
},
async deleteFolder(name) {
if (!confirm(`Delete folder "${name}"?`)) return;
this.folders.__root__.push(...this.folders[name]);
delete this.folders[name];
this.sortAll();
await this.save();
this.render();
},
/* =====================
NOTE ACTIONS
===================== */
async openNote(name) {
if (currentNote === name) return;
if (currentNote) {
await VaultDB.set(currentNote, editor.value);
}
let content = await VaultDB.get(name);
if (content == null) {
content = `# ${name}\n\n`;
await VaultDB.set(name, content);
}
currentNote = name;
editor.value = content;
updatePreviewAndSave();
},
async deleteNote(name) {
if (name === "Home") return;
if (!confirm(`Delete note "${name}"?`)) return;
Object.values(this.folders).forEach(arr => {
const i = arr.indexOf(name);
if (i !== -1) arr.splice(i, 1);
});
await VaultDB.set(name, null);
await this.save();
this.render();
if (currentNote === name) {
await this.openNote("Home");
}
},
async renameNote(oldName) {
const name = prompt("Rename note:", oldName);
if (!name || name === oldName) return;
const content = await VaultDB.get(oldName);
await VaultDB.set(name, content);
await VaultDB.set(oldName, null);
Object.values(this.folders).forEach(arr => {
const i = arr.indexOf(oldName);
if (i !== -1) arr[i] = name;
});
this.sortAll();
await this.save();
this.render();
if (currentNote === oldName) {
await this.openNote(name);
}
},
async moveNote(note, folder) {
Object.values(this.folders).forEach(arr => {
const i = arr.indexOf(note);
if (i !== -1) arr.splice(i, 1);
});
this.folders[folder].push(note);
this.sortAll();
await this.save();
this.render();
},
/* =====================
RENDER
===================== */
render() {
this.listEl.innerHTML = "";
// Root notes first
this.folders.__root__.forEach(n =>
this.listEl.appendChild(this.noteEl(n))
);
// Folders
Object.keys(this.folders)
.filter(f => f !== "__root__")
.sort((a, b) => a.localeCompare(b))
.forEach(f => this.listEl.appendChild(this.folderEl(f)));
},
noteEl(name) {
const el = document.createElement("div");
el.className = "note-item";
// Title on the left
const title = document.createElement("span");
title.className = "title";
title.textContent = name;
title.onclick = () => this.openNote(name);
// Icons on the right
const icons = document.createElement("span");
icons.className = "icons";
const ren = document.createElement("span");
ren.textContent = "✏️";
ren.title = "Rename";
ren.onclick = e => {
e.stopPropagation();
this.renameNote(name);
};
const del = document.createElement("span");
del.textContent = "🗑";
del.title = "Delete";
del.onclick = e => {
e.stopPropagation();
this.deleteNote(name);
};
icons.append(ren, del);
el.append(title, icons);
return el;
},
folderEl(name) {
const wrap = document.createElement("div");
wrap.className = "folder";
// Folder header: title + icons
const head = document.createElement("div");
head.className = "note-item";
const title = document.createElement("span");
title.className = "title";
title.textContent = `📁 ${name}`;
const icons = document.createElement("span");
icons.className = "icons";
// Rename icon
const ren = document.createElement("span");
ren.textContent = "✏️";
ren.title = "Rename Folder";
ren.onclick = e => {
e.stopPropagation();
this.renameFolder(name);
};
// Delete icon
const del = document.createElement("span");
del.textContent = "🗑";
del.title = "Delete Folder";
del.onclick = e => {
e.stopPropagation();
this.deleteFolder(name);
};
icons.append(ren, del);
head.append(title, icons);
wrap.appendChild(head);
// Add notes inside folder
this.folders[name].forEach(n => wrap.appendChild(this.noteEl(n)));
return wrap;
}
};
</script>
</body>
</html>
No comments:
Post a Comment