Merge branch 'main' into refactor/task-05b-guide-interactive-data
This commit is contained in:
@@ -20,11 +20,13 @@ palettes and a broken `@font-face`. **Load skills**: `design-tokens`,
|
|||||||
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
|
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
|
||||||
sub-perceptual and can be canonicalized. `--blue` (`#527f9f` vs `#215675`) is
|
sub-perceptual and can be canonicalized. `--blue` (`#527f9f` vs `#215675`) is
|
||||||
visibly different — screenshot both and get a human decision.
|
visibly different — screenshot both and get a human decision.
|
||||||
2. **The fonts have never rendered.** The `@font-face` in `styles.css:1` points
|
2. ~~**The fonts have never rendered.**~~ **Settled 2026-09-05 — do not
|
||||||
`src:` at a Google Fonts _stylesheet_, so Manrope and DM Mono have always
|
reopen.** The malformed `@font-face` was escalated and the human chose the
|
||||||
fallen back to Arial and generic monospace. Self-hosting them is a redesign,
|
real fonts. Manrope and DM Mono are self-hosted in `public/fonts/`, wired
|
||||||
not a refactor. Default: delete the dead rule, declare the stacks that
|
through `public/fonts/fonts.css`, which `BaseLayout.astro` links and the
|
||||||
actually render. Escalate if someone wants the real fonts.
|
legacy root `styles.css` `@import`s. **Do not delete these faces and do not
|
||||||
|
replace the stacks with `Arial`/`ui-monospace`** — that instruction is
|
||||||
|
obsolete. You may add `--font-sans` / `--font-mono` tokens pointing at them.
|
||||||
|
|
||||||
## You own
|
## You own
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ The same semantic names carry different values depending on which stylesheet
|
|||||||
loaded them:
|
loaded them:
|
||||||
|
|
||||||
| Token | `styles.css`, `rules/styles.css` | `chapters.css`, `skills-review/styles.css` | `hands-on/*/styles.css` |
|
| Token | `styles.css`, `rules/styles.css` | `chapters.css`, `skills-review/styles.css` | `hands-on/*/styles.css` |
|
||||||
| --- | --- | --- | --- |
|
| ---------- | -------------------------------- | ------------------------------------------ | ----------------------- |
|
||||||
| `--paper` | `#f5f4f1` | `#f6f3ed` | `#f4f3ef` |
|
| `--paper` | `#f5f4f1` | `#f6f3ed` | `#f4f3ef` |
|
||||||
| `--ink` | `#172f42` | `#122534` | `#173044` |
|
| `--ink` | `#172f42` | `#122534` | `#173044` |
|
||||||
| `--muted` | `#697b89` | `#65717a` | `#687d8c` |
|
| `--muted` | `#697b89` | `#65717a` | `#687d8c` |
|
||||||
@@ -34,35 +34,36 @@ exception: `#527f9f` vs `#215675` is a visible difference and may be deliberate.
|
|||||||
Do not "just pick one" silently in the middle of another task. This is its own
|
Do not "just pick one" silently in the middle of another task. This is its own
|
||||||
reviewed change with visual diffs attached.
|
reviewed change with visual diffs attached.
|
||||||
|
|
||||||
## The typography you see is not the typography that was written
|
## The typography — fixed 2026-09-05
|
||||||
|
|
||||||
`styles.css` line 1:
|
`styles.css` line 1 used to read:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
@font-face{font-family:Manrope;src:url('https://fonts.googleapis.com/css2?family=DM+Mono&family=Manrope:wght@400;600;700;800&display=swap')}
|
@font-face {
|
||||||
|
font-family: Manrope;
|
||||||
|
src: url('https://fonts.googleapis.com/css2?family=DM+Mono&family=Manrope:wght@400;600;700;800&display=swap');
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`src:` points at a **CSS stylesheet**, not a font file. No browser can load a
|
`src:` in an `@font-face` must point at a font binary. That URL returns a CSS
|
||||||
font from that, so:
|
stylesheet, so no browser could load a face from it. For the whole life of the
|
||||||
|
site, every `font-family:Manrope,Arial,sans-serif` rendered as **Arial** and
|
||||||
|
every `font:… 'DM Mono',monospace` rendered as the **generic monospace** face —
|
||||||
|
`'DM Mono'` was never declared as a family at all.
|
||||||
|
|
||||||
- every `font-family:Manrope,Arial,sans-serif` renders as **Arial**
|
**This was escalated and the human chose the real fonts.** Manrope and DM Mono
|
||||||
- every `font:… 'DM Mono',monospace` renders as the **generic monospace** face
|
are now self-hosted in `public/fonts/`, latin and latin-ext subsets only, under
|
||||||
- there are no `@font-face` blocks anywhere else and zero font files in the repo
|
the SIL Open Font License. One `fonts.css` serves both trees: Astro links it
|
||||||
- `scripts/audit-ui.mjs` only rejects external `<link>`/`<script>` tags, so this
|
from `BaseLayout.astro`, the legacy root `styles.css` `@import`s it. Self-hosted
|
||||||
slipped through the "dependency-free" audit
|
rather than linked from Google because `scripts/audit-ui.mjs` rejects any
|
||||||
|
external `<link>`/`<script>`, and because the site is presented in workshop
|
||||||
|
rooms with unreliable networks.
|
||||||
|
|
||||||
**This is a trap for the refactor.** Self-hosting Manrope and DM Mono in Astro
|
**This changed how every page renders**, deliberately. It is the one sanctioned
|
||||||
is the obvious "fix" — and it would change how every page looks, violating
|
visual change in the migration. Screenshots taken before 2026-09-05 show Arial
|
||||||
"maintain the same styles". Treat it as an explicit product decision:
|
and are no longer a valid baseline.
|
||||||
|
|
||||||
- **Keep current rendering**: delete the dead `@font-face`, replace the font
|
`Georgia, serif` for emphasis (`h1 em`, `.hero em`) is untouched and still real.
|
||||||
stacks with what actually renders today (`Arial, sans-serif` /
|
|
||||||
`ui-monospace, monospace`). Zero visual change. Honest CSS.
|
|
||||||
- **Adopt the intended fonts**: self-host the woff2 files in `public/fonts/`,
|
|
||||||
add real `@font-face` with `font-display:swap`. Better-looking, but it is a
|
|
||||||
redesign and needs sign-off plus fresh screenshots.
|
|
||||||
|
|
||||||
Default to the first unless a human says otherwise.
|
|
||||||
|
|
||||||
## Type scale
|
## Type scale
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ real — it is a system font, so it does render. Keep it.
|
|||||||
Sizes are all `clamp()`, roughly:
|
Sizes are all `clamp()`, roughly:
|
||||||
|
|
||||||
| Role | Value |
|
| Role | Value |
|
||||||
| --- | --- |
|
| --------------- | --------------------------------------------------------- |
|
||||||
| Display / `h1` | `clamp(56px,9vw,126px)` |
|
| Display / `h1` | `clamp(56px,9vw,126px)` |
|
||||||
| Section `h2` | `clamp(36px,5vw,65px)` |
|
| Section `h2` | `clamp(36px,5vw,65px)` |
|
||||||
| Sub-head | `clamp(24px,3vw,38px)` |
|
| Sub-head | `clamp(24px,3vw,38px)` |
|
||||||
@@ -87,11 +88,12 @@ should be unchanged within a pixel or two at common viewports.
|
|||||||
## Breakpoints
|
## Breakpoints
|
||||||
|
|
||||||
Sixteen distinct max-widths are in use: 420, 520, 530, 560, 600, 620, 720, 800,
|
Sixteen distinct max-widths are in use: 420, 520, 530, 560, 600, 620, 720, 800,
|
||||||
850, 880, 900, 1000, 1050, 1100 — plus `min-width:1600px` and `min-width:2200px`.
|
850, 880, 900, 1000, 1050, 1100 — plus `min-width:1600px` and
|
||||||
|
`min-width:2200px`.
|
||||||
|
|
||||||
Collapse to a named set (suggested: 560 / 800 / 1100 / 1600 / 2200) and prove
|
Collapse to a named set (suggested: 560 / 800 / 1100 / 1600 / 2200) and prove
|
||||||
equivalence with screenshots at the *old* breakpoint values, since that is
|
equivalence with screenshots at the _old_ breakpoint values, since that is where
|
||||||
where regressions will hide.
|
regressions will hide.
|
||||||
|
|
||||||
`@media(prefers-reduced-motion:reduce)` is already respected in several
|
`@media(prefers-reduced-motion:reduce)` is already respected in several
|
||||||
stylesheets. Keep it — see [`../rules/animation.md`](../rules/animation.md).
|
stylesheets. Keep it — see [`../rules/animation.md`](../rules/animation.md).
|
||||||
@@ -100,8 +102,8 @@ stylesheets. Keep it — see [`../rules/animation.md`](../rules/animation.md).
|
|||||||
|
|
||||||
The visual identity is editorial-print: flat colour blocks, hairline `1px`
|
The visual identity is editorial-print: flat colour blocks, hairline `1px`
|
||||||
rules, uppercase monospace eyebrows with wide tracking, very tight negative
|
rules, uppercase monospace eyebrows with wide tracking, very tight negative
|
||||||
letter-spacing on display type (`-.06em` … `-.08em`), grid layouts with `gap:1px`
|
letter-spacing on display type (`-.06em` … `-.08em`), grid layouts with
|
||||||
over a background colour to fake borders, and near-zero border-radius.
|
`gap:1px` over a background colour to fake borders, and near-zero border-radius.
|
||||||
|
|
||||||
That last trick (`gap:1px` + parent background) is used everywhere. It is
|
That last trick (`gap:1px` + parent background) is used everywhere. It is
|
||||||
intentional. Do not replace it with `border`.
|
intentional. Do not replace it with `border`.
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
hands-on
|
||||||
|
public/hands-on
|
||||||
|
submitted-skills
|
||||||
|
public/submitted-skills
|
||||||
|
skill-reviews
|
||||||
|
vote-service
|
||||||
|
|
||||||
|
# Legacy site sources, slated for deletion at cutover (task 20). Same list and
|
||||||
|
# same reasoning as .prettierignore: these are minified, single-line
|
||||||
|
# stylesheets. stylelint's `declaration-block-single-line-max-declarations`
|
||||||
|
# fires once per rule in them — ~180 errors for `styles.css` alone — so staging
|
||||||
|
# one to change a single declaration blocks the commit outright. The rule is
|
||||||
|
# about hand-written source readability and says nothing useful about minified
|
||||||
|
# output that is about to be deleted.
|
||||||
|
#
|
||||||
|
# `public/fonts/fonts.css` is deliberately NOT here: it is new, hand-written,
|
||||||
|
# and must stay linted.
|
||||||
|
#
|
||||||
|
# Root-anchored on purpose: a bare `rules` would also swallow .agents/rules/.
|
||||||
|
/styles.css
|
||||||
|
/landing.css
|
||||||
|
/chapters.css
|
||||||
|
/responsive.css
|
||||||
|
/skills-review/
|
||||||
|
/rules/
|
||||||
|
/skills/
|
||||||
|
/full-guide/
|
||||||
@@ -45,6 +45,8 @@ Phase 4 polish 18 ∥ 19, then 20
|
|||||||
| --- | ----------------------------------------------------------------- | --------------------- | ------------------ | ------------- |
|
| --- | ----------------------------------------------------------------- | --------------------- | ------------------ | ------------- |
|
||||||
| 01 | [scaffold + gates](task-01-scaffold.md) | astro-architect | — | — |
|
| 01 | [scaffold + gates](task-01-scaffold.md) | astro-architect | — | — |
|
||||||
| 02 | [design tokens](task-02-tokens.md) | design-system-keeper | 01 | 03, 04 |
|
| 02 | [design tokens](task-02-tokens.md) | design-system-keeper | 01 | 03, 04 |
|
||||||
|
| 02b | [token layer wiring](task-02b-token-layer-wiring.md) | design-system-keeper | 02 | 03, 04 |
|
||||||
|
| 02c | [token-gap queue](task-02c-token-gaps.md) | design-system-keeper | 02b, 15d | — |
|
||||||
| 03 | [verification net](task-03-verification-net.md) | verification-engineer | 01 | 02, 04 |
|
| 03 | [verification net](task-03-verification-net.md) | verification-engineer | 01 | 02, 04 |
|
||||||
| 04 | [content schema](task-04-content-schema.md) | content-i18n-migrator | 01 | 02, 03 |
|
| 04 | [content schema](task-04-content-schema.md) | content-i18n-migrator | 01 | 02, 03 |
|
||||||
| 05 | [guide content](task-05-content-guide.md) | content-i18n-migrator | 04 | 06 |
|
| 05 | [guide content](task-05-content-guide.md) | content-i18n-migrator | 04 | 06 |
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ walk into.
|
|||||||
|
|
||||||
## Do not
|
## Do not
|
||||||
|
|
||||||
- Do not add a webfont.
|
- Do not add a webfont. **(Reversed 2026-09-05 — see Decisions.)**
|
||||||
- Do not rename tokens to numeric scales (`--color-neutral-900`).
|
- Do not rename tokens to numeric scales (`--color-neutral-900`).
|
||||||
- Do not convert the `gap:1px` over a coloured parent trick into `border` — it
|
- Do not convert the `gap:1px` over a coloured parent trick into `border` — it
|
||||||
is deliberate house style and appears everywhere.
|
is deliberate house style and appears everywhere.
|
||||||
@@ -60,7 +60,9 @@ walk into.
|
|||||||
- **Palette Consolidation:** The three drifted palettes were canonicalized into
|
- **Palette Consolidation:** The three drifted palettes were canonicalized into
|
||||||
one. For `--blue`, `#527f9f` (from styles.css) was chosen over `#215675` (from
|
one. For `--blue`, `#527f9f` (from styles.css) was chosen over `#215675` (from
|
||||||
chapters.css) to unify the look.
|
chapters.css) to unify the look.
|
||||||
- **Fonts:** The broken `@font-face` rule pointing to a Google Fonts stylesheet
|
- **Fonts (superseded 2026-09-05):** task 02 recorded "delete the dead rule,
|
||||||
was noted to be deleted in future component tasks. The font stacks will be
|
fall back to `Arial, sans-serif` / `ui-monospace, monospace`, zero visual
|
||||||
replaced with `Arial, sans-serif` and `ui-monospace, monospace` to match what
|
change", and deferred even that deletion to "future component tasks", which
|
||||||
has actually been rendering all along. Zero visual change.
|
never picked it up. The human has since chosen the opposite: Manrope and DM
|
||||||
|
Mono are now self-hosted in `public/fonts/` and really render. The "Do not add
|
||||||
|
a webfont" line above no longer applies.
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Task 02b — Wire the token layer in
|
||||||
|
|
||||||
|
**Agent**: `design-system-keeper` · **Model**: `agy` (Gemini 3.1 Pro — needs
|
||||||
|
vision for the screenshot diffs) **Depends on**: 02 · **Blocks**: 02c
|
||||||
|
**Worktree**: `.agents/scripts/worktree.sh start 02b token-layer-wiring`
|
||||||
|
|
||||||
|
## Why this task exists
|
||||||
|
|
||||||
|
Task 02 built the token layer and it was never connected to anything. This is
|
||||||
|
not a criticism of that task — its brief and its done-when list never asked for
|
||||||
|
the wiring, and `check-tokens.mjs` only looks for _violations_ in `src/`, so
|
||||||
|
nothing could have caught it.
|
||||||
|
|
||||||
|
Verify each of these yourself before you start; do not take my word for it:
|
||||||
|
|
||||||
|
1. **`src/styles/tokens.css` is imported by nothing.** The only reference in the
|
||||||
|
repo is `src/styles/base.css:2`, and `base.css` is itself imported nowhere.
|
||||||
|
The whole layer is dead code.
|
||||||
|
2. **The pages import the legacy stylesheets instead**, drifted `:root` blocks
|
||||||
|
and all:
|
||||||
|
- `src/layouts/ChapterLayout.astro:16` → `../../chapters.css?url`
|
||||||
|
- `src/pages/skills.astro:11` → `../../skills/styles.css?url`
|
||||||
|
- `src/pages/skills-review.astro:4,5` → `skills-review/styles.css`,
|
||||||
|
`change-lens.css`
|
||||||
|
3. **So task 02's `--blue` decision never reached a page.** It chose `#527f9f`
|
||||||
|
and wrote it down. The built CSS today ships both values:
|
||||||
|
```
|
||||||
|
dist/_astro/chapters.*.css --blue:#215675
|
||||||
|
dist/_astro/rules.*.css --blue: #527f9f
|
||||||
|
dist/_astro/styles.*.css --blue:#215675
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
`src/styles/tokens.css`, `src/styles/base.css`,
|
||||||
|
`.agents/scripts/check-tokens.mjs` — you are the only writer of these — plus the
|
||||||
|
four import sites listed above and whatever layout work the wiring needs.
|
||||||
|
|
||||||
|
## Deliver
|
||||||
|
|
||||||
|
### 1. One loaded token layer
|
||||||
|
|
||||||
|
Make `tokens.css` actually load on every route. Either fix `base.css` and import
|
||||||
|
it from the layouts, or drop `base.css` and import `tokens.css` directly — your
|
||||||
|
call, but say which and why. A file that exists and is never loaded is worse
|
||||||
|
than no file, because it reads as done.
|
||||||
|
|
||||||
|
### 2. The legacy stylesheet imports reconciled
|
||||||
|
|
||||||
|
Those three `?url` imports are how the drifted palettes reach production. You do
|
||||||
|
not have to delete them in this task — much of that CSS is still load-bearing
|
||||||
|
until task 15e and task 20 — but the `:root` blocks inside them must stop
|
||||||
|
overriding the canonical tokens. Make the layer authoritative and prove with
|
||||||
|
screenshots that nothing moved except what you intend.
|
||||||
|
|
||||||
|
`--blue` is the one real difference: `#527f9f` vs `#215675` is visible. Task 02
|
||||||
|
already decided `#527f9f`. Apply that decision; do not re-open it. If applying
|
||||||
|
it looks wrong on a real page, screenshot it and escalate rather than quietly
|
||||||
|
picking the other value.
|
||||||
|
|
||||||
|
### 3. Font tokens
|
||||||
|
|
||||||
|
Manrope and DM Mono are **real now** — self-hosted in `public/fonts/`, wired
|
||||||
|
through `public/fonts/fonts.css` (linked by `BaseLayout.astro`, `@import`ed by
|
||||||
|
the legacy root `styles.css`). Read `.agents/context/design-system.md`; the old
|
||||||
|
"delete the dead rule, fall back to Arial" instruction is **superseded and must
|
||||||
|
not be re-applied**.
|
||||||
|
|
||||||
|
Add `--font-sans` and `--font-mono` and point the stacks at them. Do not touch
|
||||||
|
`public/fonts/fonts.css` itself, and do not remove the faces.
|
||||||
|
|
||||||
|
### 4. A check so this cannot recur
|
||||||
|
|
||||||
|
The gap that let a dead token layer ship is that nothing asserts the tokens are
|
||||||
|
_loaded_. Add that check — a build-output assertion that every route resolves
|
||||||
|
the canonical palette, or an equivalent. Put it in `check-tokens.mjs`, which you
|
||||||
|
own. **Do not touch `scripts/verify.mjs`**; it stays at 42 assertions and only
|
||||||
|
the verification-engineer may change it.
|
||||||
|
|
||||||
|
## Screenshots: your baseline is HEAD, not history
|
||||||
|
|
||||||
|
The self-hosted fonts landed in `7f11b6e` and **changed how every page
|
||||||
|
renders**, deliberately — that is the one sanctioned visual change in this
|
||||||
|
migration. Any screenshot taken before it shows Arial and is not a valid
|
||||||
|
baseline. Capture your own "before" from HEAD at the start of this task.
|
||||||
|
|
||||||
|
Widths: 560 / 800 / 1100 / 1600, plus the eight breakpoints task 02 removed
|
||||||
|
(520, 530, 600, 620, 720, 850, 880, 900) — that is where regressions hide.
|
||||||
|
|
||||||
|
## Do not
|
||||||
|
|
||||||
|
- Do not resolve the ~164 `token-gap:` markers. That is task 02c, and doing it
|
||||||
|
here would collide with task 15d's page work. Report the count.
|
||||||
|
- Do not delete `responsive.css`. That is 15e.
|
||||||
|
- Do not convert the `gap:1px` over a coloured parent trick into `border` — it
|
||||||
|
is deliberate house style and appears everywhere.
|
||||||
|
- Do not rename tokens to numeric scales (`--color-neutral-900`).
|
||||||
|
- Never use `--no-verify`; never set `core.hooksPath`.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
- [ ] `tokens.css` loads on every route, proven from built output
|
||||||
|
- [ ] One value per token in the shipped CSS — `--blue` is `#527f9f` everywhere
|
||||||
|
- [ ] `--font-sans` / `--font-mono` exist and are used; the faces still load
|
||||||
|
- [ ] A check exists that fails if the token layer stops being loaded
|
||||||
|
- [ ] Before/after screenshots at all twelve widths, every difference explained
|
||||||
|
- [ ] `pnpm run gate` green — the full gate; 42 assertions intact
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Task 02c — Work the token-gap queue
|
||||||
|
|
||||||
|
**Agent**: `design-system-keeper` · **Model**: `agy` (Gemini 3.1 Pro) **Depends
|
||||||
|
on**: 02b, 15d · **Blocks**: 19 **Worktree**:
|
||||||
|
`.agents/scripts/worktree.sh start 02c token-gaps`
|
||||||
|
|
||||||
|
## Why this task exists
|
||||||
|
|
||||||
|
`check-tokens.mjs` bans raw hex, `font-size: Npx` and ad-hoc breakpoints outside
|
||||||
|
the token layer. Until the escape hatch was added it offered no legal
|
||||||
|
alternative, so agents told both "keep the site identical" and "get the gate
|
||||||
|
green" cheated — writing px through the `font:` shorthand the regex misses, or
|
||||||
|
substituting the nearest token and shipping a silent redesign (`12px` and `14px`
|
||||||
|
both became `var(--step-1)`, 15px; a diff-added green became `var(--accent)`,
|
||||||
|
purple).
|
||||||
|
|
||||||
|
The `/* token-gap: <reason>; owner design-system-keeper */` marker replaced that
|
||||||
|
with an honest, visible queue. **You are the owner it names.** There are ~164
|
||||||
|
markers on `main`; get the real count yourself from `check-tokens.mjs` output.
|
||||||
|
|
||||||
|
## The work
|
||||||
|
|
||||||
|
For each marked value, one of three outcomes — and the choice is yours to make,
|
||||||
|
not to defer:
|
||||||
|
|
||||||
|
1. **The scale should cover it.** Extend `--step-*` and point the site at it.
|
||||||
|
Most of the queue is this: `24px`, `25px`, `20px`, `17px`, `13px` fixed sizes
|
||||||
|
from `chapters.css` that fall between existing steps.
|
||||||
|
2. **It is genuinely one-off.** Keep the literal value, delete the marker, and
|
||||||
|
leave a comment saying why it is exempt.
|
||||||
|
3. **It was already wrong.** Some markers record a near-miss substitution an
|
||||||
|
earlier task made under duress. Restoring the true legacy value is a visible
|
||||||
|
change — screenshot it and say so.
|
||||||
|
|
||||||
|
Work in batches by source file and commit per batch. A single 164-site commit is
|
||||||
|
unreviewable.
|
||||||
|
|
||||||
|
## Do not
|
||||||
|
|
||||||
|
- Do not weaken `check-tokens.mjs` to make markers disappear. Removing a check
|
||||||
|
is not resolving a gap.
|
||||||
|
- Do not touch `scripts/verify.mjs` — 42 assertions, verification-engineer only.
|
||||||
|
- Do not leave a marker whose reason you have satisfied.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
- [ ] Every marker either resolved into the scale or documented as a deliberate
|
||||||
|
exemption
|
||||||
|
- [ ] `check-tokens.mjs` reports zero unexplained gaps
|
||||||
|
- [ ] Screenshots for every change that moved a pixel, differences explained
|
||||||
|
- [ ] `pnpm run gate` green; 42 assertions intact
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Font licences
|
||||||
|
|
||||||
|
Both families here are licensed under the SIL Open Font License, Version 1.1,
|
||||||
|
which permits redistribution and self-hosting.
|
||||||
|
|
||||||
|
| Family | Designer | Source |
|
||||||
|
| ------- | ---------------------------- | -------------------------------------- |
|
||||||
|
| Manrope | Mikhail Sharanda | https://github.com/sharanda/manrope |
|
||||||
|
| DM Mono | Colophon Foundry for Deja Vu | https://github.com/googlefonts/dm-mono |
|
||||||
|
|
||||||
|
Full licence text: https://openfontlicense.org/open-font-license-official-text/
|
||||||
|
|
||||||
|
The `.woff2` files are the latin and latin-ext subsets as served by Google
|
||||||
|
Fonts. They are unmodified.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,100 @@
|
|||||||
|
/* Manrope and DM Mono, self-hosted.
|
||||||
|
*
|
||||||
|
* `styles.css` line 1 used to carry this:
|
||||||
|
*
|
||||||
|
* @font-face{font-family:Manrope;src:url('https://fonts.googleapis.com/css2?...')}
|
||||||
|
*
|
||||||
|
* `src:` in an @font-face must point at a font binary. That URL returns a CSS
|
||||||
|
* stylesheet, so no browser could ever load a face from it: every
|
||||||
|
* `font-family:Manrope,Arial,sans-serif` fell through to Arial, and 'DM Mono'
|
||||||
|
* was never declared at all, so it fell through to the generic monospace face.
|
||||||
|
* The intended typography has never rendered. This file is the fix.
|
||||||
|
*
|
||||||
|
* Self-hosted rather than linked from fonts.googleapis.com because
|
||||||
|
* `scripts/audit-ui.mjs` rejects any external <link>/<script>, and because the
|
||||||
|
* site is shown in workshop rooms with unreliable networks.
|
||||||
|
*
|
||||||
|
* Both families are SIL Open Font License 1.1 — see OFL.md in this directory.
|
||||||
|
* Subsets are latin and latin-ext only: the site is EN and PT-BR, so the
|
||||||
|
* cyrillic, greek and vietnamese subsets Google also serves are dropped.
|
||||||
|
*
|
||||||
|
* The url()s are relative on purpose. Both consumers resolve them against this
|
||||||
|
* file's own location:
|
||||||
|
* - Astro pages: <link> in BaseLayout.astro, served from `${base}fonts/`
|
||||||
|
* - legacy pages: @import at the top of the root `styles.css`
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* DM Mono 400 — latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('dm-mono-400-latin.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DM Mono 400 — latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('dm-mono-400-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
|
||||||
|
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DM Mono 500 — latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('dm-mono-500-latin.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DM Mono 500 — latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('dm-mono-500-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
|
||||||
|
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Manrope 400 800 — latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: Manrope;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('manrope-var-latin.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Manrope 400 800 — latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: Manrope;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('manrope-var-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
|
||||||
|
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,306 @@
|
|||||||
|
---
|
||||||
|
// 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]');
|
||||||
|
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>
|
||||||
@@ -6,6 +6,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { title, description, lang = 'en' } = Astro.props;
|
const { title, description, lang = 'en' } = Astro.props;
|
||||||
|
|
||||||
|
// Self-hosted Manrope and DM Mono. Lives in `public/fonts/` rather than the
|
||||||
|
// bundled CSS because the legacy stylesheets @import the same file, so there
|
||||||
|
// is exactly one copy of the faces. See public/fonts/fonts.css.
|
||||||
|
const fonts = `${import.meta.env.BASE_URL}fonts/fonts.css`.replace('//', '/');
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -15,6 +20,7 @@ const { title, description, lang = 'en' } = Astro.props;
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
|
<link rel="stylesheet" href={fonts} />
|
||||||
<slot name="styles" />
|
<slot name="styles" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
// /agents/ — chapter page. Migrated from agents/index.html in task 13.
|
||||||
|
// Identical URL (/agents/), zero client JS, copy lives in
|
||||||
|
// src/content/chapters/agents.json.
|
||||||
|
|
||||||
|
import { getEntry } from 'astro:content';
|
||||||
|
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||||
|
import ChapterHero from '../components/blocks/ChapterHero.astro';
|
||||||
|
|
||||||
|
// Required-field guard. The chapters schema marks section eyebrow /
|
||||||
|
// panelLabel / panelCode / steps / copy as optional because the schema
|
||||||
|
// does not know which page consumes which shape. These four pages do
|
||||||
|
// consume them — fail loudly here rather than rendering a blank section.
|
||||||
|
function requireField<T>(value: T | undefined, name: string): T {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`agents chapter: missing required field "${name}"`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = import.meta.env.BASE_URL;
|
||||||
|
const chapter = await getEntry('chapters', 'agents');
|
||||||
|
if (!chapter) {
|
||||||
|
throw new Error('agents chapter: missing collection entry');
|
||||||
|
}
|
||||||
|
const lede = chapter.data.lede.en;
|
||||||
|
const title = chapter.data.title.en;
|
||||||
|
const eyebrow = chapter.data.eyebrow.en;
|
||||||
|
const cards = chapter.data.cards ?? [];
|
||||||
|
const sections = chapter.data.sections ?? [];
|
||||||
|
const treeSection = sections[0];
|
||||||
|
const handoffSection = sections[1];
|
||||||
|
if (!treeSection) {
|
||||||
|
throw new Error('agents chapter: missing sections[0]');
|
||||||
|
}
|
||||||
|
if (!handoffSection) {
|
||||||
|
throw new Error('agents chapter: missing sections[1]');
|
||||||
|
}
|
||||||
|
// Narrow the section fields this page renders. Each `requireField` either
|
||||||
|
// returns a non-null value or throws — TS narrows from `T | undefined` to `T`.
|
||||||
|
const treeEyebrow = requireField(treeSection.eyebrow, 'sections[0].eyebrow');
|
||||||
|
const treePanelLabel = requireField(treeSection.panelLabel, 'sections[0].panelLabel');
|
||||||
|
const treePanelCode = requireField(treeSection.panelCode, 'sections[0].panelCode');
|
||||||
|
const handoffEyebrow = requireField(handoffSection.eyebrow, 'sections[1].eyebrow');
|
||||||
|
const handoffSteps = requireField(handoffSection.steps, 'sections[1].steps');
|
||||||
|
---
|
||||||
|
|
||||||
|
<ChapterLayout title="AI For Dummies — Agents and trees" description={lede.replace(/<[^>]+>/g, '')}>
|
||||||
|
<a slot="top-previous" href={`${base}summary/`}>← ROUTE MAP</a>
|
||||||
|
<span slot="top-center">02 / AGENTS & TREES</span>
|
||||||
|
<a slot="top-next" href={`${base}full-guide/`}>field guide ↗</a>
|
||||||
|
|
||||||
|
<ChapterHero eyebrow={eyebrow}>
|
||||||
|
<span slot="title" set:html={title} />
|
||||||
|
<p>{lede}</p>
|
||||||
|
</ChapterHero>
|
||||||
|
|
||||||
|
<section class="pipeline">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">{treeEyebrow.en}</p>
|
||||||
|
<h2 set:html={treeSection.title.en} />
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<strong>{treePanelLabel.en}</strong>
|
||||||
|
<code>{treePanelCode.en}</code>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid">
|
||||||
|
{
|
||||||
|
cards.map((card) => (
|
||||||
|
<article class="card">
|
||||||
|
<b>{card.label.en}</b>
|
||||||
|
<h2>{card.title.en}</h2>
|
||||||
|
<p>{card.copy.en}</p>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="practice">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">{handoffEyebrow.en}</p>
|
||||||
|
<h2 set:html={handoffSection.title.en} />
|
||||||
|
</div>
|
||||||
|
<div class="steps">
|
||||||
|
{
|
||||||
|
handoffSteps.map((step, index) => (
|
||||||
|
<article>
|
||||||
|
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||||
|
<div>
|
||||||
|
<strong>{step.label.en}</strong>
|
||||||
|
<span>{step.copy.en}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
|
||||||
|
<a href={`${base}models/`}>Previous: models →</a>
|
||||||
|
<a href={`${base}rules/`}>Rules case study →</a>
|
||||||
|
<a href={`${base}hands-on/rules/`}>Try the rules lab →</a>
|
||||||
|
</nav>
|
||||||
|
</ChapterLayout>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
---
|
||||||
|
// /models/ — chapter page. Migrated from models/index.html in task 13.
|
||||||
|
// Identical URL (/models/), zero client JS, copy lives in
|
||||||
|
// src/content/chapters/models.json. The grid + panel + steps sections
|
||||||
|
// render the chapter-collections content with bilingual `en` strings.
|
||||||
|
|
||||||
|
import { getEntry } from 'astro:content';
|
||||||
|
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||||
|
import ChapterHero from '../components/blocks/ChapterHero.astro';
|
||||||
|
|
||||||
|
// Required-field guard. The chapters schema marks section eyebrow /
|
||||||
|
// panelLabel / panelCode / steps / copy as optional because the schema
|
||||||
|
// does not know which page consumes which shape. These four pages do
|
||||||
|
// consume them — fail loudly here rather than rendering a blank section.
|
||||||
|
function requireField<T>(value: T | undefined, name: string): T {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`models chapter: missing required field "${name}"`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = import.meta.env.BASE_URL;
|
||||||
|
const chapter = await getEntry('chapters', 'models');
|
||||||
|
if (!chapter) {
|
||||||
|
throw new Error('models chapter: missing collection entry');
|
||||||
|
}
|
||||||
|
const lede = chapter.data.lede.en;
|
||||||
|
const title = chapter.data.title.en;
|
||||||
|
const eyebrow = chapter.data.eyebrow.en;
|
||||||
|
const cards = chapter.data.cards ?? [];
|
||||||
|
const sections = chapter.data.sections ?? [];
|
||||||
|
// First section carries the routing-rule panel, second carries the steps.
|
||||||
|
const ruleSection = sections[0];
|
||||||
|
const sequenceSection = sections[1];
|
||||||
|
if (!ruleSection) {
|
||||||
|
throw new Error('models chapter: missing sections[0]');
|
||||||
|
}
|
||||||
|
if (!sequenceSection) {
|
||||||
|
throw new Error('models chapter: missing sections[1]');
|
||||||
|
}
|
||||||
|
const ruleEyebrow = requireField(ruleSection.eyebrow, 'sections[0].eyebrow');
|
||||||
|
const rulePanelLabel = requireField(ruleSection.panelLabel, 'sections[0].panelLabel');
|
||||||
|
const rulePanelCode = requireField(ruleSection.panelCode, 'sections[0].panelCode');
|
||||||
|
const sequenceEyebrow = requireField(sequenceSection.eyebrow, 'sections[1].eyebrow');
|
||||||
|
const sequenceSteps = requireField(sequenceSection.steps, 'sections[1].steps');
|
||||||
|
---
|
||||||
|
|
||||||
|
<ChapterLayout title="AI For Dummies — Models" description={lede.replace(/<[^>]+>/g, '')}>
|
||||||
|
<a slot="top-previous" href={`${base}summary/`}>← ROUTE MAP</a>
|
||||||
|
<span slot="top-center">01 / MODELS</span>
|
||||||
|
<a slot="top-next" href={`${base}full-guide/`}>field guide ↗</a>
|
||||||
|
|
||||||
|
<ChapterHero eyebrow={eyebrow}>
|
||||||
|
<span slot="title" set:html={title} />
|
||||||
|
<p>{lede}</p>
|
||||||
|
</ChapterHero>
|
||||||
|
|
||||||
|
<section class="grid">
|
||||||
|
{
|
||||||
|
cards.map((card) => (
|
||||||
|
<article class="card">
|
||||||
|
<b>{card.label.en}</b>
|
||||||
|
<h2>{card.title.en}</h2>
|
||||||
|
<p>{card.copy.en}</p>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="model">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">{ruleEyebrow.en}</p>
|
||||||
|
<h2 set:html={ruleSection.title.en} />
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<strong>{rulePanelLabel.en}</strong>
|
||||||
|
<code>{rulePanelCode.en}</code>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="practice">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">{sequenceEyebrow.en}</p>
|
||||||
|
<h2 set:html={sequenceSection.title.en} />
|
||||||
|
</div>
|
||||||
|
<div class="steps">
|
||||||
|
{
|
||||||
|
sequenceSteps.map((step, index) => (
|
||||||
|
<article>
|
||||||
|
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||||
|
<div>
|
||||||
|
<strong>{step.label.en}</strong>
|
||||||
|
<span>{step.copy.en}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
|
||||||
|
<a href={`${base}agents/`}>Next: agents & trees →</a>
|
||||||
|
<a href={`${base}rules/`}>Rules case study →</a>
|
||||||
|
</nav>
|
||||||
|
</ChapterLayout>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
// /skills/ — chapter page. Migrated from skills/index.html in task 13.
|
||||||
|
// Identical URL (/skills/). The package-anatomy picker is an Astro island
|
||||||
|
// (SkillPackageExplorer) — the only JS the page ships. Copy lives in
|
||||||
|
// src/content/chapters/skills.json.
|
||||||
|
|
||||||
|
import { getEntry } from 'astro:content';
|
||||||
|
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||||
|
import ChapterHero from '../components/blocks/ChapterHero.astro';
|
||||||
|
import SkillPackageExplorer from '../components/islands/SkillPackageExplorer.astro';
|
||||||
|
import skillsStylesheet from '../../skills/styles.css?url';
|
||||||
|
|
||||||
|
// Required-field guard. The chapters schema marks section eyebrow /
|
||||||
|
// panelLabel / panelCode / steps / copy as optional because the schema
|
||||||
|
// does not know which page consumes which shape. These four pages do
|
||||||
|
// consume them — fail loudly here rather than rendering a blank section.
|
||||||
|
function requireField<T>(value: T | undefined, name: string): T {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`skills chapter: missing required field "${name}"`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = import.meta.env.BASE_URL;
|
||||||
|
const chapter = await getEntry('chapters', 'skills');
|
||||||
|
if (!chapter) {
|
||||||
|
throw new Error('skills chapter: missing collection entry');
|
||||||
|
}
|
||||||
|
const lede = chapter.data.lede.en;
|
||||||
|
const title = chapter.data.title.en;
|
||||||
|
const eyebrow = chapter.data.eyebrow.en;
|
||||||
|
const sections = chapter.data.sections ?? [];
|
||||||
|
const anatomySection = sections[0];
|
||||||
|
const createSection = sections[1];
|
||||||
|
if (!anatomySection) {
|
||||||
|
throw new Error('skills chapter: missing sections[0]');
|
||||||
|
}
|
||||||
|
if (!createSection) {
|
||||||
|
throw new Error('skills chapter: missing sections[1]');
|
||||||
|
}
|
||||||
|
const anatomyEyebrow = requireField(anatomySection.eyebrow, 'sections[0].eyebrow');
|
||||||
|
const anatomyCopy = requireField(anatomySection.copy, 'sections[0].copy');
|
||||||
|
const createEyebrow = requireField(createSection.eyebrow, 'sections[1].eyebrow');
|
||||||
|
const createSteps = requireField(createSection.steps, 'sections[1].steps');
|
||||||
|
---
|
||||||
|
|
||||||
|
<ChapterLayout title="AI For Dummies — Skills" description={lede.replace(/<[^>]+>/g, '')}>
|
||||||
|
<link slot="styles" rel="stylesheet" href={skillsStylesheet} />
|
||||||
|
<a slot="top-previous" href={`${base}summary/`}>← ROUTE MAP</a>
|
||||||
|
<span slot="top-center">03 / SKILLS</span>
|
||||||
|
<a slot="top-next" href={`${base}skills-review/`}>review desk ↗</a>
|
||||||
|
|
||||||
|
<ChapterHero eyebrow={eyebrow}>
|
||||||
|
<span slot="title" set:html={title} />
|
||||||
|
<p set:html={lede} />
|
||||||
|
</ChapterHero>
|
||||||
|
|
||||||
|
<section class="pipeline package-anatomy">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">{anatomyEyebrow.en}</p>
|
||||||
|
<h2 set:html={anatomySection.title.en} />
|
||||||
|
<p class="package-hint">{anatomyCopy.en}</p>
|
||||||
|
</div>
|
||||||
|
<SkillPackageExplorer />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="practice">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">{createEyebrow.en}</p>
|
||||||
|
<h2 set:html={createSection.title.en} />
|
||||||
|
</div>
|
||||||
|
<div class="steps">
|
||||||
|
{
|
||||||
|
createSteps.map((step, index) => (
|
||||||
|
<article>
|
||||||
|
<b>{String(index + 1).padStart(2, '0')}</b>
|
||||||
|
<div>
|
||||||
|
<strong>{step.label.en}</strong>
|
||||||
|
<span>{step.copy.en}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav slot="footer-links" class="links" aria-label="Chapter navigation">
|
||||||
|
<a href={`${base}agents/`}>Agents & trees →</a>
|
||||||
|
<a href={`${base}rules/`}>Rules case study →</a>
|
||||||
|
<a href={`${base}skills-review/`}>Review submitted skills →</a>
|
||||||
|
<a href={`${base}full-guide/#create-skill`}>Full guide: skill forge →</a>
|
||||||
|
</nav>
|
||||||
|
</ChapterLayout>
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user