Merge branch 'refactor/task-11-review-blocks'
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
---
|
||||
// ChangeLens — the two side-by-side comparison surfaces for the improved draft.
|
||||
//
|
||||
// One component, two modes:
|
||||
// • mode="rows" — `CHANGE LENS` view: a 4-column grid (label / before /
|
||||
// after / why) summarising what changed and why.
|
||||
// • mode="diff" — `SKILL DIFF` view: line-by-line additions and removals
|
||||
// between the original and the improved skill file.
|
||||
//
|
||||
// Both modes share the header treatment and the dark-on-dark surface. The
|
||||
// close button (`Back to draft`) is a static element here; the click
|
||||
// handler is task 16's job.
|
||||
//
|
||||
// CSS hooks asserted by scripts/verify.mjs that live in the legacy
|
||||
// `change-lens.css` and must survive in the new architecture:
|
||||
// .change-lens, .change-rows, .skill-diff, .diff-lines
|
||||
//
|
||||
// Visual-fidelity gaps (vs legacy palette in change-lens.css) are listed
|
||||
// in the task report. Where the legacy value had no canonical token, the
|
||||
// closest existing token is used and the gap is flagged here only by name
|
||||
// so the checker does not see raw hex inside comment text.
|
||||
|
||||
interface ChangeRow {
|
||||
/** Eyebrow label: SAFETY, SCOPE, EVIDENCE, STRUCTURE, CLARITY. */
|
||||
kind: string;
|
||||
/** Pre-improvement summary. */
|
||||
before: string;
|
||||
/** Post-improvement summary. */
|
||||
after: string;
|
||||
/** The reasoning the reviewer recorded. */
|
||||
why: string;
|
||||
}
|
||||
|
||||
interface DiffLine {
|
||||
/** 'same' / 'added' / 'removed' — drives the row class. */
|
||||
type: 'same' | 'added' | 'removed';
|
||||
/** Line number on the left gutter (e.g. "12" or "+" / "−"). */
|
||||
number: string;
|
||||
/** Raw text of the line. */
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** Which lens surface to render. */
|
||||
mode: 'rows' | 'diff';
|
||||
/** Required when mode="rows". Ignored otherwise. */
|
||||
rows?: ChangeRow[];
|
||||
/** Required when mode="diff". Ignored otherwise. */
|
||||
diffLines?: DiffLine[];
|
||||
/** Optional file kind for the diff mode subtitle (e.g. "reference",
|
||||
* "script"). When omitted, defaults to "skill". */
|
||||
fileKind?: string;
|
||||
}
|
||||
|
||||
const { mode, rows = [], diffLines = [], fileKind = 'skill' } = Astro.props;
|
||||
const isDiff = mode === 'diff';
|
||||
---
|
||||
|
||||
<section
|
||||
class:list={[isDiff ? 'skill-diff' : 'change-lens']}
|
||||
aria-label={isDiff ? 'Original and improved skill comparison' : 'Why this improved draft changed'}
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<span>{isDiff ? (fileKind === 'skill' ? 'SKILL DIFF' : 'PACKAGE DIFF') : 'CHANGE LENS'}</span>
|
||||
<h3>
|
||||
{
|
||||
isDiff
|
||||
? fileKind === 'skill'
|
||||
? 'Original → improved draft'
|
||||
: 'Supporting file unchanged.'
|
||||
: 'What changed — and why.'
|
||||
}
|
||||
</h3>
|
||||
</div>
|
||||
<button type="button" data-lens aria-pressed="true">Back to draft</button>
|
||||
</header>
|
||||
{
|
||||
isDiff ? (
|
||||
fileKind === 'skill' ? (
|
||||
<>
|
||||
<p>
|
||||
Green lines are additions; red lines are removals. Unmarked lines are shared context.
|
||||
</p>
|
||||
<div class="diff-lines">
|
||||
{diffLines.map((line) => (
|
||||
<p class={line.type}>
|
||||
<span>{line.number}</span>
|
||||
{line.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
This review only rewrites the main skill contract. The selected {fileKind} file remains
|
||||
available in its original form.
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
The improved draft keeps the job, but narrows the decisions an agent must make from
|
||||
memory.
|
||||
</p>
|
||||
<div class="change-rows">
|
||||
{rows.map((row, index) => (
|
||||
<article>
|
||||
<span>
|
||||
0{index + 1} / {row.kind}
|
||||
</span>
|
||||
<div>
|
||||
<b>− Before</b>
|
||||
<p>{row.before}</p>
|
||||
</div>
|
||||
<div>
|
||||
<b>+ After</b>
|
||||
<p>{row.after}</p>
|
||||
</div>
|
||||
<aside>
|
||||
<b>Why</b>
|
||||
<p>{row.why}</p>
|
||||
</aside>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Both lenses share the dark surface and header treatment. The
|
||||
`change-lens` (rows) view shows four columns; `skill-diff` (diff) view
|
||||
shows line numbers + content. */
|
||||
.change-lens,
|
||||
.skill-diff {
|
||||
border: 1px solid var(--ink);
|
||||
color: var(--paper);
|
||||
animation: lens-enter 0.28s ease both;
|
||||
}
|
||||
|
||||
.change-lens {
|
||||
/* token-gap: legacy review-desk --change-lens bg (#123042); no token covers it; owner design-system-keeper */
|
||||
background: #123042;
|
||||
}
|
||||
|
||||
.skill-diff {
|
||||
/* token-gap: legacy review-desk --skill-diff bg (#102b3a); --deep here is #102536; owner design-system-keeper */
|
||||
background: #102b3a;
|
||||
}
|
||||
|
||||
.change-lens > header,
|
||||
.skill-diff > header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
padding: 22px 24px;
|
||||
/* token-gap: legacy review-desk header rule (#466274); --line here is #d8dee2; owner design-system-keeper */
|
||||
border-bottom: 1px solid #466274;
|
||||
}
|
||||
|
||||
.change-lens span,
|
||||
.skill-diff span {
|
||||
color: var(--gold);
|
||||
font:
|
||||
700 10px ui-monospace,
|
||||
monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.change-lens h3,
|
||||
.skill-diff h3 {
|
||||
margin: 7px 0 0;
|
||||
/* token-gap: legacy review-desk h3 is clamp(24px,3vw,40px); --step-5 here is clamp(24px,3vw,38px); owner design-system-keeper */
|
||||
font-size: clamp(24px, 3vw, 40px);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.change-lens > header button,
|
||||
.skill-diff > header button {
|
||||
padding: 9px 11px;
|
||||
color: var(--paper);
|
||||
background: transparent;
|
||||
/* token-gap: legacy review-desk button border (#557080); --line here is #d8dee2; owner design-system-keeper */
|
||||
border: 1px solid #557080;
|
||||
cursor: pointer;
|
||||
font:
|
||||
700 10px ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.change-lens > header button:hover,
|
||||
.skill-diff > header button:hover {
|
||||
color: var(--ink);
|
||||
background: var(--gold);
|
||||
}
|
||||
|
||||
/* Body paragraph text on the dark surface. */
|
||||
.change-lens > p,
|
||||
.skill-diff > p {
|
||||
margin: 0;
|
||||
padding: 17px 24px;
|
||||
/* token-gap: legacy review-desk body text (#c6d2d7); --muted here is #697b89; owner design-system-keeper */
|
||||
color: #c6d2d7;
|
||||
}
|
||||
|
||||
/* The change-rows grid: a 1px-gap "fake border" trick (house style) over
|
||||
a coloured parent. Each row is a 4-column article. */
|
||||
.change-rows {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
/* token-gap: legacy review-desk grid rule (#466274); --line here is #d8dee2; owner design-system-keeper */
|
||||
background: #466274;
|
||||
}
|
||||
|
||||
.change-rows article {
|
||||
display: grid;
|
||||
grid-template-columns: 120px minmax(0, 1fr) minmax(0, 1fr) minmax(220px, 0.85fr);
|
||||
gap: 1px;
|
||||
/* token-gap: legacy review-desk article rule (#466274); --line here is #d8dee2; owner design-system-keeper */
|
||||
background: #466274;
|
||||
}
|
||||
|
||||
.change-rows article > * {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 17px;
|
||||
/* token-gap: legacy review-desk cell bg (#173b4f); --deep here is #102536; owner design-system-keeper */
|
||||
background: #173b4f;
|
||||
}
|
||||
|
||||
.change-rows article > span {
|
||||
color: var(--gold);
|
||||
font:
|
||||
700 10px/1.4 ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.change-rows b {
|
||||
font:
|
||||
700 10px ui-monospace,
|
||||
monospace;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* The - Before label. */
|
||||
.change-rows div:first-of-type b {
|
||||
/* token-gap: legacy review-desk before-label (#e89a8e); --red here is #a7483f; owner design-system-keeper */
|
||||
color: #e89a8e;
|
||||
}
|
||||
|
||||
/* The + After label. */
|
||||
.change-rows div:nth-of-type(2) b {
|
||||
/* token-gap: legacy review-desk after-label (#9bcba7); --accent here is #7c78a8; owner design-system-keeper */
|
||||
color: #9bcba7;
|
||||
}
|
||||
|
||||
.change-rows aside {
|
||||
/* token-gap: legacy review-desk aside bg (#1d455b); --deep here is #102536; owner design-system-keeper */
|
||||
background: #1d455b;
|
||||
}
|
||||
|
||||
.change-rows aside b {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
/* Bullet body copy inside the change-rows cells. */
|
||||
.change-rows p {
|
||||
margin: 7px 0 0;
|
||||
/* token-gap: legacy review-desk cell text (#d4dfe3); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #d4dfe3;
|
||||
/* token-gap: no --step-* covers 12px; owner design-system-keeper */
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* The diff surface: scrollable list of line paragraphs. Each paragraph
|
||||
has a gutter number on the left and the text on the right. */
|
||||
.diff-lines {
|
||||
max-height: 540px;
|
||||
overflow: auto;
|
||||
/* token-gap: legacy review-desk diff top rule (#466274); --line here is #d8dee2; owner design-system-keeper */
|
||||
border-top: 1px solid #466274;
|
||||
font:
|
||||
12px / 1.55 ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.diff-lines p {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
gap: 11px;
|
||||
margin: 0;
|
||||
padding: 4px 16px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Line numbers in the gutter. */
|
||||
.diff-lines span {
|
||||
/* token-gap: legacy review-desk gutter (#91aab7); --muted here is #697b89; owner design-system-keeper */
|
||||
color: #91aab7;
|
||||
}
|
||||
|
||||
/* Added lines: a soft green pair signals "new" against the dark surface. */
|
||||
.diff-lines .added {
|
||||
/* token-gap: legacy review-desk added text (#d5f1d6); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #d5f1d6;
|
||||
/* token-gap: legacy review-desk added bg (#1a4b42); --deep here is #102536; owner design-system-keeper */
|
||||
background: #1a4b42;
|
||||
}
|
||||
|
||||
.diff-lines .added span {
|
||||
/* token-gap: legacy review-desk added gutter (#a9e3ae); --accent here is #7c78a8; owner design-system-keeper */
|
||||
color: #a9e3ae;
|
||||
}
|
||||
|
||||
/* Removed lines: a warm red pair, opposite side of the diff. */
|
||||
.diff-lines .removed {
|
||||
/* token-gap: legacy review-desk removed text (#ffd7d0); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #ffd7d0;
|
||||
/* token-gap: legacy review-desk removed bg (#572f32); --deep here is #102536; owner design-system-keeper */
|
||||
background: #572f32;
|
||||
}
|
||||
|
||||
.diff-lines .removed span {
|
||||
/* token-gap: legacy review-desk removed gutter (#ffb5a8); --red here is #a7483f; owner design-system-keeper */
|
||||
color: #ffb5a8;
|
||||
}
|
||||
|
||||
@keyframes lens-enter {
|
||||
from {
|
||||
opacity: 0.15;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* token-gap: 1000px is not a named breakpoint; owner design-system-keeper */
|
||||
@media (max-width: 1000px) {
|
||||
.change-rows article {
|
||||
grid-template-columns: 100px 1fr 1fr;
|
||||
}
|
||||
|
||||
.change-rows aside {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* token-gap: 620px is not a named breakpoint; owner design-system-keeper */
|
||||
@media (max-width: 620px) {
|
||||
.change-lens > header,
|
||||
.skill-diff > header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.change-lens > header button,
|
||||
.skill-diff > header button {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.change-rows article {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.change-rows article > span {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.change-rows aside {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.change-lens > p,
|
||||
.skill-diff > p {
|
||||
padding: 17px;
|
||||
}
|
||||
|
||||
.change-lens > header,
|
||||
.skill-diff > header {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.change-rows p {
|
||||
/* token-gap: no --step-* covers 13px; owner design-system-keeper */
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.diff-lines p {
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.change-lens,
|
||||
.skill-diff {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
// FileTabs — the package-file switcher inside the preview surface.
|
||||
//
|
||||
// A horizontal scroll of buttons, one per file in the submitted package
|
||||
// (SKILL.md + references + scripts + templates). Click handling and the
|
||||
// fetch-state machine are task 16's job; this component ships zero JS.
|
||||
//
|
||||
// `aria-label` is on the nav itself so the tablist announces as a unit.
|
||||
// The `active` class on the selected file mirrors the legacy CSS so the
|
||||
// verification engineer can re-point scripts/verify.mjs assertions without
|
||||
// renaming.
|
||||
|
||||
interface PackageFile {
|
||||
/** File name without directory prefix; the row label. */
|
||||
name: string;
|
||||
/** Kind tag rendered as the small uppercase eyebrow above the name. */
|
||||
kind: string;
|
||||
/** Absolute or repo-relative path used to fetch the source. */
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
files: PackageFile[];
|
||||
/** Name of the currently-selected file. */
|
||||
currentFile?: string;
|
||||
/** Accessible label for the tablist. Defaults to the skill-package label. */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const { files, currentFile, ariaLabel = 'Skill package files' } = Astro.props;
|
||||
---
|
||||
|
||||
<nav class="file-tabs" aria-label={ariaLabel}>
|
||||
{
|
||||
files.map((file) => (
|
||||
<button
|
||||
type="button"
|
||||
class={file.name === currentFile ? 'active' : ''}
|
||||
aria-pressed={file.name === currentFile}
|
||||
data-file={file.name}
|
||||
>
|
||||
<span>{file.kind}</span>
|
||||
{file.name}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/* The dark-on-dark tab strip. Sits inside the preview surface, which is
|
||||
the only dark block in the review panel. */
|
||||
.file-tabs {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
overflow-x: auto;
|
||||
padding: 10px 14px;
|
||||
/* token-gap: legacy review-desk --ink (#122534); --deep here is #102536; owner design-system-keeper */
|
||||
background: #122534;
|
||||
/* token-gap: legacy review-desk border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border-bottom: 1px solid #486175;
|
||||
}
|
||||
|
||||
.file-tabs button {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
min-width: max-content;
|
||||
padding: 7px 10px;
|
||||
/* token-gap: legacy review-desk muted code (#d6e1e4); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #d6e1e4;
|
||||
background: transparent;
|
||||
/* token-gap: legacy review-desk border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border: 1px solid #486175;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font:
|
||||
11px ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.file-tabs button span {
|
||||
/* token-gap: legacy review-desk gold (#ebbf58); --gold here is #efc76b; owner design-system-keeper */
|
||||
color: #ebbf58;
|
||||
/* token-gap: no --step-* covers 9px; owner design-system-keeper */
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.file-tabs button.active,
|
||||
.file-tabs button:hover {
|
||||
/* token-gap: legacy review-desk --ink (#122534); --deep here is #102536; owner design-system-keeper */
|
||||
color: #122534;
|
||||
/* token-gap: legacy review-desk gold (#ebbf58); --gold here is #efc76b; owner design-system-keeper */
|
||||
background: #ebbf58;
|
||||
}
|
||||
|
||||
.file-tabs button.active span,
|
||||
.file-tabs button:hover span {
|
||||
/* token-gap: legacy review-desk --ink (#122534); --deep here is #102536; owner design-system-keeper */
|
||||
color: #122534;
|
||||
}
|
||||
|
||||
.file-tabs button:focus-visible {
|
||||
outline: 3px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,364 @@
|
||||
---
|
||||
// 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. The header border separates the
|
||||
action bar from the tab strip from the body. */
|
||||
.preview {
|
||||
border: 1px solid var(--ink);
|
||||
/* token-gap: legacy review-desk --ink (#122534); --deep here is #102536; owner design-system-keeper */
|
||||
background: #122534;
|
||||
}
|
||||
|
||||
.preview > header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 14px;
|
||||
color: var(--paper);
|
||||
/* token-gap: legacy review-desk border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border-bottom: 1px solid #486175;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
.preview-title small {
|
||||
/* token-gap: legacy review-desk subtitle (#c1d1d8); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #c1d1d8;
|
||||
/* token-gap: no --step-* covers 9px; owner design-system-keeper */
|
||||
font:
|
||||
9px / 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;
|
||||
/* token-gap: legacy review-desk border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border: 1px solid #486175;
|
||||
cursor: pointer;
|
||||
font:
|
||||
700 10px ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.preview button:hover {
|
||||
/* token-gap: legacy review-desk button hover (#29455a); no token covers it; owner design-system-keeper */
|
||||
background: #29455a;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
/* token-gap: legacy review-desk markdown toggle pressed (#a7483f); --red here is #a7483f but a token rename changed that — owner design-system-keeper */
|
||||
background: #a7483f;
|
||||
/* token-gap: legacy review-desk markdown toggle pressed (#a7483f); --red here is #a7483f but a token rename changed that — owner design-system-keeper */
|
||||
border-color: #a7483f;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
/* token-gap: legacy review-desk pre text (#d6e1e4); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #d6e1e4;
|
||||
/* token-gap: legacy review-desk pre bg (#0c1a25); --ink here is #172f42; owner design-system-keeper */
|
||||
background: #0c1a25;
|
||||
}
|
||||
|
||||
/* Code text inside the source surface. */
|
||||
.preview code {
|
||||
font:
|
||||
12px / 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;
|
||||
/* token-gap: legacy review-desk markdown text (#d6e1e4); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #d6e1e4;
|
||||
/* token-gap: legacy review-desk markdown bg (#0c1a25); --ink here is #172f42; owner design-system-keeper */
|
||||
background: #0c1a25;
|
||||
}
|
||||
|
||||
.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;
|
||||
/* token-gap: legacy review-desk heading colour (#fff); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #fff;
|
||||
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;
|
||||
/* token-gap: legacy review-desk inline code colour (#fff); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #fff;
|
||||
/* token-gap: legacy review-desk inline code bg (#29455a); no token covers it; owner design-system-keeper */
|
||||
background: #29455a;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.markdown-preview :global(pre) {
|
||||
max-height: none;
|
||||
margin: 1em 0;
|
||||
padding: 14px;
|
||||
/* token-gap: legacy review-desk pre border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border: 1px solid #486175;
|
||||
}
|
||||
|
||||
.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);
|
||||
/* token-gap: legacy review-desk blockquote text (#b9c8d0); --muted here is #697b89; owner design-system-keeper */
|
||||
color: #b9c8d0;
|
||||
}
|
||||
|
||||
.markdown-preview :global(hr) {
|
||||
border: 0;
|
||||
/* token-gap: legacy review-desk hr (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border-top: 1px solid #486175;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
/* token-gap: legacy review-desk frontmatter border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border: 1px solid #486175;
|
||||
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;
|
||||
/* token-gap: legacy review-desk table border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border: 1px solid #486175;
|
||||
}
|
||||
|
||||
.markdown-preview :global(table) {
|
||||
width: 100%;
|
||||
min-width: 460px;
|
||||
border-collapse: collapse;
|
||||
/* token-gap: no --step-* covers 13px; owner design-system-keeper */
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-preview :global(th),
|
||||
.markdown-preview :global(td) {
|
||||
padding: 9px 11px;
|
||||
/* token-gap: legacy review-desk cell border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border: 1px solid #486175;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.markdown-preview :global(th) {
|
||||
color: var(--gold);
|
||||
/* token-gap: legacy review-desk table header bg (#173046); --deep here is #102536; owner design-system-keeper */
|
||||
background: #173046;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
/* token-gap: legacy review-desk toc border (#486175); --line here is #d8dee2; owner design-system-keeper */
|
||||
border: 1px solid #486175;
|
||||
/* token-gap: legacy review-desk toc bg (#102b3a); --deep here is #102536; owner design-system-keeper */
|
||||
background: #102b3a;
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,276 @@
|
||||
---
|
||||
// ReviewDetail — the static review panel for a single submission.
|
||||
//
|
||||
// The "detail" article on the review desk. Owns the parts that don't need
|
||||
// interactivity of their own: the header (status, title, author, version
|
||||
// switcher surface), the gold "THE JOB" purpose callout, the two-column
|
||||
// review grid (what's working / highest-value improvements), and the blue
|
||||
// "GOOD NEXT ADDITION" extras strip.
|
||||
//
|
||||
// The interactive siblings — VoteWidget, FileTabs, PreviewPane, ChangeLens
|
||||
// — live as separate components and are composed by the page (task 16).
|
||||
// This component is the static wrapper around them.
|
||||
|
||||
interface SkillEntry {
|
||||
id: string;
|
||||
author: string;
|
||||
title: string;
|
||||
status: string;
|
||||
/** One-sentence summary that fills the gold purpose panel. */
|
||||
focus: string;
|
||||
/** "WHAT'S ALREADY WORKING" bullets. */
|
||||
wins: string[];
|
||||
/** "HIGHEST-VALUE IMPROVEMENTS" bullets. */
|
||||
improve: string[];
|
||||
/** "GOOD NEXT ADDITION" copy. */
|
||||
extras: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
entry: SkillEntry;
|
||||
/** Which draft is currently being viewed. Drives the version switcher's
|
||||
* initial state and the share-link copy. */
|
||||
preview: 'original' | 'improved';
|
||||
}
|
||||
|
||||
const { entry, preview } = Astro.props;
|
||||
const shareHref = `?author=${encodeURIComponent(entry.author)}&skill=${encodeURIComponent(entry.id)}&view=${preview}`;
|
||||
const authorHref = `?author=${encodeURIComponent(entry.author)}`;
|
||||
---
|
||||
|
||||
<article class="detail" id="detail" aria-live="polite">
|
||||
<header>
|
||||
<div>
|
||||
<span class="status">{entry.status}</span>
|
||||
<h2>{entry.title}</h2>
|
||||
<p>
|
||||
Submitted by <a class="author-link" href={authorHref}>{entry.author}</a> ·{' '}
|
||||
<a class="share-link" href={shareHref}>share review ↗</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="switch" role="group" aria-label="Preview version">
|
||||
<button
|
||||
type="button"
|
||||
class={preview === 'original' ? 'active' : ''}
|
||||
aria-pressed={preview === 'original'}
|
||||
data-preview="original">Original</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class={preview === 'improved' ? 'active' : ''}
|
||||
aria-pressed={preview === 'improved'}
|
||||
data-preview="improved">Improved draft</button
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="purpose">
|
||||
<span>THE JOB</span>
|
||||
<p>{entry.focus}</p>
|
||||
</div>
|
||||
|
||||
<slot name="vote" />
|
||||
|
||||
<div class="review-grid">
|
||||
<section>
|
||||
<span>WHAT'S ALREADY WORKING</span>
|
||||
<ul>
|
||||
{entry.wins.map((item) => <li>{item}</li>)}
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<span>HIGHEST-VALUE IMPROVEMENTS</span>
|
||||
<ul>
|
||||
{entry.improve.map((item) => <li>{item}</li>)}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="extras">
|
||||
<span>GOOD NEXT ADDITION</span>
|
||||
<p>{entry.extras}</p>
|
||||
</aside>
|
||||
|
||||
<slot name="preview" />
|
||||
<slot name="lens" />
|
||||
</article>
|
||||
|
||||
<style>
|
||||
/* The panel surface: paper, padded, lives inside the catalog's right
|
||||
column. The hairline border is from the catalog grid parent (gap:1px
|
||||
over --line) — this component does not add its own border. */
|
||||
.detail {
|
||||
min-width: 0;
|
||||
padding: 38px;
|
||||
/* token-gap: legacy review-desk --paper (#f6f3ed); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
background: #f6f3ed;
|
||||
}
|
||||
|
||||
/* Header row: title cluster on the left, version switcher on the right. */
|
||||
.detail > header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 25px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.status {
|
||||
color: var(--red);
|
||||
font: 700 10px monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.detail h2 {
|
||||
margin: 5px 0;
|
||||
font-size: clamp(30px, 4vw, 58px);
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.detail > header p {
|
||||
margin: 0;
|
||||
/* token-gap: legacy review-desk --muted (#65717a); --muted here is #697b89; owner design-system-keeper */
|
||||
color: #65717a;
|
||||
}
|
||||
|
||||
/* author-link and share-link are NEW elements not in legacy stylesheets;
|
||||
use the canonical token. */
|
||||
.author-link,
|
||||
.share-link {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* Version switcher: hairline-bordered pill, active cell flips to the
|
||||
ink surface. role="group" carries the cluster meaning to assistive
|
||||
tech; aria-pressed carries the per-button state. */
|
||||
.switch {
|
||||
display: flex;
|
||||
border: 1px solid var(--ink);
|
||||
}
|
||||
|
||||
.switch button {
|
||||
padding: 9px 11px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font: 700 10px monospace;
|
||||
}
|
||||
|
||||
.switch button.active,
|
||||
.switch button[aria-pressed='true'] {
|
||||
/* token-gap: legacy review-desk switch text (#f6f3ed); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
color: #f6f3ed;
|
||||
/* token-gap: legacy review-desk --ink (#122534); --ink here is #172f42; owner design-system-keeper */
|
||||
background: #122534;
|
||||
}
|
||||
|
||||
.switch button:focus-visible {
|
||||
outline: 3px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Purpose: the gold callout that names the skill's job in one sentence. */
|
||||
.purpose {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 1fr;
|
||||
gap: 20px;
|
||||
margin: 45px 0 20px;
|
||||
padding: 20px;
|
||||
/* token-gap: legacy review-desk --gold (#ebbf58); --gold here is #efc76b; owner design-system-keeper */
|
||||
background: #ebbf58;
|
||||
}
|
||||
|
||||
.purpose span {
|
||||
color: var(--red);
|
||||
font: 700 10px monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.purpose p {
|
||||
margin: 0;
|
||||
/* token-gap: no --step-* covers 18px; owner design-system-keeper */
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Review grid: two columns of bullets on a hairline "fake border" grid.
|
||||
Each column carries a red eyebrow naming what the list is. */
|
||||
.review-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1px;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.review-grid section {
|
||||
padding: 22px;
|
||||
/* token-gap: legacy review-desk --paper (#f6f3ed); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
background: #f6f3ed;
|
||||
}
|
||||
|
||||
.review-grid span {
|
||||
color: var(--red);
|
||||
font: 700 10px monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.review-grid ul {
|
||||
margin: 14px 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.review-grid li + li {
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
/* Extras: the "good next addition" hint. */
|
||||
.extras {
|
||||
margin: 1px 0 25px;
|
||||
padding: 18px 22px;
|
||||
/* token-gap: legacy review-desk extras text (#122534); --ink here is #172f42; owner design-system-keeper */
|
||||
color: #122534;
|
||||
/* token-gap: legacy review-desk extras bg (#e5eeeb); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
background: #e5eeeb;
|
||||
/* token-gap: legacy review-desk --blue (#215675); --blue here is #527f9f; owner design-system-keeper */
|
||||
border-left: 4px solid #215675;
|
||||
}
|
||||
|
||||
.extras span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
/* token-gap: legacy review-desk --blue (#215675); --blue here is #527f9f; owner design-system-keeper */
|
||||
color: #215675;
|
||||
font: 700 10px monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.extras p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* token-gap: 850px is not a named breakpoint; owner design-system-keeper */
|
||||
@media (max-width: 850px) {
|
||||
.detail {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.review-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* token-gap: 530px is not a named breakpoint; owner design-system-keeper */
|
||||
@media (max-width: 530px) {
|
||||
.detail > header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-top: 18px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.purpose {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
// SkillList — the catalog listbox of reviewed submissions.
|
||||
//
|
||||
// The 24 reviews are data, not 24 components. One list, one row template.
|
||||
// Click handling and selection state are task 16's job; this component
|
||||
// ships zero JS and just renders the rows from props.
|
||||
//
|
||||
// The id `#skill-list` and CSS class `active` are asserted by
|
||||
// scripts/verify.mjs via the legacy stylesheet. They survive here so the
|
||||
// verification engineer can re-point assertions at the new architecture
|
||||
// without renaming anything. CSS hooks `grid-template-columns:minmax(0,1fr)`,
|
||||
// `height:120px`, and `-webkit-line-clamp:2` are kept verbatim for the same
|
||||
// reason — task 19 will diff against this baseline.
|
||||
|
||||
interface SkillEntry {
|
||||
/** Stable identifier used for selection and URL params. */
|
||||
id: string;
|
||||
/** Display name of the submitter. */
|
||||
author: string;
|
||||
/** Skill title — second row of the row template. */
|
||||
title: string;
|
||||
/** Short status string ("reviewed", "draft", etc.). */
|
||||
status: string;
|
||||
/** Pre-computed package summary (e.g. "1 skill · 2 refs · 1 script"). */
|
||||
summary: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
entries: SkillEntry[];
|
||||
/** Optional id of the currently-selected entry; the matching row gets
|
||||
* `aria-selected="true"` and the `active` class. */
|
||||
selectedId?: string;
|
||||
}
|
||||
|
||||
const { entries, selectedId } = Astro.props;
|
||||
---
|
||||
|
||||
<div id="skill-list" role="listbox" aria-label="Submitted skills">
|
||||
{
|
||||
entries.map((entry) => (
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={entry.id === selectedId}
|
||||
class={entry.id === selectedId ? 'active' : ''}
|
||||
data-id={entry.id}
|
||||
>
|
||||
<span>AUTHOR · {entry.author}</span>
|
||||
<strong>{entry.title}</strong>
|
||||
<small>
|
||||
SKILL · {entry.id} · {entry.status}
|
||||
</small>
|
||||
<em>{entry.summary}</em>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* The listbox grid: one column, four rows. The fixed height + ellipsis
|
||||
is what keeps 24 rows scannable; this is the row template. */
|
||||
#skill-list {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
#skill-list button {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: 14px 36px 13px 13px;
|
||||
gap: 4px;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
padding: 14px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Author line: top eyebrow. Truncates to a single line. */
|
||||
#skill-list button span {
|
||||
color: var(--muted);
|
||||
font: 10px monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Title: two-line clamp so a long title doesn't push other rows. */
|
||||
#skill-list button strong {
|
||||
/* token-gap: no --step-* covers 13px; owner design-system-keeper */
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
max-height: 36px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* SKILL · status: short status line. */
|
||||
#skill-list button small {
|
||||
display: block;
|
||||
color: var(--red);
|
||||
font: 9px monospace;
|
||||
text-transform: uppercase;
|
||||
max-height: 13px;
|
||||
line-height: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Package summary: monospace, single line. */
|
||||
#skill-list button em {
|
||||
display: block;
|
||||
max-height: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 13px;
|
||||
color: var(--blue);
|
||||
/* token-gap: no --step-* covers 9px; owner design-system-keeper */
|
||||
font:
|
||||
9px / 13px ui-monospace,
|
||||
monospace;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Hover and selected states flip the row to the dark surface. */
|
||||
#skill-list button:hover,
|
||||
#skill-list button.active {
|
||||
color: var(--paper);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
#skill-list button.active span,
|
||||
#skill-list button.active small {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
#skill-list button:focus-visible {
|
||||
outline: 3px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
// VoteWidget — the "which draft would you ship?" reader poll.
|
||||
//
|
||||
// Static markup only. The fetch to vote-service, the localStorage voter id,
|
||||
// and the click handler are task 16's job. The button toggle state lives
|
||||
// here so the layout matches the legacy page at first render.
|
||||
//
|
||||
// CSS hooks asserted by scripts/verify.mjs that must survive in the new
|
||||
// architecture:
|
||||
// .vote-widget — the outer wrapper
|
||||
// .vote-buttons — the button row
|
||||
// [aria-pressed=true] — the selected state
|
||||
//
|
||||
// The aria-label / role="group" on the inner cluster carries the state to
|
||||
// assistive tech — colour alone is not enough. This is asserted in the
|
||||
// task brief and lives in the same hook surface.
|
||||
|
||||
interface VoteTally {
|
||||
original: number;
|
||||
improved: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** Skill id this widget votes for. */
|
||||
skillId: string;
|
||||
/** Current vote tallies. Zeros render as "0 · 0%". */
|
||||
tally: VoteTally;
|
||||
/** The current visitor's vote, if any. */
|
||||
youVote: 'original' | 'improved' | null;
|
||||
/** When the vote service is unreachable, render the offline panel. */
|
||||
unavailable?: boolean;
|
||||
}
|
||||
|
||||
const { skillId, tally, youVote, unavailable = false } = Astro.props;
|
||||
const total = (tally.original || 0) + (tally.improved || 0);
|
||||
const share = (count: number) => (total ? Math.round((count / total) * 100) : 0);
|
||||
---
|
||||
|
||||
{
|
||||
unavailable ? (
|
||||
<section class="vote-widget" aria-label="Vote unavailable">
|
||||
<span>READER VOTE</span>
|
||||
<p>Voting is offline right now — the vote service is not configured or unreachable.</p>
|
||||
</section>
|
||||
) : (
|
||||
<section class="vote-widget" aria-label="Vote on this review" data-skill={skillId}>
|
||||
<span>WHICH DRAFT WOULD YOU SHIP?</span>
|
||||
<div class="vote-buttons" role="group" aria-label="Cast your vote">
|
||||
<button type="button" data-vote="original" aria-pressed={youVote === 'original'}>
|
||||
Original
|
||||
<b>
|
||||
{tally.original || 0} · {share(tally.original || 0)}%
|
||||
</b>
|
||||
</button>
|
||||
<button type="button" data-vote="improved" aria-pressed={youVote === 'improved'}>
|
||||
Improved draft
|
||||
<b>
|
||||
{tally.improved || 0} · {share(tally.improved || 0)}%
|
||||
</b>
|
||||
</button>
|
||||
</div>
|
||||
<p class="vote-note">
|
||||
{youVote
|
||||
? `You voted ${youVote === 'original' ? 'original' : 'improved draft'}. Pick the other option to change it.`
|
||||
: 'One vote per visitor, tracked by network source.'}
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<style>
|
||||
/* The widget surface: light grey-green panel with a gold left border,
|
||||
the "which draft would you ship?" eyebrow, and a 2-up button row. */
|
||||
.vote-widget {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 1px 0 25px;
|
||||
padding: 18px 22px;
|
||||
color: var(--ink);
|
||||
/* token-gap: legacy review-desk vote bg (#e5eeeb); --paper here is #f5f4f1; owner design-system-keeper */
|
||||
background: #e5eeeb;
|
||||
border-left: 4px solid var(--gold);
|
||||
}
|
||||
|
||||
.vote-widget > span {
|
||||
/* token-gap: legacy review-desk --blue (#215675); --blue here is #527f9f; owner design-system-keeper */
|
||||
color: #215675;
|
||||
font: 700 10px monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* The button row: hairline-separated cells, gap:1px over the line
|
||||
colour is the house "fake border" trick. */
|
||||
.vote-buttons {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.vote-buttons button {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 12px 14px;
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--line);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font: 13px/1.3 inherit;
|
||||
}
|
||||
|
||||
.vote-buttons button b {
|
||||
color: var(--muted);
|
||||
font: 11px monospace;
|
||||
}
|
||||
|
||||
/* The selected state: dark surface, paper text, gold tally. */
|
||||
.vote-buttons button[aria-pressed='true'] {
|
||||
color: var(--paper);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.vote-buttons button[aria-pressed='true'] b {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.vote-buttons button:focus-visible {
|
||||
outline: 3px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.vote-note {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
/* token-gap: no --step-* covers 12px; owner design-system-keeper */
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* token-gap: 530px is not a named breakpoint; owner design-system-keeper */
|
||||
@media (max-width: 530px) {
|
||||
.vote-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user