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:
@@ -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>
|
||||
Reference in New Issue
Block a user