Skip to content

Latest commit

 

History

History
218 lines (161 loc) · 10.5 KB

app-shell.md

File metadata and controls

218 lines (161 loc) · 10.5 KB

Behavior: App Shell

Rule

Every screen on the site is rendered inside the same chrome — a sticky header, an optional breadcrumb row, the page content area, and a footer. The shell carries navigation, search, and auth state. Individual screen specs declare their content; this spec declares the frame.

Applies To

Every URL except /login (which intentionally renders a minimal shell). The GitHub OAuth callback handler and the account-claim screens (not yet specified) will also use the minimal shell.

Composition

┌─────────────────────────────────────────────────────┐
│                    Header                           │  sticky
├─────────────────────────────────────────────────────┤
│                    Breadcrumbs (optional)           │  conditional
├─────────────────────────────────────────────────────┤
│                                                     │
│                    Page content                     │  per-screen spec
│                                                     │
├─────────────────────────────────────────────────────┤
│                    Footer                           │
└─────────────────────────────────────────────────────┘

Header

Sticky at the top of the viewport. Background opaque, slight shadow on scroll.

Left

  • Logo (/img/logo.png) — 32px tall, links to /
  • Site name "Code for Philly" — visible at ≥ md, hidden below to save space

Center / right at ≥ lg

Two clusters. The content cluster sits next to the logo; the utility cluster is pinned to the right edge and carries the outbound links, search, auth, and the call to action.

Content cluster, items in this order:

Item Target Style
Projects /projects text link
Events /events text link
Help Wanted /help-wanted text link
Members /members text link
About ▾ dropdown text link with caret

Utility cluster, items in this order (left to right):

Item Target Style
Slack /chat icon-only link, accessible name "Join Code for Philly on Slack", opens in a new tab; a full-page navigation to the server redirect (chat.md)
GitHub https://github.com/CodeForPhilly icon-only external link, accessible name "Code for Philly on GitHub", opens in a new tab
Search 🔍 inline expand icon button
Sign in / account menu see Auth controls button / avatar dropdown
Volunteer /volunteer button (success, filled) — emphasized because it's the call to action

Volunteer is the rightmost element in the header. It sits after the auth control rather than among the content links so the call to action reads as the header's terminal step, not as one more section.

Slack is always one step from the header. It is where the community actually works, and a newcomer looking for it should not have to scroll to the footer. The footer's Connect column keeps its Slack link too.

The desktop header starts at lg (1024px). Below that the content cluster and the utility cluster do not fit on one line (the content links wrap and Volunteer runs off the edge), so tablets in portrait get the mobile header and its sheet.

About dropdown

  • Mission → /pages/mission
  • Leadership → /pages/leadership
  • Code of Conduct → /pages/code-of-conduct
  • Hackathons → /pages/hackathons
  • Sponsor → /sponsor
  • Contact → /contact (not yet specified; rendered as mailto: for v1)

