feat(admin-ui): softer pre-material styling and dark theme

- Rebuild admin console styling on semantic CSS variables (light + dark palettes)
- Soften palette, shadows and corner radii for a calmer pre-material look
- Add theme provider with light/dark toggle in the topbar and login header
- Respect prefers-color-scheme and persist choice, with anti-FOUC inline script
- Polish Russian translations (yo letters, consistent 'Звёзды' currency, wording)
- Rebuild dist bundle to match the updated source
This commit is contained in:
epilepticseizureee 2026-07-22 23:45:09 +03:00
parent 045e0f6db4
commit ad9d535edc
15 changed files with 601 additions and 251 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,30 @@
<!doctype html>
<html lang="en" translate="no">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title>
<script type="module" crossorigin src="/assets/index-Duge82ST.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DHdrFM5j.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
<!doctype html>
<html lang="en" translate="no">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title>
<script>
(function () {
try {
var stored = localStorage.getItem("telesrv.admin.theme");
var theme =
stored === "light" || stored === "dark"
? stored
: window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;
} catch (e) {
document.documentElement.setAttribute("data-theme", "light");
}
})();
</script>
<script type="module" crossorigin src="/assets/index-CqgHld2y.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DuOdm70q.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View file

@ -4,6 +4,23 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title>
<script>
(function () {
try {
var stored = localStorage.getItem("telesrv.admin.theme");
var theme =
stored === "light" || stored === "dark"
? stored
: window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;
} catch (e) {
document.documentElement.setAttribute("data-theme", "light");
}
})();
</script>
</head>
<body>
<div id="root"></div>

View file

