Files
bedolaga-cabinet/src/components/ErrorBoundary.tsx
c0mrade 7f68e2cd5e fix(theme): readable text on any operator palette + admin consistency pass
Контраст-аудит (WCAG-обход всех текстовых узлов, 4 палитры × юзер+админ
страницы) показал системные провалы читаемости: вторичный текст 1.5-2.5
на светлой/кастомных темах, статусные цвета 1.3-1.6, белый текст на
светлых акцентах 1.9.

Корневые фиксы:
- useThemeColors: контраст-кламп производных серых токенов — dark-400/
  champagne-600 (вторичный текст) держат >=5.0 к поверхности, dark-500/
  champagne-500 (подсказки) >=3.8; палитры с достаточным контрастом
  рендерятся байт-в-байт как раньше
- --color-on-accent/-success/-warning/-error: чёрный или белый текст
  по фактической яркости цвета оператора; text-white на accent-заливках
  заменён на text-on-accent (135 мест + variant примитива Button +
  .btn-primary)
- .light: статусные текстовые оттенки *-300/400 ремапятся на тёмные
  *-700 (жёлтая «Админка» 1.26 -> 3.8+, зелёные суммы 1.4 -> 3.4+,
  бейджи состояний)

Замер после: операторская тёмная палитра — 0 нарушений (всё >=4.5),
худшие точки остальных подняты с 1.26-1.6 до 3.2+.

Консистентность:
- заголовки админ-страниц приведены к text-xl font-bold text-dark-100
  (были 18/20/24/30px и 600/700 вперемешку)
- «Создать FAQ»: warning-500/80+белый -> warning-500+text-on-warning
- formatTraffic: единицы из i18n (убран микс «GB»/«ГБ» на одном экране)
- categoryCanvas -> categoryCANVAS в локалях (битый ключ на настройках)
- «Управление сквадами» -> «Управление локациями» (юзерский экран)
- мин-макс сумм на выборе метода: dark-600 -> dark-400
- неактивные пункты нижнего нава: dark-500 -> dark-400
2026-06-11 15:49:17 +03:00

122 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Component, type ErrorInfo, type ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
level?: 'app' | 'page' | 'widget';
onReset?: () => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
function isChunkLoadError(error: Error): boolean {
const msg = error.message || '';
return (
msg.includes('dynamically imported module') ||
msg.includes('Importing a module script failed') ||
msg.includes('Failed to fetch dynamically imported module') ||
msg.includes('Loading chunk') ||
msg.includes('ChunkLoadError') ||
error.name === 'ChunkLoadError'
);
}
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('[ErrorBoundary]', error, errorInfo);
// Auto-reload on chunk load failures (stale deploy)
if (isChunkLoadError(error)) {
const reloadKey = 'chunk_reload_ts';
const lastReload = Number(sessionStorage.getItem(reloadKey) || '0');
const now = Date.now();
// Prevent reload loop — only auto-reload once per 30 seconds
if (now - lastReload > 30_000) {
sessionStorage.setItem(reloadKey, String(now));
window.location.reload();
return;
}
}
}
handleReset = () => {
this.setState({ hasError: false, error: null });
this.props.onReset?.();
};
render() {
if (!this.state.hasError) {
return this.props.children;
}
const { level = 'page' } = this.props;
const isChunk = this.state.error ? isChunkLoadError(this.state.error) : false;
if (level === 'app') {
return (
<div className="min-h-viewport flex items-center justify-center bg-dark-900 p-4">
<div className="max-w-md text-center">
<div className="mb-4 text-4xl"></div>
<h1 className="mb-2 text-xl font-bold text-dark-50">Something went wrong</h1>
<p className="mb-6 text-dark-400">
An unexpected error occurred. Please try reloading the page.
</p>
<button
onClick={() => window.location.reload()}
className="rounded-xl bg-accent-500 px-6 py-3 font-medium text-on-accent transition-colors hover:bg-accent-600"
>
Reload page
</button>
</div>
</div>
);
}
if (level === 'widget') {
return (
<div className="rounded-xl border border-error-500/30 bg-error-500/10 p-4 text-center">
<p className="text-sm text-error-400">Failed to load this section</p>
<button
onClick={this.handleReset}
className="mt-2 text-sm text-accent-400 hover:text-accent-300"
>
Try again
</button>
</div>
);
}
// level === 'page' (default)
return (
<div className="flex min-h-[50vh] items-center justify-center p-4">
<div className="max-w-md text-center">
<div className="mb-4 text-4xl"></div>
<h1 className="mb-2 text-xl font-bold text-dark-50">Something went wrong</h1>
<p className="mb-6 text-sm text-dark-400">
{isChunk
? 'App was updated. Reloading...'
: this.state.error?.message || 'An unexpected error occurred'}
</p>
<button
onClick={() => window.location.reload()}
className="rounded-xl bg-accent-500 px-6 py-3 font-medium text-on-accent transition-colors hover:bg-accent-600"
>
{isChunk ? 'Reload' : 'Try again'}
</button>
</div>
</div>
);
}
}