Files
gitmost/apps/client/src/main.tsx
T
agent_coder 51260793c0 fix(ui/toasts): глобальная видимость тостов + перенос в top-center (#517)
Тост-уведомления 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>
2026-07-12 01:14:38 +03:00

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();