1import type { EnvironmentTheme } from "@t3tools/contracts";
2import { useAtomValue } from "@effect/atom-react";
3import * as Equal from "effect/Equal";
4import { useEffect, useRef, useSyncExternalStore } from "react";
5
6import { primaryServerEnvironmentThemesAtom } from "../state/server";
7import {
8 createVividThemeColors,
9 getDefaultThemeColors,
10 getEnvironmentThemes,
11 isReservedThemeId,
12 lenientThemeColorOverrides,
13 setEnvironmentThemes,
14 subscribeToCustomThemes,
15 type ThemeAppearance,
16 type ThemeColors,
17 type ThemeDefinition,
18} from "../themePalette";
19import { useTheme } from "./useTheme";
20
21function publishedThemeColors(
22 theme: EnvironmentTheme,
23 appearance: ThemeAppearance,
24 colors: Readonly<Record<string, string>> | undefined,
25): ThemeColors {
26 // Seeds generate the base with the guided theme editor's generator, so a
27 // desktop theme arrives as a coherent T3 Code palette rather than a foreign
28 // one — but only for the appearance they describe. A variant builds on that
29 // appearance's stock defaults: the generator follows the seed canvas's
30 // luminance, so dark seeds would give a light variant unreadable colors.
31 const base =
32 appearance === theme.appearance && theme.canvas !== undefined && theme.accent !== undefined
33 ? createVividThemeColors(appearance, theme.canvas, theme.accent)
34 : getDefaultThemeColors(appearance);
35 return { ...base, ...lenientThemeColorOverrides(colors ?? {}) };
36}
37
38/**
39 * A published theme as the theme library renders it. Both published forms are
40 * accepted: the seeded short form a desktop generates, and the standard
41 * exported theme file the Download button produces — so any theme someone
42 * shared can be dropped into the machine's themes directory as-is.
43 */
44export function environmentThemeDefinition(theme: EnvironmentTheme): ThemeDefinition {
45 const variants: Partial<Record<ThemeAppearance, ThemeColors>> = {};
46 for (const [variantAppearance, variantColors] of Object.entries(theme.variants ?? {})) {
47 if (variantAppearance === theme.appearance) continue;
48 variants[variantAppearance as ThemeAppearance] = publishedThemeColors(
49 theme,
50 variantAppearance as ThemeAppearance,
51 variantColors,
52 );
53 }
54
55 return {
56 id: theme.id,
57 label: theme.name,
58 appearance: theme.appearance,
59 colors: publishedThemeColors(theme, theme.appearance, theme.colors),
60 ...(Object.keys(variants).length > 0 ? { variants } : {}),
61 // Only the pure seeded form is guided-generator output. An exported file,
62 // or seeds carrying explicit role overrides, must open Duplicate in the
63 // advanced editor -- the guided one regenerates from canvas and accent and
64 // would discard whatever the machine hand-tuned.
65 ...(theme.canvas !== undefined &&
66 theme.accent !== undefined &&
67 theme.colors === undefined &&
68 theme.variants === undefined
69 ? { managed: true }
70 : {}),
71 };
72}
73
74/**
75 * Published palettes this client can render. Reserved ids are dropped here rather
76 * than rendered: a published `t3-iris.json` would show this palette on its
77 * card while "Use" resolved the built-in, and a published `dark.json` would
78 * capture everyone whose stored preference is the stock `"dark"`.
79 */
80export function publishedThemeDefinitions(
81 themes: ReadonlyArray<EnvironmentTheme>,
82): ReadonlyArray<ThemeDefinition> {
83 return themes
84 .filter((theme) => {
85 if (isReservedThemeId(theme.id)) return false;
86 if (theme.canvas !== undefined && theme.accent !== undefined) return true;
87 const otherAppearance = theme.appearance === "dark" ? "light" : "dark";
88 return [theme.colors, theme.variants?.[otherAppearance]].some(
89 (colors) =>
90 colors !== undefined && Object.keys(lenientThemeColorOverrides(colors)).length > 0,
91 );
92 })
93 .map(environmentThemeDefinition);
94}
95
96/** The published themes as library entries; empty while none are published. */
97export function useEnvironmentThemeDefinitions(): ReadonlyArray<ThemeDefinition> {
98 return useSyncExternalStore(subscribeToCustomThemes, getEnvironmentThemes, () => []);
99}
100
101/**
102 * Keeps the machine's published themes in the theme library for as long as
103 * the primary environment publishes them. A client with a published theme
104 * selected retints the moment the machine rewrites it; everyone else just
105 * gains cards in the theme library.
106 */
107export function useEnvironmentThemeSync(): void {
108 const published = useAtomValue(primaryServerEnvironmentThemesAtom);
109 const { refreshTheme } = useTheme();
110 const lastPublished = useRef<ReadonlyArray<EnvironmentTheme> | null>(null);
111
112 useEffect(() => {
113 // Every reconnect snapshot delivers a fresh but usually identical array;
114 // regenerating palettes and repainting the document for it is exactly the
115 // wasted-frame class this codebase audits for.
116 if (lastPublished.current !== null && Equal.equals(lastPublished.current, published)) return;
117 lastPublished.current = published;
118
119 // The palette is painted from a snapshot taken when the theme last
120 // changed, so new colors only land if the active theme is re-applied.
121 if (setEnvironmentThemes(publishedThemeDefinitions(published))) {
122 refreshTheme({ preservePreview: true });
123 }
124 }, [published, refreshTheme]);
125}