Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ACCESSIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ OffOn is a platform for open source enthusiasts. We want everyone to be able to
- One `<h1>` per page with no skipped heading levels.
- Meaningful `alt` text on informational images, empty `alt=""` paired with `aria-hidden="true"` on decorative ones.
- Screen reader announcement of links that open in a new tab.
- Color contrast verified at 4.5:1 for body text and 3:1 for large text and UI controls in both modes.
- Color contrast verified at 7:1 for body text and 4.5:1 for large text (both WCAG AAA), and 3:1 for UI controls, in both modes.
- Tested with [axe-core](https://github.com/dequelabs/axe-core) on every pull request preview, in both light and dark mode.
- Self-hosted fonts so users on restricted networks are not locked out.
- Google Analytics is opt-in only via the consent banner. No tracking runs until the user accepts.
Expand Down Expand Up @@ -100,15 +100,17 @@ Apply this to every component you write or modify.

### Color contrast

- Normal text (under 18px / non-bold under 14px): minimum 4.5:1.
- Large text (18px+ or bold 14px+): minimum 3:1.
- Normal text (under 18px / non-bold under 14px): minimum 7:1 (WCAG AAA).
- Large text (18px+ or bold 14px+): minimum 4.5:1 (WCAG AAA).
- UI components and focus indicators: minimum 3:1 against adjacent colors.
- Focus indicators (WCAG 2.4.11): the focus indicator area must be at least as large as a 2px perimeter outline of the component, and the focused/unfocused contrast ratio must be at least 3:1.
- Never use `hsl(41 100% 60%)` (`#ffc034` yellow) as text in light mode. Fails contrast.
- Never place text on `bg-primary` without verifying light mode contrast.
- Never use `opacity-*` on an element that contains visible text. Use an explicit CSS color token instead (e.g. `text-[hsl(var(--text-faint))]`).
- Always verify contrast in both light and dark mode.
- Never rely on color alone to convey meaning. Always pair with text, icon, or pattern.
- **Hover state contrast in light mode:** `hover:text-primary` resolves to amber (`#ffc034`) on a light surface, which fails contrast. Never use `hover:text-primary` on its own. Use `hover:text-foreground dark:hover:text-primary` so light mode gets a dark, accessible color and dark mode gets the amber accent. Apply the same logic to any interactive element whose hover color differs by mode.
- **Icon/indicator colors in light mode:** CSS variables like `--difficulty-builder` are set to a pale tint in light mode (`hsl(85 48% 75%)`) and will be near-invisible on light surfaces. Do not use these variables for icon foreground colors. Use a hardcoded accessible value (e.g. `#15803d` for green) that passes contrast in both modes.

### Focus rings

Expand Down
9 changes: 5 additions & 4 deletions ADVENTURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,11 @@ When a new level is ready in the challenges repo after the first adventure PR ha
These scripts run automatically on the hourly schedule but can also be run locally.

```sh
# Requires DISCOURSE_API_KEY and DISCOURSE_API_USERNAME in .env
node scripts/refresh-discussions.mjs # Fetch discussion posts for each level
node scripts/refresh-leaderboard.mjs # Fetch leaderboard data per adventure/level
node scripts/refresh-community-leaders.mjs # Fetch community leader data
node scripts/refresh-discussions.mjs # Fetch discussion posts for each level (no credentials needed)

# The following two scripts require DISCOURSE_API_KEY and DISCOURSE_API_USERNAME in .env
node scripts/refresh-leaderboard.mjs # Fetch leaderboard data per adventure/level
node scripts/refresh-community-leaders.mjs # Fetch community leader data
```

Create a `.env` file at the repo root for local use:
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ public/
refresh-community-sitemap.yml # Daily community sitemap regeneration
sync-adventure.yml # workflow_dispatch: sync an adventure from the challenges repo
validate-adventures.yml # PR check: validates adventure YAML, routes, and sitemap consistency
validate-docs.yml # PR check: ensures styleguide.md/README.md updated with code changes
add-discussion-url.yml # workflow_dispatch: set discussionUrl for a level and fetch initial posts
```

---
Expand Down Expand Up @@ -199,7 +201,7 @@ When diagnosing a bug, especially in the production build, follow these rules wi
- **Author-controlled prose fields contain pre-rendered HTML.** Every YAML/TS field that holds prose written by a challenge author (`level.audience`, `tool.description`, `step.title`, `step.content`, `contributor.about`, `rewards.eligibility`, `tier.description`, `rewards.rankingNote`, `level.learnings`, `level.objective`, `level.intro`, `level.backstory`, `level.scenario`, `level.architecture`, `adventure.story`, `adventure.backstory`) is converted from Markdown to sanitised HTML at build time by `scripts/generate-adventures.mjs`. Always render them with `dangerouslySetInnerHTML={{ __html: value }}` and the `md-inline` (inline prose) or `md-content` (block content) CSS class. Never render as `{value}` directly. Identifier fields (`id`, URLs, enum values like `difficulty`, emoji) are not author prose and are rendered directly.
- **When the container is an interactive element** (e.g. a `<Link>` card or a `<button>`), call `stripLinks(html)` from `src/lib/markdown.ts` before passing to `dangerouslySetInnerHTML` to prevent nested `<a>` inside `<a>` or `<button>`, which is invalid HTML.
- **Exception: `adventure.story` in `AdventureCard` and `summaries.ts`:** The summary card and `ADVENTURE_SUMMARIES` store `story` as plain text (no HTML) so the home page renders it as a plain `<span>` with no markdown overhead. The generator emits a build-time warning if any story value contains markdown syntax (`*`, `_`, `` ` ``). Keep story field values as plain prose.
- **`react-markdown` is a dev-only dependency** (used only by `scripts/generate-adventures.mjs`). Do not import it in any component or page file.
- **The markdown pipeline (`unified`, `remark-parse`, `remark-gfm`, `remark-rehype`, `rehype-sanitize`, `rehype-stringify`) is dev-only**, used only by `scripts/generate-adventures.mjs`. Do not import any of these packages in component or page files.

