import React, { createContext, ReactNode, useContext, useMemo } from 'react'; import { darken } from '../lib/color.js'; import { merge } from '../lib/merge.js'; import { DeepPartial } from '../lib/types.js'; import { defaultTheme, IMagicBellTheme } from './Theme.js'; const MagicBellThemeContext = createContext(defaultTheme); type MagicBellProviderProps = { value: DeepPartial; children: ReactNode; }; export function MagicBellThemeProvider({ value: partialTheme = {}, ...props }: MagicBellProviderProps) { const theme = useMemo(() => { // don't run backwards compatibility code if no partial theme was provided if (!partialTheme || Object.keys(partialTheme).length === 0) return defaultTheme; const merged = merge(defaultTheme, partialTheme) as IMagicBellTheme; // backwards compatibility for prose if (!partialTheme.prose) { for (const key of Object.keys(merged.prose).filter((x) => !/code|pre/i.test(x))) { merged.prose[key] = merged.notification.default.textColor; } } // backwards compatibility for notification titles if (!partialTheme.notification?.default?.title?.textColor) { merged.notification.default.title.textColor = merged.notification.default.textColor; } // make notification unseen & unread state fallback to custom default state before falling back to default theme for (const variant of ['unseen', 'unread']) { merged.notification[variant] = merge( merged.notification[variant], partialTheme.notification?.default, partialTheme.notification?.[variant], ); } // backwards compatibility for notification hover styles. We only compute hover styles if // the partially provided theme doesn't define them for any of the notification states. const hasDeclaredHoverStyles = ['default', 'unseen', 'unread'].some((variant) => Boolean(partialTheme.notification?.[variant]?.hover), ); if (!hasDeclaredHoverStyles) { for (const variant of ['default', 'unseen', 'unread']) { const current = merged.notification[variant]; merged.notification[variant].backgroundOpacity = variant === 'default' ? 0 : 0.05; merged.notification[variant].hover = merge( { backgroundColor: darken(current.backgroundColor, 5), backgroundOpacity: 0.15, }, partialTheme.notification?.default?.hover, partialTheme.notification?.[variant]?.hover, ); } } // backwards compatibility for status styles. We only compute status style if // the partially provided theme doesn't define them for any of the notification states. const hasDeclaredStateStyles = ['default', 'unseen', 'unread'].some((variant) => Boolean(partialTheme.notification?.[variant]?.state), ); if (!hasDeclaredStateStyles) { for (const variant of ['default', 'unseen', 'unread']) { const current = merged.notification[variant]; // note that default was technically `toRGBA(current.textColor, 0.5)`, but it also didn't show // the state dot back then, as it was hidden behind the menu button when the notification was read. // so this color differs, but the result is closer to the old behavior - don't show the dot -. const color = variant === 'default' ? 'transparent' : merged.header.backgroundColor; merged.notification[variant].state = merge(current.state, { color }); } } // backwards compatibility for unseenBadge merged.unseenBadge.borderColor = partialTheme?.unseenBadge?.borderColor || 'transparent'; // backwards compatibility for container merged.container.borderColor = partialTheme?.container?.borderColor || ''; // backwards compatibility for banner style merged.banner = merge( merged.banner, { backgroundColor: merged.notification.unseen.backgroundColor, backgroundOpacity: 0.1, textColor: merged.notification.unseen.textColor, fontFamily: merged.footer.fontFamily, textAlign: merged.footer.textAlign, fontSize: merged.footer.fontSize, }, partialTheme.banner, ) as IMagicBellTheme['banner']; return merged; }, [partialTheme]); return ; } export const useTheme = () => useContext(MagicBellThemeContext); export default MagicBellThemeContext;