Skip to content

feat: implement theme toggle functionality and enhance dark mode styles #140 - #139

Open
theavitw wants to merge 2 commits into
guidewire-oss:mainfrom
theavitw:feat-Darkmode
Open

feat: implement theme toggle functionality and enhance dark mode styles #140#139
theavitw wants to merge 2 commits into
guidewire-oss:mainfrom
theavitw:feat-Darkmode

Conversation

@theavitw

@theavitw theavitw commented Aug 1, 2026

Copy link
Copy Markdown

Summary by cubic

Adds a theme toggle with persisted preference and server-safe initial theme to deliver a consistent dark mode across the app. Updates login page and global styles for better contrast and polish in dark mode.

  • New Features
    • Added ThemeToggle (bottom-right) to switch light/dark, storing preference in localStorage and syncing data-theme and html.dark.
    • Injected an inline theme script in layout.tsx and enabled suppressHydrationWarning to set the theme before hydration and prevent flash.
    • Enabled darkMode: 'class' and expanded globals.css with variables and dark overrides for surfaces, text, borders, shadows, gradients, inputs, and selection.
    • Updated login page styles to fully support dark mode (backgrounds, text, borders, alerts, and demo info panel).
    • Added tests for ThemeToggle covering bootstrapped state, system preference fallback, persistence, and storage errors.

Written for commit 4f767a5. Summary will update on new commits.

Review in cubic

@theavitw theavitw changed the title feat: implement theme toggle functionality and enhance dark mode styles feat: implement theme toggle functionality and enhance dark mode styles #140 Aug 1, 2026
@theavitw

theavitw commented Aug 1, 2026

Copy link
Copy Markdown
Author

#140

@cubic-dev-ai cubic-dev-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.

7 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/app/globals.css">

<violation number="1" location="frontend/app/globals.css:89">
P1: The dark-mode color remapping uses substring selectors, so semantic solid colors such as `bg-red-500` and `bg-green-500` also match the `bg-red-50`/`bg-green-50` selectors. Their backgrounds are then replaced with the same translucent blue value via `!important`, making red/green health indicators blue in dark mode. Matching complete class tokens or using explicit dark variants would preserve those status colors.</violation>

<violation number="2" location="frontend/app/globals.css:93">
P2: Dark mode collapses success, error, and warning surfaces into the same blue tint, removing the color distinction used for Good/Poor ratings, validation errors, and selected survey responses. Preserve distinct semantic dark tints for green, red, and amber classes.</violation>

<violation number="3" location="frontend/app/globals.css:143">
P1: In dark mode, every button is forced to `background-color: var(--surface) !important` and `color: var(--foreground) !important`, so existing primary actions lose their Tailwind colors and white text. For example, the login submit button in `frontend/app/login/page.tsx` uses `bg-indigo-600 text-white`, but this global rule overrides both declarations; the same applies to the dashboard and admin action buttons. Restrict this generic control rule to form controls, or provide button-specific dark styles instead.</violation>
</file>

<file name="frontend/app/login/page.tsx">

