diff --git a/frontend/src/core/App.tsx b/frontend/src/core/App.tsx index e87843325..c21ad03b3 100644 --- a/frontend/src/core/App.tsx +++ b/frontend/src/core/App.tsx @@ -1,5 +1,6 @@ import { Suspense } from "react"; import { AppProviders } from "@app/components/AppProviders"; +import { AppLayout } from "@app/components/AppLayout"; import { LoadingFallback } from "@app/components/shared/LoadingFallback"; import HomePage from "@app/pages/HomePage"; import OnboardingTour from "@app/components/onboarding/OnboardingTour"; @@ -16,8 +17,10 @@ export default function App() { return ( }> - - + + + + ); diff --git a/frontend/src/core/components/AppLayout.tsx b/frontend/src/core/components/AppLayout.tsx new file mode 100644 index 000000000..39de5dc65 --- /dev/null +++ b/frontend/src/core/components/AppLayout.tsx @@ -0,0 +1,31 @@ +import { ReactNode } from 'react'; +import { useBanner } from '@app/contexts/BannerContext'; + +interface AppLayoutProps { + children: ReactNode; +} + +/** + * App layout wrapper that handles banner rendering and viewport sizing + * Automatically adjusts child components to fit remaining space after banner + */ +export function AppLayout({ children }: AppLayoutProps) { + const { banner } = useBanner(); + + return ( + <> + +
+ {banner} +
+ {children} +
+
+ + ); +} diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index 24f793188..7b09bcc8a 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -16,6 +16,7 @@ import { OnboardingProvider } from "@app/contexts/OnboardingContext"; import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext"; import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext"; import { PageEditorProvider } from "@app/contexts/PageEditorContext"; +import { BannerProvider } from "@app/contexts/BannerContext"; import ErrorBoundary from "@app/components/shared/ErrorBoundary"; import { useScarfTracking } from "@app/hooks/useScarfTracking"; import { useAppInitialization } from "@app/hooks/useAppInitialization"; @@ -50,22 +51,23 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -76,16 +78,17 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide - - - - - - - - - - + + + + + + + + + + + diff --git a/frontend/src/core/components/shared/InfoBanner.tsx b/frontend/src/core/components/shared/InfoBanner.tsx new file mode 100644 index 000000000..f29406c2e --- /dev/null +++ b/frontend/src/core/components/shared/InfoBanner.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Paper, Group, Text, Button, ActionIcon } from '@mantine/core'; +import LocalIcon from '@app/components/shared/LocalIcon'; + +interface InfoBannerProps { + icon: string; + message: string; + buttonText: string; + buttonIcon?: string; + onButtonClick: () => void; + onDismiss: () => void; + loading?: boolean; + show?: boolean; +} + +/** + * Generic info banner component for displaying dismissible messages at the top of the app + */ +export const InfoBanner: React.FC = ({ + icon, + message, + buttonText, + buttonIcon = 'check-circle-rounded', + onButtonClick, + onDismiss, + loading = false, + show = true, +}) => { + if (!show) { + return null; + } + + return ( + + + + + {message} + + + + + + + + ); +}; diff --git a/frontend/src/core/contexts/BannerContext.tsx b/frontend/src/core/contexts/BannerContext.tsx new file mode 100644 index 000000000..810d50bd6 --- /dev/null +++ b/frontend/src/core/contexts/BannerContext.tsx @@ -0,0 +1,26 @@ +import { createContext, useContext, useState, ReactNode } from 'react'; + +interface BannerContextType { + banner: ReactNode; + setBanner: (banner: ReactNode) => void; +} + +const BannerContext = createContext(undefined); + +export function BannerProvider({ children }: { children: ReactNode }) { + const [banner, setBanner] = useState(null); + + return ( + + {children} + + ); +} + +export function useBanner() { + const context = useContext(BannerContext); + if (!context) { + throw new Error('useBanner must be used within BannerProvider'); + } + return context; +} diff --git a/frontend/src/desktop/App.tsx b/frontend/src/desktop/App.tsx deleted file mode 100644 index 89e092aa3..000000000 --- a/frontend/src/desktop/App.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Suspense } from "react"; -import { Routes, Route } from "react-router-dom"; -import { AppProviders } from "@app/components/AppProviders"; -import { LoadingFallback } from "@app/components/shared/LoadingFallback"; -import Landing from "@app/routes/Landing"; -import Login from "@app/routes/Login"; -import Signup from "@app/routes/Signup"; -import AuthCallback from "@app/routes/AuthCallback"; -import InviteAccept from "@app/routes/InviteAccept"; -import OnboardingTour from "@app/components/onboarding/OnboardingTour"; -import { DefaultAppBanner } from "@app/components/shared/DefaultAppBanner"; - -// Import global styles -import "@app/styles/tailwind.css"; -import "@app/styles/cookieconsent.css"; -import "@app/styles/index.css"; -import "@app/styles/auth-theme.css"; - -// Import file ID debugging helpers (development only) -import "@app/utils/fileIdSafety"; - -export default function App() { - return ( - }> - -
- -
- - - {/* Auth routes - no nested providers needed */} - } /> - } /> - } /> - } /> - - {/* Main app routes - Landing handles auth logic */} - } /> - - -
-
-
-
- ); -} diff --git a/frontend/src/desktop/components/AppProviders.tsx b/frontend/src/desktop/components/AppProviders.tsx index c04ede4f9..f5363b5e7 100644 --- a/frontend/src/desktop/components/AppProviders.tsx +++ b/frontend/src/desktop/components/AppProviders.tsx @@ -1,6 +1,7 @@ import { ReactNode } from "react"; import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders"; import { DesktopConfigSync } from '@app/components/DesktopConfigSync'; +import { DesktopBannerInitializer } from '@app/components/DesktopBannerInitializer'; import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig'; /** @@ -22,6 +23,7 @@ export function AppProviders({ children }: { children: ReactNode }) { }} > + {children} ); diff --git a/frontend/src/desktop/components/DesktopBannerInitializer.tsx b/frontend/src/desktop/components/DesktopBannerInitializer.tsx new file mode 100644 index 000000000..9a74f0511 --- /dev/null +++ b/frontend/src/desktop/components/DesktopBannerInitializer.tsx @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import { useBanner } from '@app/contexts/BannerContext'; +import { DefaultAppBanner } from '@app/components/shared/DefaultAppBanner'; + +export function DesktopBannerInitializer() { + const { setBanner } = useBanner(); + + useEffect(() => { + setBanner(); + }, [setBanner]); + + return null; +} diff --git a/frontend/src/desktop/components/shared/DefaultAppBanner.tsx b/frontend/src/desktop/components/shared/DefaultAppBanner.tsx index 1e75faa01..a0f617c86 100644 --- a/frontend/src/desktop/components/shared/DefaultAppBanner.tsx +++ b/frontend/src/desktop/components/shared/DefaultAppBanner.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { Paper, Group, Text, Button, ActionIcon } from '@mantine/core'; import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; +import { InfoBanner } from '@app/components/shared/InfoBanner'; import { defaultAppService } from '@app/services/defaultAppService'; import { alert } from '@app/components/toast'; @@ -64,47 +63,16 @@ export const DefaultAppBanner: React.FC = () => { localStorage.setItem(PROMPT_DISMISSED_KEY, 'true'); }; - if (promptDismissed || isDefault !== false) { - return null; - } - return ( - - - - - {t('defaultApp.prompt.message', 'Make Stirling PDF your default application for opening PDF files.')} - - - - - - - + ); }; diff --git a/frontend/src/proprietary/App.tsx b/frontend/src/proprietary/App.tsx index eba2fa5c4..9edb9ab83 100644 --- a/frontend/src/proprietary/App.tsx +++ b/frontend/src/proprietary/App.tsx @@ -1,6 +1,7 @@ import { Suspense } from "react"; import { Routes, Route } from "react-router-dom"; import { AppProviders } from "@app/components/AppProviders"; +import { AppLayout } from "@app/components/AppLayout"; import { LoadingFallback } from "@app/components/shared/LoadingFallback"; import Landing from "@app/routes/Landing"; import Login from "@app/routes/Login"; @@ -22,17 +23,19 @@ export default function App() { return ( }> - - {/* Auth routes - no nested providers needed */} - } /> - } /> - } /> - } /> + + + {/* Auth routes - no nested providers needed */} + } /> + } /> + } /> + } /> - {/* Main app routes - Landing handles auth logic */} - } /> - - + {/* Main app routes - Landing handles auth logic */} + } /> + + + );