| author | |
| committer | |
| log | 3a4304710221c35732a1fdd5e865e33d64f3fbac |
| tree | ae9c66c6350ee48a5762ce5501adaefc64474fde |
| parent | 041acf158a277c9f8eca5e844770ffa74d9ed752 |
- 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 reliably3 files changed, 61 insertions(+), 8 deletions(-)
apps/web/src/components/DiffPanel.tsx+8-2| ... | ... | @@ -16,7 +16,11 @@ import { useStore } from "../store"; |
| 16 | 16 | import { ToggleGroup, Toggle } from "./ui/toggle-group"; |
| 17 | 17 | |
| 18 | 18 | type DiffRenderMode = "stacked" | "split"; |
| 19 | type DiffThemeType = "light" | "dark" | "system"; | |
| 19 | type DiffThemeType = "light" | "dark"; | |
| 20 | ||
| 21 | function resolveDiffThemeName(theme: "light" | "dark") { | |
| 22 | return theme === "dark" ? "pierre-dark" : "pierre-light"; | |
| 23 | } | |
| 20 | 24 | |
| 21 | 25 | type RenderablePatch = |
| 22 | 26 | | { |
| ... | ... | @@ -472,13 +476,15 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 472 | 476 | {renderableFiles.map((fileDiff) => { |
| 473 | 477 | const filePath = resolveFileDiffPath(fileDiff); |
| 474 | 478 | const fileKey = buildFileDiffRenderKey(fileDiff); |
| 479 | const themedFileKey = `${fileKey}:${resolvedTheme}`; | |
| 475 | 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 | 482 | <FileDiff |
| 478 | 483 | fileDiff={fileDiff} |
| 479 | 484 | options={{ |
| 480 | 485 | diffStyle: diffRenderMode === "split" ? "split" : "unified", |
| 481 | 486 | lineDiffType: "none", |
| 487 | theme: resolveDiffThemeName(resolvedTheme), | |
| 482 | 488 | themeType: resolvedTheme as DiffThemeType, |
| 483 | 489 | }} |
| 484 | 490 | /> |
apps/web/src/components/DiffWorkerPoolProvider.tsx+35-2| ... | ... | @@ -1,8 +1,39 @@ |
| 1 | import { WorkerPoolContextProvider } from "@pierre/diffs/react"; | |
| 1 | import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react"; | |
| 2 | 2 | import DiffsWorker from "@pierre/diffs/worker/worker.js?worker"; |
| 3 | import { useMemo, type ReactNode } from "react"; | |
| 3 | import { useEffect, useMemo, type ReactNode } from "react"; | |
| 4 | import { useTheme } from "../hooks/useTheme"; | |
| 5 | ||
| 6 | function resolveDiffThemeName(theme: "light" | "dark") { | |
| 7 | return theme === "dark" ? "pierre-dark" : "pierre-light"; | |
| 8 | } | |
| 9 | ||
| 10 | function 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 | } | |
| 4 | 33 | |
| 5 | 34 | export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { |
| 35 | const { resolvedTheme } = useTheme(); | |
| 36 | const diffThemeName = resolveDiffThemeName(resolvedTheme); | |
| 6 | 37 | const workerPoolSize = useMemo(() => { |
| 7 | 38 | const cores = |
| 8 | 39 | typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4); |
| ... | ... | @@ -17,9 +48,11 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { |
| 17 | 48 | totalASTLRUCacheSize: 240, |
| 18 | 49 | }} |
| 19 | 50 | highlighterOptions={{ |
| 51 | theme: diffThemeName, | |
| 20 | 52 | tokenizeMaxLineLength: 1_000, |
| 21 | 53 | }} |
| 22 | 54 | > |
| 55 | <DiffWorkerThemeSync themeName={diffThemeName} /> | |
| 23 | 56 | {children} |
| 24 | 57 | </WorkerPoolContextProvider> |
| 25 | 58 | ); |
apps/web/src/hooks/useTheme.ts+18-4| ... | ... | @@ -1,11 +1,16 @@ |
| 1 | 1 | import { useCallback, useEffect, useSyncExternalStore } from "react"; |
| 2 | 2 | |
| 3 | 3 | type Theme = "light" | "dark" | "system"; |
| 4 | type ThemeSnapshot = { | |
| 5 | theme: Theme; | |
| 6 | systemDark: boolean; | |
| 7 | }; | |
| 4 | 8 | |
| 5 | 9 | const STORAGE_KEY = "t3code:theme"; |
| 6 | 10 | const MEDIA_QUERY = "(prefers-color-scheme: dark)"; |
| 7 | 11 | |
| 8 | 12 | let listeners: Array<() => void> = []; |
| 13 | let lastSnapshot: ThemeSnapshot | null = null; | |
| 9 | 14 | function emitChange() { |
| 10 | 15 | for (const listener of listeners) listener(); |
| 11 | 16 | } |
| ... | ... | @@ -39,8 +44,16 @@ function applyTheme(theme: Theme, suppressTransitions = false) { |
| 39 | 44 | // Apply immediately on module load to prevent flash |
| 40 | 45 | applyTheme(getStored()); |
| 41 | 46 | |
| 42 | function getSnapshot(): Theme { | |
| 43 | return getStored(); | |
| 47 | function getSnapshot(): ThemeSnapshot { | |
| 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 | } |
| 45 | 58 | |
| 46 | 59 | function subscribe(listener: () => void): () => void { |
| ... | ... | @@ -71,10 +84,11 @@ function subscribe(listener: () => void): () => void { |
| 71 | 84 | } |
| 72 | 85 | |
| 73 | 86 | export function useTheme() { |
| 74 | const theme = useSyncExternalStore(subscribe, getSnapshot); | |
| 87 | const snapshot = useSyncExternalStore(subscribe, getSnapshot); | |
| 88 | const theme = snapshot.theme; | |
| 75 | 89 | |
| 76 | 90 | const resolvedTheme: "light" | "dark" = |
| 77 | theme === "system" ? (getSystemDark() ? "dark" : "light") : theme; | |
| 91 | theme === "system" ? (snapshot.systemDark ? "dark" : "light") : theme; | |
| 78 | 92 | |
| 79 | 93 | const setTheme = useCallback((next: Theme) => { |
| 80 | 94 | localStorage.setItem(STORAGE_KEY, next); |