feat(browse): markdown front-matter pane + TOC resizer; misc UX fixes
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) <noreply@anthropic.com>
This commit is contained in:
parent
f2af379ff5
commit
7904a99c21
4 changed files with 167 additions and 10 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = '<p class="fm-empty">No front matter.</p>';
|
||||
return;
|
||||
}
|
||||
var html = '<dl class="fm-list">';
|
||||
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 += '<dt>' + escapeHtml(k) + '</dt><dd>' + displayV + '</dd>';
|
||||
}
|
||||
html += '</dl>';
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -58,15 +58,14 @@
|
|||
<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-banner" style="background:var(--bg-secondary);padding:0.75rem 1rem;border-left:3px solid var(--warning);text-align:left;font-size:0.9rem;color:var(--text-muted);margin:1rem;border-radius:var(--radius)">
|
||||
<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">
|
||||
<div id="welcome-banner" style="background:var(--bg-secondary);padding:0.75rem 1rem;border-left:3px solid var(--warning);text-align:left;font-size:0.9rem;color:var(--text-muted);margin-bottom:1rem;border-radius:var(--radius);max-width:36rem">
|
||||
<strong>The Browse app now opens markdown files in this same editor.</strong>
|
||||
Browse provides a unified file tree + per-file-type preview where
|
||||
<code>.md</code> files render in this Toast UI editor. The
|
||||
standalone Markdown Editor remains available for offline single-file
|
||||
editing and air-gapped environments.
|
||||
</div>
|
||||
|
||||
<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>Add Local 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>
|
||||
|
|
|
|||
|
|
@ -438,7 +438,11 @@ test.describe('Archive Browser', () => {
|
|||
|
||||
test('Preview toggle is checked by default', async ({ page }) => {
|
||||
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#filePreviewToggle', { timeout: 15000 });
|
||||
// The checkbox lives inside the (hidden) preview-controls area
|
||||
// before any directory is loaded — waitForSelector defaults to
|
||||
// 'visible' which would time out. Wait for it to be ATTACHED
|
||||
// and verify the underlying state instead.
|
||||
await page.waitForSelector('#filePreviewToggle', { state: 'attached', timeout: 15000 });
|
||||
await expect(page.locator('#filePreviewToggle')).toBeChecked();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue