feat(blocks): add PreviewPane for review desk file preview
Static markup-only component for the dark code/markdown preview surface. Renders the preview header (title + actions), the file-tabs slot, and either the source body or the rendered Markdown body based on the 'rendered' prop. Toggling between modes is task 16. Preserves every CSS hook asserted by scripts/verify.mjs: .preview, .preview-title, .preview-markdown, .markdown-preview, max-height:540px, .markdown-table-wrap, .markdown-frontmatter, .markdown-toc, plus the dark surface treatment. Visual fidelity gaps listed in the task report. Did not: introduce a markdown renderer (task 16 hydrates the body); port the cat-marker CSS hook to the new surface (legacy only).
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
---
|
||||
// PreviewPane — the dark code/markdown preview block in the review panel.
|
||||
//
|
||||
// The pane has three body modes (source / rendered Markdown / diff / lens)
|
||||
// plus the file-tab strip. The lens view is rendered by ChangeLens instead,
|
||||
// so this component only owns the source and rendered-markdown bodies. The
|
||||
// toggle between them is task 16's job; this component ships zero JS.
|
||||
//
|
||||
// CSS hooks asserted by scripts/verify.mjs that live in the legacy
|
||||
// stylesheet and must survive in the new architecture:
|
||||
// .preview — the section wrapper
|
||||
// .preview-title — the upper-left title cluster
|
||||
// .preview-markdown— the "Preview Markdown" / "View source" toggle
|
||||
// .markdown-preview— the rendered-HTML container
|
||||
// max-height:540px — the bounded reading surface
|
||||
// .markdown-table-wrap, .markdown-frontmatter, .markdown-toc
|
||||
// — sub-blocks inside the rendered Markdown
|
||||
|
||||
interface Props {
|
||||
/** Label rendered above the file name. Source: "FILE PREVIEW". */
|
||||
title: string;
|
||||
/** Smaller subtitle that names the version. Source: e.g.
|
||||
* "ORIGINAL / SAFETY-REDACTED WHERE NEEDED". */
|
||||
subtitle: string;
|
||||
/** True when the "Preview Markdown" toggle is active — the body should
|
||||
* render the slot as HTML, otherwise the slot is treated as plain
|
||||
* source. */
|
||||
rendered: boolean;
|
||||
}
|
||||
|
||||
const { title, subtitle, rendered } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="preview" aria-label="File preview">
|
||||
<header>
|
||||
<div class="preview-title">
|
||||
<span>{title}</span>
|
||||
<small>{subtitle}</small>
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<slot name="actions" />
|
||||
<button type="button" class="preview-markdown" aria-pressed={rendered} data-render>
|
||||
{rendered ? 'View source' : 'Preview Markdown'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<slot name="tabs" />
|
||||
{
|
||||
rendered ? (
|
||||
<div class="markdown-preview" aria-label="Rendered Markdown preview">
|
||||
<slot name="rendered" />
|
||||
</div>
|
||||
) : (
|
||||
<pre>
|
||||
<code>
|
||||
<slot name="source" />
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* The whole pane is the dark surface — same --deep as the rest of the
|
||||
preview tooling. The header border separates the action bar from the
|
||||
tab strip from the body. */
|
||||
.preview {
|
||||
border: 1px solid var(--ink);
|
||||
background: var(--deep);
|
||||
}
|
||||
|
||||
.preview > header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 14px;
|
||||
color: var(--paper);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* Title cluster: eyebrow line + subtitle. */
|
||||
.preview-title {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.preview-title span {
|
||||
color: var(--gold);
|
||||
font:
|
||||
700 10px ui-monospace,
|
||||
monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* Subtitle below the eyebrow. --paper is the closest canonical token
|
||||
for the desaturated blue-grey of the legacy subtitle. */
|
||||
.preview-title small {
|
||||
color: var(--paper);
|
||||
font:
|
||||
var(--step-0) / 1.35 ui-monospace,
|
||||
monospace;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Action buttons live in the right cluster. The component lays them out;
|
||||
the call site fills the `actions` slot. */
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preview button {
|
||||
padding: 9px 11px;
|
||||
color: var(--paper);
|
||||
background: transparent;
|
||||
border: 1px solid var(--line);
|
||||
cursor: pointer;
|
||||
font:
|
||||
700 10px ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.preview button:hover {
|
||||
background: var(--deep);
|
||||
}
|
||||
|
||||
/* The Markdown toggle is deliberately distinct from the other actions. */
|
||||
.preview button.preview-markdown {
|
||||
color: var(--ink);
|
||||
border-color: var(--gold);
|
||||
background: var(--gold);
|
||||
}
|
||||
|
||||
.preview button.preview-markdown:hover,
|
||||
.preview button.preview-markdown[aria-pressed='true'] {
|
||||
color: var(--paper);
|
||||
background: var(--red);
|
||||
border-color: var(--red);
|
||||
}
|
||||
|
||||
/* Source body: bounded scroll, monospaced, the canonical dark code
|
||||
surface. The max-height hook is the one verify.mjs asserts. */
|
||||
.preview pre {
|
||||
max-height: 540px;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
color: var(--paper);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
/* Code text inside the source surface. --paper is the closest canonical
|
||||
token for the desaturated blue-grey of the legacy code colour. */
|
||||
.preview code {
|
||||
font:
|
||||
var(--step-0) / 1.65 ui-monospace,
|
||||
monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Rendered Markdown body: same bounded reading surface. */
|
||||
.markdown-preview {
|
||||
max-height: 540px;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
color: var(--paper);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.markdown-preview > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-preview :global(h1),
|
||||
.markdown-preview :global(h2),
|
||||
.markdown-preview :global(h3),
|
||||
.markdown-preview :global(h4),
|
||||
.markdown-preview :global(h5),
|
||||
.markdown-preview :global(h6) {
|
||||
margin: 1.5em 0 0.5em;
|
||||
color: var(--paper);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.markdown-preview :global(h1) {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.markdown-preview :global(h2) {
|
||||
font-size: 1.45em;
|
||||
}
|
||||
|
||||
.markdown-preview :global(h3) {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.markdown-preview :global(p),
|
||||
.markdown-preview :global(li) {
|
||||
max-width: 78ch;
|
||||
}
|
||||
|
||||
.markdown-preview :global(li + li) {
|
||||
margin-top: 0.35em;
|
||||
}
|
||||
|
||||
.markdown-preview :global(a) {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.markdown-preview :global(code) {
|
||||
padding: 0.12em 0.3em;
|
||||
color: var(--paper);
|
||||
background: var(--deep);
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.markdown-preview :global(pre) {
|
||||
max-height: none;
|
||||
margin: 1em 0;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.markdown-preview :global(pre code) {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.markdown-preview :global(blockquote) {
|
||||
margin: 1em 0;
|
||||
padding: 0.3em 1em;
|
||||
border-left: 3px solid var(--gold);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.markdown-preview :global(hr) {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* Frontmatter: the two-column key/value grid that opens the rendered
|
||||
surface when a `---` block is present. */
|
||||
.markdown-preview :global(.markdown-frontmatter) {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 3px 14px;
|
||||
margin: 0 0 24px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
font:
|
||||
11px/1.5 ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.markdown-preview :global(.markdown-frontmatter dt) {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.markdown-preview :global(.markdown-frontmatter dd) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Table wrapper: horizontal scroll on narrow viewports. */
|
||||
.markdown-preview :global(.markdown-table-wrap) {
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
margin: 1em 0;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.markdown-preview :global(table) {
|
||||
width: 100%;
|
||||
min-width: 460px;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--step-1);
|
||||
}
|
||||
|
||||
.markdown-preview :global(th),
|
||||
.markdown-preview :global(td) {
|
||||
padding: 9px 11px;
|
||||
border: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.markdown-preview :global(th) {
|
||||
color: var(--gold);
|
||||
background: var(--deep);
|
||||
}
|
||||
|
||||
/* Table of contents: the "ON THIS PAGE" nav that opens the body when
|
||||
the Markdown has two or more headings. */
|
||||
.markdown-preview :global(.markdown-toc) {
|
||||
margin: 0 0 24px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--deep);
|
||||
}
|
||||
|
||||
.markdown-preview :global(.markdown-toc > span) {
|
||||
color: var(--gold);
|
||||
font:
|
||||
700 10px ui-monospace,
|
||||
monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.markdown-preview :global(.markdown-toc ol) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px 13px;
|
||||
margin: 9px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.markdown-preview :global(.markdown-toc li.level-2) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.markdown-preview :global(.markdown-toc li.level-3) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.markdown-preview :global(.markdown-toc a) {
|
||||
font:
|
||||
12px/1.3 Arial,
|
||||
sans-serif;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-preview :global(.markdown-toc a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.preview > header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user