39948-vm/frontend/src/hooks/useTransitionSettings.ts

176 lines
5.2 KiB
TypeScript

/**
* useTransitionSettings Hook
*
* Resolves transition settings by cascading through three levels:
* Element → Project → Global (fallback)
*
* Video transitions always take precedence over CSS-based transitions.
*/
import { useMemo } from 'react';
import type {
GlobalTransitionDefaults,
ProjectTransitionSettings,
ElementTransitionSettings,
ResolvedTransitionSettings,
TransitionType,
EasingFunction,
DEFAULT_TRANSITION_SETTINGS,
} from '../types/transition';
export interface UseTransitionSettingsParams {
/** Global defaults (from global_transition_defaults table) */
globalDefaults: GlobalTransitionDefaults | null;
/** Project-level settings (from project_transition_settings table, environment-aware) */
projectSettings?: ProjectTransitionSettings | null;
/** Element-level settings (from ui_schema_json) */
elementSettings?: ElementTransitionSettings | null;
}
/**
* Default values used when no settings are available at any level
*/
const FALLBACK_DEFAULTS = {
type: 'fade' as TransitionType,
durationMs: 700,
easing: 'ease-in-out' as EasingFunction,
overlayColor: '#000000',
};
/**
* Hook to resolve transition settings with cascade:
* Element → Project → Global → Fallback defaults
*
* @example
* ```tsx
* // Project settings are fetched from project_transition_settings store
* // and converted using entityToProjectSettings() helper
* const transitionSettings = useTransitionSettings({
* globalDefaults,
* projectSettings, // from projectTransitionSettingsSlice (environment-aware)
* elementSettings: element?.transitionSettings,
* });
*
* // Use resolved settings
* if (transitionSettings.type === 'video') {
* // Play video transition
* } else {
* // Apply CSS transition with transitionSettings.durationMs and transitionSettings.easing
* }
* ```
*/
export function useTransitionSettings({
globalDefaults,
projectSettings,
elementSettings,
}: UseTransitionSettingsParams): ResolvedTransitionSettings {
return useMemo(() => {
// Video transitions always take precedence
if (elementSettings?.transitionVideoUrl) {
return {
type: 'video' as TransitionType,
durationMs:
elementSettings.transitionDurationMs ?? FALLBACK_DEFAULTS.durationMs,
easing: elementSettings.transitionEasing ?? FALLBACK_DEFAULTS.easing,
overlayColor:
elementSettings.transitionOverlayColor ??
projectSettings?.overlayColor ??
globalDefaults?.overlay_color ??
FALLBACK_DEFAULTS.overlayColor,
videoUrl: elementSettings.transitionVideoUrl,
reverseVideoUrl: elementSettings.reverseVideoUrl,
};
}
// Cascade: Element → Project → Global → Fallback
const type: TransitionType =
elementSettings?.transitionType ??
projectSettings?.transitionType ??
globalDefaults?.transition_type ??
FALLBACK_DEFAULTS.type;
const durationMs: number =
elementSettings?.transitionDurationMs ??
projectSettings?.durationMs ??
globalDefaults?.duration_ms ??
FALLBACK_DEFAULTS.durationMs;
const easing: EasingFunction =
elementSettings?.transitionEasing ??
projectSettings?.easing ??
globalDefaults?.easing ??
FALLBACK_DEFAULTS.easing;
const overlayColor: string =
elementSettings?.transitionOverlayColor ??
projectSettings?.overlayColor ??
globalDefaults?.overlay_color ??
FALLBACK_DEFAULTS.overlayColor;
return {
type,
durationMs,
easing,
overlayColor,
};
}, [globalDefaults, projectSettings, elementSettings]);
}
/**
* Resolve transition settings without React hook (for non-component contexts)
*/
export function resolveTransitionSettings({
globalDefaults,
projectSettings,
elementSettings,
}: UseTransitionSettingsParams): ResolvedTransitionSettings {
// Video transitions always take precedence
if (elementSettings?.transitionVideoUrl) {
return {
type: 'video' as TransitionType,
durationMs:
elementSettings.transitionDurationMs ?? FALLBACK_DEFAULTS.durationMs,
easing: elementSettings.transitionEasing ?? FALLBACK_DEFAULTS.easing,
overlayColor:
elementSettings.transitionOverlayColor ??
projectSettings?.overlayColor ??
globalDefaults?.overlay_color ??
FALLBACK_DEFAULTS.overlayColor,
videoUrl: elementSettings.transitionVideoUrl,
reverseVideoUrl: elementSettings.reverseVideoUrl,
};
}
// Cascade: Element → Project → Global → Fallback
const type: TransitionType =
elementSettings?.transitionType ??
projectSettings?.transitionType ??
globalDefaults?.transition_type ??
FALLBACK_DEFAULTS.type;
const durationMs: number =
elementSettings?.transitionDurationMs ??
projectSettings?.durationMs ??
globalDefaults?.duration_ms ??
FALLBACK_DEFAULTS.durationMs;
const easing: EasingFunction =
elementSettings?.transitionEasing ??
projectSettings?.easing ??
globalDefaults?.easing ??
FALLBACK_DEFAULTS.easing;
const overlayColor: string =
elementSettings?.transitionOverlayColor ??
projectSettings?.overlayColor ??
globalDefaults?.overlay_color ??
FALLBACK_DEFAULTS.overlayColor;
return {
type,
durationMs,
easing,
overlayColor,
};
}