From b456dca4b33ad511794f70faf449a8f41b5877f7 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Sat, 16 May 2026 19:47:53 -0400 Subject: [PATCH] feat(ui): render markdown in peek pane - Add marked.js for full markdown rendering - Stream peek body renders as markdown - Card peek non-code content renders as markdown - Code/snippet cards keep escaped pre/code display - Styled: headers, lists, blockquotes, inline code, code blocks, links, hr - Graceful fallback to escHtml if marked fails to load --- web/app.js | 14 +++++++-- web/index.html | 1 + web/style.css | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/web/app.js b/web/app.js index fae28ba..5bff2e8 100644 --- a/web/app.js +++ b/web/app.js @@ -768,7 +768,7 @@ ${fmtDateLong(e.created_at)} ${e.title ? `
${escHtml(e.title)}
` : ''} -
${escHtml(e.body)}
+
${renderMd(e.body)}
${tags ? `
tags
${tags}
` : ''}
context
@@ -825,9 +825,13 @@ if (!hasDecision && e.body) { const lang = data.lang || ''; + const isCode = lang || e.card_type === 'snippet'; + const bodyHtml = isCode + ? `
${escHtml(e.body)}
` + : `
${renderMd(e.body)}
`; sections += `
content${lang ? `${lang}` : ''}${hasFill ? `` : ''}
-
${escHtml(e.body)}
+
${bodyHtml}
`; } @@ -1629,6 +1633,12 @@ return escHtml(s).replace(/'/g, '''); } + function renderMd(s) { + if (!s) return ''; + if (typeof marked === 'undefined') return escHtml(s); + return marked.parse(s, { breaks: true }); + } + function isSafeUrl(url) { return /^https?:\/\//i.test(url); } diff --git a/web/index.html b/web/index.html index 2c29c7b..0f2c4d9 100644 --- a/web/index.html +++ b/web/index.html @@ -82,6 +82,7 @@
+ diff --git a/web/style.css b/web/style.css index 49c87a2..e44e00c 100644 --- a/web/style.css +++ b/web/style.css @@ -877,6 +877,85 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: .peek-body:hover { background: var(--raised); } +.peek-body.md { + font-family: var(--sans); + font-size: 13px; + line-height: 1.65; + white-space: normal; +} + +.peek-body.md h1, .peek-body.md h2, .peek-body.md h3, +.peek-body.md h4, .peek-body.md h5, .peek-body.md h6 { + font-weight: 600; + color: var(--text); + margin: 14px 0 6px; + line-height: 1.3; +} + +.peek-body.md h1 { font-size: 17px; } +.peek-body.md h2 { font-size: 15px; } +.peek-body.md h3 { font-size: 14px; } + +.peek-body.md p { margin: 0 0 10px; } +.peek-body.md p:last-child { margin-bottom: 0; } + +.peek-body.md ul, .peek-body.md ol { + padding-left: 20px; + margin: 0 0 10px; +} + +.peek-body.md li { margin-bottom: 3px; } + +.peek-body.md blockquote { + border-left: 2px solid var(--accent); + padding-left: 12px; + color: var(--muted); + margin: 0 0 10px; +} + +.peek-body.md code { + font-family: var(--mono); + font-size: 11px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--r1); + padding: 1px 5px; +} + +.peek-body.md pre { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--r2); + padding: 10px 12px; + overflow-x: auto; + margin: 0 0 10px; +} + +.peek-body.md pre code { + background: none; + border: none; + padding: 0; + font-size: 11px; + line-height: 1.6; +} + +.peek-body.md a { + color: var(--event); + text-decoration: none; + border-bottom: 1px solid rgba(104,152,200,.3); +} + +.peek-body.md a:hover { border-bottom-color: var(--event); } + +.peek-body.md strong { font-weight: 600; } +.peek-body.md em { font-style: italic; color: var(--muted); } + +.peek-body.md hr { + border: none; + border-top: 1px solid var(--border); + margin: 14px 0; +} + .peek-meta { display: flex; align-items: center;