Files
Netcatty/application/state/themeTransition.ts
2026-06-12 01:21:28 +08:00

109 lines
2.9 KiB
TypeScript

import { TERMINAL_HOST_TREE_ANIMATION_MS } from './terminalHostTreeAnimation';
export const THEME_TRANSITION_ATTR = 'data-theme-transition';
export const THEME_TRANSITION_MS = TERMINAL_HOST_TREE_ANIMATION_MS;
export type ThemeTransitionMode = 'view' | 'css' | 'instant';
type DocumentWithViewTransition = Document & {
startViewTransition?: (callback: () => void | Promise<void>) => {
finished: Promise<void>;
skipTransition: () => void;
};
};
type ThemeTransitionOptions = {
root?: HTMLElement;
mode?: ThemeTransitionMode;
};
let cancelThemeTransitionReset: (() => void) | null = null;
function resolveOptions(rootOrOptions?: HTMLElement | ThemeTransitionOptions): Required<ThemeTransitionOptions> {
if (
rootOrOptions
&& (
Object.prototype.hasOwnProperty.call(rootOrOptions, 'root')
|| Object.prototype.hasOwnProperty.call(rootOrOptions, 'mode')
)
) {
const options = rootOrOptions as ThemeTransitionOptions;
return {
root: options.root ?? document.documentElement,
mode: options.mode ?? 'view',
};
}
return {
root: rootOrOptions as HTMLElement | undefined ?? document.documentElement,
mode: 'view',
};
}
function runCssThemeTransition(apply: () => void, root: HTMLElement, cleanup: () => void): void {
root.setAttribute(THEME_TRANSITION_ATTR, 'true');
apply();
const timer = globalThis.setTimeout(cleanup, THEME_TRANSITION_MS + 40);
cancelThemeTransitionReset = () => {
globalThis.clearTimeout(timer);
cleanup();
};
}
function skipViewTransition(transition: ReturnType<NonNullable<DocumentWithViewTransition['startViewTransition']>>): void {
try {
transition.skipTransition();
} catch {
// Already finished or skipped by the browser.
}
}
export function runThemeTransition(
apply: () => void,
rootOrOptions?: HTMLElement | ThemeTransitionOptions,
): void {
const { root, mode } = resolveOptions(rootOrOptions);
cancelThemeTransitionReset?.();
const cleanup = () => {
root.removeAttribute(THEME_TRANSITION_ATTR);
cancelThemeTransitionReset = null;
};
if (mode === 'instant') {
apply();
cleanup();
return;
}
if (mode === 'css') {
runCssThemeTransition(apply, root, cleanup);
return;
}
const doc = root.ownerDocument as DocumentWithViewTransition | null;
const startViewTransition = doc?.startViewTransition?.bind(doc);
if (startViewTransition) {
let transition: ReturnType<NonNullable<DocumentWithViewTransition['startViewTransition']>> | null = null;
try {
transition = startViewTransition(() => {
apply();
});
} catch {
runCssThemeTransition(apply, root, cleanup);
return;
}
cancelThemeTransitionReset = () => {
if (transition) {
skipViewTransition(transition);
}
cleanup();
};
void transition.finished.then(cleanup, cleanup);
return;
}
runCssThemeTransition(apply, root, cleanup);
}