From 51260793c0b70f376ca66a8ffe97ad6a56831173 Mon Sep 17 00:00:00 2001 From: agent_coder Date: Sun, 12 Jul 2026 01:03:48 +0300 Subject: [PATCH] =?UTF-8?q?fix(ui/toasts):=20=D0=B3=D0=BB=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B8=D0=B4=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D1=82=D0=BE=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20+=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20?= =?UTF-8?q?=D0=B2=20top-center=20(#517)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тост-уведомления 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) --- apps/client/src/main.tsx | 11 +++- .../src/styles/notification-overrides.css | 64 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 apps/client/src/styles/notification-overrides.css diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx index eb94771d..caa4545d 100644 --- a/apps/client/src/main.tsx +++ b/apps/client/src/main.tsx @@ -3,6 +3,7 @@ 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"; @@ -47,7 +48,15 @@ function renderApp() { - + {/* 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. */} + {/* Root boundary above every lazy route's Suspense: a stale-chunk 404 after a deploy is caught and recovered here instead of diff --git a/apps/client/src/styles/notification-overrides.css b/apps/client/src/styles/notification-overrides.css new file mode 100644 index 00000000..a1dbbe33 --- /dev/null +++ b/apps/client/src/styles/notification-overrides.css @@ -0,0 +1,64 @@ +/* + * Toast (Mantine Notification) visibility overrides. + * Mantine renders colorless toasts on --mantine-color-body (== the page + * background: white in light mode) with a faint shadow, so on white pages the + * card has no visible edge. These rules give every toast a type-tinted + * background, a WCAG-checked border and a stronger shadow so it separates from + * the page. The [data-mantine-color-scheme] + static-class selector (0,2,0) + * beats Mantine's own (0,1,0) rules regardless of stylesheet order (Mantine's + * bg/border rules wrap the scheme attribute in :where(), so they stay (0,1,0)). + * --notification-color is defined on the same element (defaults to primary, + * set per `color` prop), so tint/border follow the toast type. This also covers + * the loading/import toast (no accent bar, since the spinner takes the icon + * slot): its visibility comes from tone + border + shadow + the colored spinner. + */ + +/* + * Push the top-anchored toast containers below the top chrome (fixed 45px + * header + optional 45px format toolbar + ~6px gap) so a toast (z-index 10000) + * neither covers nor intercepts clicks on the header/toolbar (both z-index 99). + * + * Scoped to [data-position^='top'] on purpose. Mantine renders ALL SIX position + * containers simultaneously (`position` only routes toasts into one via the + * store); the root `style` prop would be applied to every one of them by + * getStyles("root"). A blanket `top` would land on the bottom-* containers too + * (which carry `bottom:16px`) → position:fixed + both edges + height:auto makes + * them stretch the full viewport height, and the container root has neither + * pointer-events:none nor a background, so those transparent z-10000 overlays + * would swallow clicks across the whole page. Restricting to top-* leaves the + * bottom containers at height:0. + * + * Specificity: `.mantine-Notifications-root[data-position^='top']` is (0,2,0) + * (class + attribute) and beats Mantine's own top rule + * `.m_b37d9ac7:where([data-position='top-center']){top:16px}` which is (0,1,0) + * (the :where() contributes 0), regardless of stylesheet order. + */ +.mantine-Notifications-root[data-position^='top'] { + top: 96px; +} + +[data-mantine-color-scheme='light'] .mantine-Notification-root { + /* ~10% type color over white: clearly off-white, text contrast preserved */ + background-color: color-mix(in srgb, var(--notification-color) 10%, var(--mantine-color-white)); + /* Border must clear WCAG 3:1 non-text contrast on white. The repo rejects + gray-4 for this (a11y-overrides.css); gray-6 base (~3.32:1) darkened by the + type color stays >= 3:1. */ + border: 1px solid color-mix(in srgb, var(--notification-color) 45%, var(--mantine-color-gray-6)); + box-shadow: var(--mantine-shadow-xl); +} + +[data-mantine-color-scheme='dark'] .mantine-Notification-root { + /* Dark page (dark-7/8) vs toast (dark-6) already separate a little; border + + shadow carry the type cue here (a 7% dark tint was near-invisible). */ + background-color: color-mix(in srgb, var(--notification-color) 14%, var(--mantine-color-dark-6)); + border: 1px solid color-mix(in srgb, var(--notification-color) 45%, var(--mantine-color-dark-3)); + box-shadow: var(--mantine-shadow-xl); +} + +/* Mantine's message-with-title color is gray-6 (#868e96, already only ~3.32:1 + on white — below AA 4.5:1); the new tint pushes it lower. Bump to gray-7 to + keep multi-line colored toasts readable, consistent with the repo's existing + WCAG tuning (theme.ts already bumps this same gray-6 up elsewhere). */ +[data-mantine-color-scheme='light'] .mantine-Notification-description[data-with-title] { + color: var(--mantine-color-gray-7); +}