### Component CSS patterns

Expand All @@ -208,7 +210,7 @@ When diagnosing a bug, especially in the production build, follow these rules wi
- `data-difficulty` attribute on `DifficultyBadge`. It is used for CSS targeting of badge text color.
- `contributor-pill` class on `ContributorBadge`. Scopes light mode overrides: transparent background with slate border instead of the near-invisible `bg-primary/5`.
- `contributor-pill-glow` class on `ContributorBadge` (applied via `glow` prop). Static amber box-shadow glow, sized for a small pill. Used only on `ChallengeDetail` -- not in `AdventureCard`.
- `docs-ext-link` class on all inline prose links site-wide. Bundles `inline-flex`, `align-items: center`, `gap`, `underline`, `decoration-thickness`, `underline-offset`, `border-radius`, focus-visible ring, and color/hover transitions. Handles both modes: dark mode foreground text with amber underline, hover to full `#ffc034`; light mode near-black text with `currentColor` underline, hover to `--link-hover-light` (`hsl(41 100% 25%)` dark amber, ~5.5:1 contrast). Used in `CommunityGuide`, `DiscussionSection`, `CommunitySection`, `LevelCard`, `PersonNameLink`, `ChallengeBuildersSection`, `ChallengeDetail`, `CommunitySidebar`, `RewardsCard`, `Accessibility`, and `Privacy`. Links inside pre-rendered adventure HTML use the `.md-inline a` and `.md-content a` rules in `src/index.css` instead. Do not use `hover:text-primary` or `hover:underline` on inline links, and do not add redundant `inline-flex items-center gap-*` utilities. Use `docs-ext-link` alone, adding only contextual utilities (font-size, weight, margin).
- `docs-ext-link` class on all inline prose links site-wide. Bundles `inline-flex`, `align-items: center`, `gap`, `underline`, `decoration-thickness`, `underline-offset`, `border-radius`, focus-visible ring, and color/hover transitions. Handles both modes: dark mode foreground text with amber underline, hover to full `#ffc034`; light mode near-black text with `currentColor` underline, hover to `--link-hover-light` (`hsl(41 100% 22%)` dark amber, ~7.4:1 contrast). Used in `CommunityGuide`, `DiscussionSection`, `CommunitySection`, `LevelCard`, `PersonNameLink`, `ChallengeBuildersSection`, `ChallengeDetail`, `CommunitySidebar`, `RewardsCard`, `Accessibility`, and `Privacy`. Links inside pre-rendered adventure HTML use the `.md-inline a` and `.md-content a` rules in `src/index.css` instead. Do not use `hover:text-primary` or `hover:underline` on inline links, and do not add redundant `inline-flex items-center gap-*` utilities. Use `docs-ext-link` alone, adding only contextual utilities (font-size, weight, margin).

