feat: migrate chapter pages models, agents, skills to Astro
Migrate the three remaining chapter pages to Astro routes, sharing
ChapterLayout and ChapterHero/TopBar/SiteFooter blocks. /summary/
already in place from task 12.
- /models/ -> src/pages/models.astro (zero JS)
- /agents/ -> src/pages/agents.astro (zero JS)
- /skills/ -> src/pages/skills.astro (one island: SkillPackageExplorer)
SkillPackageExplorer is the only JS across the four chapter pages;
moves vanilla skills/app.js content verbatim into the island. Uses
data-skill-file as the new hook (vanilla used data-package-file;
verify.mjs still asserts that on the legacy index.html).
Copy lives in src/content/chapters/{models,agents,skills}.json. All
four pages pass empty-text snapshot diffs against
.agents/snapshots/{models,agents,skills,summary}.txt. pnpm run verify
green: verify.mjs (16 sections), audit-ui.mjs, and check-tokens.mjs
(212 marked token-gap markers, 0 unsuppressed).
Did not touch ChapterLayout, the verification suite, contents of the
summary.astro file (it shipped with task 12), or the vanilla
chapters' HTML files at the repo root (verify.mjs still reads those).
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
---
|
||||
// SkillPackageExplorer — the four-file skill-package picker. The interactive
|
||||
// version of the `data-package-file` buttons that the vanilla skills page
|
||||
// shipped, rewritten for the Astro chapter surface. The four package entries
|
||||
// mirror skills/app.js so the migration preserves content 1:1; the buttons
|
||||
// carry `data-skill-file` and the click handler swaps the preview panel
|
||||
// contents client-side.
|
||||
//
|
||||
// Page is `client:visible` rather than `client:load`: the picker sits below
|
||||
// the hero and grid; deferring until it scrolls into view keeps initial JS
|
||||
// to zero for the above-the-fold content. The page ships zero JS for the
|
||||
// hero/grid/footer parts — only the picker island hydrates.
|
||||
|
||||
interface PackageFile {
|
||||
id: 'skill' | 'references' | 'scripts' | 'assets';
|
||||
prefix: '├── ' | '└── ';
|
||||
label: string;
|
||||
/** Small caption under the file label, mirrors the vanilla source. */
|
||||
caption: string;
|
||||
title: string;
|
||||
body: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
const packageFiles: PackageFile[] = [
|
||||
{
|
||||
id: 'skill',
|
||||
prefix: '├── ',
|
||||
label: 'SKILL.md',
|
||||
caption: 'trigger + workflow',
|
||||
title: 'The operating contract',
|
||||
body: 'The one file that should always be loaded. Define the exact trigger, the ordered workflow, safety limits, and the evidence the agent returns.',
|
||||
code: '---\nname: review-ui\ndescription: Review a changed UI for focus, reflow, and motion.\n---\n\n1. Inspect the changed interaction.\n2. Run the UI checks.\n3. Return findings with evidence.',
|
||||
},
|
||||
{
|
||||
id: 'references',
|
||||
prefix: '├── ',
|
||||
label: 'references/',
|
||||
caption: 'conditional facts',
|
||||
title: 'Facts, only when needed',
|
||||
body: 'Keep conditional detail out of the main instruction. A dialog pattern, framework caveat, or accessibility checklist belongs here when it is not needed for every review.',
|
||||
code: 'references/\n└── accessibility.md\n ├── keyboard interaction patterns\n └── focus and reflow checklist',
|
||||
},
|
||||
{
|
||||
id: 'scripts',
|
||||
prefix: '├── ',
|
||||
label: 'scripts/',
|
||||
caption: 'deterministic checks',
|
||||
title: 'Mechanics that should not depend on memory',
|
||||
body: 'Turn deterministic checks into runnable tools. The agent still judges the result, but it should not have to recreate a viewport test or filename rule by hand.',
|
||||
code: 'scripts/\n└── check-reflow.mjs\n └── checks 320px, 1280px, and 4K widths',
|
||||
},
|
||||
{
|
||||
id: 'assets',
|
||||
prefix: '└── ',
|
||||
label: 'assets/',
|
||||
caption: 'templates + examples',
|
||||
title: 'Starting material, not hidden instructions',
|
||||
body: 'Use assets for templates and examples a person or agent can copy. Keep them clearly named so package readers can choose the right starting point.',
|
||||
code: 'assets/\n├── review-report.md\n└── focus-test-fixture.html',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class="package-workbench" data-package-workbench>
|
||||
<div class="package-tree" role="tablist" aria-label="Files in the review-ui skill package">
|
||||
<p>REVIEW-UI / SKILL PACKAGE</p>
|
||||
{
|
||||
packageFiles.map((file, index) => (
|
||||
<button
|
||||
class:list={[{ active: index === 0 }]}
|
||||
data-skill-file={file.id}
|
||||
role="tab"
|
||||
aria-selected={String(index === 0)}
|
||||
>
|
||||
<code>
|
||||
{file.prefix}
|
||||
{file.label}
|
||||
</code>
|
||||
<small>{file.caption}</small>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<article class="package-preview" id="package-preview" aria-live="polite"></article>
|
||||
</div>
|
||||
|
||||
<script type="application/json" data-skill-files set:html={JSON.stringify(packageFiles)} />
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const preview = document.querySelector('#package-preview');
|
||||
const buttons = document.querySelectorAll('[data-skill-file]');
|
||||
const dataNode = document.querySelector('[data-skill-files]');
|
||||
if (!preview || !dataNode) return;
|
||||
const files = JSON.parse(dataNode.textContent || '[]');
|
||||
|
||||
function renderPackage(id) {
|
||||
const item = files.find((entry) => entry.id === id);
|
||||
if (!item) return;
|
||||
preview.classList.remove('is-swapping');
|
||||
void preview.offsetWidth;
|
||||
preview.classList.add('is-swapping');
|
||||
preview.innerHTML =
|
||||
'<span>SELECTED / ' +
|
||||
item.label +
|
||||
'</span>' +
|
||||
'<h3>' +
|
||||
item.title +
|
||||
'</h3>' +
|
||||
'<p>' +
|
||||
item.body +
|
||||
'</p>' +
|
||||
'<pre><code>' +
|
||||
item.code +
|
||||
'</code></pre>';
|
||||
buttons.forEach(function (button) {
|
||||
const active = button.dataset.skillFile === id;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-selected', String(active));
|
||||
});
|
||||
}
|
||||
|
||||
buttons.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
renderPackage(button.dataset.skillFile);
|
||||
});
|
||||
});
|
||||
|
||||
renderPackage('skill');
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.package-workbench {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(190px, 0.85fr) minmax(0, 1.3fr);
|
||||
min-width: 0;
|
||||
background: var(--ink);
|
||||
border: 1px solid var(--ink);
|
||||
box-shadow: 10px 10px 0 color-mix(in srgb, var(--gold) 55%, transparent);
|
||||
}
|
||||
|
||||
.package-tree {
|
||||
padding: 22px 16px;
|
||||
/* token-gap: legacy 1px solid #426070 over --ink; no token matches; owner design-system-keeper */
|
||||
border-right: 1px solid #426070;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.package-tree > p,
|
||||
.package-preview > span {
|
||||
margin: 0 0 14px;
|
||||
color: var(--gold);
|
||||
/* token-gap: source uses 11px monospace; no --step-* covers 11px on this surface; design-system-keeper */
|
||||
font:
|
||||
700 11px / 1.35 ui-monospace,
|
||||
monospace;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.package-tree button {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 12px 8px;
|
||||
border: 0;
|
||||
border-left: 2px solid transparent;
|
||||
background: transparent;
|
||||
/* token-gap: legacy #d6e1e4 over --ink tree buttons; no token matches; design-system-keeper */
|
||||
color: #d6e1e4;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.package-tree button:hover,
|
||||
.package-tree button:focus-visible,
|
||||
.package-tree button.active {
|
||||
border-left-color: var(--gold);
|
||||
/* token-gap: legacy #1f3a4b active/hover on --ink tree; no token matches; design-system-keeper */
|
||||
background: #1f3a4b;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.package-tree button:hover {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.package-tree code {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font:
|
||||
700 13px / 1.4 ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.package-tree small {
|
||||
/* token-gap: legacy #aebfc7 muted text on --ink tree; no token matches; design-system-keeper */
|
||||
color: #aebfc7;
|
||||
font:
|
||||
11px / 1.25 Arial,
|
||||
sans-serif;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.package-preview {
|
||||
min-width: 0;
|
||||
padding: 26px;
|
||||
/* token-gap: legacy #173245 preview surface (between --ink and --blue); no token matches; design-system-keeper */
|
||||
background: #173245;
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.package-preview h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: clamp(24px, 3vw, 38px);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
|
||||
.package-preview p {
|
||||
max-width: 52ch;
|
||||
margin: 0;
|
||||
/* token-gap: legacy #d6e1e4 muted text on #173245 preview; no token matches; design-system-keeper */
|
||||
color: #d6e1e4;
|
||||
}
|
||||
|
||||
.package-preview pre {
|
||||
max-width: 100%;
|
||||
margin: 20px 0 0;
|
||||
padding: 15px;
|
||||
overflow: auto;
|
||||
/* token-gap: legacy #466274 rule on preview pre border; no token matches; design-system-keeper */
|
||||
border: 1px solid #466274;
|
||||
/* token-gap: legacy #102837 fill on preview pre background; no token matches; design-system-keeper */
|
||||
background: #102837;
|
||||
/* token-gap: legacy #d6e1e4 code text on #102837; no token matches; design-system-keeper */
|
||||
color: #d6e1e4;
|
||||
font:
|
||||
12px / 1.55 ui-monospace,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.package-preview.is-swapping {
|
||||
animation: package-preview-in 0.34s ease both;
|
||||
}
|
||||
|
||||
@keyframes package-preview-in {
|
||||
from {
|
||||
opacity: 0.25;
|
||||
transform: translateY(7px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.package-workbench {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.package-tree {
|
||||
border-right: 0;
|
||||
/* token-gap: legacy #426070 mobile layout rule over --ink; no token matches; design-system-keeper */
|
||||
border-bottom: 1px solid #426070;
|
||||
}
|
||||
.package-preview {
|
||||
padding: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
/* token-gap: legacy 520px breakpoint from skills/styles.css (mobile phone), not in named set 560/800/1100/1600/2200; design-system-keeper */
|
||||
@media (max-width: 520px) {
|
||||
.package-tree {
|
||||
padding: 18px 10px;
|
||||
}
|
||||
.package-tree button {
|
||||
padding: 12px 6px;
|
||||
}
|
||||
.package-tree small {
|
||||
display: none;
|
||||
}
|
||||
.package-preview {
|
||||
padding: 18px;
|
||||
}
|
||||
.package-preview pre {
|
||||
/* token-gap: legacy 11px mobile font-size from skills/styles.css; no --step-* covers 11px; design-system-keeper */
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user