@ -14,6 +14,7 @@ import { useEffect, useState, type ReactNode } from "react";
import { api } from "../api";
import { LanguageSwitch, useI18n } from "../i18n";
import { type Navigate, type RouteState, routeSubtitle, routeTitle } from "../routing";
import { ThemeSwitch } from "../theme";
import { AppLink } from "./AppLink";
export function BootScreen() {
@ -123,6 +124,7 @@ export function Shell({
<h1>{routeTitle(route.path, t)}</h1>
</div>
<div className="topbar-actions">
<ThemeSwitch />
<LanguageSwitch />
<span className="actor-pill">{t("layout.actor", { actor })}</span>
<button className="btn ghost icon-text" type="button" onClick={logout} title={t("layout.logout")}>

View file

@ -84,6 +84,8 @@ const translations: Record<Language, Record<string, string>> = {
"language.en": "EN",
"language.zh": "中文",
"language.ru": "RU",
"theme.switchToDark": "Switch to dark theme",
"theme.switchToLight": "Switch to light theme",
"login.heading": "Operations Admin",
"login.body": "Enter credentials to open the console.",
"login.secret": "Admin password or token",
@ -456,6 +458,8 @@ const translations: Record<Language, Record<string, string>> = {
"language.en": "EN",
"language.zh": "中文",
"language.ru": "RU",
"theme.switchToDark": "切换到深色主题",
"theme.switchToLight": "切换到浅色主题",
"login.heading": "运维后台",
"login.body": "输入凭据后进入控制台。",
"login.secret": "管理员密码或 token",
@ -772,7 +776,7 @@ const translations: Record<Language, Record<string, string>> = {
"common.group": "Группа",
"common.id": "ID",
"common.limit": "Лимит",
"common.loading": "Загрузка...",
"common.loading": "Загрузка",
"common.member": "Участник",
"common.members": "Участники",
"common.messageId": "ID сообщения",
@ -794,7 +798,7 @@ const translations: Record<Language, Record<string, string>> = {
"common.updatedAt": "Обновлено",
"common.username": "Имя пользователя",
"common.valid": "Действителен",
"common.verified": "Подтвержден",
"common.verified": "Подтверждён",
"common.views": "Просмотры",
"common.yes": "Да",
"route.accounts": "Аккаунты",
@ -805,15 +809,15 @@ const translations: Record<Language, Record<string, string>> = {
"route.dashboardSubtitle": "Консоль / Обзор",
"route.messages": "Аудит сообщений",
"route.messagesSubtitle": "Консоль / Сообщения",
"route.gifts": "Звездные подарки",
"route.giftsSubtitle": "Консоль / Звездные подарки",
"route.gifts": "Звёздные подарки",
"route.giftsSubtitle": "Консоль / Звёздные подарки",
"layout.navigation": "Навигация",
"layout.primaryNav": "Основное меню",
"layout.dashboard": "Обзор",
"layout.accounts": "Аккаунты",
"layout.channels": "Супергруппы / Каналы",
"layout.messages": "Сообщения",
"layout.gifts": "Звездные подарки",
"layout.gifts": "Звёздные подарки",
"layout.privateMessages": "Личные",
"layout.groupMessages": "Группы",
"layout.runtime": "Среда выполнения",
@ -823,16 +827,18 @@ const translations: Record<Language, Record<string, string>> = {
"layout.readOnly": "Только чтение",
"layout.writeOps": "Операции записи",
"layout.dryRun": "Тестовый запуск",
"layout.actor": "Вход выполнен как: {actor}",
"layout.actor": "Вы вошли как: {actor}",
"layout.logout": "Выйти",
"language.en": "EN",
"language.zh": "中文",
"language.ru": "RU",
"theme.switchToDark": "Тёмная тема",
"theme.switchToLight": "Светлая тема",
"login.heading": "Панель администратора",
"login.body": "Введите учетные данные для входа в консоль.",
"login.body": "Введите учётные данные для входа в консоль.",
"login.secret": "Пароль или токен администратора",
"login.submit": "Войти",
"login.submitting": "Вход...",
"login.submitting": "Вход",
"dashboard.eyebrow": "Состояние системы",
"dashboard.title": "Обзор консоли",
"dashboard.readPath": "Путь чтения",
@ -840,8 +846,8 @@ const translations: Record<Language, Record<string, string>> = {
"dashboard.writePath": "Путь записи",
"dashboard.executionPolicy": "Политика выполнения",
"dashboard.dryRunFirst": "Сначала тестовый запуск",
"dashboard.accountsText": "Статус аккаунтов, премиум, верификация, сессии.",
"dashboard.channelsText": "Публичные каналы и группы, количество участников, статус верификации.",
"dashboard.accountsText": "Статус аккаунтов, Premium, подтверждение, сессии.",
"dashboard.channelsText": "Публичные каналы и группы, число участников, статус подтверждения.",
"dashboard.messagesText": "Ящики сообщений, обновления, состояние исходящих.",
"dashboard.strip.dryRun": "Все опасные действия начинаются с тестового запуска",
"dashboard.strip.token": "Браузер никогда не сохраняет внутренние токены",
@ -858,10 +864,10 @@ const translations: Record<Language, Record<string, string>> = {
"account.userID": "ID пользователя",
"account.phone": "Телефон",
"account.lastActive": "Последняя активность",
"account.notVerified": "Не подтвержден",
"account.notVerified": "Не подтверждён",
"account.notPremium": "Без Premium",
"account.premiumUntil": "Premium истекает",
"account.starsBalance": "Баланс Звезд",
"account.starsBalance": "Баланс Звёзд",
"account.startingGrantApplied": "стартовый бонус начислен",
"account.startingGrantPending": "ожидает стартового бонуса",
"account.activeSessions": "Авторизованные устройства",
@ -894,17 +900,17 @@ const translations: Record<Language, Record<string, string>> = {
"account.premiumMonthsAria": "Указать срок действия Premium в месяцах",
"account.setPremium": "Выдать Premium",
"account.clearPremium": "Снять Premium",
"account.starsAmount": "Количество звёзд",
"account.starsAmountAria": "Указать количество начисляемых звёзд",
"account.grantStars": "Начислить звёзды",
"account.starsAmount": "Количество Звёзд",
"account.starsAmountAria": "Указать количество начисляемых Звёзд",
"account.grantStars": "Начислить Звёзды",
"account.setVerified": "Подтвердить аккаунт",
"account.clearVerified": "Снять подтверждение",
"channel.pageTitle": "Супергруппы и каналы",
"channel.recentUpdated": "Недавно обновленные",
"channel.recentUpdated": "Недавно обновлённые",
"channel.currentPage": "Объекты на странице",
"channel.megagroups": "Супергруппы",
"channel.broadcasts": "Каналы",
"channel.verifiedCount": "Подтверждено",
"channel.verifiedCount": "Подтверждённые",
"channel.searchPlaceholder": "ID канала / имя пользователя / название",
"channel.channelID": "ID канала",
"channel.kind": "Тип",
@ -943,7 +949,7 @@ const translations: Record<Language, Record<string, string>> = {
"messages.outgoing": "Исходящее",
"messages.incoming": "Входящее",
"messages.ownerPeer": "Владелец / Собеседник",
"messages.deleteSelected": "Указать и удалить выбранные сообщения",
"messages.deleteSelected": "Удалить выбранные сообщения",
"messages.idsPlaceholder": "ID сообщений через запятую",
"messages.revoke": "Удалить для обеих сторон",
"messages.previewDelete": "Тестовое удаление",
@ -989,7 +995,7 @@ const translations: Record<Language, Record<string, string>> = {
"messages.channelGroup": "Канал / Группа",
"messages.pinned": "Закреплено",
"messages.channelPost": "Пост в канале",
"gifts.pageTitle": "Каталог звездных подарков",
"gifts.pageTitle": "Каталог звёздных подарков",
"gifts.eyebrow": "Каталог, неизменяемые версии и файлы анимаций",
"gifts.total": "Подарков в каталоге",
"gifts.enabled": "Включено",
@ -1000,7 +1006,7 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.listSummary": "Показано {shown} из {total}",
"gifts.idRevision": "ID / Версия",
"gifts.price": "Цена / Конвертация",
"gifts.importTitle": "Импорт звездного подарка",
"gifts.importTitle": "Импорт звёздного подарка",
"gifts.importEyebrow": "Управление каталогом подарков",
"gifts.newRevision": "Создать версию для подарка #{id}",
"gifts.importHint": "Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.",
@ -1031,8 +1037,8 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.changeFile": "Изменить файл",
"gifts.title": "Отображаемое название",
"gifts.titlePlaceholder": "например, Праздничная звезда",
"gifts.stars": "Цена в Звездах",
"gifts.convertStars": "Звезд при конвертации",
"gifts.stars": "Цена в Звёздах",
"gifts.convertStars": "Звёзд при конвертации",
"gifts.sortOrder": "Порядок сортировки",
"gifts.reason": "Причина для аудита",
"gifts.reasonPlaceholder": "Кратко опишите причину импорта этого подарка",
@ -1047,7 +1053,7 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.replace": "Новая версия",
"gifts.disable": "Отключить",
"gifts.enable": "Включить",
"gifts.empty": "Звездные подарки еще не импортированы.",
"gifts.empty": "Звёздные подарки ещё не импортированы.",
"gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.",
"gifts.validationReady": "Проверка пройдена",
"gifts.validationHint": "Проверьте нормализованные метаданные и подтвердите импорт.",
@ -1061,7 +1067,7 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.noPoolHint": "Опубликуйте модели, узоры и фоны для активации улучшений.",
"collectibles.publishNew": "Опубликовать новую неизменяемую версию",
"collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.",
"collectibles.upgradeStars": "Цена улучшения в Звездах",
"collectibles.upgradeStars": "Цена улучшения в Звёздах",
"collectibles.supply": "Уникальный тираж",
"collectibles.slug": "Публичный префикс ссылки (slug)",
"collectibles.models": "Модели",
@ -1071,7 +1077,7 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.pattern": "Узор",
"collectibles.backdrop": "Фон",
"collectibles.rarity": "Редкость ‰",
"collectibles.rarityHint": "В каждой категории должно быть не менее двух атрибутов. При добавлении и удалении веса permille перераспределяются до 1000.",
"collectibles.rarityHint": "В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.",
"collectibles.minimumAttributes": "Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.",
"collectibles.duplicateBackdropID": "ID фонов в одном наборе должны быть уникальными.",
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.",
@ -1093,10 +1099,10 @@ const translations: Record<Language, Record<string, string>> = {
"auth.lastActive": "Последняя активность",
"auth.revokeCurrent": "Отозвать текущую",
"auth.keepCurrent": "Оставить текущую",
"auth.revokeAll": "Разлогинить все устройства",
"auth.revokeAll": "Отозвать все устройства",
"picker.userPlaceholder": "Поиск по user_id / телефону / имени пользователя",
"picker.channelPlaceholder": "Поиск по channel_id / имени пользователя / названию",
"picker.verified": "Подтвержденные",
"picker.verified": "Подтверждённые",
"picker.regular": "Обычные",
"action.reasonRequired": "Пожалуйста, укажите причину операции",
"action.flow": "Процесс выполнения",
@ -1111,7 +1117,7 @@ const translations: Record<Language, Record<string, string>> = {
"action.commandID": "ID команды",
"action.status": "Статус",
"action.dryRun": "Тестовый запуск",
"action.runAgain": "Запустить тестовый запуск снова",
"action.runAgain": "Повторить тестовый запуск",
"action.runDry": "Сначала выполните тестовый запуск",
"action.confirm": "Подтвердить выполнение",
"audit.id": "ID",

View file

@ -2,12 +2,15 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import { I18nProvider } from "./i18n";
import { ThemeProvider } from "./theme";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<I18nProvider>
<App />
</I18nProvider>
<ThemeProvider>
<I18nProvider>
<App />
</I18nProvider>
</ThemeProvider>
</React.StrictMode>
);

View file

@ -3,6 +3,7 @@ import { useState } from "react";
import { api, errorMessage } from "../api";
import { Alert } from "../components/ui";
import { LanguageSwitch, useI18n } from "../i18n";
import { ThemeSwitch } from "../theme";
export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
const { t } = useI18n();
@ -36,6 +37,7 @@ export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
</span>
</div>
<div className="login-head-actions">
<ThemeSwitch />
<LanguageSwitch />
<span className="login-chip">{t("app.localAccess")}</span>
</div>

View file

@ -1,24 +1,155 @@
:root {
color-scheme: light;
--bg: #f3f5f7;
/* Surfaces */
--bg: #eef1f5;
--bg-accent: #e7ecf1;
--panel: #ffffff;
--panel-subtle: #f8fafb;
--panel-strong: #eef2f5;
--line: #d9e1e8;
--line-strong: #c2ccd6;
--text: #101828;
--muted: #667085;
--muted-2: #98a2b3;
--brand: #176d61;
--brand-2: #245b9d;
--good: #167447;
--warn: #a15c07;
--danger: #b42318;
--sidebar: #11161d;
--sidebar-soft: #1b222b;
--sidebar-line: #2c3541;
--focus: rgba(23, 109, 97, 0.16);
--shadow: 0 18px 52px rgba(16, 24, 40, 0.14);
--panel-subtle: #f5f8fb;
--panel-strong: #eef2f6;
--surface-soft: #f2f7f6;
--overlay: rgba(24, 34, 47, 0.42);
--topbar-bg: rgba(255, 255, 255, 0.86);
/* Lines */
--line: #e5eaf0;
--line-strong: #d3dce4;
/* Text */
--heading: #253040;
--text: #333f4d;
--text-soft: #45525f;
--muted: #6d7885;
--muted-2: #9aa4b1;
/* Brand */
--brand: #1f7d6f;
--brand-strong: #196155;
--brand-2: #3a6cae;
--brand-tint: #e8f4f0;
--brand-tint-border: #c8e2db;
--brand-tint-text: #235d53;
/* Semantic */
--good: #1f8a57;
--good-tint: #eaf6ef;
--good-border: #c1e1cf;
--warn: #a86a12;
--warn-tint: #fcf4e4;
--warn-border: #e7d09e;
--danger: #c0392b;
--danger-tint: #fcefec;
--danger-border: #eecac3;
--danger-text: #8f2f27;
/* Accent (collectibles / craft) */
--purple: #6a4fa3;
--purple-tint: #f4effb;
--purple-border: #dcd0f0;
--purple-text: #5a4590;
/* Inputs & controls */
--input-bg: #ffffff;
--btn-bg: #ffffff;
--btn-text: #29323d;
--btn-hover: #f4f7fa;
--switch-track: #c8d0d6;
/* Code / JSON blocks */
--code-bg: #1b2733;
--code-text: #d6e3ef;
--code-border: #2b3a49;
/* Sidebar */
--sidebar: #1c2530;
--sidebar-soft: #26313d;
--sidebar-line: #313c4a;
--sidebar-row: #232d38;
--sidebar-text: #dbe3ec;
--sidebar-muted: #8b98a8;
--sidebar-faint: #7c8a9a;
--sidebar-heading: #ffffff;
/* Effects */
--focus: rgba(31, 125, 111, 0.16);
--shadow: 0 12px 34px rgba(24, 39, 56, 0.1);
--shadow-sm: 0 2px 10px rgba(24, 39, 56, 0.05);
--shadow-brand: 0 8px 22px rgba(31, 125, 111, 0.22);
/* Radii */
--radius-xs: 8px;
--radius-sm: 9px;
--radius: 11px;
--radius-lg: 14px;
}
[data-theme="dark"] {
color-scheme: dark;
--bg: #0f141a;
--bg-accent: #131a22;
--panel: #171f28;
--panel-subtle: #1c2530;
--panel-strong: #212c38;
--surface-soft: #1a232d;
--overlay: rgba(5, 8, 12, 0.62);
--topbar-bg: rgba(21, 28, 36, 0.86);
--line: #29333f;
--line-strong: #38434f;
--heading: #eef3f8;
--text: #d5dde6;
--text-soft: #c2ccd6;
--muted: #98a4b1;
--muted-2: #6d7885;
--brand: #37a596;
--brand-strong: #45b6a6;
--brand-2: #6fa8e6;
--brand-tint: #14322d;
--brand-tint-border: #245349;
--brand-tint-text: #7fd3c4;
--good: #47c281;
--good-tint: #12301f;
--good-border: #245639;
--warn: #e0aa4d;
--warn-tint: #322810;
--warn-border: #574413;
--danger: #e6695c;
--danger-tint: #35201d;
--danger-border: #5c332d;
--danger-text: #f0a49b;
--purple: #ac90e2;
--purple-tint: #221b31;
--purple-border: #3d3357;
--purple-text: #c9b6ef;
--input-bg: #131a22;
--btn-bg: #1e2731;
--btn-text: #dbe2ea;
--btn-hover: #26313d;
--switch-track: #3a454f;
--code-bg: #0c1218;
--code-text: #cdd9e5;
--code-border: #232f3b;
--sidebar: #10151b;
--sidebar-soft: #1c242f;
--sidebar-line: #262f3a;
--sidebar-row: #161d25;
--sidebar-text: #cbd4de;
--sidebar-muted: #7c8794;
--sidebar-faint: #6f7b88;
--sidebar-heading: #f0f4f8;
--focus: rgba(55, 165, 150, 0.24);
--shadow: 0 16px 40px rgba(0, 0, 0, 0.46);
--shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.38);
--shadow-brand: 0 8px 22px rgba(55, 165, 150, 0.26);
}
* {
@ -35,7 +166,10 @@ body {
margin: 0;
color: var(--text);
background: var(--bg);
font: 13px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font: 13px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
transition: background-color 200ms ease, color 200ms ease;
}
button,
@ -64,7 +198,7 @@ a {
gap: 16px;
overflow-y: auto;
padding: 18px 12px;
color: #eef2f6;
color: var(--sidebar-text);
background: var(--sidebar);
border-right: 1px solid var(--sidebar-line);
}
@ -82,7 +216,7 @@ a {
}
.brand-elevated .brand-mark {
box-shadow: 0 8px 24px rgba(23, 109, 97, 0.26);
box-shadow: var(--shadow-brand);
}
.brand-mark {
@ -93,7 +227,7 @@ a {
color: #ffffff;
background: var(--brand);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
border-radius: var(--radius-sm);
font-weight: 800;
}
@ -106,16 +240,17 @@ a {
.brand small {
display: block;
margin-top: 3px;
color: #aeb8c4;
color: var(--sidebar-muted);
font-size: 11px;
}
.sidebar-label {
padding: 0 8px;
color: #8492a6;
color: var(--sidebar-faint);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.nav-list {
@ -136,26 +271,27 @@ a {
align-items: center;
gap: 9px;
padding: 0 10px;
color: #8fa0b4;
color: var(--sidebar-muted);
background: transparent;
border: 1px solid transparent;
border-radius: 7px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12px;
font-weight: 800;
text-align: left;
transition: color 140ms ease, background-color 140ms ease, border-color 140ms ease;
}
.nav-section-toggle:hover,
.nav-section.active .nav-section-toggle {
color: #ffffff;
color: var(--sidebar-heading);
background: var(--sidebar-soft);
border-color: #34404d;
border-color: var(--sidebar-line);
}
.nav-section-chevron {
justify-self: end;
color: #8fa0b4;
color: var(--sidebar-muted);
transition: transform 140ms ease;
}
@ -176,24 +312,25 @@ a {
align-items: center;
gap: 9px;
padding: 0 10px;
color: #c6d0dc;
color: var(--sidebar-text);
border: 1px solid transparent;
border-radius: 7px;
border-radius: var(--radius-sm);
transition: color 140ms ease, background-color 140ms ease, border-color 140ms ease;
}
.nav-dot {
width: 6px;
height: 6px;
justify-self: center;
background: #687789;
background: var(--sidebar-faint);
border-radius: 999px;
}
.nav-item:hover,
.nav-item.active {
color: #ffffff;
color: var(--sidebar-heading);
background: var(--sidebar-soft);
border-color: #34404d;
border-color: var(--sidebar-line);
}
.nav-item.active .nav-dot {
@ -213,14 +350,14 @@ a {
align-items: center;
gap: 7px;
padding: 0 8px;
color: #cbd5df;
background: #171d25;
border: 1px solid #27313c;
border-radius: 7px;
color: var(--sidebar-text);
background: var(--sidebar-row);
border: 1px solid var(--sidebar-line);
border-radius: var(--radius-sm);
}
.runtime-row strong {
color: #ffffff;
color: var(--sidebar-heading);
font-size: 11px;
}
@ -238,13 +375,14 @@ a {
justify-content: space-between;
gap: 18px;
padding: 12px 24px;
background: rgba(255, 255, 255, 0.94);
background: var(--topbar-bg);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(12px);
}
.topbar h1 {
margin: 2px 0 0;
color: var(--heading);
font-size: 20px;
line-height: 1.2;
}
@ -281,6 +419,7 @@ a {
border-radius: 999px;
cursor: pointer;
font-weight: 800;
transition: color 140ms ease, background-color 140ms ease;
}
.language-switch button.active {
@ -293,12 +432,36 @@ a {
outline-offset: 2px;
}
.theme-toggle {
display: inline-grid;
width: 34px;
height: 34px;
place-items: center;
color: var(--muted);
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease;
}
.theme-toggle:hover {
color: var(--brand);
border-color: var(--brand-tint-border);
background: var(--brand-tint);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.actor-pill {
display: inline-flex;
min-height: 30px;
align-items: center;
padding: 0 10px;
color: #344054;
color: var(--text-soft);
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 999px;
@ -315,4 +478,5 @@ a {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
}

View file

@ -9,7 +9,8 @@
min-width: 0;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.overview-band {
@ -25,6 +26,7 @@
.section-head h2,
.modal h2 {
margin: 0;
color: var(--heading);
font-size: 18px;
line-height: 1.25;
}
@ -47,7 +49,7 @@
padding: 10px;
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 7px;
border-radius: var(--radius-sm);
}
.status-item span,
@ -70,16 +72,16 @@
.status-item.good,
.metric.good {
border-color: #afd8bf;
border-color: var(--good-border);
}
.status-item.warn,
.metric.warn {
border-color: #e7c77e;
border-color: var(--warn-border);
}
.metric.danger {
border-color: #efb4ad;
border-color: var(--danger-border);
}
.command-grid {
@ -97,11 +99,15 @@
padding: 14px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}
.launcher:hover {
border-color: var(--brand);
border-color: var(--brand-tint-border);
box-shadow: var(--shadow);
transform: translateY(-1px);
}
.launcher-icon {
@ -110,9 +116,9 @@
height: 38px;
place-items: center;
color: var(--brand);
background: #edf7f4;
border: 1px solid #c9e2dc;
border-radius: 8px;
background: var(--brand-tint);
border: 1px solid var(--brand-tint-border);
border-radius: var(--radius-sm);
}
.launcher-copy {
@ -121,6 +127,7 @@
}
.launcher-copy strong {
color: var(--heading);
font-size: 15px;
}
@ -140,10 +147,10 @@
align-items: center;
gap: 8px;
padding: 0 10px;
color: #344054;
color: var(--text-soft);
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius-sm);
}
.page-frame {
@ -165,7 +172,7 @@
padding: 10px;
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius-sm);
}
.toolbar {
@ -195,9 +202,9 @@
min-width: 0;
gap: 8px;
padding: 10px;
background: #ffffff;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius-sm);
}
.picker-head {
@ -206,7 +213,7 @@
align-items: center;
justify-content: space-between;
gap: 8px;
color: #344054;
color: var(--text-soft);
font-weight: 800;
}
@ -217,10 +224,10 @@
align-items: center;
gap: 8px;
padding: 7px 9px;
color: #0f3f38;
background: #eef8f5;
border: 1px solid #b9dcd3;
border-radius: 7px;
color: var(--brand-tint-text);
background: var(--brand-tint);
border: 1px solid var(--brand-tint-border);
border-radius: var(--radius-sm);
}
.selected-entity strong,
@ -237,8 +244,9 @@
}
.selected-entity div span {
color: #52606d;
color: var(--brand-tint-text);
font-size: 11px;
opacity: 0.85;
}
.picker-search {
@ -250,7 +258,7 @@
padding: 0 6px 0 9px;
background: var(--panel-subtle);
border: 1px solid var(--line-strong);
border-radius: 7px;
border-radius: var(--radius-sm);
}
.picker-search input {
@ -267,7 +275,7 @@
max-height: 236px;
overflow: auto;
border: 1px solid var(--line);
border-radius: 7px;
border-radius: var(--radius-sm);
}
.picker-row {
@ -278,7 +286,7 @@
gap: 8px;
padding: 6px 8px;
color: var(--text);
background: #ffffff;
background: var(--panel);
border: 0;
border-bottom: 1px solid var(--line);
cursor: pointer;
@ -291,7 +299,7 @@
.picker-row:hover,
.picker-row.selected {
background: #f3f8f6;
background: var(--surface-soft);
}
.picker-row strong,
@ -311,18 +319,24 @@
.picker-error {
color: var(--danger);
background: #fff2f0;
border: 1px solid #efb4ad;
border-radius: 7px;
background: var(--danger-tint);
border: 1px solid var(--danger-border);
border-radius: var(--radius-sm);
}
input,
textarea {
color: var(--text);
background: #ffffff;
background: var(--input-bg);
border: 1px solid var(--line-strong);
border-radius: 7px;
border-radius: var(--radius-sm);
outline: none;
transition: border-color 140ms ease, box-shadow 140ms ease;
}
input::placeholder,
textarea::placeholder {
color: var(--muted-2);
}
input {
@ -366,9 +380,10 @@ textarea:focus {
width: min(380px, 100%);
height: 34px;
padding: 0 10px;
background: #ffffff;
color: var(--text);
background: var(--input-bg);
border: 1px solid var(--line-strong);
border-radius: 7px;
border-radius: var(--radius-sm);
}
.searchbox input {
@ -386,16 +401,17 @@ textarea:focus {
justify-content: center;
gap: 6px;
padding: 0 12px;
color: #1d2939;
background: #ffffff;
color: var(--btn-text);
background: var(--btn-bg);
border: 1px solid var(--line-strong);
border-radius: 7px;
border-radius: var(--radius-sm);
cursor: pointer;
white-space: nowrap;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease;
}
.btn:hover:not(:disabled) {
background: #f7f9fb;
background: var(--btn-hover);
}
.btn:disabled {
@ -410,7 +426,8 @@ textarea:focus {
}
.btn.primary:hover:not(:disabled) {
background: #12594f;
background: var(--brand-strong);
border-color: var(--brand-strong);
}
.btn.ghost {
@ -419,22 +436,24 @@ textarea:focus {
.btn.danger {
color: var(--danger);
background: #fff7f5;
border-color: #efb4ad;
background: var(--danger-tint);
border-color: var(--danger-border);
}
.btn.danger:hover:not(:disabled) {
background: #ffeceb;
background: var(--danger-tint);
border-color: var(--danger);
}
.btn.warn {
color: var(--warn);
background: #fff8ec;
border-color: #e7c77e;
background: var(--warn-tint);
border-color: var(--warn-border);
}
.btn.warn:hover:not(:disabled) {
background: #fff1d6;
background: var(--warn-tint);
border-color: var(--warn);
}
.btn:disabled,
@ -442,7 +461,7 @@ textarea:focus {
.btn.warn:disabled,
.btn.danger:disabled {
color: var(--muted-2);
background: #f3f5f7;
background: var(--panel-strong);
border-color: var(--line);
cursor: not-allowed;
}
@ -476,8 +495,9 @@ textarea:focus {
.table-wrap {
width: 100%;
overflow-x: auto;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
}
.data-table {
@ -500,13 +520,13 @@ textarea:focus {
position: sticky;
top: 0;
z-index: 0;
color: #475467;
color: var(--muted);
background: var(--panel-strong);
font-weight: 800;
}
.data-table tbody tr:hover {
background: #fbfcfd;
background: var(--panel-subtle);
}
.data-table tr:last-child td {
@ -528,29 +548,29 @@ textarea:focus {
min-height: 22px;
align-items: center;
padding: 1px 8px;
color: #4f5b68;
background: #f3f6f8;
border: 1px solid #d7e0e8;
color: var(--muted);
background: var(--panel-strong);
border: 1px solid var(--line-strong);
border-radius: 999px;
white-space: nowrap;
}
.badge.good {
color: var(--good);
background: #eef8f2;
border-color: #b9dcc7;
background: var(--good-tint);
border-color: var(--good-border);
}
.badge.danger {
color: var(--danger);
background: #fff2f0;
border-color: #efb4ad;
background: var(--danger-tint);
border-color: var(--danger-border);
}
.badge.warn {
color: var(--warn);
background: #fff8e7;
border-color: #e7c77e;
background: var(--warn-tint);
border-color: var(--warn-border);
}
.empty-cell {

View file

@ -18,10 +18,11 @@
padding: 14px;
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
}
.entity-title {
color: var(--heading);
font-size: 20px;
font-weight: 800;
line-height: 1.25;
@ -41,10 +42,10 @@
.about-text {
margin: 0;
padding: 10px;
color: #344054;
background: #fbfcfd;
color: var(--text-soft);
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
}
.section-block,
@ -54,7 +55,8 @@
padding: 12px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.section-head {
@ -79,7 +81,7 @@
.dock-title {
padding-bottom: 4px;
color: #344054;
color: var(--text-soft);
font-weight: 800;
border-bottom: 1px solid var(--line);
}
@ -184,7 +186,7 @@
padding: 10px;
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
}
.operation-title {
@ -192,6 +194,7 @@
width: 100%;
align-items: center;
gap: 6px;
color: var(--heading);
font-weight: 800;
}
@ -212,10 +215,10 @@
align-items: flex-start;
gap: 8px;
padding: 9px 10px;
color: #8a251d;
background: #fff2f0;
border: 1px solid #efb4ad;
border-radius: 8px;
color: var(--danger-text);
background: var(--danger-tint);
border: 1px solid var(--danger-border);
border-radius: var(--radius);
}
.json-block {
@ -223,10 +226,10 @@
overflow: auto;
margin: 0;
padding: 12px;
color: #d8e6f0;
background: #141a22;
border: 1px solid #2a3542;
border-radius: 8px;
color: var(--code-text);
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: var(--radius);
font-size: 12px;
}
@ -250,12 +253,12 @@
color: var(--muted);
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
}
.gift-metrics .metric {
min-height: 68px;
padding: 12px;
background: linear-gradient(145deg, #ffffff, #f6f9f9);
background: var(--panel-subtle);
}
.gift-metrics .metric strong { font-size: 17px; }
@ -265,12 +268,12 @@
flex: 0 0 auto;
place-items: center;
color: var(--brand);
background: #eaf6f3;
border: 1px solid #c7e3dc;
background: var(--brand-tint);
border: 1px solid var(--brand-tint-border);
}
.gift-format-chips { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
.gift-format-chips span { padding: 4px 8px; color: #33645d; background: #eef8f5; border: 1px solid #cfe5df; border-radius: 999px; font-size: 10px; font-weight: 800; letter-spacing: .02em; }
.gift-format-chips span { padding: 4px 8px; color: var(--brand-tint-text); background: var(--brand-tint); border: 1px solid var(--brand-tint-border); border-radius: 999px; font-size: 10px; font-weight: 800; letter-spacing: .02em; }
.gift-list-summary { margin-left: auto; color: var(--muted); font-size: 11px; font-weight: 700; }
@ -284,48 +287,48 @@
.official-gift-categories { display: flex; flex-wrap: wrap; gap: 7px; }
.official-gift-categories button {
display: inline-flex; align-items: center; gap: 7px; min-height: 32px; padding: 5px 10px;
color: #49605c; background: #f7faf9; border: 1px solid #d7e2df; border-radius: 999px;
color: var(--text-soft); background: var(--panel-subtle); border: 1px solid var(--line-strong); border-radius: 999px;
font: inherit; font-size: 11px; font-weight: 800; cursor: pointer;
transition: color .15s ease, background .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.official-gift-categories button:hover { color: var(--brand); border-color: #9fc9c0; }
.official-gift-categories button.active { color: #ffffff; background: var(--brand); border-color: var(--brand); box-shadow: 0 4px 12px rgba(23, 109, 97, .17); }
.official-gift-categories button:hover { color: var(--brand); border-color: var(--brand-tint-border); }
.official-gift-categories button.active { color: #ffffff; background: var(--brand); border-color: var(--brand); box-shadow: var(--shadow-brand); }
.official-gift-categories button span {
display: grid; min-width: 20px; height: 20px; padding: 0 5px; place-items: center;
color: inherit; background: rgba(255,255,255,.65); border-radius: 999px; font-size: 10px;
color: inherit; background: rgba(125, 140, 155, .22); border-radius: 999px; font-size: 10px;
}
.official-gift-categories button.active span { color: var(--brand); }
.official-gift-categories button.active span { color: var(--brand); background: rgba(255, 255, 255, .85); }
.official-gift-list {
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; max-height: 314px;
min-height: 126px; padding: 8px; overflow: auto; border: 1px solid var(--line); border-radius: 14px;
background: #f6f9f8; scrollbar-gutter: stable;
min-height: 126px; padding: 8px; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius-lg);
background: var(--panel-subtle); scrollbar-gutter: stable;
}
.official-gift-option {
display: grid; min-width: 0; gap: 8px; padding: 11px 12px; text-align: left; color: var(--text);
background: #ffffff; border: 1px solid #dce6e3; border-radius: 11px; cursor: pointer;
box-shadow: 0 1px 2px rgba(32, 54, 50, .03);
background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer;
box-shadow: var(--shadow-sm);
transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.official-gift-option:hover { border-color: #9fc9c0; box-shadow: 0 5px 14px rgba(32, 76, 68, .08); transform: translateY(-1px); }
.official-gift-option.selected { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(23, 109, 97, .12), 0 5px 14px rgba(32, 76, 68, .08); }
.official-gift-option:hover { border-color: var(--brand-tint-border); box-shadow: var(--shadow); transform: translateY(-1px); }
.official-gift-option.selected { border-color: var(--brand); box-shadow: 0 0 0 2px var(--focus), var(--shadow); }
.official-gift-option-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: baseline; }
.official-gift-option-head strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.official-gift-option-head .mono { color: var(--muted); font-size: 9px; }
.official-gift-option-meta { display: flex; flex-wrap: wrap; gap: 10px; color: #667773; font-size: 10px; font-weight: 700; }
.official-gift-option-meta { display: flex; flex-wrap: wrap; gap: 10px; color: var(--muted); font-size: 10px; font-weight: 700; }
.official-gift-capabilities { display: flex; flex-wrap: wrap; gap: 5px; }
.official-gift-capabilities > span {
padding: 3px 7px; border: 1px solid transparent; border-radius: 999px; font-size: 9px; font-weight: 850; letter-spacing: .01em;
}
.official-gift-capabilities > span.yes { color: #136b4d; background: #e9f8f0; border-color: #bde6cf; }
.official-gift-capabilities > span.craft { color: #6e3ca0; background: #f3ebfb; border-color: #d9c5ef; }
.official-gift-capabilities > span.no { color: #78837f; background: #f1f3f2; border-color: #dde2e0; }
.official-gift-capabilities > span.yes { color: var(--good); background: var(--good-tint); border-color: var(--good-border); }
.official-gift-capabilities > span.craft { color: var(--purple); background: var(--purple-tint); border-color: var(--purple-border); }
.official-gift-capabilities > span.no { color: var(--muted); background: var(--panel-strong); border-color: var(--line-strong); }
.official-gift-empty {
display: grid; grid-column: 1 / -1; min-height: 108px; place-items: center; padding: 20px;
color: var(--muted); text-align: center; font-size: 12px;
}
.official-gift-selected {
display: grid; grid-template-columns: 108px minmax(0, 1fr); gap: 14px; align-items: center;
padding: 12px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-soft);
padding: 12px; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface-soft);
}
.official-gift-selected .gift-animation-shell { width: 96px; height: 96px; }
.official-gift-selected > div:last-child { display: grid; gap: 5px; min-width: 0; }
@ -341,22 +344,22 @@
gap: 12px;
padding: 12px 14px;
color: var(--text);
background: #ffffff;
border: 1px dashed #b7ccc8;
border-radius: 10px;
background: var(--panel);
border: 1px dashed var(--line-strong);
border-radius: var(--radius);
cursor: pointer;
transition: border-color .16s ease, background .16s ease, box-shadow .16s ease;
}
.gift-file-picker:hover,
.gift-file-picker.has-file { background: #f8fcfb; border-color: var(--brand); box-shadow: 0 0 0 2px rgba(23, 109, 97, .05); }
.gift-file-picker.has-file { background: var(--brand-tint); border-color: var(--brand); box-shadow: 0 0 0 2px var(--focus); }
.gift-file-picker input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.gift-file-icon { width: 40px; height: 40px; border-radius: 9px; }
.gift-file-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); }
.gift-file-copy { display: grid; min-width: 0; gap: 2px; }
.gift-field-label { color: var(--muted); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
.gift-file-copy strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.gift-file-copy strong { overflow: hidden; color: var(--heading); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.gift-file-copy small { color: var(--muted); font-size: 11px; font-weight: 500; }
.gift-file-action { padding: 7px 10px; color: var(--brand); background: #f0f8f6; border: 1px solid #c7e3dc; border-radius: 7px; font-size: 11px; font-weight: 800; }
.gift-file-action { padding: 7px 10px; color: var(--brand); background: var(--brand-tint); border: 1px solid var(--brand-tint-border); border-radius: var(--radius-sm); font-size: 11px; font-weight: 800; }
.gift-fields-grid {
display: grid;
@ -380,37 +383,37 @@
height: 38px;
padding: 0 10px;
color: var(--text);
background: #fff;
background: var(--input-bg);
border: 1px solid var(--line);
border-radius: 7px;
border-radius: var(--radius-sm);
}
.gift-fields-grid input:focus,
.gift-reason-field input:focus { border-color: #77b6aa; box-shadow: 0 0 0 3px rgba(23, 109, 97, .08); outline: none; }
.gift-reason-field input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--focus); outline: none; }
.gift-switch { display: inline-flex; align-items: center; gap: 9px; color: #344054; font-size: 12px; font-weight: 700; cursor: pointer; }
.gift-switch { display: inline-flex; align-items: center; gap: 9px; color: var(--text-soft); font-size: 12px; font-weight: 700; cursor: pointer; }
.gift-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.gift-switch-track { display: flex; width: 34px; height: 19px; align-items: center; padding: 2px; background: #c8d0d5; border-radius: 999px; transition: background .16s ease; }
.gift-switch-track { display: flex; width: 34px; height: 19px; align-items: center; padding: 2px; background: var(--switch-track); border-radius: 999px; transition: background .16s ease; }
.gift-switch-track span { width: 15px; height: 15px; background: #ffffff; border-radius: 50%; box-shadow: 0 1px 3px rgba(16, 24, 40, .22); transition: transform .16s ease; }
.gift-switch input:checked + .gift-switch-track { background: var(--brand); }
.gift-switch input:checked + .gift-switch-track span { transform: translateX(15px); }
.gift-switch input:focus-visible + .gift-switch-track { outline: 3px solid rgba(23, 109, 97, .16); outline-offset: 2px; }
.gift-validation { overflow: hidden; color: #d5fff5; background: #173631; border: 1px solid #24564e; border-radius: 9px; }
.gift-validation-head { display: flex; align-items: center; gap: 9px; padding: 10px 12px; color: #e3fff9; background: rgba(255, 255, 255, .035); border-bottom: 1px solid rgba(255, 255, 255, .09); }
.gift-switch input:focus-visible + .gift-switch-track { outline: 3px solid var(--focus); outline-offset: 2px; }
.gift-validation { overflow: hidden; color: var(--code-text); background: var(--code-bg); border: 1px solid var(--code-border); border-radius: var(--radius-sm); }
.gift-validation-head { display: flex; align-items: center; gap: 9px; padding: 10px 12px; color: var(--code-text); background: rgba(255, 255, 255, .035); border-bottom: 1px solid rgba(255, 255, 255, .09); }
.gift-validation-head div { display: grid; gap: 2px; }
.gift-validation-head span { color: #99cfc4; font-size: 10px; }
.gift-validation pre { max-height: 180px; overflow: auto; margin: 0; padding: 11px 12px; color: #d5fff5; font-size: 11px; }
.gift-validation-head span { color: var(--brand); font-size: 10px; }
.gift-validation pre { max-height: 180px; overflow: auto; margin: 0; padding: 11px 12px; color: var(--code-text); font-size: 11px; }
.gift-animation-shell { position: relative; display: grid; min-height: 210px; place-items: center; background: radial-gradient(circle, #f9f3ff, #eef8f5); }
.gift-animation-shell { position: relative; display: grid; min-height: 210px; place-items: center; background: var(--surface-soft); }
.gift-animation { width: 200px; height: 200px; }
.gift-animation canvas { width: 100% !important; height: 100% !important; }
.gift-play { position: absolute; right: 8px; bottom: 8px; display: grid; width: 30px; height: 30px; place-items: center; color: var(--text); background: rgba(255,255,255,.9); border: 1px solid var(--line); border-radius: 50%; }
.gift-play { position: absolute; right: 8px; bottom: 8px; display: grid; width: 30px; height: 30px; place-items: center; color: var(--text); background: var(--panel); border: 1px solid var(--line); border-radius: 50%; }
.gift-table-wrap { background: #ffffff; }
.gift-table-wrap { background: var(--panel); }
.gift-table { min-width: 1080px; }
.gift-table th:first-child { width: 74px; }
.gift-table td { vertical-align: middle; }
.gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; }
.gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius-sm); }
.gift-animation-shell.compact .gift-animation { width: 54px; height: 54px; }
.gift-animation-shell.compact .gift-play { right: 3px; bottom: 3px; width: 20px; height: 20px; }
.gift-row-disabled { opacity: .68; }
@ -422,60 +425,60 @@
.gift-sort-order,
.gift-source-size,
.gift-convert-price { margin-top: 3px; color: var(--muted); font-size: 10px; }
.gift-table-price { color: #755b00; }
.gift-table-price { color: var(--warn); }
.gift-table-actions { display: flex; align-items: center; gap: 6px; }
.collectible-button { color: #6548a8; background: #f7f3ff; border-color: #ddd2f5; }
.collectible-button:hover { background: #efe8ff; border-color: #cbbaf0; }
.collectible-button { color: var(--purple); background: var(--purple-tint); border-color: var(--purple-border); }
.collectible-button:hover { background: var(--purple-tint); border-color: var(--purple); }
.collectible-modal { width: min(1180px, 100%); max-height: min(92vh, 980px); }
.collectible-modal .modal-head p { margin: 4px 0 0; color: var(--muted); font-size: 11px; }
.collectible-modal-body { gap: 16px; overflow: auto; padding: 16px 18px 22px; background: #f5f7fa; }
.collectible-modal-body { gap: 16px; overflow: auto; padding: 16px 18px 22px; background: var(--bg); }
.collectible-loading { display: flex; min-height: 90px; align-items: center; justify-content: center; gap: 8px; color: var(--muted); }
.collectible-empty { display: flex; align-items: center; gap: 12px; padding: 16px; color: #66568c; background: linear-gradient(135deg, #fbf9ff, #f2f7ff); border: 1px dashed #cfc3e9; border-radius: 12px; }
.collectible-empty { display: flex; align-items: center; gap: 12px; padding: 16px; color: var(--purple-text); background: var(--purple-tint); border: 1px dashed var(--purple-border); border-radius: var(--radius); }
.collectible-empty div,
.collectible-definition-head > div:first-child,
.collectible-section-head > div:first-child { display: grid; gap: 3px; }
.collectible-empty span,
.collectible-definition-head span,
.collectible-section-head span { color: var(--muted); font-size: 10px; font-weight: 500; }
.collectible-active { overflow: hidden; background: #ffffff; border: 1px solid #ddd6ee; border-radius: 12px; box-shadow: 0 5px 16px rgba(66, 46, 110, .05); }
.collectible-active-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: linear-gradient(100deg, #fbf9ff, #f4f9ff); border-bottom: 1px solid #e9e4f3; }
.collectible-active-head > div { display: flex; align-items: center; gap: 9px; color: #60458f; }
.collectible-active { overflow: hidden; background: var(--panel); border: 1px solid var(--purple-border); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
.collectible-active-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: var(--purple-tint); border-bottom: 1px solid var(--purple-border); }
.collectible-active-head > div { display: flex; align-items: center; gap: 9px; color: var(--purple-text); }
.collectible-active-head > div > div { display: grid; gap: 2px; }
.collectible-active-head span { color: var(--muted); font-size: 10px; }
.collectible-active-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: 1px; background: var(--line); }
.collectible-active-grid article { display: flex; min-width: 0; align-items: center; gap: 9px; padding: 9px 11px; background: #ffffff; }
.collectible-active-grid article { display: flex; min-width: 0; align-items: center; gap: 9px; padding: 9px 11px; background: var(--panel); }
.collectible-active-grid article > div:last-child { display: grid; min-width: 0; gap: 2px; }
.collectible-active-grid article strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.collectible-active-grid article span { color: var(--muted); font-size: 9px; }
.collectible-definition { overflow: hidden; background: #ffffff; border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 8px 24px rgba(16, 24, 40, .04); }
.collectible-definition-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; background: linear-gradient(110deg, #f8fbfa, #fbf9ff); border-bottom: 1px solid var(--line); }
.collectible-main-fields { padding: 14px 16px; background: #fbfcfd; border-bottom: 1px solid var(--line); }
.collectible-definition { overflow: hidden; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-sm); }
.collectible-definition-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; background: var(--panel-subtle); border-bottom: 1px solid var(--line); }
.collectible-main-fields { padding: 14px 16px; background: var(--panel-subtle); border-bottom: 1px solid var(--line); }
.collectible-section { padding: 14px 16px; border-bottom: 1px solid var(--line); }
.collectible-section:last-child { border-bottom: 0; }
.collectible-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.collectible-section-tools { display: flex; align-items: center; gap: 7px; }
.collectible-rows { display: grid; gap: 7px; }
.collectible-row { position: relative; display: grid; align-items: end; gap: 7px; padding: 9px 9px 9px 36px; background: #fafbfc; border: 1px solid #e1e6eb; border-radius: 9px; }
.collectible-row:hover { background: #ffffff; border-color: #cbd7dd; box-shadow: 0 3px 10px rgba(16, 24, 40, .035); }
.collectible-row { position: relative; display: grid; align-items: end; gap: 7px; padding: 9px 9px 9px 36px; background: var(--panel-subtle); border: 1px solid var(--line); border-radius: var(--radius-sm); }
.collectible-row:hover { background: var(--panel); border-color: var(--line-strong); box-shadow: var(--shadow-sm); }
.collectible-row.animated { grid-template-columns: minmax(120px, 1.2fr) 90px 78px minmax(160px, 1.4fr) 48px 30px; }
.collectible-row.backdrop { grid-template-columns: minmax(110px, 1.2fr) 70px 80px 70px repeat(4, 52px) 48px 30px; }
.collectible-row-index { position: absolute; top: 0; bottom: 0; left: 0; display: grid; width: 27px; place-items: center; color: #71668c; background: #f0edf7; border-right: 1px solid #e0d9ed; border-radius: 8px 0 0 8px; font-size: 10px; font-weight: 800; }
.collectible-row-index { position: absolute; top: 0; bottom: 0; left: 0; display: grid; width: 27px; place-items: center; color: var(--purple-text); background: var(--purple-tint); border-right: 1px solid var(--purple-border); border-radius: var(--radius-xs) 0 0 var(--radius-xs); font-size: 10px; font-weight: 800; }
.collectible-row label { display: grid; min-width: 0; gap: 4px; }
.collectible-row label > span { color: var(--muted); font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .025em; }
.collectible-row input:not([type="file"]) { width: 100%; min-width: 0; height: 32px; padding: 0 8px; color: var(--text); background: #ffffff; border: 1px solid #d5dde3; border-radius: 7px; font: inherit; font-size: 11px; }
.collectible-row input:focus { border-color: #8d7aba; box-shadow: 0 0 0 3px rgba(111, 91, 174, .08); outline: none; }
.collectible-row input:not([type="file"]) { width: 100%; min-width: 0; height: 32px; padding: 0 8px; color: var(--text); background: var(--input-bg); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); font: inherit; font-size: 11px; }
.collectible-row input:focus { border-color: var(--purple); box-shadow: 0 0 0 3px var(--purple-tint); outline: none; }
.collectible-file input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.collectible-file em { display: flex; min-width: 0; height: 32px; align-items: center; gap: 5px; overflow: hidden; padding: 0 8px; color: #625080; background: #f7f4fd; border: 1px dashed #cfc4e1; border-radius: 7px; font-size: 10px; font-style: normal; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.collectible-inline-preview { display: grid; width: 42px; height: 42px; place-items: center; overflow: hidden; color: #8c7cae; background: radial-gradient(circle, #ffffff, #eee8f8); border: 1px solid #ded5ed; border-radius: 8px; }
.collectible-file em { display: flex; min-width: 0; height: 32px; align-items: center; gap: 5px; overflow: hidden; padding: 0 8px; color: var(--purple-text); background: var(--purple-tint); border: 1px dashed var(--purple-border); border-radius: var(--radius-sm); font-size: 10px; font-style: normal; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.collectible-inline-preview { display: grid; width: 42px; height: 42px; place-items: center; overflow: hidden; color: var(--purple); background: var(--purple-tint); border: 1px solid var(--purple-border); border-radius: var(--radius-sm); }
.collectible-animation { width: 100%; height: 100%; overflow: hidden; }
.collectible-animation.compact { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; background: radial-gradient(circle, #ffffff, #f0ebfa); border: 1px solid #e0d9ec; border-radius: 8px; }
.collectible-animation.compact { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; background: var(--purple-tint); border: 1px solid var(--purple-border); border-radius: var(--radius-sm); }
.collectible-animation canvas { width: 100% !important; height: 100% !important; }
.collectible-animation.failed { color: #b42318; background: #fff4f2; }
.collectible-animation.loading { color: #807397; }
.collectible-file-error { grid-column: 1 / -1; color: #b42318; font-size: 10px; }
.collectible-animation.failed { color: var(--danger); background: var(--danger-tint); }
.collectible-animation.loading { color: var(--purple-text); }
.collectible-file-error { grid-column: 1 / -1; color: var(--danger); font-size: 10px; }
.collectible-color input { height: 32px !important; padding: 3px !important; cursor: pointer; }
.collectible-backdrop-preview { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; border: 1px solid rgba(42, 31, 71, .18); border-radius: 8px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.2); font-size: 11px; font-weight: 900; }
.collectible-backdrop-preview { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; border: 1px solid rgba(42, 31, 71, .18); border-radius: var(--radius-sm); box-shadow: inset 0 0 0 1px rgba(255,255,255,.2); font-size: 11px; font-weight: 900; }
.collectible-row .icon-btn { align-self: center; }
.collectible-row .icon-btn:disabled { opacity: .28; }

View file

@ -5,7 +5,8 @@
display: grid;
place-items: center;
padding: 24px;
background: rgba(17, 24, 39, 0.52);
background: var(--overlay);
backdrop-filter: blur(2px);
}
.modal {
@ -13,9 +14,9 @@
max-height: min(820px, calc(100vh - 48px));
overflow: hidden;
padding: 0;
background: #ffffff;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
@ -43,10 +44,17 @@
width: 30px;
height: 30px;
place-items: center;
color: var(--text-soft);
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 7px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}
.icon-btn:hover {
background: var(--btn-hover);
border-color: var(--line-strong);
}
.command-steps {
@ -73,7 +81,7 @@
color: var(--muted);
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius-sm);
}
.command-step span {
@ -81,7 +89,7 @@
width: 20px;
height: 20px;
place-items: center;
background: #ffffff;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 999px;
font-size: 11px;
@ -90,12 +98,12 @@
.command-step.active {
color: var(--brand);
border-color: #a9d8ce;
border-color: var(--brand-tint-border);
}
.command-step.done {
color: var(--good);
border-color: #b9dcc7;
border-color: var(--good-border);
}
.form-field {
@ -105,7 +113,7 @@
.form-field span,
.form-stack span {
color: #4b5563;
color: var(--text-soft);
font-weight: 800;
}
@ -123,7 +131,7 @@
display: flex;
align-items: center;
gap: 7px;
color: #344054;
color: var(--text-soft);
font-weight: 800;
}
@ -131,9 +139,9 @@
display: grid;
gap: 8px;
padding: 10px;
background: #fbfcfd;
background: var(--panel-subtle);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius);
}
.result-line {
@ -151,13 +159,13 @@
}
.result-message {
color: #344054;
color: var(--text-soft);
}
.modal-actions {
justify-content: flex-end;
padding: 12px 18px;
background: #ffffff;
background: var(--panel);
border-top: 1px solid var(--line);
}
@ -174,9 +182,9 @@
width: min(420px, 100%);
gap: 18px;
padding: 22px;
background: #ffffff;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
@ -201,14 +209,15 @@
align-items: center;
padding: 0 8px;
color: var(--brand);
background: #edf7f4;
border: 1px solid #c9e2dc;
background: var(--brand-tint);
border: 1px solid var(--brand-tint-border);
border-radius: 999px;
font-size: 12px;
}
.login-copy h1 {
margin: 0;
color: var(--heading);
font-size: 22px;
}
@ -237,13 +246,14 @@
place-items: center;
align-content: center;
gap: 18px;
background: var(--bg);
}
.loader-bar {
width: 180px;
height: 4px;
overflow: hidden;
background: #d7dde4;
background: var(--line-strong);
border-radius: 999px;
}

View file

@ -0,0 +1,106 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { Moon, Sun } from "lucide-react";
import { useI18n } from "./i18n";
export type Theme = "light" | "dark";
const storageKey = "telesrv.admin.theme";
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function applyTheme(theme: Theme) {
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => initialTheme());
useEffect(() => {
applyTheme(theme);
try {
localStorage.setItem(storageKey, theme);
} catch {
// Theme persistence is best-effort.
}
}, [theme]);
// Follow the OS preference until the user makes an explicit choice.
useEffect(() => {
if (!window.matchMedia) {
return;
}
const media = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = (event: MediaQueryListEvent) => {
let stored: string | null = null;
try {
stored = localStorage.getItem(storageKey);
} catch {
stored = null;
}
if (stored !== "light" && stored !== "dark") {
setThemeState(event.matches ? "dark" : "light");
}
};
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
}, []);
const setTheme = useCallback((next: Theme) => setThemeState(next), []);
const toggleTheme = useCallback(() => setThemeState((current) => (current === "dark" ? "light" : "dark")), []);
const value = useMemo<ThemeContextValue>(() => ({ theme, setTheme, toggleTheme }), [theme, setTheme, toggleTheme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme(): ThemeContextValue {
const value = useContext(ThemeContext);
if (!value) {
throw new Error("useTheme must be used inside ThemeProvider");
}
return value;
}
export function ThemeSwitch() {
const { theme, toggleTheme } = useTheme();
const { t } = useI18n();
const nextIsDark = theme === "light";
const label = t(nextIsDark ? "theme.switchToDark" : "theme.switchToLight");
return (
<button
className="theme-toggle"
type="button"
onClick={toggleTheme}
aria-label={label}
title={label}
>
{theme === "dark" ? <Sun size={16} /> : <Moon size={16} />}
</button>
);
}
function initialTheme(): Theme {
try {
const stored = localStorage.getItem(storageKey);
if (stored === "light" || stored === "dark") {
return stored;
}
} catch {
// Storage is optional.
}
try {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
} catch {
// matchMedia can be unavailable in unusual embedded contexts.
}
return "light";
}