991ddebe90
Root causes the prior attempts missed: 1) Safari iOS/iPadOS ignores `interactive-widget=resizes-content`, so `100dvh` never shrinks when the soft keyboard opens. Every previous override that used `dvh` to size the dialog was sized to the full screen and ran behind the keyboard. 2) The CSS rule from #622 (`[role="dialog"]:has(:focus)`) was unconditional. On desktop, focusing any input inside a Radix DialogContent instantly stretched it to width:100vw + top:0, which collided with Radix's open-state slide-animation transforms and rendered the dialog as a broken narrow strip pinned in the top-left corner — visible in Tahani's laptop screenshot when opening "Add Meeting". 3) `AdminFormDialog` puts `role="dialog"` on a NON-fixed inner card, so the rule's `top/left/right` had no effect there anyway; the card stayed centred in a layout-viewport-sized wrapper which iOS does not shrink. Fix: - New `useVisualViewportVars` hook mounted in NotificationsSocketBridge. Writes `--vv-height` and `--vv-offset-top` on `<html>` from `window.visualViewport`, updated on resize / scroll / orientationchange. Falls back to `innerHeight`/0 if visualViewport is absent. - Rewrote the index.css dialog rule: * Gated inside `@media (any-pointer: coarse)` so desktop is completely unaffected. * Uses `var(--vv-height, 100vh)` and `var(--vv-offset-top, 0px)` instead of `100dvh` / `0`, so dialogs actually track the visible band on iOS. - New `.dialog-vv-wrapper` helper class that applies the same vv-sized top/height to bespoke full-screen overlay wrappers. - Applied `.dialog-vv-wrapper` to AdminFormDialog and the admin ConfirmDialog wrappers; switched their `fixed inset-0` to `fixed inset-x-0 top-0 h-screen` and their inner cards from `max-h-[90vh]`/`max-h-[85vh]` to `max-h-full` so the card shrinks with the wrapper. Kept `useScrollFocusedDialogField` unchanged — works correctly once the dialog itself is sized to the visible band. `tsc --noEmit` clean.
106 lines
4.2 KiB
TypeScript
106 lines
4.2 KiB
TypeScript
import { Switch, Route, Redirect, Router as WouterRouter, useLocation } from "wouter";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { Toaster } from "@/components/ui/toaster";
|
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
import { AuthProvider } from "@/contexts/AuthContext";
|
|
import { IncomingNotePopupProvider } from "@/contexts/IncomingNotePopupContext";
|
|
import { IncomingNotePopup } from "@/components/notes/incoming-note-popup";
|
|
import { useNotificationsSocket } from "@/hooks/use-notifications-socket";
|
|
import { useAudioUnlock } from "@/hooks/use-audio-unlock";
|
|
import { useAutoplayHint } from "@/hooks/use-autoplay-hint";
|
|
import { useScrollFocusedDialogField } from "@/hooks/use-scroll-focused-dialog-field";
|
|
import { useVisualViewportVars } from "@/hooks/use-visual-viewport-vars";
|
|
import { UpcomingMeetingAlert } from "@/components/executive-meetings/upcoming-meeting-alert";
|
|
import { PushEnablePrompt } from "@/components/push-enable-prompt";
|
|
import NotFound from "@/pages/not-found";
|
|
import LoginPage from "@/pages/login";
|
|
import RegisterPage from "@/pages/register";
|
|
import ForgotPasswordPage from "@/pages/forgot-password";
|
|
import ResetPasswordPage from "@/pages/reset-password";
|
|
import HomePage from "@/pages/home";
|
|
import ServicesPage from "@/pages/services";
|
|
import NotificationsPage from "@/pages/notifications";
|
|
import AdminPage from "@/pages/admin";
|
|
import NotesPage from "@/pages/notes";
|
|
import MyOrdersPage from "@/pages/my-orders";
|
|
import OrdersIncomingPage from "@/pages/orders-incoming";
|
|
import ExecutiveMeetingsPage from "@/pages/executive-meetings";
|
|
import EmbeddedAppPage from "@/pages/embedded-app";
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
staleTime: 30_000,
|
|
},
|
|
},
|
|
});
|
|
|
|
function ExecutiveMeetingsRedirect() {
|
|
const [location] = useLocation();
|
|
const suffix =
|
|
typeof window !== "undefined" ? window.location.search + window.location.hash : "";
|
|
const subPath = location.replace(/^\/executive-meetings/, "");
|
|
return <Redirect to={`/meetings${subPath}${suffix}`} replace />;
|
|
}
|
|
|
|
function NotificationsSocketBridge() {
|
|
useNotificationsSocket();
|
|
useAudioUnlock();
|
|
useAutoplayHint();
|
|
useScrollFocusedDialogField();
|
|
useVisualViewportVars();
|
|
return null;
|
|
}
|
|
|
|
function Router() {
|
|
return (
|
|
<AuthProvider>
|
|
<IncomingNotePopupProvider>
|
|
<NotificationsSocketBridge />
|
|
<UpcomingMeetingAlert />
|
|
<IncomingNotePopup />
|
|
<PushEnablePrompt />
|
|
<Switch>
|
|
<Route path="/login" component={LoginPage} />
|
|
<Route path="/register" component={RegisterPage} />
|
|
<Route path="/forgot-password" component={ForgotPasswordPage} />
|
|
<Route path="/reset-password" component={ResetPasswordPage} />
|
|
<Route path="/services" component={ServicesPage} />
|
|
<Route path="/notifications" component={NotificationsPage} />
|
|
<Route path="/admin" component={AdminPage} />
|
|
<Route path="/notes" component={NotesPage} />
|
|
<Route path="/my-orders" component={MyOrdersPage} />
|
|
<Route path="/orders/incoming" component={OrdersIncomingPage} />
|
|
<Route path="/meetings" component={ExecutiveMeetingsPage} />
|
|
{/* Back-compat redirect: the page used to live at
|
|
/executive-meetings. Stored PDFs, email links, and bookmarks
|
|
still point there, so catch the old path (and any deep
|
|
sub-path) and forward to /meetings while preserving the
|
|
user's query string and hash. */}
|
|
<Route path="/executive-meetings" component={ExecutiveMeetingsRedirect} />
|
|
<Route path="/executive-meetings/:rest*" component={ExecutiveMeetingsRedirect} />
|
|
<Route path="/embedded/:id" component={EmbeddedAppPage} />
|
|
<Route path="/" component={HomePage} />
|
|
<Route component={NotFound} />
|
|
</Switch>
|
|
</IncomingNotePopupProvider>
|
|
</AuthProvider>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<TooltipProvider>
|
|
<WouterRouter base={import.meta.env.BASE_URL.replace(/\/$/, "")}>
|
|
<Router />
|
|
</WouterRouter>
|
|
<Toaster />
|
|
</TooltipProvider>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
export default App;
|