Skip to content

Commit 9ee4817

Browse files
committed
feat: re/org theme code and fix dark/light mode flickering
1 parent d3af458 commit 9ee4817

File tree

5 files changed

+67
-18
lines changed

5 files changed

+67
-18
lines changed

apps/captable/app/layout.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import "@/styles/globals.css";
33
import { cn } from "@/lib/utils";
44
import { robotoMono, satoshi } from "@/styles/fonts";
55
import { ProgressBarProvider } from "@/providers/progress-bar";
6-
import { ThemeProvider } from "@/providers/theme-provider";
6+
import { ThemeProvider, ThemeToggle } from "@/components/theme";
77
import { TRPCProvider } from "@/providers/trpc-provider";
88
import { PublicEnvScript } from "@/components/public-env-script";
9-
import { ThemeToggle } from "@/components/theme-toggle";
109
import { Toaster } from "sonner";
1110
import logo from "@/assets/logo.svg";
1211
import { META } from "@captable/utils/constants";
@@ -56,6 +55,35 @@ export default function RootLayout({
5655
suppressHydrationWarning
5756
>
5857
<head>
58+
<script
59+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Required for theme application before render
60+
dangerouslySetInnerHTML={{
61+
__html: `
62+
(function() {
63+
try {
64+
var storageKey = 'captable-theme';
65+
var theme = localStorage.getItem(storageKey) || 'system';
66+
var root = document.documentElement;
67+
68+
root.classList.remove('light', 'dark');
69+
70+
if (theme === 'system') {
71+
var systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
72+
root.classList.add(systemTheme);
73+
root.style.colorScheme = systemTheme;
74+
} else {
75+
root.classList.add(theme);
76+
root.style.colorScheme = theme;
77+
}
78+
} catch (e) {
79+
// Fallback to light theme if anything fails
80+
document.documentElement.classList.add('light');
81+
document.documentElement.style.colorScheme = 'light';
82+
}
83+
})();
84+
`,
85+
}}
86+
/>
5987
<PublicEnvScript />
6088
</head>
6189
<body className="min-h-screen bg-background text-foreground">
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ThemeProvider, useTheme } from "./provider";
2+
export { ThemeToggle } from "./toggle";

apps/captable/providers/theme-provider.tsx renamed to apps/captable/components/theme/provider.tsx

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,44 +22,57 @@ const initialState: ThemeProviderState = {
2222

2323
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
2424

25+
function getInitialTheme(storageKey: string, defaultTheme: Theme): Theme {
26+
if (typeof window === "undefined") return defaultTheme;
27+
28+
try {
29+
const savedTheme = localStorage.getItem(storageKey) as Theme | null;
30+
return savedTheme || defaultTheme;
31+
} catch {
32+
return defaultTheme;
33+
}
34+
}
35+
2536
export function ThemeProvider({
2637
children,
2738
defaultTheme = "system",
2839
storageKey = "captable-theme",
2940
...props
3041
}: ThemeProviderProps) {
31-
const [theme, setTheme] = useState<Theme>(defaultTheme);
32-
33-
useEffect(() => {
34-
const savedTheme = localStorage.getItem(storageKey) as Theme | null;
35-
if (savedTheme) {
36-
setTheme(savedTheme);
37-
}
38-
}, [storageKey]);
42+
const [theme, setTheme] = useState<Theme>(() =>
43+
getInitialTheme(storageKey, defaultTheme),
44+
);
3945

4046
useEffect(() => {
41-
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
42-
4347
const applyTheme = (newTheme: Theme) => {
4448
const root = window.document.documentElement;
4549
root.classList.remove("light", "dark");
4650

4751
if (newTheme === "system") {
48-
const systemTheme = mediaQuery.matches ? "dark" : "light";
52+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
53+
.matches
54+
? "dark"
55+
: "light";
4956
root.classList.add(systemTheme);
57+
root.style.colorScheme = systemTheme;
5058
} else {
5159
root.classList.add(newTheme);
60+
root.style.colorScheme = newTheme;
5261
}
5362
};
5463

55-
applyTheme(theme);
64+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
5665

5766
const handleChange = () => {
5867
if (theme === "system") {
5968
applyTheme("system");
6069
}
6170
};
6271

72+
// Apply theme when it changes
73+
applyTheme(theme);
74+
75+
// Listen for system theme changes only if current theme is "system"
6376
mediaQuery.addEventListener("change", handleChange);
6477

6578
return () => {
@@ -70,8 +83,13 @@ export function ThemeProvider({
7083
const value = {
7184
theme,
7285
setTheme: (newTheme: Theme) => {
73-
localStorage.setItem(storageKey, newTheme);
74-
setTheme(newTheme);
86+
try {
87+
localStorage.setItem(storageKey, newTheme);
88+
setTheme(newTheme);
89+
} catch {
90+
// Fallback if localStorage is not available
91+
setTheme(newTheme);
92+
}
7593
},
7694
};
7795

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useTheme } from "@/providers/theme-provider";
3+
import { useTheme } from "./provider";
44
import { Button } from "@/components/ui/button";
55
import {
66
DropdownMenu,

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
"db:migrate": "dotenv -- turbo run db:migrate",
1313
"db:studio": "dotenv -- turbo run db:studio",
1414
"email:dev": "bun run --cwd packages/email dev --port 3001",
15+
1516
"// Parallel execution scripts": "",
16-
"dev:all": "dotenv -- turbo run dev db:studio email:dev --parallel",
17+
"dx": "dotenv -- turbo run dev db:studio email:dev --parallel",
1718
"lint:all": "dotenv -- turbo run lint check-types format --parallel",
1819
"clean": "dotenv -- turbo run clean && rm -rf node_modules/.cache",
1920
"fresh": "bun run clean && bun install && bun run db:generate"

0 commit comments

Comments
 (0)