diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index de0407d5..572d5aa5 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef, type JSX } from "react"; -import { Link, useLocation } from "react-router"; +import { Link } from "react-router"; import { Sun, Moon, Menu, X, ExternalLink } from "lucide-react"; import { NavLink } from "@/components/NavLink"; import { useTheme } from "@/hooks/useTheme"; @@ -27,21 +27,11 @@ const NavThemeToggle = ({ theme, onToggle, className }: NavThemeToggleProps): JS ); type NavLinksProps = { - homeActive: boolean; onNavigate?: () => void; }; -const NavLinks = ({ homeActive, onNavigate }: NavLinksProps): JSX.Element => ( +const NavLinks = ({ onNavigate }: NavLinksProps): JSX.Element => ( <> - - Home - Challenges About ( export const Navbar = (): JSX.Element => { const { theme, toggle } = useTheme(); const [menuOpen, setMenuOpen] = useState(false); - const location = useLocation(); - const homeActive = location.pathname === "/"; const triggerRef = useRef(null); @@ -140,14 +128,14 @@ export const Navbar = (): JSX.Element => { >
- {/* Both always in DOM so React Router preloads both; CSS controls visibility. */} + {/* Dark logo is high-priority: it's visible in the default (dark) theme. Light logo uses auto priority since it's hidden until the user switches theme. */} - + {/* Desktop nav */}
- +
@@ -173,7 +161,7 @@ export const Navbar = (): JSX.Element => { id="mobile-menu" className="md:hidden border-t border-[hsl(var(--surface-border))] bg-background px-6 py-2 flex flex-col gap-1" > - +
)} diff --git a/src/components/StarterNudge.tsx b/src/components/StarterNudge.tsx index 1176248a..27db1e79 100644 --- a/src/components/StarterNudge.tsx +++ b/src/components/StarterNudge.tsx @@ -32,7 +32,7 @@ export const StarterNudge = (): JSX.Element | null => { New here?{" "} Start with {starterAdventure.title}, a {starterAdventure.tags[0]} adventure diff --git a/src/components/WalkthroughSection.tsx b/src/components/WalkthroughSection.tsx index 6a2abf44..af50b8d8 100644 --- a/src/components/WalkthroughSection.tsx +++ b/src/components/WalkthroughSection.tsx @@ -71,7 +71,7 @@ export const WalkthroughSection = ({ steps }: WalkthroughSectionProps): JSX.Elem className="flex w-full items-start gap-4 p-5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg" >
- ); -const DownloadGroup = ({ downloads }: { downloads: DownloadSpec[] }): JSX.Element => ( -
+const DownloadBadgeGroup = ({ downloads, center }: { downloads: DownloadSpec[]; center?: boolean }): JSX.Element => ( +
{downloads.map((d) => ( - + ))}
); @@ -286,51 +287,61 @@ const BrandGuidelines = (): JSX.Element => { {/* Logo */} - {/* Icon mark */} + {/* Project Mark */} +

Project Mark

+

+ The horizontal wordmark is the preferred form. Use the icon mark alone only when the wordmark won't fit (minimum 24 px icon height). +

+
+ {LOGO_CARDS.map((card) => ( +
+
+ {card.alt} +
+
+

{card.label}

+
+

{card.note}

+ +
+
+
+ ))} +
+ + {/* Icon Mark */}

Icon Mark

A geometric amber firefly bolt. Use it at 24 px minimum. Never rotate, recolor, or distort it.

-
+
{[ - { bg: "bg-[#0a0a0a]", border: "border-[hsl(var(--surface-border))]", label: "On dark" }, - { bg: "bg-[#f8f9fb]", border: "border-[hsl(220,12%,87%)]", label: "On light" }, - { bg: "bg-primary", border: "border-primary/30", label: "On amber" }, - ].map(({ bg, border, label }) => ( -
-
+ { bg: "bg-[#0a0a0a]", label: "On dark" }, + { bg: "bg-[#f8f9fb]", label: "On light" }, + { bg: "bg-primary", label: "On amber" }, + ].map(({ bg, label }) => ( +
+
{`OffOn
- {label} -
- ))} -
- - - {/* Wordmark */} -

Wordmark

-

- Dark backgrounds use the color variant. Light and amber backgrounds use the light mono. Single-color print on dark uses the dark mono. -

-
- {LOGO_CARDS.map((card) => ( -
-
- {card.alt} +
+

{label}

-

{card.label}

-

{card.note}

-
))} + {/* Single download card for all icon mark variants */} +
+

Download

+ +
{/* Do / Don't */} @@ -427,19 +438,23 @@ const BrandGuidelines = (): JSX.Element => {

Syne

font-heading / 700 / Headings and display
-
-

Open Source

-

Community First

-

Build Together

-

Vendor-Neutral

-

Always Learning

+
+ + + + +

Inter

- font-sans / 400-700 / Body and UI + font-sans / 400-600 / Body and UI
{FONT_WEIGHTS.map(({ weight, label, sample }) => ( @@ -474,9 +489,9 @@ hsl(var(--foreground))`}

Nyx is the {BRAND_NAME} firefly mascot. Nyx is gender neutral, so avoid gendered pronouns. Use Nyx in event materials, swag, and community campaigns. Don't alter the colors, proportions, or shape.

-
-
-
+
+
+
Nyx, the OffOn firefly mascot, full view decoding="async" />
-

Nyx: Full

-

Hero sections, event banners, swag.

- +
+

Nyx: Full

+
+

Hero sections, event banners, swag.

+ +
+
-
-
+
+
Nyx the OffOn firefly mascot, peeking variant decoding="async" />
-

Nyx: Peek

-

Page hero backgrounds, corner decorations.

- +
+

Nyx: Peek

+
+

Page hero backgrounds, corner decorations.

+ +
+

Open Graph Image

-

+

Used as the social media preview when {BRAND_NAME} links are shared. Dimensions: 1200 x 630 px.

-
+
OffOn Open Graph preview image, 1200 by 630 pixels +
+

og.png · 1200 × 630

+ +
- {/* Photography */} @@ -578,7 +604,7 @@ hsl(var(--foreground))`}

Brand Name

-

{BRAND_NAME}

+

Always camelCase. Domain is always lowercase:{" "} offon.dev diff --git a/src/root.tsx b/src/root.tsx index 55f5185a..dc7b237b 100644 --- a/src/root.tsx +++ b/src/root.tsx @@ -5,7 +5,7 @@ import "./index.css"; export const links: LinksFunction = () => [ // Preload fonts used above the fold on every page. - // Inter 400–700: body text, semibold labels, bold headings used on every page. + // Inter 400–600: body text, semibold labels, and all button variants use at most font-semibold (600). // Syne 700: all h1–h6 via the @layer base rule in index.css. // Latin-only subsets are always needed for English content and never generate "preloaded but not used" warnings. // font-display: optional requires preloads to succeed. Without them, the optional window expires @@ -13,7 +13,6 @@ export const links: LinksFunction = () => [ { rel: "preload", href: `${import.meta.env.BASE_URL}fonts/inter-latin-400-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, { rel: "preload", href: `${import.meta.env.BASE_URL}fonts/inter-latin-500-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, { rel: "preload", href: `${import.meta.env.BASE_URL}fonts/inter-latin-600-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, - { rel: "preload", href: `${import.meta.env.BASE_URL}fonts/inter-latin-700-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, { rel: "preload", href: `${import.meta.env.BASE_URL}fonts/syne-latin-700-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, ]; diff --git a/src/test/navbar.test.tsx b/src/test/navbar.test.tsx index 8314d6f5..1bab7e3a 100644 --- a/src/test/navbar.test.tsx +++ b/src/test/navbar.test.tsx @@ -56,13 +56,6 @@ describe("Navbar - desktop navigation", () => { expect(screen.getByRole("navigation", { name: "Main" })).toBeTruthy(); }); - it("has a Home link", () => { - renderNavbar(); - const nav = screen.getByRole("navigation", { name: "Main" }); - // There may be duplicates in mobile menu; at least one must be present - expect(within(nav).getAllByRole("link", { name: /Home/i }).length).toBeGreaterThan(0); - }); - it("has an About link pointing to /about", () => { renderNavbar(); const aboutLinks = screen.getAllByRole("link", { name: /About/i }); @@ -158,7 +151,6 @@ describe("Navbar - mobile menu", () => { const mobileMenu = document.getElementById("mobile-menu"); expect(mobileMenu).toBeTruthy(); const mobileNav = within(mobileMenu!); - expect(mobileNav.getByRole("link", { name: /Home/i })).toBeTruthy(); expect(mobileNav.getByRole("link", { name: /About/i })).toBeTruthy(); }); diff --git a/styleguide.md b/styleguide.md index 2c1bd9b9..16e9cbf4 100644 --- a/styleguide.md +++ b/styleguide.md @@ -27,7 +27,7 @@ All brand copy constants live in `src/data/constants.ts`. Use them instead of ha | Role | Family | Key weights | Format | |---|---|---|---| | Headings / display (`font-heading`) | Syne | 700 | WOFF2 only (`public/fonts/syne-*.woff2`) | -| Body & UI (`font-sans`) | Inter | 400, 500, 600 primary (700 available) | WOFF2 only (`public/fonts/inter-*.woff2`) | +| Body & UI (`font-sans`) | Inter | 400, 500, 600 | WOFF2 only (`public/fonts/inter-*.woff2`) | | Code / mono (`font-mono`, `code`, `pre`) | JetBrains Mono | 400 primary (500, 600 available) | WOFF2 only (`public/fonts/jetbrains-mono-*.woff2`) | All fonts are fully self-hosted as WOFF2. No TTF fallbacks. No external network requests. @@ -48,7 +48,6 @@ Fonts are preloaded to avoid the three-level font discovery delay (HTML parse - `inter-latin-400-normal.woff2`: body text (Navbar, paragraphs) - `inter-latin-500-normal.woff2`: medium-weight body text (Navbar links) - `inter-latin-600-normal.woff2`: semibold text (section labels, card titles) -- `inter-latin-700-normal.woff2`: bold text (CTA buttons using `font-bold` on non-heading elements) - `syne-latin-700-normal.woff2`: h1 and h2 elements (the `@layer base` rule in `src/index.css` applies `font-family: 'Syne'` to h1 and h2 only; h3–h6 use Inter) Only Latin subset variants are preloaded. Other subsets are served from `public/fonts/` but are not preloaded. Update `src/root.tsx` whenever above-the-fold typography changes. @@ -206,8 +205,8 @@ In light mode, `bg-primary` sections (PageHero, BottomCTA) stay amber. Do **not* | `.btn-primary` | Filled amber, `rounded-md px-5 py-3 text-sm font-semibold`, `brightness-110` on hover | Default CTA on page background | | `.btn-ghost` | Outlined, `border-foreground/35`, amber border and text on hover | Secondary CTA on page background | | `.btn-soft` | Tinted `bg-primary/10 border-primary/30`, no glow | Tertiary / low-emphasis action | -| `.btn-inverse` | White/background fill with primary border, primary text; inverts on hover to primary bg | Primary CTA inside a `bg-primary` section (e.g. `PageHero`, `BottomCTA`) | -| `.btn-ghost-inverse` | Transparent with background-colored border and text; inverts on hover to background fill | Secondary CTA inside a `bg-primary` section | +| `.btn-inverse` | White/background fill with primary border, primary text, `font-semibold`; inverts on hover to primary bg | Primary CTA inside a `bg-primary` section (e.g. `PageHero`, `BottomCTA`) | +| `.btn-ghost-inverse` | Transparent with background-colored border and text, `font-semibold`; inverts on hover to background fill | Secondary CTA inside a `bg-primary` section | #### Button contrast rule (light mode)