SHA256
UI: keep local shell updates
This commit is contained in:
+308
-3
@@ -52,8 +52,284 @@ window.__SHINE_BUILD_HASH__ = '20260819190000';
|
|||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
(function attachBootErrorOverlay() {
|
(function attachBootErrorOverlay() {
|
||||||
const show = (title, text) => {
|
const stateKey = '__SHINE_BOOT_ERROR_STATE__';
|
||||||
|
const menuId = 'boot-error-action-sheet';
|
||||||
|
const escapeText = (value) => String(value == null ? '' : value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
const safeString = (value, maxLen = 1000) => {
|
||||||
|
const text = String(value == null ? '' : value).trim();
|
||||||
|
if (text.length <= maxLen) return text;
|
||||||
|
return `${text.slice(0, Math.max(0, maxLen - 3))}...`;
|
||||||
|
};
|
||||||
|
const setState = (next) => {
|
||||||
try {
|
try {
|
||||||
|
window[stateKey] = next;
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
const getState = () => {
|
||||||
|
try {
|
||||||
|
return window[stateKey] || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getKnownClientError = () => {
|
||||||
|
try {
|
||||||
|
return typeof window.__SHINE_GET_LAST_CLIENT_ERROR__ === 'function'
|
||||||
|
? window.__SHINE_GET_LAST_CLIENT_ERROR__()
|
||||||
|
: null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showFeedback = (message, kind = 'success') => {
|
||||||
|
try {
|
||||||
|
if (typeof window.__SHINE_SHOW_TOAST__ === 'function') {
|
||||||
|
window.__SHINE_SHOW_TOAST__(message, kind);
|
||||||
|
} else {
|
||||||
|
console.info(message);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
const buildReport = () => {
|
||||||
|
const state = getState() || {};
|
||||||
|
const known = getKnownClientError();
|
||||||
|
const viewport = {
|
||||||
|
width: Math.round(window.innerWidth || 0),
|
||||||
|
height: Math.round(window.innerHeight || 0),
|
||||||
|
dpr: Number(window.devicePixelRatio || 1),
|
||||||
|
visualWidth: Math.round(window.visualViewport?.width || 0),
|
||||||
|
visualHeight: Math.round(window.visualViewport?.height || 0),
|
||||||
|
visualScale: Number(window.visualViewport?.scale || 1),
|
||||||
|
};
|
||||||
|
const screenInfo = window.screen ? {
|
||||||
|
width: Math.round(window.screen.width || 0),
|
||||||
|
height: Math.round(window.screen.height || 0),
|
||||||
|
availWidth: Math.round(window.screen.availWidth || 0),
|
||||||
|
availHeight: Math.round(window.screen.availHeight || 0),
|
||||||
|
pixelDepth: Number(window.screen.pixelDepth || 0),
|
||||||
|
} : null;
|
||||||
|
return {
|
||||||
|
kind: safeString(state.kind || 'boot_error', 64),
|
||||||
|
title: safeString(state.title || 'BOOT ERROR', 128),
|
||||||
|
message: safeString(state.message || '', 500),
|
||||||
|
stack: safeString(state.stack || '', 8000),
|
||||||
|
sourceUrl: safeString(state.sourceUrl || '', 240),
|
||||||
|
lineNumber: Number.isFinite(state.lineNumber) ? state.lineNumber : null,
|
||||||
|
columnNumber: Number.isFinite(state.columnNumber) ? state.columnNumber : null,
|
||||||
|
reasonType: safeString(state.reasonType || '', 64),
|
||||||
|
route: safeString(window.location.hash || window.location.pathname || '', 200),
|
||||||
|
href: safeString(window.location.href || '', 240),
|
||||||
|
pageTitle: safeString(document.title || '', 200),
|
||||||
|
pageVisibility: safeString(document.visibilityState || '', 32),
|
||||||
|
userAgent: safeString(navigator.userAgent || '', 240),
|
||||||
|
locale: safeString(navigator.language || '', 32),
|
||||||
|
clientTs: Number.isFinite(state.clientTs) ? state.clientTs : Date.now(),
|
||||||
|
viewport,
|
||||||
|
screenInfo,
|
||||||
|
lastKnownClientError: known || null,
|
||||||
|
contextJson: safeString(JSON.stringify({
|
||||||
|
bootState: state,
|
||||||
|
currentRoute: window.location.hash || window.location.pathname || '',
|
||||||
|
hasClientErrorSender: typeof window.__SHINE_SEND_CLIENT_ERROR__ === 'function',
|
||||||
|
}), 2000),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const buildSendPayload = (report) => {
|
||||||
|
const payload = {
|
||||||
|
kind: safeString(report?.kind || 'boot_error', 64),
|
||||||
|
message: safeString(report?.message || report?.title || 'Неизвестная ошибка', 500),
|
||||||
|
stack: safeString(report?.stack || '', 8000),
|
||||||
|
sourceUrl: safeString(report?.sourceUrl || '', 240),
|
||||||
|
lineNumber: Number.isFinite(report?.lineNumber) ? report.lineNumber : null,
|
||||||
|
columnNumber: Number.isFinite(report?.columnNumber) ? report.columnNumber : null,
|
||||||
|
route: safeString(report?.route || '', 200),
|
||||||
|
href: safeString(report?.href || '', 240),
|
||||||
|
userAgent: safeString(report?.userAgent || '', 240),
|
||||||
|
clientTs: Number.isFinite(report?.clientTs) ? report.clientTs : Date.now(),
|
||||||
|
requestOp: '',
|
||||||
|
requestIdRef: '',
|
||||||
|
contextJson: safeString(JSON.stringify({
|
||||||
|
...report,
|
||||||
|
contextJson: undefined,
|
||||||
|
}), 2000),
|
||||||
|
};
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
const shareErrorText = async (report) => {
|
||||||
|
const text = formatReportText(report);
|
||||||
|
if (!text) throw new Error('Текст ошибки пуст');
|
||||||
|
if (!navigator.share) {
|
||||||
|
throw new Error('Отправка через системное меню недоступна в этом браузере');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.share({
|
||||||
|
title: report?.title || 'Описание ошибки',
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError') return false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const formatReportText = (report) => {
|
||||||
|
const lines = [
|
||||||
|
`Ошибка: ${report.title || report.kind || 'unknown'}`,
|
||||||
|
`Описание: ${report.message || '—'}`,
|
||||||
|
`Окно: ${report.pageTitle || '—'}`,
|
||||||
|
`Маршрут: ${report.route || '—'}`,
|
||||||
|
`URL: ${report.href || '—'}`,
|
||||||
|
`Видимость: ${report.pageVisibility || '—'}`,
|
||||||
|
`Время: ${new Date(Number(report.clientTs || Date.now())).toISOString()}`,
|
||||||
|
`UA: ${report.userAgent || '—'}`,
|
||||||
|
`Экран: ${report.viewport ? `${report.viewport.width}x${report.viewport.height} @${report.viewport.dpr || 1}x` : '—'}`,
|
||||||
|
`Монитор: ${report.screenInfo ? `${report.screenInfo.width}x${report.screenInfo.height}` : '—'}`,
|
||||||
|
`Источник: ${report.sourceUrl || '—'}`,
|
||||||
|
`Строка: ${Number.isFinite(report.lineNumber) ? report.lineNumber : '—'}`,
|
||||||
|
`Колонка: ${Number.isFinite(report.columnNumber) ? report.columnNumber : '—'}`,
|
||||||
|
`Тип причины: ${report.reasonType || '—'}`,
|
||||||
|
];
|
||||||
|
if (report.stack) {
|
||||||
|
lines.push('Stack:', report.stack);
|
||||||
|
}
|
||||||
|
if (report.lastKnownClientError) {
|
||||||
|
lines.push('Последняя известная ошибка:', JSON.stringify(report.lastKnownClientError, null, 2));
|
||||||
|
}
|
||||||
|
if (report.contextJson) {
|
||||||
|
lines.push('Контекст:', report.contextJson);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
};
|
||||||
|
const copyText = async (text) => {
|
||||||
|
const value = String(text || '');
|
||||||
|
if (!value) return false;
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = value;
|
||||||
|
ta.setAttribute('readonly', 'readonly');
|
||||||
|
ta.style.position = 'fixed';
|
||||||
|
ta.style.opacity = '0';
|
||||||
|
ta.style.pointerEvents = 'none';
|
||||||
|
document.body.append(ta);
|
||||||
|
ta.focus();
|
||||||
|
ta.select();
|
||||||
|
const ok = document.execCommand('copy');
|
||||||
|
ta.remove();
|
||||||
|
return !!ok;
|
||||||
|
};
|
||||||
|
const removeMenu = () => {
|
||||||
|
try {
|
||||||
|
document.getElementById(menuId)?.remove();
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
const openMenu = () => {
|
||||||
|
try {
|
||||||
|
removeMenu();
|
||||||
|
const report = buildReport();
|
||||||
|
const root = document.getElementById('modal-root') || document.body;
|
||||||
|
const shell = document.createElement('div');
|
||||||
|
shell.id = menuId;
|
||||||
|
shell.className = 'modal-shell boot-error-menu-shell';
|
||||||
|
shell.innerHTML = `
|
||||||
|
<div class="modal-backdrop" data-action="close"></div>
|
||||||
|
<div class="modal-dialog boot-error-menu-dialog" role="dialog" aria-modal="true" aria-labelledby="boot-error-menu-title" tabindex="-1">
|
||||||
|
<div class="modal-card stack boot-error-menu-card">
|
||||||
|
<strong class="modal-title" id="boot-error-menu-title">Описание ошибки</strong>
|
||||||
|
<p class="meta-muted boot-error-menu-message">${escapeText(report.message || report.title || 'Ошибка')}</p>
|
||||||
|
<button type="button" class="secondary-btn" data-action="copy">Скопировать текст ошибки</button>
|
||||||
|
<button type="button" class="secondary-btn" data-action="share">Отправить текст ошибки</button>
|
||||||
|
<button type="button" class="secondary-btn" data-action="send"${typeof window.__SHINE_SEND_CLIENT_ERROR_TO_SERVER__ === 'function' ? '' : ' disabled'}>Отправить в лог на сервере</button>
|
||||||
|
<button type="button" class="ghost-btn" data-action="close">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
root.append(shell);
|
||||||
|
|
||||||
|
const dialog = shell.querySelector('.modal-dialog');
|
||||||
|
const close = () => {
|
||||||
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
|
shell.remove();
|
||||||
|
};
|
||||||
|
const onKeyDown = (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
shell.addEventListener('click', (event) => {
|
||||||
|
if (event.target === shell || event.target?.dataset?.action === 'close') {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
shell.querySelector('[data-action="copy"]')?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await copyText(formatReportText(report));
|
||||||
|
showFeedback('Описание ошибки скопировано');
|
||||||
|
} catch {
|
||||||
|
showFeedback('Не удалось скопировать описание ошибки', 'error');
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
|
||||||
|
shell.querySelector('[data-action="share"]')?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const shared = await shareErrorText(report);
|
||||||
|
if (shared === false) return close();
|
||||||
|
showFeedback('Текст ошибки открыт для отправки');
|
||||||
|
} catch (error) {
|
||||||
|
showFeedback(error?.message || 'Не удалось открыть системное меню отправки', 'error');
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
|
||||||
|
shell.querySelector('[data-action="send"]')?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const sender = window.__SHINE_SEND_CLIENT_ERROR_TO_SERVER__;
|
||||||
|
if (typeof sender !== 'function') {
|
||||||
|
throw new Error('Отправка в лог на сервере недоступна');
|
||||||
|
}
|
||||||
|
const ok = await sender(buildSendPayload(report));
|
||||||
|
if (!ok) {
|
||||||
|
throw new Error('Не удалось отправить ошибку в лог на сервере');
|
||||||
|
}
|
||||||
|
showFeedback('Ошибка отправлена в лог на сервере');
|
||||||
|
} catch (error) {
|
||||||
|
showFeedback(error?.message || 'Не удалось отправить ошибку в лог на сервере', 'error');
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
|
||||||
|
shell.querySelector('[data-action="close"]')?.addEventListener('click', close);
|
||||||
|
dialog?.focus?.();
|
||||||
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('boot error menu failed', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const show = (title, text, extra = {}) => {
|
||||||
|
try {
|
||||||
|
const reportState = {
|
||||||
|
kind: safeString(extra.kind || title || 'boot_error', 64),
|
||||||
|
title: safeString(title || 'BOOT ERROR', 128),
|
||||||
|
message: safeString(extra.message || text || '', 500),
|
||||||
|
stack: safeString(extra.stack || '', 8000),
|
||||||
|
sourceUrl: safeString(extra.sourceUrl || extra.filename || '', 240),
|
||||||
|
lineNumber: Number.isFinite(extra.lineNumber) ? extra.lineNumber : (Number.isFinite(extra.lineno) ? extra.lineno : null),
|
||||||
|
columnNumber: Number.isFinite(extra.columnNumber) ? extra.columnNumber : (Number.isFinite(extra.colno) ? extra.colno : null),
|
||||||
|
reasonType: safeString(extra.reasonType || '', 64),
|
||||||
|
clientTs: Number.isFinite(extra.clientTs) ? extra.clientTs : Date.now(),
|
||||||
|
};
|
||||||
|
setState(reportState);
|
||||||
|
|
||||||
let el = document.getElementById('boot-error-overlay');
|
let el = document.getElementById('boot-error-overlay');
|
||||||
if (!el) {
|
if (!el) {
|
||||||
el = document.createElement('pre');
|
el = document.createElement('pre');
|
||||||
@@ -72,17 +348,46 @@ window.__SHINE_BUILD_HASH__ = '20260819190000';
|
|||||||
el.style.lineHeight = '1.4';
|
el.style.lineHeight = '1.4';
|
||||||
el.style.zIndex = '999999';
|
el.style.zIndex = '999999';
|
||||||
el.style.whiteSpace = 'pre-wrap';
|
el.style.whiteSpace = 'pre-wrap';
|
||||||
|
el.style.cursor = 'pointer';
|
||||||
|
el.style.userSelect = 'none';
|
||||||
|
el.style.webkitUserSelect = 'none';
|
||||||
|
el.style.touchAction = 'manipulation';
|
||||||
|
el.setAttribute('role', 'button');
|
||||||
|
el.setAttribute('tabindex', '0');
|
||||||
|
el.setAttribute('aria-haspopup', 'dialog');
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
|
el.addEventListener('click', openMenu);
|
||||||
|
el.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
openMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
el.textContent = `[BOOT ERROR] ${title}\n${String(text || '')}`;
|
el.textContent = `[BOOT ERROR] ${title}\n${String(text || '')}`;
|
||||||
|
el.setAttribute('aria-label', `${title}. Нажмите, чтобы открыть меню действий.`);
|
||||||
|
el.title = 'Нажмите, чтобы открыть меню действий';
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
window.addEventListener('error', (e) => {
|
window.addEventListener('error', (e) => {
|
||||||
show('window.error', `${e?.message || ''}\n${e?.filename || ''}:${e?.lineno || ''}:${e?.colno || ''}`);
|
show('window.error', `${e?.message || ''}\n${e?.filename || ''}:${e?.lineno || ''}:${e?.colno || ''}`, {
|
||||||
|
kind: 'window_error',
|
||||||
|
message: e?.message || '',
|
||||||
|
stack: e?.error?.stack || '',
|
||||||
|
sourceUrl: e?.filename || '',
|
||||||
|
lineNumber: e?.lineno,
|
||||||
|
columnNumber: e?.colno,
|
||||||
|
reasonType: e?.error?.constructor?.name || e?.constructor?.name || 'ErrorEvent',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
window.addEventListener('unhandledrejection', (e) => {
|
window.addEventListener('unhandledrejection', (e) => {
|
||||||
const reason = e?.reason;
|
const reason = e?.reason;
|
||||||
show('unhandledrejection', reason?.stack || reason?.message || String(reason || 'unknown'));
|
show('unhandledrejection', reason?.stack || reason?.message || String(reason || 'unknown'), {
|
||||||
|
kind: 'unhandled_rejection',
|
||||||
|
message: reason?.message || String(reason || 'Unhandled promise rejection'),
|
||||||
|
stack: reason?.stack || '',
|
||||||
|
reasonType: reason?.constructor?.name || typeof reason,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}());
|
}());
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+10
-1
@@ -5,7 +5,12 @@ import {
|
|||||||
syncTrackedRouteHistory,
|
syncTrackedRouteHistory,
|
||||||
} from './router.js';
|
} from './router.js';
|
||||||
import { renderToolbar } from './components/toolbar.js';
|
import { renderToolbar } from './components/toolbar.js';
|
||||||
import { captureClientError, setClientErrorSentNotifier, setClientErrorTransport } from './services/client-error-reporter.js';
|
import {
|
||||||
|
captureClientError,
|
||||||
|
getLastClientErrorPayload,
|
||||||
|
setClientErrorSentNotifier,
|
||||||
|
setClientErrorTransport,
|
||||||
|
} from './services/client-error-reporter.js';
|
||||||
import { initPwaInstallPromptHandling } from './services/pwa-install-service.js';
|
import { initPwaInstallPromptHandling } from './services/pwa-install-service.js';
|
||||||
import { initPwaPush } from './services/pwa-push-service.js';
|
import { initPwaPush } from './services/pwa-push-service.js';
|
||||||
import { initCallUiOverlay } from './services/call-ui-service.js';
|
import { initCallUiOverlay } from './services/call-ui-service.js';
|
||||||
@@ -208,6 +213,10 @@ setClientErrorSentNotifier((payload) => {
|
|||||||
const isoTs = new Date(Number(payload?.clientTs || Date.now())).toISOString();
|
const isoTs = new Date(Number(payload?.clientTs || Date.now())).toISOString();
|
||||||
showToast(`Ошибка отправлена на сервер · ${login} · ${isoTs}`);
|
showToast(`Ошибка отправлена на сервер · ${login} · ${isoTs}`);
|
||||||
});
|
});
|
||||||
|
window.__SHINE_SEND_CLIENT_ERROR_TO_SERVER__ = (payload) => authService.reportClientError(payload);
|
||||||
|
window.__SHINE_SEND_CLIENT_ERROR__ = window.__SHINE_SEND_CLIENT_ERROR_TO_SERVER__;
|
||||||
|
window.__SHINE_GET_LAST_CLIENT_ERROR__ = () => getLastClientErrorPayload();
|
||||||
|
window.__SHINE_SHOW_TOAST__ = (message, kind = 'success') => showToast(message, { kind });
|
||||||
initPwaInstallPromptHandling();
|
initPwaInstallPromptHandling();
|
||||||
initCallUiOverlay();
|
initCallUiOverlay();
|
||||||
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ let transport = null;
|
|||||||
let transportDepth = 0;
|
let transportDepth = 0;
|
||||||
const recentFingerprints = new Map();
|
const recentFingerprints = new Map();
|
||||||
let notifySent = null;
|
let notifySent = null;
|
||||||
|
let lastCapturedPayload = null;
|
||||||
|
|
||||||
function nowTs() {
|
function nowTs() {
|
||||||
return Date.now();
|
return Date.now();
|
||||||
@@ -85,6 +86,11 @@ export function setClientErrorSentNotifier(fn) {
|
|||||||
notifySent = typeof fn === 'function' ? fn : null;
|
notifySent = typeof fn === 'function' ? fn : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLastClientErrorPayload() {
|
||||||
|
if (!lastCapturedPayload) return null;
|
||||||
|
return { ...lastCapturedPayload };
|
||||||
|
}
|
||||||
|
|
||||||
export function isClientErrorReportingEnabled() {
|
export function isClientErrorReportingEnabled() {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(UI_ERROR_REPORTING_KEY) === '1';
|
return localStorage.getItem(UI_ERROR_REPORTING_KEY) === '1';
|
||||||
@@ -104,6 +110,7 @@ export function setClientErrorReportingEnabled(enabled) {
|
|||||||
export async function captureClientError(details = {}) {
|
export async function captureClientError(details = {}) {
|
||||||
const payload = buildPayload(details);
|
const payload = buildPayload(details);
|
||||||
if (!payload.message) return false;
|
if (!payload.message) return false;
|
||||||
|
lastCapturedPayload = payload;
|
||||||
|
|
||||||
const fingerprint = details.dedupeKey || makeFingerprint(payload);
|
const fingerprint = details.dedupeKey || makeFingerprint(payload);
|
||||||
if (isDuplicate(fingerprint)) return false;
|
if (isDuplicate(fingerprint)) return false;
|
||||||
|
|||||||
@@ -3324,6 +3324,10 @@ textarea.input {
|
|||||||
z-index: 24;
|
z-index: 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.boot-error-menu-shell {
|
||||||
|
z-index: 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -3341,6 +3345,16 @@ textarea.input {
|
|||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.boot-error-menu-card {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boot-error-menu-message {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.network-board {
|
.network-board {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 290px;
|
height: 290px;
|
||||||
|
|||||||
Reference in New Issue
Block a user