<violation number="1" location="frontend/app/login/page.tsx:106">
P3: The per-element dark background utilities added here (dark:bg-slate-900/800, dark:bg-amber-950/40, dark:bg-red-950/*, dark:bg-indigo-950/70, and the dark:from/to-slate gradient on the demo panel) are overridden by the broad `!important` rules in app/globals.css (e.g. `html.dark [class*='bg-white']`, `[class*='bg-amber-50']`, `[class*='bg-red-50']`, `[class*='bg-indigo-100']`, `[class*='bg-gradient-to-br']`) that match the base light class still on the same element. They therefore never take effect; dark backgrounds come from the global overrides instead. Either drop these redundant dark:bg classes or make the dark mode styling consistent (e.g. remove the conflicting global overrides) so the intent isn't misleading.</violation>
</file>

<file name="frontend/components/ThemeToggle.tsx">

<violation number="1" location="frontend/components/ThemeToggle.tsx:22">
P2: When browser storage is blocked or unavailable, the global theme bootstrap safely falls back, but `ThemeToggle` still performs uncaught `localStorage.getItem` and `setItem` calls. In privacy-restricted or storage-disabled contexts this throws from the client effects on every page containing the root layout, leaving the toggle uninitialized and producing an uncaught runtime error. The component should treat storage as optional, just as the bootstrap script does.</violation>

<violation number="2" location="frontend/components/ThemeToggle.tsx:27">
P2: A saved dark preference is briefly treated as light during mount. The layout bootstrap script has already applied the saved theme before hydration, but this component starts with `theme` set to `'light'`; the `[theme]` effect therefore applies and persists light before the initialization effect's `setTheme(initialTheme)` update is reflected, then applies dark again. This can cause a theme flash and an unnecessary overwrite of the user's saved preference during startup.</violation>

<violation number="3" location="frontend/components/ThemeToggle.tsx:39">
P2: After the first page load, toggling the theme updates `data-theme` and the `dark` class but leaves the inline `document.documentElement.style.colorScheme` set by the layout bootstrap script unchanged. As a result, native controls and other browser UI can remain in the old color scheme after a toggle, even though the page CSS has switched themes. Updating the inline property whenever `theme` changes keeps the browser color scheme synchronized.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread frontend/app/globals.css Outdated
Comment thread frontend/app/globals.css Outdated
Comment thread frontend/app/globals.css Outdated
}

export default function ThemeToggle() {
const [theme, setTheme] = useState<Theme>('light');

@cubic-dev-ai cubic-dev-ai Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: A saved dark preference is briefly treated as light during mount. The layout bootstrap script has already applied the saved theme before hydration, but this component starts with theme set to 'light'; the [theme] effect therefore applies and persists light before the initialization effect's setTheme(initialTheme) update is reflected, then applies dark again. This can cause a theme flash and an unnecessary overwrite of the user's saved preference during startup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/components/ThemeToggle.tsx, line 27:

<comment>A saved dark preference is briefly treated as light during mount. The layout bootstrap script has already applied the saved theme before hydration, but this component starts with `theme` set to `'light'`; the `[theme]` effect therefore applies and persists light before the initialization effect's `setTheme(initialTheme)` update is reflected, then applies dark again. This can cause a theme flash and an unnecessary overwrite of the user's saved preference during startup.</comment>

<file context>
@@ -0,0 +1,56 @@
+}
+
+export default function ThemeToggle() {
+  const [theme, setTheme] = useState<Theme>('light');
+
+  useEffect(() => {
</file context>
Fix with cubic

Comment thread frontend/components/ThemeToggle.tsx Outdated
Comment thread frontend/components/ThemeToggle.tsx Outdated
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-slate-900 dark:to-slate-950 flex items-center justify-center p-4">
<div className="max-w-4xl w-full">
<div className="bg-white rounded-2xl shadow-xl overflow-hidden">
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-xl overflow-hidden border border-gray-200 dark:border-slate-800">

@cubic-dev-ai cubic-dev-ai Bot Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The per-element dark background utilities added here (dark:bg-slate-900/800, dark:bg-amber-950/40, dark:bg-red-950/*, dark:bg-indigo-950/70, and the dark:from/to-slate gradient on the demo panel) are overridden by the broad !important rules in app/globals.css (e.g. html.dark [class*='bg-white'], [class*='bg-amber-50'], [class*='bg-red-50'], [class*='bg-indigo-100'], [class*='bg-gradient-to-br']) that match the base light class still on the same element. They therefore never take effect; dark backgrounds come from the global overrides instead. Either drop these redundant dark:bg classes or make the dark mode styling consistent (e.g. remove the conflicting global overrides) so the intent isn't misleading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/app/login/page.tsx, line 106:

<comment>The per-element dark background utilities added here (dark:bg-slate-900/800, dark:bg-amber-950/40, dark:bg-red-950/*, dark:bg-indigo-950/70, and the dark:from/to-slate gradient on the demo panel) are overridden by the broad `!important` rules in app/globals.css (e.g. `html.dark [class*='bg-white']`, `[class*='bg-amber-50']`, `[class*='bg-red-50']`, `[class*='bg-indigo-100']`, `[class*='bg-gradient-to-br']`) that match the base light class still on the same element. They therefore never take effect; dark backgrounds come from the global overrides instead. Either drop these redundant dark:bg classes or make the dark mode styling consistent (e.g. remove the conflicting global overrides) so the intent isn't misleading.</comment>

<file context>
@@ -101,24 +101,24 @@ function LoginPageContent() {
+    <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-slate-900 dark:to-slate-950 flex items-center justify-center p-4">
       <div className="max-w-4xl w-full">
-        <div className="bg-white rounded-2xl shadow-xl overflow-hidden">
+        <div className="bg-white dark:bg-slate-900 rounded-2xl shadow-xl overflow-hidden border border-gray-200 dark:border-slate-800">
           <div className={`grid ${isDemoMode ? 'md:grid-cols-2' : ''}`}>
             {/* Login Form */}
</file context>
Fix with cubic

@ANIRUDH-333

Copy link
Copy Markdown
Collaborator

Thanks for the dark-mode work. I re-reviewed the latest head 737e026a against issue #140 and the frontend behavior. Before this is ready for approval, please address the following:

  1. Fix the global color selectors
    frontend/app/globals.css:89-97 uses substring selectors such as [class*='bg-red-50']. These also match bg-red-500 (and the corresponding green/indigo variants), so red/green indicators and survey rating colors can be recolored to the same blue tint. Please match complete class tokens or use explicit semantic/dark variants, while preserving red/yellow/green meaning. The broad gradient rules at lines 99-103 also replace intentional gradients, and the !important rules make per-element dark:* classes ineffective.

  2. Do not override every button
    globals.css:140-150 applies background-color: var(--surface) !important and foreground color to every button. This breaks primary actions (for example the admin Add Team button and login actions) and the survey score buttons in frontend/app/survey/page.tsx:581-624, removing their intended indigo/red/yellow/green states and hover styling. Restrict control styling to the appropriate form elements or use explicit component-level dark variants.

  3. Make theme persistence safe and deterministic
    frontend/components/ThemeToggle.tsx:22-40 needs guarded localStorage reads and writes; storage can be unavailable in blocked/private contexts and currently throws. The component also starts with theme='light', while the persistence effect can run before the initialization effect applies the saved/system theme. That can overwrite a saved dark preference and cause a flash. Initialize from the bootstrapped document state (or otherwise ensure only the resolved initial theme is persisted).

  4. Keep the browser color scheme synchronized
    The head bootstrap sets document.documentElement.style.colorScheme, but the toggle does not update it after a user switch. Update it whenever the theme changes and handle the fallback consistently.

  5. Add coverage and verify the full app
    Please add tests or an explicit browser/e2e check for saved dark/light preference and system fallback, storage failures, and toggle updates to data-theme, the dark class, and colorScheme. Verify the complete frontend build/lint/test and ensure the normal CI checks are present; the current PR only reports Cubic’s check.