---

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Open PRs from your fork against `main` on the upstream repo.
```sh
npm run lint # ESLint
npm test # Vitest unit tests
npm run build && npm run test:e2e # Playwright smoke + axe a11y tests
npm run build && npm run test:e2e # Playwright smoke, WSG, a11y, and hydration tests
```

All three must pass with zero failures before opening a PR.
Expand Down
2 changes: 1 addition & 1 deletion PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Target these thresholds at the 75th percentile of real users:
{ rel: "preload", href: `${import.meta.env.BASE_URL}fonts/jetbrains-mono-latin-600-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
];
```
- The `src/index.css` `@font-face` declarations cover only the `latin` and `latin-ext` subsets. Non-English subsets (cyrillic, greek, vietnamese) were removed. The site is English-only and `unicode-range` already prevented those files from being fetched, but the declarations added unnecessary CSS weight.
- The `src/index.css` `@font-face` declarations cover only the `latin` and `latin-ext` subsets. Non-English subset declarations (cyrillic, greek, vietnamese) were removed from CSS. The site is English-only and `unicode-range` already prevented those files from being fetched, but the declarations added unnecessary CSS weight. Note: the corresponding `.woff2` files remain in `public/fonts/` but are never declared in CSS and will never be fetched by the browser.
- When adding a new route that uses JetBrains Mono (e.g. a page with code blocks or difficulty badges), add the JetBrains Mono preloads to that route's `links()` export.

---
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Node.js **24** is required. Version is pinned in `.nvmrc`, run `nvm use` to swit
| `npm test` | Run the full test suite once (Vitest) |
| `npm run test:watch` | Run tests in watch mode |
| `npm run test:coverage` | Run tests with v8 coverage report |
| `npm run test:e2e` | Playwright smoke and WSG tests (requires `npm run build` first) |
| `npm run test:e2e` | Playwright smoke, WSG, accessibility, and hydration tests (requires `npm run build` first) |
| `npm run generate` | Regenerate TypeScript from adventure YAML files |
| `npm run generate:validate` | Validate adventure YAML against schema without writing files |

Expand All @@ -67,8 +67,10 @@ src/
entry.server.tsx # Server/prerender entry: renderToPipeableStream for static HTML generation
Layout.tsx # App shell with all providers and Outlet
e2e/
smoke.spec.ts # Playwright smoke tests: route titles, axe a11y audit (requires npm run build first)
wsg.spec.ts # Web Sustainability Guidelines checks: page weight, third-party requests, image optimisation
smoke.spec.ts # Playwright smoke tests: route titles, axe a11y audit (requires npm run build first)
wsg.spec.ts # Web Sustainability Guidelines checks: page weight, third-party requests, image optimisation
a11y.spec.ts # Targeted accessibility checks: keyboard navigation, Windows High Contrast Mode
hydration.spec.ts # Hydration verification: confirms prerendered HTML matches client hydration
schemas/
adventure.schema.json # JSON Schema for adventure YAML validation
scripts/
Expand Down Expand Up @@ -106,6 +108,7 @@ Adventures are authored as YAML at `src/data/adventures/<id>/adventure.yaml` and
| `/handbook` | `CommunityGuide.tsx` | Community handbook / documentation |
| `/privacy` | `Privacy.tsx` | GDPR-compliant privacy policy |
| `/accessibility` | `Accessibility.tsx` | WCAG accessibility statement |
| `/brand` | `BrandGuidelines.tsx` | Brand guidelines: logos, colors, typography, voice |
| `/404` | `NotFound.tsx` | Prerendered 404 page |
| `/community-guide` | redirects to `/handbook` | Legacy alias |
| `/docs` | redirects to `/handbook` | Legacy alias |
Expand Down
1 change: 1 addition & 0 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const ROUTES: RouteSpec[] = [
{ path: "/handbook", title: /Handbook/ },
{ path: "/privacy", title: /Privacy Policy/ },
{ path: "/accessibility", title: /Accessibility Statement/ },
{ path: "/brand", title: /Brand Guidelines/ },
{ path: "/404", title: /Page Not Found/ },
{ path: "/adventures", title: /Adventures - Open Source Learning Paths/ },
// GENERATED:adventures
Expand Down
Binary file added public/brand/offon-favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions public/brand/offon-favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/brand/offon-logo-dark-color.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading