Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8aa8f8e
feat(react-email): added a theme switcher to the dev preview (#1749)
KayleeWilliams Feb 17, 2025
2e94623
fix conflict resolution issues
gabrielmfern Oct 15, 2025
0e7ae4c
feat(demo): use Tailwind v4 on all templates (#2487)
bukinoshita Oct 16, 2025
0a7d210
fix: lockfile
gabrielmfern Oct 17, 2025
ddd37a4
feat(tailwind): update to tailwind v4 (#2425)
gabrielmfern Oct 17, 2025
19de23f
chore(demo): use local tailwind version
gabrielmfern Oct 17, 2025
9360e39
chore: remove preview version and add changeset
gabrielmfern Oct 17, 2025
26ee920
chore: update lockfile
gabrielmfern Oct 17, 2025
3c8938f
chore(root): version packages (canary) (#2578)
github-actions[bot] Oct 17, 2025
733dc39
feat(tailwind): test non-inlinable custom utilities (#2586)
gabrielmfern Oct 22, 2025
add4539
wip
gabrielmfern Oct 23, 2025
84b0727
first working version! keying the iframe with the theme
gabrielmfern Oct 23, 2025
358ac3a
add comment
gabrielmfern Oct 23, 2025
52643e5
lint
gabrielmfern Oct 23, 2025
09145b0
fix ts issue
gabrielmfern Oct 23, 2025
8783f0a
invert main body text color, and invert border, background
gabrielmfern Oct 23, 2025
4859be2
add missing darkmode applying
gabrielmfern Oct 23, 2025
42e9499
improve inversion function
gabrielmfern Oct 23, 2025
27a7100
add changeset
gabrielmfern Oct 23, 2025
829880f
Merge branch 'canary' into feat/dark-mode-switcher
gabrielmfern Oct 23, 2025
aad374b
fix acronyms
gabrielmfern Oct 23, 2025
0864ba8
add types for color
gabrielmfern Oct 23, 2025
5bc245d
add aria-labels, improve accessibility
gabrielmfern Oct 23, 2025
e01c086
preserve hash on theme change
gabrielmfern Oct 23, 2025
231ef55
improve hex regex
gabrielmfern Oct 23, 2025
3c0b355
Merge branch 'canary' into feat/dark-mode-switcher
gabrielmfern Oct 24, 2025
a6e487f
Merge branch 'canary' into feat/dark-mode-switcher
gabrielmfern Oct 24, 2025
15b4dbc
fix resizing not working
gabrielmfern Oct 24, 2025
7ae751b
improve code a bit
gabrielmfern Oct 24, 2025
e28bb90
reduce chroma by 20% when making colors dark
gabrielmfern Oct 24, 2025
fa3ff5b
avoid inversion being applied more than once, use a better color library
gabrielmfern Oct 24, 2025
cbf1c23
lint
gabrielmfern Oct 24, 2025
f151725
change the dark mode toggle group to just a toggle
gabrielmfern Oct 27, 2025
f98a84a
change moon icon
gabrielmfern Oct 27, 2025
7daccba
Revert "change moon icon"
gabrielmfern Oct 27, 2025
59accc2
Merge branch 'canary' into feat/dark-mode-switcher
gabrielmfern Oct 28, 2025
d2ebc56
lint
gabrielmfern Oct 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-needles-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-email": minor
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

This changeset bumps react-email, but the dark mode switcher work in this PR targets the preview server package. Without releasing @react-email/preview-server, the new feature will not ship to users. Please update the changeset to point at the preview server package.

Prompt for AI agents
Address the following comment on .changeset/dirty-needles-chew.md at line 2:

<comment>This changeset bumps `react-email`, but the dark mode switcher work in this PR targets the preview server package. Without releasing `@react-email/preview-server`, the new feature will not ship to users. Please update the changeset to point at the preview server package.</comment>

<file context>
@@ -0,0 +1,5 @@
+---
+&quot;react-email&quot;: minor
+---
+
</file context>

[internal] Confidence score: 6/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

---

Theme switcher for email template
6 changes: 6 additions & 0 deletions .changeset/flat-masks-take.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@react-email/preview-server": minor
"react-email": minor
---

Dark mode switcher emulating email client color inversion
3 changes: 3 additions & 0 deletions packages/preview-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toggle": "1.1.10",
"@radix-ui/react-toggle-group": "1.1.11",
"@radix-ui/react-tooltip": "1.2.8",
"@types/node": "22.14.1",
Expand All @@ -32,6 +33,7 @@
"@types/webpack": "5.28.5",
"autoprefixer": "10.4.21",
"clsx": "2.1.1",
"colorjs.io": "0.5.2",
"esbuild": "0.25.10",
"framer-motion": "12.23.22",
"json5": "2.2.3",
Expand Down Expand Up @@ -59,6 +61,7 @@
"@react-email/components": "workspace:*",
"@types/babel__core": "7.20.5",
"@types/babel__traverse": "7.20.7",
"@types/color": "4.2.0",
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

@types/color provides declarations for the color package, but this project imports colorjs.io, which already includes its own types. Keeping this mismatch adds an unused dependency and still leaves colorjs.io untyped. Please drop this line (or supply the correct module declarations).

Prompt for AI agents
Address the following comment on packages/preview-server/package.json at line 64:

<comment>`@types/color` provides declarations for the `color` package, but this project imports `colorjs.io`, which already includes its own types. Keeping this mismatch adds an unused dependency and still leaves `colorjs.io` untyped. Please drop this line (or supply the correct module declarations).</comment>

<file context>
@@ -59,6 +61,7 @@
     &quot;@react-email/components&quot;: &quot;workspace:*&quot;,
     &quot;@types/babel__core&quot;: &quot;7.20.5&quot;,
     &quot;@types/babel__traverse&quot;: &quot;7.20.7&quot;,
+    &quot;@types/color&quot;: &quot;4.2.0&quot;,
     &quot;@types/fs-extra&quot;: &quot;11.0.1&quot;,
     &quot;@types/mime-types&quot;: &quot;2.1.4&quot;,
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link

@cubic-dev-ai cubic-dev-ai bot Nov 7, 2025

Choose a reason for hiding this comment

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

@types/color provides typings for the color package, but the new code imports Color from colorjs.io. This mismatch leaves colorjs.io without typings and makes the new dependency misleading; please remove it or add the correct type declarations for colorjs.io.

    DEV MODE: This violation would have been filtered out by GPT-5.

Reasoning:
GPT-5: colorjs.io already ships official TypeScript declarations (for example, the package’s published types/ directory contains numerous .d.ts files such as index.d.ts and color.d.ts, e.g. https://www.bolt.magwiji.com/node_modules/colorjs.io/types/src/). Because the dependency itself includes typings, adding @types/color neither fixes nor relates to the new colorjs.io import. The reported issue that colorjs.io is left without typings is therefore incorrect.

Exa queries: "colorjs.io TypeScript definitions package 0.5.2", ""colorjs.io" TypeScript definitions", ""colorjs.io" "index.d.ts""

Prompt for AI agents
Address the following comment on packages/preview-server/package.json at line 64:

<comment>`@types/color` provides typings for the `color` package, but the new code imports `Color` from `colorjs.io`. This mismatch leaves `colorjs.io` without typings and makes the new dependency misleading; please remove it or add the correct type declarations for `colorjs.io`.

        DEV MODE: This violation would have been filtered out by GPT-5.
Reasoning:
• **GPT-5**: `colorjs.io` already ships official TypeScript declarations (for example, the package’s published `types/` directory contains numerous `.d.ts` files such as `index.d.ts` and `color.d.ts`, e.g. https://www.bolt.magwiji.com/node_modules/colorjs.io/types/src/). Because the dependency itself includes typings, adding `@types/color` neither fixes nor relates to the new `colorjs.io` import. The reported issue that `colorjs.io` is left without typings is therefore incorrect.

• **Exa queries**: &quot;colorjs.io TypeScript definitions package 0.5.2&quot;, &quot;&quot;colorjs.io&quot; TypeScript definitions&quot;, &quot;&quot;colorjs.io&quot; &quot;index.d.ts&quot;&quot;</comment>

<file context>
@@ -59,6 +61,7 @@
     &quot;@react-email/components&quot;: &quot;workspace:*&quot;,
     &quot;@types/babel__core&quot;: &quot;7.20.5&quot;,
     &quot;@types/babel__traverse&quot;: &quot;7.20.7&quot;,
+    &quot;@types/color&quot;: &quot;4.2.0&quot;,
     &quot;@types/fs-extra&quot;: &quot;11.0.1&quot;,
     &quot;@types/mime-types&quot;: &quot;2.1.4&quot;,
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

Incorrect TypeScript types are installed for the new colorjs.io dependency. The package.json adds types for the color package, but the implementation uses colorjs.io. This will lead to type errors and developer confusion as the types do not match the library being used.

Prompt for AI agents
Address the following comment on packages/preview-server/package.json at line 64:

<comment>Incorrect TypeScript types are installed for the new `colorjs.io` dependency. The `package.json` adds types for the `color` package, but the implementation uses `colorjs.io`. This will lead to type errors and developer confusion as the types do not match the library being used.</comment>

<file context>
@@ -59,6 +61,7 @@
     &quot;@react-email/components&quot;: &quot;workspace:*&quot;,
     &quot;@types/babel__core&quot;: &quot;7.20.5&quot;,
     &quot;@types/babel__traverse&quot;: &quot;7.20.7&quot;,
+    &quot;@types/color&quot;: &quot;4.2.0&quot;,
     &quot;@types/fs-extra&quot;: &quot;11.0.1&quot;,
     &quot;@types/mime-types&quot;: &quot;2.1.4&quot;,
</file context>

[internal] Confidence score: 10/10

[internal] Posted by: System Design Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

@types/color provides type definitions for the color package, but the codebase imports Color from colorjs.io, which already ships its own TypeScript definitions. Keeping this mismatched types package adds an unused dependency and can cause confusion; please remove it.

Prompt for AI agents
Address the following comment on packages/preview-server/package.json at line 64:

<comment>`@types/color` provides type definitions for the `color` package, but the codebase imports `Color` from `colorjs.io`, which already ships its own TypeScript definitions. Keeping this mismatched types package adds an unused dependency and can cause confusion; please remove it.</comment>

<file context>
@@ -59,6 +61,7 @@
     &quot;@react-email/components&quot;: &quot;workspace:*&quot;,
     &quot;@types/babel__core&quot;: &quot;7.20.5&quot;,
     &quot;@types/babel__traverse&quot;: &quot;7.20.7&quot;,
+    &quot;@types/color&quot;: &quot;4.2.0&quot;,
     &quot;@types/fs-extra&quot;: &quot;11.0.1&quot;,
     &quot;@types/mime-types&quot;: &quot;2.1.4&quot;,
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

@types/color provides typings for the legacy color package, but this project depends on colorjs.io. Because the runtime import is colorjs.io, this added type package is unused and misleading; please remove it or replace it with typings for colorjs.io.

Prompt for AI agents
Address the following comment on packages/preview-server/package.json at line 64:

<comment>`@types/color` provides typings for the legacy `color` package, but this project depends on `colorjs.io`. Because the runtime import is `colorjs.io`, this added type package is unused and misleading; please remove it or replace it with typings for `colorjs.io`.</comment>

<file context>
@@ -59,6 +61,7 @@
     &quot;@react-email/components&quot;: &quot;workspace:*&quot;,
     &quot;@types/babel__core&quot;: &quot;7.20.5&quot;,
     &quot;@types/babel__traverse&quot;: &quot;7.20.7&quot;,
+    &quot;@types/color&quot;: &quot;4.2.0&quot;,
     &quot;@types/fs-extra&quot;: &quot;11.0.1&quot;,
     &quot;@types/mime-types&quot;: &quot;2.1.4&quot;,
</file context>

[internal] Confidence score: 7/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

@types/color provides typings for the color package, but the new code imports colorjs.io. This dependency does not match any usage and should be removed or replaced with the correct typings to avoid a dead/misleading dependency.

Prompt for AI agents
Address the following comment on packages/preview-server/package.json at line 64:

<comment>`@types/color` provides typings for the `color` package, but the new code imports `colorjs.io`. This dependency does not match any usage and should be removed or replaced with the correct typings to avoid a dead/misleading dependency.</comment>

<file context>
@@ -59,6 +61,7 @@
     &quot;@react-email/components&quot;: &quot;workspace:*&quot;,
     &quot;@types/babel__core&quot;: &quot;7.20.5&quot;,
     &quot;@types/babel__traverse&quot;: &quot;7.20.7&quot;,
+    &quot;@types/color&quot;: &quot;4.2.0&quot;,
     &quot;@types/fs-extra&quot;: &quot;11.0.1&quot;,
     &quot;@types/mime-types&quot;: &quot;2.1.4&quot;,
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

"@types/fs-extra": "11.0.1",
"@types/mime-types": "2.1.4",
"@types/node": "22.10.2",
Expand Down
138 changes: 138 additions & 0 deletions packages/preview-server/src/app/preview/[...slug]/email-frame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Slot } from '@radix-ui/react-slot';
import Color from 'colorjs.io';
import type { ComponentProps } from 'react';

function* walkDom(element: Element): Generator<Element> {
if (element.children.length > 0) {
for (let i = 0; i < element.children.length; i++) {
const child = element.children.item(i)!;
yield child;
yield* walkDom(child);
}
}
}

function invertColor(colorString: string, mode: 'foreground' | 'background') {
const color = new Color(colorString).to('lch');

if (mode === 'foreground') {
if (color.lch.l! < 50) {
color.lch.l = 100 - color.lch.l! * 0.75;
}
} else if (mode === 'background') {
if (color.lch.l! >= 50) {
color.lch.l = 100 - color.lch.l! * 0.75;
}
}

color.lch.c! *= 0.8;

return color.toString();
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

invertColor emits LCH color strings that Firefox treats as invalid, so dark-mode styles break

    DEV MODE: This violation would have been filtered out by GPT-5.

Reasoning:
GPT-5: The violation claims that Firefox treats LCH color strings as invalid, breaking dark-mode styles. However, documentation confirms that Firefox has supported CSS lch() and oklch() color functions since version 113 (released in 2023). Given the current date (2025), all modern Firefox versions support these functions. The Color.js library's toString() method outputs valid CSS lch() strings when in the 'lch' space, which are parsable by supporting browsers. The repro step assumes a current Firefox, where the styles should apply correctly. This makes the violation factually incorrect for current browser versions. Although email clients may have limited support, the preview server renders in a browser iframe, so browser support is relevant. Filter out due to technical inaccuracy.

Exa queries: "colorjs.io Color.toString() output when color space is lch latest version", "CSS lch() and oklch() color functions browser support Firefox latest version", "caniuse CSS lch() oklch() color functions Firefox support", "caniuse CSS lch() color function browser support Firefox"

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/email-frame.tsx at line 30:

<comment>invertColor emits LCH color strings that Firefox treats as invalid, so dark-mode styles break

        DEV MODE: This violation would have been filtered out by GPT-5.
Reasoning:
• **GPT-5**: The violation claims that Firefox treats LCH color strings as invalid, breaking dark-mode styles. However, documentation confirms that Firefox has supported CSS lch() and oklch() color functions since version 113 (released in 2023). Given the current date (2025), all modern Firefox versions support these functions. The Color.js library&#39;s toString() method outputs valid CSS lch() strings when in the &#39;lch&#39; space, which are parsable by supporting browsers. The repro step assumes a current Firefox, where the styles should apply correctly. This makes the violation factually incorrect for current browser versions. Although email clients may have limited support, the preview server renders in a browser iframe, so browser support is relevant. Filter out due to technical inaccuracy.

• **Exa queries**: &quot;colorjs.io Color.toString() output when color space is lch latest version&quot;, &quot;CSS lch() and oklch() color functions browser support Firefox latest version&quot;, &quot;caniuse CSS lch() oklch() color functions Firefox support&quot;, &quot;caniuse CSS lch() color function browser support Firefox&quot;</comment>

<file context>
@@ -0,0 +1,138 @@
+
+  color.lch.c! *= 0.8;
+
+  return color.toString();
+}
+
</file context>

[internal] Confidence score: 7/10

[internal] Posted by: Functional Bugs Agent

Fix with Cubic

}

const colorRegex =
/#[0-9a-fA-F]{3,4}|#[0-9a-fA-F]{6,8}|rgba?\(.*?\)|hsl\(.*?\)|hsv\(.*?\)|oklab\(.*?\)|oklch\(.*?\)/g;

function applyColorInversion(iframe: HTMLIFrameElement) {
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

The color inversion logic for dark mode emulation only targets inline styles, completely ignoring CSS rules defined in <style> blocks. Since other packages in the project, like the Tailwind integration, generate non-inline styles, this will lead to an inconsistent and partially applied dark mode.

Exa queries: "React key prop behavior when changed dynamic value unmount remount"

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/email-frame.tsx at line 36:

<comment>The color inversion logic for dark mode emulation only targets inline styles, completely ignoring CSS rules defined in `&lt;style&gt;` blocks. Since other packages in the project, like the Tailwind integration, generate non-inline styles, this will lead to an inconsistent and partially applied dark mode.

• **Exa queries**: &quot;React key prop behavior when changed dynamic value unmount remount&quot;</comment>

<file context>
@@ -0,0 +1,138 @@
+const colorRegex =
+  /#[0-9a-fA-F]{3,4}|#[0-9a-fA-F]{6,8}|rgba?\(.*?\)|hsl\(.*?\)|hsv\(.*?\)|oklab\(.*?\)|oklch\(.*?\)/g;
+
+function applyColorInversion(iframe: HTMLIFrameElement) {
+  const { contentDocument, contentWindow } = iframe;
+  if (!contentDocument || !contentWindow) return;
</file context>

[internal] Confidence score: 10/10

[internal] Posted by: System Design Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

The dark mode emulation only inverts colors from inline styles, ignoring styles defined in <style> blocks. Since many email components in this repository use <style> blocks for styling (e.g., for responsiveness), this will result in a broken user experience where only parts of the email are inverted, creating an inconsistent and unusable preview.

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/email-frame.tsx at line 36:

<comment>The dark mode emulation only inverts colors from inline styles, ignoring styles defined in `&lt;style&gt;` blocks. Since many email components in this repository use `&lt;style&gt;` blocks for styling (e.g., for responsiveness), this will result in a broken user experience where only parts of the email are inverted, creating an inconsistent and unusable preview.</comment>

<file context>
@@ -0,0 +1,138 @@
+const colorRegex =
+  /#[0-9a-fA-F]{3,4}|#[0-9a-fA-F]{6,8}|rgba?\(.*?\)|hsl\(.*?\)|hsv\(.*?\)|oklab\(.*?\)|oklch\(.*?\)/g;
+
+function applyColorInversion(iframe: HTMLIFrameElement) {
+  const { contentDocument, contentWindow } = iframe;
+  if (!contentDocument || !contentWindow) return;
</file context>

[internal] Confidence score: 10/10

[internal] Posted by: System Design Agent

Fix with Cubic

const { contentDocument, contentWindow } = iframe;
if (!contentDocument || !contentWindow) return;

if (contentDocument.body.hasAttribute('inverted-colors')) return;
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

The color inversion logic does not account for emails that already have a dark mode implementation using standard methods like @media (prefers-color-scheme: dark). This will cause rendering issues, such as double-inverting colors back to light, on well-authored email templates that provide their own dark theme.

    DEV MODE: This violation would have been filtered out by GPT-5.

Reasoning:
GPT-5: Although the inversion runs unconditionally when darkMode is enabled, the toggle never switches the iframe’s prefers-color-scheme, so templates that provide their own dark-mode styles will not automatically apply them here. The claim of “double inversion” lacks supporting evidence and contradicts the actual flow, so this is too speculative to report.

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/email-frame.tsx at line 40:

<comment>The color inversion logic does not account for emails that already have a dark mode implementation using standard methods like `@media (prefers-color-scheme: dark)`. This will cause rendering issues, such as double-inverting colors back to light, on well-authored email templates that provide their own dark theme.

        DEV MODE: This violation would have been filtered out by GPT-5.
Reasoning:
• **GPT-5**: Although the inversion runs unconditionally when darkMode is enabled, the toggle never switches the iframe’s prefers-color-scheme, so templates that provide their own dark-mode styles will not automatically apply them here. The claim of “double inversion” lacks supporting evidence and contradicts the actual flow, so this is too speculative to report.</comment>

<file context>
@@ -0,0 +1,138 @@
+  const { contentDocument, contentWindow } = iframe;
+  if (!contentDocument || !contentWindow) return;
+
+  if (contentDocument.body.hasAttribute(&#39;inverted-colors&#39;)) return;
+
+  contentDocument.body.setAttribute(&#39;inverted-colors&#39;, &#39;&#39;);
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: System Design Agent

Fix with Cubic


contentDocument.body.setAttribute('inverted-colors', '');

if (!contentDocument.body.style.color) {
contentDocument.body.style.color = 'rgb(0, 0, 0)';
}

for (const element of walkDom(contentDocument.documentElement)) {
if (
element instanceof
(contentWindow as unknown as typeof globalThis).HTMLElement
) {
if (element.style.color) {
element.style.color = element.style.color.replaceAll(
colorRegex,
(color) => invertColor(color, 'foreground'),
);
colorRegex.lastIndex = 0;
}
if (element.style.background) {
element.style.background = element.style.background.replaceAll(
colorRegex,
(color) => invertColor(color, 'background'),
);
colorRegex.lastIndex = 0;
}
if (element.style.backgroundColor) {
element.style.backgroundColor =
element.style.backgroundColor.replaceAll(colorRegex, (color) =>
invertColor(color, 'background'),
);
colorRegex.lastIndex = 0;
}
if (element.style.borderColor) {
element.style.borderColor = element.style.borderColor.replaceAll(
colorRegex,
(color) => invertColor(color, 'background'),
);
colorRegex.lastIndex = 0;
}
if (element.style.border) {
element.style.border = element.style.border.replaceAll(
colorRegex,
(color) => invertColor(color, 'background'),
);
colorRegex.lastIndex = 0;
}
}
}
}

interface EmailFrameProps extends ComponentProps<'iframe'> {
markup: string;
width: number;
height: number;
darkMode: boolean;
}

export function EmailFrame({
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

EmailFrame is declared as a plain function component, so React will not forward refs to it. preview.tsx still renders <EmailFrame ref={...}> to wire makeIframeDocumentBubbleEvents, which now fails with the "Function components cannot be given refs" warning and breaks the bubbling logic. Wrap the component in React.forwardRef (or otherwise forward the iframe ref) so the parent ref keeps working.

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/email-frame.tsx at line 99:

<comment>`EmailFrame` is declared as a plain function component, so React will not forward refs to it. `preview.tsx` still renders `&lt;EmailFrame ref={...}&gt;` to wire `makeIframeDocumentBubbleEvents`, which now fails with the &quot;Function components cannot be given refs&quot; warning and breaks the bubbling logic. Wrap the component in `React.forwardRef` (or otherwise forward the iframe ref) so the parent ref keeps working.</comment>

<file context>
@@ -0,0 +1,138 @@
+  darkMode: boolean;
+}
+
+export function EmailFrame({
+  markup,
+  width,
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

markup,
width,
height,
darkMode,
...rest
}: EmailFrameProps) {
return (
<Slot
ref={(iframe: HTMLIFrameElement) => {
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

The callback ref adds a load listener but relies on returning a cleanup function to remove it. React ignores return values from callback refs, so the listener is never removed; on re-renders new listeners accumulate and applyColorInversion fires multiple times. Move cleanup to the iframe === null branch or manage it with an effect so the listener is properly detached.

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/email-frame.tsx at line 108:

<comment>The callback ref adds a `load` listener but relies on returning a cleanup function to remove it. React ignores return values from callback refs, so the listener is never removed; on re-renders new listeners accumulate and `applyColorInversion` fires multiple times. Move cleanup to the `iframe === null` branch or manage it with an effect so the listener is properly detached.</comment>

<file context>
@@ -0,0 +1,138 @@
+}: EmailFrameProps) {
+  return (
+    &lt;Slot
+      ref={(iframe: HTMLIFrameElement) =&gt; {
+        if (!iframe) return;
+
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

if (!iframe) return;

if (darkMode) {
applyColorInversion(iframe);
}

const handleLoad = () => {
if (darkMode) {
applyColorInversion(iframe);
}
};

iframe.addEventListener('load', handleLoad);
return () => {
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

React ignores the value returned from a ref callback, so this cleanup never runs. Because this inline ref is recreated each render, the old listener stays attached and a new one is added, leading to accumulating load handlers on the same iframe. Please move the add/remove logic into an effect or handle the null ref case explicitly to detach the previous listener.

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/email-frame.tsx at line 122:

<comment>React ignores the value returned from a ref callback, so this cleanup never runs. Because this inline ref is recreated each render, the old listener stays attached and a new one is added, leading to accumulating `load` handlers on the same iframe. Please move the add/remove logic into an effect or handle the null ref case explicitly to detach the previous listener.</comment>

<file context>
@@ -0,0 +1,138 @@
+        };
+
+        iframe.addEventListener(&#39;load&#39;, handleLoad);
+        return () =&gt; {
+          iframe.removeEventListener(&#39;load&#39;, handleLoad);
+        };
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

iframe.removeEventListener('load', handleLoad);
};
}}
>
<iframe
srcDoc={markup}
width={width}
height={height}
{...rest}
// This key makes sure that the iframe itself remounts to the DOM when theme changes, so
// that the color changes in dark mode can be easily undone when switching to light mode.
key={darkMode ? 'iframe-inverted-colors' : 'iframe-normal-colors'}
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

Using a dynamic key on the <iframe> to toggle dark mode forces a full remount of the component on every theme change. This is an inefficient architectural choice that leads to a poor user experience, including content flicker and loss of scroll position.

    DEV MODE: This violation would have been filtered out by GPT-5.

Reasoning:
GPT-5: Although changing the key causes the iframe to remount, this is an intentional design choice to easily reset the color inversions when switching themes, as noted in the code comment. While it may cause minor UX issues like brief flicker or loss of scroll position, the impact is low for short email previews, and the code is functional. React documentation confirms key changes trigger remount, but optimization is not critical here.

Exa queries: "React key prop behavior when changed dynamic value unmount remount"

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/email-frame.tsx at line 134:

<comment>Using a dynamic `key` on the `&lt;iframe&gt;` to toggle dark mode forces a full remount of the component on every theme change. This is an inefficient architectural choice that leads to a poor user experience, including content flicker and loss of scroll position.

        DEV MODE: This violation would have been filtered out by GPT-5.
Reasoning:
• **GPT-5**: Although changing the key causes the iframe to remount, this is an intentional design choice to easily reset the color inversions when switching themes, as noted in the code comment. While it may cause minor UX issues like brief flicker or loss of scroll position, the impact is low for short email previews, and the code is functional. React documentation confirms key changes trigger remount, but optimization is not critical here.

• **Exa queries**: &quot;React key prop behavior when changed dynamic value unmount remount&quot;</comment>

<file context>
@@ -0,0 +1,138 @@
+        {...rest}
+        // This key makes sure that the iframe itself remounts to the DOM when theme changes, so
+        // that the color changes in dark mode can be easily undone when switching to light mode.
+        key={darkMode ? &#39;iframe-inverted-colors&#39; : &#39;iframe-normal-colors&#39;}
+      /&gt;
+    &lt;/Slot&gt;
</file context>

[internal] Confidence score: 10/10

[internal] Posted by: System Design Agent

Fix with Cubic

/>
</Slot>
);
}
78 changes: 48 additions & 30 deletions packages/preview-server/src/app/preview/[...slug]/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import { Send } from '../../../components/send';
import { useToolbarState } from '../../../components/toolbar';
import { Tooltip } from '../../../components/tooltip';
import { ActiveViewToggleGroup } from '../../../components/topbar/active-view-toggle-group';
import { EmulatedDarkModeToggle } from '../../../components/topbar/emulated-dark-mode-toggle';
import { ViewSizeControls } from '../../../components/topbar/view-size-controls';
import { usePreviewContext } from '../../../contexts/preview';
import { useClampedState } from '../../../hooks/use-clamped-state';
import { cn } from '../../../utils';
import { EmailFrame } from './email-frame';
import { ErrorOverlay } from './error-overlay';

interface PreviewProps extends React.ComponentProps<'div'> {
Expand All @@ -32,9 +34,20 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
const pathname = usePathname();
const searchParams = useSearchParams();

const isDarkModeEnabled = searchParams.get('dark') !== null;
const activeView = searchParams.get('view') ?? 'preview';
const activeLang = searchParams.get('lang') ?? 'jsx';

const handleDarkModeChange = (enabled: boolean) => {
const params = new URLSearchParams(searchParams);
if (enabled) {
params.set('dark', '');
} else {
params.delete('dark');
}
router.push(`${pathname}?${params.toString()}${location.hash}`);
};

const handleViewChange = (view: string) => {
const params = new URLSearchParams(searchParams);
params.set('view', view);
Expand Down Expand Up @@ -83,26 +96,32 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
return (
<>
<Topbar emailTitle={emailTitle}>
{activeView === 'preview' && (
<ViewSizeControls
setViewHeight={(height) => {
setHeight(height);
flushSync(() => {
handleSaveViewSize();
});
}}
setViewWidth={(width) => {
setWidth(width);
flushSync(() => {
handleSaveViewSize();
});
}}
viewHeight={height}
viewWidth={width}
minWidth={minWidth}
minHeight={minHeight}
/>
)}
{activeView === 'preview' ? (
<>
<ViewSizeControls
setViewHeight={(height) => {
setHeight(height);
flushSync(() => {
handleSaveViewSize();
});
}}
setViewWidth={(width) => {
setWidth(width);
flushSync(() => {
handleSaveViewSize();
});
}}
viewHeight={height}
viewWidth={width}
minWidth={minWidth}
minHeight={minHeight}
/>
<EmulatedDarkModeToggle
enabled={isDarkModeEnabled}
onChange={(enabled) => handleDarkModeChange(enabled)}
/>
</>
) : null}
<ActiveViewToggleGroup
activeView={activeView}
setActiveView={handleViewChange}
Expand Down Expand Up @@ -165,19 +184,18 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => {
}}
width={width}
>
<iframe
<EmailFrame
Copy link

@cubic-dev-ai cubic-dev-ai bot Nov 7, 2025

Choose a reason for hiding this comment

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

EmailFrame does not forward refs, so makeIframeDocumentBubbleEvents is no longer attached and resizing breaks when the cursor enters the iframe.

Exa queries: "React function components ref without forwardRef does ref work latest version", "react docs function components cannot be given refs forwardRef warning", "react forwardRef docs function components cannot be given refs warning"

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/preview.tsx at line 187:

<comment>EmailFrame does not forward refs, so makeIframeDocumentBubbleEvents is no longer attached and resizing breaks when the cursor enters the iframe.

• **Exa queries**: &quot;React function components ref without forwardRef does ref work latest version&quot;, &quot;react docs function components cannot be given refs forwardRef warning&quot;, &quot;react forwardRef docs function components cannot be given refs warning&quot;</comment>

<file context>
@@ -165,19 +184,18 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) =&gt; {
                 width={width}
               &gt;
-                &lt;iframe
+                &lt;EmailFrame
                   className=&quot;max-h-full rounded-lg bg-white [color-scheme:auto]&quot;
+                  darkMode={isDarkModeEnabled}
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: Functional Bugs Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

EmailFrame is a plain function component, so the ref you pass here never reaches the underlying <iframe>. As a result makeIframeDocumentBubbleEvents no longer runs and iframe events stop bubbling to the shell. Please forward the ref inside EmailFrame (e.g. with React.forwardRef).

Prompt for AI agents
Address the following comment on packages/preview-server/src/app/preview/[...slug]/preview.tsx at line 187:

<comment>`EmailFrame` is a plain function component, so the `ref` you pass here never reaches the underlying `&lt;iframe&gt;`. As a result `makeIframeDocumentBubbleEvents` no longer runs and iframe events stop bubbling to the shell. Please forward the ref inside `EmailFrame` (e.g. with `React.forwardRef`).</comment>

<file context>
@@ -165,19 +184,18 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) =&gt; {
                 width={width}
               &gt;
-                &lt;iframe
+                &lt;EmailFrame
                   className=&quot;max-h-full rounded-lg bg-white [color-scheme:auto]&quot;
+                  darkMode={isDarkModeEnabled}
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

className="max-h-full rounded-lg bg-white [color-scheme:auto]"
darkMode={isDarkModeEnabled}
markup={renderedEmailMetadata.markup}
width={width}
height={height}
title={emailTitle}
ref={(iframe) => {
if (iframe) {
return makeIframeDocumentBubbleEvents(iframe);
}
}}
srcDoc={renderedEmailMetadata.markup}
style={{
width: `${width}px`,
height: `${height}px`,
if (!iframe) return;

return makeIframeDocumentBubbleEvents(iframe);
}}
title={emailTitle}
/>
</ResizableWrapper>
)}
Expand Down
16 changes: 16 additions & 0 deletions packages/preview-server/src/components/icons/icon-moon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import type { IconElement, IconProps } from './icon-base';
import { IconBase } from './icon-base';

export const IconMoon = React.forwardRef<IconElement, Readonly<IconProps>>(
({ ...props }, forwardedRef) => (
<IconBase ref={forwardedRef} {...props}>
<path
fill="currentColor"
d="m17.75 4.09l-2.53 1.94l.91 3.06l-2.63-1.81l-2.63 1.81l.91-3.06l-2.53-1.94L12.44 4l1.06-3l1.06 3zm3.5 6.91l-1.64 1.25l.59 1.98l-1.7-1.17l-1.7 1.17l.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85c-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14c.4-.4.82-.76 1.27-1.08c.75-.53 1.93.36 1.85 1.19c-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82c-2.81 3.14-2.7 7.96.31 10.98c3.02 3.01 7.84 3.12 10.98.31"
/>
</IconBase>
),
);

IconMoon.displayName = 'IconMoon';
16 changes: 16 additions & 0 deletions packages/preview-server/src/components/icons/icon-sun.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import type { IconElement, IconProps } from './icon-base';
import { IconBase } from './icon-base';

export const IconSun = React.forwardRef<IconElement, Readonly<IconProps>>(
({ ...props }, forwardedRef) => (
<IconBase ref={forwardedRef} {...props}>
<path
fill="currentColor"
d="m3.55 19.09l1.41 1.41l1.8-1.79l-1.42-1.42M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6c0-3.32-2.69-6-6-6m8 7h3v-2h-3m-2.76 7.71l1.8 1.79l1.41-1.41l-1.79-1.8M20.45 5l-1.41-1.4l-1.8 1.79l1.42 1.42M13 1h-2v3h2M6.76 5.39L4.96 3.6L3.55 5l1.79 1.81zM1 13h3v-2H1m12 9h-2v3h2"
/>
</IconBase>
),
);

IconSun.displayName = 'IconSun';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as Toggle from '@radix-ui/react-toggle';
import { cn } from '../../utils';
import { IconMoon } from '../icons/icon-moon';
import { Tooltip } from '../tooltip';

interface EmulatedDarkModeToggleProps {
enabled: boolean;
onChange: (enabled: boolean) => unknown;
}

export const EmulatedDarkModeToggle = ({
enabled,
onChange,
}: EmulatedDarkModeToggleProps) => {
return (
<Tooltip>
<Tooltip.Trigger asChild>
<Toggle.Root
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

Toggle.Root never receives the controlled pressed state, so its internal state drifts out of sync with the enabled prop—screen readers will hear the opposite of the selected theme. Pass pressed={enabled} (and optionally switch to onPressedChange) so the toggle mirrors the actual dark-mode state.

Exa queries: "Radix UI React Toggle component API documentation pressed prop onPressedChange accessibility aria-pressed"

Prompt for AI agents
Address the following comment on packages/preview-server/src/components/topbar/emulated-dark-mode-toggle.tsx at line 18:

<comment>`Toggle.Root` never receives the controlled `pressed` state, so its internal state drifts out of sync with the `enabled` prop—screen readers will hear the opposite of the selected theme. Pass `pressed={enabled}` (and optionally switch to `onPressedChange`) so the toggle mirrors the actual dark-mode state.

• **Exa queries**: &quot;Radix UI React Toggle component API documentation pressed prop onPressedChange accessibility aria-pressed&quot;</comment>

<file context>
@@ -0,0 +1,38 @@
+  return (
+    &lt;Tooltip&gt;
+      &lt;Tooltip.Trigger asChild&gt;
+        &lt;Toggle.Root
+          value=&quot;dark&quot;
+          className={cn(
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

This toggle renders only an icon, so it needs an accessible label. Add an aria-label (or similar) on <Toggle.Root> so screen readers can announce what the button does.

Prompt for AI agents
Address the following comment on packages/preview-server/src/components/topbar/emulated-dark-mode-toggle.tsx at line 18:

<comment>This toggle renders only an icon, so it needs an accessible label. Add an `aria-label` (or similar) on `&lt;Toggle.Root&gt;` so screen readers can announce what the button does.</comment>

<file context>
@@ -0,0 +1,38 @@
+  return (
+    &lt;Tooltip&gt;
+      &lt;Tooltip.Trigger asChild&gt;
+        &lt;Toggle.Root
+          value=&quot;dark&quot;
+          className={cn(
</file context>

[internal] Confidence score: 9/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

Please bind the Radix toggle’s pressed state to the enabled prop so the rendered aria-pressed reflects the actual theme selection; otherwise screen readers will report the wrong state when the component initializes with dark mode enabled.

Exa queries: "radix ui react toggle pressed state controlled"

Prompt for AI agents
Address the following comment on packages/preview-server/src/components/topbar/emulated-dark-mode-toggle.tsx at line 18:

<comment>Please bind the Radix toggle’s `pressed` state to the `enabled` prop so the rendered `aria-pressed` reflects the actual theme selection; otherwise screen readers will report the wrong state when the component initializes with dark mode enabled.

• **Exa queries**: &quot;radix ui react toggle pressed state controlled&quot;</comment>

<file context>
@@ -0,0 +1,38 @@
+  return (
+    &lt;Tooltip&gt;
+      &lt;Tooltip.Trigger asChild&gt;
+        &lt;Toggle.Root
+          value=&quot;dark&quot;
+          className={cn(
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

Bind the Radix toggle to the controlled enabled state using pressed={enabled} with onPressedChange={onChange} instead of relying on onClick. Without this, aria-pressed and data-state stay off when the parent sets dark mode programmatically, so the button advertises the wrong state to assistive tech.

Prompt for AI agents
Address the following comment on packages/preview-server/src/components/topbar/emulated-dark-mode-toggle.tsx at line 18:

<comment>Bind the Radix toggle to the controlled `enabled` state using `pressed={enabled}` with `onPressedChange={onChange}` instead of relying on `onClick`. Without this, `aria-pressed` and `data-state` stay `off` when the parent sets dark mode programmatically, so the button advertises the wrong state to assistive tech.</comment>

<file context>
@@ -0,0 +1,38 @@
+  return (
+    &lt;Tooltip&gt;
+      &lt;Tooltip.Trigger asChild&gt;
+        &lt;Toggle.Root
+          value=&quot;dark&quot;
+          className={cn(
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

value="dark"
className={cn(
'relative w-9 h-9 flex items-center justify-center border border-slate-6 text-sm rounded-lg transition duration-200 ease-in-out hover:text-slate-12',
{
'text-slate-11': !enabled,
'text-slate-12 bg-slate-4': enabled,
},
)}
onClick={() => onChange(!enabled)}
Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

<Toggle.Root> is treated as a controlled toggle (the enabled prop drives styling), but the component never binds the Radix pressed state. When enabled is set externally (e.g., on initial load), the toggle's internal state and aria-pressed stay false, so assistive tech sees the wrong state. Bind pressed={enabled} and use onPressedChange={onChange} instead of the onClick handler.

Prompt for AI agents
Address the following comment on packages/preview-server/src/components/topbar/emulated-dark-mode-toggle.tsx at line 27:

<comment>`&lt;Toggle.Root&gt;` is treated as a controlled toggle (the `enabled` prop drives styling), but the component never binds the Radix `pressed` state. When `enabled` is set externally (e.g., on initial load), the toggle&#39;s internal state and `aria-pressed` stay `false`, so assistive tech sees the wrong state. Bind `pressed={enabled}` and use `onPressedChange={onChange}` instead of the `onClick` handler.</comment>

<file context>
@@ -0,0 +1,38 @@
+              &#39;text-slate-12 bg-slate-4&#39;: enabled,
+            },
+          )}
+          onClick={() =&gt; onChange(!enabled)}
+        &gt;
+          &lt;IconMoon /&gt;
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

Copy link
Author

@cubic-dev-local cubic-dev-local bot Nov 7, 2025

Choose a reason for hiding this comment

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

Toggle.Root should be controlled with pressed={enabled} and onPressedChange={onChange}; relying on onClick leaves Radix’s internal aria-pressed state out of sync with the theme prop, breaking assistive technology expectations.

Prompt for AI agents
Address the following comment on packages/preview-server/src/components/topbar/emulated-dark-mode-toggle.tsx at line 27:

<comment>`Toggle.Root` should be controlled with `pressed={enabled}` and `onPressedChange={onChange}`; relying on `onClick` leaves Radix’s internal `aria-pressed` state out of sync with the theme prop, breaking assistive technology expectations.</comment>

<file context>
@@ -0,0 +1,38 @@
+              &#39;text-slate-12 bg-slate-4&#39;: enabled,
+            },
+          )}
+          onClick={() =&gt; onChange(!enabled)}
+        &gt;
+          &lt;IconMoon /&gt;
</file context>

[internal] Confidence score: 8/10

[internal] Posted by: General AI Review Agent

Fix with Cubic

>
<IconMoon />
</Toggle.Root>
</Tooltip.Trigger>
<Tooltip.Content>
When enabled, inverts colors in the preview emulating what email clients
do in dark mode.
</Tooltip.Content>
</Tooltip>
);
};
Loading