Conversation
Introduce a token-driven dark theme with a system-preference-first fallback and a persistent client toggle. Added an inline theme bootstrap script in layout.tsx to apply saved 'agentrelay-theme' or let the browser/system preference win when no explicit choice exists; updated globals.css to use data-theme and color-scheme, dark-aware Shiki overrides, and multiple semantic token replacements. Updated landing.module.css and other component styles (BYOH dark styling, terminal previews, syntax colors, icon fills → currentColor) to respect theme tokens. Added ThemeToggle component and its styles, plus trajectory metadata files recording the work.
Replace the light, washed-out dark-mode primary button hover color with a denser blue (#4A90C2) in web/public/brand.css (updated both scopes). Add a new completed trajectory (traj_9tukgrm6vgrq) as .json and .md under .trajectories/completed and update .trajectories/index.json with the new entry and lastUpdated timestamp. Reason: improve hover contrast so primary actions don't appear faded in dark mode.
|
Preview deployed!
This preview will be cleaned up when the PR is merged or closed. |
Make dark the unconditional default across server and client: set html[data-theme="dark"] on the root HTML, set color-scheme: dark in globals.css, update the layout bootstrap script and ThemeToggle to default to 'dark' and remove system-prefers fallbacks and matchMedia listeners. Remove the prefers-color-scheme dark media rules from public/brand.css so theme variables are deterministic. Add .brandCtaPrimary and .brandCtaSecondary styles and apply them in BrandShowcase to match the landing CTA treatment and strengthen the secondary appearance. Adjust footer links: move OpenClaw under Product and remove the textual GitHub/Twitter links (icons remain). Add completed trajectory metadata files and update .trajectories index to reflect these changes.
| background: var(--surface-strong); | ||
| color: var(--nav-bg); |
There was a problem hiding this comment.
🔴 GitHub stars badge hover text is invisible due to --nav-bg semantic change
The badge hover state uses color: var(--nav-bg) which, before this PR, resolved to the brand blue var(--primary-500) = #4A90C2. After refactoring, --nav-bg was changed to rgba(255, 255, 255, 0.76) (light mode) and rgba(12, 28, 42, 0.92) (dark mode) at web/public/brand.css:106 and web/public/brand.css:180. Combined with background: var(--surface-strong) on hover — which is #FFFFFF in light mode and #132234 in dark mode — both modes produce near-zero contrast between text and background, making the badge completely unreadable on hover.
Was this helpful? React with 👍 or 👎 to provide feedback.
| .syn-string { color: #b45309; } | ||
| .syn-keyword { color: var(--primary); } | ||
| .syn-string { color: var(--syntax-string); } | ||
| .syn-type { color: var(--primary-500); } |
There was a problem hiding this comment.
🟡 .syn-type not converted to dark-mode-aware variable, unlike all other syntax classes
The PR converts all syntax highlighting classes in web/app/globals.css:38-43 to use dark-mode-aware CSS variables (e.g., .syn-keyword → var(--primary), .syn-string → var(--syntax-string)), but .syn-type at line 40 was left using var(--primary-500). The --primary-500 token is only defined in the light :root block at web/public/brand.css:47 and is not overridden in the :root[data-theme='dark'] block at web/public/brand.css:150. In dark mode, .syn-type will render at #4A90C2 (the light-mode primary scale value) while all neighboring syntax classes use their adapted dark-mode colors. This is used in the BrandShowcase code block (web/app/brand/BrandShowcase.tsx:435-436).
| .syn-type { color: var(--primary-500); } | |
| .syn-type { color: var(--primary); } |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Pull request overview
Adds a token-driven dark theme to the Next.js web landing experience (including docs code blocks), with a persistent theme toggle in the site navigation and dark-aware syntax highlighting.
Changes:
- Introduces dark-theme token overrides and additional semantic tokens in the shared brand palette.
- Adds a client-side theme toggle plus an early inline bootstrap script to apply the saved theme before hydration.
- Updates landing/nav/footer/docs UI styles (and Shiki highlighting) to use theme tokens and support dark mode.
Reviewed changes
Copilot reviewed 40 out of 40 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| web/public/brand.css | Adds new semantic tokens and a :root[data-theme='dark'] override palette. |
| web/lib/syntax.ts | Updates Shiki highlighting to emit dual light/dark theme metadata. |
| web/components/theme-toggle.module.css | Styles for the new theme toggle button (desktop + mobile variants). |
| web/components/site-nav.module.css | Switches nav styling to new nav tokens and adds layout for actions area. |
| web/components/site-footer.module.css | Refactors footer colors to use new footer tokens. |
| web/components/sdk-code.module.css | Makes SDK code tabs/syntax colors token-driven for theming. |
| web/components/node-relay.module.css | Updates “agent card” styling to use theme tokens. |
| web/components/github-stars.module.css | Updates GitHub stars badge to use theme tokens. |
| web/components/docs/docs.module.css | Updates docs code group tabs border color to token. |
| web/components/docs/docs-search.module.css | Updates docs search UI (trigger, overlay, modal shadow) to theme tokens. |
| web/components/docs/HighlightedCode.tsx | Extracts Shiki <pre> metadata and applies it to existing <pre> wrapper. |
| web/components/ThemeToggle.tsx | New persistent light/dark toggle that sets html[data-theme]. |
| web/components/SiteNav.tsx | Adds the theme toggle to desktop nav and mobile menu. |
| web/components/SiteFooter.tsx | Reorganizes footer links (adds OpenClaw under Product; removes some text links). |
| web/components/NodeRelayAnimation.tsx | Switches provider logo SVG fills to currentColor for theme awareness. |
| web/app/page.tsx | Updates inline SVG fills to currentColor for theme awareness on the landing page. |
| web/app/layout.tsx | Adds theme bootstrap inline script + sets default data-theme="dark" on <html>. |
| web/app/landing.module.css | Swaps hardcoded light colors for theme tokens; adds dark-specific BYOH styling. |
| web/app/globals.css | Adds color-scheme handling and dark-mode Shiki overrides; tokenizes syntax colors. |
| web/app/brand/brand.module.css | Adds landing-style CTA button classes with dark-mode adjustments. |
| web/app/brand/BrandShowcase.tsx | Applies new CTA button classes to match landing CTAs on the brand showcase page. |
| .trajectories/index.json | Updates trajectories index timestamp and adds entries related to this work. |
| .trajectories/completed/2026-03/traj_wtbcox8epx03.md | New trajectory artifact for dark mode work. |
| .trajectories/completed/2026-03/traj_wtbcox8epx03.json | New trajectory artifact for dark mode work (JSON). |
| .trajectories/completed/2026-03/traj_sova3x4pggbf.md | New trajectory artifact for BYOH dark-mode refinement. |
| .trajectories/completed/2026-03/traj_sova3x4pggbf.json | New trajectory artifact for BYOH dark-mode refinement (JSON). |
| .trajectories/completed/2026-03/traj_pne3ja2bbfge.md | New trajectory artifact for brand button alignment. |
| .trajectories/completed/2026-03/traj_pne3ja2bbfge.json | New trajectory artifact for brand button alignment (JSON). |
| .trajectories/completed/2026-03/traj_lt4z4nd05lme.md | New trajectory artifact for theme default behavior decision. |
| .trajectories/completed/2026-03/traj_lt4z4nd05lme.json | New trajectory artifact for theme default behavior decision (JSON). |
| .trajectories/completed/2026-03/traj_k3nw85s9i5e5.md | New trajectory artifact for footer link reorg. |
| .trajectories/completed/2026-03/traj_k3nw85s9i5e5.json | New trajectory artifact for footer link reorg (JSON). |
| .trajectories/completed/2026-03/traj_hx1310ftp8xg.md | New trajectory artifact for OpenClaw footer link move. |
| .trajectories/completed/2026-03/traj_hx1310ftp8xg.json | New trajectory artifact for OpenClaw footer link move (JSON). |
| .trajectories/completed/2026-03/traj_ah4nx4fflbyr.md | New trajectory artifact for removing footer text links. |
| .trajectories/completed/2026-03/traj_ah4nx4fflbyr.json | New trajectory artifact for removing footer text links (JSON). |
| .trajectories/completed/2026-03/traj_9yf0nxle9kv2.md | New trajectory artifact for “default to dark unless overridden”. |
| .trajectories/completed/2026-03/traj_9yf0nxle9kv2.json | New trajectory artifact for “default to dark unless overridden” (JSON). |
| .trajectories/completed/2026-03/traj_9tukgrm6vgrq.md | New trajectory artifact for dark-mode hover token adjustment. |
| .trajectories/completed/2026-03/traj_9tukgrm6vgrq.json | New trajectory artifact for dark-mode hover token adjustment (JSON). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .badge:hover { | ||
| background: #ffffff; | ||
| background: var(--surface-strong); | ||
| color: var(--nav-bg); | ||
| animation: none; |
There was a problem hiding this comment.
In dark theme, --nav-bg is a very dark color while --surface-strong is also dark, so color: var(--nav-bg) on hover will likely produce very low contrast text. Use a foreground token (e.g. --nav-fg/--fg) for the hover text color, or adjust the hover background to a light surface when using --nav-bg as the text color.
| "title": "Align the /brand page tokens with the actual site theme and refine dark-mode secondary buttons", | ||
| "status": "completed", | ||
| "startedAt": "2026-03-25T22:41:58.152Z", | ||
| "completedAt": "2026-03-25T22:44:48.512Z", | ||
| "path": "/Users/will/Projects/relay/.trajectories/completed/2026-03/traj_9tukgrm6vgrq.json" |
There was a problem hiding this comment.
These new trajectory entries store absolute local filesystem paths (including a username) in the repo. This is not portable and can leak developer machine details; consider storing repo-relative paths (or an ID) instead of /Users/... values.
| ], | ||
| "commits": [], | ||
| "filesChanged": [], | ||
| "projectId": "/Users/will/Projects/relay", |
There was a problem hiding this comment.
This trajectory JSON includes an absolute projectId path ("/Users/..."), which is environment-specific and can leak local machine details. Consider writing repo-relative identifiers/paths in committed trajectory artifacts.
| "projectId": "/Users/will/Projects/relay", | |
| "projectId": "relay", |
| const match = html.match(/<pre(?: class="([^"]*)")?(?: style="([^"]*)")?[^>]*><code>([\s\S]*)<\/code><\/pre>/); | ||
|
|
||
| if (!match) { | ||
| return { codeHtml: html }; | ||
| } |
There was a problem hiding this comment.
If the
/ extraction regex fails, highlightCode() currently returns codeHtml: html, which still contains a wrapper. HighlightedPre then injects that into its own element, producing nested/invalid markup. Consider making the fallback extract the inner contents (and/or always return separate pre/code fields) so callers never receive wrapper markup in codeHtml.
| <h4 className={s.colTitle}>Product</h4> | ||
| <Link href="/docs" className={s.link}>Documentation</Link> | ||
| <Link href="/docs/quickstart" className={s.link}>Quickstart</Link> | ||
| <Link href="/docs/reference/sdk" className={s.link}>SDK Reference</Link> | ||
| <Link href="/openclaw" className={s.link}>OpenClaw</Link> | ||
| <a href="https://agent-relay.com" className={s.link}>Cloud</a> | ||
| </div> | ||
| <div className={s.col}> | ||
| <h4 className={s.colTitle}>Community</h4> | ||
| <a href="https://github.com/agentworkforce/relay" target="_blank" rel="noopener noreferrer" className={s.link}>GitHub</a> | ||
| <Link href="/blog" className={s.link}>Blog</Link> | ||
| <a href="https://twitter.com/agent_relay" target="_blank" rel="noopener noreferrer" className={s.link}>Twitter</a> | ||
| </div> | ||
| <div className={s.col}> | ||
| <h4 className={s.colTitle}>Company</h4> | ||
| <Link href="/openclaw" className={s.link}>OpenClaw</Link> | ||
| <a href="mailto:hello@agentrelay.dev" className={s.link}>Contact</a> | ||
| </div> |
There was a problem hiding this comment.
This footer change removes the GitHub and Twitter text links from the column lists (leaving only the icon links in the bottom row). The PR description is focused on adding dark mode, so this looks like an unrelated behavioral/content change—please confirm this is intended and, if so, consider noting it in the PR description.
| function applyTheme(theme: Theme) { | ||
| document.documentElement.dataset.theme = theme; | ||
| document.documentElement.style.colorScheme = theme; | ||
| localStorage.setItem(STORAGE_KEY, theme); |
There was a problem hiding this comment.
applyTheme() writes to localStorage without a try/catch. In some environments (Safari private browsing, hardened settings, storage quota issues) localStorage.setItem can throw, which would break the click handler and potentially crash the client UI. Consider wrapping the localStorage write in a try/catch (similar to the bootstrap script in layout.tsx) or falling back to an in-memory theme if storage is unavailable.
| localStorage.setItem(STORAGE_KEY, theme); | |
| try { | |
| localStorage.setItem(STORAGE_KEY, theme); | |
| } catch { | |
| // Ignore storage errors (e.g., disabled storage, private browsing, quota issues) | |
| } |
barryonthecape
left a comment
There was a problem hiding this comment.
PR #647 — Dark Mode Review
✅ Approve — solid implementation overall.
Key strengths:
- No FOUC — the blocking inline script sets
data-themebefore body renders - Smooth 0.3s transitions on background and color throughout
- Proper localStorage persistence with key
agentrelay-theme - Hydration-safe
ThemeTogglewithmountedstate - Comprehensive token coverage in
brand.cssdark override
prefers-color-scheme:
The inline script in layout.tsx defaults to dark when there's no stored preference — it never checks the user's OS/browser light/dark preference. A light-mode-preferring user who hasn't toggled yet will always see dark on first visit.
File: web/app/layout.tsx (around line 61)
// Current (always defaults to dark):
document.documentElement.dataset.theme = 'dark';
// Suggested fix — respect OS preference when no stored value:
if (stored === 'dark' || stored === 'light') {
document.documentElement.dataset.theme = stored;
} else {
document.documentElement.dataset.theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}Minor note: The .trajectories/completed/2026-03/*.json and *.md files are generated artifacts included in the diff — consider excluding them from future PRs, though not a blocker.
Override the .headline opacity for dark mode by adding a global rule targeting html[data-theme='dark']. This forces headline opacity to 1 (instead of the default 0.85), improving contrast and readability in the dark theme.
Enhance landing page visuals and refine navigation theming. Add layered radial gradients and a dedicated --landing-hero-bg to the page and hero for light/dark modes, and tweak hero background opacity/filter. Strengthen SVG background accents by increasing stroke widths and opacities for connection lines, nodes, card outlines and pulse dots. Update nav styling to use a solid background variable, explicit border and shadow tokens, smoother transitions, and separate logo color variables. Add and adjust related CSS variables in brand.css to support the new light/dark nav and hero appearances.
| // since our components already provide the <pre> wrapper | ||
| const match = html.match(/<code[^>]*>([\s\S]*)<\/code>/); | ||
| return match ? match[1] : html; | ||
| const match = html.match(/<pre(?: class="([^"]*)")?(?: style="([^"]*)")?[^>]*><code>([\s\S]*)<\/code><\/pre>/); |
There was a problem hiding this comment.
🟡 Shiki HTML regex drops <code> attribute tolerance, risking silent parsing failure
The old regex used <code[^>]*> to extract Shiki output, tolerating any attributes on the <code> element. The new regex uses a literal <code> match (no [^>]*), meaning if Shiki ever outputs <code style=""> or <code tabindex="0">, the extraction silently fails and codeHtml returns the entire raw HTML (including <pre> and <code> wrapper tags). This would be rendered inside the existing <code> wrapper in web/components/docs/HighlightedCode.tsx:82-84, producing malformed nested HTML and losing the preClassName/preStyle needed for dark-mode Shiki highlighting.
| const match = html.match(/<pre(?: class="([^"]*)")?(?: style="([^"]*)")?[^>]*><code>([\s\S]*)<\/code><\/pre>/); | |
| const match = html.match(/<pre(?: class="([^"]*)")?(?: style="([^"]*)")?[^>]*><code[^>]*>([\s\S]*)<\/code><\/pre>/); |
Was this helpful? React with 👍 or 👎 to provide feedback.
Add a dark mode to the web landing pages