| 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,7 +16,11 @@ import { useStore } from "../store"; |
| 16 | import { ToggleGroup, Toggle } from "./ui/toggle-group"; | 16 | import { ToggleGroup, Toggle } from "./ui/toggle-group"; |
| 17 | 17 | ||
| 18 | type DiffRenderMode = "stacked" | "split"; | 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 | type RenderablePatch = | 25 | type 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 | <FileDiff | 482 | <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 @@ |
| 1 | import { WorkerPoolContextProvider } from "@pierre/diffs/react"; | 1 | import { WorkerPoolContextProvider, useWorkerPool } from "@pierre/diffs/react"; |
| 2 | import DiffsWorker from "@pierre/diffs/worker/worker.js?worker"; | 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 | export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { | 34 | export 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 @@ |
| 1 | import { useCallback, useEffect, useSyncExternalStore } from "react"; | 1 | import { useCallback, useEffect, useSyncExternalStore } from "react"; |
| 2 | 2 | ||
| 3 | type Theme = "light" | "dark" | "system"; | 3 | type Theme = "light" | "dark" | "system"; |
| 4 | type ThemeSnapshot = { | ||
| 5 | theme: Theme; | ||
| 6 | systemDark: boolean; | ||
| 7 | }; | ||
| 4 | 8 | ||
| 5 | const STORAGE_KEY = "t3code:theme"; | 9 | const STORAGE_KEY = "t3code:theme"; |
| 6 | const MEDIA_QUERY = "(prefers-color-scheme: dark)"; | 10 | const MEDIA_QUERY = "(prefers-color-scheme: dark)"; |
| 7 | 11 | ||
| 8 | let listeners: Array<() => void> = []; | 12 | let listeners: Array<() => void> = []; |
| 13 | let lastSnapshot: ThemeSnapshot | null = null; | ||
| 9 | function emitChange() { | 14 | function 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 flash | 44 | // Apply immediately on module load to prevent flash |
| 40 | applyTheme(getStored()); | 45 | applyTheme(getStored()); |
| 41 | 46 | ||
| 42 | function getSnapshot(): Theme { | 47 | function 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 | } |
| 45 | 58 | ||
| 46 | function subscribe(listener: () => void): () => void { | 59 | function subscribe(listener: () => void): () => void { |
| ... | @@ -71,10 +84,11 @@ function subscribe(listener: () => void): () => void { | ... | @@ -71,10 +84,11 @@ function subscribe(listener: () => void): () => void { |
| 71 | } | 84 | } |
| 72 | 85 | ||
| 73 | export function useTheme() { | 86 | export function useTheme() { |
| 74 | const theme = useSyncExternalStore(subscribe, getSnapshot); | 87 | const snapshot = useSyncExternalStore(subscribe, getSnapshot); |
| 88 | const theme = snapshot.theme; | ||
| 75 | 89 | ||
| 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; |
| 78 | 92 | ||
| 79 | const setTheme = useCallback((next: Theme) => { | 93 | const setTheme = useCallback((next: Theme) => { |
| 80 | localStorage.setItem(STORAGE_KEY, next); | 94 | localStorage.setItem(STORAGE_KEY, next); |