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}
+
+ }
+ style={{ flexShrink: 0 }}
+ >
+ {buttonText}
+
+
+
+
+
+
+ );
+};
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.')}
-
- }
- style={{ flexShrink: 0 }}
- >
- {t('defaultApp.setDefault', 'Set Default')}
-
-
-
-
-
-
+
);
};
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 */}
+ } />
+
+
+
);