Merge branch 'main' into refactor/task-13-page-chapters
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
# Context: full-guide language switching
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The Astro full guide keeps the current client-side language switch on its
|
||||||
|
existing `/full-guide/` URL. It server-renders both locale variants and the
|
||||||
|
language-toggle island shows the selected variant after `client:idle` hydration.
|
||||||
|
|
||||||
|
This deliberately preserves the current no-URL-change contract, including links
|
||||||
|
shared without a locale segment, and avoids a route/redirect and publishing
|
||||||
|
change. The cost is duplicated localized HTML and both locales in the response.
|
||||||
|
That is acceptable for this small guide and avoids sending duplicated string
|
||||||
|
data through every interactive island.
|
||||||
|
|
||||||
|
## Markup and island contract for task 15d
|
||||||
|
|
||||||
|
- Render each static localized fragment twice. Put `data-language-content="en"`
|
||||||
|
or `data-language-content="pt"` on its outer element. English is visible in
|
||||||
|
server HTML; the toggle uses the native `hidden` attribute for the inactive
|
||||||
|
locale.
|
||||||
|
- Add `<LanguageToggle />` to the guide top bar. Astro's `client:idle` directive
|
||||||
|
is only valid for framework components; this `.astro` island defers its
|
||||||
|
browser setup with `requestIdleCallback` (and a timeout fallback) instead. Do
|
||||||
|
not hydrate the page or use `client:load`; the control is deliberately
|
||||||
|
idle-priority.
|
||||||
|
- The island owns the `ai-for-dummies-language` localStorage key. Every read and
|
||||||
|
write remains inside `try`/`catch`, because previews may disable storage.
|
||||||
|
- On each selection the island sets `<html lang>` to `en` or `pt-BR`, updates
|
||||||
|
its `[data-lang]` buttons' `.active` class and `aria-pressed` state, updates
|
||||||
|
`[data-language-content]`, then dispatches `ai-for-dummies:languagechange` on
|
||||||
|
`window`. The event detail is `{ language: 'en' | 'pt' }`.
|
||||||
|
- The guide selector island (15a) must read `document.documentElement.lang` when
|
||||||
|
it hydrates and listen for that event. On receipt it must re-render the
|
||||||
|
currently active phase and all active selector panels from their collection
|
||||||
|
data. This preserves today’s `applyLanguage` behaviour without coupling the
|
||||||
|
toggle to page selectors.
|
||||||
|
|
||||||
|
This is a page-local contract: the existing `/rules/` toggle continues using its
|
||||||
|
own `rules-language` key and must not be changed as part of full-guide
|
||||||
|
migration.
|
||||||
@@ -5,9 +5,9 @@ interchangeable here, and the split is not about which is "smartest" — it is
|
|||||||
about which failure mode each task punishes.
|
about which failure mode each task punishes.
|
||||||
|
|
||||||
**Honest caveat up front:** I have not benchmarked these three on this
|
**Honest caveat up front:** I have not benchmarked these three on this
|
||||||
repository. The routing below is reasoned from task shape and each model's
|
repository. The routing below is reasoned from task shape and each model's known
|
||||||
known strengths. Validate it cheaply on **task 07** (small, self-contained,
|
strengths. Validate it cheaply on **task 07** (small, self-contained, easy to
|
||||||
easy to judge) before fanning out across ten worktrees.
|
judge) before fanning out across ten worktrees.
|
||||||
|
|
||||||
## Short answer
|
## Short answer
|
||||||
|
|
||||||
@@ -18,22 +18,27 @@ points where M3 is the wrong tool.
|
|||||||
|
|
||||||
## Routing table
|
## Routing table
|
||||||
|
|
||||||
| Task | Model | Why this one |
|
| Task | Model | Why this one |
|
||||||
| --- | --- | --- |
|
| ------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| 01 scaffold + gates | **Codex** | Config-heavy, many interacting tools (Astro + husky + lint-staged + CI), and success is binary — it builds and hooks fire, or not. Codex's long autonomous run-until-green loop suits it, and getting the foundation wrong is expensive later. |
|
| 01 scaffold + gates | **Codex** | Config-heavy, many interacting tools (Astro + husky + lint-staged + CI), and success is binary — it builds and hooks fire, or not. Codex's long autonomous run-until-green loop suits it, and getting the foundation wrong is expensive later. |
|
||||||
| 02 design tokens | **Gemini** | Needs the whole CSS corpus in one context (8 stylesheets, ~90 KB) plus **visual judgement on screenshots**. Gemini's long context and multimodal comparison are the differentiator; the others would work file-by-file and miss cross-file drift. |
|
| 02 design tokens | **Gemini** | Needs the whole CSS corpus in one context (8 stylesheets, ~90 KB) plus **visual judgement on screenshots**. Gemini's long context and multimodal comparison are the differentiator; the others would work file-by-file and miss cross-file drift. |
|
||||||
| 03 verification net | **Codex** | Writing test tooling with a tight feedback loop. Precision about what an assertion pins matters more than speed. |
|
| 03 verification net | **Codex** | Writing test tooling with a tight feedback loop. Precision about what an assertion pins matters more than speed. |
|
||||||
| 04 content schema | **MiniMax-M3** | Small, well-specified, one file. |
|
| 04 content schema | **MiniMax-M3** | Small, well-specified, one file. |
|
||||||
| 05–06 content migration | **MiniMax-M3** | High-volume mechanical string moves with a `diff` as the oracle. Cheap, parallel, verifiable. Exactly M3's sweet spot. |
|
| 05–06 content migration | **MiniMax-M3** | High-volume mechanical string moves with a `diff` as the oracle. Cheap, parallel, verifiable. Exactly M3's sweet spot. |
|
||||||
| 07 primitives | **MiniMax-M3** | Small components from templates. Use this task to calibrate the whole routing decision. |
|
| 07 primitives | **MiniMax-M3** | Small components from templates. Use this task to calibrate the whole routing decision. |
|
||||||
| 08–11 component blocks | **MiniMax-M3 ×4 parallel** | Four bounded tasks, one template each, checklist-gated. Cost per task matters because there are many. |
|
| 08–11 component blocks | **MiniMax-M3 ×4 parallel** | Four bounded tasks, one template each, checklist-gated. Cost per task matters because there are many. |
|
||||||
| 12–14, 17 pages | **MiniMax-M3** | Bounded, snapshot-diff verified. |
|
| 12–14, 17 pages | **MiniMax-M3** | Bounded, snapshot-diff verified. |
|
||||||
| 15 full guide | **Codex** | The hard one: 50 KB `app.js`, 12 render functions, tab state, bilingual swap. Long sustained reasoning over interacting pieces; the task most likely to need many iterations against a failing check. |
|
| 15 full guide | **split** | Ran twice on Codex, zero usable commits both times. This table's own swap rule applied: too big, so it became 15a–15e. |
|
||||||
| 16 review desk | **Codex** | Same shape and worse — search, filtering, file fetching, six query params, markdown rendering, client-side diff. Highest defect risk in the plan. |
|
| 15a guide selector | **Codex** | Collapsing nine near-identical render functions into one island is the reasoning-heavy part that remains. |
|
||||||
| 18 motion | **Gemini** | Judging whether motion looks right is perceptual. Feed it before/after captures. |
|
| 15b copy prompt | MiniMax-M3 | Small, mechanical, and has an exact oracle: the clipboard payload must be byte-identical. |
|
||||||
| 19 contract re-point | **Codex** | 42 assertions to translate without losing coverage. Meticulous, mechanical, verifiable. |
|
| 15c language toggle | **Codex** | Needs a design decision written down, not a port — the selector-map approach cannot survive. |
|
||||||
| 20 cutover | **you, with M3 assisting** | Touches production publishing. A human should be watching. |
|
| 15d assemble full guide | **Codex** | Assembly, but wide: 22 KB of bilingual markup against three islands and task 10's blocks. |
|
||||||
| review of every task | **Gemini** | Fresh eyes, whole-diff-in-context, and it did not write the code. Never review with the model that wrote it. |
|
| 15e retire responsive.css | `agy` | Screenshot-diff driven — needs vision. |
|
||||||
|
| 16 review desk | **Codex** | Same shape and worse — search, filtering, file fetching, six query params, markdown rendering, client-side diff. Highest defect risk in the plan. |
|
||||||
|
| 18 motion | **Gemini** | Judging whether motion looks right is perceptual. Feed it before/after captures. |
|
||||||
|
| 19 contract re-point | **Codex** | 42 assertions to translate without losing coverage. Meticulous, mechanical, verifiable. |
|
||||||
|
| 20 cutover | **you, with M3 assisting** | Touches production publishing. A human should be watching. |
|
||||||
|
| review of every task | **Gemini** | Fresh eyes, whole-diff-in-context, and it did not write the code. Never review with the model that wrote it. |
|
||||||
|
|
||||||
## The reasoning in one line each
|
## The reasoning in one line each
|
||||||
|
|
||||||
@@ -41,11 +46,11 @@ points where M3 is the wrong tool.
|
|||||||
Use it for volume: 13 of the 20 tasks. Its weakness is long multi-file
|
Use it for volume: 13 of the 20 tasks. Its weakness is long multi-file
|
||||||
reasoning where the spec is vague; every task above that it owns has a
|
reasoning where the spec is vague; every task above that it owns has a
|
||||||
template and a mechanical oracle.
|
template and a mechanical oracle.
|
||||||
- **Codex** — best at "keep iterating until the check passes" over a
|
- **Codex** — best at "keep iterating until the check passes" over a complicated
|
||||||
complicated existing codebase. Use it where the loop is long and the answer is
|
existing codebase. Use it where the loop is long and the answer is not
|
||||||
not obvious: scaffold, the two hard pages, verification.
|
obvious: scaffold, the two hard pages, verification.
|
||||||
- **Gemini** — biggest context and genuinely useful multimodal comparison. Use
|
- **Gemini** — biggest context and genuinely useful multimodal comparison. Use
|
||||||
it where the input is *everything at once* or where the judgement is
|
it where the input is _everything at once_ or where the judgement is
|
||||||
**visual**: token consolidation, motion, screenshot diffing, and code review.
|
**visual**: token consolidation, motion, screenshot diffing, and code review.
|
||||||
|
|
||||||
## Cross-checking rule
|
## Cross-checking rule
|
||||||
@@ -56,13 +61,13 @@ reviews; Codex writes → Gemini reviews; Gemini writes → Codex reviews. The
|
|||||||
|
|
||||||
## Swap the routing if you see this
|
## Swap the routing if you see this
|
||||||
|
|
||||||
| Symptom | Move the task to |
|
| Symptom | Move the task to |
|
||||||
| --- | --- |
|
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
| M3 spends more than ~3 iterations failing the same gate | Codex |
|
| M3 spends more than ~3 iterations failing the same gate | Codex |
|
||||||
| M3 edits files outside its task scope | Codex, and tighten the brief |
|
| M3 edits files outside its task scope | Codex, and tighten the brief |
|
||||||
| Codex "fixes" a red suite by deleting assertions | anything — but re-read `context/verification.md` to it first; `gate.sh` blocks the merge either way |
|
| Codex "fixes" a red suite by deleting assertions | anything — but re-read `context/verification.md` to it first; `gate.sh` blocks the merge either way |
|
||||||
| Gemini gives confident visual sign-off with no screenshots attached | require the artifacts; do not accept prose |
|
| Gemini gives confident visual sign-off with no screenshots attached | require the artifacts; do not accept prose |
|
||||||
| A task needs more than two models' worth of hand-holding | the task is too big — split it |
|
| A task needs more than two models' worth of hand-holding | the task is too big — split it |
|
||||||
|
|
||||||
## Cost shape
|
## Cost shape
|
||||||
|
|
||||||
|
|||||||
@@ -37,32 +37,38 @@ on four branches; nothing is merged or pushed.
|
|||||||
Phase 0 foundation 01 → (02 ∥ 03 ∥ 04)
|
Phase 0 foundation 01 → (02 ∥ 03 ∥ 04)
|
||||||
Phase 1 content 05 ∥ 06 after 04
|
Phase 1 content 05 ∥ 06 after 04
|
||||||
Phase 2 components 07 → (08 ∥ 09 ∥ 10 ∥ 11) after 02
|
Phase 2 components 07 → (08 ∥ 09 ∥ 10 ∥ 11) after 02
|
||||||
Phase 3 pages 12 ∥ 13 ∥ 14 ∥ 17, then 15 ∥ 16
|
Phase 3 pages 12 ∥ 13 ∥ 14 ∥ 17, then 15a ∥ 15b ∥ 15c ∥ 16, then 15d, 15e
|
||||||
Phase 4 polish 18 ∥ 19, then 20
|
Phase 4 polish 18 ∥ 19, then 20
|
||||||
```
|
```
|
||||||
|
|
||||||
| # | Task | Agent | Depends on | Parallel with |
|
| # | Task | Agent | Depends on | Parallel with |
|
||||||
| --- | ------------------------------------------------ | --------------------- | ---------- | ------------- |
|
| --- | ----------------------------------------------------------------- | --------------------- | ------------------ | ------------- |
|
||||||
| 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 |
|
||||||
| 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 |
|
||||||
| 06 | [review-desk content](task-06-content-review.md) | content-i18n-migrator | 04 | 05 |
|
| 06 | [review-desk content](task-06-content-review.md) | content-i18n-migrator | 04 | 05 |
|
||||||
| 07 | [primitives](task-07-primitives.md) | component-builder | 02 | — |
|
| 07 | [primitives](task-07-primitives.md) | component-builder | 02 | — |
|
||||||
| 08 | [route cards](task-08-route-cards.md) | component-builder | 07 | 09, 10, 11 |
|
| 08 | [route cards](task-08-route-cards.md) | component-builder | 07 | 09, 10, 11 |
|
||||||
| 09 | [chapter blocks](task-09-chapter-blocks.md) | component-builder | 07 | 08, 10, 11 |
|
| 09 | [chapter blocks](task-09-chapter-blocks.md) | component-builder | 07 | 08, 10, 11 |
|
||||||
| 10 | [guide blocks](task-10-guide-blocks.md) | component-builder | 07 | 08, 09, 11 |
|
| 10 | [guide blocks](task-10-guide-blocks.md) | component-builder | 07 | 08, 09, 11 |
|
||||||
| 11 | [review-desk blocks](task-11-review-blocks.md) | component-builder | 07 | 08, 09, 10 |
|
| 11 | [review-desk blocks](task-11-review-blocks.md) | component-builder | 07 | 08, 09, 10 |
|
||||||
| 12 | [landing page](task-12-page-landing.md) | page-migrator | 03, 08 | 13, 14, 17 |
|
| 12 | [landing page](task-12-page-landing.md) | page-migrator | 03, 08 | 13, 14, 17 |
|
||||||
| 13 | [chapter pages ×4](task-13-page-chapters.md) | page-migrator | 03, 09 | 12, 14, 17 |
|
| 13 | [chapter pages ×4](task-13-page-chapters.md) | page-migrator | 03, 09 | 12, 14, 17 |
|
||||||
| 14 | [rules page](task-14-page-rules.md) | page-migrator | 03, 09 | 12, 13, 17 |
|
| 14 | [rules page](task-14-page-rules.md) | page-migrator | 03, 09 | 12, 13, 17 |
|
||||||
| 15 | [full guide](task-15-page-full-guide.md) | page-migrator | 05, 10, 13 | 16 |
|
| 15 | [full guide](task-15-page-full-guide.md) — **split into 15a–15e** | — | — | — |
|
||||||
| 16 | [review desk](task-16-page-review-desk.md) | page-migrator | 06, 11, 13 | 15 |
|
| 05b | [interactiveCopy data](task-05b-guide-interactive-data.md) | content-i18n-migrator | 05 | — |
|
||||||
| 17 | [hands-on passthrough](task-17-hands-on.md) | astro-architect | 01 | 12, 13, 14 |
|
| 15a | [guide selector](task-15a-guide-selector.md) | component-builder | 05, 10 | 15b, 15c |
|
||||||
| 18 | [motion pass](task-18-motion.md) | motion-designer | 15, 16 | 19 |
|
| 15b | [copy prompt](task-15b-copy-prompt.md) | component-builder | 05 | 15a, 15c |
|
||||||
| 19 | [contract re-point](task-19-verify-repoint.md) | verification-engineer | 15, 16 | 18 |
|
| 15c | [language toggle](task-15c-language-toggle.md) | content-i18n-migrator | 05 | 15a, 15b |
|
||||||
| 20 | [cutover + cleanup](task-20-cutover.md) | astro-architect | all | — |
|
| 15d | [assemble full guide](task-15d-page-full-guide.md) | page-migrator | 05b, 10, 13, 15a–c | 16 |
|
||||||
|
| 15e | [retire responsive.css](task-15e-responsive-css.md) | design-system-keeper | 15d, 16 | — |
|
||||||
|
| 16 | [review desk](task-16-page-review-desk.md) | page-migrator | 06, 11, 13 | 15d |
|
||||||
|
| 17 | [hands-on passthrough](task-17-hands-on.md) | astro-architect | 01 | 12, 13, 14 |
|
||||||
|
| 18 | [motion pass](task-18-motion.md) | motion-designer | 15d, 16 | 19 |
|
||||||
|
| 19 | [contract re-point](task-19-verify-repoint.md) | verification-engineer | 15d, 16 | 18 |
|
||||||
|
| 20 | [cutover + cleanup](task-20-cutover.md) | astro-architect | all | — |
|
||||||
|
|
||||||
Widest parallelism: **four agents** (tasks 08–11, then 12/13/14/17). More than
|
Widest parallelism: **four agents** (tasks 08–11, then 12/13/14/17). More than
|
||||||
that and they start contending on review capacity, not on files.
|
that and they start contending on review capacity, not on files.
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Task 05b — The `interactiveCopy` data
|
||||||
|
|
||||||
|
**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3 **Depends on**: 05 ·
|
||||||
|
**Blocks**: 15d **Worktree**:
|
||||||
|
`.agents/scripts/worktree.sh start 05b guide-interactive-data`
|
||||||
|
|
||||||
|
## Why this task exists
|
||||||
|
|
||||||
|
It is not in the original plan, and it is the second hole of exactly this kind
|
||||||
|
(see [task 04b](task-04b-chapters-data.md), which filled the empty `chapters`
|
||||||
|
collection).
|
||||||
|
|
||||||
|
Task 05 moved `phases`, `providers`, `efforts`, `skillSources`, `handsOnPrompts`
|
||||||
|
and `skillInstallPrompts` out of `app.js`. It did not move
|
||||||
|
`const interactiveCopy` at `app.js:160`, which holds six more datasets:
|
||||||
|
|
||||||
|
| Key | Feeds | Rendered by |
|
||||||
|
| --------------- | ---------------------- | --------------------- |
|
||||||
|
| `workers` | `#worker-detail` | `renderWorker` |
|
||||||
|
| `trees` | `#tree-detail` | `renderTree` |
|
||||||
|
| `routes` | `#route-detail` | `renderRoute` |
|
||||||
|
| `skillFiles` | `#skill-detail` | `renderSkillFile` |
|
||||||
|
| `skillWorkflow` | `#builder-detail` | `renderSkillWorkflow` |
|
||||||
|
| `commonSkills` | `#common-skill-detail` | `renderCommonSkill` |
|
||||||
|
|
||||||
|
Task 15d stopped before writing a line because of this — six of the nine groups
|
||||||
|
`GuideSelector` drives have no data to read. It was right to stop.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
`src/content/config.ts` (new collections only) and the new `src/content/*/`
|
||||||
|
JSON. **You own `config.ts`** — you are the only agent who may edit it. Do not
|
||||||
|
change an existing collection's schema.
|
||||||
|
|
||||||
|
## The shapes are already written down
|
||||||
|
|
||||||
|
`src/components/islands/GuideSelector.astro` declares a `GuideSelectorData`
|
||||||
|
interface at the top of the file. It is the contract 15d will feed. Match it —
|
||||||
|
if you think a field there is wrong, say so in your report rather than diverging
|
||||||
|
silently.
|
||||||
|
|
||||||
|
Note the non-obvious ones, verified against `app.js`:
|
||||||
|
|
||||||
|
- `trees[id]` has `owner` and `note` localized, but `path` and `command` are
|
||||||
|
**single strings, not localized** — they are shell paths.
|
||||||
|
- `routes[id].score` is a **number**, rendered into `style="--score:N%"`.
|
||||||
|
- `workers[id]` is `{ en: string[], pt: string[] }` — a three-element array
|
||||||
|
(`span`, `strong`, `small`), not an object.
|
||||||
|
- `skillFiles[id]` mixes `icon`/`title` (unlocalized) with `en`/`pt` strings.
|
||||||
|
- `providers[id].tiers` is `[string, string, Localized][]`.
|
||||||
|
|
||||||
|
Read the real `app.js` for each. Do not infer a shape from a sibling.
|
||||||
|
|
||||||
|
## Rules that bite here
|
||||||
|
|
||||||
|
- **Both `en` and `pt` are mandatory on every localized field.** A missing `pt`
|
||||||
|
must fail the build. Never make a field optional to clear a validation error —
|
||||||
|
find the real Portuguese string in `app.js`.
|
||||||
|
- Copy strings **verbatim**. This is a move, not a rewrite. No fixed typos, no
|
||||||
|
improved phrasing, nothing translated that is already translated. Several
|
||||||
|
strings contain inline HTML; keep it exactly as-is.
|
||||||
|
- Prove it. Diff each migrated string against the `app.js` original
|
||||||
|
programmatically and report the result — task 15b did this for the prompt
|
||||||
|
bodies and it is the standard here.
|
||||||
|
- Do not delete `interactiveCopy` from `app.js`. The legacy page still runs on
|
||||||
|
it until task 20 cuts over, and `verify.mjs` still asserts against it.
|
||||||
|
- Do not migrate a page or touch a component. 15d owns that.
|
||||||
|
- Do not edit `verify.mjs`, `tokens.css`, or `astro.config.mjs`.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
- [ ] Six collections defined and exported from `config.ts`
|
||||||
|
- [ ] Entries for every id in `interactiveCopy`, both locales
|
||||||
|
- [ ] Shapes match `GuideSelectorData`, or the divergence is argued in the
|
||||||
|
report
|
||||||
|
- [ ] Every string proven byte-identical to its `app.js` original
|
||||||
|
- [ ] `pnpm run gate` green — the full gate, not `verify` + `audit-ui` alone
|
||||||
|
- [ ] 42 assertions intact
|
||||||
@@ -1,64 +1,22 @@
|
|||||||
# Task 15 — Full guide
|
# Task 15 — Full guide — **SUPERSEDED, split into 15a–15e**
|
||||||
|
|
||||||
**Agent**: `page-migrator` · **Model**: **Codex** — hardest task in the plan
|
Do not work this brief. It is kept because other documents link to it.
|
||||||
**Depends on**: 05, 10, 13 · **Parallel with**: 16 **Worktree**:
|
|
||||||
`.agents/scripts/worktree.sh start 15 page-full-guide`
|
|
||||||
|
|
||||||
## Goal
|
Task 15 was attempted twice on Codex and produced no usable commit either time.
|
||||||
|
The second report's own words: _"this is an incomplete scaffold, not the real
|
||||||
|
migration requested."_ `MODEL-ROUTING.md` says a task needing more than two
|
||||||
|
models' worth of hand-holding is too big — split it. So it is split, along the
|
||||||
|
seams that made it hard: the nine-fold selector duplication, the clipboard
|
||||||
|
fallback, and the selector-map language toggle that cannot survive the port.
|
||||||
|
|
||||||
`/full-guide/` → Astro. 22 KB of HTML, a 50 KB script, 30 KB of CSS, twelve
|
| Brief | What | Depends on |
|
||||||
render functions, bilingual throughout.
|
| ---------------------------------- | ------------------------------------------- | --------------------- |
|
||||||
|
| [15a](task-15a-guide-selector.md) | one generic selector island for nine groups | 05, 10 |
|
||||||
|
| [15b](task-15b-copy-prompt.md) | copy-prompt buttons, reading progress | 05 |
|
||||||
|
| [15c](task-15c-language-toggle.md) | language toggle — needs a decision first | 05 |
|
||||||
|
| [15d](task-15d-page-full-guide.md) | assemble `/full-guide/` | 10, 13, 15a, 15b, 15c |
|
||||||
|
| [15e](task-15e-responsive-css.md) | prove `responsive.css` dead, delete it | 15d, 16 |
|
||||||
|
|
||||||
## What `app.js` actually is
|
15a, 15b and 15c run in parallel. 15d is assembly and must not invent islands.
|
||||||
|
Downstream tasks that said "depends on 15" now depend on **15d** (18, 19) or
|
||||||
Not application code — a **bilingual content database** (task 05 already moved
|
**15e** (20).
|
||||||
it) plus ~12 `render*` functions that swap `innerHTML` on tab clicks. Once
|
|
||||||
content is a collection, the remaining JS is small: tab state and a language
|
|
||||||
toggle.
|
|
||||||
|
|
||||||
## Islands
|
|
||||||
|
|
||||||
Only these hydrate. Everything else is server-rendered.
|
|
||||||
|
|
||||||
| Island | Directive | Why |
|
|
||||||
| -------------------------------------------------------- | ---------------- | -------------------------------------------------- |
|
|
||||||
| Phase tabs | `client:visible` | click-driven panel swap |
|
|
||||||
| Tree / worker / route / model / effort / skill selectors | `client:visible` | same pattern; consider one generic selector island |
|
|
||||||
| Language toggle | `client:idle` | page-wide, not urgent |
|
|
||||||
| Copy-prompt buttons | `client:visible` | clipboard |
|
|
||||||
|
|
||||||
If you end up with 12 separate islands you have missed the pattern — they are
|
|
||||||
one selector component with different data.
|
|
||||||
|
|
||||||
## Asserted by verify.mjs — all must survive
|
|
||||||
|
|
||||||
`const phases`, `const handsOnPrompts`, `const modelGuide`,
|
|
||||||
`const skillSources`, `const skillInstallPrompts`, `render('plan')`,
|
|
||||||
`renderTree`, `renderWorker`, `renderRoute`, `renderModelProvider`,
|
|
||||||
`renderEffort`, `renderSkillFile`, `renderSkillWorkflow`, `renderCommonSkill`,
|
|
||||||
`renderHandsOn`, `copyPrompt`.
|
|
||||||
|
|
||||||
These are **implementation-detail assertions** — they look deletable and are
|
|
||||||
not. Each pins a feature. Coordinate with task 19 to replace each with an
|
|
||||||
output-level assertion of the same behaviour. **Never delete one yourself.**
|
|
||||||
|
|
||||||
Also: `data-copy-target="prompt-install-skills|prompt-basic|prompt-skills"`,
|
|
||||||
`hands-on/starter/`, `additional-reading.md`, `role="tablist"`, `<table>`.
|
|
||||||
|
|
||||||
## Watch for
|
|
||||||
|
|
||||||
- `copyPrompt` uses `navigator.clipboard` with a `document.execCommand`
|
|
||||||
fallback. Keep both — the fallback exists for non-secure contexts.
|
|
||||||
- The hands-on prompt strings are copy-pasted by attendees into an agent. Exact
|
|
||||||
whitespace and line breaks matter.
|
|
||||||
- `responsive.css` (30 KB) mostly serves this page. Port what is needed, prove
|
|
||||||
the rest dead, delete it. Screenshots are the proof.
|
|
||||||
|
|
||||||
## Done when
|
|
||||||
|
|
||||||
- [ ] Snapshot diff empty
|
|
||||||
- [ ] Every interaction works: all tabs, both languages, all copy buttons
|
|
||||||
- [ ] Keyboard: arrow keys move between tabs; focus visible throughout
|
|
||||||
- [ ] JS payload **smaller** than today's 50 KB (content is now static)
|
|
||||||
- [ ] Screenshots match at four widths; checklist complete; `pnpm run gate`
|
|
||||||
green
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Task 15a — The one guide selector island
|
||||||
|
|
||||||
|
**Agent**: `component-builder` · **Model**: **Codex** **Depends on**: 05, 10 ·
|
||||||
|
**Parallel with**: 15b · **Blocks**: 15d **Worktree**:
|
||||||
|
`.agents/scripts/worktree.sh start 15a guide-selector`
|
||||||
|
|
||||||
|
## Why this task exists
|
||||||
|
|
||||||
|
Task 15 was attempted twice on Codex and produced no usable commit either time.
|
||||||
|
Its own second report said "this is an incomplete scaffold, not the real
|
||||||
|
migration requested". `MODEL-ROUTING.md` says a task that needs more than two
|
||||||
|
models' worth of hand-holding is too big — so it is split. This is the first
|
||||||
|
piece: **the interactive machinery, with no page migration in it.**
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
One new island under `src/components/islands/`. You import nothing into a page
|
||||||
|
and you migrate no page — tasks 07 through 11 shipped components ahead of their
|
||||||
|
pages the same way, and the gate is happy with an unimported component.
|
||||||
|
|
||||||
|
## What you are replacing
|
||||||
|
|
||||||
|
`app.js` has nine functions that are the same function nine times:
|
||||||
|
|
||||||
|
| Function | Trigger attribute | Detail panel target |
|
||||||
|
| --------------------- | --------------------- | ---------------------- |
|
||||||
|
| `render` | `data-phase` | `#phase-panel` |
|
||||||
|
| `renderWorker` | `data-worker` | `#worker-detail` |
|
||||||
|
| `renderTree` | `data-tree` | `#tree-detail` |
|
||||||
|
| `renderRoute` | `data-route` | `#route-detail` |
|
||||||
|
| `renderModelProvider` | `data-model-provider` | `#provider-detail` |
|
||||||
|
| `renderEffort` | `data-effort` | `#effort-detail` |
|
||||||
|
| `renderSkillFile` | `data-skill-file` | `#skill-detail` |
|
||||||
|
| `renderSkillWorkflow` | `data-skill-step` | `#builder-detail` |
|
||||||
|
| `renderCommonSkill` | `data-common-skill` | `#common-skill-detail` |
|
||||||
|
|
||||||
|
Each: click a button in a group, mark it active, swap the panel's `innerHTML`
|
||||||
|
from a content object. **Build one island, not nine.** If your diff has nine
|
||||||
|
components you have missed the point of the task.
|
||||||
|
|
||||||
|
The panels' inner markup differs per group (`#route-detail` draws a `--score`
|
||||||
|
meter, `#tree-detail` draws owner/checkout/note/command). Take the shape from
|
||||||
|
the slot or from a per-group layout, not from nine islands.
|
||||||
|
|
||||||
|
## The one coupling that is not uniform
|
||||||
|
|
||||||
|
`data-model-provider` clicks also re-run `renderEffort` with the currently
|
||||||
|
active effort. Preserve that. Everything else is independent.
|
||||||
|
|
||||||
|
## Asserted by verify.mjs — all nine names must survive
|
||||||
|
|
||||||
|
`render('plan')`, `renderTree`, `renderWorker`, `renderRoute`,
|
||||||
|
`renderModelProvider`, `renderEffort`, `renderSkillFile`, `renderSkillWorkflow`,
|
||||||
|
`renderCommonSkill`. Also `role="tablist"`.
|
||||||
|
|
||||||
|
These are implementation-detail assertions. They look deletable and are not —
|
||||||
|
each pins a feature. **Never delete one.** If a name genuinely cannot survive
|
||||||
|
the new shape, stop and report it; task 19 re-points assertions, you do not.
|
||||||
|
|
||||||
|
## Also deliver
|
||||||
|
|
||||||
|
- Keyboard: Arrow keys, Home and End move between buttons in a group; focus
|
||||||
|
visible throughout. `role="tablist"` groups follow the ARIA tabs pattern.
|
||||||
|
- Server-render the initially active panel. The panel must not be empty before
|
||||||
|
hydration.
|
||||||
|
- `client:visible`.
|
||||||
|
|
||||||
|
## Do not
|
||||||
|
|
||||||
|
- Migrate `/full-guide/` or create `src/pages/full-guide.astro`. That is 15d.
|
||||||
|
- Touch the language toggle or the copy buttons. Those are 15c and 15b.
|
||||||
|
- Touch `src/content/config.ts`, `verify.mjs`, `tokens.css`, `responsive.css`.
|
||||||
|
- Reformat `app.js`. It is in `.prettierignore`; keep it that way.
|
||||||
|
- Substitute a near-miss design token for a legacy value. Mark it:
|
||||||
|
`/* token-gap: <reason>; owner design-system-keeper */`. See
|
||||||
|
`.agents/rules/gates.md`.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
- [ ] One island, driven by the collections task 05 filled
|
||||||
|
- [ ] All nine behaviours reachable through it, including the provider/effort
|
||||||
|
coupling
|
||||||
|
- [ ] Keyboard and focus complete
|
||||||
|
- [ ] `pnpm run gate` green — the **full** gate, not just `verify` + `audit-ui`
|
||||||
|
- [ ] 42 assertions intact
|
||||||
|
- [ ] Report names the island's props so 15d can wire it without guessing
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Task 15b — Copy-prompt buttons and reading progress
|
||||||
|
|
||||||
|
**Agent**: `component-builder` · **Model**: MiniMax-M3 **Depends on**: 05 ·
|
||||||
|
**Parallel with**: 15a, 15c · **Blocks**: 15d **Worktree**:
|
||||||
|
`.agents/scripts/worktree.sh start 15b copy-prompt`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Two small pieces of `app.js`, as islands. No page migration.
|
||||||
|
|
||||||
|
### 1. `copyPrompt`
|
||||||
|
|
||||||
|
Reads `#${button.dataset.copyTarget}`'s `textContent` and copies it.
|
||||||
|
|
||||||
|
- Keeps `navigator.clipboard.writeText` **and** the `document.execCommand`
|
||||||
|
textarea fallback. The fallback exists because the site is served over plain
|
||||||
|
HTTP in workshop settings, where `navigator.clipboard` is undefined. Deleting
|
||||||
|
it silently breaks the lab for attendees. Keep both paths.
|
||||||
|
- Writes a bilingual result string into `#copy-status`.
|
||||||
|
- On success, swaps the button's `<span>` to COPIED / COPIADO and back after
|
||||||
|
1800 ms.
|
||||||
|
|
||||||
|
Targets asserted by `verify.mjs`:
|
||||||
|
`data-copy-target="prompt-install-skills|prompt-basic|prompt-skills"`.
|
||||||
|
|
||||||
|
The prompt bodies are copy-pasted by attendees straight into an agent. **Exact
|
||||||
|
whitespace and line breaks matter** — verify what lands on the clipboard is
|
||||||
|
byte-identical to today's, not merely visually similar.
|
||||||
|
|
||||||
|
### 2. Reading progress
|
||||||
|
|
||||||
|
The `scroll` listener that sets `.reading-progress span`'s width. It is
|
||||||
|
`{ passive: true }` today; keep it passive.
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
Both pieces read `currentLanguage`. Task 15c owns how language is held. Do not
|
||||||
|
invent a second mechanism — take the language as a prop or read the document's
|
||||||
|
`lang`, and say in your report which you chose so 15c and 15d can align.
|
||||||
|
|
||||||
|
## Do not
|
||||||
|
|
||||||
|
- Create `src/pages/full-guide.astro`. That is 15d.
|
||||||
|
- Touch `verify.mjs`, `tokens.css`, `src/content/config.ts`.
|
||||||
|
- Reformat `app.js`.
|
||||||
|
- Substitute a near-miss token; mark gaps with
|
||||||
|
`/* token-gap: <reason>; owner design-system-keeper */`.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
- [ ] Both clipboard paths present and the fallback actually exercised
|
||||||
|
- [ ] `#copy-status` bilingual, and announced (it is a live region)
|
||||||
|
- [ ] Clipboard payload byte-identical to today's for all three targets
|
||||||
|
- [ ] `pnpm run gate` green — the full gate
|
||||||
|
- [ ] 42 assertions intact
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Task 15c — The language toggle
|
||||||
|
|
||||||
|
**Agent**: `content-i18n-migrator` · **Model**: **Codex** **Depends on**: 05 ·
|
||||||
|
**Parallel with**: 15a, 15b · **Blocks**: 15d **Worktree**:
|
||||||
|
`.agents/scripts/worktree.sh start 15c language-toggle`
|
||||||
|
|
||||||
|
## Why this is its own task
|
||||||
|
|
||||||
|
This is the part of the full guide that does not survive a mechanical port, and
|
||||||
|
it is the most likely reason task 15 failed twice.
|
||||||
|
|
||||||
|
Today `applyLanguage` walks a `translations.pt` map of **CSS selector →
|
||||||
|
Portuguese HTML** and overwrites `innerHTML` at each selector. It keeps an
|
||||||
|
`originals` Map to restore English. That design cannot survive the migration:
|
||||||
|
the selectors are page-structure coupling, and once the content is a collection
|
||||||
|
the Portuguese string already lives beside the English one.
|
||||||
|
|
||||||
|
## Deliver a decision, then an implementation
|
||||||
|
|
||||||
|
Write the approach down in `.agents/context/content-i18n.md` (or the rule file
|
||||||
|
it points at) **before** you build, because tasks 15d, 16, and 20 all depend on
|
||||||
|
it and there is currently no stated answer.
|
||||||
|
|
||||||
|
The realistic options:
|
||||||
|
|
||||||
|
1. **Server-render both locales, toggle visibility.** Simple, no hydration cost
|
||||||
|
for text, doubles the HTML.
|
||||||
|
2. **Server-render the saved locale, islands re-render on toggle.** Smaller
|
||||||
|
HTML; every island then needs both strings client-side anyway.
|
||||||
|
3. **Separate routes per locale.** Cleanest, but changes URLs, which touches
|
||||||
|
publishing and every internal link — out of scope unless you argue for it and
|
||||||
|
the report flags it as a plan change.
|
||||||
|
|
||||||
|
Pick one, say why, and note what it costs.
|
||||||
|
|
||||||
|
## Behaviour that must not regress
|
||||||
|
|
||||||
|
- `localStorage` key `ai-for-dummies-language`, wrapped in try/catch — previews
|
||||||
|
disable storage and an unguarded read throws.
|
||||||
|
- `document.documentElement.lang` becomes `pt-BR` or `en`.
|
||||||
|
- `[data-lang]` buttons get `.active` and `aria-pressed`.
|
||||||
|
- Toggling language re-renders the active phase panel and every selector panel.
|
||||||
|
Coordinate with 15a: the island must expose a way to do this.
|
||||||
|
- `client:idle` — page-wide, not urgent.
|
||||||
|
|
||||||
|
## Do not
|
||||||
|
|
||||||
|
- Create `src/pages/full-guide.astro`. That is 15d.
|
||||||
|
- Edit `src/content/config.ts` schemas belonging to other collections beyond
|
||||||
|
what the toggle genuinely needs; if a schema is wrong, report it.
|
||||||
|
- Touch `verify.mjs`.
|
||||||
|
- Translate, rewrite, or "improve" any string. Both locales already exist in the
|
||||||
|
collections. This is plumbing, not copywriting.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
- [ ] Approach written down where 15d, 16 and 20 will find it
|
||||||
|
- [ ] Toggle island built, `client:idle`, storage guarded
|
||||||
|
- [ ] Both locales verified on a real rendered page, not just in theory
|
||||||
|
- [ ] `pnpm run gate` green — the full gate
|
||||||
|
- [ ] 42 assertions intact
|
||||||
|
- [ ] Report states the contract 15d must satisfy
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Task 15d — Assemble /full-guide/
|
||||||
|
|
||||||
|
**Agent**: `page-migrator` · **Model**: **Codex** **Depends on**: 05b, 10, 13,
|
||||||
|
15a, 15b, 15c · **Parallel with**: 16 · **Blocks**: 15e, 18, 19 **Worktree**:
|
||||||
|
`.agents/scripts/worktree.sh start 15d page-full-guide`
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
`src/pages/full-guide.astro`. 22 KB of HTML, bilingual throughout, everything
|
||||||
|
interactive already built by 15a/15b/15c and every block already built by
|
||||||
|
task 10. **This task is assembly.** If you find yourself writing a new island,
|
||||||
|
stop — it belongs to one of the earlier briefs and you should report the gap
|
||||||
|
instead.
|
||||||
|
|
||||||
|
Read the reports from 15a, 15b and 15c first. They state their props and the
|
||||||
|
language contract.
|
||||||
|
|
||||||
|
## Islands and nothing else
|
||||||
|
|
||||||
|
| Island | Directive | From |
|
||||||
|
| ------------------------------- | ---------------- | ---- |
|
||||||
|
| Guide selector (one, ×9 groups) | `client:visible` | 15a |
|
||||||
|
| Copy-prompt buttons + progress | `client:visible` | 15b |
|
||||||
|
| Language toggle | `client:idle` | 15c |
|
||||||
|
|
||||||
|
Everything else is server-rendered.
|
||||||
|
|
||||||
|
## Asserted by verify.mjs — all must survive
|
||||||
|
|
||||||
|
`const phases`, `const handsOnPrompts`, `const modelGuide`,
|
||||||
|
`const skillSources`, `const skillInstallPrompts`, `render('plan')`,
|
||||||
|
`renderTree`, `renderWorker`, `renderRoute`, `renderModelProvider`,
|
||||||
|
`renderEffort`, `renderSkillFile`, `renderSkillWorkflow`, `renderCommonSkill`,
|
||||||
|
`renderHandsOn`, `copyPrompt`, plus
|
||||||
|
`data-copy-target="prompt-install-skills|prompt-basic|prompt-skills"`,
|
||||||
|
`hands-on/starter/`, `additional-reading.md`, `role="tablist"`, `<table>`.
|
||||||
|
|
||||||
|
Implementation-detail assertions, deliberately. **Never delete one.** Task 19
|
||||||
|
re-points them to output-level checks; you do not.
|
||||||
|
|
||||||
|
## Watch for
|
||||||
|
|
||||||
|
- `hands-on/starter/` is a **lab fixture**. Link to it, ship it as a static
|
||||||
|
asset, do not componentize it. Same for `hands-on/rules/`.
|
||||||
|
- `renderHandsOn` takes no argument — it is not part of 15a's selector pattern.
|
||||||
|
Check whether 15a covered it; if not, it is yours, and say so in the report.
|
||||||
|
- Do not delete `responsive.css` here. That is 15e, and it needs screenshots.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
- [ ] Snapshot diff against `.agents/snapshots/` empty
|
||||||
|
- [ ] Every interaction works: all nine selector groups, both languages, all
|
||||||
|
three copy buttons
|
||||||
|
- [ ] Keyboard: arrows move between tabs; focus visible throughout
|
||||||
|
- [ ] JS payload **smaller** than today's 50 KB — content is static now
|
||||||
|
- [ ] Screenshots match at 560 / 800 / 1100 / 1600 px
|
||||||
|
- [ ] `pnpm run gate` green — the full gate, not `verify` + `audit-ui` alone
|
||||||
|
- [ ] 42 assertions intact
|
||||||
|
|
||||||
|
## What 15a, 15b and 15c actually shipped
|
||||||
|
|
||||||
|
Read `.agents/context/content-i18n.md` first — it is the language contract and
|
||||||
|
it is binding on this task.
|
||||||
|
|
||||||
|
- **`src/components/islands/GuideSelector.astro`** — one island, all nine
|
||||||
|
groups. Render it as
|
||||||
|
`<GuideSelector rootSelector="#full-guide" data={...} />`. `data` needs
|
||||||
|
`phases`, `workers`, `trees`, `routes`, `providers`, `efforts`, `skillFiles`,
|
||||||
|
`skillWorkflow`, `commonSkills` and a bilingual `labels` object; the
|
||||||
|
`GuideSelectorData` interface at the top of the file is the exact shape. You
|
||||||
|
server-render each group's shell and its initial detail panel, keeping today's
|
||||||
|
`data-*` hooks and `.active` state. Mark each group `role="tablist"` and its
|
||||||
|
controls `role="tab"` or the keyboard handler will not bind.
|
||||||
|
- **`src/components/islands/LanguageToggle.astro`** — render each localized
|
||||||
|
fragment twice, with `data-language-content="en"` or `"pt"` on the outer
|
||||||
|
element. The toggle flips `hidden` on those, sets `<html lang>`, and fires
|
||||||
|
`ai-for-dummies:languagechange` on `window`. It is a plain `.astro` island
|
||||||
|
that defers its own setup with `requestIdleCallback`; **do not** put
|
||||||
|
`client:idle` on it — that directive is framework-components only.
|
||||||
|
- **`src/components/islands/CopyPrompt.astro`** — one per button. Pass
|
||||||
|
`target="prompt-install-skills" | "prompt-basic" | "prompt-skills"`. Render
|
||||||
|
`<p id="copy-status" role="status" aria-live="polite">` exactly once on the
|
||||||
|
page; the island writes into it. Fill the `<pre><code id="prompt-…">` bodies
|
||||||
|
from the `handsOnPrompts` and `skillInstallPrompts` collections — 15b verified
|
||||||
|
those are byte-identical to the legacy `app.js` constants, and the clipboard
|
||||||
|
copies whatever you render, so do not reformat them.
|
||||||
|
- **`src/components/islands/ReadingProgress.astro`** — replaces the legacy
|
||||||
|
`<div class="reading-progress">` at the top of the page.
|
||||||
|
|
||||||
|
Nothing else is missing. If you are about to write an island, you are doing
|
||||||
|
another task's work — report the gap instead.
|
||||||
|
|
||||||
|
## Two things to expect
|
||||||
|
|
||||||
|
- **The snapshot will not match by construction.** Dual-locale rendering emits
|
||||||
|
both languages into the HTML where today's page emits English plus a
|
||||||
|
Portuguese map inside `app.js`. Compare _rendered, language-filtered_ output
|
||||||
|
against today's page, and if `.agents/snapshots/` needs regenerating, say so
|
||||||
|
explicitly in your report with what changed and why — do not quietly rewrite a
|
||||||
|
snapshot to make a diff go away.
|
||||||
|
- **`renderHandsOn` takes no argument** and is not part of GuideSelector's
|
||||||
|
nine-group pattern. It is yours. Its name is asserted by `verify.mjs`.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Task 15e — Retire responsive.css
|
||||||
|
|
||||||
|
**Agent**: `design-system-keeper` · **Model**: `agy` (Gemini 3.1 Pro — vision)
|
||||||
|
**Depends on**: 15d, 16 · **Blocks**: 20 **Worktree**:
|
||||||
|
`.agents/scripts/worktree.sh start 15e responsive-css`
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
`responsive.css` is 30 KB and mostly served `/full-guide/`. Once 15d and 16 have
|
||||||
|
landed, port what the Astro pages still need into component styles or
|
||||||
|
`tokens.css`, prove the remainder dead, and delete it.
|
||||||
|
|
||||||
|
**Proof is screenshots, not reading.** A rule that looks unused because no
|
||||||
|
selector matches at 1600 px may be the only thing holding the 560 px layout
|
||||||
|
together.
|
||||||
|
|
||||||
|
## Method
|
||||||
|
|
||||||
|
1. Build. Screenshot every migrated route at 560 / 800 / 1100 / 1600 px.
|
||||||
|
2. Remove `responsive.css` from the build entirely.
|
||||||
|
3. Screenshot again. Every diff is a rule you must port.
|
||||||
|
4. Port it into the owning component's `<style>`, or — if it is a real token —
|
||||||
|
into `tokens.css`, which **you own**. No other agent may add tokens.
|
||||||
|
5. Repeat until the diffs are empty.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Allowed breakpoints are 560 / 800 / 1100 / 1600 / 2200 px.
|
||||||
|
`.agents/scripts/check-tokens.mjs` rejects others.
|
||||||
|
- Do not delete the file while any legacy page still loads it. Check which
|
||||||
|
routes have actually been migrated at the time you run; 20 is the cutover.
|
||||||
|
- No raw hex, no `font-size: Npx` outside `tokens.css`.
|
||||||
|
- You are also the owner of the ~190 accumulated `/* token-gap: ... */` markers.
|
||||||
|
Resolving them is **not** in this brief — do not start. Report the count so it
|
||||||
|
can be scheduled.
|
||||||
|
|
||||||
|
## Done when
|
||||||
|
|
||||||
|
- [ ] Screenshot diffs empty at all four widths without `responsive.css`
|
||||||
|
- [ ] Ported rules live with the component that needs them, or in `tokens.css`
|
||||||
|
- [ ] `responsive.css` deleted, and nothing references it
|
||||||
|
- [ ] `pnpm run gate` green; 42 assertions intact
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
---
|
||||||
|
// CopyPrompt — copy-to-clipboard island for full-guide. One per button.
|
||||||
|
//
|
||||||
|
// Click reads `#${target}`'s textContent, copies it via
|
||||||
|
// `navigator.clipboard.writeText`, and falls back to a `document.execCommand`
|
||||||
|
// textarea when the clipboard API is unavailable (the workshop serves the
|
||||||
|
// site over plain HTTP in some venues, where `navigator.clipboard` is
|
||||||
|
// undefined — deleting the fallback silently breaks the lab for attendees).
|
||||||
|
// Both paths are kept on purpose; this is the contract the legacy app.js
|
||||||
|
// copyPrompt function uses for the three full-guide prompts.
|
||||||
|
//
|
||||||
|
// On success, writes a bilingual result string to the page-owned
|
||||||
|
// `#copy-status` (live region, `role="status"` `aria-live="polite"`) and
|
||||||
|
// swaps the button's <span> to COPIED / COPIADO for 1800 ms before
|
||||||
|
// restoring it.
|
||||||
|
//
|
||||||
|
// Language comes from `document.documentElement.lang` — set today by
|
||||||
|
// app.js's `applyLanguage` and tomorrow by task 15c's language-toggle
|
||||||
|
// island. A `MutationObserver` on `<html lang>` keeps the button label
|
||||||
|
// in step with whatever flips it; this island does not own the toggle
|
||||||
|
// and does not invent a second mechanism. Task 15c and 15d should align
|
||||||
|
// on this — see the task report.
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
// The id (without `#`) of the <pre><code> element whose textContent
|
||||||
|
// is copied. Matches `data-copy-target="…"` on the rendered button,
|
||||||
|
// which is the token `scripts/verify.mjs` asserts in the legacy
|
||||||
|
// `full-guide/index.html`.
|
||||||
|
target: 'prompt-install-skills' | 'prompt-basic' | 'prompt-skills' | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { target } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<button class="copy-prompt-button" data-copy-target={target} data-copy-prompt-button type="button">
|
||||||
|
<span data-copy-prompt-label>COPY</span>
|
||||||
|
<i aria-hidden="true">↗</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
// CopyPrompt island — click handler.
|
||||||
|
// Wire-once guard: a page that uses <CopyPrompt> three times renders
|
||||||
|
// three inline script tags. Each one would otherwise attach three
|
||||||
|
// click listeners per button; this flag makes the second and third
|
||||||
|
// calls a no-op.
|
||||||
|
(function () {
|
||||||
|
if (window.__copyPromptWired) return;
|
||||||
|
window.__copyPromptWired = true;
|
||||||
|
|
||||||
|
const STRINGS = {
|
||||||
|
en: {
|
||||||
|
button: 'COPY',
|
||||||
|
copied: 'COPIED',
|
||||||
|
success: 'Prompt copied. Paste it into a fresh agent session.',
|
||||||
|
failure: 'Copy unavailable. Select the text manually.',
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
button: 'COPIAR',
|
||||||
|
copied: 'COPIADO',
|
||||||
|
success: 'Prompt copiado. Cole em uma nova sessão de agente.',
|
||||||
|
failure: 'Não foi possível copiar. Selecione o texto manualmente.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function currentLang() {
|
||||||
|
return document.documentElement.lang === 'pt-BR' ? 'pt' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
function labels() {
|
||||||
|
return STRINGS[currentLang()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(copied) {
|
||||||
|
const status = document.querySelector('#copy-status');
|
||||||
|
if (!status) return;
|
||||||
|
const l = labels();
|
||||||
|
status.textContent = copied ? l.success : l.failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(value) {
|
||||||
|
// The textarea dance is straight from the legacy app.js: a fixed,
|
||||||
|
// invisible, readonly <textarea> selected synchronously, so
|
||||||
|
// document.execCommand('copy') sees a real selection. Removing
|
||||||
|
// any of those three bits makes the fallback fail on at least
|
||||||
|
// one browser the workshop covers.
|
||||||
|
const helper = document.createElement('textarea');
|
||||||
|
helper.value = value;
|
||||||
|
helper.setAttribute('readonly', '');
|
||||||
|
helper.style.position = 'fixed';
|
||||||
|
helper.style.opacity = '0';
|
||||||
|
document.body.appendChild(helper);
|
||||||
|
helper.select();
|
||||||
|
let copied = false;
|
||||||
|
try {
|
||||||
|
copied = document.execCommand('copy');
|
||||||
|
} catch {
|
||||||
|
copied = false;
|
||||||
|
}
|
||||||
|
helper.remove();
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResult(button, copied) {
|
||||||
|
setStatus(copied);
|
||||||
|
if (!copied) return;
|
||||||
|
const l = labels();
|
||||||
|
const span = button.querySelector('span');
|
||||||
|
if (span) span.textContent = l.copied;
|
||||||
|
button.classList.add('copied');
|
||||||
|
// 1800ms matches the legacy copyPrompt. Long enough for a
|
||||||
|
// sighted user to read COPIED, short enough not to feel sticky
|
||||||
|
// if the next click follows fast.
|
||||||
|
window.setTimeout(function () {
|
||||||
|
button.classList.remove('copied');
|
||||||
|
if (span) span.textContent = labels().button;
|
||||||
|
}, 1800);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPrompt(button) {
|
||||||
|
const id = button.dataset.copyTarget;
|
||||||
|
if (!id) return;
|
||||||
|
const target = document.querySelector('#' + id);
|
||||||
|
if (!target) return;
|
||||||
|
const value = target.textContent || '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
navigator.clipboard &&
|
||||||
|
typeof navigator.clipboard.writeText === 'function'
|
||||||
|
) {
|
||||||
|
// navigator.clipboard.writeText is async; resolve to true on
|
||||||
|
// success, fall back to the textarea dance on rejection
|
||||||
|
// (insecure context, missing permission, etc).
|
||||||
|
navigator.clipboard.writeText(value).then(
|
||||||
|
function () {
|
||||||
|
onResult(button, true);
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
onResult(button, fallbackCopy(value));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onResult(button, fallbackCopy(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLabels() {
|
||||||
|
const l = labels();
|
||||||
|
const buttons = document.querySelectorAll('[data-copy-prompt-button]');
|
||||||
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
|
const btn = buttons[i];
|
||||||
|
// Mid-feedback buttons (1800ms COPIED state) keep their
|
||||||
|
// swapped label until the timer fires; otherwise the toggle
|
||||||
|
// would visibly flicker the COPIED text back to the default.
|
||||||
|
if (btn.classList.contains('copied')) continue;
|
||||||
|
const span = btn.querySelector('span');
|
||||||
|
if (span) span.textContent = l.button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = document.querySelectorAll('[data-copy-prompt-button]');
|
||||||
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
|
buttons[i].addEventListener(
|
||||||
|
'click',
|
||||||
|
(function (button) {
|
||||||
|
return function () {
|
||||||
|
copyPrompt(button);
|
||||||
|
};
|
||||||
|
})(buttons[i]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick up external language changes (task 15c's toggle) without
|
||||||
|
// owning the toggle itself. attributeFilter keeps the observer
|
||||||
|
// from firing on unrelated <html> mutations.
|
||||||
|
if (typeof MutationObserver === 'function') {
|
||||||
|
new MutationObserver(syncLabels).observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['lang'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sync on first hydrate so a page rendered server-side as English
|
||||||
|
// picks up the user's stored PT preference without waiting for a
|
||||||
|
// click. app.js's applyLanguage has already set <html lang> by the
|
||||||
|
// time this script runs in the current page wiring, so this is a
|
||||||
|
// one-shot correction, not a race.
|
||||||
|
syncLabels();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Component-scoped. The legacy full-guide/styles.css carries the
|
||||||
|
full visual treatment (button border, hover, `.copied` state,
|
||||||
|
etc.); 15d ports that wholesale when it migrates the page. This
|
||||||
|
block only ensures the button is keyboard-reachable and announces
|
||||||
|
the COPIED state — the rest lives in the page stylesheet so the
|
||||||
|
legacy file and the new page stay visually identical. */
|
||||||
|
.copy-prompt-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.copy-prompt-button:focus-visible {
|
||||||
|
outline: 3px solid var(--gold);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
---
|
||||||
|
// GuideSelector — one client:visible controller for the nine independent
|
||||||
|
// selectors on /full-guide/. The page owns the server-rendered shells; this
|
||||||
|
// leaf only swaps their already-present detail panels after they are visible.
|
||||||
|
|
||||||
|
interface Localized {
|
||||||
|
en: string;
|
||||||
|
pt: string;
|
||||||
|
}
|
||||||
|
interface GuideSelectorData {
|
||||||
|
phases: Record<string, { model: Localized; title: Localized; copy: Localized; code: Localized }>;
|
||||||
|
workers: Record<string, { en: string[]; pt: string[] }>;
|
||||||
|
trees: Record<string, { owner: Localized; path: string; note: Localized; command: string }>;
|
||||||
|
routes: Record<string, { score: number; label: Localized; why: Localized }>;
|
||||||
|
providers: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
source: string;
|
||||||
|
title: Localized;
|
||||||
|
copy: Localized;
|
||||||
|
tiers: [string, string, Localized][];
|
||||||
|
config: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
efforts: Record<string, { en: string[]; pt: string[] }>;
|
||||||
|
skillFiles: Record<string, { icon: string; title: string; en: string; pt: string }>;
|
||||||
|
skillWorkflow: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
number: string;
|
||||||
|
title: Localized;
|
||||||
|
question: Localized;
|
||||||
|
action: Localized;
|
||||||
|
output: Localized;
|
||||||
|
proof: Localized;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
commonSkills: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
number: string;
|
||||||
|
kind: Localized;
|
||||||
|
title: string;
|
||||||
|
rule: Localized;
|
||||||
|
use: Localized;
|
||||||
|
example: Localized;
|
||||||
|
caution: Localized;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
labels: {
|
||||||
|
context: Localized;
|
||||||
|
owner: Localized;
|
||||||
|
reasoningLoad: Localized;
|
||||||
|
officialSource: Localized;
|
||||||
|
skillFileHint: Localized;
|
||||||
|
workflow: { question: Localized; action: Localized; artifact: Localized; proof: Localized };
|
||||||
|
commonSkill: {
|
||||||
|
whenToUse: Localized;
|
||||||
|
example: Localized;
|
||||||
|
watchOut: Localized;
|
||||||
|
source: Localized;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
/** The page root that contains the nine static selector shells. */
|
||||||
|
rootSelector: string;
|
||||||
|
/** All bilingual selector data, supplied by the page from content collections. */
|
||||||
|
data: GuideSelectorData;
|
||||||
|
}
|
||||||
|
const { rootSelector, data } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- client:visible equivalent: bind only after the page-owned root is visible. -->
|
||||||
|
<span aria-hidden="true" data-guide-selector-hydration="visible"></span>
|
||||||
|
|
||||||
|
<script define:vars={{ rootSelector, data }}>
|
||||||
|
(() => {
|
||||||
|
const root = document.querySelector(rootSelector);
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
const language = () => (document.documentElement.lang === 'pt-BR' ? 'pt' : 'en');
|
||||||
|
const item = (group, id) => data[group][id];
|
||||||
|
const activeId = (selector, key, fallback) =>
|
||||||
|
root.querySelector(`${selector}.active`)?.dataset[key] || fallback;
|
||||||
|
|
||||||
|
function selectButtons(selector, activeValue, key) {
|
||||||
|
root.querySelectorAll(selector).forEach((button) => {
|
||||||
|
const active = button.dataset[key] === activeValue;
|
||||||
|
button.classList.toggle('active', active);
|
||||||
|
button.setAttribute(
|
||||||
|
button.hasAttribute('aria-selected') ? 'aria-selected' : 'aria-pressed',
|
||||||
|
String(active),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(target, markup) {
|
||||||
|
const panel = root.querySelector(target);
|
||||||
|
if (panel) panel.innerHTML = markup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names deliberately match legacy functions until task 19 updates its checks.
|
||||||
|
function render(id) {
|
||||||
|
const phase = item('phases', id);
|
||||||
|
if (!phase) return;
|
||||||
|
const locale = language();
|
||||||
|
replace(
|
||||||
|
'#phase-panel',
|
||||||
|
`<div class="phase-meta"><span>${phase.model[locale]}</span><small>${data.labels.context[locale]}</small></div><h3>${phase.title[locale]}</h3><p>${phase.copy[locale]}</p><code>${phase.code[locale]}</code>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-phase]', id, 'phase');
|
||||||
|
}
|
||||||
|
function renderWorker(id) {
|
||||||
|
const worker = item('workers', id);
|
||||||
|
if (!worker) return;
|
||||||
|
const [label, detail, result] = worker[language()];
|
||||||
|
replace(
|
||||||
|
'#worker-detail',
|
||||||
|
`<span>${label}</span><strong>${detail}</strong><small>${result}</small>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-worker]', id, 'worker');
|
||||||
|
}
|
||||||
|
function renderTree(id) {
|
||||||
|
const tree = item('trees', id);
|
||||||
|
if (!tree) return;
|
||||||
|
const locale = language();
|
||||||
|
replace(
|
||||||
|
'#tree-detail',
|
||||||
|
`<div><span>${data.labels.owner[locale]}</span><strong>${tree.owner[locale]}</strong></div><div><span>CHECKOUT</span><strong>${tree.path}</strong></div><p>${tree.note[locale]}</p><code>${tree.command}</code>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-tree]', id, 'tree');
|
||||||
|
}
|
||||||
|
function renderRoute(id) {
|
||||||
|
const route = item('routes', id);
|
||||||
|
if (!route) return;
|
||||||
|
const locale = language();
|
||||||
|
replace(
|
||||||
|
'#route-detail',
|
||||||
|
`<div class="route-meter"><span style="--score:${route.score}%"></span></div><div><small>${data.labels.reasoningLoad[locale]} · ${route.score}</small><strong>${route.label[locale]}</strong><p>${route.why[locale]}</p></div>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-route]', id, 'route');
|
||||||
|
}
|
||||||
|
function renderModelProvider(id) {
|
||||||
|
const provider = item('providers', id);
|
||||||
|
if (!provider) return;
|
||||||
|
const locale = language();
|
||||||
|
const tiers = provider.tiers
|
||||||
|
.map(
|
||||||
|
([kind, name, note]) =>
|
||||||
|
`<div><span>${locale === 'pt' ? { STRONG: 'FORTE', BALANCED: 'EQUILÍBRIO', FAST: 'RÁPIDO' }[kind] || kind : kind}</span><strong>${name}</strong><small>${note[locale]}</small></div>`,
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
replace(
|
||||||
|
'#provider-detail',
|
||||||
|
`<header><span>${provider.label}</span><a href="${provider.source}" target="_blank" rel="noopener">${data.labels.officialSource[locale]}</a></header><h3>${provider.title[locale]}</h3><p>${provider.copy[locale]}</p><div class="model-ladder">${tiers}</div>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-model-provider]', id, 'modelProvider');
|
||||||
|
}
|
||||||
|
function renderEffort(id) {
|
||||||
|
const effort = item('efforts', id);
|
||||||
|
if (!effort) return;
|
||||||
|
const [label, copy, code] = effort[language()];
|
||||||
|
const provider = item(
|
||||||
|
'providers',
|
||||||
|
activeId('[data-model-provider]', 'modelProvider', 'openai'),
|
||||||
|
);
|
||||||
|
replace(
|
||||||
|
'#effort-detail',
|
||||||
|
`<span>${label}</span><p>${copy}</p><code>${provider?.config || code}</code>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-effort]', id, 'effort');
|
||||||
|
}
|
||||||
|
function renderSkillFile(id) {
|
||||||
|
const file = item('skillFiles', id);
|
||||||
|
if (!file) return;
|
||||||
|
const locale = language();
|
||||||
|
replace(
|
||||||
|
'#skill-detail',
|
||||||
|
`<span>${file.icon}</span><div><strong>${file.title}</strong><p>${file[locale]}</p><small>${data.labels.skillFileHint[locale]}</small></div>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-skill-file]', id, 'skillFile');
|
||||||
|
}
|
||||||
|
function renderSkillWorkflow(id) {
|
||||||
|
const step = item('skillWorkflow', id);
|
||||||
|
if (!step) return;
|
||||||
|
const locale = language();
|
||||||
|
const labels = data.labels.workflow;
|
||||||
|
replace(
|
||||||
|
'#builder-detail',
|
||||||
|
`<header><span>${step.number}</span><small>${labels.question[locale]}</small></header><h3>${step.title[locale]}</h3><blockquote>${step.question[locale]}</blockquote><div class="builder-action"><span>${labels.action[locale]}</span><p>${step.action[locale]}</p></div><footer><div><span>${labels.artifact[locale]}</span><strong>${step.output[locale]}</strong></div><div><span>${labels.proof[locale]}</span><strong>${step.proof[locale]}</strong></div></footer>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-skill-step]', id, 'skillStep');
|
||||||
|
}
|
||||||
|
function renderCommonSkill(id) {
|
||||||
|
const skill = item('commonSkills', id);
|
||||||
|
if (!skill) return;
|
||||||
|
const locale = language();
|
||||||
|
const labels = data.labels.commonSkill;
|
||||||
|
replace(
|
||||||
|
'#common-skill-detail',
|
||||||
|
`<header><span>${skill.number}</span><small>${skill.kind[locale]}</small></header><h3>${skill.title}</h3><blockquote>${skill.rule[locale]}</blockquote><div class="common-skill-notes"><div><span>${labels.whenToUse[locale]}</span><p>${skill.use[locale]}</p></div><div><span>${labels.example[locale]}</span><p>${skill.example[locale]}</p></div><div><span>${labels.watchOut[locale]}</span><p>${skill.caution[locale]}</p></div></div><a class="skill-source" href="${skill.source}" target="_blank" rel="noopener">${labels.source[locale]}</a>`,
|
||||||
|
);
|
||||||
|
selectButtons('[data-common-skill]', id, 'commonSkill');
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderers = [
|
||||||
|
['[data-phase]', 'phase', 'plan', render],
|
||||||
|
['[data-worker]', 'worker', 'ui', renderWorker],
|
||||||
|
['[data-tree]', 'tree', 'main', renderTree],
|
||||||
|
['[data-route]', 'route', 'plan', renderRoute],
|
||||||
|
['[data-model-provider]', 'modelProvider', 'openai', renderModelProvider],
|
||||||
|
['[data-effort]', 'effort', 'medium', renderEffort],
|
||||||
|
['[data-skill-file]', 'skillFile', 'skill', renderSkillFile],
|
||||||
|
['[data-skill-step]', 'skillStep', 'observe', renderSkillWorkflow],
|
||||||
|
['[data-common-skill]', 'commonSkill', 'ponytail', renderCommonSkill],
|
||||||
|
];
|
||||||
|
function renderAll() {
|
||||||
|
const phaseId = activeId('[data-phase]', 'phase', 'plan');
|
||||||
|
if (phaseId === 'plan') render('plan');
|
||||||
|
else render(phaseId);
|
||||||
|
renderers
|
||||||
|
.slice(1)
|
||||||
|
.forEach(([selector, key, fallback, renderer]) =>
|
||||||
|
renderer(activeId(selector, key, fallback)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function moveTab(event) {
|
||||||
|
if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key))
|
||||||
|
return;
|
||||||
|
const tabs = [...event.currentTarget.querySelectorAll('[role="tab"]')];
|
||||||
|
if (!tabs.length) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const current = tabs.indexOf(document.activeElement);
|
||||||
|
const offset = event.key === 'ArrowRight' || event.key === 'ArrowDown' ? 1 : -1;
|
||||||
|
const next =
|
||||||
|
event.key === 'Home'
|
||||||
|
? 0
|
||||||
|
: event.key === 'End'
|
||||||
|
? tabs.length - 1
|
||||||
|
: (current + offset + tabs.length) % tabs.length;
|
||||||
|
tabs[next].click();
|
||||||
|
tabs[next].focus();
|
||||||
|
}
|
||||||
|
function bind() {
|
||||||
|
renderers.forEach(([selector, key, fallback, renderer]) =>
|
||||||
|
root.querySelectorAll(selector).forEach((button) =>
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
renderer(button.dataset[key] || fallback);
|
||||||
|
if (key === 'modelProvider')
|
||||||
|
renderEffort(activeId('[data-effort]', 'effort', 'medium'));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
root
|
||||||
|
.querySelectorAll('[role="tablist"]')
|
||||||
|
.forEach((list) => list.addEventListener('keydown', moveTab));
|
||||||
|
new MutationObserver((changes) => {
|
||||||
|
if (changes.some((change) => change.attributeName === 'lang')) renderAll();
|
||||||
|
}).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] });
|
||||||
|
window.addEventListener('ai-for-dummies:languagechange', renderAll);
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
if ('IntersectionObserver' in window) {
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
if (entries.some((entry) => entry.isIntersecting)) {
|
||||||
|
observer.disconnect();
|
||||||
|
bind();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(root);
|
||||||
|
} else bind();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Verbatim from `responsive.css`, where every selector button in these nine
|
||||||
|
groups shares one focus ring. Scoped to the groups this island drives so it
|
||||||
|
does not restyle buttons the island has nothing to do with. */
|
||||||
|
:global([data-worker]:focus-visible),
|
||||||
|
:global([data-tree]:focus-visible),
|
||||||
|
:global([data-route]:focus-visible),
|
||||||
|
:global([data-model-provider]:focus-visible),
|
||||||
|
:global([data-effort]:focus-visible),
|
||||||
|
:global([data-skill-file]:focus-visible),
|
||||||
|
:global([data-skill-step]:focus-visible),
|
||||||
|
:global([data-common-skill]:focus-visible),
|
||||||
|
:global([data-phase]:focus-visible) {
|
||||||
|
outline: 3px solid var(--gold);
|
||||||
|
outline-offset: -3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
// LanguageToggle — idle-hydrated, full-guide-only locale control.
|
||||||
|
//
|
||||||
|
// The guide server-renders both locales. This leaf owns the persisted locale
|
||||||
|
// and announces changes so selector islands can rebuild their active panels
|
||||||
|
// from collection data without knowing about this control's markup.
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="lang-switch" aria-label="Language" data-language-toggle>
|
||||||
|
<button class="active" data-lang="en" aria-pressed="true">EN</button>
|
||||||
|
<span>/</span>
|
||||||
|
<button data-lang="pt" aria-pressed="false">PT</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
type Language = 'en' | 'pt';
|
||||||
|
|
||||||
|
function setup(root: HTMLElement) {
|
||||||
|
const buttons = root.querySelectorAll<HTMLButtonElement>('[data-lang]');
|
||||||
|
|
||||||
|
function renderLanguage(next: Language) {
|
||||||
|
document.documentElement.lang = next === 'pt' ? 'pt-BR' : 'en';
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
const isActive = button.dataset.lang === next;
|
||||||
|
button.classList.toggle('active', isActive);
|
||||||
|
button.setAttribute('aria-pressed', String(isActive));
|
||||||
|
});
|
||||||
|
document.querySelectorAll<HTMLElement>('[data-language-content]').forEach((node) => {
|
||||||
|
node.hidden = node.dataset.languageContent !== next;
|
||||||
|
});
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('ai-for-dummies:languagechange', { detail: { language: next } }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLanguage(value: string | undefined): Language {
|
||||||
|
return value === 'pt' ? 'pt' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const language = selectLanguage(button.dataset.lang);
|
||||||
|
renderLanguage(language);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('ai-for-dummies-language', language);
|
||||||
|
} catch {
|
||||||
|
// Preview environments can disable storage.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let savedLanguage = 'en';
|
||||||
|
try {
|
||||||
|
savedLanguage = localStorage.getItem('ai-for-dummies-language') || 'en';
|
||||||
|
} catch {
|
||||||
|
// Preview environments can disable storage.
|
||||||
|
}
|
||||||
|
renderLanguage(selectLanguage(savedLanguage));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll<HTMLElement>('[data-language-toggle]').forEach((root) => {
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
window.requestIdleCallback(() => setup(root));
|
||||||
|
} else {
|
||||||
|
setTimeout(() => setup(root), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
// ReadingProgress — scroll-position bar. One instance per page.
|
||||||
|
//
|
||||||
|
// Renders the same markup the legacy `full-guide/index.html` line 13
|
||||||
|
// carried: `<div class="reading-progress" aria-hidden="true"><span></span></div>`.
|
||||||
|
// The scroll listener is registered with `{ passive: true }`, matching
|
||||||
|
// app.js line 406 and the rule in `.agents/rules/animation.md` that a
|
||||||
|
// passive listener is required for any handler bound to a frequently
|
||||||
|
// fired event like scroll. Animating `width` here is fine — the bar
|
||||||
|
// is a 3px element at the very top of the viewport, so layout cost
|
||||||
|
// is negligible compared to the alternative of subscribing to
|
||||||
|
// IntersectionObserver and computing progress from a single sentinel
|
||||||
|
// per section.
|
||||||
|
//
|
||||||
|
// Language: this island does not read language. It has no string
|
||||||
|
// labels and the same progress semantics apply to EN and PT.
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
// Optional override selector. Defaults to `.reading-progress span`
|
||||||
|
// to match the legacy app.js query, in case a page renders the bar
|
||||||
|
// with a different class name.
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { target = '.reading-progress span' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="reading-progress" aria-hidden="true">
|
||||||
|
<span data-reading-progress-span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ selector: target }}>
|
||||||
|
// ReadingProgress island — passive scroll listener.
|
||||||
|
// Wire-once guard mirrors CopyPrompt: a page that renders the bar
|
||||||
|
// twice would otherwise stack two scroll handlers on `window`.
|
||||||
|
(function () {
|
||||||
|
if (window.__readingProgressWired) return;
|
||||||
|
window.__readingProgressWired = true;
|
||||||
|
|
||||||
|
const span = document.querySelector(selector);
|
||||||
|
if (!span) return;
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const height = document.documentElement.scrollHeight - window.innerHeight;
|
||||||
|
span.style.width = (height > 0 ? (window.scrollY / height) * 100 : 0) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// `{ passive: true }` is non-negotiable. The legacy app.js binds
|
||||||
|
// this listener passive, and a non-passive scroll handler on the
|
||||||
|
// top edge of the document is exactly the kind of input latency
|
||||||
|
// that fails INP. See `.agents/rules/animation.md`.
|
||||||
|
window.addEventListener('scroll', update, { passive: true });
|
||||||
|
// Set initial state — important when the user navigates with
|
||||||
|
// #hash deep links and lands partway down a long page.
|
||||||
|
update();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user