From dd34602b0ea0e11cdc65de592983a51ba9621102 Mon Sep 17 00:00:00 2001
From: Heyoub
Date: Mon, 24 Aug 2026 11:47:14 -0400
Subject: [PATCH 01/18] chore(plans): add site-check-153 plan
Issue #153 walks the live site on desktop and mobile and collects five
findings. Four are shippable together because they all land in the app
shell or in one screen's outbound links; the fifth (replace the Home hero
CTA with a mailing-list invite) has no mechanism to build against, so the
plan records the block up front rather than inventing one.
Co-Authored-By: Claude Fable 5
---
plans/site-check-153.md | 151 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 151 insertions(+)
create mode 100644 plans/site-check-153.md
diff --git a/plans/site-check-153.md b/plans/site-check-153.md
new file mode 100644
index 0000000..d288c9a
--- /dev/null
+++ b/plans/site-check-153.md
@@ -0,0 +1,151 @@
+---
+status: in-progress
+depends: []
+specs:
+ - specs/behaviors/app-shell.md
+ - specs/screens/volunteer.md
+issues: [153]
+---
+
+# Plan: site check — header order, mobile sheet padding, dead outbound links
+
+## Scope
+
+Issue [#153](https://github.com/CodeForPhilly/codeforphilly-ng/issues/153)
+("Site check for desktop & mobile") collects a walkthrough of the live site on
+both breakpoints. Four of its items are shippable now; one is blocked (see
+Follow-ups).
+
+What ships:
+
+- **Desktop header reorder** (spec-governed). The Volunteer CTA leaves the
+ content nav and becomes the rightmost element of the header, after the auth
+ control; About joins the left cluster's text links; a GitHub icon link is
+ added to the right cluster.
+- **Mobile sheet padding + accessible name.** The sheet's nav and search sat
+ flush against the panel edge. Fixed with the intended shadcn structure
+ (`SheetHeader` + `SheetTitle`) plus explicit horizontal padding — which also
+ gives the underlying Radix dialog the accessible name it was missing.
+- **Header ARIA cleanups.** Three defects surfaced by an accessibility pass over
+ the header, done here because this plan rewrites the same file.
+- **Dead outbound links** (spec-governed). The whole `codeforphilly.gitbook.io`
+ space returns 404 "Content owner not found"; `Volunteer.tsx`'s two remaining
+ GitBook links are repointed at live equivalents. Same class of defect as
+ [`home-start-project-cta`](home-start-project-cta.md) (PR #128), which fixed
+ the Home screen's copy of the same dead URL.
+- **Footer repo URL.** The "view this site on GitHub" link still pointed at
+ `codeforphilly-rewrite`; the repo is `codeforphilly-ng` and the old URL only
+ resolves through GitHub's rename redirect.
+
+Explicitly out of scope:
+
+- **Replacing the Home hero's Volunteer CTA with a mailing-list invite** (also
+ recommended by #153) — blocked, see Follow-ups. `Home.tsx` is untouched.
+- Any other visual restyle of the header. The Volunteer button keeps its
+ existing green treatment; only its position changes.
+
+## Implements
+
+- [app-shell.md](../specs/behaviors/app-shell.md) — "Center / right at ≥ md"
+ split into a left content cluster and a right utility cluster, with the new
+ item order; "Auth controls" repositioned second-from-right; GitHub link added
+ to the right cluster and to the mobile sheet; the sheet's accessible name
+ added under Accessibility.
+- [volunteer.md](../specs/screens/volunteer.md) — "Show up to meetups" card
+ links to the Meetup group; "Start your own project" band links to the
+ `CodeForPhilly/partnerships` first-steps guide. Both replace dead GitBook
+ URLs.
+
+## Approach
+
+### 1. Spec changes first (specops — source of truth leads)
+
+`specs/behaviors/app-shell.md` and `specs/screens/volunteer.md` both prescribed
+the current (wrong) state, so they lead. Header spacing is deliberately *not*
+specced — [specs/README.md:49](../specs/README.md) puts spacing outside spec
+scope — so the mobile-sheet padding fix carries no spec change.
+
+### 2. `apps/web/src/components/AppHeader.tsx`
+
+- Left `
-
+
When we meet →
diff --git a/apps/web/tests/AppFooter.test.tsx b/apps/web/tests/AppFooter.test.tsx
index 6b0eb49..8924baf 100644
--- a/apps/web/tests/AppFooter.test.tsx
+++ b/apps/web/tests/AppFooter.test.tsx
@@ -17,7 +17,7 @@ describe('AppFooter', () => {
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
'href',
- 'https://github.com/CodeForPhilly/codeforphilly-rewrite',
+ 'https://github.com/CodeForPhilly/codeforphilly-ng',
);
});
diff --git a/apps/web/tests/Volunteer.test.tsx b/apps/web/tests/Volunteer.test.tsx
new file mode 100644
index 0000000..3a45b0c
--- /dev/null
+++ b/apps/web/tests/Volunteer.test.tsx
@@ -0,0 +1,86 @@
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
+import { screen, waitFor } from '@testing-library/react';
+import { renderScreen, mockPaginated } from './test-utils.js';
+import { Volunteer } from '../src/screens/Volunteer.js';
+import { AuthProvider } from '../src/hooks/useAuth.js';
+
+const MEETUP_URL = 'https://www.meetup.com/Code-for-Philly/';
+const START_PROJECT_URL =
+ 'https://github.com/CodeForPhilly/partnerships/blob/master/creating-new-partnerships/first-steps.md';
+
+describe('Volunteer', () => {
+ beforeEach(() => {
+ vi.spyOn(globalThis, 'fetch').mockImplementation(((input: string) => {
+ if (input.startsWith('/api/auth/me')) {
+ return Promise.resolve(new Response(null, { status: 404 }));
+ }
+ if (input.startsWith('/api/projects')) {
+ return Promise.resolve(
+ new Response(JSON.stringify(mockPaginated([], { totalItems: 268 })), {
+ status: 200,
+ headers: { 'content-type': 'application/json' },
+ }),
+ );
+ }
+ return Promise.resolve(
+ new Response(JSON.stringify(mockPaginated([])), {
+ status: 200,
+ headers: { 'content-type': 'application/json' },
+ }),
+ );
+ }) as typeof fetch);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ function renderVolunteer() {
+ return renderScreen(
+
+
+ ,
+ );
+ }
+
+ it('renders the hero headline', () => {
+ renderVolunteer();
+ expect(
+ screen.getByRole('heading', {
+ name: /volunteer with code for philly/i,
+ level: 1,
+ }),
+ ).toBeInTheDocument();
+ });
+
+ it('points "When we meet →" at the live Meetup group, not the dead GitBook page', () => {
+ renderVolunteer();
+ const link = screen.getByRole('link', { name: /when we meet/i });
+ expect(link).toHaveAttribute('href', MEETUP_URL);
+ expect(link).toHaveAttribute('target', '_blank');
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
+ });
+
+ it('points "Read the guide →" at the partnerships repo, not the dead GitBook page', () => {
+ renderVolunteer();
+ const link = screen.getByRole('link', { name: /read the guide/i });
+ expect(link).toHaveAttribute('href', START_PROJECT_URL);
+ expect(link).toHaveAttribute('target', '_blank');
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
+ });
+
+ it('has no codeforphilly.gitbook.io links anywhere on the screen', async () => {
+ const { container } = renderVolunteer();
+
+ // Wait for the live project count so the fully-settled DOM is asserted on.
+ await waitFor(() => {
+ expect(screen.getByText(/browse 268 active projects/i)).toBeInTheDocument();
+ });
+
+ const hrefs = Array.from(container.querySelectorAll('a')).map(
+ (a) => a.getAttribute('href') ?? '',
+ );
+ expect(hrefs.filter((h) => h.includes('gitbook.io'))).toHaveLength(0);
+ expect(container.innerHTML).not.toContain('codeforphilly.gitbook.io');
+ });
+});
From 3ca3d959c7d67e6e7ebf75c8e57c415ab3d92546 Mon Sep 17 00:00:00 2001
From: Heyoub
Date: Mon, 24 Aug 2026 12:04:30 -0400
Subject: [PATCH 05/18] fix(web): let the mobile sheet nav scroll when it
overflows
The sheet is a full-height flex column with no scroll container, so a nav
list taller than the viewport was simply unreachable below the fold. That
was already latent; adding the GitHub row makes it one row likelier on
short phones. min-h-0 lets the flex child shrink at all, and overflow-y-auto
gives it somewhere to put the excess.
Co-Authored-By: Claude Fable 5
---
apps/web/src/components/AppHeader.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/components/AppHeader.tsx b/apps/web/src/components/AppHeader.tsx
index dd414de..7991907 100644
--- a/apps/web/src/components/AppHeader.tsx
+++ b/apps/web/src/components/AppHeader.tsx
@@ -288,9 +288,11 @@ export function AppHeader() {
Menu
+ {/* min-h-0 + overflow-y-auto so the list stays reachable on
+ short viewports instead of overflowing the panel. */}
Date: Mon, 24 Aug 2026 12:24:58 -0400
Subject: [PATCH 06/18] chore(plans): record site-check-153 validation results
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ticks the criteria verified during implementation and, deliberately,
leaves two unticked rather than rewriting them to match what was
achievable: the browser pass belongs to whoever does UI QA, and the
all-workspaces test gate cannot close on this Windows dev box.
Notes record why. apps/api fails ten tests here on a tree whose API code
is byte-identical to develop, because the fixtures assume POSIX —
store.test.ts injects a write failure via /dev/null/impossible-path,
which Windows will happily create, so the expected rejection never comes.
Reproduces with the files run alone, so it is not runner contention.
Filed as a follow-up rather than fixed: cross-platform fixtures are their
own scope, and silently ticking a gate that did not run is worse than
leaving the box open.
Also flags HomeStub.tsx, which carries the same stale repo URL the footer
had but is imported by nothing.
Co-Authored-By: Claude Fable 5
---
plans/site-check-153.md | 53 +++++++++++++++++++++++++++++++----------
1 file changed, 41 insertions(+), 12 deletions(-)
diff --git a/plans/site-check-153.md b/plans/site-check-153.md
index d288c9a..5a18097 100644
--- a/plans/site-check-153.md
+++ b/plans/site-check-153.md
@@ -106,17 +106,20 @@ One-line repo URL swap to `codeforphilly-ng`.
## Validation
-- [ ] Specs updated before code: app-shell header clusters + volunteer link targets.
-- [ ] Desktop header order is Projects · Help Wanted · Members · About ▾ … GitHub · Search · Sign in · Volunteer, with Volunteer rightmost and still green.
-- [ ] No `ml-1` spacing hacks remain among the header nav's children.
-- [ ] GitHub link is icon-only, labelled "Code for Philly on GitHub", and opens `https://github.com/CodeForPhilly` in a new tab with `rel="noopener noreferrer"`.
-- [ ] Mobile sheet has a "Menu" title, horizontal padding on nav + search, and no `pt-8`; the title does not collide with the close button.
-- [ ] The sheet dialog exposes an accessible name; Radix still supplies `aria-expanded` on the trigger.
-- [ ] Loading skeleton uses `aria-hidden`; About trigger's accessible name is its visible text; account-menu label retained.
-- [ ] Every mobile sheet item closes the sheet on click, including Contact.
-- [ ] No `codeforphilly.gitbook.io` URL remains in `apps/web/src`.
-- [ ] Footer "view this site on GitHub" points at `codeforphilly-ng`.
-- [ ] `npm run -w packages/shared build && npm run type-check && npm run lint && npm test` clean.
+- [x] Specs updated before code: app-shell header clusters + volunteer link targets.
+- [x] Desktop header order is Projects · Help Wanted · Members · About ▾ … GitHub · Search · Sign in · Volunteer, with Volunteer rightmost and still green.
+- [x] No `ml-1` spacing hacks remain among the header nav's children.
+- [x] GitHub link is icon-only, labelled "Code for Philly on GitHub", and opens `https://github.com/CodeForPhilly` in a new tab with `rel="noopener noreferrer"`.
+- [x] Mobile sheet has a "Menu" title, horizontal padding on nav + search, and no `pt-8`; the title does not collide with the close button.
+- [x] The sheet dialog exposes an accessible name; Radix still supplies `aria-expanded` on the trigger.
+- [x] Loading skeleton uses `aria-hidden`; About trigger's accessible name is its visible text; account-menu label retained.
+- [x] Every mobile sheet item closes the sheet on click, including Contact.
+- [x] No `codeforphilly.gitbook.io` URL remains in `apps/web/src`.
+- [x] Footer "view this site on GitHub" points at `codeforphilly-ng`.
+- [x] Both replacement URLs return 200 and carry the expected content.
+- [x] `npm run -w packages/shared build`, `npm run type-check`, and `npm run lint` clean.
+- [x] `npm test` clean for the workspaces this plan touches: web 96/96, shared 75/75.
+- [ ] `npm test` clean for **all** workspaces — `apps/api` cannot pass on the Windows dev box used here (see Notes); needs a Linux run or CI to close.
- [ ] Browser test: desktop header order + mobile sheet padding at < md, both breakpoints.
## Risks
@@ -131,7 +134,20 @@ One-line repo URL swap to `codeforphilly-ng`.
## Notes
-(To be populated at closeout.)
+(To be populated at closeout. Recorded during implementation:)
+
+- **`apps/api` tests do not pass on Windows, independent of this plan.** Ten
+ failures across `scrub-data.test.ts` (4), `internal-reload.test.ts` (4), and
+ `store.test.ts` (2), on a tree where `git diff develop..HEAD -- apps/api
+ packages/` is empty — this branch touches no API code. The mechanism is
+ POSIX-isms in the test fixtures: `store.test.ts` injects a write failure by
+ pointing the private store at `/dev/null/impossible-path` and asserting the
+ transaction rejects, but on Windows that is an ordinary creatable directory,
+ so the write succeeds and the expected throw never happens. They reproduce
+ with the files run alone, so it is not test-runner contention. CI runs the
+ same gate on Linux, where the fixture behaves as intended. Worth a
+ cross-platform fixture cleanup if Windows dev boxes are to be supported;
+ filed under Follow-ups.
## Follow-ups
@@ -149,3 +165,16 @@ One-line repo URL swap to `codeforphilly-ng`.
worker, unsubscribe tokens). Building an anonymous-capture CTA ahead of that
spec would invent unspecified behavior. `Home.tsx` is deliberately untouched
here; the CTA swap should follow the newsletter spec work, not precede it.
+
+- **Tracked as: dead file, not fixed here — `apps/web/src/pages/HomeStub.tsx`.**
+ It carries the same stale `codeforphilly-rewrite` URL the footer had, but
+ nothing imports or routes it (`App.tsx` imports only `LoginPlaceholder` from
+ `src/pages/`; every live screen lives in `src/screens/`). Left alone because
+ the right fix is deleting the file, not patching a URL nobody renders — and
+ that deletion wants its own scope. Flagging so a future grep for the old repo
+ name doesn't read as an unfixed live link.
+
+- **Issue — make the `apps/api` test fixtures cross-platform.** The `/dev/null`
+ failure-injection idiom (and whatever the other seven failures share) makes
+ the API suite unrunnable on a Windows dev box, so the documented validation
+ gate can only be completed on Linux or in CI. See Notes for the mechanism.
From 5729702ae1017d50c570b31cb59cb20e2fedac7a Mon Sep 17 00:00:00 2001
From: Heyoub
Date: Mon, 24 Aug 2026 12:48:12 -0400
Subject: [PATCH 07/18] chore(plans): record exact api test counts in
site-check-153 notes
Pins the numbers behind the unticked all-workspaces gate so a future
reader can tell a known Windows baseline from a real regression, and
notes the develop re-run that confirms it.
Co-Authored-By: Claude Fable 5
---
plans/site-check-153.md | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/plans/site-check-153.md b/plans/site-check-153.md
index 5a18097..1f0fe6d 100644
--- a/plans/site-check-153.md
+++ b/plans/site-check-153.md
@@ -136,10 +136,13 @@ One-line repo URL swap to `codeforphilly-ng`.
(To be populated at closeout. Recorded during implementation:)
-- **`apps/api` tests do not pass on Windows, independent of this plan.** Ten
- failures across `scrub-data.test.ts` (4), `internal-reload.test.ts` (4), and
- `store.test.ts` (2), on a tree where `git diff develop..HEAD -- apps/api
- packages/` is empty — this branch touches no API code. The mechanism is
+- **`apps/api` tests do not pass on Windows, independent of this plan.**
+ `apps/api` finishes 3 failed | 30 passed (33 files), 10 failed | 413 passed
+ (423 tests) — the ten spread across `scrub-data.test.ts` (4),
+ `internal-reload.test.ts` (4), and `store.test.ts` (2), on a tree where
+ `git diff develop..HEAD -- apps/api packages/` is empty — this branch touches
+ no API code. Checking out `develop` and re-running `store.test.ts` there
+ reproduces its 2 failed | 11 passed exactly. The mechanism is
POSIX-isms in the test fixtures: `store.test.ts` injects a write failure by
pointing the private store at `/dev/null/impossible-path` and asserting the
transaction rejects, but on Windows that is an ordinary creatable directory,
From bb05e0f1c7ed4dd907c8add68295ddf6a27272a3 Mon Sep 17 00:00:00 2001
From: Heyoub
Date: Mon, 24 Aug 2026 12:52:56 -0400
Subject: [PATCH 08/18] chore(plans): record browser validation for
site-check-153
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Desktop header verified in headed Chrome at 1400px; the sheet verified via
its portal at desktop width because the automation harness could not shrink
a maximized window below md — same limitation web-shell.md's plan recorded.
Co-Authored-By: Claude Fable 5
---
plans/site-check-153.md | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/plans/site-check-153.md b/plans/site-check-153.md
index 1f0fe6d..9178493 100644
--- a/plans/site-check-153.md
+++ b/plans/site-check-153.md
@@ -120,7 +120,15 @@ One-line repo URL swap to `codeforphilly-ng`.
- [x] `npm run -w packages/shared build`, `npm run type-check`, and `npm run lint` clean.
- [x] `npm test` clean for the workspaces this plan touches: web 96/96, shared 75/75.
- [ ] `npm test` clean for **all** workspaces — `apps/api` cannot pass on the Windows dev box used here (see Notes); needs a Linux run or CI to close.
-- [ ] Browser test: desktop header order + mobile sheet padding at < md, both breakpoints.
+- [x] Browser test (headed Chrome, Vite dev server): desktop header order,
+ spacing, GitHub icon, and Volunteer-rightmost verified at 1400px; the
+ sheet verified open — "Menu" title, padded nav/search, no title/close
+ collision — and Escape closes it. Caveat: the harness could not shrink
+ the (maximized) window below md, so the sheet was opened via its
+ CSS-hidden trigger at desktop width. The sheet is a fixed `w-72`
+ portal, so its rendering is identical at < md; the < md *header bar*
+ (logo + auth + hamburger row) still rides on the jsdom tests, same
+ limitation `web-shell.md:109` recorded.
## Risks
From e092de3752184545aa25a5eb998e5167d6563630 Mon Sep 17 00:00:00 2001
From: Heyoub
Date: Mon, 24 Aug 2026 12:54:45 -0400
Subject: [PATCH 09/18] chore(plans): mark site-check-153 done (PR #154)
Co-Authored-By: Claude Fable 5
---
plans/site-check-153.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/plans/site-check-153.md b/plans/site-check-153.md
index 9178493..021ed1a 100644
--- a/plans/site-check-153.md
+++ b/plans/site-check-153.md
@@ -1,10 +1,11 @@
---
-status: in-progress
+status: done
depends: []
specs:
- specs/behaviors/app-shell.md
- specs/screens/volunteer.md
issues: [153]
+pr: 154
---
# Plan: site check — header order, mobile sheet padding, dead outbound links
From eb1cbc3ffd6ba2f6e767895986eef32ace91d251 Mon Sep 17 00:00:00 2001
From: Chris Alfano
Date: Tue, 8 Sep 2026 20:59:56 -0400
Subject: [PATCH 10/18] docs: repoint stale codeforphilly-rewrite GitHub URLs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The repo was renamed to codeforphilly-ng; the old
github.com/CodeForPhilly/codeforphilly-rewrite URLs in the setup
snippets and the cutover announcement are a hard 404. Only the GitHub
URLs change here — the k8s namespace and package name still carry the
old slug on purpose.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
---
docs/operations/cutover-announcement.md | 2 +-
specs/architecture.md | 4 ++--
specs/behaviors/storage.md | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/operations/cutover-announcement.md b/docs/operations/cutover-announcement.md
index 6fd73cc..1adf51b 100644
--- a/docs/operations/cutover-announcement.md
+++ b/docs/operations/cutover-announcement.md
@@ -116,7 +116,7 @@ What you can do now:
- Use the Slack workspace as normal; SSO is now backed by our own IdP
Bugs / weirdness: file an issue on
-https://github.com/CodeForPhilly/codeforphilly-rewrite/issues or just reply
+https://github.com/CodeForPhilly/codeforphilly-ng/issues or just reply
here.
```
diff --git a/specs/architecture.md b/specs/architecture.md
index 1dbff64..d80b0bc 100644
--- a/specs/architecture.md
+++ b/specs/architecture.md
@@ -135,9 +135,9 @@ The web and api share Zod schemas for every request/response shape and every rec
The "no moving pieces" promise: a contributor needs git, Node, and two clones. No Docker compose, no database to install, no migrations to run.
```bash
-git clone https://github.com/CodeForPhilly/codeforphilly-rewrite.git
+git clone https://github.com/CodeForPhilly/codeforphilly-ng.git
git clone https://github.com/CodeForPhilly/codeforphilly-data-snapshot.git ../codeforphilly-data
-cd codeforphilly-rewrite
+cd codeforphilly-ng
npm install
npm run dev # api + web concurrently with watch
```
diff --git a/specs/behaviors/storage.md b/specs/behaviors/storage.md
index 334668e..b5d8a3f 100644
--- a/specs/behaviors/storage.md
+++ b/specs/behaviors/storage.md
@@ -84,7 +84,7 @@ A scrubbed snapshot of the data repo is published as a public tag (e.g., `snapsh
An `apps/api/scripts/scrub-data.ts` in the code repo produces the snapshot. The contributor bootstrap is:
```bash
-git clone https://github.com/CodeForPhilly/codeforphilly-rewrite.git
+git clone https://github.com/CodeForPhilly/codeforphilly-ng.git
git clone --bare https://github.com/CodeForPhilly/codeforphilly-data-snapshot.git ../codeforphilly-data
npm install
npm run dev # api + web boot, data already there
From 12dea00f513ff723951e86025291a72f138c29f4 Mon Sep 17 00:00:00 2001
From: Chris Alfano
Date: Tue, 8 Sep 2026 21:00:03 -0400
Subject: [PATCH 11/18] chore(web): remove dead HomeStub page
Nothing has imported or routed it since the scaffold (every live screen
lives in src/screens/), and it carried the last stale
codeforphilly-rewrite link in the web bundle.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
---
apps/web/src/pages/HomeStub.tsx | 29 -----------------------------
1 file changed, 29 deletions(-)
delete mode 100644 apps/web/src/pages/HomeStub.tsx
diff --git a/apps/web/src/pages/HomeStub.tsx b/apps/web/src/pages/HomeStub.tsx
deleted file mode 100644
index 63afc92..0000000
--- a/apps/web/src/pages/HomeStub.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-export function HomeStub() {
- return (
-
-
- Code for Philly is being rebuilt
-
-
- We are modernizing the platform. Check back soon for the full
- experience. In the meantime, you can browse our projects below.
-
- );
-}
From 9201cbe51c501508836e835d6c72ece0fdf1b797 Mon Sep 17 00:00:00 2001
From: Chris Alfano
Date: Tue, 8 Sep 2026 21:00:12 -0400
Subject: [PATCH 12/18] test(web): settle AuthProvider before Volunteer tests
return
Three synchronous tests returned while AuthProvider's /api/auth/me fetch
was still in flight, so its state update landed outside act() and
warned on every run. Each now awaits the settled DOM the way the
fourth test already does.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
---
apps/web/tests/Volunteer.test.tsx | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/apps/web/tests/Volunteer.test.tsx b/apps/web/tests/Volunteer.test.tsx
index 3a45b0c..af17c99 100644
--- a/apps/web/tests/Volunteer.test.tsx
+++ b/apps/web/tests/Volunteer.test.tsx
@@ -43,7 +43,7 @@ describe('Volunteer', () => {
);
}
- it('renders the hero headline', () => {
+ it('renders the hero headline', async () => {
renderVolunteer();
expect(
screen.getByRole('heading', {
@@ -51,22 +51,26 @@ describe('Volunteer', () => {
level: 1,
}),
).toBeInTheDocument();
+ // Let AuthProvider's /api/auth/me fetch settle before the test returns.
+ await screen.findByText(/browse 268 active projects/i);
});
- it('points "When we meet →" at the live Meetup group, not the dead GitBook page', () => {
+ it('points "When we meet →" at the live Meetup group, not the dead GitBook page', async () => {
renderVolunteer();
const link = screen.getByRole('link', { name: /when we meet/i });
expect(link).toHaveAttribute('href', MEETUP_URL);
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
+ await screen.findByText(/browse 268 active projects/i);
});
- it('points "Read the guide →" at the partnerships repo, not the dead GitBook page', () => {
+ it('points "Read the guide →" at the partnerships repo, not the dead GitBook page', async () => {
renderVolunteer();
const link = screen.getByRole('link', { name: /read the guide/i });
expect(link).toHaveAttribute('href', START_PROJECT_URL);
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
+ await screen.findByText(/browse 268 active projects/i);
});
it('has no codeforphilly.gitbook.io links anywhere on the screen', async () => {
From bebd03421099d0de06db1a8605898d194bdc3238 Mon Sep 17 00:00:00 2001
From: Chris Alfano
Date: Tue, 8 Sep 2026 21:03:27 -0400
Subject: [PATCH 13/18] refactor(web): lift GitHubIcon into a shared component
AppHeader.tsx and LoginPlaceholder.tsx carried byte-identical copies of
the same SVG. One component under components/icons/ keeps the path
data in a single place for the next screen that needs it.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
---
apps/web/src/components/AppHeader.tsx | 16 +---------------
apps/web/src/components/icons/GitHubIcon.tsx | 15 +++++++++++++++
apps/web/src/pages/LoginPlaceholder.tsx | 16 +---------------
3 files changed, 17 insertions(+), 30 deletions(-)
create mode 100644 apps/web/src/components/icons/GitHubIcon.tsx
diff --git a/apps/web/src/components/AppHeader.tsx b/apps/web/src/components/AppHeader.tsx
index 7991907..d7ae03d 100644
--- a/apps/web/src/components/AppHeader.tsx
+++ b/apps/web/src/components/AppHeader.tsx
@@ -17,6 +17,7 @@ import {
} from '@/components/ui/sheet';
import { Separator } from '@/components/ui/separator';
import { SearchBox } from '@/components/SearchBox';
+import { GitHubIcon } from '@/components/icons/GitHubIcon';
import { useAuth } from '@/hooks/useAuth';
const GITHUB_URL = 'https://github.com/CodeForPhilly';
@@ -61,21 +62,6 @@ function MenuIcon() {
);
}
-function GitHubIcon() {
- return (
-
- );
-}
-
function AuthControls({ mobile = false }: { mobile?: boolean }) {
const { person, loading, signOut } = useAuth();
diff --git a/apps/web/src/components/icons/GitHubIcon.tsx b/apps/web/src/components/icons/GitHubIcon.tsx
new file mode 100644
index 0000000..6862c4e
--- /dev/null
+++ b/apps/web/src/components/icons/GitHubIcon.tsx
@@ -0,0 +1,15 @@
+/** GitHub mark, sized to sit inline with text and icon buttons. */
+export function GitHubIcon() {
+ return (
+
+ );
+}
diff --git a/apps/web/src/pages/LoginPlaceholder.tsx b/apps/web/src/pages/LoginPlaceholder.tsx
index 5b82177..581185a 100644
--- a/apps/web/src/pages/LoginPlaceholder.tsx
+++ b/apps/web/src/pages/LoginPlaceholder.tsx
@@ -10,6 +10,7 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
+import { GitHubIcon } from '@/components/icons/GitHubIcon';
import { useAuth } from '@/hooks/useAuth';
import { api, ApiError } from '@/lib/api';
@@ -46,21 +47,6 @@ const ERROR_MESSAGES: Record = {
),
};
-function GitHubIcon() {
- return (
-
- );
-}
-
function WhyGitHub() {
const [open, setOpen] = useState(false);
From ffdc5e9f5e9731fdb3f9820d98c407a1e3f7bb82 Mon Sep 17 00:00:00 2001
From: Chris Alfano
Date: Tue, 8 Sep 2026 21:03:39 -0400
Subject: [PATCH 14/18] fix(web): close the mobile sheet on any navigation
The eleven per-item onClick closers missed the inline SearchBox's Enter
path, so "every item in the sheet closes the sheet when activated"
(specs/behaviors/app-shell.md) did not hold there. Derive the open
state from the location.key the sheet was opened at instead: any
client-side navigation closes it, including a same-path re-click.
Derived during render rather than in a useLocation effect because
react-hooks/set-state-in-effect flags the effect form; this is the
same state-sync pattern ProjectEdit.tsx uses. The two anchors that do
not change location (Contact mailto:, GitHub new tab) keep an explicit
closer. Tests cover a sheet link click and the search Enter path.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
---
apps/web/src/components/AppHeader.tsx | 66 ++++++++-------------------
apps/web/tests/AppHeader.test.tsx | 33 ++++++++++++++
2 files changed, 52 insertions(+), 47 deletions(-)
diff --git a/apps/web/src/components/AppHeader.tsx b/apps/web/src/components/AppHeader.tsx
index d7ae03d..7f2b6e4 100644
--- a/apps/web/src/components/AppHeader.tsx
+++ b/apps/web/src/components/AppHeader.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react';
-import { Link, NavLink } from 'react-router';
+import { Link, NavLink, useLocation } from 'react-router';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -202,7 +202,15 @@ function GitHubLink() {
}
export function AppHeader() {
- const [mobileOpen, setMobileOpen] = useState(false);
+ const location = useLocation();
+ // The sheet is open only for the location it was opened at, so any
+ // client-side navigation — a NavLink or Enter in the inline search —
+ // closes it without per-item onClick closers. Derived during render
+ // rather than synced in an effect (react-hooks/set-state-in-effect).
+ const [openedAtKey, setOpenedAtKey] = useState(null);
+ const mobileOpen = openedAtKey === location.key;
+ const setMobileOpen = (open: boolean) =>
+ setOpenedAtKey(open ? location.key : null);
return (
@@ -280,64 +288,32 @@ export function AppHeader() {
aria-label="Mobile navigation"
className="flex flex-col gap-2 px-4 min-h-0 overflow-y-auto"
>
- setMobileOpen(false)}
- >
+
Projects
- setMobileOpen(false)}
- >
+
Help Wanted
- setMobileOpen(false)}
- >
+
Members
About
- setMobileOpen(false)}
- >
+
Mission
- setMobileOpen(false)}
- >
+
Leadership
- setMobileOpen(false)}
- >
+
Code of Conduct
- setMobileOpen(false)}
- >
+
Hackathons
- setMobileOpen(false)}
- >
+
Sponsor
GitHub
- setMobileOpen(false)}
- >
+
Volunteer
diff --git a/apps/web/tests/AppHeader.test.tsx b/apps/web/tests/AppHeader.test.tsx
index 116d260..9f3c3c3 100644
--- a/apps/web/tests/AppHeader.test.tsx
+++ b/apps/web/tests/AppHeader.test.tsx
@@ -137,4 +137,37 @@ describe('AppHeader', () => {
'/volunteer',
);
});
+
+ it('closes the mobile sheet when a sheet link navigates', async () => {
+ const user = userEvent.setup();
+ renderWithRouter();
+
+ await user.click(screen.getByRole('button', { name: /open navigation menu/i }));
+ const nav = await screen.findByRole('navigation', { name: /mobile navigation/i });
+
+ await user.click(within(nav).getByRole('link', { name: 'Members' }));
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog', { name: 'Menu' })).not.toBeInTheDocument();
+ });
+ });
+
+ it('closes the mobile sheet when the inline search navigates', async () => {
+ const user = userEvent.setup();
+ renderWithRouter();
+
+ await user.click(screen.getByRole('button', { name: /open navigation menu/i }));
+ const dialog = await screen.findByRole('dialog', { name: 'Menu' });
+
+ // Scope to the sheet: jsdom applies no breakpoints, so the desktop
+ // search box is in the DOM too.
+ await user.type(
+ within(dialog).getByRole('searchbox', { name: /search the site/i }),
+ 'civic{Enter}',
+ );
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog', { name: 'Menu' })).not.toBeInTheDocument();
+ });
+ });
});
From 6cdccd1a1cac2d49d2cebe36c124ce95d4442354 Mon Sep 17 00:00:00 2001
From: Chris Alfano
Date: Tue, 8 Sep 2026 21:03:48 -0400
Subject: [PATCH 15/18] fix(web): describe the sheet dialog, drop its dead
width class
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Radix warned on every open that the dialog's aria-describedby pointed
at nothing; a visually-hidden SheetDescription satisfies it. The
className="w-72" on SheetContent never applied — the primitive's
data-[side=right]:w-3/4 and sm:max-w-sm win — so it goes.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
---
apps/web/src/components/AppHeader.tsx | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/components/AppHeader.tsx b/apps/web/src/components/AppHeader.tsx
index 7f2b6e4..ca3611d 100644
--- a/apps/web/src/components/AppHeader.tsx
+++ b/apps/web/src/components/AppHeader.tsx
@@ -11,6 +11,7 @@ import {
import {
Sheet,
SheetContent,
+ SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
@@ -276,11 +277,15 @@ export function AppHeader() {
-
+
{/* SheetHeader/SheetTitle carry the panel's own padding and give
- the underlying Radix dialog its accessible name. */}
+ the underlying Radix dialog its accessible name; the
+ visually-hidden description satisfies aria-describedby. */}
Menu
+
+ Site navigation
+
{/* min-h-0 + overflow-y-auto so the list stays reachable on
short viewports instead of overflowing the panel. */}
From b417c87a0a5e83b77925637777f39070d170fadc Mon Sep 17 00:00:00 2001
From: Chris Alfano
Date: Tue, 8 Sep 2026 21:03:57 -0400
Subject: [PATCH 16/18] fix(web): wrap the utility cluster in a navigation
landmark
Volunteer and the GitHub link left "Primary navigation" in this PR and
landed in a plain div, so they fell out of every navigation landmark.
A second nav labelled "Utility" keeps them reachable by landmark
without moving them back; app-shell.md's Accessibility section now
names both landmarks.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
---
apps/web/src/components/AppHeader.tsx | 7 +++++--
specs/behaviors/app-shell.md | 1 +
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/components/AppHeader.tsx b/apps/web/src/components/AppHeader.tsx
index ca3611d..a781e09 100644
--- a/apps/web/src/components/AppHeader.tsx
+++ b/apps/web/src/components/AppHeader.tsx
@@ -250,7 +250,10 @@ export function AppHeader() {
{/* Desktop utility cluster: GitHub, search, auth, then the Volunteer
CTA pinned rightmost (specs/behaviors/app-shell.md). */}
-
diff --git a/specs/behaviors/app-shell.md b/specs/behaviors/app-shell.md
index 2262f09..95c0b52 100644
--- a/specs/behaviors/app-shell.md
+++ b/specs/behaviors/app-shell.md
@@ -190,6 +190,7 @@ It does not block initial paint waiting on `me`. Auth controls render skeletons
- All dropdowns are keyboard-navigable
- The mobile sheet traps focus while open and returns it to the trigger on close
- The mobile sheet is a dialog with the accessible name "Menu"
+- At ≥ md the header exposes two navigation landmarks: "Primary navigation" (the content cluster) and "Utility" (GitHub, search, auth, Volunteer)
- Every icon-only control carries an accessible name; controls with visible text use that text as their accessible name rather than duplicating it in a label
## Print
From d5a4221932f2158baba7a334df9829dff0a8ac3e Mon Sep 17 00:00:00 2001
From: Chris Alfano
Date: Tue, 8 Sep 2026 21:04:09 -0400
Subject: [PATCH 17/18] fix(web): keep the header on one line between md and lg
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Tailwind's container is 768px wide across the whole 768–1023 range
(736px content box). Measured at 768: logo 128 + nav 297 + gaps 48 +
utility cluster 387 = 860, so the document scrolled to 845px and
"Help Wanted" wrapped onto two lines.
Hide the GitHub icon link below lg (the mobile sheet keeps its own
GitHub row) and narrow the desktop search to w-24 there, restoring
w-48 / focus-within:w-72 at lg. The results dropdown is anchored
right-0 with min-w-72 instead of stretching left-0 right-0, so it
stays 288px wide over the narrow input — the width it already had at
lg. After: 728px used at 768, no horizontal scroll; 1024 fits with
the icon and wide search back.
Co-Authored-By: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
---
apps/web/src/components/AppHeader.tsx | 5 ++++-
apps/web/src/components/SearchBox.tsx | 4 ++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/apps/web/src/components/AppHeader.tsx b/apps/web/src/components/AppHeader.tsx
index a781e09..bdde238 100644
--- a/apps/web/src/components/AppHeader.tsx
+++ b/apps/web/src/components/AppHeader.tsx
@@ -189,7 +189,10 @@ const navLinkClass = ({ isActive }: { isActive: boolean }) =>
function GitHubLink() {
return (
-
+ // Desktop-only: between md and lg the header has no room for it (the
+ // utility cluster would push "Help Wanted" onto two lines); the mobile
+ // sheet carries its own GitHub row.
+
{loading && results.length === 0 && (