Files
glchat/web/src/index.css
T

342 lines
8.7 KiB
CSS
Raw Normal View History

@import 'tailwindcss';
/*
* Дизайн-токены (docs/client-web.md §3). Тёмная тема по умолчанию,
* светлая — тот же набор токенов. Акцент сервера переопределяет --color-accent
* в скоупе сервера.
*/
@theme {
--color-bg: oklch(0.19 0.02 265);
--color-surface-1: oklch(0.23 0.02 265);
--color-surface-2: oklch(0.27 0.02 265);
--color-surface-3: oklch(0.32 0.02 265);
--color-fg: oklch(0.96 0.005 265);
--color-fg-muted: oklch(0.72 0.01 265);
--color-accent: oklch(0.66 0.16 265);
--color-accent-fg: oklch(0.99 0.005 265);
--color-danger: oklch(0.62 0.19 25);
--color-success: oklch(0.72 0.15 150);
--color-warning: oklch(0.79 0.14 85);
/* Тёмно-голубой: точка статуса «невидимый» в разделе друзей. */
--color-info: oklch(0.45 0.09 240);
--color-border: oklch(0.35 0.02 265);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--shadow-1: 0 1px 2px rgb(0 0 0 / 0.3);
--shadow-2: 0 4px 12px rgb(0 0 0 / 0.35);
--shadow-3: 0 12px 32px rgb(0 0 0 / 0.4);
--duration-fast: 120ms;
--duration-base: 200ms;
/* Шаг каскадного появления элементов списка (см. .gl-stagger). */
--stagger-step: 28ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
:root[data-theme='light'] {
--color-bg: oklch(0.98 0.003 265);
--color-surface-1: oklch(1 0 0);
--color-surface-2: oklch(0.96 0.004 265);
--color-surface-3: oklch(0.92 0.006 265);
--color-fg: oklch(0.2 0.02 265);
--color-fg-muted: oklch(0.48 0.015 265);
--color-border: oklch(0.87 0.008 265);
--shadow-1: 0 1px 2px rgb(0 0 0 / 0.08);
--shadow-2: 0 4px 12px rgb(0 0 0 / 0.1);
--shadow-3: 0 12px 32px rgb(0 0 0 / 0.12);
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
background-color: var(--color-bg);
color: var(--color-fg);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Inter,
sans-serif;
font-size: 14px;
line-height: 1.45;
}
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/*
* Анимации клиента (docs/client-web.md §3): сдержанные, быстрые, без
* зависимостей. Ключевые кадры и классы живут здесь, а не в inline-стилях,
* чтобы их можно было отключить одним media-запросом ниже.
*
* Все входные анимации используют fill-mode `backwards`: до старта элемент
* уже в начальном кадре (это нужно каскаду с задержками), а после финиша
* отдаёт управление обычным стилям — hover-трансформации продолжают работать.
*/
/* Появление страницы при смене маршрута: fade + сдвиг на 6px. */
@keyframes gl-page-in {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: none;
}
}
.gl-page {
animation: gl-page-in var(--duration-base) var(--ease-out) backwards;
}
/* Затемнение подложки модального окна. */
@keyframes gl-backdrop-in {
from {
background-color: rgb(0 0 0 / 0);
}
to {
background-color: rgb(0 0 0 / 0.6);
}
}
@keyframes gl-backdrop-out {
from {
background-color: rgb(0 0 0 / 0.6);
}
to {
background-color: rgb(0 0 0 / 0);
}
}
.gl-backdrop-in {
animation: gl-backdrop-in var(--duration-fast) var(--ease-out) backwards;
}
.gl-backdrop-out {
animation: gl-backdrop-out var(--duration-fast) var(--ease-out) forwards;
}
/* Панель модального окна: scale(0.96 → 1) с небольшим подъёмом. */
@keyframes gl-pop-in {
from {
opacity: 0;
transform: scale(0.96) translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes gl-pop-out {
from {
opacity: 1;
transform: none;
}
to {
opacity: 0;
transform: scale(0.96) translateY(8px);
}
}
.gl-pop-in {
animation: gl-pop-in var(--duration-base) var(--ease-out) backwards;
}
.gl-pop-out {
animation: gl-pop-out var(--duration-fast) var(--ease-out) forwards;
}
/* Всплывающее меню/поповер: fade + сдвиг от якоря (меню растёт вверх). */
@keyframes gl-popover-in {
from {
opacity: 0;
transform: translateY(6px) scale(0.98);
}
to {
opacity: 1;
transform: none;
}
}
.gl-popover {
transform-origin: bottom;
animation: gl-popover-in var(--duration-fast) var(--ease-out) backwards;
}
/* То же меню, но раскрывающееся вниз (действия у сообщения). */
@keyframes gl-popover-down-in {
from {
opacity: 0;
transform: translateY(-6px) scale(0.98);
}
to {
opacity: 1;
transform: none;
}
}
.gl-popover-down {
transform-origin: top;
animation: gl-popover-down-in var(--duration-fast) var(--ease-out) backwards;
}
/* Подсветка сообщения, к которому перешли из поиска или от ответа. */
@keyframes gl-highlight-fade {
from {
background-color: color-mix(in oklch, var(--color-accent) 28%, transparent);
}
to {
background-color: transparent;
}
}
.gl-highlight {
animation: gl-highlight-fade 1.6s var(--ease-out) backwards;
}
/*
* Каскадное появление списка. Задержка растёт только у первых восьми
* элементов — дальше список появляется без ступенек, чтобы длинные
* списки не «догоняли» пользователя.
*/
.gl-stagger > * {
animation: gl-page-in var(--duration-base) var(--ease-out) backwards;
}
.gl-stagger > *:nth-child(1) {
animation-delay: 0ms;
}
.gl-stagger > *:nth-child(2) {
animation-delay: calc(var(--stagger-step) * 1);
}
.gl-stagger > *:nth-child(3) {
animation-delay: calc(var(--stagger-step) * 2);
}
.gl-stagger > *:nth-child(4) {
animation-delay: calc(var(--stagger-step) * 3);
}
.gl-stagger > *:nth-child(5) {
animation-delay: calc(var(--stagger-step) * 4);
}
.gl-stagger > *:nth-child(6) {
animation-delay: calc(var(--stagger-step) * 5);
}
.gl-stagger > *:nth-child(7) {
animation-delay: calc(var(--stagger-step) * 6);
}
.gl-stagger > *:nth-child(8) {
animation-delay: calc(var(--stagger-step) * 7);
}
.gl-stagger > *:nth-child(n + 9) {
animation-delay: 0ms;
}
/* Индикатор активной комнаты: полоска слева плавно «вырастает». */
@keyframes gl-indicator-in {
from {
opacity: 0;
transform: translateY(-50%) scaleY(0.3);
}
to {
opacity: 1;
transform: translateY(-50%) scaleY(1);
}
}
.gl-indicator::before {
content: '';
position: absolute;
left: 0;
top: 50%;
width: 3px;
height: 60%;
border-radius: 999px;
background-color: var(--color-accent);
transform: translateY(-50%);
animation: gl-indicator-in var(--duration-base) var(--ease-out) backwards;
}
/* Индикатор активного сервера в рейке: кольцо, проявляющееся из центра. */
@keyframes gl-ring-in {
from {
opacity: 0;
transform: scale(0.88);
}
to {
opacity: 1;
transform: none;
}
}
.gl-ring::after {
content: '';
position: absolute;
inset: -3px;
border: 2px solid var(--color-accent);
border-radius: var(--radius-lg);
pointer-events: none;
animation: gl-ring-in var(--duration-base) var(--ease-out) backwards;
}
/* Скелетон загрузки: полоса с бегущим блеском. */
@keyframes gl-shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
.gl-skeleton {
display: block;
border-radius: var(--radius-sm);
background-color: var(--color-surface-2);
background-image: linear-gradient(
90deg,
var(--color-surface-2) 25%,
var(--color-surface-3) 50%,
var(--color-surface-2) 75%
);
background-size: 200% 100%;
animation: gl-shimmer 1.4s linear infinite;
}
/*
* Доступность: при `prefers-reduced-motion: reduce` анимации становятся
* мгновенными. Задержки каскада тоже обнуляем — иначе элементы списка
* появлялись бы по очереди без анимации.
*/
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-delay: 0ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
transition-delay: 0ms !important;
scroll-behavior: auto !important;
}
}