79920c9e6c
Three strands of work on the chapter surface, all reading from the same constraint: this site ships no runtime dependencies, so every effect below is native CSS or an .astro component. Motion (src/styles/motion.css, DrawRule.astro). A scroll-driven layer of reveals, hero parallax, section depth, and a hairline that paints itself along its path, scrubbed by `animation-timeline: view()` — 0KB against lottie-web's ~60KB gzipped on the main thread. Every scrubbed rule sits inside `prefers-reduced-motion: no-preference` and `@supports`, so a Firefox reader or an opted-out one gets the complete static page rather than one with holes in it. Ranges key to `cover 40%`-`cover 75%` where the motion is meant to be watched: `view()` ranges key to first visibility, which on a 2600px page is long before anyone is reading the section. `.agents/skills/motion/references/scroll-driven.md` records the technique and the two ways `pathLength` normalisation was broken while building it. Diagrams (StepFlow.astro, WorktreeMap.astro). The `.steps` stack on /skills/, /models/, and /agents/ becomes a numbered flow with connectors, and /agents/ grows the worktree map it was describing in prose — reusing the `trees` collection rather than a second set of strings. The map's static variant is gated one class deeper than full-guide's page styles, so the interactive copy renders byte-identical. Connectors are pseudo-elements, not SVG: a stretched path desynchronises its own dash pattern, and a straight line does not need one. Mermaid was considered and rejected at ~1MB of runtime. Retrieval practice (/rules/, /skills/). A "check yourself" section of native `<details>` question/answer pairs plus a citation row, both bilingual through the existing `data-copy` toggle, and both JavaScript-free. Two audit blind spots surfaced and are closed rather than worked around: `build.inlineStylesheets: 'never'`, because Astro inlined sheets under ~4kB and audit-ui.mjs reads its colour and size baseline from dist/_astro/*.css; and `--columns` declared in the grid components, because an element-level custom property is not a declaration the audit can resolve. legacy/styles/skills.css is renamed and imported for its side effect. Inside a *page*, `?url` resolves to that page's own CSS chunk whatever file it names, so the link pointed at the wrong asset and the sheet was emitted but never loaded — the package preview had been rendering unstyled and overflowing since the Astro cutover. verify.mjs gains 5 assertions for the recall sections and their Portuguese copy: 89 now, against the 84 baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
---
|
|
// 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={index === 0 ? 'true' : 'false'}
|
|
>
|
|
<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]');
|
|
const tree = document.querySelector('[data-package-workbench] .package-tree');
|
|
if (!preview || !dataNode) return;
|
|
const files = JSON.parse(dataNode.textContent || '[]');
|
|
|
|
// Position the gold selection bar over the active button. The bar is a
|
|
// 1px-tall pseudo-element on the tree, moved and stretched by transform
|
|
// alone, so the slide composites — animating its `top`/`height` would
|
|
// force layout on every frame. Measured rather than computed from the
|
|
// index: the buttons are not all the same height once a long filename
|
|
// wraps.
|
|
function placeIndicator() {
|
|
const active = tree && tree.querySelector('.active');
|
|
if (!active) return;
|
|
tree.style.setProperty('--tab-x', active.offsetLeft + 'px');
|
|
tree.style.setProperty('--tab-y', active.offsetTop + 'px');
|
|
tree.style.setProperty('--tab-h', String(active.offsetHeight));
|
|
}
|
|
|
|
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));
|
|
});
|
|
placeIndicator();
|
|
}
|
|
|
|
buttons.forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
renderPackage(button.dataset.skillFile);
|
|
});
|
|
});
|
|
|
|
// Button heights change at the two breakpoints below (the caption is
|
|
// dropped, the padding shrinks), which moves every offset under the bar.
|
|
window.addEventListener('resize', placeIndicator);
|
|
|
|
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 {
|
|
/* Declared, not left to a `var()` fallback: audit-ui.mjs resolves every
|
|
variable a page uses against the stylesheets it links, and a fallback
|
|
does not count as a definition. The script below overrides these
|
|
inline once it has measured the active button. */
|
|
--tab-x: 0;
|
|
--tab-y: 0;
|
|
--tab-h: 0;
|
|
|
|
position: relative;
|
|
padding: 22px 16px;
|
|
border-right: 1px solid var(--muted);
|
|
min-width: 0;
|
|
}
|
|
|
|
/* Selection bar. One mark that slides between files, rather than a border
|
|
that blinks off one button and on to another — the movement is what
|
|
says "this is the same selection, now here". A 1px source box scaled by
|
|
`--tab-h`, so both the travel and the resize are `transform`.
|
|
|
|
`--tab-y` / `--tab-h` are written by the script above. Until it runs the
|
|
bar has zero height and is invisible, which is also what a JS-disabled
|
|
reader sees; `aria-selected` on the buttons is what actually conveys
|
|
the selection (.agents/rules/accessibility.md — motion is redundant
|
|
reinforcement, never the only signal). */
|
|
.package-tree::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 2px;
|
|
height: 1px;
|
|
background: var(--gold);
|
|
/* `--tab-x` because an absolutely positioned child resolves `left: 0`
|
|
against the tree's padding box, which sits 16px (10px at the narrow
|
|
breakpoint) left of where the buttons actually start. */
|
|
transform: translate(var(--tab-x), var(--tab-y)) scaleY(var(--tab-h));
|
|
transform-origin: top left;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
.package-tree::after {
|
|
transition: transform var(--dur-rise) var(--ease-out);
|
|
}
|
|
}
|
|
|
|
.package-tree > p,
|
|
.package-preview > span {
|
|
margin: 0 0 14px;
|
|
color: var(--gold);
|
|
font: 700 var(--step-0) / 1.35 var(--font-mono);
|
|
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;
|
|
color: var(--line);
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition:
|
|
background 0.2s ease,
|
|
transform 0.2s ease;
|
|
}
|
|
|
|
/* The 2px transparent border-left stays: it reserves the gutter the
|
|
selection bar slides through, so the label never shifts when a file is
|
|
selected. */
|
|
.package-tree button:hover,
|
|
.package-tree button:focus-visible,
|
|
.package-tree button.active {
|
|
background: var(--blue);
|
|
}
|
|
|
|
/* Gold belongs to the selection bar now, so focus gets its own mark
|
|
instead of borrowing the border. Inset, so the tree's edge does not
|
|
clip it. */
|
|
.package-tree button:focus-visible {
|
|
outline: 2px solid var(--gold);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
.package-tree button:hover {
|
|
transform: translateX(3px);
|
|
}
|
|
|
|
.package-tree code {
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
font: 700 var(--step-13) / 1.4 var(--font-mono);
|
|
}
|
|
|
|
.package-tree small {
|
|
color: var(--muted);
|
|
font: var(--step-0) / 1.25 var(--font-sans);
|
|
text-align: right;
|
|
}
|
|
|
|
.package-preview {
|
|
min-width: 0;
|
|
padding: 26px;
|
|
background: var(--ink);
|
|
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;
|
|
color: var(--line);
|
|
}
|
|
|
|
.package-preview pre {
|
|
max-width: 100%;
|
|
margin: 20px 0 0;
|
|
padding: 15px;
|
|
overflow: auto;
|
|
border: 1px solid var(--muted);
|
|
background: var(--deep);
|
|
color: var(--line);
|
|
font: var(--step-12) / 1.55 var(--font-mono);
|
|
}
|
|
|
|
.package-preview.is-swapping {
|
|
/* purpose: review-desk detail swap on selection (state change) */
|
|
animation: package-preview-in 200ms cubic-bezier(0.2, 0, 0, 1) 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 var(--muted) mobile layout rule over --ink; no token matches; design-system-keeper */
|
|
border-bottom: 1px solid var(--muted);
|
|
}
|
|
.package-preview {
|
|
padding: 22px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
.package-tree {
|
|
padding: 18px 10px;
|
|
}
|
|
.package-tree button {
|
|
padding: 12px 6px;
|
|
}
|
|
.package-tree small {
|
|
display: none;
|
|
}
|
|
.package-preview {
|
|
padding: 18px;
|
|
}
|
|
.package-preview pre {
|
|
font-size: var(--step-0);
|
|
}
|
|
}
|
|
|
|
@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>
|