Skip to content

Dark mode#647

Merged
willwashburn merged 5 commits intomainfrom
dark-mode
Mar 26, 2026
Merged

Dark mode#647
willwashburn merged 5 commits intomainfrom
dark-mode

Conversation

@willwashburn
Copy link
Member

@willwashburn willwashburn commented Mar 25, 2026

Add a dark mode to the web landing pages


Open with Devin

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.
devin-ai-integration[bot]

This comment was marked as resolved.

@github-actions
Copy link
Contributor

Preview deployed!

Environment URL
Web https://pr-647.agentrelay.net

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.
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +18 to 19
background: var(--surface-strong);
color: var(--nav-bg);
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Open in Devin Review

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); }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 .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-keywordvar(--primary), .syn-stringvar(--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).

Suggested change
.syn-type { color: var(--primary-500); }
.syn-type { color: var(--primary); }
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 17 to 20
.badge:hover {
background: #ffffff;
background: var(--surface-strong);
color: var(--nav-bg);
animation: none;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +868 to +872
"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"
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
],
"commits": [],
"filesChanged": [],
"projectId": "/Users/will/Projects/relay",
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"projectId": "/Users/will/Projects/relay",
"projectId": "relay",

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +40
const match = html.match(/<pre(?: class="([^"]*)")?(?: style="([^"]*)")?[^>]*><code>([\s\S]*)<\/code><\/pre>/);

if (!match) {
return { codeHtml: html };
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 34
<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>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
function applyTheme(theme: Theme) {
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
localStorage.setItem(STORAGE_KEY, theme);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
localStorage.setItem(STORAGE_KEY, theme);
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch {
// Ignore storage errors (e.g., disabled storage, private browsing, quota issues)
}

Copilot uses AI. Check for mistakes.
barryonthecape
barryonthecape previously approved these changes Mar 25, 2026
Copy link
Collaborator

@barryonthecape barryonthecape left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #647 — Dark Mode Review

Approve — solid implementation overall.

Key strengths:

  • No FOUC — the blocking inline script sets data-theme before body renders
  • Smooth 0.3s transitions on background and color throughout
  • Proper localStorage persistence with key agentrelay-theme
  • Hydration-safe ThemeToggle with mounted state
  • Comprehensive token coverage in brand.css dark override

⚠️ One real issue — init script doesn't respect 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.
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

// 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>/);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
const match = html.match(/<pre(?: class="([^"]*)")?(?: style="([^"]*)")?[^>]*><code>([\s\S]*)<\/code><\/pre>/);
const match = html.match(/<pre(?: class="([^"]*)")?(?: style="([^"]*)")?[^>]*><code[^>]*>([\s\S]*)<\/code><\/pre>/);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@willwashburn willwashburn merged commit 6f60065 into main Mar 26, 2026
34 checks passed
@willwashburn willwashburn deleted the dark-mode branch March 26, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants