diff --git a/src/components/blocks/ChangeLens.astro b/src/components/blocks/ChangeLens.astro new file mode 100644 index 0000000..f149422 --- /dev/null +++ b/src/components/blocks/ChangeLens.astro @@ -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'; +--- + +
+
+
+ {isDiff ? (fileKind === 'skill' ? 'SKILL DIFF' : 'PACKAGE DIFF') : 'CHANGE LENS'} +

+ { + isDiff + ? fileKind === 'skill' + ? 'Original → improved draft' + : 'Supporting file unchanged.' + : 'What changed — and why.' + } +

+
+ +
+ { + isDiff ? ( + fileKind === 'skill' ? ( + <> +

+ Green lines are additions; red lines are removals. Unmarked lines are shared context. +

+
+ {diffLines.map((line) => ( +

+ {line.number} + {line.text} +

+ ))} +
+ + ) : ( +

+ This review only rewrites the main skill contract. The selected {fileKind} file remains + available in its original form. +

+ ) + ) : ( + <> +

+ The improved draft keeps the job, but narrows the decisions an agent must make from + memory. +

+
+ {rows.map((row, index) => ( +
+ + 0{index + 1} / {row.kind} + +
+ − Before +

{row.before}

+
+
+ + After +

{row.after}

+
+ +
+ ))} +
+ + ) + } +
+ + diff --git a/src/components/blocks/FileTabs.astro b/src/components/blocks/FileTabs.astro new file mode 100644 index 0000000..26868cf --- /dev/null +++ b/src/components/blocks/FileTabs.astro @@ -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; +--- + + + + diff --git a/src/components/blocks/PreviewPane.astro b/src/components/blocks/PreviewPane.astro new file mode 100644 index 0000000..73ed231 --- /dev/null +++ b/src/components/blocks/PreviewPane.astro @@ -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; +--- + +
+
+
+ {title} + {subtitle} +
+
+ + +
+
+ + { + rendered ? ( +
+ +
+ ) : ( +
+        
+          
+        
+      
+ ) + } +
+ + diff --git a/src/components/blocks/ReviewDetail.astro b/src/components/blocks/ReviewDetail.astro new file mode 100644 index 0000000..fdd3278 --- /dev/null +++ b/src/components/blocks/ReviewDetail.astro @@ -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)}`; +--- + +
+
+
+ {entry.status} +

{entry.title}

+

+ Submitted by {entry.author} ·{' '} + +

+
+
+ + +
+
+ +
+ THE JOB +

{entry.focus}

+
+ + + +
+
+ WHAT'S ALREADY WORKING +
    + {entry.wins.map((item) =>
  • {item}
  • )} +
+
+
+ HIGHEST-VALUE IMPROVEMENTS +
    + {entry.improve.map((item) =>
  • {item}
  • )} +
+
+
+ + + + + +
+ + diff --git a/src/components/blocks/SkillList.astro b/src/components/blocks/SkillList.astro new file mode 100644 index 0000000..d4317ae --- /dev/null +++ b/src/components/blocks/SkillList.astro @@ -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; +--- + +
+ { + entries.map((entry) => ( + + )) + } +
+ + diff --git a/src/components/blocks/VoteWidget.astro b/src/components/blocks/VoteWidget.astro new file mode 100644 index 0000000..0861176 --- /dev/null +++ b/src/components/blocks/VoteWidget.astro @@ -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 ? ( +
+ READER VOTE +

Voting is offline right now — the vote service is not configured or unreachable.

+
+ ) : ( +
+ WHICH DRAFT WOULD YOU SHIP? +
+ + +
+

+ {youVote + ? `You voted ${youVote === 'original' ? 'original' : 'improved draft'}. Pick the other option to change it.` + : 'One vote per visitor, tracked by network source.'} +

+
+ ) +} + +