import { useAtomValue } from "@effect/atom-react";import { useEffect } from "react";import { primaryEnvironmentIdAtom } from "../state/primaryEnvironment";import { primaryServerSettingsAtom } from "../state/server";import { getThemeDefinition, singleAppearanceOf } from "../themePalette";import { useEnvironmentThemeDefinitions } from "./useEnvironmentTheme";import { useTheme } from "./useTheme";/** * Scoped per environment: each machine's `t3 theme set` is its own act, so * hopping between primary environments neither replays one environment's * theme over the user's pick nor swallows another's. */const APPLIED_DEFAULT_THEME_STORAGE_PREFIX = "t3code:default-theme-applied:v2:";/** * One generation per set: keyed on when the theme was set, not just its * value, so re-asserting the same theme still acts. Environments provisioned * by builds without the timestamp degrade to applying once per value. */export function defaultThemeGeneration(theme: string, setAt: string): string { return setAt.length > 0 ? `${theme}@${setAt}` : theme;}/** * The generation to apply, or null to leave this client alone. Pure so the * rule -- apply once per set, never replay one already applied, wait for a * theme that has not arrived yet -- can be tested without a renderer. */export function defaultThemeToApply(input: { readonly environmentId: string | null; readonly defaultTheme: string; readonly defaultThemeSetAt: string; readonly appliedGeneration: string | null; readonly resolves: boolean;}): string | null { if (input.environmentId === null || input.defaultTheme.length === 0) return null; const generation = defaultThemeGeneration(input.defaultTheme, input.defaultThemeSetAt); if (input.appliedGeneration === generation) return null; // The setting and the palette it names arrive independently, so an id that // does not resolve yet is not a failure -- the effect runs again when the // published set changes. if (!input.resolves) return null; return generation;}function readAppliedGeneration(storageKey: string): string | null { if (typeof window === "undefined") return null; try { return window.localStorage.getItem(storageKey); } catch { return null; }}function writeAppliedGeneration(storageKey: string, generation: string): void { try { window.localStorage.setItem(storageKey, generation); } catch { // Unrecordable means the next config event applies again; harmless. }}/** * Applies the environment's theme (`t3 theme set <id>`). Each set switches * this client once — live when connected, on the next connect otherwise — * and then steps aside: a theme the user picks in Settings afterwards wins * until the environment's theme is set again. The environment's own published * themes are valid targets, which is why this waits for an id that does not * resolve yet — the setting and the palette it names arrive independently. */export function useDefaultThemeAdoption(): void { const environmentId = useAtomValue(primaryEnvironmentIdAtom); const settings = useAtomValue(primaryServerSettingsAtom); const { defaultTheme, defaultThemeSetAt } = settings; const { setTheme, setAppearanceMode } = useTheme(); // Re-runs adoption when a late-arriving published theme makes the // requested id resolvable. const environmentThemes = useEnvironmentThemeDefinitions(); useEffect(() => { if (typeof window === "undefined" || environmentId === null) return; const storageKey = `${APPLIED_DEFAULT_THEME_STORAGE_PREFIX}${environmentId}`; const definition = getThemeDefinition(defaultTheme); const generation = defaultThemeToApply({ environmentId, defaultTheme, defaultThemeSetAt, appliedGeneration: readAppliedGeneration(storageKey), resolves: definition !== null, }); if (generation === null || definition === null) return; // Deliberately not the card's rule. Clicking a card is a user choosing one // half of their own mix; a set theme is the environment saying what this // client opens on, so it takes the base preference and, for a // single-appearance theme, the matching mode -- otherwise a dark-only // theme on a light client is recorded as applied while nothing changes. if (!setTheme(defaultTheme)) return; const half = singleAppearanceOf(definition); // Recorded only once both land: marking the generation applied while the // appearance switch failed would leave a dark-only theme rendering its // light half with no retry. if (half !== null && !setAppearanceMode(half)) return; writeAppliedGeneration(storageKey, generation); }, [ environmentId, defaultTheme, defaultThemeSetAt, environmentThemes, setTheme, setAppearanceMode, ]);}
import { useAtomValue } from "@effect/atom-react";
import { useEffect } from "react";
import { primaryEnvironmentIdAtom } from "../state/primaryEnvironment";
import { primaryServerSettingsAtom } from "../state/server";
import { getThemeDefinition, singleAppearanceOf } from "../themePalette";
import { useEnvironmentThemeDefinitions } from "./useEnvironmentTheme";
import { useTheme } from "./useTheme";
/**
* Scoped per environment: each machine's `t3 theme set` is its own act, so
* hopping between primary environments neither replays one environment's
* theme over the user's pick nor swallows another's.
*/
const APPLIED_DEFAULT_THEME_STORAGE_PREFIX = "t3code:default-theme-applied:v2:";
/**
* One generation per set: keyed on when the theme was set, not just its
* value, so re-asserting the same theme still acts. Environments provisioned
* by builds without the timestamp degrade to applying once per value.
*/
export function defaultThemeGeneration(theme: string, setAt: string): string {
return setAt.length > 0 ? `${theme}@${setAt}` : theme;
}
/**
* The generation to apply, or null to leave this client alone. Pure so the
* rule -- apply once per set, never replay one already applied, wait for a
* theme that has not arrived yet -- can be tested without a renderer.
*/
export function defaultThemeToApply(input: {
readonly environmentId: string | null;
readonly defaultTheme: string;
readonly defaultThemeSetAt: string;
readonly appliedGeneration: string | null;
readonly resolves: boolean;
}): string | null {
if (input.environmentId === null || input.defaultTheme.length === 0) return null;
const generation = defaultThemeGeneration(input.defaultTheme, input.defaultThemeSetAt);
if (input.appliedGeneration === generation) return null;
// The setting and the palette it names arrive independently, so an id that
// does not resolve yet is not a failure -- the effect runs again when the
// published set changes.
if (!input.resolves) return null;
return generation;
}
function readAppliedGeneration(storageKey: string): string | null {
if (typeof window === "undefined") return null;
try {
return window.localStorage.getItem(storageKey);
} catch {
return null;
}
}
function writeAppliedGeneration(storageKey: string, generation: string): void {
try {
window.localStorage.setItem(storageKey, generation);
} catch {
// Unrecordable means the next config event applies again; harmless.
}
}
/**
* Applies the environment's theme (`t3 theme set <id>`). Each set switches
* this client once — live when connected, on the next connect otherwise —
* and then steps aside: a theme the user picks in Settings afterwards wins
* until the environment's theme is set again. The environment's own published
* themes are valid targets, which is why this waits for an id that does not
* resolve yet — the setting and the palette it names arrive independently.
*/
export function useDefaultThemeAdoption(): void {
const environmentId = useAtomValue(primaryEnvironmentIdAtom);
const settings = useAtomValue(primaryServerSettingsAtom);
const { defaultTheme, defaultThemeSetAt } = settings;
const { setTheme, setAppearanceMode } = useTheme();
// Re-runs adoption when a late-arriving published theme makes the
// requested id resolvable.
const environmentThemes = useEnvironmentThemeDefinitions();
useEffect(() => {
if (typeof window === "undefined" || environmentId === null) return;
const storageKey = `${APPLIED_DEFAULT_THEME_STORAGE_PREFIX}${environmentId}`;
const definition = getThemeDefinition(defaultTheme);
const generation = defaultThemeToApply({
environmentId,
defaultTheme,
defaultThemeSetAt,
appliedGeneration: readAppliedGeneration(storageKey),
resolves: definition !== null,
});
if (generation === null || definition === null) return;
// Deliberately not the card's rule. Clicking a card is a user choosing one
// half of their own mix; a set theme is the environment saying what this
// client opens on, so it takes the base preference and, for a
// single-appearance theme, the matching mode -- otherwise a dark-only
// theme on a light client is recorded as applied while nothing changes.
if (!setTheme(defaultTheme)) return;
const half = singleAppearanceOf(definition);
// Recorded only once both land: marking the generation applied while the
// appearance switch failed would leave a dark-only theme rendering its
// light half with no retry.
if (half !== null && !setAppearanceMode(half)) return;
writeAppliedGeneration(storageKey, generation);
}, [
environmentId,
defaultTheme,
defaultThemeSetAt,
environmentThemes,
setTheme,
setAppearanceMode,
]);
}