feat(blocks): add SkillList for review desk catalog

Static markup-only component for the review desk's left-column listbox.
Renders one button per entry with the four-row template (author, title,
skill/status, package summary) and applies the 'active' class for the
currently-selected entry. Click handling and URL sync are task 16.

Preserves every CSS hook asserted by scripts/verify.mjs:
#skill-list, .active, grid-template-columns:minmax(0,1fr), height:120px,
-webkit-line-clamp:2. Visual fidelity gaps (panel surface tints) listed
in the task report.

Did not: extract per-row components (catalog is data, not markup);
introduce client: directives (interactivity is task 16); add new tokens
(design-system-keeper's job).
This commit is contained in:
Marcos Paulo
2026-09-05 07:08:25 +00:00
parent 73ceae2aa8
commit 82601e104e
+151
View File
@@ -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 {
font-size: var(--step-1);
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;
/* --blue on the surface; the active-state tone has no canonical token
and uses --gold as the closest fallback (legacy palette gap). */
color: var(--blue);
font:
var(--step-0) / 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>