diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1cfde1..cd85dd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,12 @@ jobs: - name: Build run: pnpm build + # The app shell links to routes Phase 07 has not built yet, so `.lycheeignore` excludes + # exactly those. This fails if one of them has since been built, so the exclusions cannot + # outlive their purpose and start hiding a real 404. + - name: Check for stale link exclusions + run: node tools/checks/stale-link-ignores.mjs + # --root-dir is required for the root-relative hrefs Astro emits (/_astro/...); # without it lychee cannot resolve them in local files and errors on every page. - name: Link check diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 0000000..6674d25 --- /dev/null +++ b/.lycheeignore @@ -0,0 +1,29 @@ +# Routes the app shell links to that do not exist yet. +# +# The header, footer, and 404 page are built in Phase 05 but link to the full site's routes, +# which Phase 07 builds. Until then the offline link check would fail on every page carrying the +# chrome — so these ten routes are excluded, and nothing else is. +# +# **Delete each entry as its page lands.** `tools/checks/stale-link-ignores.mjs` runs in CI and +# fails if an excluded route exists in `dist`, so an entry cannot quietly outlive its purpose and +# start hiding a real 404. Phase 07 is done when this file is gone. +# +# Anchored on purpose: /dist/programs$ must not also swallow /dist/programs/frc. + +# Phase 07 group A +/dist/about$ +/dist/sponsors$ + +# Phase 07 group B +/dist/contact$ +/dist/donate$ +/dist/get-involved$ + +# Phase 07 group C +/dist/programs$ +/dist/programs/fll$ +/dist/programs/frc$ +/dist/programs/frc/robots$ + +# Phase 07 group D +/dist/calendar/sc2$ diff --git a/knip.jsonc b/knip.jsonc index cc8a1e7..47711c5 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -3,8 +3,9 @@ "entry": [ "src/pages/**", "functions/**", - // Exports the font URLs BaseLayout preloads; wired up in Phase 05. - "src/styles/fonts.ts", + // Used by the program pages in Phase 07. It exists now because the theming contract + // belongs with the rest of the shell, not with the first page that happens to need it. + "src/layouts/ProgramLayout.astro", // Formats an event's date from `start`/`end`, which replaced the `displayDate` string every // event used to restate. Phase 08 builds the pages that call it; that phase's acceptance // criteria require this entry to be deleted (plan/08-events.md). diff --git a/plan/05-app-shell.md b/plan/05-app-shell.md index 5219e15..c16a826 100644 --- a/plan/05-app-shell.md +++ b/plan/05-app-shell.md @@ -39,6 +39,8 @@ Rebuild (reference `legacy/src/components/Navbar.tsx`, 261 LOC, for link invento - Links: About / Programs ▾ / Sponsors / Donate + primary "Get involved" button (≥44px target). `aria-current="page"` states. - **Programs dropdown**: CSS `:focus-within`/popover-attribute disclosure listing FLL, FRC, Robots, Calendar — while the "Programs" link itself navigates to the **`/programs` hub page** (Phase 07), so touch and no-JS users get a real destination. Keyboard reachable, Esc closes. - Mobile menu: **full-height sheet** — ≥48px rows (About, Programs, Sponsors, Calendar), "Get involved" + "Donate" as large buttons pinned at the bottom; `aria-expanded` toggle script (~20 lines), Esc/backdrop close, body scroll locked while open. + (Backdrop close is not implemented: the sheet is opaque and its nav fills it, so there is no + backdrop to tap — the listener that claimed to do this could never fire. See the notes below.) ### 5. Footer — `src/components/ui/Footer.astro` @@ -54,9 +56,72 @@ Point `src/pages/index.astro` (still placeholder) at BaseLayout so the shell is ## Acceptance criteria -- [ ] Omitting `description` on a page using BaseLayout is a type error. -- [ ] View-source on any page: title, description, canonical, OG/Twitter set, NGO JSON-LD present and valid (paste into Google Rich Results test manually once). -- [ ] Navbar: keyboard-only operation works (tab order, Esc, aria-expanded state); usable at 360px; zero JS beyond the disclosure toggle. -- [ ] Skip link functions; landmarks: exactly one `header`, `main`, `footer`, labeled `nav`s. -- [ ] Web manifest + icons valid (Lighthouse PWA-adjacent audits pass; no console 404s). -- [ ] `pnpm check && pnpm build` green. +- [x] Omitting `description` on a page using BaseLayout is a type error — verified: `Property 'description' is missing in type '{ children: any; title: string; }' but required in type 'Props'`. +- [x] View-source on any page: title, description, canonical, full OG/Twitter set, and NGO JSON-LD present. Homepage also emits WebSite. (Rich Results test is blocked from this environment — `validator.schema.org` and `search.google.com` are not reachable — so that one manual check is outstanding; noted for Phase 10, which already tasks validating every JSON-LD shape.) +- [x] Navbar keyboard-only operation: skip link is the first tab stop and becomes visible on focus; the Programs panel is keyboard reachable and **Esc closes it**; the mobile toggle is 44×44, flips `aria-expanded`, locks body scroll, and Esc closes it and returns focus. Usable at 360px; the only script is the ~30-line sheet toggle. Verified in a real browser: the panel opens on click, closes on Esc and on light-dismiss, and exposes `expanded` in the accessibility tree; the sheet swaps its icon, locks scroll, marks the page behind it `inert` (only the close button stays focusable), and closes when the viewport crosses `md`. +- [x] Skip link functions; landmarks are exactly one `header`, `main`, `footer`, and three labeled `nav`s (Main, Mobile, Footer). +- [x] Web manifest + icons valid, no console 404s: `favicon.ico` carried over, `icon.svg` from the square mark, and 180/192/512 PNGs rendered from it. +- [x] `pnpm check && pnpm build` green. + +### Notes and deviations + +- **The Programs panel is a native `popover`, not a `:focus-within` disclosure.** The brief named + both options; `:focus-within` has no dismissal path, so it could not satisfy this phase's own + "Esc closes" requirement (§9: "menus close on Esc") and missed WCAG 2.2 SC 1.4.13. The + `popover` attribute supplies Esc, light-dismiss, top-layer placement and the `expanded` state + with no script — rung 1 of the primitives README's interactivity ladder. The `/programs` link + keeps its real destination; the chevron beside it is the invoker. +- **Nav inventory lives in `src/data/site.ts`.** The header, the mobile sheet, the footer and the + 404 page each declared their own list, and they had already diverged. The desktop/sheet split is + now the `surfaces` field on each entry, so it reads as a decision rather than an omission. +- **`ui-link`, `external-link` and `container-page` are utilities.** DESIGN.md §8's "UI links may + drop underline at rest but underline on hover/focus" was half-implemented: twelve call sites + wrote `no-underline` and no chrome link underlined on hover. Same for §8's external-link icon and + §5's container gutters, which were a class string repeated at five sites. +- **The sheet marks the page behind it `inert`.** It is opaque and covers the viewport, so Tab past + its last item used to walk every invisible link in `main` and `footer`. +- **The header lockup animates `scale`, not `height`.** `height` is not a compositor property, so + the old keyframe relayouted the sticky header and reflowed the page on every scroll frame + through the first 120px — against §6's "only `opacity` and `transform` animate". +- **`site.webmanifest` is a generated route.** It restated the org name, a third variant of the + description, and both brand colours as literals. `src/pages/site.webmanifest.ts` builds it from + `site.ts`, and its maskable entry points at a separate padded render: the plain 512 spans ~88% of + its box, so a launcher mask clipped the gear teeth — reusing it was worse than declaring no + maskable icon at all. + +- **The mobile sheet has no backdrop dismissal.** The brief asks for "Esc/backdrop close". The + sheet is `fixed inset-0` and opaque, and its `