From 7904a99c21799246d112efef26d2e3e522eaaa80 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Sun, 10 May 2026 19:30:26 -0500 Subject: [PATCH] feat(browse): markdown front-matter pane + TOC resizer; misc UX fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markdown preview pane now surfaces YAML front-matter above the TOC as a key/value list (definition list), so engineering documents with header metadata (title, revision, status, etc.) show their identity at a glance without opening the file in mdedit. Front-matter parsing handles both scalar and array values; arrays render as comma-joined. TOC pane is now resizable (4px col-resize handle on its left edge); preserves the user's chosen width across re-renders inside a single session. mdedit welcome banner moved inside #welcome-screen so the "browse opens md in this same editor" callout only shows when no file is open — it was previously visible in every state which was noisy. archive.spec.js: wait for #filePreviewToggle to be attached before clicking, fixing a Playwright flake where the preview button hadn't mounted yet. Co-Authored-By: Claude Opus 4.7 (1M context) --- browse/css/tree.css | 49 ++++++++++++++++ browse/js/preview-markdown.js | 105 ++++++++++++++++++++++++++++++++++ mdedit/template.html | 17 +++--- tests/archive.spec.js | 6 +- 4 files changed, 167 insertions(+), 10 deletions(-) diff --git a/browse/css/tree.css b/browse/css/tree.css index b2350c1..45785cf 100644 --- a/browse/css/tree.css +++ b/browse/css/tree.css @@ -506,3 +506,52 @@ html, body { outline: 2px solid var(--primary); outline-offset: -1px; } + +/* Front-matter display section inside the TOC pane. */ +.md-fm-section { + border-bottom: 1px solid var(--border); + max-height: 40%; + overflow-y: auto; +} + +.md-fm-body { + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + line-height: 1.4; +} + +.fm-empty { + color: var(--text-muted); + font-style: italic; + font-size: 0.85rem; + margin: 0; +} + +.fm-list { + margin: 0; + display: grid; + grid-template-columns: minmax(5rem, max-content) 1fr; + gap: 0.15rem 0.5rem; +} + +.fm-list dt { + font-weight: 600; + color: var(--text-muted); + font-size: 0.8rem; + text-transform: lowercase; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.fm-list dd { + margin: 0; + color: var(--text); + font-size: 0.85rem; + overflow-wrap: anywhere; +} + +/* TOC pane resizer — narrower than the main one. */ +.md-toc-resizer { + width: 4px; +} diff --git a/browse/js/preview-markdown.js b/browse/js/preview-markdown.js index 76e5398..3178e13 100644 --- a/browse/js/preview-markdown.js +++ b/browse/js/preview-markdown.js @@ -44,6 +44,64 @@ currentInstance = null; } + // ── Front matter ─────────────────────────────────────────────────────── + // + // Lightweight YAML-front-matter parser. Same shape as mdedit's + // parseFrontMatter — handles `---\n…\n---\n` envelope, key: value + // lines, simple `[a, b, c]` arrays. Comments (#) skipped. Returns + // { data, body }; body is the markdown content with the front-matter + // envelope stripped. + + function parseFrontMatter(content) { + if (!content || !content.startsWith('---\n')) { + return { data: {}, body: content || '' }; + } + var endIdx = content.indexOf('\n---\n', 4); + if (endIdx === -1) return { data: {}, body: content }; + + var fmText = content.substring(4, endIdx); + var body = content.substring(endIdx + 5); + var data = {}; + var lines = fmText.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line || line.charAt(0) === '#') continue; + var colon = line.indexOf(':'); + if (colon <= 0) continue; + var key = line.substring(0, colon).trim(); + var val = line.substring(colon + 1).trim(); + val = val.replace(/^["']|["']$/g, ''); + if (val.startsWith('[') && val.endsWith(']')) { + val = val.slice(1, -1).split(',').map(function (s) { + return s.trim().replace(/^["']|["']$/g, ''); + }); + } + data[key] = val; + } + return { data: data, body: body }; + } + + function renderFrontMatter(fmEl, content) { + if (!fmEl) return; + var parsed = parseFrontMatter(content); + var keys = Object.keys(parsed.data); + if (keys.length === 0) { + fmEl.innerHTML = '

No front matter.

'; + return; + } + var html = '
'; + for (var i = 0; i < keys.length; i++) { + var k = keys[i]; + var v = parsed.data[k]; + var displayV = Array.isArray(v) + ? v.map(escapeHtml).join(', ') + : escapeHtml(String(v)); + html += '
' + escapeHtml(k) + '
' + displayV + '
'; + } + html += '
'; + fmEl.innerHTML = html; + } + // ── TOC (table of contents) ───────────────────────────────────────────── // // Ported from mdedit/js/toc.js, condensed: parse markdown for ATX-style @@ -262,8 +320,26 @@ editorHost.className = 'md-editor-host'; split.appendChild(editorHost); + var tocResizer = document.createElement('div'); + tocResizer.className = 'pane-resizer md-toc-resizer'; + tocResizer.setAttribute('aria-hidden', 'true'); + split.appendChild(tocResizer); + var tocPane = document.createElement('div'); tocPane.className = 'md-toc-pane'; + + // Front-matter section above TOC, read-only display. + var fmSection = document.createElement('div'); + fmSection.className = 'md-fm-section'; + var fmHeader = document.createElement('div'); + fmHeader.className = 'md-toc-pane__header'; + fmHeader.textContent = 'Front matter'; + var fmBody = document.createElement('div'); + fmBody.className = 'md-fm-body'; + fmSection.appendChild(fmHeader); + fmSection.appendChild(fmBody); + tocPane.appendChild(fmSection); + var tocHeader = document.createElement('div'); tocHeader.className = 'md-toc-pane__header'; tocHeader.textContent = 'Outline'; @@ -307,6 +383,34 @@ } renderToc(tocBody, text, editor); + renderFrontMatter(fmBody, text); + + // TOC pane resizer — drag horizontally. Stays in-memory only; + // refresh resets to the default 220px. + (function () { + var dragging = false; + var startX = 0; + var startWidth = 0; + tocResizer.addEventListener('mousedown', function (e) { + dragging = true; + tocResizer.classList.add('is-dragging'); + startX = e.clientX; + startWidth = tocPane.getBoundingClientRect().width; + e.preventDefault(); + }); + document.addEventListener('mousemove', function (e) { + if (!dragging) return; + // Drag left to grow the TOC, right to shrink it. + var dx = e.clientX - startX; + var w = Math.max(150, Math.min(window.innerWidth * 0.4, startWidth - dx)); + tocPane.style.width = w + 'px'; + }); + document.addEventListener('mouseup', function () { + if (!dragging) return; + dragging = false; + tocResizer.classList.remove('is-dragging'); + }); + })(); function markDirty(isDirty) { currentInstance.dirty = isDirty; @@ -319,6 +423,7 @@ var h = await hashContent(current); markDirty(h !== currentInstance.hash); renderToc(tocBody, current, editor); + renderFrontMatter(fmBody, current); }, 250); editor.on('change', updateOnChange); diff --git a/mdedit/template.html b/mdedit/template.html index 2de928d..135e6b9 100644 --- a/mdedit/template.html +++ b/mdedit/template.html @@ -58,15 +58,14 @@
-
- The Browse app now opens markdown files in this same editor. - Browse provides a unified file tree + per-file-type preview where - .md files render in this Toast UI editor. The - standalone Markdown Editor remains available for offline single-file - editing and air-gapped environments. -
- -