Minor accessibility follow-up: add aria-pressed to the toggle so assistive technology receives the current state.

Please re-request review after these fixes, keeping the change scoped to issue #140.

@cubic-dev-ai cubic-dev-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.

2 issues found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/app/globals.css">

<violation number="1" location="frontend/app/globals.css:45">
P2: Dark-mode hover states now use light Tailwind colors: these exact-class selectors match only unprefixed utility tokens, so `hover:bg-*` and `hover:text-*` classes are no longer remapped and controls can flash light or near-black against the dark surface. Add explicit dark hover utilities in the affected components or exact escaped variant selectors in this stylesheet.</violation>
</file>

<file name="frontend/components/__tests__/ThemeToggle.test.tsx">

<violation number="1" location="frontend/components/__tests__/ThemeToggle.test.tsx:61">
P3: This test's title claims it verifies a "stable initial render" when the document is bootstrapped to dark, but it actually asserts the post-effect DOM state (☀️ Light / aria-pressed=true), which is the same resolved-dark state checked by the other tests. Because ThemeToggle starts with theme='light', its first paint is always '🌙 Dark' and only flips to dark after the mount effect; this test would still pass if the initial render flickered to light, so it gives false confidence about the theme-flash behavior it's named to protect against. Consider asserting the actual first-paint state (or resolving this by having the component derive the initial theme instead of starting at 'light') so the test matches its intent.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread frontend/app/globals.css
html.dark .bg-white,
html.dark .bg-slate-50,
html.dark .bg-slate-100,
html.dark .bg-gray-50,

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Dark-mode hover states now use light Tailwind colors: these exact-class selectors match only unprefixed utility tokens, so hover:bg-* and hover:text-* classes are no longer remapped and controls can flash light or near-black against the dark surface. Add explicit dark hover utilities in the affected components or exact escaped variant selectors in this stylesheet.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/app/globals.css, line 45:

<comment>Dark-mode hover states now use light Tailwind colors: these exact-class selectors match only unprefixed utility tokens, so `hover:bg-*` and `hover:text-*` classes are no longer remapped and controls can flash light or near-black against the dark surface. Add explicit dark hover utilities in the affected components or exact escaped variant selectors in this stylesheet.</comment>

<file context>
@@ -39,92 +39,64 @@ html.dark body {
+html.dark .bg-white,
+html.dark .bg-slate-50,
+html.dark .bg-slate-100,
+html.dark .bg-gray-50,
+html.dark .bg-gray-100 {
   background-color: var(--surface) !important;
</file context>
Fix with cubic

});
});

it('uses a stable initial render even when the document is already bootstrapped to dark mode', () => {

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This test's title claims it verifies a "stable initial render" when the document is bootstrapped to dark, but it actually asserts the post-effect DOM state (☀️ Light / aria-pressed=true), which is the same resolved-dark state checked by the other tests. Because ThemeToggle starts with theme='light', its first paint is always '🌙 Dark' and only flips to dark after the mount effect; this test would still pass if the initial render flickered to light, so it gives false confidence about the theme-flash behavior it's named to protect against. Consider asserting the actual first-paint state (or resolving this by having the component derive the initial theme instead of starting at 'light') so the test matches its intent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/components/__tests__/ThemeToggle.test.tsx, line 61:

<comment>This test's title claims it verifies a "stable initial render" when the document is bootstrapped to dark, but it actually asserts the post-effect DOM state (☀️ Light / aria-pressed=true), which is the same resolved-dark state checked by the other tests. Because ThemeToggle starts with theme='light', its first paint is always '🌙 Dark' and only flips to dark after the mount effect; this test would still pass if the initial render flickered to light, so it gives false confidence about the theme-flash behavior it's named to protect against. Consider asserting the actual first-paint state (or resolving this by having the component derive the initial theme instead of starting at 'light') so the test matches its intent.</comment>

<file context>
@@ -0,0 +1,100 @@
+    });
+  });
+
+  it('uses a stable initial render even when the document is already bootstrapped to dark mode', () => {
+    document.documentElement.setAttribute('data-theme', 'dark');
+    document.documentElement.classList.add('dark');
</file context>
Fix with cubic

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.

2 participants