The /pages/* URLs serve static content pages authored as MDX/Markdown in the code repo (apps/web/src/content/pages/). They have no per-page screen spec — the content is the spec. Source copy ports from codeforphilly.org/site-root/pages/ in the legacy repo.

Auth controls

Second from the right in the utility cluster — between Search and the Volunteer button. On mobile the auth control sits in the header bar itself, outside the sheet.

  • Anonymous: "Sign in" (primary button) → /login. There is no separate "Sign up" button — sign-in and sign-up are the same flow once GitHub OAuth is specified (first sign-in creates the account).
  • User: Avatar + name dropdown:
    • "My profile" → /members/<slug>
    • "Account settings" → /account
    • separator
    • "My projects" → /projects?memberSlug=<slug>
    • separator
    • "Sign out" → calls POST /api/auth/logout, redirects to /
  • Staff: Same dropdown plus a "Staff tools" section:
    • "Manage tags" → /tags?staff=true
    • "Recent staff actions" → deferred placeholder
  • Administrator: Staff items plus "Admin" section:
    • "Manage members" → /members?staff=true&accountLevel=all

Mobile (< lg)

Header collapses to: logo + hamburger menu + auth control. Hamburger opens a sheet (right-side overlay) with all nav items stacked vertically — the content-cluster links, the About items under an "About" label, the Slack and GitHub links, and Volunteer last, mirroring the desktop order. Search is inside the sheet, not inline. The auth control stays in the header bar, outside the sheet.

Every item in the sheet closes the sheet when activated.

Search

Single search input in the header. Behavior:

  • Placeholder: "Search projects, members, tags…"
  • On focus, expands to fit the available space
  • As the user types (debounced 200ms), a dropdown shows up to 12 results across types (4 per category, see groups below) — matches laddr's site-wide search but in a typeahead form instead of a full results page
  • Result groups:
    • "Projects" — GET /api/projects?q=...&perPage=4
    • "Members" — GET /api/people?q=...&perPage=4
    • "Tags" — GET /api/tags?q=...&perPage=4
  • "See all results for <q>" link at the bottom → /search?q=<q>
  • The dropdown is an ARIA combobox: ArrowDown / ArrowUp move a highlight through the results (including the "See all results" link, wrapping at either end); focus stays in the input
  • Enter with a highlighted result navigates to that result; with no highlight it submits to /search?q=<q>
  • Escape closes the dropdown and clears the query

The /search results page is deferred; for v1 we ship only the typeahead and the /search page renders a redirect-to-projects with the q prefilled. Tracked in deferred.md as a follow-up.

Sort order within each group: relevance (SQLite FTS5 BM25 rank — see behaviors/storage.md).

Breadcrumbs

Optional row below the header. A screen opts in by declaring a breadcrumbs trail; screens without one render the page content immediately under the header.

Screen Breadcrumb trail
/projects (none — top-level)
/projects/:slug Projects › <title>
/projects/:slug/edit Projects › <title> › Edit
/projects/create Projects › New project
/members (none)
/members/:slug Members › <fullName>
/tags/:namespace/:slug Tags › <namespace> › <title>
/help-wanted (none)
/events (none)
/events/past Events › Past
/events/:slug Events › <title>
/events/:slug/edit Events › <title> › Edit
/events/new Events › New event
/events/series/:slug Events › <series title>
/account Settings

Each segment except the last is a link to the parent route.

Footer

Three columns at ≥ md, stacked below.

Column 1: Explore

  • Active Projects → /projects
  • Start a Project → /projects/create (or external link for anonymous)
  • Events → /events
  • Hackathons → /pages/hackathons
  • Members → /members
  • Help Wanted → /help-wanted
  • Blog → /blog

Column 2: About

  • Mission → /pages/mission
  • Leadership → /pages/leadership
  • Code of Conduct → /pages/code-of-conduct
  • Sponsor → /sponsor
  • Contact → /contact

Column 3: Connect

  • Slack → /chat
  • Newsletter signup (defer)
  • Social icons: Instagram, LinkedIn, Facebook, Meetup, Mastodon, Bluesky

Bottom strip

  • "Copyright © Code for Philly 2011 – {currentYear}" (year computed at render time)
  • "Open source — view this site on GitHub" link to the rewrite repo
  • "Powered by laddr" credit is gone (see deferred.md)

Page-level state in shell

The shell reads:

  • GET /api/auth/me on first load — once, then cached in a React context
  • The current route — for active-link styling and breadcrumb construction

It does not block initial paint waiting on me. Auth controls render skeletons until the response arrives.

Loading + errors

  • Top-of-page progress bar (1–2px) animates during full-page navigations
  • Top-of-page red banner when an API call returns 5xx ("Something went wrong. We're looking at it. [Retry]"). Retry re-fetches every active query and dismisses the banner. When the failed call has nothing to re-issue (the header typeahead, which re-runs on the next keystroke), the button reads Dismiss instead — the visible label always says what the button does
  • A red banner when offline ("You're offline. Some features may not work.") — driven by navigator.onLine

Accessibility

  • Logo is a link with visible text inside it ("Code for Philly")
  • Skip link at the very top: "Skip to main content" → focuses the <main> element
  • 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 ≥ lg the header exposes two navigation landmarks: "Primary navigation" (the content cluster) and "Utility" (Slack, 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

A print stylesheet hides the header, footer, breadcrumbs, search dropdown, and any auth controls. Pages print as their content area.