/**
* React Fabric
* @version 1.0.0
* @license MIT
* @copyright 2024 Adarsh Pastakia
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import i18next from "i18next";
import type { PropsWithChildren } from "react";
import { useCallback, useEffect, useEffectEvent, useLayoutEffect, useMemo, useState } from "react";
import { flushSync } from "react-dom";
import { Toaster } from "sonner";
import { AnimationIndicator } from "../components/animations/Indicator";
import { Icon } from "../components/icon/Icon";
import type { State } from "./context";
import { GlobalContext } from "./context";
import { ModalManagerProvider } from "./ModalManager";
export interface GlobalProps extends PropsWithChildren {
/**
* application default error display
*/
errorElement?: React.ComponentType<{ error?: string }>;
// Overlay effect
styleEffect?: "glass" | "matte";
defaultLocale?: string;
defaultCalendar?: State["calendar"];
defaultColorScheme?: State["colorScheme"];
}
const KEY_LOCALE = "ruf:locale";
const KEY_CALENDAR = "ruf:calendar";
const KEY_COLOR_SCHEME = "ruf:colorScheme";
const systemLocale = () => {
return (typeof window !== "undefined" ? localStorage.getItem(KEY_LOCALE) : undefined) ?? "en";
};
const systemCalendar = () => {
return (typeof window !== "undefined" ? (localStorage.getItem(KEY_CALENDAR) as State["calendar"]) : undefined) ?? "gregorian";
};
const systemColorScheme = () => {
const theme: AnyObject =
typeof window !== "undefined"
? ((localStorage.getItem(KEY_COLOR_SCHEME) as State["colorScheme"]) ?? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"))
: "light";
return theme as "light" | "dark";
};
/**
* Root context provider for the application.
* Wraps the app with global state management for locale, calendar system, and color scheme,
* and provides context to all descendant components via `GlobalContext`.
*
* @example
* ```tsx
*
*
*
* ```
*/
export function ApplicationProvider({ children, errorElement, defaultLocale, defaultCalendar, defaultColorScheme, styleEffect }: GlobalProps) {
const [colorScheme, setColorScheme] = useState(() => defaultColorScheme ?? systemColorScheme());
const [calendar, setCalendar] = useState(() => defaultCalendar ?? systemCalendar());
const [locale, setLocale] = useState(() => defaultLocale ?? systemLocale());
const [areas, setAreas] = useState void>>({});
/** ***************** set initial theme and locale dir *******************/
const init = useEffectEvent(() => {
document.documentElement.lang = locale;
if (!document.documentElement.dataset.colorScheme) {
document.documentElement.dataset.colorScheme = colorScheme;
} else {
// eslint-disable-next-line @eslint-react/set-state-in-effect
setColorScheme(document.documentElement.dataset.colorScheme as "light" | "dark");
}
if (typeof window !== "undefined") {
try {
void i18next.changeLanguage(locale).then(() => {
document.documentElement.dir = i18next.dir();
});
} catch {
//
}
}
});
// eslint-disable-next-line @eslint-react/exhaustive-deps
useEffect(init, []);
/** ***************** theme toggle *******************/
const toggleColorScheme = useCallback(
(forceTheme?: State["colorScheme"]) => {
const newTheme = forceTheme ?? (colorScheme === "dark" ? "light" : "dark");
if (!document.activeViewTransition)
document.startViewTransition(() => {
// eslint-disable-next-line @eslint-react/dom-no-flush-sync
flushSync(() => {
setColorScheme(newTheme);
localStorage.setItem(KEY_COLOR_SCHEME, newTheme);
document.documentElement.dataset.colorScheme = newTheme;
});
});
else {
setColorScheme(newTheme);
localStorage.setItem(KEY_COLOR_SCHEME, newTheme);
document.documentElement.dataset.colorScheme = newTheme;
}
return newTheme;
},
[colorScheme],
);
/** ***************** change locale *******************/
const changeLocale = useCallback((locale: string) => {
try {
void i18next.changeLanguage(locale).then(() => {
localStorage.setItem(KEY_LOCALE, locale);
document.documentElement.lang = locale;
document.documentElement.dir = i18next.dir();
});
} catch {
//
}
// eslint-disable-next-line @eslint-react/set-state-in-effect
setLocale(locale);
return locale;
}, []);
/** ***************** change calendar *******************/
const changeCalendar = useCallback((calendar: State["calendar"]) => {
setCalendar(calendar);
localStorage.setItem(KEY_CALENDAR, calendar);
return calendar;
}, []);
useLayoutEffect(() => {
if (defaultLocale) changeLocale(defaultLocale);
// eslint-disable-next-line @eslint-react/exhaustive-deps
}, [defaultLocale]);
const registerArea = useCallback((area: string, callback: (open?: boolean) => void) => {
setAreas((prev) => ({ ...prev, [area]: callback }));
}, []);
const unregisterArea = useCallback((area: string) => {
setAreas((prev) => {
const newAreas = { ...prev };
delete newAreas[area];
return newAreas;
});
}, []);
const toggleArea = useCallback(
(area: string, open?: boolean) => {
if (areas[area]) areas[area](open);
},
[areas],
);
const [alert, setAlert] = useState();
const showAlert = useCallback((alert?: React.ReactElement) => {
setAlert(alert);
}, []);
const isDark = useMemo(() => colorScheme === "dark", [colorScheme]);
/** ***************** context provider *******************/
return (
,
info: ,
success: ,
warning: ,
loading: ,
}}
/>
,
info: ,
success: ,
warning: ,
}}
/>
{alert}
{children}
);
}