authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 10:37:27-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-27 11:09:21-08:00
log3a4304710221c35732a1fdd5e865e33d64f3fbac
treeae9c66c6350ee48a5762ce5501adaefc64474fde
parent041acf158a277c9f8eca5e844770ffa74d9ed752

Sync diff renderer theme with app theme changes

- Pass explicit Pierre light/dark theme into diff render options - Re-key file diffs on resolved theme so views rerender on theme switch - Stabilize `useTheme` snapshots to propagate system theme updates reliably

3 files changed, 61 insertions(+), 8 deletions(-)

apps/web/src/components/DiffPanel.tsx+8-2
...@@ -16,7 +16,11 @@ import { useStore } from "../store";...@@ -16,7 +16,11 @@ import { useStore } from "../store";
16import { ToggleGroup, Toggle } from "./ui/toggle-group";16import { ToggleGroup, Toggle } from "./ui/toggle-group";
1717
18type DiffRenderMode = "stacked" | "split";18type DiffRenderMode = "stacked" | "split";
19type DiffThemeType = "light" | "dark" | "system";19type DiffThemeType = "light" | "dark";
20
21function resolveDiffThemeName(theme: "light" | "dark") {
22 return theme === "dark" ? "pierre-dark" : "pierre-light";
23}
2024
21type RenderablePatch =25type RenderablePatch =
22 | {26 | {
...@@ -472,13 +476,15 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -472,13 +476,15 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
472 {renderableFiles.map((fileDiff) => {476 {renderableFiles.map((fileDiff) => {
473 const filePath = resolveFileDiffPath(fileDiff);477 const filePath = resolveFileDiffPath(fileDiff);
474 const fileKey = buildFileDiffRenderKey(fileDiff);478 const fileKey = buildFileDiffRenderKey(fileDiff);
479 const themedFileKey = `${fileKey}:${resolvedTheme}`;
475 return (480 return (
476 <div key={fileKey} data-diff-file-path={filePath} className="rounded-md">481 <div key={themedFileKey} data-diff-file-path={filePath} className="rounded-md">
477 <FileDiff482 <FileDiff
478 fileDiff={fileDiff}483 fileDiff={fileDiff}
479 options={{484 options={{
480 diffStyle: diffRenderMode === "split" ? "split" : "unified",485 diffStyle: diffRenderMode === "split" ? "split" : "unified",
481 lineDiffType: "none",486 lineDiffType: "none",
487 theme: resolveDiffThemeName(resolvedTheme),
482 themeType: resolvedTheme as DiffThemeType,488 themeType: resolvedTheme as DiffThemeType,
483 }}489 }}
484 />490 />
apps/web/src/components/DiffWorkerPoolProvider.tsx+35-2
...@@ -1,8 +1,39 @@...@@ -1,8 +1,39 @@
1import { WorkerPoolContextProvider } from "@pierre/diffs/react";1import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react";
2import DiffsWorker from "@pierre/diffs/worker/worker.js?worker";2import DiffsWorker from "@pierre/diffs/worker/worker.js?worker";
3import { useMemo, type ReactNode } from "react";3import { useEffect, useMemo, type ReactNode } from "react";
4import { useTheme } from "../hooks/useTheme";
5
6function resolveDiffThemeName(theme: "light" | "dark") {
7 return theme === "dark" ? "pierre-dark" : "pierre-light";
8}
9
10function DiffWorkerThemeSync({ themeName }: { themeName: "pierre-light" | "pierre-dark" }) {
11 const workerPool = useWorkerPool();
12
13 useEffect(() => {
14 if (!workerPool) {
15 return;
16 }
17
18 const current = workerPool.getDiffRenderOptions();
19 if (current.theme === themeName) {
20 return;
21 }
22
23 void workerPool
24 .setRenderOptions({
25 ...current,
26 theme: themeName,
27 })
28 .catch(() => undefined);
29 }, [themeName, workerPool]);
30
31 return null;
32}
433
5export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {34export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
35 const { resolvedTheme } = useTheme();
36 const diffThemeName = resolveDiffThemeName(resolvedTheme);
6 const workerPoolSize = useMemo(() => {37 const workerPoolSize = useMemo(() => {
7 const cores =38 const cores =
8 typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4);39 typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4);
...@@ -17,9 +48,11 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {...@@ -17,9 +48,11 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
17 totalASTLRUCacheSize: 240,48 totalASTLRUCacheSize: 240,
18 }}49 }}
19 highlighterOptions={{50 highlighterOptions={{
51 theme: diffThemeName,
20 tokenizeMaxLineLength: 1_000,52 tokenizeMaxLineLength: 1_000,
21 }}53 }}
22 >54 >
55 <DiffWorkerThemeSync themeName={diffThemeName} />
23 {children}56 {children}
24 </WorkerPoolContextProvider>57 </WorkerPoolContextProvider>
25 );58 );
apps/web/src/hooks/useTheme.ts+18-4
...@@ -1,11 +1,16 @@...@@ -1,11 +1,16 @@
1import { useCallback, useEffect, useSyncExternalStore } from "react";1import { useCallback, useEffect, useSyncExternalStore } from "react";
22
3type Theme = "light" | "dark" | "system";3type Theme = "light" | "dark" | "system";
4type ThemeSnapshot = {
5 theme: Theme;
6 systemDark: boolean;
7};
48
5const STORAGE_KEY = "t3code:theme";9const STORAGE_KEY = "t3code:theme";
6const MEDIA_QUERY = "(prefers-color-scheme: dark)";10const MEDIA_QUERY = "(prefers-color-scheme: dark)";
711
8let listeners: Array<() => void> = [];12let listeners: Array<() => void> = [];
13let lastSnapshot: ThemeSnapshot | null = null;
9function emitChange() {14function emitChange() {
10 for (const listener of listeners) listener();15 for (const listener of listeners) listener();
11}16}
...@@ -39,8 +44,16 @@ function applyTheme(theme: Theme, suppressTransitions = false) {...@@ -39,8 +44,16 @@ function applyTheme(theme: Theme, suppressTransitions = false) {
39// Apply immediately on module load to prevent flash44// Apply immediately on module load to prevent flash
40applyTheme(getStored());45applyTheme(getStored());
4146
42function getSnapshot(): Theme {47function getSnapshot(): ThemeSnapshot {
43 return getStored();48 const theme = getStored();
49 const systemDark = theme === "system" ? getSystemDark() : false;
50
51 if (lastSnapshot && lastSnapshot.theme === theme && lastSnapshot.systemDark === systemDark) {
52 return lastSnapshot;
53 }
54
55 lastSnapshot = { theme, systemDark };
56 return lastSnapshot;
44}57}
4558
46function subscribe(listener: () => void): () => void {59function subscribe(listener: () => void): () => void {
...@@ -71,10 +84,11 @@ function subscribe(listener: () => void): () => void {...@@ -71,10 +84,11 @@ function subscribe(listener: () => void): () => void {
71}84}
7285
73export function useTheme() {86export function useTheme() {
74 const theme = useSyncExternalStore(subscribe, getSnapshot);87 const snapshot = useSyncExternalStore(subscribe, getSnapshot);
88 const theme = snapshot.theme;
7589
76 const resolvedTheme: "light" | "dark" =90 const resolvedTheme: "light" | "dark" =
77 theme === "system" ? (getSystemDark() ? "dark" : "light") : theme;91 theme === "system" ? (snapshot.systemDark ? "dark" : "light") : theme;
7892
79 const setTheme = useCallback((next: Theme) => {93 const setTheme = useCallback((next: Theme) => {
80 localStorage.setItem(STORAGE_KEY, next);94 localStorage.setItem(STORAGE_KEY, next);