Files
magistr/frontend/ui-foundation.css
dipatrik10 97bf3cc50a feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента
полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md).

- 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой
- Удалены legacy-views и переходный слой LegacyTabView (45 файлов)
- Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки
- CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf)
- Фиксы: lazy-импорт именованных экспортов (краш после авторизации),
  растяжение #admin-root/#settings-root на всю ширину,
  возврат subjects/lessonTypes из справочников (краш просмотра расписаний),
  отказ от заведомо широкого поиска расписания без выбранной цели,
  центрирование карточки логина
- Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину
- Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
2026-09-28 23:15:04 +03:00

488 lines
12 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Единая визуальная основа Magistr.
*
* Файл подключается последним во всех пользовательских оболочках и задаёт
* общие инварианты: типографику, размеры элементов управления, фокус,
* радиусы, ритм и поведение на узких экранах. Локальные CSS отвечают только
* за компоновку и предметные компоненты конкретного раздела.
*/
:root {
--ui-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--ui-font-display: var(--ui-font-sans);
--ui-font-mono: "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
--ui-control-sm: 32px;
--ui-control-md: 40px;
--ui-control-lg: 48px;
--ui-radius-control: 8px;
--ui-radius-field: 10px;
--ui-radius-panel: 16px;
--ui-radius-large: 24px;
--ui-space-1: 4px;
--ui-space-2: 8px;
--ui-space-3: 12px;
--ui-space-4: 16px;
--ui-space-5: 20px;
--ui-space-6: 24px;
--ui-space-8: 32px;
--ui-shadow-panel: 0 12px 32px rgba(15, 23, 42, 0.08);
--ui-shadow-floating: 0 20px 56px rgba(15, 23, 42, 0.18);
--ui-transition: 160ms cubic-bezier(0.2, 0, 0, 1);
--ui-focus-ring: var(--button-focus-ring, rgba(79, 70, 229, 0.35));
/* Совместимость с уже существующими токенами разделов. */
--button-radius: var(--ui-radius-control);
}
html {
color-scheme: dark;
font-size: 16px;
text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html[data-theme="light"] {
color-scheme: light;
}
body {
font-family: var(--ui-font-sans);
font-size: 1rem;
line-height: 1.5;
text-rendering: optimizeLegibility;
}
button,
input,
select,
textarea {
font: inherit;
letter-spacing: inherit;
}
button,
[role="button"],
a {
-webkit-tap-highlight-color: transparent;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--ui-font-display);
font-weight: 700;
line-height: 1.18;
letter-spacing: -0.025em;
text-wrap: balance;
}
p,
li,
dd {
text-wrap: pretty;
}
time,
input[type="date"],
input[type="time"],
.schedule-overview-next-time,
.academic-year-code,
.version-number,
.metric-value {
font-variant-numeric: tabular-nums;
}
::selection {
background: color-mix(in srgb, var(--accent, #4f46e5) 28%, transparent);
}
/* ===== Доступный фокус ===== */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
outline: 2px solid var(--accent, #4f46e5);
outline-offset: 2px;
}
:where(input, select, textarea):focus-visible {
border-color: var(--accent, #4f46e5);
box-shadow: 0 0 0 3px var(--ui-focus-ring);
}
/* ===== Кнопки ===== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--ui-space-2);
min-width: 0;
min-height: var(--ui-control-md);
padding: 0 var(--ui-space-4);
border: 1px solid var(--button-secondary-border, var(--panel-border, var(--bg-card-border)));
border-radius: var(--ui-radius-control);
font-family: var(--ui-font-sans);
font-size: 0.875rem;
font-weight: 650;
line-height: 1;
text-align: center;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
user-select: none;
transition:
background-color var(--ui-transition),
border-color var(--ui-transition),
color var(--ui-transition),
box-shadow var(--ui-transition),
transform var(--ui-transition);
}
.btn:hover:not(:disabled):not([aria-disabled="true"]) {
transform: translateY(-1px);
}
.btn:active:not(:disabled):not([aria-disabled="true"]) {
transform: translateY(0);
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--ui-focus-ring);
}
.btn:disabled,
.btn[aria-disabled="true"] {
cursor: not-allowed;
opacity: 0.62;
transform: none;
}
.btn-sm {
min-height: var(--ui-control-sm);
padding-inline: var(--ui-space-3);
font-size: 0.8125rem;
}
.btn-md {
min-height: var(--ui-control-md);
padding-inline: var(--ui-space-4);
font-size: 0.875rem;
}
.btn-lg {
min-height: var(--ui-control-lg);
padding-inline: var(--ui-space-5);
font-size: 0.9375rem;
font-weight: 700;
}
.btn-icon-sm,
.btn-icon-md {
flex: 0 0 auto;
padding: 0;
border-radius: var(--ui-radius-control);
}
.btn-icon-sm {
width: var(--ui-control-sm);
height: var(--ui-control-sm);
min-height: var(--ui-control-sm);
}
.btn-icon-md {
width: var(--ui-control-md);
height: var(--ui-control-md);
min-height: var(--ui-control-md);
}
.btn-primary {
border-color: var(--button-primary-border, var(--accent, #4f46e5));
background: var(--button-primary-bg, var(--accent, #4f46e5));
color: var(--button-primary-text, #fff);
}
.btn-primary:hover:not(:disabled) {
border-color: var(--button-primary-border, var(--accent-strong, var(--accent-hover, #4338ca)));
background: var(--button-primary-bg-hover, var(--accent-strong, var(--accent-hover, #4338ca)));
color: var(--button-primary-text, #fff);
}
.btn-secondary {
border-color: var(--button-secondary-border, var(--panel-border, var(--bg-card-border)));
background: var(--button-secondary-bg, var(--panel, var(--bg-card)));
color: var(--button-secondary-text, var(--text, var(--text-primary)));
}
.btn-secondary:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--accent, #4f46e5) 38%, var(--button-secondary-border, var(--panel-border, var(--bg-card-border))));
background: var(--button-secondary-bg-hover, var(--bg-hover, color-mix(in srgb, var(--accent, #4f46e5) 7%, var(--panel, var(--bg-card)))));
color: var(--button-secondary-text, var(--text, var(--text-primary)));
}
.btn-ghost {
border-color: var(--button-ghost-border, transparent);
background: transparent;
color: var(--text-secondary, var(--muted));
}
.btn-ghost:hover:not(:disabled) {
border-color: var(--button-ghost-border, var(--panel-border, var(--bg-card-border)));
background: var(--button-ghost-bg-hover, var(--bg-hover, rgba(148, 163, 184, 0.1)));
color: var(--text-primary, var(--text));
}
.btn-danger {
border-color: var(--button-danger-border, #b91c1c);
background: var(--button-danger-bg, #dc2626);
color: #fff;
}
.btn-danger-subtle {
border-color: var(--button-danger-subtle-border, rgba(239, 68, 68, 0.28));
background: var(--button-danger-subtle-bg, rgba(239, 68, 68, 0.12));
color: var(--button-danger-subtle-text, #b91c1c);
}
.btn-danger-subtle:hover:not(:disabled) {
border-color: var(--button-danger-subtle-border, rgba(239, 68, 68, 0.38));
background: var(--button-danger-subtle-bg-hover, rgba(239, 68, 68, 0.18));
color: var(--button-danger-subtle-text, #b91c1c);
}
.input-wrapper .toggle-password {
border-color: transparent;
background: transparent;
color: var(--text-placeholder, var(--muted));
}
/* ===== Поля форм ===== */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
select,
textarea {
border-radius: var(--ui-radius-field);
}
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
select {
min-height: var(--ui-control-md);
}
textarea {
min-height: 96px;
line-height: 1.5;
resize: vertical;
}
input::placeholder,
textarea::placeholder {
color: var(--text-placeholder, var(--muted));
opacity: 1;
}
label {
line-height: 1.35;
}
/* ===== Состояния React-срезов ===== */
.ui-fatal-state {
display: grid;
width: min(100% - 32px, 720px);
min-height: 100vh;
margin: 0 auto;
place-items: center;
}
.ui-state {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: var(--ui-space-4);
width: 100%;
padding: var(--ui-space-5);
border: 1px solid var(--panel-border, var(--bg-card-border, rgba(148, 163, 184, 0.24)));
border-radius: var(--ui-radius-panel);
background: var(--panel, var(--bg-card, rgba(255, 255, 255, 0.04)));
box-shadow: var(--ui-shadow-panel);
}
.ui-state__signal {
width: 12px;
height: 48px;
border-radius: 999px;
background: var(--accent, #4f46e5);
}
.ui-state--loading .ui-state__signal {
animation: ui-state-pulse 1.1s ease-in-out infinite alternate;
}
.ui-state--empty .ui-state__signal {
background: var(--text-secondary, #64748b);
}
.ui-state--error .ui-state__signal {
background: var(--error, #ef4444);
}
.ui-state__title {
margin: 0;
font-size: 1rem;
}
.ui-state__message {
margin: var(--ui-space-1) 0 0;
color: var(--text-secondary, var(--muted, #64748b));
font-size: 0.875rem;
}
@keyframes ui-state-pulse {
from { opacity: 0.45; transform: scaleY(0.68); }
to { opacity: 1; transform: scaleY(1); }
}
/* ===== Поверхности и ритм ===== */
.card,
.settings-card,
.toolbar,
.schedule-overview,
.teacher-absence-panel,
.preference-board,
.preference-card,
.request-ledger-head,
.request-dialog {
border-radius: var(--ui-radius-panel);
}
.card,
.settings-card,
.toolbar,
.schedule-overview {
box-shadow: var(--ui-shadow-panel);
}
.card:hover {
transform: none;
box-shadow: var(--ui-shadow-panel);
}
.card-header-row,
.settings-header-row,
.dashboard-card-header,
.preference-board-head {
gap: var(--ui-space-4);
}
.form-row,
.filter-row,
.actions,
.week-nav {
column-gap: var(--ui-space-3);
}
table {
font-variant-numeric: tabular-nums;
}
th {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.035em;
}
.table-actions-cell {
text-align: right;
}
.table-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--ui-space-2);
}
.schedule-visual-lesson {
padding-right: 2.55rem;
}
/* ===== Унифицированные кнопки закрытия ===== */
.modal-close,
.version-modal-close,
.settings-modal-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--ui-control-md);
height: var(--ui-control-md);
min-height: var(--ui-control-md);
padding: 0;
border: 1px solid var(--button-ghost-border, var(--panel-border, var(--bg-card-border)));
border-radius: var(--ui-radius-control);
background: transparent;
color: var(--text-secondary, var(--muted));
font-family: var(--ui-font-sans);
font-size: 1.35rem;
line-height: 1;
cursor: pointer;
transition: background-color var(--ui-transition), border-color var(--ui-transition), color var(--ui-transition);
}
.modal-close:hover,
.version-modal-close:hover,
.settings-modal-close:hover {
border-color: color-mix(in srgb, var(--error, #ef4444) 35%, transparent);
background: color-mix(in srgb, var(--error, #ef4444) 10%, transparent);
color: var(--error, #ef4444);
}
.modal-close--static {
position: static;
flex: 0 0 auto;
}
/* ===== Скролл и движение ===== */
* {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--text-secondary, var(--muted)) 42%, transparent) transparent;
}
@media (max-width: 640px) {
.shell {
padding: var(--ui-space-4);
}
.actions .btn:not(.btn-icon-sm):not(.btn-icon-md) {
flex: 1 1 auto;
}
.modal-content,
.request-dialog,
.version-modal-card {
border-radius: var(--ui-radius-panel);
}
.ui-state {
grid-template-columns: auto minmax(0, 1fr);
}
.ui-state .btn {
grid-column: 1 / -1;
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}