dashboard: render markdown file previews in the side panel

clicking a .md / .markdown path reference now opens a marked-rendered
view in the slide-in panel instead of raw text; other files stay raw
in a <pre>. serves the vendored marked bundle at /static/marked.js and
scopes a .md stylesheet to the panel body.
This commit is contained in:
müde 2026-05-20 11:01:16 +02:00
parent f13c3dff8f
commit 0c62bbf1cd
4 changed files with 67 additions and 4 deletions

View file

@ -85,13 +85,25 @@
return text; return text;
} }
// Lazy-load `path` from /api/state-file into the side panel. // Lazy-load `path` from /api/state-file into the side panel.
// Markdown files render through `marked` into a `.md` block; every
// other file stays raw text in a <pre>.
async function openFilePanel(path) { async function openFilePanel(path) {
const pre = el('pre', { class: 'path-preview-body' }, '(fetching…)'); const isMd = /\.(md|markdown)$/i.test(path);
Panel.open('↳ ' + path, pre); const view = isMd
? el('div', { class: 'md' })
: el('pre', { class: 'path-preview-body' });
view.textContent = '(fetching…)';
Panel.open('↳ ' + path, view);
try { try {
pre.textContent = await fetchStateFile(path); const text = await fetchStateFile(path);
if (isMd && window.marked && typeof window.marked.parse === 'function') {
marked.setOptions({ breaks: true, gfm: true });
view.innerHTML = marked.parse(text);
} else {
view.textContent = text;
}
} catch (e) { } catch (e) {
pre.textContent = 'error: ' + (e.message || e); view.textContent = 'error: ' + (e.message || e);
} }
} }
function makePathLink(path) { function makePathLink(path) {

View file

@ -805,3 +805,43 @@ footer a { color: var(--purple); }
overflow: auto; overflow: auto;
padding: 1em; padding: 1em;
} }
/* Markdown file previews rendered by `marked`. TERMINAL_CSS scopes
its own `.md` rules to `.live .row`, so the panel needs its own. */
.side-panel-body .md { color: var(--fg); line-height: 1.5; }
.side-panel-body .md > :first-child { margin-top: 0; }
.side-panel-body .md > :last-child { margin-bottom: 0; }
.side-panel-body .md p { margin: 0.5em 0; }
.side-panel-body .md h1,
.side-panel-body .md h2,
.side-panel-body .md h3,
.side-panel-body .md h4 { color: var(--purple); margin: 0.9em 0 0.4em; }
.side-panel-body .md code {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.05em 0.3em;
font-size: 0.9em;
}
.side-panel-body .md pre {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.6em 0.8em;
overflow-x: auto;
}
.side-panel-body .md pre code { background: none; border: none; padding: 0; }
.side-panel-body .md a { color: var(--cyan); }
.side-panel-body .md ul,
.side-panel-body .md ol { margin: 0.4em 0; padding-left: 1.5em; }
.side-panel-body .md blockquote {
margin: 0.5em 0;
padding-left: 0.8em;
border-left: 2px solid var(--border);
color: var(--muted);
}
.side-panel-body .md table { border-collapse: collapse; margin: 0.5em 0; }
.side-panel-body .md th,
.side-panel-body .md td {
border: 1px solid var(--border);
padding: 0.2em 0.5em;
}

View file

@ -102,6 +102,7 @@
</div> </div>
<script src="/static/hive-fr0nt.js" defer></script> <script src="/static/hive-fr0nt.js" defer></script>
<script src="/static/marked.js" defer></script>
<script src="/static/app.js" defer></script> <script src="/static/app.js" defer></script>
</body> </body>
</html> </html>

View file

@ -65,6 +65,7 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
.route("/dashboard/stream", get(dashboard_stream)) .route("/dashboard/stream", get(dashboard_stream))
.route("/dashboard/history", get(dashboard_history)) .route("/dashboard/history", get(dashboard_history))
.route("/static/hive-fr0nt.js", get(serve_shared_js)) .route("/static/hive-fr0nt.js", get(serve_shared_js))
.route("/static/marked.js", get(serve_marked_js))
.with_state(AppState { coord }); .with_state(AppState { coord });
let addr = SocketAddr::from(([0, 0, 0, 0], port)); let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = bind_with_retry(addr).await?; let listener = bind_with_retry(addr).await?;
@ -146,6 +147,15 @@ async fn serve_shared_js() -> impl IntoResponse {
) )
} }
/// Vendored `marked` bundle — the side panel renders markdown file
/// previews with it.
async fn serve_marked_js() -> impl IntoResponse {
(
[("content-type", "application/javascript")],
hive_fr0nt::MARKED_JS,
)
}
#[derive(Serialize)] #[derive(Serialize)]
struct StateSnapshot { struct StateSnapshot {
/// Broker seq at the moment this snapshot was assembled. Clients /// Broker seq at the moment this snapshot was assembled. Clients