Skip to content

feat: add light theme token set, prefers-color-scheme support, and ex… - #21

Open
sharancreates wants to merge 20 commits into
stupnd:mainfrom
sharancreates:add-light-mode
Open

feat: add light theme token set, prefers-color-scheme support, and ex…#21
sharancreates wants to merge 20 commits into
stupnd:mainfrom
sharancreates:add-light-mode

Conversation

@sharancreates

@sharancreates sharancreates commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

feat: add light theme token set, prefers-color-scheme support, and explicit theme toggle

Summary

This PR adds a near-neutral light canvas token set to Tinted behind @media (prefers-color-scheme: light) and explicit data-theme="light" / data-theme="dark" attribute selectors, re-verifies all text/surface pairs at WCAG AA contrast, and introduces an explicit Theme Toggle component in the navigation bar.

Like the dark canvas, the light canvas is kept near-neutral (#f7f8f9 bg, #ffffff surface, #e8eaea raised) to prevent background tint from distorting perceived skin tone swatches.

Changes Made

  • [globals.css]:
    • Defined light mode token set (--color-bg: #f7f8f9, --color-surface: #ffffff, --color-raised: #e8eaea, --color-accent: #875018).
    • Added @media (prefers-color-scheme: light) and html[data-theme="light"] / html[data-theme="dark"] rules.
    • Documented WCAG AA contrast measurements for both dark and light modes.
    • Updated scrollbar thumb and cut-out photo checkerboard (.bg-swatch-check) to dynamically adapt using CSS variables.
  • [ThemeToggle.tsx]:
    • Built an accessible theme switcher component that cycles between Auto (System OS preference), Light, and Dark modes.
    • Persists theme selection in localStorage under tinted-theme.
    • Dynamically updates data-theme on document.documentElement and <meta name="theme-color">.
  • [Navbar.tsx]:
    • Rendered <ThemeToggle /> in the header next to <AuthButton />.
  • [layout.tsx]:
    • Added an inline script in <head> to read localStorage theme preference before render, preventing Flash of Unstyled Content (FOUC).

WCAG AA Contrast Verification

Token Light Value Surface Contrast Ratio WCAG AA Status
text #141517 --color-bg (#f7f8f9) 17.15:1 Passed (AAA)
text #141517 --color-surface (#ffffff) 18.23:1 Passed (AAA)
text #141517 --color-raised (#e8eaea) 15.24:1 Passed (AAA)
text-soft #484a4e --color-bg (#f7f8f9) 8.29:1 Passed (AAA)
text-soft #484a4e --color-surface (#ffffff) 8.82:1 Passed (AAA)
text-soft #484a4e --color-raised (#e8eaea) 7.37:1 Passed (AAA)
text-muted #616368 --color-bg (#f7f8f9) 5.49:1 Passed (AA)
text-muted #616368 --color-surface (#ffffff) 5.83:1 Passed (AA)
text-muted #616368 --color-raised (#e8eaea) 4.88:1 Passed (AA)
accent #875018 --color-bg (#f7f8f9) 6.10:1 Passed (AA)
accent #875018 --color-surface (#ffffff) 6.49:1 Passed (AA)
accent #875018 --color-raised (#e8eaea) 5.43:1 Passed (AA)

Summary by Sourcery

Introduce a theme system with light and dark token sets, automatic system preference support, and an explicit theme toggle in the navbar.

New Features:

  • Add light theme design tokens alongside the existing dark theme, with near-neutral surfaces verified for WCAG AA contrast.
  • Introduce a ThemeToggle component that cycles between system, light, and dark modes and persists the choice in localStorage.
  • Add pre-hydration theme bootstrapping to apply stored or system theme before React renders to avoid flashes of incorrect theme.

Enhancements:

  • Refactor global color, typography, and elevation tokens to route through theme-scoped CSS variables for dynamic theming.
  • Update scrollbars and swatch checkerboard backgrounds to derive styling from theme-aware variables.

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

@sharancreates is attempting to deploy a commit to the stupnd's projects Team on Vercel.

A member of the Team first needs to authorize it.

@sourcery-ai

sourcery-ai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduce a two-theme token system (light and dark) driven by theme-scoped CSS variables, wire it to prefers-color-scheme and a new ThemeToggle UI, and preload the chosen theme (including meta theme-color) to avoid FOUC.

Sequence diagram for theme toggle interaction and application

sequenceDiagram
  actor User
  participant ThemeToggle
  participant theme_lib as theme_ts
  participant Document
  participant LocalStorage
  participant MetaTag as meta_theme_color

  User->>ThemeToggle: click
  ThemeToggle->>ThemeToggle: cycleTheme()
  ThemeToggle->>ThemeToggle: updateThemeMode(nextTheme)
  ThemeToggle->>theme_lib: applyTheme(nextTheme)
  theme_lib->>Document: getThemeAttribute(nextTheme)
  alt attr is light/dark
    theme_lib->>Document: setAttribute(data-theme, attr)
  else system/unknown
    theme_lib->>Document: removeAttribute(data-theme)
  end
  theme_lib->>LocalStorage: localStorage.setItem(THEME_STORAGE_KEY, nextTheme)
  theme_lib->>theme_lib: updateMetaThemeColor(nextTheme)
  theme_lib->>MetaTag: resolveMetaThemeColor(nextTheme, matchMedia("(prefers-color-scheme: light)").matches)
  MetaTag->>MetaTag: setAttribute(content, themeColor)
Loading

File-Level Changes

Change Details Files
Refactor global design tokens to be theme-scoped and define concrete light/dark token sets with prefers-color-scheme and data-theme support.
  • Replace direct design tokens with indirection through --theme-color-* variables for surfaces, text, accents, status, lines, and shadows.
  • Define canonical light and dark token values on :root using --light-* and --dark-* variables.
  • Add mapping rules for default dark, explicit html[data-theme="dark"], html[data-theme="light"], and a prefers-color-scheme: light override.
  • Hook base utilities (selection, scrollbar, checkerboard swatch) into the new variable-based colors.
frontend/app/globals.css
Add a reusable theme utilities module to read, resolve, and apply theme modes and keep meta[name="theme-color"] in sync.
  • Introduce ThemeMode type and constants for storage key and light/dark meta theme colors.
  • Provide helpers to interpret stored theme strings, resolve effective theme and meta color using prefers-color-scheme, and safely read from localStorage.
  • Implement applyTheme to set/remove documentElement data-theme, persist to storage, and update meta theme-color.
  • Add subscribeToSystemTheme to observe OS color-scheme changes and an inline THEME_PRELOAD_SCRIPT that sets data-theme and meta theme-color before hydration.
frontend/lib/theme.ts
Introduce an accessible ThemeToggle client component and surface it in the navbar.
  • Create ThemeToggle that initialises from stored theme, applies it on mount, and cycles between system → light → dark while updating document and storage.
  • Use a ref plus subscribeToSystemTheme so system-following users track OS changes without reattaching listeners.
  • Render an aria-hidden skeleton toggle before mount to avoid layout shift and hydration mismatch.
  • Insert ThemeToggle next to AuthButton in Navbar and fix the nav link transition duration variable syntax.
frontend/components/layout/ThemeToggle.tsx
frontend/components/layout/Navbar.tsx
Preload theme selection in the Next.js layout head to avoid FOUC and keep React hydration stable.
  • Import next/script and THEME_PRELOAD_SCRIPT into RootLayout and inject an inline beforeInteractive script that sets data-theme and meta theme-color before rendering.
  • Add suppressHydrationWarning to so server- and client-rendered data-theme differences do not trigger React warnings.
frontend/app/layout.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The light theme token values are duplicated in both the @media (prefers-color-scheme: light) block and html[data-theme="light"]; consider extracting them into a shared CSS custom properties block to avoid divergence over time.
  • Instead of injecting the inline theme-preload script with dangerouslySetInnerHTML in <head>, consider using next/script with strategy="beforeInteractive" to better align with Next.js’ script loading model.
  • In ThemeToggle, you might want to listen for prefers-color-scheme changes when in system mode so the meta theme-color stays in sync if the OS theme toggles while the page is open.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The light theme token values are duplicated in both the `@media (prefers-color-scheme: light)` block and `html[data-theme="light"]`; consider extracting them into a shared CSS custom properties block to avoid divergence over time.
- Instead of injecting the inline theme-preload script with `dangerouslySetInnerHTML` in `<head>`, consider using `next/script` with `strategy="beforeInteractive"` to better align with Next.js’ script loading model.
- In `ThemeToggle`, you might want to listen for `prefers-color-scheme` changes when in `system` mode so the meta `theme-color` stays in sync if the OS theme toggles while the page is open.

## Individual Comments

### Comment 1
<location path="frontend/components/layout/ThemeToggle.tsx" line_range="48-55" />
<code_context>
+    else updateTheme("system");
+  };
+
+  if (!mounted) {
+    return (
+      <div
+        className={cn(
+          "inline-flex h-9 w-9 items-center justify-center rounded-pill border border-line bg-raised/50",
+          className,
+        )}
+        aria-hidden="true"
+      />
+    );
</code_context>
<issue_to_address>
**nitpick (performance):** The pre-mount placeholder has a fixed size that may differ from the actual button, causing subtle layout shift.

The skeleton uses `inline-flex h-9 w-9`, while the real button adds horizontal padding (`px-2.5`) and a text label at `sm` breakpoints, so its width changes on mount and reflows the navbar. Consider aligning the placeholder’s layout with the final button (same padding, gap, and possibly a dummy label at `sm`) to minimize layout shift.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread frontend/components/layout/ThemeToggle.tsx
@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The preloading script only applies data-theme for "light"/"dark", but ThemeToggle now persists "system" as well; consider either omitting "system" from localStorage or updating the preload logic to handle it explicitly so the two code paths stay in sync.
  • The theme handling logic (localStorage key, data-theme attribute, and meta theme-color values) is duplicated between the inline script and ThemeToggle; extracting shared constants/utilities would reduce the risk of these drifting apart over time.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The preloading script only applies `data-theme` for `"light"`/`"dark"`, but `ThemeToggle` now persists `"system"` as well; consider either omitting `"system"` from localStorage or updating the preload logic to handle it explicitly so the two code paths stay in sync.
- The theme handling logic (localStorage key, `data-theme` attribute, and meta `theme-color` values) is duplicated between the inline script and `ThemeToggle`; extracting shared constants/utilities would reduce the risk of these drifting apart over time.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="frontend/components/layout/ThemeToggle.tsx" line_range="49-58" />
<code_context>
+      onClick={cycleTheme}
+      aria-label={label}
+      title={label}
+      className={cn(
+        "inline-flex h-9 items-center gap-1.5 rounded-pill border border-line bg-raised/60 px-2.5 py-1",
+        "text-small font-medium text-text-soft transition-colors duration-[--duration-fast]",
+        "hover:bg-raised hover:text-text focus-visible:outline-2",
+        className,
</code_context>
<issue_to_address>
**issue (bug_risk):** The Tailwind `duration-[--duration-fast]` class likely compiles to invalid CSS.

Tailwind arbitrary values must be valid CSS, so this will likely emit `transition-duration: --duration-fast`, which is invalid. If you want to use the custom property, use `duration-[var(--duration-fast)]`; otherwise, use a concrete utility like `duration-150`.
</issue_to_address>

### Comment 2
<location path="frontend/components/layout/ThemeToggle.tsx" line_range="16-25" />
<code_context>
+  const [theme, setTheme] = useState<ThemeMode>("system");
+  const [mounted, setMounted] = useState(false);
+
+  useEffect(() => {
+    setMounted(true);
+    const stored = localStorage.getItem(THEME_STORAGE_KEY) as ThemeMode | null;
+    if (stored === "light" || stored === "dark" || stored === "system") {
+      setTheme(stored);
+    }
+  }, []);
+
+  // Listen for system preference changes when in 'system' mode
+  useEffect(() => {
+    if (theme !== "system") return;
+    const mediaQuery = window.matchMedia("(prefers-color-scheme: light)");
+    const handleChange = () => {
+      updateMetaThemeColor("system");
+    };
+    mediaQuery.addEventListener("change", handleChange);
+    return () => mediaQuery.removeEventListener("change", handleChange);
+  }, [theme]);
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `addEventListener('change', ...)` on `matchMedia` can cause compatibility issues in some browsers.

`MediaQueryList.addEventListener('change', ...)` isn’t universally supported (e.g. older Safari). To avoid runtime issues and keep system-mode working across browsers, feature-detect and fall back to `addListener`:

```ts
const mediaQuery = window.matchMedia('(prefers-color-scheme: light)');
const handleChange = () => updateMetaThemeColor('system');

if ('addEventListener' in mediaQuery) {
  mediaQuery.addEventListener('change', handleChange);
  return () => mediaQuery.removeEventListener('change', handleChange);
} else {
  mediaQuery.addListener(handleChange);
  return () => mediaQuery.removeListener(handleChange);
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread frontend/components/layout/ThemeToggle.tsx
Comment thread frontend/components/layout/ThemeToggle.tsx Outdated
@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The dark theme surface and text token values are now defined both in the @theme block and in the :root/html[data-theme="dark"] rules; consider centralizing these in one place to avoid them drifting out of sync in future updates.
  • In ThemeToggle, the system-mode matchMedia listener only updates the meta theme color and not the CSS data-theme attribute; if you intend the UI to live-switch between light/dark tokens when the OS preference changes, you may want to call applyTheme("system") in the handler instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The dark theme surface and text token values are now defined both in the @theme block and in the :root/html[data-theme="dark"] rules; consider centralizing these in one place to avoid them drifting out of sync in future updates.
- In ThemeToggle, the system-mode matchMedia listener only updates the meta theme color and not the CSS data-theme attribute; if you intend the UI to live-switch between light/dark tokens when the OS preference changes, you may want to call applyTheme("system") in the handler instead.

## Individual Comments

### Comment 1
<location path="frontend/components/layout/ThemeToggle.tsx" line_range="68-73" />
<code_context>
+    );
+  }
+
+  const label =
+    theme === "system"
+      ? "Theme: System (click to switch to Light)"
+      : theme === "light"
+      ? "Theme: Light (click to switch to Dark)"
+      : "Theme: Dark (click to switch to System)";
+
+  return (
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The aria-label describes the *next* action rather than the current state, which can be confusing for screen reader users.

Because the label mixes current state with the future action, it’s harder for assistive technologies to convey what is selected versus what will happen on click. It’s preferable for the accessible name to reflect only the current theme (e.g. "Theme: Light"), and to communicate the switching behaviour separately (e.g. via tooltip or nearby text) rather than in the aria-label/title itself.

Suggested implementation:

```typescript
  const label =
    theme === "system"
      ? "Theme: System"
      : theme === "light"
      ? "Theme: Light"
      : "Theme: Dark";

```

1. Ensure the `label` continues to be used as the `aria-label` (or accessible name) for the toggle control, without adding future-action wording elsewhere in the accessible name (e.g. `title`).
2. If you want to preserve the "click to switch" guidance, consider adding it as visible helper text near the toggle or as a tooltip that is *not* used as the accessible name (depending on your tooltip implementation).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread frontend/components/layout/ThemeToggle.tsx Outdated
@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The theme token assignments for dark/light are duplicated across :root, html[data-theme] and the prefers-color-scheme media query; consider extracting a shared helper or using CSS custom properties more compositionally to reduce repetition and ease future palette changes.
  • The ThemeToggle’s aria-label only reflects the current mode while the control’s behavior is to cycle modes; consider using a more descriptive label (e.g. including the next state) or separate status text to better communicate the interaction to screen-reader users.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The theme token assignments for dark/light are duplicated across :root, html[data-theme] and the prefers-color-scheme media query; consider extracting a shared helper or using CSS custom properties more compositionally to reduce repetition and ease future palette changes.
- The ThemeToggle’s aria-label only reflects the current mode while the control’s behavior is to cycle modes; consider using a more descriptive label (e.g. including the next state) or separate status text to better communicate the interaction to screen-reader users.

## Individual Comments

### Comment 1
<location path="frontend/components/layout/Navbar.tsx" line_range="52" />
<code_context>
                   className={cn(
                     "inline-flex min-h-11 items-center whitespace-nowrap rounded-pill px-3",
-                    "text-small font-medium transition-colors duration-(--duration-fast)",
+                    "text-small font-medium transition-colors duration-[--duration-fast]",
                     active
                       ? "bg-accent-dim text-accent"
</code_context>
<issue_to_address>
**issue (bug_risk):** The Tailwind arbitrary duration value should wrap the CSS variable in `var(...)`.

`duration-[--duration-fast]` generates `animation-duration: --duration-fast`, which is not a valid time value. To correctly use the CSS variable, this should be `duration-[var(--duration-fast)]`, consistent with `ThemeToggle`.
</issue_to_address>

### Comment 2
<location path="frontend/app/globals.css" line_range="48-51" />
<code_context>
-  /* accent = fills and small text (white on it passes AA).
-     accent-vivid = display type and decoration only. */
-  --color-accent: #b23f13;
+  --color-accent: var(--dark-color-accent);
+  --color-accent-bright: var(--dark-color-accent-bright);
   --color-accent-vivid: #d9541f;
-  /* Hover on a filled button darkens rather than brightens — white on this is
-     7.57, so the label stays readable through the transition. */
   --color-accent-hover: #96330c;
-  --color-accent-dim: rgba(217, 84, 31, 0.1);
+  --color-accent-dim: var(--dark-color-accent-dim);
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Accent hover and vivid colors remain hard-coded and don’t follow the new dark/light token system.

These two variables remain hard-coded, so they won’t respond to theme changes or stay aligned with the WCAG-reviewed palette. Please add corresponding dark/light accent-vivid and hover tokens (e.g. `--dark-color-accent-vivid`, `--light-color-accent-vivid`) and use them here so accent states are tokenized consistently across themes.

Suggested implementation:

```
  /* ── Accent ───────────────────────────────────────────────────────────── */
  --color-accent: var(--dark-color-accent);
  --color-accent-bright: var(--dark-color-accent-bright);
  --color-accent-vivid: var(--dark-color-accent-vivid);
  --color-accent-hover: var(--dark-color-accent-hover);
  --color-accent-dim: var(--dark-color-accent-dim);
  --color-rose: #a8395e;

```

To fully implement the dark/light token system for accent states, you should also:
1. Define `--dark-color-accent-vivid` and `--dark-color-accent-hover` alongside the other `--dark-color-*` tokens (using the WCAG-reviewed values that were previously hard-coded: `#d9541f` and `#96330c`).
2. Define corresponding `--light-color-accent-vivid` and `--light-color-accent-hover` tokens in the light theme section.
3. In the light theme block where `--color-accent` and related properties are set, mirror this pattern by wiring `--color-accent-vivid` and `--color-accent-hover` to the light tokens (`var(--light-color-accent-vivid)` / `var(--light-color-accent-hover)`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread frontend/components/layout/Navbar.tsx Outdated
Comment thread frontend/app/globals.css Outdated
@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The ThemeToggle effect that uses matchMedia.addListener should guard against missing window to avoid potential SSR/hydration issues and could be simplified by always using addEventListener with a feature check at module level.
  • The logic in THEME_PRELOAD_SCRIPT duplicates parts of applyTheme and updateMetaThemeColor; consider extracting a small shared helper or aligning the branches to keep theme handling consistent and easier to maintain.
  • In updateMetaThemeColor, repeatedly querying document.querySelector('meta[name="theme-color"]') on every theme change could be optimized by caching the element reference once, reducing DOM lookups during rapid toggling.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `ThemeToggle` effect that uses `matchMedia.addListener` should guard against missing `window` to avoid potential SSR/hydration issues and could be simplified by always using `addEventListener` with a feature check at module level.
- The logic in `THEME_PRELOAD_SCRIPT` duplicates parts of `applyTheme` and `updateMetaThemeColor`; consider extracting a small shared helper or aligning the branches to keep theme handling consistent and easier to maintain.
- In `updateMetaThemeColor`, repeatedly querying `document.querySelector('meta[name="theme-color"]')` on every theme change could be optimized by caching the element reference once, reducing DOM lookups during rapid toggling.

## Individual Comments

### Comment 1
<location path="frontend/components/layout/ThemeToggle.tsx" line_range="16-19" />
<code_context>
+  const [theme, setTheme] = useState<ThemeMode>("system");
+  const [mounted, setMounted] = useState(false);
+
+  useEffect(() => {
+    setMounted(true);
+    const stored = localStorage.getItem(THEME_STORAGE_KEY) as ThemeMode | null;
+    if (stored === "light" || stored === "dark" || stored === "system") {
+      setTheme(stored);
+    }
</code_context>
<issue_to_address>
**issue:** Guard localStorage access in effect to avoid runtime errors in restricted environments.

In some environments (e.g. Safari private mode or strict privacy settings), `localStorage.getItem` can throw instead of returning `null`. Because this effect runs on every mount, an exception here could break the toggle or page. Please wrap the `localStorage.getItem` call in a `try/catch`, similar to `THEME_PRELOAD_SCRIPT`, to make this hook resilient to those cases.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread frontend/components/layout/ThemeToggle.tsx Outdated
@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="frontend/lib/theme.ts" line_range="7" />
<code_context>
+export const THEME_COLOR_LIGHT = "#f7f8f9";
+export const THEME_COLOR_DARK = "#0b0b0c";
+
+let cachedMetaTag: HTMLMetaElement | null = null;
+
+/**
</code_context>
<issue_to_address>
**suggestion:** Align cachedMetaTag typing with querySelector return type to keep TypeScript happy.

`document.querySelector` returns `Element | null`, so assigning directly to `cachedMetaTag: HTMLMetaElement | null` will cause a TypeScript type error. Add an explicit assertion when caching the element to keep the strong `HTMLMetaElement` type:

```ts
let cachedMetaTag: HTMLMetaElement | null = null;

export function updateMetaThemeColor(theme: ThemeMode): void {
  if (typeof window === "undefined") return;

  if (!cachedMetaTag || !document.contains(cachedMetaTag)) {
    cachedMetaTag = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement | null;
  }
  if (!cachedMetaTag) return;
  // ...
}
```

Suggested implementation:

```typescript
let cachedMetaTag: HTMLMetaElement | null = null;

```

```typescript
    cachedMetaTag = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement | null;

```

I only see the top of the file, so you’ll need to:
1. Locate the `updateMetaThemeColor` (or equivalent) function where `cachedMetaTag` is assigned from `document.querySelector('meta[name="theme-color"]')` and ensure the SEARCH block matches that exact line.
2. If the assignment is slightly different (e.g. using double quotes or different spacing), adjust the SEARCH string accordingly, keeping the REPLACE as the asserted type `as HTMLMetaElement | null`.
3. Confirm there are no other places where `cachedMetaTag` is assigned from `querySelector` and apply the same assertion there to maintain consistent typing.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread frontend/lib/theme.ts
@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The light/dark theme token application blocks in globals.css are quite repetitive across :root, html[data-theme="light"/"dark"], and the prefers-color-scheme rule; consider consolidating these assignments (e.g. via a preprocessor or custom properties indirection) to reduce duplication and make future tuning easier.
  • ThemeToggle’s system-mode media query listener currently only reacts to changes; if a user switches from an explicit light/dark mode back to system, you may want to immediately re-evaluate and apply the current OS preference (rather than waiting for the next change event) to avoid any transient mismatch.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The light/dark theme token application blocks in globals.css are quite repetitive across :root, html[data-theme="light"/"dark"], and the prefers-color-scheme rule; consider consolidating these assignments (e.g. via a preprocessor or custom properties indirection) to reduce duplication and make future tuning easier.
- ThemeToggle’s system-mode media query listener currently only reacts to changes; if a user switches from an explicit light/dark mode back to system, you may want to immediately re-evaluate and apply the current OS preference (rather than waiting for the next change event) to avoid any transient mismatch.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In globals.css, the default active theme variable assignments appear outside of any selector block (immediately after the shared token :root definition), which will result in invalid CSS; these should be moved inside a selector (e.g. :root) or the stray closing brace adjusted.
  • The active-* CSS custom properties for dark and light themes are duplicated across html[data-theme] and the prefers-color-scheme media query; consider extracting a shared mixin or restructuring to reduce repetition and the risk of the two paths diverging.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In globals.css, the default active theme variable assignments appear outside of any selector block (immediately after the shared token :root definition), which will result in invalid CSS; these should be moved inside a selector (e.g. :root) or the stray closing brace adjusted.
- The active-* CSS custom properties for dark and light themes are duplicated across html[data-theme] and the prefers-color-scheme media query; consider extracting a shared mixin or restructuring to reduce repetition and the risk of the two paths diverging.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The theme token mapping for dark/light (--active-* assignments) is duplicated across :root, html[data-theme="dark"], html[data-theme="light"], and the media query; consider centralizing this logic (e.g., via shared utility classes or a smaller set of custom properties) to reduce maintenance overhead when tokens change.
  • The pre-hydration THEME_PRELOAD_SCRIPT reimplements parts of applyTheme/updateMetaThemeColor; aligning these flows or deriving the inline script from the same helpers would help avoid drift if the theme application logic evolves.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The theme token mapping for dark/light (`--active-*` assignments) is duplicated across `:root`, `html[data-theme="dark"]`, `html[data-theme="light"]`, and the media query; consider centralizing this logic (e.g., via shared utility classes or a smaller set of custom properties) to reduce maintenance overhead when tokens change.
- The pre-hydration `THEME_PRELOAD_SCRIPT` reimplements parts of `applyTheme`/`updateMetaThemeColor`; aligning these flows or deriving the inline script from the same helpers would help avoid drift if the theme application logic evolves.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The light theme variables are duplicated in both html[data-theme="light"] and the @media (prefers-color-scheme: light) block; consider extracting a shared rule (e.g. a class or selector that both extend) to reduce maintenance overhead when adjusting tokens.
  • The @theme block still hardcodes the dark palette values that are then overridden by the theme rules; if Tailwind doesn’t rely on those base values directly, simplifying or aligning them with the active theme system could make the token source of truth clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The light theme variables are duplicated in both `html[data-theme="light"]` and the `@media (prefers-color-scheme: light)` block; consider extracting a shared rule (e.g. a class or selector that both extend) to reduce maintenance overhead when adjusting tokens.
- The `@theme` block still hardcodes the dark palette values that are then overridden by the theme rules; if Tailwind doesn’t rely on those base values directly, simplifying or aligning them with the active theme system could make the token source of truth clearer.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The dark theme token definitions are duplicated in both :root and html[data-theme="dark"]; consider centralizing these values (e.g., via a shared class or CSS custom property aliasing) to reduce maintenance overhead when tweaking the palette.
  • THEME_PRELOAD_SCRIPT reimplements parts of applyTheme/updateMetaThemeColor logic as an inline string; factoring the decision logic into a shared helper that can be reused for both the preload script and runtime functions would help avoid subtle drift between behaviours over time.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The dark theme token definitions are duplicated in both :root and html[data-theme="dark"]; consider centralizing these values (e.g., via a shared class or CSS custom property aliasing) to reduce maintenance overhead when tweaking the palette.
- THEME_PRELOAD_SCRIPT reimplements parts of applyTheme/updateMetaThemeColor logic as an inline string; factoring the decision logic into a shared helper that can be reused for both the preload script and runtime functions would help avoid subtle drift between behaviours over time.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The THEME_PRELOAD_SCRIPT currently relies on getThemeAttribute.toString() and isLightTheme.toString(), which can be brittle under minification or future refactors; consider inlining a minimal, explicit version of that logic in the script string instead.
  • In ThemeToggle, the useEffect watching theme re-attaches the matchMedia listener whenever the mode changes to system; you could simplify by keeping a single listener and toggling behavior based on current mode to avoid repeated setup/teardown.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `THEME_PRELOAD_SCRIPT` currently relies on `getThemeAttribute.toString()` and `isLightTheme.toString()`, which can be brittle under minification or future refactors; consider inlining a minimal, explicit version of that logic in the script string instead.
- In `ThemeToggle`, the `useEffect` watching `theme` re-attaches the `matchMedia` listener whenever the mode changes to `system`; you could simplify by keeping a single listener and toggling behavior based on current mode to avoid repeated setup/teardown.

## Individual Comments

### Comment 1
<location path="frontend/components/layout/ThemeToggle.tsx" line_range="17-19" />
<code_context>
+
+  useEffect(() => {
+    setMounted(true);
+    const stored = getStoredTheme();
+    if (stored) {
+      setTheme(stored);
+    }
+  }, []);
</code_context>
<issue_to_address>
**issue (bug_risk):** Stored theme is loaded into state but never applied to the DOM if the preload script fails.

The mount effect only calls `setTheme(stored)`, but `applyTheme` is never run here, so `data-theme` and `meta[name="theme-color"]` won’t reflect the stored preference if the preload script doesn’t execute. To keep the DOM in sync with the resolved theme in all environments, call `applyTheme(stored ?? "system")` after `setTheme`, or replace `setTheme(stored)` with `updateThemeMode(stored)` in this effect.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread frontend/components/layout/ThemeToggle.tsx Outdated
@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The theme token rules in globals.css duplicate assignments across :root, .theme-dark, .theme-light, and the prefers-color-scheme block; consider extracting shared mappings into a single custom property set (e.g. via CSS custom properties or a utility class) to reduce repetition and make future palette changes less error-prone.
  • The logic for resolving the effective theme and updating meta[name="theme-color"] is implemented both in THEME_PRELOAD_SCRIPT and in updateMetaThemeColor/applyTheme; centralizing this decision logic in one place and reusing it in the inline script would make behavior easier to reason about and keep in sync.
  • The top-level suppressHydrationWarning on the element hides all hydration mismatches; if possible, narrow this to the specific theme-related markup or attributes that can diverge between server and client to avoid masking other potential hydration issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The theme token rules in globals.css duplicate assignments across :root, .theme-dark, .theme-light, and the prefers-color-scheme block; consider extracting shared mappings into a single custom property set (e.g. via CSS custom properties or a utility class) to reduce repetition and make future palette changes less error-prone.
- The logic for resolving the effective theme and updating meta[name="theme-color"] is implemented both in THEME_PRELOAD_SCRIPT and in updateMetaThemeColor/applyTheme; centralizing this decision logic in one place and reusing it in the inline script would make behavior easier to reason about and keep in sync.
- The top-level suppressHydrationWarning on the <html> element hides all hydration mismatches; if possible, narrow this to the specific theme-related markup or attributes that can diverge between server and client to avoid masking other potential hydration issues.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The light/dark token assignment blocks in globals.css are quite duplicated between :root/html[data-theme="dark"], .theme-light/html[data-theme="light"], and the prefers-color-scheme media query; consider consolidating shared property sets into a single rule or using CSS custom property indirection to reduce maintenance overhead when adjusting theme values.
  • In ThemeToggle, the mounted placeholder and the interactive button have slightly different contents and widths (e.g., icon + label vs skeleton blocks), which may cause minor layout shift on hydration; you may want to align their structure and sizing more closely to avoid any visual jump when the component mounts.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The light/dark token assignment blocks in globals.css are quite duplicated between :root/html[data-theme="dark"], .theme-light/html[data-theme="light"], and the prefers-color-scheme media query; consider consolidating shared property sets into a single rule or using CSS custom property indirection to reduce maintenance overhead when adjusting theme values.
- In ThemeToggle, the mounted placeholder and the interactive button have slightly different contents and widths (e.g., icon + label vs skeleton blocks), which may cause minor layout shift on hydration; you may want to align their structure and sizing more closely to avoid any visual jump when the component mounts.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The theme resolution logic is duplicated between THEME_PRELOAD_SCRIPT and the TypeScript helpers (isLightTheme/resolveMetaThemeColor); consider centralizing the conditions to reduce the risk of them drifting apart over time.
  • ThemeToggle’s matchMedia listener logic could be moved into a small helper in lib/theme to keep side-effectful DOM interactions in one place and make the component itself more declarative.
  • The pre-hydration theme script assumes a meta[name="theme-color"] tag exists; adding a guard or creating the tag if missing would make the behavior more robust across different HTML head configurations.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The theme resolution logic is duplicated between THEME_PRELOAD_SCRIPT and the TypeScript helpers (isLightTheme/resolveMetaThemeColor); consider centralizing the conditions to reduce the risk of them drifting apart over time.
- ThemeToggle’s matchMedia listener logic could be moved into a small helper in lib/theme to keep side-effectful DOM interactions in one place and make the component itself more declarative.
- The pre-hydration theme script assumes a meta[name="theme-color"] tag exists; adding a guard or creating the tag if missing would make the behavior more robust across different HTML head configurations.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The THEME_PRELOAD_SCRIPT currently relies on getThemeAttribute.toString() and isLightTheme.toString(), which can be brittle under minification/transform; consider inlining minimal helper logic in the string literal instead to avoid dependency on function serialization.
  • In layout.tsx you introduce a manual with a beforeInteractive Script; double-check that this doesn’t conflict with Next’s automatic head/metadata handling, and consider attaching the preload script via or using the metadata API if you run into head composition issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The THEME_PRELOAD_SCRIPT currently relies on getThemeAttribute.toString() and isLightTheme.toString(), which can be brittle under minification/transform; consider inlining minimal helper logic in the string literal instead to avoid dependency on function serialization.
- In layout.tsx you introduce a manual <head> with a beforeInteractive Script; double-check that this doesn’t conflict with Next’s automatic head/metadata handling, and consider attaching the preload script via <body> or using the metadata API if you run into head composition issues.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…beforeInteractive script placement in layout
@sharancreates

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sharancreates

Copy link
Copy Markdown
Contributor Author

@stupnd kindly check

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.

1 participant