release: archive/transmittal/classifier/mdedit/landing v0.0.2 stable
First stable bump for the HTML tools since v0.0.1 — drags the stable
channel forward to absorb the months of work that has been riding
alpha (landing rework, presets cleanup, mdedit module split, shared
build-lib changes, etc.).
Each tool independently bumped to v0.0.2 (the tools are independently
versioned by git-tag prefix; their numbers do not need to align with
each other or with zddc-server's 0.0.6).
Per-tool changes:
- website/releases/<tool>_v0.0.2.html new immutable snapshot
- website/releases/<tool>_stable.html symlink → _v0.0.2.html
- website/releases/<tool>_alpha.html freshened from v0.0.2 tag
- website/releases/<tool>_beta.html freshened from v0.0.2 tag
Tags created locally and pushed alongside this commit:
archive-v0.0.2, transmittal-v0.0.2, classifier-v0.0.2,
mdedit-v0.0.2, landing-v0.0.2
Bootstrap zips (install.zip, track-{alpha,beta,stable}.zip) regenerated
by the same build pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c95f07966d
commit
4d6e497510
24 changed files with 35805 additions and 829 deletions
Binary file not shown.
|
|
@ -2095,7 +2095,7 @@ td[data-field="trackingNumber"] {
|
|||
<div class="header-left">
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Archive</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 17:45:13 · cf4101b-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 · c95f079</span></span>
|
||||
</div>
|
||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data" style="font-size:1.1rem;">⟳</button>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1 +1 @@
|
|||
archive_v0.0.1.html
|
||||
archive_v0.0.2.html
|
||||
7833
website/releases/archive_v0.0.2.html
Normal file
7833
website/releases/archive_v0.0.2.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1358,7 +1358,7 @@ body.help-open .app-header {
|
|||
<div class="header-left">
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Classifier</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 17:45:13 · cf4101b-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 · c95f079</span></span>
|
||||
</div>
|
||||
<button id="selectDirectoryBtn" class="btn btn-primary">Select Directory</button>
|
||||
<button id="refreshBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory">Refresh</button>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ZDDC Classifier</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZTNhNWYiLz4KICA8ZyBmaWxsPSIjZmZmIj4KICAgIDxyZWN0IHg9IjE0IiB5PSIxOCIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iNDMsMjUgNTAsMjUgMjEsNDMgMTQsNDMiLz4KICAgIDxyZWN0IHg9IjE0IiB5PSI0MyIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICA8L2c+Cjwvc3ZnPgo=">
|
||||
<style>
|
||||
/* ==========================================================================
|
||||
ZDDC Shared Base — single source of truth for tokens and primitives
|
||||
|
|
@ -1357,7 +1358,7 @@ body.help-open .app-header {
|
|||
<div class="header-left">
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Classifier</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">beta · 2026-04-28 · 67f794e</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">beta · 2026-04-29 · c95f079</span></span>
|
||||
</div>
|
||||
<button id="selectDirectoryBtn" class="btn btn-primary">Select Directory</button>
|
||||
<button id="refreshBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory">Refresh</button>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
classifier_v0.0.1.html
|
||||
classifier_v0.0.2.html
|
||||
6972
website/releases/classifier_v0.0.2.html
Normal file
6972
website/releases/classifier_v0.0.2.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -884,7 +884,7 @@ body {
|
|||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<span class="app-header__title">ZDDC Archive</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 17:45:13 · cf4101b-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 · c95f079</span></span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)">◐</button>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1 +1 @@
|
|||
landing_v0.0.1.html
|
||||
landing_v0.0.2.html
|
||||
2199
website/releases/landing_v0.0.2.html
Normal file
2199
website/releases/landing_v0.0.2.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1650,7 +1650,7 @@ body.help-open .app-header {
|
|||
<div class="header-left">
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Markdown</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 17:45:13 · cf4101b-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 · c95f079</span></span>
|
||||
</div>
|
||||
<button id="select-directory" class="btn btn-primary" title="Select a Directory">Select Directory</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ZDDC Markdown</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZTNhNWYiLz4KICA8ZyBmaWxsPSIjZmZmIj4KICAgIDxyZWN0IHg9IjE0IiB5PSIxOCIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iNDMsMjUgNTAsMjUgMjEsNDMgMTQsNDMiLz4KICAgIDxyZWN0IHg9IjE0IiB5PSI0MyIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICA8L2c+Cjwvc3ZnPgo=">
|
||||
<!-- Toast UI Editor v3.2.2 -->
|
||||
<style>
|
||||
/*!
|
||||
|
|
@ -797,6 +798,15 @@ body.help-open .app-header {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Always-visible action buttons (e.g. scratchpad download) */
|
||||
.tree-actions--always { opacity: 1; }
|
||||
|
||||
.tree-btn:disabled,
|
||||
.tree-btn.is-disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tree-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -944,77 +954,59 @@ body.help-open .app-header {
|
|||
.bg-white { background-color: var(--bg) !important; }
|
||||
.bg-gray-100 { background-color: var(--bg-secondary) !important; }
|
||||
|
||||
/* ── Front matter nav bar ──────────────────────────────────────────────────── */
|
||||
.front-matter-nav {
|
||||
border-bottom: 1px solid var(--border);
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.front-matter-nav__header {
|
||||
/* ── Section headers (YAML front matter, TOC, etc.) ───────────────────────── */
|
||||
/* Shared style for all collapsible/section headers inside the side pane —
|
||||
keeps font, padding, weight identical to the file-tree pane header. */
|
||||
.pane-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.front-matter-nav__header:hover {
|
||||
.pane-section-header .toggle-icon {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
width: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Front matter section ──────────────────────────────────────────────────── */
|
||||
.front-matter-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
.front-matter-header:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
.front-matter-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.front-matter-toggle:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.front-matter-toggle svg {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Toggle arrow rotation for collapsed state */
|
||||
.front-matter-nav.collapsed .front-matter-toggle .arrow-down {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* Front matter content area */
|
||||
.front-matter-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.25s ease, padding 0.25s ease, opacity 0.25s ease;
|
||||
max-height: 500px;
|
||||
padding: 0 1rem;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* When collapsed, hide content completely */
|
||||
/* When collapsed, hide content; height shrinks to header */
|
||||
.front-matter-nav.collapsed {
|
||||
border-bottom: none;
|
||||
height: auto !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.front-matter-nav.collapsed .front-matter-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Front matter textarea */
|
||||
/* Front matter textarea fills the content area */
|
||||
.front-matter-textarea {
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
|
|
@ -1022,14 +1014,34 @@ body.help-open .app-header {
|
|||
resize: none;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
white-space: pre; /* preserve yaml structure, enables horiz scroll */
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden; /* height is set by JS to fit content exactly */
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.front-matter-textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ── Horizontal pane resizer (height split) ─────────────────────────────── */
|
||||
.pane-resizer.horizontal {
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
cursor: row-resize;
|
||||
background-color: var(--border);
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.pane-resizer.horizontal:hover,
|
||||
.pane-resizer.horizontal.active {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ── Hidden utility (for disabled buttons) ─────────────────────────────────── */
|
||||
.hide { display: none; }
|
||||
|
||||
|
|
@ -1130,22 +1142,26 @@ body.help-open .app-header {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toc-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.toc-container,
|
||||
.toc-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Header layout — font/padding/weight come from .pane-section-header. */
|
||||
.toc-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.toc-depth-selector {
|
||||
|
|
@ -1634,7 +1650,7 @@ body.help-open .app-header {
|
|||
<div class="header-left">
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Markdown</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">beta · 2026-04-28 · 67f794e</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">beta · 2026-04-29 · c95f079</span></span>
|
||||
</div>
|
||||
<button id="select-directory" class="btn btn-primary" title="Select a Directory">Select Directory</button>
|
||||
</div>
|
||||
|
|
@ -1665,11 +1681,9 @@ body.help-open .app-header {
|
|||
<div class="pane-resizer bg-gray-200 dark:bg-gray-700 transition-colors relative z-10 w-1 cursor-col-resize hover:bg-blue-500" data-resizer-for="file-nav"></div>
|
||||
|
||||
<div class="pane content-pane flex-1 relative flex flex-col bg-white dark:bg-gray-900 overflow-hidden" id="main-content">
|
||||
<div id="welcome-screen" class="welcome-screen flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400 text-center p-6">
|
||||
<h2 class="mb-2 text-xl">Welcome to ZDDC Markdown</h2>
|
||||
<p class="mb-4">All files are edited on your local computer.</p>
|
||||
<p id="welcome-hint" class="text-sm">Click <strong>Scratchpad</strong> to start editing,<br>or <strong>Select Directory</strong> to work with files.</p>
|
||||
<p id="welcome-firefox" class="text-sm text-amber-600 hidden mt-2">Your browser doesn't support the File System API.<br>Use <strong>Scratchpad</strong> to edit markdown and save via download.</p>
|
||||
<div id="welcome-screen" class="welcome-screen hidden flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400 text-center p-6">
|
||||
<p id="welcome-hint" class="text-sm">Click <strong>Scratchpad</strong> in the file list to start editing,<br>or <strong>Select Directory</strong> to work with files.</p>
|
||||
<p id="welcome-firefox" class="text-sm text-amber-600 hidden mt-2">Your browser doesn't support the File System API.<br>Use <strong>Scratchpad</strong> to edit markdown and download as a file.</p>
|
||||
</div>
|
||||
|
||||
<div id="content-container" class="content-container flex flex-col h-full hidden">
|
||||
|
|
@ -2247,6 +2261,10 @@ let directoryHandle = null;
|
|||
let fileTree = {};
|
||||
let currentFileHandle = null;
|
||||
|
||||
// True when the page is served over HTTP(S) and the file tree is sourced
|
||||
// from the server's JSON directory listing instead of the local FS API.
|
||||
let serverSourceMode = false;
|
||||
|
||||
// Map to store editor instances for each file
|
||||
// Key: file path, Value: { editor, container, tocContainer, etc. }
|
||||
const editorInstances = new Map();
|
||||
|
|
@ -2257,10 +2275,34 @@ let tocMaxDepth = 3;
|
|||
// Scratchpad ID constant
|
||||
const SCRATCHPAD_ID = '__scratchpad__';
|
||||
|
||||
// Default scratchpad markdown — shown the first time mdedit loads.
|
||||
// Acts as both a welcome message and a starter pad for quick notes.
|
||||
const SCRATCHPAD_WELCOME = [
|
||||
'# Welcome to ZDDC Markdown',
|
||||
'',
|
||||
'All editing happens locally on your computer — nothing is uploaded.',
|
||||
'',
|
||||
'Use this **Scratchpad** for quick notes. Download it any time with the ⬇',
|
||||
'button on the Scratchpad row in the file list.',
|
||||
'',
|
||||
'Click **Select Directory** above to open a folder of Markdown files,',
|
||||
'or just start typing here.',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* HTML-escape a string for safe insertion into innerHTML.
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text == null ? '' : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function calls
|
||||
* @param {Function} func - Function to debounce
|
||||
|
|
@ -2898,8 +2940,8 @@ function openScratchpad() {
|
|||
document.getElementById('welcome-screen').classList.add('hidden');
|
||||
document.getElementById('content-container').classList.remove('hidden');
|
||||
|
||||
// Initialize editor with no file handle
|
||||
initializeEditor('', true, SCRATCHPAD_ID, 'Scratchpad', null, null);
|
||||
// Initialize editor with the welcome text seeded as the starting content.
|
||||
initializeEditor(SCRATCHPAD_WELCOME, true, SCRATCHPAD_ID, 'Scratchpad', null, null);
|
||||
|
||||
// Mark as scratchpad
|
||||
const instance = editorInstances.get(SCRATCHPAD_ID);
|
||||
|
|
@ -2907,9 +2949,63 @@ function openScratchpad() {
|
|||
instance.isScratchpad = true;
|
||||
}
|
||||
|
||||
// Reflect non-empty starting content on the scratchpad row's download button.
|
||||
updateScratchpadDownloadState();
|
||||
|
||||
if (DEBUG) console.log('Opened scratchpad');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable the scratchpad-row download button based on whether the
|
||||
* scratchpad currently holds any content. Idempotent — safe to call from
|
||||
* editor change listeners.
|
||||
*/
|
||||
function updateScratchpadDownloadState() {
|
||||
const btn = document.getElementById('scratchpad-download-btn');
|
||||
if (!btn) return;
|
||||
const instance = editorInstances.get(SCRATCHPAD_ID);
|
||||
let hasContent = false;
|
||||
if (instance && instance.editor) {
|
||||
try {
|
||||
hasContent = (instance.editor.getMarkdown() || '').trim().length > 0;
|
||||
} catch (_) { /* editor may not be ready yet */ }
|
||||
}
|
||||
btn.disabled = !hasContent;
|
||||
btn.classList.toggle('is-disabled', !hasContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a browser download of the current scratchpad markdown.
|
||||
* No-op if the scratchpad has no content.
|
||||
*/
|
||||
function downloadScratchpad() {
|
||||
const instance = editorInstances.get(SCRATCHPAD_ID);
|
||||
if (!instance || !instance.editor) return;
|
||||
let content = '';
|
||||
try { content = instance.editor.getMarkdown() || ''; } catch (_) { return; }
|
||||
|
||||
// Pull front matter from the textarea if any
|
||||
if (instance.frontMatterTextarea) {
|
||||
const fmText = instance.frontMatterTextarea.value.trim();
|
||||
if (fmText) content = `---\n${fmText}\n---\n${content}`;
|
||||
}
|
||||
|
||||
if (!content.trim()) return;
|
||||
|
||||
// Suggest a filename derived from the first H1 if present
|
||||
let suggested = 'scratchpad.md';
|
||||
const h1 = content.match(/^#\s+(.+)$/m);
|
||||
if (h1) {
|
||||
const slug = h1[1].trim().toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.substring(0, 60);
|
||||
if (slug) suggested = `${slug}.md`;
|
||||
}
|
||||
|
||||
saveFileAs(content, suggested);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file using Save As dialog (for scratchpads or new saves)
|
||||
* @param {string} content - Content to save
|
||||
|
|
@ -3138,6 +3234,17 @@ async function saveFile(filePath) {
|
|||
? stringifyFrontMatter(markdownContent, frontMatterData)
|
||||
: markdownContent;
|
||||
|
||||
// Server-mode files are read-only: fall back to a Save-As download.
|
||||
if (typeof fileHandle.createWritable !== 'function') {
|
||||
const downloadName = (fileHandle.name || filePath.split('/').pop() || 'untitled.md');
|
||||
await saveFileAs(finalContent, downloadName);
|
||||
editorInstance.isDirty = false;
|
||||
updateFileDirtyStatus(filePath, false);
|
||||
updateUnsavedCount();
|
||||
if (editorInstance.saveButton) editorInstance.saveButton.disabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Write to file
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(finalContent);
|
||||
|
|
@ -3332,6 +3439,10 @@ async function applyAfterSaveHooks(frontMatter, markdownContent, fileHandle) {
|
|||
* Refresh directory from disk without losing unsaved work
|
||||
*/
|
||||
async function refreshDirectory() {
|
||||
if (serverSourceMode) {
|
||||
await loadServerDirectory();
|
||||
return;
|
||||
}
|
||||
if (!directoryHandle) {
|
||||
if (DEBUG) console.log('No directory selected, cannot refresh');
|
||||
return;
|
||||
|
|
@ -3366,6 +3477,144 @@ async function refreshDirectory() {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a synthetic, read-only "file handle" backed by a URL.
|
||||
* Implements `getFile()` so the rest of the app (which only needs to read)
|
||||
* works without changes. Lacks `createWritable()` — saveFile detects this
|
||||
* and routes to a Save-As download.
|
||||
*/
|
||||
function createServerFileHandle(name, url) {
|
||||
let cached = null;
|
||||
return {
|
||||
kind: 'file',
|
||||
name,
|
||||
_serverUrl: url,
|
||||
_readOnly: true,
|
||||
async getFile() {
|
||||
if (cached) return cached;
|
||||
const resp = await fetch(url, { cache: 'no-cache' });
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status} fetching ${url}`);
|
||||
const lastMod = resp.headers.get('Last-Modified');
|
||||
const lastModified = lastMod ? Date.parse(lastMod) : Date.now();
|
||||
const blob = await resp.blob();
|
||||
cached = new File([blob], name, { type: blob.type, lastModified });
|
||||
return cached;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a synthetic directory handle (read-only) backed by a server URL.
|
||||
* Returned for nested entries so existing code paths that probe for `.handle`
|
||||
* still work; not currently used for traversal.
|
||||
*/
|
||||
function createServerDirectoryHandle(name, url) {
|
||||
return {
|
||||
kind: 'directory',
|
||||
name,
|
||||
_serverUrl: url,
|
||||
_readOnly: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively fetch the JSON directory listing for `dirUrl` and populate
|
||||
* `parentNode.entries` with synthetic handles. Returns folder/file counts.
|
||||
* Uses the same Caddy/zddc-server JSON shape archive's source.js consumes.
|
||||
*/
|
||||
async function readServerDirectory(dirUrl, parentNode, depth) {
|
||||
if (depth > 10) return { folderCount: 0, fileCount: 0 };
|
||||
|
||||
let items;
|
||||
try {
|
||||
const resp = await fetch(dirUrl, { headers: { 'Accept': 'application/json' }, cache: 'no-cache' });
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
items = await resp.json();
|
||||
if (!Array.isArray(items)) throw new Error('Expected JSON array');
|
||||
} catch (err) {
|
||||
if (DEBUG) console.warn(`Server listing failed for ${dirUrl}:`, err);
|
||||
return { folderCount: 0, fileCount: 0 };
|
||||
}
|
||||
|
||||
const stats = { folderCount: 0, fileCount: 0 };
|
||||
const subdirPromises = [];
|
||||
|
||||
for (const item of items) {
|
||||
const rawName = item.name.endsWith('/') ? item.name.slice(0, -1) : item.name;
|
||||
if (rawName.startsWith('.') || rawName.startsWith('_')) continue;
|
||||
|
||||
const base = dirUrl.endsWith('/') ? dirUrl : dirUrl + '/';
|
||||
const childUrl = base + encodeURIComponent(rawName) + (item.is_dir ? '/' : '');
|
||||
|
||||
if (item.is_dir) {
|
||||
const dirNode = {
|
||||
name: rawName,
|
||||
type: 'directory',
|
||||
handle: createServerDirectoryHandle(rawName, childUrl),
|
||||
entries: {},
|
||||
};
|
||||
parentNode.entries[rawName] = dirNode;
|
||||
stats.folderCount++;
|
||||
subdirPromises.push(
|
||||
readServerDirectory(childUrl, dirNode, depth + 1).then((s) => {
|
||||
stats.folderCount += s.folderCount;
|
||||
stats.fileCount += s.fileCount;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
parentNode.entries[rawName] = {
|
||||
name: rawName,
|
||||
type: 'file',
|
||||
handle: createServerFileHandle(rawName, childUrl),
|
||||
};
|
||||
stats.fileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(subdirPromises);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect HTTP context, fetch the directory the page lives under, and render
|
||||
* the resulting subtree in the file pane. Idempotent — safe to re-call.
|
||||
*/
|
||||
async function loadServerDirectory() {
|
||||
if (!(location.protocol === 'http:' || location.protocol === 'https:')) return;
|
||||
serverSourceMode = true;
|
||||
|
||||
let href = window.location.href.split('?')[0].split('#')[0];
|
||||
const lastSlash = href.lastIndexOf('/');
|
||||
const baseUrl = (lastSlash >= 0) ? href.substring(0, lastSlash + 1) : href + '/';
|
||||
|
||||
const rootName = (() => {
|
||||
const path = baseUrl.replace(/\/$/, '');
|
||||
const seg = path.substring(path.lastIndexOf('/') + 1);
|
||||
return seg || baseUrl;
|
||||
})();
|
||||
|
||||
fileTree = {
|
||||
name: rootName,
|
||||
type: 'directory',
|
||||
handle: createServerDirectoryHandle(rootName, baseUrl),
|
||||
entries: {},
|
||||
};
|
||||
|
||||
// Surface refresh, hide write-only controls
|
||||
const refreshBtn = document.getElementById('refresh-directory');
|
||||
if (refreshBtn) refreshBtn.classList.remove('hidden');
|
||||
const newFileRootBtn = document.getElementById('new-file-root');
|
||||
if (newFileRootBtn) newFileRootBtn.classList.add('hidden');
|
||||
const selectDirBtn = document.getElementById('select-directory');
|
||||
if (selectDirBtn) {
|
||||
selectDirBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
const stats = await readServerDirectory(baseUrl, fileTree, 0);
|
||||
renderFileTree();
|
||||
updateStatusCounts(stats.folderCount, stats.fileCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring files for external changes
|
||||
*/
|
||||
|
|
@ -3375,6 +3624,7 @@ function startFileChangeMonitoring() {
|
|||
try {
|
||||
const fileHandle = editorInstance.fileHandle;
|
||||
if (!fileHandle) continue;
|
||||
if (fileHandle._readOnly) continue;
|
||||
|
||||
const file = await fileHandle.getFile();
|
||||
const currentLastModified = file.lastModified;
|
||||
|
|
@ -3438,6 +3688,9 @@ function createActionButtons(filePath, type) {
|
|||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'tree-actions';
|
||||
|
||||
// Server mode is read-only: no rename, delete, or new-file actions.
|
||||
if (serverSourceMode) return actionsDiv;
|
||||
|
||||
if (type === 'directory') {
|
||||
// Directory: + (new file) + ✕ (delete)
|
||||
const newFileBtn = document.createElement('button');
|
||||
|
|
@ -3495,22 +3748,46 @@ function renderFileTree() {
|
|||
|
||||
// Always show scratchpad at top
|
||||
const scratchpadElement = document.createElement('div');
|
||||
scratchpadElement.className = 'file-item px-2 py-1 cursor-pointer rounded whitespace-nowrap overflow-hidden hover:bg-gray-100 dark:hover:bg-gray-800 border-b border-gray-200 dark:border-gray-700 mb-2';
|
||||
scratchpadElement.className = 'file-item tree-row px-2 py-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800 border-b border-gray-200 dark:border-gray-700 mb-2';
|
||||
scratchpadElement.dataset.type = 'file';
|
||||
scratchpadElement.dataset.path = SCRATCHPAD_ID;
|
||||
scratchpadElement.dataset.name = 'Scratchpad';
|
||||
scratchpadElement.innerHTML = '<div class="filename-main">📝 Scratchpad</div><div class="filename-secondary">Quick editing (no file)</div>';
|
||||
|
||||
const scratchLabel = document.createElement('span');
|
||||
scratchLabel.className = 'tree-row__label';
|
||||
scratchLabel.innerHTML = '<div class="filename-main">📝 Scratchpad</div><div class="filename-secondary">Quick notes — no directory needed</div>';
|
||||
scratchpadElement.appendChild(scratchLabel);
|
||||
|
||||
const scratchActions = document.createElement('div');
|
||||
scratchActions.className = 'tree-actions tree-actions--always';
|
||||
|
||||
const scratchDownloadBtn = document.createElement('button');
|
||||
scratchDownloadBtn.id = 'scratchpad-download-btn';
|
||||
scratchDownloadBtn.className = 'tree-btn';
|
||||
scratchDownloadBtn.title = 'Download scratchpad as a Markdown file';
|
||||
scratchDownloadBtn.setAttribute('aria-label', 'Download scratchpad');
|
||||
scratchDownloadBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="M7 10l5 5 5-5"/><path d="M5 21h14"/></svg>';
|
||||
scratchDownloadBtn.disabled = true;
|
||||
scratchDownloadBtn.classList.add('is-disabled');
|
||||
scratchDownloadBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (scratchDownloadBtn.disabled) return;
|
||||
downloadScratchpad();
|
||||
};
|
||||
scratchActions.appendChild(scratchDownloadBtn);
|
||||
scratchpadElement.appendChild(scratchActions);
|
||||
|
||||
scratchpadElement.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
openScratchpad();
|
||||
|
||||
// Update active state
|
||||
document.querySelectorAll('.file-item').forEach(el => el.classList.remove('active-file'));
|
||||
scratchpadElement.classList.add('active-file');
|
||||
updateScratchpadDownloadState();
|
||||
});
|
||||
|
||||
fileTreeElement.appendChild(scratchpadElement);
|
||||
// Sync button state with current scratchpad content (re-renders preserve it)
|
||||
updateScratchpadDownloadState();
|
||||
|
||||
function createFileTreeHTML(directory, parentElement, path = '') {
|
||||
if (!directory || !directory.entries) return;
|
||||
|
|
@ -3540,7 +3817,14 @@ function renderFileTree() {
|
|||
dirIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" 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>';
|
||||
|
||||
const dirName = document.createElement('span');
|
||||
dirName.textContent = `📁 ${name}`;
|
||||
dirName.className = 'tree-row__name';
|
||||
const parsedFolder = zddc.parseFolder(name);
|
||||
if (parsedFolder && parsedFolder.valid) {
|
||||
const meta = `${parsedFolder.trackingNumber} (${parsedFolder.status}) — ${parsedFolder.date}`;
|
||||
dirName.innerHTML = `<div class="filename-main">📁 ${escapeHtml(parsedFolder.title)}</div><div class="filename-secondary">${escapeHtml(meta)}</div>`;
|
||||
} else {
|
||||
dirName.textContent = `📁 ${name}`;
|
||||
}
|
||||
|
||||
const dirLabel = document.createElement('span');
|
||||
dirLabel.className = 'tree-row__label';
|
||||
|
|
@ -3582,18 +3866,16 @@ function renderFileTree() {
|
|||
let fileNameDisplay;
|
||||
const parsed = zddc.parseFilename(name);
|
||||
if (parsed && parsed.valid) {
|
||||
// Strip extension from title for display (it's already in the icon)
|
||||
const titleDisplay = parsed.title;
|
||||
const metaDisplay = `${parsed.trackingNumber}_${parsed.revision} (${parsed.status})`;
|
||||
const titleDisplay = escapeHtml(parsed.title);
|
||||
const metaDisplay = escapeHtml(`${parsed.trackingNumber}_${parsed.revision} (${parsed.status})`);
|
||||
fileNameDisplay = `<div class="filename-main">${fileIcon} ${titleDisplay}</div><div class="filename-secondary">${metaDisplay}</div>`;
|
||||
} else if (name.includes(' - ')) {
|
||||
// Fallback: simple split for files with ' - ' but not fully ZDDC-compliant
|
||||
const dashIdx = name.lastIndexOf(' - ');
|
||||
const secondary = name.substring(0, dashIdx);
|
||||
const primary = name.substring(dashIdx + 3).replace(/\.[^.]+$/, '');
|
||||
const secondary = escapeHtml(name.substring(0, dashIdx));
|
||||
const primary = escapeHtml(name.substring(dashIdx + 3).replace(/\.[^.]+$/, ''));
|
||||
fileNameDisplay = `<div class="filename-main">${fileIcon} ${primary}</div><div class="filename-secondary">${secondary}</div>`;
|
||||
} else {
|
||||
fileNameDisplay = `<span>${fileIcon} ${name}</span>`;
|
||||
fileNameDisplay = `<span>${fileIcon} ${escapeHtml(name)}</span>`;
|
||||
}
|
||||
|
||||
const fileLabel = document.createElement('span');
|
||||
|
|
@ -4200,21 +4482,23 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
|||
|
||||
// Determine if this is a scratchpad (no file handle)
|
||||
const isScratchpad = !fileHandle;
|
||||
const isReadOnlyHandle = !!(fileHandle && fileHandle._readOnly);
|
||||
|
||||
// Save button (or Save As for scratchpads)
|
||||
// Save button (or Save As for scratchpads / read-only server files)
|
||||
const saveButton = document.createElement('button');
|
||||
saveButton.className = 'btn btn-primary btn-sm';
|
||||
saveButton.textContent = isScratchpad ? 'Save As...' : 'Save File';
|
||||
saveButton.disabled = !isScratchpad; // Scratchpads can always save
|
||||
saveButton.textContent = (isScratchpad || isReadOnlyHandle) ? 'Save As...' : 'Save File';
|
||||
saveButton.disabled = !isScratchpad; // Scratchpads can always save; read-only enables on edit
|
||||
buttonContainer.appendChild(saveButton);
|
||||
|
||||
// Reload button (only for files, not scratchpads)
|
||||
// Reload button (only for files, not scratchpads) — icon to match file-tree refresh
|
||||
let reloadButton = null;
|
||||
if (!isScratchpad) {
|
||||
reloadButton = document.createElement('button');
|
||||
reloadButton.className = 'btn btn-secondary btn-sm';
|
||||
reloadButton.textContent = 'Reload from Disk';
|
||||
reloadButton.title = 'Reload file from disk (discards unsaved changes)';
|
||||
reloadButton.textContent = '↻';
|
||||
reloadButton.title = 'Reload from disk (discards unsaved changes)';
|
||||
reloadButton.setAttribute('aria-label', 'Reload from disk');
|
||||
buttonContainer.appendChild(reloadButton);
|
||||
}
|
||||
|
||||
|
|
@ -4239,16 +4523,17 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
|||
tocPane.style.width = '325px';
|
||||
tocPane.style.minWidth = '150px';
|
||||
|
||||
// Front matter nav bar (collapsible)
|
||||
// Front matter section (collapsible, height-resizable)
|
||||
const frontMatterNav = document.createElement('div');
|
||||
frontMatterNav.className = 'front-matter-nav border-b border-gray-200 dark:border-gray-700';
|
||||
frontMatterNav.className = 'front-matter-nav';
|
||||
frontMatterNav.style.height = '180px';
|
||||
|
||||
const frontMatterHeader = document.createElement('div');
|
||||
frontMatterHeader.className = 'front-matter-header px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 font-medium border-b border-gray-200 dark:border-gray-700 cursor-pointer flex items-center gap-2';
|
||||
frontMatterHeader.className = 'front-matter-header pane-section-header cursor-pointer';
|
||||
|
||||
const toggleIcon = document.createElement('span');
|
||||
toggleIcon.textContent = '▼';
|
||||
toggleIcon.className = 'toggle-icon text-sm';
|
||||
toggleIcon.className = 'toggle-icon';
|
||||
frontMatterHeader.appendChild(toggleIcon);
|
||||
|
||||
const headerText = document.createElement('span');
|
||||
|
|
@ -4257,11 +4542,13 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
|||
|
||||
frontMatterNav.appendChild(frontMatterHeader);
|
||||
|
||||
const frontMatterContent = document.createElement('div');
|
||||
frontMatterContent.className = 'front-matter-content';
|
||||
|
||||
frontMatterTextarea = document.createElement('textarea');
|
||||
frontMatterTextarea.className = 'front-matter-textarea w-full px-4 py-2 text-sm focus:outline-none resize-none overflow-x-auto';
|
||||
frontMatterTextarea.className = 'front-matter-textarea';
|
||||
frontMatterTextarea.placeholder = 'title: Document Title\ndate: 2024-01-01\ntags: [example]';
|
||||
|
||||
// Set front matter content
|
||||
if (frontMatterData && Object.keys(frontMatterData).length > 0) {
|
||||
try {
|
||||
let yamlText = '';
|
||||
|
|
@ -4279,11 +4566,21 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
|||
}
|
||||
}
|
||||
|
||||
frontMatterNav.appendChild(frontMatterTextarea);
|
||||
frontMatterContent.appendChild(frontMatterTextarea);
|
||||
frontMatterNav.appendChild(frontMatterContent);
|
||||
tocPane.appendChild(frontMatterNav);
|
||||
|
||||
// Horizontal resizer between front-matter and TOC
|
||||
const fmTocResizer = document.createElement('div');
|
||||
fmTocResizer.className = 'pane-resizer horizontal';
|
||||
tocPane.appendChild(fmTocResizer);
|
||||
|
||||
// TOC section
|
||||
const tocSection = document.createElement('div');
|
||||
tocSection.className = 'toc-section';
|
||||
|
||||
const tocHeader = document.createElement('div');
|
||||
tocHeader.className = 'toc-header px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 font-medium border-b border-gray-200 dark:border-gray-700';
|
||||
tocHeader.className = 'toc-header pane-section-header';
|
||||
|
||||
const tocTitle = document.createElement('span');
|
||||
tocTitle.textContent = 'Table of Contents';
|
||||
|
|
@ -4301,42 +4598,41 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
|||
`;
|
||||
tocHeader.appendChild(tocDepthSelector);
|
||||
|
||||
tocPane.appendChild(tocHeader);
|
||||
tocSection.appendChild(tocHeader);
|
||||
|
||||
tocContainer = document.createElement('div');
|
||||
tocContainer.className = 'toc-container toc-content p-4 h-full overflow-auto';
|
||||
tocPane.appendChild(tocContainer);
|
||||
tocContainer.className = 'toc-container toc-content';
|
||||
tocSection.appendChild(tocContainer);
|
||||
|
||||
// Set up TOC container overflow when front matter is toggled
|
||||
tocPane.appendChild(tocSection);
|
||||
|
||||
// Toggle: collapsed only shows the header. Hide content + horizontal resizer.
|
||||
let fmIsCollapsed = false;
|
||||
frontMatterHeader.addEventListener('click', () => {
|
||||
fmIsCollapsed = !fmIsCollapsed;
|
||||
frontMatterNav.classList.toggle('collapsed', fmIsCollapsed);
|
||||
toggleIcon.textContent = fmIsCollapsed ? '▶' : '▼';
|
||||
fmTocResizer.style.display = fmIsCollapsed ? 'none' : '';
|
||||
if (fmIsCollapsed) {
|
||||
frontMatterNav.style.height = '';
|
||||
} else {
|
||||
frontMatterNav.style.height = '180px';
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-size textarea: no vertical scroll, horizontal scroll for long lines
|
||||
frontMatterTextarea.style.overflowY = 'hidden';
|
||||
frontMatterTextarea.style.overflowX = 'auto';
|
||||
const autoResizeFm = () => {
|
||||
frontMatterTextarea.style.height = 'auto';
|
||||
frontMatterTextarea.style.height = frontMatterTextarea.scrollHeight + 'px';
|
||||
};
|
||||
frontMatterTextarea.addEventListener('input', autoResizeFm);
|
||||
// Defer initial resize until element is in the DOM and has layout
|
||||
requestAnimationFrame(() => requestAnimationFrame(autoResizeFm));
|
||||
|
||||
editorArea.appendChild(tocPane);
|
||||
|
||||
// TOC resizer
|
||||
// Vertical resizer between toc-pane and editor (placed inside editorArea)
|
||||
const tocResizer = document.createElement('div');
|
||||
tocResizer.className = 'pane-resizer bg-gray-200 dark:bg-gray-700 transition-colors relative z-10 w-1 cursor-col-resize hover:bg-blue-500';
|
||||
tocResizer.setAttribute('data-resizer-for', 'toc-pane');
|
||||
contentArea.appendChild(tocResizer);
|
||||
editorArea.appendChild(tocResizer);
|
||||
|
||||
makeResizable(tocResizer, tocPane);
|
||||
|
||||
// TOC depth selector event
|
||||
// Make the front-matter / TOC split height-adjustable
|
||||
makeHeightResizable(fmTocResizer, frontMatterNav, tocPane);
|
||||
|
||||
tocDepthSelector.addEventListener('change', function () {
|
||||
const depth = parseInt(this.value);
|
||||
if (window.updateToc && editorInstance) {
|
||||
|
|
@ -4410,6 +4706,8 @@ function initializeEditor(content, isMarkdown = true, filePath = '', fileName =
|
|||
updateUnsavedCount();
|
||||
}
|
||||
saveButton.disabled = false;
|
||||
|
||||
if (filePath === SCRATCHPAD_ID) updateScratchpadDownloadState();
|
||||
});
|
||||
|
||||
// Scroll listener for TOC highlighting
|
||||
|
|
@ -4841,6 +5139,50 @@ function makeResizable(resizer, pane) {
|
|||
resizer.addEventListener('mousedown', mouseDownHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a horizontal split height-adjustable: the resizer drags the height
|
||||
* of `topPane` while it remains a sibling of the bottom section inside `container`.
|
||||
*
|
||||
* @param {HTMLElement} resizer - The horizontal resizer between the panes
|
||||
* @param {HTMLElement} topPane - The pane whose height is set
|
||||
* @param {HTMLElement} container - The flex column containing both panes
|
||||
*/
|
||||
function makeHeightResizable(resizer, topPane, container) {
|
||||
let y = 0;
|
||||
let topHeight = 0;
|
||||
let containerHeight = 0;
|
||||
|
||||
const mouseDownHandler = (e) => {
|
||||
y = e.clientY;
|
||||
topHeight = topPane.offsetHeight;
|
||||
containerHeight = container.offsetHeight;
|
||||
document.addEventListener('mousemove', mouseMoveHandler);
|
||||
document.addEventListener('mouseup', mouseUpHandler);
|
||||
resizer.classList.add('active');
|
||||
document.body.style.cursor = 'row-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
};
|
||||
|
||||
const mouseMoveHandler = (e) => {
|
||||
const dy = e.clientY - y;
|
||||
// Reserve at least 80px for the bottom pane (TOC); cap top at containerHeight - 80.
|
||||
const minTop = 60;
|
||||
const maxTop = Math.max(minTop, containerHeight - 100);
|
||||
const newHeight = Math.max(minTop, Math.min(maxTop, topHeight + dy));
|
||||
topPane.style.height = `${newHeight}px`;
|
||||
};
|
||||
|
||||
const mouseUpHandler = () => {
|
||||
document.removeEventListener('mousemove', mouseMoveHandler);
|
||||
document.removeEventListener('mouseup', mouseUpHandler);
|
||||
resizer.classList.remove('active');
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
resizer.addEventListener('mousedown', mouseDownHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the file navigation pane resizer
|
||||
*/
|
||||
|
|
@ -4988,7 +5330,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
// Show scratchpad in file tree on startup
|
||||
renderFileTree();
|
||||
|
||||
// Always start with scratchpad selected and loaded
|
||||
openScratchpad();
|
||||
const scratchpadEl = document.querySelector(`.file-item[data-path="${SCRATCHPAD_ID}"]`);
|
||||
if (scratchpadEl) scratchpadEl.classList.add('active-file');
|
||||
|
||||
// In server (HTTP) mode, fetch and render the current directory subtree.
|
||||
if (location.protocol === 'http:' || location.protocol === 'https:') {
|
||||
loadServerDirectory().catch((err) => {
|
||||
if (DEBUG) console.warn('Server directory load failed:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
mdedit_v0.0.1.html
|
||||
mdedit_v0.0.2.html
|
||||
5416
website/releases/mdedit_v0.0.2.html
Normal file
5416
website/releases/mdedit_v0.0.2.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -2192,7 +2192,7 @@ dialog.modal--narrow {
|
|||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Transmittal</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 17:45:12 · cf4101b-dirty</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">alpha · 2026-04-29 · c95f079</span></span>
|
||||
</div>
|
||||
<div class="app-header__spacer"></div>
|
||||
<div class="app-header__icons">
|
||||
|
|
|
|||
|
|
@ -2179,6 +2179,7 @@ dialog.modal--narrow {
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ZDDC Transmittal</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxZTNhNWYiLz4KICA8ZyBmaWxsPSIjZmZmIj4KICAgIDxyZWN0IHg9IjE0IiB5PSIxOCIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iNDMsMjUgNTAsMjUgMjEsNDMgMTQsNDMiLz4KICAgIDxyZWN0IHg9IjE0IiB5PSI0MyIgd2lkdGg9IjM2IiBoZWlnaHQ9IjciLz4KICA8L2c+Cjwvc3ZnPgo=">
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-gray-900">
|
||||
|
|
@ -2191,7 +2192,7 @@ dialog.modal--narrow {
|
|||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||
<div class="header-title-group">
|
||||
<span class="app-header__title">ZDDC Transmittal</span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">beta · 2026-04-28 · 67f794e</span></span>
|
||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">beta · 2026-04-29 · c95f079</span></span>
|
||||
</div>
|
||||
<div class="app-header__spacer"></div>
|
||||
<div class="app-header__icons">
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
transmittal_v0.0.1.html
|
||||
transmittal_v0.0.2.html
|
||||
10999
website/releases/transmittal_v0.0.2.html
Normal file
10999
website/releases/transmittal_v0.0.2.html
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue