feat(viewer): show document summary together with the original (TODO.md L5)

This commit is contained in:
2026-08-26 19:11:15 -04:00
parent 1925bb66a8
commit 9efffcb428
8 changed files with 501 additions and 7 deletions
+37 -5
View File
@@ -12,7 +12,13 @@
* in a ≤46rem centered column;
* • any other → the raw content as a text node inside
* <pre class="doc-raw"> (mono, horizontal
* scroll).
* scroll);
* • doc.summary non-empty (phase 36 — phase 30 summaries exist
* only on non-markdown docs) → a labeled
* .doc-summary section ABOVE the content;
* null / empty / whitespace renders nothing, so
* markdown docs and fail-soft rows are
* byte-for-byte unchanged.
*
* 2. The /document.html page itself: reads `source`/`path` query
* params, fetches the stateless content endpoint
@@ -40,6 +46,11 @@
* around the page block below). Importing renderDocument elsewhere has
* no side effects: no back-link resolution, no whoami, no content
* fetch, no New Chat binding.
*
* Phase 36: the renderer owns the optional summary panel — a non-empty
* doc.summary renders as a labeled .doc-summary section above the
* original content on BOTH surfaces (page + modal) through this one
* core; the summary text is a text node (XSS contract unchanged).
*/
import { fetchIsAdmin, initSharedHeader } from "./header.js";
@@ -66,10 +77,13 @@ function metaBadge(cls, text) {
/* ---------- shared renderer (phase 26, task 02) ----------
* Populates the three elements every render surface provides: a title,
* a .doc-meta badge row (source · format · mono path · indexed ·
* chunks), and a content container — .doc-md for md/markdown (the
* shared escape-first renderer), <pre class="doc-raw"> otherwise. The
* XSS contract: innerHTML only through renderMarkdown; every
* document-derived string is a text node. */
* chunks), and a content container — an optional .doc-summary section
* first (phase 36: only when doc.summary is non-empty — markdown docs
* and fail-soft rows carry none, so they render exactly as before),
* then .doc-md for md/markdown (the shared escape-first renderer),
* <pre class="doc-raw"> otherwise. The XSS contract: innerHTML only
* through renderMarkdown; every document-derived string (summary text
* included) is a text node. */
export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
titleEl.textContent = doc.title;
// Phase 34 task 04: the titlebar title ellipsizes — the full title
@@ -88,6 +102,24 @@ export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
);
contentEl.replaceChildren();
// Phase 36: the summary panel — labeled section ABOVE the original
// content, on BOTH surfaces (page + modal) through this one core.
// Only a non-empty summary renders: markdown docs carry none (phase
// 30) and the fail-soft path leaves summary NULL, so both are
// byte-for-byte unchanged here.
if (doc.summary && doc.summary.trim() !== "") {
const section = document.createElement("section");
section.className = "doc-summary";
section.setAttribute("aria-label", "Summary");
const title = document.createElement("h2");
title.className = "doc-summary-title";
title.textContent = "Summary";
const body = document.createElement("p");
body.className = "doc-summary-text";
body.textContent = doc.summary; // text node — XSS contract unchanged
section.append(title, body);
contentEl.appendChild(section);
}
if (doc.format === "md" || doc.format === "markdown") {
const wrap = document.createElement("div");
wrap.className = "doc-md";
+40
View File
@@ -1675,6 +1675,45 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
.doc-md code { font-family: var(--mono); font-size: 0.88em; background: var(--brand-soft); padding: 0.08em 0.35em; border-radius: 5px; }
.doc-md pre code { background: none; padding: 0; }
/* Summary panel (phase 36): the labeled "Summary" section the shared
renderDocument core (document.js) draws ABOVE the original content
whenever doc.summary is non-empty (phase 30 — non-markdown docs
only). A "summary, not content" look: the surface card carries a 3px
brand left border (no shadow — the content cards own those) and a
small-caps brand-ink label. Phase-08 tokens only; static content —
no animation (nothing for prefers-reduced-motion to still), and the
aria-label + heading carry the accessibility. */
.doc-summary {
width: 100%;
background: var(--surface);
border: 1px solid var(--line);
border-left: 3px solid var(--brand);
border-radius: var(--radius);
padding: 0.9rem 1.1rem;
margin-bottom: 1rem;
overflow-wrap: anywhere;
}
/* md/markdown: the panel matches the .doc-md ≤46rem centered reading
column — it is the column's label. Raw formats stay full width (the
.doc-raw default above), matching the full-width pre; in engines
without :has() the panel degrades to that full-width default. */
.doc-summary:has(+ .doc-md) {
max-width: 46rem;
margin-inline: auto;
}
.doc-summary-title {
margin: 0 0 0.4rem;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand-ink); /* #a5b4fc on --surface ≈8.7:1 */
}
.doc-summary-text {
margin: 0;
color: var(--ink); /* on --surface ≈14.5:1 */
}
/* Raw (non-markdown) formats: full-width mono pre, horizontal scroll. */
.doc-raw {
width: 100%;
@@ -2011,6 +2050,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
#doc-title { font-size: 1.1rem; }
.doc-path { max-width: 16rem; }
.doc-md { padding: 1.1rem 1rem; }
.doc-summary { padding: 0.75rem 0.9rem; }
.doc-raw { padding: 1rem; font-size: 0.8rem; }
/* Phase 26: the modal bar squeezes like the other bars — the Full page
pill goes icon-only (aria-label keeps the name), the title clips;