51260793c0
Тост-уведомления Mantine сливались с фоном: у бесцветных тостов фон
карточки == var(--mantine-color-body) (белый, как страница) при слабой
тени, поэтому на белых страницах у карточки не было видимого края. Плюс
тосты всплывали снизу по центру и перекрывали контент.
Чиним глобально, без правок в 213 местах вызова:
- notification-overrides.css: каждому тосту даём тонированный по типу фон,
рамку с контрастом WCAG >= 3:1 и усиленную тень (shadow-xl). Селектор
[data-mantine-color-scheme=...] .mantine-Notification-root имеет
специфичность (0,2,0) и стабильно бьёт правила Mantine (0,1,0) (у Mantine
атрибут схемы обёрнут в :where()) — независимо от порядка стилей. Тон/рамка
идут от --notification-color (определён на том же элементе), поэтому следуют
типу тоста и покрывают loading/импортный тост (полосы-акцента нет — несут
тон+рамка+тень+цветной спиннер). Обе темы; текст-с-заголовком поднят до
gray-7 ради AA-контраста на тонированном фоне.
- main.tsx: position bottom-center -> top-center. Вертикальное смещение
контейнера ниже верхней хромы делаем НЕ инлайн-стилем, а CSS-правилом со
скоупом по позиции: Mantine рендерит все шесть позиционных контейнеров
одновременно, и корневой style-проп ушёл бы во все шесть — нижним (bottom:16)
добавился бы top:96 → position:fixed + оба края + height:auto растянули бы их
на весь вьюпорт; у корня нет pointer-events:none/фона → прозрачные оверлеи
z-10000 перехватывали бы клики по всей странице.
- notification-overrides.css: .mantine-Notifications-root[data-position^='top']
{ top:96px } (шапка 45 + опц. тулбар 45 + зазор). Скоуп ^='top' смещает
только верхние контейнеры; нижние остаются height:0 и кликов не перехватывают.
Специфичность (0,2,0) бьёт mantine top:16px (0,1,0), тост z-10000 стоит ниже
шапки/тулбара (z-99) и их не перекрывает.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
4.6 KiB
TypeScript
112 lines
4.6 KiB
TypeScript
import "@mantine/core/styles.css";
|
|
import "@mantine/spotlight/styles.css";
|
|
import "@mantine/notifications/styles.css";
|
|
import '@mantine/dates/styles.css';
|
|
import "@/styles/a11y-overrides.css";
|
|
import "@/styles/notification-overrides.css";
|
|
|
|
import ReactDOM from "react-dom/client";
|
|
import App from "./App.tsx";
|
|
import { mantineCssResolver, theme } from "@/theme";
|
|
import { MantineProvider } from "@mantine/core";
|
|
import { BrowserRouter } from "react-router-dom";
|
|
import { ModalsProvider } from "@mantine/modals";
|
|
import { Notifications } from "@mantine/notifications";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { HelmetProvider } from "react-helmet-async";
|
|
import { ChunkLoadErrorBoundary } from "@/components/chunk-load-error-boundary.tsx";
|
|
import "./i18n";
|
|
import {
|
|
getPostHogHost,
|
|
getPostHogKey,
|
|
isCloud,
|
|
isPostHogEnabled,
|
|
} from "@/lib/config.ts";
|
|
import { initVitals } from "@/lib/telemetry/vitals";
|
|
|
|
export const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
refetchOnMount: false,
|
|
refetchOnWindowFocus: false,
|
|
retry: false,
|
|
staleTime: 5 * 60 * 1000,
|
|
},
|
|
},
|
|
});
|
|
|
|
// #355 — client perf-telemetry. Decides sampling ONCE (25%/session) before
|
|
// subscribing to any observer; non-sampled sessions send nothing.
|
|
initVitals();
|
|
|
|
const container = document.getElementById("root") as HTMLElement;
|
|
const root = (container as any).__reactRoot ??= ReactDOM.createRoot(container);
|
|
|
|
function renderApp() {
|
|
root.render(
|
|
<BrowserRouter>
|
|
<MantineProvider theme={theme} cssVariablesResolver={mantineCssResolver}>
|
|
<ModalsProvider>
|
|
<QueryClientProvider client={queryClient}>
|
|
{/* top-center: toasts sit in the top of the viewport, in the line
|
|
of sight, and no longer cover centered content (e.g. "Load
|
|
more"). The below-chrome vertical offset is applied via a
|
|
position-scoped CSS rule in notification-overrides.css (NOT an
|
|
inline `style`): Mantine renders all six position containers at
|
|
once and an inline root style would land on every one, giving the
|
|
bottom-* containers both top+bottom → full-viewport transparent
|
|
overlays that swallow clicks. */}
|
|
<Notifications position="top-center" limit={3} zIndex={10000} />
|
|
<HelmetProvider>
|
|
{/* Root boundary above every lazy route's Suspense: a stale-chunk
|
|
404 after a deploy is caught and recovered here instead of
|
|
blanking the whole app. */}
|
|
<ChunkLoadErrorBoundary>
|
|
<App />
|
|
</ChunkLoadErrorBoundary>
|
|
</HelmetProvider>
|
|
</QueryClientProvider>
|
|
</ModalsProvider>
|
|
</MantineProvider>
|
|
</BrowserRouter>,
|
|
);
|
|
}
|
|
|
|
async function initAnalytics() {
|
|
// posthog-js is only pulled in for cloud deployments with analytics enabled, so
|
|
// self-hosted builds never download it. The gate is kept identical to the
|
|
// previous eager code so cloud analytics behavior is unchanged; the import is
|
|
// simply deferred behind it.
|
|
//
|
|
// Crucially this runs AFTER the immediate first render below, so first paint is
|
|
// never gated on the analytics chunk. Any failure (network, stale 404, or an
|
|
// ad-blocker blocking a chunk named "posthog") is swallowed so the user keeps a
|
|
// working app without analytics instead of a permanently blank page.
|
|
//
|
|
// NOTE: we init the posthog SINGLETON only and do NOT wrap the tree in
|
|
// <PostHogProvider>. The app has zero consumers of the PostHog React context
|
|
// (no usePostHog / useFeatureFlag* / PostHogFeature), and PostHogProvider given
|
|
// an already-initialized `client` is a no-op — all capture goes through the
|
|
// singleton. Re-rendering to attach the provider would only REMOUNT the whole
|
|
// App (running every mount effect twice and dropping local state / focus /
|
|
// in-progress input on cloud cold-load) for no functional gain.
|
|
if (!(isCloud() && isPostHogEnabled)) return;
|
|
try {
|
|
const { default: posthog } = await import("posthog-js");
|
|
posthog.init(getPostHogKey(), {
|
|
api_host: getPostHogHost(),
|
|
defaults: "2025-05-24",
|
|
disable_session_recording: true,
|
|
capture_pageleave: false,
|
|
});
|
|
} catch {
|
|
// Analytics failed to load — degrade gracefully; the app already rendered.
|
|
}
|
|
}
|
|
|
|
// Paint immediately for everyone (self-hosted stays exactly as instant as before,
|
|
// cloud no longer blocks on the analytics import). The posthog singleton is
|
|
// initialized after, without re-rendering the tree.
|
|
renderApp();
|
|
void initAnalytics();
|