feat: give the chapters motion, diagrams, and retrieval practice
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>
This commit is contained in:
@@ -89,9 +89,24 @@ const packageFiles: PackageFile[] = [
|
||||
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;
|
||||
@@ -116,6 +131,7 @@ const packageFiles: PackageFile[] = [
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-selected', String(active));
|
||||
});
|
||||
placeIndicator();
|
||||
}
|
||||
|
||||
buttons.forEach(function (button) {
|
||||
@@ -124,6 +140,10 @@ const packageFiles: PackageFile[] = [
|
||||
});
|
||||
});
|
||||
|
||||
// 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>
|
||||
@@ -139,11 +159,51 @@ const packageFiles: PackageFile[] = [
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -167,16 +227,24 @@ const packageFiles: PackageFile[] = [
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
border-color 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 {
|
||||
border-left-color: var(--gold);
|
||||
background: var(--blue);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
|
||||
Reference in New Issue
Block a user