diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 1ad174863c..755fc5e1be 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -30,16 +30,16 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => { }, [clearStorage]); return ( - - + + {!didStudioInit && } - - + + ); }; diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx index 9aa70a5512..0a21348e98 100644 --- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx @@ -2,6 +2,7 @@ import { useGlobalModifiersInit } from '@invoke-ai/ui-library'; import { setupListeners } from '@reduxjs/toolkit/query'; import type { StudioInitAction } from 'app/hooks/useStudioInitAction'; import { useStudioInitAction } from 'app/hooks/useStudioInitAction'; +import { useSyncLangDirection } from 'app/hooks/useSyncLangDirection'; import { useSyncQueueStatus } from 'app/hooks/useSyncQueueStatus'; import { useLogger } from 'app/logging/useLogger'; import { useSyncLoggingConfig } from 'app/logging/useSyncLoggingConfig'; @@ -50,6 +51,7 @@ export const GlobalHookIsolator = memo( useNavigationApi(); useDndMonitor(); useSyncNodeErrors(); + useSyncLangDirection(); // Persistent subscription to the queue counts query - canvas relies on this to know if there are pending // and/or in progress canvas sessions. diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx index 25ee01903b..437cecd492 100644 --- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx +++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx @@ -3,43 +3,39 @@ import 'overlayscrollbars/overlayscrollbars.css'; import '@xyflow/react/dist/base.css'; import 'common/components/OverlayScrollbars/overlayscrollbars.css'; -import { ChakraProvider, DarkMode, extendTheme, theme as _theme, TOAST_OPTIONS } from '@invoke-ai/ui-library'; +import { ChakraProvider, DarkMode, extendTheme, theme as baseTheme, TOAST_OPTIONS } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $direction } from 'app/hooks/useSyncLangDirection'; import type { ReactNode } from 'react'; -import { memo, useEffect, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo, useMemo } from 'react'; type ThemeLocaleProviderProps = { children: ReactNode; }; +const buildTheme = (direction: 'ltr' | 'rtl') => { + return extendTheme({ + ...baseTheme, + direction, + shadows: { + ...baseTheme.shadows, + selected: + 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)', + hoverSelected: + 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)', + hoverUnselected: + 'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)', + selectedForCompare: + 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)', + hoverSelectedForCompare: + 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)', + }, + }); +}; + function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { - const { i18n } = useTranslation(); - - const direction = i18n.dir(); - - const theme = useMemo(() => { - return extendTheme({ - ..._theme, - direction, - shadows: { - ..._theme.shadows, - selected: - 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)', - hoverSelected: - 'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)', - hoverUnselected: - 'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)', - selectedForCompare: - 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)', - hoverSelectedForCompare: - 'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)', - }, - }); - }, [direction]); - - useEffect(() => { - document.body.dir = direction; - }, [direction]); + const direction = useStore($direction); + const theme = useMemo(() => buildTheme(direction), [direction]); return ( diff --git a/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts b/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts new file mode 100644 index 0000000000..da1e0dbbcb --- /dev/null +++ b/invokeai/frontend/web/src/app/hooks/useSyncLangDirection.ts @@ -0,0 +1,36 @@ +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { atom } from 'nanostores'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +/** + * Global atom storing the language direction, to be consumed by the Chakra theme. + * + * Why do we need this? We have a kind of catch-22: + * - The Chakra theme needs to know the language direction to apply the correct styles. + * - The language direction is determined by i18n and the language selection. + * - We want our error boundary to be themed. + * - It's possible that i18n can throw if the language selection is invalid or not supported. + * + * Previously, we had the logic in this file in the theme provider, which wrapped the error boundary. The error + * was properly themed. But then, if i18n threw in the theme provider, the error boundary does not catch the + * error. The app would crash to a white screen. + * + * We tried swapping the component hierarchy so that the error boundary wraps the theme provider, but then the + * error boundary isn't themed! + * + * The solution is to move this i18n direction logic out of the theme provider and into a hook that we can use + * within the error boundary. The error boundary will be themed, _and_ catch any i18n errors. + */ +export const $direction = atom<'ltr' | 'rtl'>('ltr'); + +export const useSyncLangDirection = () => { + useAssertSingleton('useSyncLangDirection'); + const { i18n, t } = useTranslation(); + + useEffect(() => { + const direction = i18n.dir(); + $direction.set(direction); + document.body.dir = direction; + }, [i18n, t]); +};