From 7abfb90258051e2c821e017662d67ea02bdfaf3a Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Fri, 5 Jun 2026 14:09:35 +0200 Subject: [PATCH 1/2] fix: remove redundant Home nav link, refine brand guidelines and font preloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop Home NavLink from Navbar — logo link (aria-label="offon.dev") already serves as home link, removing the axe duplicate-link warning - Remove Inter 700 preload from root.tsx; no component uses font-bold on non-heading elements - Switch btn-inverse and btn-ghost-inverse from font-bold to font-semibold in index.css - Refactor BrandGuidelines asset cards to a unified card-with-footer pattern for logos, icon mark, Nyx, and OG image - Add JetBrains Mono 400/600 font preloads to the Brand Guidelines page Signed-off-by: Sinduri Guntupalli --- src/components/Navbar.tsx | 24 ++---- src/index.css | 4 +- src/pages/BrandGuidelines.tsx | 147 ++++++++++++++++++++-------------- src/root.tsx | 3 +- src/test/navbar.test.tsx | 8 -- styleguide.md | 7 +- 6 files changed, 100 insertions(+), 93 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index de0407d50..572d5aa5d 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/index.css b/src/index.css index 306b38f78..1a388bfb9 100644 --- a/src/index.css +++ b/src/index.css @@ -478,7 +478,7 @@ html { /* Inverse button - for use on primary-colored section backgrounds */ .btn-inverse { - @apply inline-flex items-center gap-2 rounded-md bg-background text-primary font-bold text-sm px-5 py-3 border-2 border-primary transition-all active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; + @apply inline-flex items-center gap-2 rounded-md bg-background text-primary font-semibold text-sm px-5 py-3 border-2 border-primary transition-all active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; } .btn-inverse:hover { background-color: hsl(var(--primary)); @@ -488,7 +488,7 @@ html { /* Ghost inverse button - for use on primary-colored section backgrounds */ .btn-ghost-inverse { - @apply inline-flex items-center gap-2 rounded-md bg-transparent text-background font-bold text-sm px-5 py-3 border-2 border-background/70 transition-all active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; + @apply inline-flex items-center gap-2 rounded-md bg-transparent text-background font-semibold text-sm px-5 py-3 border-2 border-background/70 transition-all active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2; } .btn-ghost-inverse:hover { background-color: hsl(var(--primary-foreground)); diff --git a/src/pages/BrandGuidelines.tsx b/src/pages/BrandGuidelines.tsx index 2c044d048..c4f3c1169 100644 --- a/src/pages/BrandGuidelines.tsx +++ b/src/pages/BrandGuidelines.tsx @@ -1,5 +1,5 @@ import type { JSX, ReactNode } from "react"; -import type { MetaFunction } from "react-router"; +import type { MetaFunction, LinksFunction } from "react-router"; import { Link } from "react-router"; import { useEffect, useState } from "react"; import { Check, X, Download, Zap } from "lucide-react"; @@ -23,6 +23,11 @@ export const meta: MetaFunction = () => url: `${SITE_URL}/brand`, }); +export const links: LinksFunction = () => [ + { rel: "preload", href: `${import.meta.env.BASE_URL}fonts/jetbrains-mono-latin-400-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, + { rel: "preload", href: `${import.meta.env.BASE_URL}fonts/jetbrains-mono-latin-600-normal.woff2`, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, +]; + // Compile-time constant — safe to evaluate at module level in a Vite app const BASE = import.meta.env.BASE_URL; @@ -168,22 +173,22 @@ const ColorCard = ({ swatch, dark }: { swatch: ColorSwatch; dark: boolean }): JS ); -const DownloadBtn = ({ href, filename, label }: DownloadSpec): JSX.Element => ( +const DownloadBadge = ({ href, filename, label }: DownloadSpec): JSX.Element => (
- ); -const DownloadGroup = ({ downloads }: { downloads: DownloadSpec[] }): JSX.Element => ( -
+const DownloadBadgeGroup = ({ downloads, center }: { downloads: DownloadSpec[]; center?: boolean }): JSX.Element => ( +
{downloads.map((d) => ( - + ))}
); @@ -286,51 +291,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,12 +442,15 @@ const BrandGuidelines = (): JSX.Element => {

Syne

font-heading / 700 / Headings and display
-
-

Open Source

-

Community First

-

Build Together

-

Vendor-Neutral

-

Always Learning

+
+ + + + +
@@ -474,9 +492,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 +607,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 55f5185a5..dc7b237b5 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 8314d6f5f..1bab7e3ad 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 2c1bd9b95..16e9cbf47 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) From f2b554882fc2e29ebab56f7876d7739e21e0d159 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Fri, 5 Jun 2026 14:18:52 +0200 Subject: [PATCH 2/2] fix: remove Inter 700 usage, dead LOGO_CARDS data, and aria violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch font-bold → font-semibold in StarterNudge and WalkthroughSection so no component relies on the unpreloaded Inter 700 weight - Remove dead `border` property from all three LOGO_CARDS entries (no longer consumed after the card layout refactor) - Use filter/join instead of string interpolation in DownloadBadgeGroup className - Drop the Bold (700) row from the FONT_WEIGHTS specimen and update the Inter label to 400-600 to match styleguide - Add role="img" to the Syne specimen div so aria-label is valid (fixes axe aria-prohibited-attr violation on /brand) Signed-off-by: Sinduri Guntupalli --- src/components/StarterNudge.tsx | 2 +- src/components/WalkthroughSection.tsx | 2 +- src/pages/BrandGuidelines.tsx | 9 +++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/StarterNudge.tsx b/src/components/StarterNudge.tsx index 1176248ac..27db1e79c 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 6a2abf448..af50b8d89 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" >

+
{downloads.map((d) => ( ))} @@ -444,6 +440,7 @@ const BrandGuidelines = (): JSX.Element => {
@@ -457,7 +454,7 @@ const BrandGuidelines = (): JSX.Element => {

Inter

- font-sans / 400-700 / Body and UI + font-sans / 400-600 / Body and UI
{FONT_WEIGHTS.map(({ weight, label, sample }) => (