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