chore(embedded): cut v0.0.17-beta
This commit is contained in:
parent
0b382716e3
commit
89d96b784f
8 changed files with 379 additions and 68 deletions
|
|
@ -2316,7 +2316,7 @@ td[data-field="trackingNumber"] {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Archive</span>
|
<span class="app-header__title">ZDDC Archive</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -1137,6 +1137,140 @@ html, body {
|
||||||
.status-bar.is-error { color: var(--danger); }
|
.status-bar.is-error { color: var(--danger); }
|
||||||
.status-bar.is-info { color: var(--text); }
|
.status-bar.is-info { color: var(--text); }
|
||||||
|
|
||||||
|
/* ── Markdown plugin (right-pane internals when a .md is selected) ──────── */
|
||||||
|
|
||||||
|
/* Editor toolbar (above the editor+TOC split): Save + dirty marker +
|
||||||
|
status + source hint. Sticks to the top of the pane body. */
|
||||||
|
.md-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-toolbar__dirty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
min-width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-toolbar__status {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-toolbar__source {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-style: italic;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor + TOC two-pane split inside the preview body. */
|
||||||
|
.md-split {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-editor-host {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TOC pane sits on the right. Fixed width by default; the user can't
|
||||||
|
resize it (yet) — kept simple in v1. */
|
||||||
|
.md-toc-pane {
|
||||||
|
width: 220px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
background: var(--bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-toc-pane__header {
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-toc-pane__body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-empty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.2rem 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text);
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
transition: background 0.1s, border-color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item a:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-left-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item a:focus-visible {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-level-1 a { padding-left: 0.75rem; font-weight: 600; }
|
||||||
|
.toc-level-2 a { padding-left: 1.4rem; }
|
||||||
|
.toc-level-3 a { padding-left: 2.05rem; }
|
||||||
|
.toc-level-4 a { padding-left: 2.7rem; color: var(--text-muted); }
|
||||||
|
.toc-level-5 a { padding-left: 3.35rem; color: var(--text-muted); font-size: 0.8rem; }
|
||||||
|
.toc-level-6 a { padding-left: 4rem; color: var(--text-muted); font-size: 0.8rem; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -1152,7 +1286,7 @@ html, body {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Browse</span>
|
<span class="app-header__title">ZDDC Browse</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
||||||
|
|
@ -5051,13 +5185,17 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
|
|
||||||
// preview-markdown.js — markdown plugin for the browse preview pane.
|
// preview-markdown.js — markdown plugin for the browse preview pane.
|
||||||
// Click a .md / .markdown file in the tree → instantiate Toast UI
|
// Click a .md / .markdown file in the tree → instantiate Toast UI
|
||||||
// editor inside the right pane. Save (Ctrl+S) writes back via PUT
|
// editor inside the right pane, alongside a TOC pane on the right.
|
||||||
// when the file came from a server URL; FS-API and zip-virtual files
|
// Save (Ctrl+S) writes back via:
|
||||||
// are read-only for now (toolbar shows a hint).
|
// - PUT to the file's server URL when in server mode, or
|
||||||
|
// - FileSystemWritableFileStream when in FS-API mode (local folder
|
||||||
|
// picker). Both paths set dirty=false + a status timestamp on
|
||||||
|
// success.
|
||||||
|
// zip-virtual files are read-only — the save button stays disabled.
|
||||||
//
|
//
|
||||||
// Toast UI Editor is loaded from mdedit's bundled vendor file in the
|
// Toast UI Editor is bundled (shared/vendor/toastui-editor-all.min.js)
|
||||||
// browse build (see browse/build.sh). window.toastui is available
|
// and is available synchronously as window.toastui by the time this
|
||||||
// synchronously when this module runs.
|
// module runs.
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
@ -5068,11 +5206,10 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
.replace(/>/g, '>').replace(/"/g, '"');
|
.replace(/>/g, '>').replace(/"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentInstance = null; // { editor, container, dirty, node, hash }
|
var currentInstance = null; // { editor, container, dirty, node, hash, tocEl }
|
||||||
|
|
||||||
// Compute SHA-256 hex of a string for a quick "is this content
|
// Compute SHA-256 hex of a string for a "is this content different
|
||||||
// different from what was loaded?" check. Used to decide whether
|
// from what we loaded?" check. Used to enable/disable Save.
|
||||||
// the save button should be active. Not used for integrity.
|
|
||||||
async function hashContent(text) {
|
async function hashContent(text) {
|
||||||
if (!window.crypto || !window.crypto.subtle) return null;
|
if (!window.crypto || !window.crypto.subtle) return null;
|
||||||
var enc = new TextEncoder().encode(text);
|
var enc = new TextEncoder().encode(text);
|
||||||
|
|
@ -5092,6 +5229,152 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
currentInstance = null;
|
currentInstance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── TOC (table of contents) ─────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Ported from mdedit/js/toc.js, condensed: parse markdown for ATX-style
|
||||||
|
// headings, build a flat hierarchical list, click jumps the editor to
|
||||||
|
// the heading's line. We track WYSIWYG vs markdown mode and route the
|
||||||
|
// scroll behaviour to whichever pane is visible.
|
||||||
|
|
||||||
|
function parseHeadings(content) {
|
||||||
|
var headings = [];
|
||||||
|
var lines = content.split('\n');
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var m = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
||||||
|
if (!m) continue;
|
||||||
|
var text = m[2].trim()
|
||||||
|
.replace(/\\(.)/g, '$1')
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '$1')
|
||||||
|
.replace(/\*(.*?)\*/g, '$1')
|
||||||
|
.replace(/`(.*?)`/g, '$1')
|
||||||
|
.replace(/\[(.*?)\]\(.*?\)/g, '$1')
|
||||||
|
.replace(/~~(.*?)~~/g, '$1')
|
||||||
|
.trim();
|
||||||
|
headings.push({ level: m[1].length, text: text, lineIndex: i });
|
||||||
|
}
|
||||||
|
return headings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollEditorToHeading(editor, heading) {
|
||||||
|
try {
|
||||||
|
var els = editor.getEditorElements();
|
||||||
|
if (editor.isWysiwygMode && editor.isWysiwygMode()) {
|
||||||
|
var ww = els.wwEditor;
|
||||||
|
if (!ww) return;
|
||||||
|
var hs = ww.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||||
|
for (var i = 0; i < hs.length; i++) {
|
||||||
|
if (hs[i].textContent.trim() === heading.text) {
|
||||||
|
var top = hs[i].getBoundingClientRect().top - ww.getBoundingClientRect().top;
|
||||||
|
ww.scrollTop = top - 10;
|
||||||
|
flashHeading(hs[i]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var line = heading.lineIndex + 1;
|
||||||
|
try { editor.setSelection([line, 1], [line, 1]); } catch (_) { /* ignore */ }
|
||||||
|
var preview = els.mdPreview;
|
||||||
|
if (!preview) return;
|
||||||
|
var phs = preview.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||||
|
for (var j = 0; j < phs.length; j++) {
|
||||||
|
if (phs[j].textContent.trim() === heading.text) {
|
||||||
|
var ptop = phs[j].getBoundingClientRect().top - preview.getBoundingClientRect().top;
|
||||||
|
preview.scrollTop = ptop - 10;
|
||||||
|
flashHeading(phs[j]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* swallow; click was best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function flashHeading(el) {
|
||||||
|
if (!el) return;
|
||||||
|
el.style.transition = 'background-color 0.3s ease';
|
||||||
|
el.style.backgroundColor = 'var(--primary-light)';
|
||||||
|
setTimeout(function () {
|
||||||
|
el.style.backgroundColor = '';
|
||||||
|
setTimeout(function () { el.style.transition = ''; }, 300);
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToc(tocEl, content, editor) {
|
||||||
|
if (!tocEl) return;
|
||||||
|
var headings = parseHeadings(content);
|
||||||
|
if (!content.trim()) {
|
||||||
|
tocEl.innerHTML = '<p class="toc-empty">Empty file.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (headings.length === 0) {
|
||||||
|
tocEl.innerHTML = '<p class="toc-empty">No headings.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Build a flat ordered list; CSS handles the visual indent.
|
||||||
|
var html = '<ul class="toc-list">';
|
||||||
|
for (var i = 0; i < headings.length; i++) {
|
||||||
|
var h = headings[i];
|
||||||
|
html += '<li class="toc-item toc-level-' + h.level + '" data-line="' + h.lineIndex + '" data-text="' + escapeHtml(h.text) + '">'
|
||||||
|
+ '<a href="#" tabindex="0">' + escapeHtml(h.text) + '</a></li>';
|
||||||
|
}
|
||||||
|
html += '</ul>';
|
||||||
|
tocEl.innerHTML = html;
|
||||||
|
|
||||||
|
// One delegated click handler.
|
||||||
|
tocEl.querySelectorAll('.toc-item').forEach(function (li) {
|
||||||
|
li.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var idx = parseInt(li.dataset.line, 10);
|
||||||
|
var text = li.dataset.text;
|
||||||
|
scrollEditorToHeading(editor, { text: text, lineIndex: idx });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Light debounce so TOC doesn't rebuild on every keystroke.
|
||||||
|
function debounce(fn, ms) {
|
||||||
|
var t;
|
||||||
|
return function () {
|
||||||
|
clearTimeout(t);
|
||||||
|
var args = arguments, self = this;
|
||||||
|
t = setTimeout(function () { fn.apply(self, args); }, ms);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save (server + FS-API) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function saveContent(node, content) {
|
||||||
|
// FS-API mode: write via the local file handle.
|
||||||
|
if (node.handle && typeof node.handle.createWritable === 'function') {
|
||||||
|
var writable = await node.handle.createWritable();
|
||||||
|
await writable.write(content);
|
||||||
|
await writable.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Server mode: PUT the new bytes.
|
||||||
|
if (node.url && window.app.state.source === 'server') {
|
||||||
|
var resp = await fetch(node.url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||||
|
body: content,
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error('HTTP ' + resp.status);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error('No write target for this file (read-only source).');
|
||||||
|
}
|
||||||
|
|
||||||
|
function canSave(node) {
|
||||||
|
if (node.zipParentId != null) return false;
|
||||||
|
if (node.handle && typeof node.handle.createWritable === 'function') return true;
|
||||||
|
if (node.url && window.app.state.source === 'server') return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mount ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function render(node, container, ctx) {
|
async function render(node, container, ctx) {
|
||||||
if (typeof window.toastui === 'undefined') {
|
if (typeof window.toastui === 'undefined') {
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
|
|
@ -5100,10 +5383,9 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tear down any previous markdown instance (single-file model).
|
|
||||||
dispose();
|
dispose();
|
||||||
|
|
||||||
// Read the file content.
|
// Read content.
|
||||||
var text;
|
var text;
|
||||||
try {
|
try {
|
||||||
var buf = await ctx.getArrayBuffer(node);
|
var buf = await ctx.getArrayBuffer(node);
|
||||||
|
|
@ -5117,45 +5399,65 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the markdown plugin's DOM:
|
// Build the markdown plugin's DOM:
|
||||||
// ┌──────────────────────────────────┐
|
// ┌──────────────────────────────────────────────────┐
|
||||||
// │ toolbar (Save, dirty marker) │
|
// │ toolbar (Save, ● modified, status, source hint) │
|
||||||
// ├──────────────────────────────────┤
|
// ├──────────────────────────────────┬───────────────┤
|
||||||
// │ Toast UI editor │
|
// │ editor (Toast UI) │ TOC pane │
|
||||||
// └──────────────────────────────────┘
|
// └──────────────────────────────────┴───────────────┘
|
||||||
//
|
|
||||||
// TOC pane is deferred — a near-term iteration can split this
|
|
||||||
// into editor | toc once the simpler form is exercised.
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
container.style.display = 'flex';
|
container.style.display = 'flex';
|
||||||
container.style.flexDirection = 'column';
|
container.style.flexDirection = 'column';
|
||||||
|
|
||||||
var toolbar = document.createElement('div');
|
var toolbar = document.createElement('div');
|
||||||
toolbar.className = 'md-toolbar';
|
toolbar.className = 'md-toolbar';
|
||||||
toolbar.style.cssText = 'display:flex;align-items:center;gap:0.5rem;'
|
|
||||||
+ 'padding:0.35rem 0.75rem;background:var(--bg-secondary);'
|
|
||||||
+ 'border-bottom:1px solid var(--border);flex-shrink:0;';
|
|
||||||
|
|
||||||
var saveBtn = document.createElement('button');
|
var saveBtn = document.createElement('button');
|
||||||
saveBtn.className = 'btn btn-sm btn-primary';
|
saveBtn.className = 'btn btn-sm btn-primary';
|
||||||
saveBtn.type = 'button';
|
saveBtn.type = 'button';
|
||||||
saveBtn.textContent = 'Save';
|
saveBtn.textContent = 'Save';
|
||||||
saveBtn.disabled = true; // enabled when content changes
|
saveBtn.disabled = true;
|
||||||
|
|
||||||
var dirty = document.createElement('span');
|
var dirty = document.createElement('span');
|
||||||
dirty.style.cssText = 'color:var(--text-muted);font-size:0.85rem;';
|
dirty.className = 'md-toolbar__dirty';
|
||||||
dirty.textContent = '';
|
|
||||||
|
|
||||||
var status = document.createElement('span');
|
var status = document.createElement('span');
|
||||||
status.style.cssText = 'flex:1;text-align:right;color:var(--text-muted);font-size:0.85rem;';
|
status.className = 'md-toolbar__status';
|
||||||
|
|
||||||
|
var sourceHint = document.createElement('span');
|
||||||
|
sourceHint.className = 'md-toolbar__source';
|
||||||
|
if (node.zipParentId != null) {
|
||||||
|
sourceHint.textContent = 'read-only (inside zip)';
|
||||||
|
} else if (node.handle) {
|
||||||
|
sourceHint.textContent = 'local';
|
||||||
|
} else if (node.url) {
|
||||||
|
sourceHint.textContent = 'server';
|
||||||
|
}
|
||||||
|
|
||||||
toolbar.appendChild(saveBtn);
|
toolbar.appendChild(saveBtn);
|
||||||
toolbar.appendChild(dirty);
|
toolbar.appendChild(dirty);
|
||||||
toolbar.appendChild(status);
|
toolbar.appendChild(status);
|
||||||
|
toolbar.appendChild(sourceHint);
|
||||||
container.appendChild(toolbar);
|
container.appendChild(toolbar);
|
||||||
|
|
||||||
|
var split = document.createElement('div');
|
||||||
|
split.className = 'md-split';
|
||||||
|
container.appendChild(split);
|
||||||
|
|
||||||
var editorHost = document.createElement('div');
|
var editorHost = document.createElement('div');
|
||||||
editorHost.style.cssText = 'flex:1;min-height:0;overflow:hidden;';
|
editorHost.className = 'md-editor-host';
|
||||||
container.appendChild(editorHost);
|
split.appendChild(editorHost);
|
||||||
|
|
||||||
|
var tocPane = document.createElement('div');
|
||||||
|
tocPane.className = 'md-toc-pane';
|
||||||
|
var tocHeader = document.createElement('div');
|
||||||
|
tocHeader.className = 'md-toc-pane__header';
|
||||||
|
tocHeader.textContent = 'Outline';
|
||||||
|
var tocBody = document.createElement('div');
|
||||||
|
tocBody.className = 'md-toc-pane__body';
|
||||||
|
tocBody.innerHTML = '<p class="toc-empty">Loading…</p>';
|
||||||
|
tocPane.appendChild(tocHeader);
|
||||||
|
tocPane.appendChild(tocBody);
|
||||||
|
split.appendChild(tocPane);
|
||||||
|
|
||||||
var initialHash = await hashContent(text);
|
var initialHash = await hashContent(text);
|
||||||
var editor = new window.toastui.Editor({
|
var editor = new window.toastui.Editor({
|
||||||
|
|
@ -5174,47 +5476,56 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
currentInstance = { editor: editor, container: container, dirty: false, node: node, hash: initialHash };
|
currentInstance = {
|
||||||
|
editor: editor,
|
||||||
|
container: container,
|
||||||
|
dirty: false,
|
||||||
|
node: node,
|
||||||
|
hash: initialHash,
|
||||||
|
tocEl: tocBody
|
||||||
|
};
|
||||||
|
|
||||||
|
var writable = canSave(node);
|
||||||
|
if (!writable) {
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.title = 'Save not available — read-only source.';
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToc(tocBody, text, editor);
|
||||||
|
|
||||||
function markDirty(isDirty) {
|
function markDirty(isDirty) {
|
||||||
currentInstance.dirty = isDirty;
|
currentInstance.dirty = isDirty;
|
||||||
saveBtn.disabled = !isDirty;
|
saveBtn.disabled = !isDirty || !writable;
|
||||||
dirty.textContent = isDirty ? '● modified' : '';
|
dirty.textContent = isDirty ? '● modified' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.on('change', async function () {
|
var updateOnChange = debounce(async function () {
|
||||||
var current = editor.getMarkdown();
|
var current = editor.getMarkdown();
|
||||||
var h = await hashContent(current);
|
var h = await hashContent(current);
|
||||||
markDirty(h !== currentInstance.hash);
|
markDirty(h !== currentInstance.hash);
|
||||||
});
|
renderToc(tocBody, current, editor);
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
editor.on('change', updateOnChange);
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (!currentInstance.dirty) return;
|
if (!currentInstance.dirty || !writable) return;
|
||||||
var content = editor.getMarkdown();
|
var content = editor.getMarkdown();
|
||||||
// Read-only sources: zip-virtual, FS-API without write
|
|
||||||
// permission. For now we only attempt PUT against server URLs;
|
|
||||||
// FS-API saves can be wired in a later iteration via the
|
|
||||||
// existing zddc-source polyfill.
|
|
||||||
if (!node.url || window.app.state.source !== 'server') {
|
|
||||||
status.textContent = 'Save not yet supported for this source.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
status.textContent = 'Saving…';
|
status.textContent = 'Saving…';
|
||||||
var resp = await fetch(node.url, {
|
await saveContent(node, content);
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
|
||||||
body: content,
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error('HTTP ' + resp.status);
|
|
||||||
}
|
|
||||||
currentInstance.hash = await hashContent(content);
|
currentInstance.hash = await hashContent(content);
|
||||||
markDirty(false);
|
markDirty(false);
|
||||||
status.textContent = 'Saved ' + new Date().toLocaleTimeString();
|
var now = new Date();
|
||||||
|
status.textContent = 'Saved ' + now.toLocaleTimeString();
|
||||||
|
if (window.zddc && window.zddc.toast) {
|
||||||
|
window.zddc.toast('Saved ' + node.name, 'success');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status.textContent = 'Save failed: ' + (e.message || e);
|
status.textContent = 'Save failed: ' + (e.message || e);
|
||||||
|
if (window.zddc && window.zddc.toast) {
|
||||||
|
window.zddc.toast('Save failed: ' + (e.message || e), 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5222,7 +5533,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
|
||||||
|
|
||||||
// Ctrl+S / Cmd+S inside the editor → save.
|
// Ctrl+S / Cmd+S inside the editor → save.
|
||||||
container.addEventListener('keydown', function (e) {
|
container.addEventListener('keydown', function (e) {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1536,7 +1536,7 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Classifier</span>
|
<span class="app-header__title">ZDDC Classifier</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -1225,7 +1225,7 @@ body {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC</span>
|
<span class="app-header__title">ZDDC</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -2010,7 +2010,7 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Markdown</span>
|
<span class="app-header__title">ZDDC Markdown</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary" title="Add a local directory">Add Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary" title="Add a local directory">Add Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh directory" aria-label="Refresh">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh directory" aria-label="Refresh">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -2378,7 +2378,7 @@ dialog.modal--narrow {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Transmittal</span>
|
<span class="app-header__title">ZDDC Transmittal</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · diamond-flame-kettle</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
|
||||||
</div>
|
</div>
|
||||||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||||
<!-- Publish split-button (Transmittal-specific primary action;
|
<!-- Publish split-button (Transmittal-specific primary action;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||||
archive=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
|
archive=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
|
||||||
transmittal=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
|
transmittal=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
|
||||||
classifier=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
|
classifier=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
|
||||||
mdedit=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
|
mdedit=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
|
||||||
landing=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
|
landing=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
|
||||||
form=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
|
form=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
|
||||||
tables=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
|
tables=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
|
||||||
browse=v0.0.17-beta · 2026-05-10 · diamond-flame-kettle
|
browse=v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory
|
||||||
|
|
|
||||||
|
|
@ -1155,7 +1155,7 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 00:02:27 · d779814-dirty</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-11 · brass-dolphin-ivory</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue