feat(ui): render markdown in peek pane #18

Merged
lerko merged 1 commits from feat/peek-markdown into main 2026-05-16 23:52:57 +00:00
3 changed files with 92 additions and 2 deletions
+12 -2
View File
@@ -768,7 +768,7 @@
<span class="peek-brow-ts">${fmtDateLong(e.created_at)}</span>
</div>
${e.title ? `<div class="peek-title" data-id="${e.id}">${escHtml(e.title)}</div>` : ''}
<div class="peek-body" data-id="${e.id}">${escHtml(e.body)}</div>
<div class="peek-body md" data-id="${e.id}">${renderMd(e.body)}</div>
${tags ? `<div class="peek-sec"><div class="peek-sec-lbl">tags</div><div class="peek-sec-inner tag-pills">${tags}</div></div>` : ''}
<div class="peek-sec">
<div class="peek-sec-lbl">context</div>
@@ -825,9 +825,13 @@
if (!hasDecision && e.body) {
const lang = data.lang || '';
const isCode = lang || e.card_type === 'snippet';
const bodyHtml = isCode
? `<div class="peek-code"><pre><code>${escHtml(e.body)}</code></pre></div>`
: `<div class="peek-body md">${renderMd(e.body)}</div>`;
sections += `<div class="peek-sec">
<div class="peek-sec-lbl">content${lang ? `<span class="peek-sec-lang">${lang}</span>` : ''}${hasFill ? `<button class="peek-sec-run" onclick="nibApp.enterMode('fill')">⤓ fill</button>` : ''}</div>
<div class="peek-sec-inner"><div class="peek-code"><pre><code>${escHtml(e.body)}</code></pre></div></div>
<div class="peek-sec-inner">${bodyHtml}</div>
</div>`;
}
@@ -1629,6 +1633,12 @@
return escHtml(s).replace(/'/g, '&#39;');
}
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);
}
+1
View File
@@ -82,6 +82,7 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
<script src="/app.js"></script>
</body>
</html>
+79
View File
@@ -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;