updated admin panel style
This commit is contained in:
parent
9844d78c5b
commit
deae6c4a4a
26 changed files with 493 additions and 1356 deletions
File diff suppressed because one or more lines are too long
9
cmd/telesrv-admin/web/dist/assets/index-CpvYOqis.js
vendored
Normal file
9
cmd/telesrv-admin/web/dist/assets/index-CpvYOqis.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
cmd/telesrv-admin/web/dist/assets/index-Da0KbwLb.css
vendored
Normal file
1
cmd/telesrv-admin/web/dist/assets/index-Da0KbwLb.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-400.woff2
vendored
Normal file
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-400.woff2
vendored
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-500.woff2
vendored
Normal file
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-500.woff2
vendored
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-600.woff2
vendored
Normal file
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-600.woff2
vendored
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-700.woff2
vendored
Normal file
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-700.woff2
vendored
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-800.woff2
vendored
Normal file
BIN
cmd/telesrv-admin/web/dist/fonts/plus-jakarta-sans-800.woff2
vendored
Normal file
Binary file not shown.
7
cmd/telesrv-admin/web/dist/index.html
vendored
7
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -3,9 +3,10 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>telesrv admin</title>
|
<link rel="icon" type="image/png" href="/logo.png" />
|
||||||
<script type="module" crossorigin src="/assets/index-ybbM_ULl.js"></script>
|
<title>OwpenGram Admin</title>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BVA68zgn.css">
|
<script type="module" crossorigin src="/assets/index-CpvYOqis.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-Da0KbwLb.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
BIN
cmd/telesrv-admin/web/dist/logo.png
vendored
Normal file
BIN
cmd/telesrv-admin/web/dist/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -3,7 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>telesrv admin</title>
|
<link rel="icon" type="image/png" href="/logo.png" />
|
||||||
|
<title>OwpenGram Admin</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-400.woff2
Normal file
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-400.woff2
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-500.woff2
Normal file
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-500.woff2
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-600.woff2
Normal file
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-600.woff2
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-700.woff2
Normal file
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-700.woff2
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-800.woff2
Normal file
BIN
cmd/telesrv-admin/web/public/fonts/plus-jakarta-sans-800.woff2
Normal file
Binary file not shown.
BIN
cmd/telesrv-admin/web/public/logo.png
Normal file
BIN
cmd/telesrv-admin/web/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState, type ReactNode } from "react";
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import { LanguageSwitch, useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import { type Navigate, type RouteState, routeSubtitle, routeTitle } from "../routing";
|
import { type Navigate, type RouteState, routeSubtitle, routeTitle } from "../routing";
|
||||||
import { AppLink } from "./AppLink";
|
import { AppLink } from "./AppLink";
|
||||||
|
|
||||||
|
|
@ -21,9 +21,9 @@ export function BootScreen() {
|
||||||
return (
|
return (
|
||||||
<div className="boot-screen">
|
<div className="boot-screen">
|
||||||
<div className="brand compact brand-elevated">
|
<div className="brand compact brand-elevated">
|
||||||
<span className="brand-mark">T</span>
|
<span className="brand-mark"><img src="/logo.png" alt="OwpenGram" /></span>
|
||||||
<span>
|
<span>
|
||||||
<strong>telesrv</strong>
|
<strong>OwpenGram</strong>
|
||||||
<small>{t("app.adminConsole")}</small>
|
<small>{t("app.adminConsole")}</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -64,9 +64,9 @@ export function Shell({
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
<AppLink className="brand" href="/" navigate={navigate}>
|
<AppLink className="brand" href="/" navigate={navigate}>
|
||||||
<span className="brand-mark">T</span>
|
<span className="brand-mark"><img src="/logo.png" alt="OwpenGram" /></span>
|
||||||
<span>
|
<span>
|
||||||
<strong>telesrv</strong>
|
<strong>OwpenGram</strong>
|
||||||
<small>{t("app.adminConsole")}</small>
|
<small>{t("app.adminConsole")}</small>
|
||||||
</span>
|
</span>
|
||||||
</AppLink>
|
</AppLink>
|
||||||
|
|
@ -123,7 +123,6 @@ export function Shell({
|
||||||
<h1>{routeTitle(route.path, t)}</h1>
|
<h1>{routeTitle(route.path, t)}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="topbar-actions">
|
<div className="topbar-actions">
|
||||||
<LanguageSwitch />
|
|
||||||
<span className="actor-pill">{t("layout.actor", { actor })}</span>
|
<span className="actor-pill">{t("layout.actor", { actor })}</span>
|
||||||
<button className="btn ghost icon-text" type="button" onClick={logout} title={t("layout.logout")}>
|
<button className="btn ghost icon-text" type="button" onClick={logout} title={t("layout.logout")}>
|
||||||
<LogOut size={16} /> {t("layout.logout")}
|
<LogOut size={16} /> {t("layout.logout")}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
import { createContext, useContext, useEffect, useMemo, type ReactNode } from "react";
|
||||||
|
|
||||||
export type Language = "en" | "zh" | "ru";
|
|
||||||
export type TranslationParams = Record<string, string | number | boolean>;
|
export type TranslationParams = Record<string, string | number | boolean>;
|
||||||
export type TFunction = (key: string, params?: TranslationParams) => string;
|
export type TFunction = (key: string, params?: TranslationParams) => string;
|
||||||
|
|
||||||
const storageKey = "telesrv.admin.lang";
|
const translations: Record<string, string> = {
|
||||||
|
|
||||||
const translations: Record<Language, Record<string, string>> = {
|
|
||||||
en: {
|
|
||||||
"app.adminConsole": "Admin Console",
|
"app.adminConsole": "Admin Console",
|
||||||
"app.localAccess": "Local access",
|
"app.localAccess": "Local access",
|
||||||
"app.title": "telesrv admin",
|
"app.title": "OwpenGram Admin",
|
||||||
"common.actions": "Actions",
|
"common.actions": "Actions",
|
||||||
"common.admins": "Admins",
|
"common.admins": "Admins",
|
||||||
"common.backToList": "Back to list",
|
"common.backToList": "Back to list",
|
||||||
|
|
@ -81,9 +77,6 @@ const translations: Record<Language, Record<string, string>> = {
|
||||||
"layout.dryRun": "Dry-run",
|
"layout.dryRun": "Dry-run",
|
||||||
"layout.actor": "Actor: {actor}",
|
"layout.actor": "Actor: {actor}",
|
||||||
"layout.logout": "Log out",
|
"layout.logout": "Log out",
|
||||||
"language.en": "EN",
|
|
||||||
"language.zh": "中文",
|
|
||||||
"language.ru": "RU",
|
|
||||||
"login.heading": "Operations Admin",
|
"login.heading": "Operations Admin",
|
||||||
"login.body": "Enter credentials to open the console.",
|
"login.body": "Enter credentials to open the console.",
|
||||||
"login.secret": "Admin password or token",
|
"login.secret": "Admin password or token",
|
||||||
|
|
@ -383,793 +376,26 @@ const translations: Record<Language, Record<string, string>> = {
|
||||||
"audit.dryRun": "Dry-run",
|
"audit.dryRun": "Dry-run",
|
||||||
"audit.reason": "Reason",
|
"audit.reason": "Reason",
|
||||||
"audit.time": "Time"
|
"audit.time": "Time"
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
"app.adminConsole": "管理控制台",
|
|
||||||
"app.localAccess": "本地访问",
|
|
||||||
"app.title": "telesrv 管理后台",
|
|
||||||
"common.actions": "操作",
|
|
||||||
"common.admins": "管理员",
|
|
||||||
"common.backToList": "返回列表",
|
|
||||||
"common.channel": "频道",
|
|
||||||
"common.channelOrGroup": "频道/群",
|
|
||||||
"common.clear": "清除",
|
|
||||||
"common.close": "关闭",
|
|
||||||
"common.count": "数量",
|
|
||||||
"common.deleted": "已删除",
|
|
||||||
"common.detail": "详情",
|
|
||||||
"common.device": "设备",
|
|
||||||
"common.disabled": "已禁用",
|
|
||||||
"common.enabled": "已启用",
|
|
||||||
"common.fromPeer": "From Peer",
|
|
||||||
"common.group": "群组",
|
|
||||||
"common.id": "ID",
|
|
||||||
"common.limit": "条数",
|
|
||||||
"common.loading": "加载中",
|
|
||||||
"common.member": "成员",
|
|
||||||
"common.members": "成员",
|
|
||||||
"common.messageId": "消息 ID",
|
|
||||||
"common.name": "姓名",
|
|
||||||
"common.no": "否",
|
|
||||||
"common.noResults": "无结果",
|
|
||||||
"common.none": "无",
|
|
||||||
"common.normal": "正常",
|
|
||||||
"common.operations": "操作",
|
|
||||||
"common.owner": "所属",
|
|
||||||
"common.platform": "平台",
|
|
||||||
"common.refresh": "刷新",
|
|
||||||
"common.search": "查询",
|
|
||||||
"common.sender": "发送方",
|
|
||||||
"common.status": "状态",
|
|
||||||
"common.survived": "存活",
|
|
||||||
"common.time": "时间",
|
|
||||||
"common.type": "类型",
|
|
||||||
"common.updatedAt": "更新时间",
|
|
||||||
"common.username": "用户名",
|
|
||||||
"common.valid": "有效",
|
|
||||||
"common.verified": "已认证",
|
|
||||||
"common.views": "浏览",
|
|
||||||
"common.yes": "是",
|
|
||||||
"route.accounts": "账号管理",
|
|
||||||
"route.accountsSubtitle": "控制台 / 账号",
|
|
||||||
"route.channels": "超级群与频道",
|
|
||||||
"route.channelsSubtitle": "控制台 / 频道",
|
|
||||||
"route.dashboard": "运维控制台",
|
|
||||||
"route.dashboardSubtitle": "控制台 / 总览",
|
|
||||||
"route.messages": "消息审计",
|
|
||||||
"route.messagesSubtitle": "控制台 / 消息",
|
|
||||||
"route.gifts": "星星礼物",
|
|
||||||
"route.giftsSubtitle": "控制台 / 星星礼物",
|
|
||||||
"layout.navigation": "导航",
|
|
||||||
"layout.primaryNav": "主导航",
|
|
||||||
"layout.dashboard": "总览",
|
|
||||||
"layout.accounts": "账号",
|
|
||||||
"layout.channels": "超级群/频道",
|
|
||||||
"layout.messages": "消息",
|
|
||||||
"layout.gifts": "礼物目录",
|
|
||||||
"layout.privateMessages": "私聊",
|
|
||||||
"layout.groupMessages": "群聊",
|
|
||||||
"layout.runtime": "运行状态",
|
|
||||||
"layout.adminBackend": "管理后台",
|
|
||||||
"layout.ready": "就绪",
|
|
||||||
"layout.pgRead": "PG 读取",
|
|
||||||
"layout.readOnly": "只读",
|
|
||||||
"layout.writeOps": "写操作",
|
|
||||||
"layout.dryRun": "预演",
|
|
||||||
"layout.actor": "操作者:{actor}",
|
|
||||||
"layout.logout": "退出",
|
|
||||||
"language.en": "EN",
|
|
||||||
"language.zh": "中文",
|
|
||||||
"language.ru": "RU",
|
|
||||||
"login.heading": "运维后台",
|
|
||||||
"login.body": "输入凭据后进入控制台。",
|
|
||||||
"login.secret": "管理员密码或 token",
|
|
||||||
"login.submit": "登录",
|
|
||||||
"login.submitting": "登录中",
|
|
||||||
"dashboard.eyebrow": "运行总览",
|
|
||||||
"dashboard.title": "控制台总览",
|
|
||||||
"dashboard.readPath": "读路径",
|
|
||||||
"dashboard.readPathValue": "PG 只读",
|
|
||||||
"dashboard.writePath": "写路径",
|
|
||||||
"dashboard.executionPolicy": "执行策略",
|
|
||||||
"dashboard.dryRunFirst": "先预演",
|
|
||||||
"dashboard.accountsText": "账号状态、会员、认证、会话。",
|
|
||||||
"dashboard.channelsText": "公开实体、成员计数、认证状态。",
|
|
||||||
"dashboard.messagesText": "消息盒、update、outbox 状态。",
|
|
||||||
"dashboard.strip.dryRun": "所有危险操作先预演",
|
|
||||||
"dashboard.strip.token": "浏览器不持有内部 token",
|
|
||||||
"dashboard.strip.pagination": "列表使用游标分页",
|
|
||||||
"dashboard.strip.snapshot": "详情页保留原始状态快照",
|
|
||||||
"account.pageTitle": "账号",
|
|
||||||
"account.queryResults": "查询结果",
|
|
||||||
"account.recentActive": "最近活跃账号",
|
|
||||||
"account.currentPage": "当前页账号",
|
|
||||||
"account.onlineDevices": "在线设备记录",
|
|
||||||
"account.premium": "会员",
|
|
||||||
"account.frozen": "冻结",
|
|
||||||
"account.searchPlaceholder": "用户 ID / 手机号 / 用户名",
|
|
||||||
"account.userID": "用户 ID",
|
|
||||||
"account.phone": "手机号",
|
|
||||||
"account.lastActive": "最近活跃",
|
|
||||||
"account.notVerified": "未认证",
|
|
||||||
"account.notPremium": "非会员",
|
|
||||||
"account.premiumUntil": "会员到期",
|
|
||||||
"account.starsBalance": "Stars 余额",
|
|
||||||
"account.startingGrantApplied": "初始赠送已发放",
|
|
||||||
"account.startingGrantPending": "初始赠送未触发",
|
|
||||||
"account.activeSessions": "授权设备",
|
|
||||||
"account.accountFlags": "账号标记",
|
|
||||||
"account.restriction": "限制状态",
|
|
||||||
"account.restricted": "已限制",
|
|
||||||
"account.createdAt": "创建时间",
|
|
||||||
"account.detailTitle": "账号 #{id}",
|
|
||||||
"account.profile": "账号档案",
|
|
||||||
"account.loadingDetail": "加载账号详情",
|
|
||||||
"account.waitingData": "等待数据",
|
|
||||||
"account.noUsername": "无用户名",
|
|
||||||
"account.noPhone": "无手机号",
|
|
||||||
"account.accountFrozen": "账号已冻结",
|
|
||||||
"account.accountActive": "账号正常",
|
|
||||||
"account.authorizationsTitle": "授权设备",
|
|
||||||
"account.authorizationsCount": "共 {count} 个授权",
|
|
||||||
"account.recentAdminOps": "最近后台操作",
|
|
||||||
"account.recent30Audit": "最近 30 条审计",
|
|
||||||
"account.actionDock": "账号操作",
|
|
||||||
"account.freezeAccount": "冻结账号",
|
|
||||||
"account.updateFreeze": "更新冻结信息",
|
|
||||||
"account.unfreezeAccount": "解冻账号",
|
|
||||||
"account.freezeSince": "冻结开始时间",
|
|
||||||
"account.freezeUntil": "申诉截止时间",
|
|
||||||
"account.freezeUntilAria": "账号冻结申诉截止时间",
|
|
||||||
"account.freezeAppealURL": "申诉链接",
|
|
||||||
"account.freezeAppealURLAria": "账号冻结申诉链接",
|
|
||||||
"account.premiumMonths": "会员时长(月)",
|
|
||||||
"account.premiumMonthsAria": "设置会员时长,单位月",
|
|
||||||
"account.setPremium": "设置会员",
|
|
||||||
"account.clearPremium": "取消会员",
|
|
||||||
"account.starsAmount": "赠送 Stars 数量",
|
|
||||||
"account.starsAmountAria": "设置要赠送的 Stars 数量",
|
|
||||||
"account.grantStars": "赠送 Stars",
|
|
||||||
"account.setVerified": "设置认证",
|
|
||||||
"account.clearVerified": "取消认证",
|
|
||||||
"channel.pageTitle": "超级群与频道",
|
|
||||||
"channel.recentUpdated": "最近更新",
|
|
||||||
"channel.currentPage": "当前页实体",
|
|
||||||
"channel.megagroups": "超级群",
|
|
||||||
"channel.broadcasts": "频道",
|
|
||||||
"channel.verifiedCount": "已认证",
|
|
||||||
"channel.searchPlaceholder": "频道 ID / 用户名 / 标题",
|
|
||||||
"channel.channelID": "频道 ID",
|
|
||||||
"channel.kind": "类型",
|
|
||||||
"channel.title": "标题",
|
|
||||||
"channel.pts": "PTS",
|
|
||||||
"channel.detailProfile": "频道档案",
|
|
||||||
"channel.loadingDetail": "加载频道详情",
|
|
||||||
"channel.creator": "创建者 {id}",
|
|
||||||
"channel.governance": "治理状态",
|
|
||||||
"channel.governanceValue": "封禁 {banned} / 踢出 {kicked}",
|
|
||||||
"channel.flags": "频道标记",
|
|
||||||
"channel.rawRow": "频道原始行",
|
|
||||||
"channel.rawRowText": "数据库只读快照",
|
|
||||||
"channel.actionDock": "频道操作",
|
|
||||||
"channel.setVerified": "设置认证",
|
|
||||||
"channel.clearVerified": "取消认证",
|
|
||||||
"channel.kind.broadcast": "频道",
|
|
||||||
"channel.kind.forum": "超级群/论坛",
|
|
||||||
"channel.kind.megagroup": "超级群",
|
|
||||||
"channel.kind.generic": "频道/群",
|
|
||||||
"messages.privateTitle": "私聊消息",
|
|
||||||
"messages.privateEyebrow": "私聊消息盒",
|
|
||||||
"messages.groupTitle": "群聊消息",
|
|
||||||
"messages.groupEyebrow": "超级群 / 频道消息",
|
|
||||||
"messages.selectPrivatePeers": "请先搜索并选择所属用户和对端用户",
|
|
||||||
"messages.selectChannel": "请先搜索并选择超级群或频道",
|
|
||||||
"messages.ownerUser": "所属用户",
|
|
||||||
"messages.peerUser": "对端用户",
|
|
||||||
"messages.beforeDatePlaceholder": "before_date 游标",
|
|
||||||
"messages.beforeIDPlaceholder": "before_msg_id 游标",
|
|
||||||
"messages.limitPlaceholder": "条数 <= 100",
|
|
||||||
"messages.searchMessages": "查询消息",
|
|
||||||
"messages.nextPage": "下一页",
|
|
||||||
"messages.currentPage": "当前页消息",
|
|
||||||
"messages.deleted": "已删除",
|
|
||||||
"messages.outgoing": "发出消息",
|
|
||||||
"messages.incoming": "收到",
|
|
||||||
"messages.ownerPeer": "所属 / 对端",
|
|
||||||
"messages.deleteSelected": "删除指定消息",
|
|
||||||
"messages.idsPlaceholder": "消息 ID,逗号分隔",
|
|
||||||
"messages.revoke": "同步撤回",
|
|
||||||
"messages.previewDelete": "预演删除",
|
|
||||||
"messages.clearHistory": "清空私聊历史",
|
|
||||||
"messages.maxIDPlaceholder": "max_id 截止消息",
|
|
||||||
"messages.maxBatchesPlaceholder": "max_batches 批次数",
|
|
||||||
"messages.justClear": "仅清本侧",
|
|
||||||
"messages.previewClearHistory": "预演清历史",
|
|
||||||
"messages.direction": "方向",
|
|
||||||
"messages.body": "正文",
|
|
||||||
"messages.privateDetailTitle": "消息 #{id}",
|
|
||||||
"messages.detailEyebrow": "消息详情",
|
|
||||||
"messages.backPrivate": "返回私聊消息",
|
|
||||||
"messages.backGroup": "返回群聊消息",
|
|
||||||
"messages.ownerPeerTitle": "所属 {owner} · 对端 {peer}",
|
|
||||||
"messages.senderSubtitle": "发送方 {sender} · {date}",
|
|
||||||
"messages.boxID": "消息盒 ID",
|
|
||||||
"messages.privateMessageID": "私聊消息 ID",
|
|
||||||
"messages.messageSender": "发送方",
|
|
||||||
"messages.messageBox": "消息盒",
|
|
||||||
"messages.dialogRow": "会话行",
|
|
||||||
"messages.privateRow": "私聊消息行",
|
|
||||||
"messages.channelMessageRow": "消息行",
|
|
||||||
"messages.channelRow": "频道行",
|
|
||||||
"messages.userUpdateEvents": "更新事件",
|
|
||||||
"messages.channelUpdateEvents": "频道更新事件",
|
|
||||||
"messages.eventJson": "事件 JSON",
|
|
||||||
"messages.dispatchOutbox": "分发队列",
|
|
||||||
"messages.messageBoxesSnapshot": "message_boxes 只读快照",
|
|
||||||
"messages.dialogSnapshot": "dialogs 只读快照",
|
|
||||||
"messages.privateSnapshot": "private_messages 只读快照",
|
|
||||||
"messages.channelMessagesSnapshot": "channel_messages 只读快照",
|
|
||||||
"messages.channelSnapshot": "channels 只读快照",
|
|
||||||
"messages.userEventsSource": "durable user_update_events",
|
|
||||||
"messages.channelEventsSource": "durable channel_update_events",
|
|
||||||
"messages.outboxSource": "在线/离线 dispatch_outbox",
|
|
||||||
"messages.attempts": "尝试",
|
|
||||||
"messages.deleteThis": "删除此消息",
|
|
||||||
"messages.groupDetailTitle": "群聊消息 #{id}",
|
|
||||||
"messages.channelGroupTitle": "频道/群 {id}",
|
|
||||||
"messages.mediaCount": "有媒体",
|
|
||||||
"messages.channelPosts": "频道帖子",
|
|
||||||
"messages.channelGroup": "频道 / 群",
|
|
||||||
"messages.pinned": "置顶",
|
|
||||||
"messages.channelPost": "频道帖子",
|
|
||||||
"gifts.pageTitle": "星星礼物目录",
|
|
||||||
"gifts.eyebrow": "目录、不可变版本与动画资源",
|
|
||||||
"gifts.total": "目录条目",
|
|
||||||
"gifts.enabled": "已启用",
|
|
||||||
"gifts.received": "已领取礼物",
|
|
||||||
"gifts.formats": "支持格式",
|
|
||||||
"gifts.add": "添加礼物",
|
|
||||||
"gifts.importAll": "导入全部官方礼物",
|
|
||||||
"gifts.searchPlaceholder": "搜索礼物 ID、标题或格式",
|
|
||||||
"gifts.listSummary": "显示 {shown} / {total} 项",
|
|
||||||
"gifts.idRevision": "ID / 版本",
|
|
||||||
"gifts.price": "售价 / 兑换",
|
|
||||||
"gifts.importTitle": "导入星星礼物",
|
|
||||||
"gifts.importEyebrow": "礼物目录操作",
|
|
||||||
"gifts.newRevision": "为礼物 #{id} 创建新版本",
|
|
||||||
"gifts.importHint": "支持 TGS 或纯 Lottie JSON;Lottie 会规范化并压缩成 TGS。",
|
|
||||||
"gifts.officialSource": "官方资源库",
|
|
||||||
"gifts.fileSource": "上传文件",
|
|
||||||
"gifts.officialHint": "从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。",
|
|
||||||
"gifts.officialSearch": "搜索官方礼物 ID 或标题",
|
|
||||||
"gifts.officialSelect": "请选择官方礼物",
|
|
||||||
"gifts.officialRequired": "请先选择一个官方礼物",
|
|
||||||
"gifts.officialResults": "显示 {shown} / {total} 项",
|
|
||||||
"gifts.officialCategoryLabel": "官方礼物能力分类",
|
|
||||||
"gifts.officialCategory.all": "全部",
|
|
||||||
"gifts.officialCategory.upgrade": "可升级",
|
|
||||||
"gifts.officialCategory.craft": "可 Craft",
|
|
||||||
"gifts.officialCategory.basic": "不可升级",
|
|
||||||
"gifts.officialUnnamed": "未命名官方礼物 #{id}",
|
|
||||||
"gifts.officialAttributes": "{count} 个属性",
|
|
||||||
"gifts.canUpgrade": "可升级",
|
|
||||||
"gifts.cannotUpgrade": "不可升级",
|
|
||||||
"gifts.canCraft": "可 Craft",
|
|
||||||
"gifts.cannotCraft": "不可 Craft",
|
|
||||||
"gifts.officialEmpty": "当前分类和搜索条件下没有官方礼物。",
|
|
||||||
"gifts.includeCollectible": "完整导入 collectible 属性池(包含 crafted 模型)",
|
|
||||||
"gifts.animation": "动画文件",
|
|
||||||
"gifts.filePrompt": "拖放或选择 TGS / Lottie 文件",
|
|
||||||
"gifts.fileHint": "支持 TGS、JSON、Lottie,导入前会先进行校验",
|
|
||||||
"gifts.chooseFile": "选择文件",
|
|
||||||
"gifts.changeFile": "更换文件",
|
|
||||||
"gifts.title": "显示标题",
|
|
||||||
"gifts.titlePlaceholder": "例如:庆典星星",
|
|
||||||
"gifts.stars": "售价 Stars",
|
|
||||||
"gifts.convertStars": "可兑换 Stars",
|
|
||||||
"gifts.sortOrder": "排序值",
|
|
||||||
"gifts.reason": "审计原因",
|
|
||||||
"gifts.reasonPlaceholder": "简要说明本次导入礼物的原因",
|
|
||||||
"gifts.enableAfterImport": "导入后启用",
|
|
||||||
"gifts.validate": "Dry-run 校验",
|
|
||||||
"gifts.confirmImport": "确认导入",
|
|
||||||
"gifts.stepDetails": "文件与信息",
|
|
||||||
"gifts.stepValidate": "Dry-run 校验",
|
|
||||||
"gifts.stepImport": "确认导入",
|
|
||||||
"gifts.fileRequired": "请先选择 TGS 或 Lottie 文件",
|
|
||||||
"gifts.source": "来源",
|
|
||||||
"gifts.replace": "创建新版本",
|
|
||||||
"gifts.disable": "停用",
|
|
||||||
"gifts.enable": "启用",
|
|
||||||
"gifts.bulkSelected": "已选择 {count} 个",
|
|
||||||
"gifts.bulkSelectAll": "选择所有可见礼物",
|
|
||||||
"gifts.bulkSelectOne": "选择礼物 {id}",
|
|
||||||
"gifts.bulkEnable": "启用所选",
|
|
||||||
"gifts.bulkDisable": "停用所选",
|
|
||||||
"gifts.bulkStatusFailed": "{total} 个中有 {failed} 个失败",
|
|
||||||
"gifts.empty": "尚未导入星星礼物。",
|
|
||||||
"gifts.emptyHint": "从上方导入第一个动画,开始搭建礼物目录。",
|
|
||||||
"gifts.validationReady": "校验已通过",
|
|
||||||
"gifts.validationHint": "确认规范化后的元数据无误,再执行正式导入。",
|
|
||||||
"gifts.confirmState": "确认执行礼物 #{id} 的状态变更吗?",
|
|
||||||
"collectibles.manage": "属性池",
|
|
||||||
"collectibles.title": "Collectibles 属性池 · 礼物 #{id}",
|
|
||||||
"collectibles.eyebrow": "唯一礼物属性管理",
|
|
||||||
"collectibles.activeRevision": "已发布版本 {revision}",
|
|
||||||
"collectibles.published": "已发布",
|
|
||||||
"collectibles.noPool": "尚未发布 Collectibles 属性池",
|
|
||||||
"collectibles.noPoolHint": "发布模型、图案与背景后,客户端即可升级为唯一礼物。",
|
|
||||||
"collectibles.publishNew": "发布新的不可变版本",
|
|
||||||
"collectibles.immutableHint": "Dry-run 会校验全部文件和稀有度总和,通过后才切换为当前版本。",
|
|
||||||
"collectibles.upgradeStars": "升级价格 Stars",
|
|
||||||
"collectibles.supply": "唯一礼物总量",
|
|
||||||
"collectibles.slug": "公开 Slug 前缀",
|
|
||||||
"collectibles.models": "模型",
|
|
||||||
"collectibles.patterns": "图案",
|
|
||||||
"collectibles.backdrops": "背景",
|
|
||||||
"collectibles.model": "模型",
|
|
||||||
"collectibles.pattern": "图案",
|
|
||||||
"collectibles.backdrop": "背景",
|
|
||||||
"collectibles.rarity": "稀有度 ‰",
|
|
||||||
"collectibles.rarityHint": "Permille 是普通升级的相对权重,不要求每类合计正好为 1000。",
|
|
||||||
"collectibles.colorHint": "颜色会按 24 位 RGB 数值保存。",
|
|
||||||
"collectibles.addAttribute": "添加",
|
|
||||||
"collectibles.remove": "删除属性",
|
|
||||||
"collectibles.fileRequired": "每个模型和图案都必须选择 TGS 或 Lottie 文件。",
|
|
||||||
"collectibles.backdropID": "背景 ID",
|
|
||||||
"collectibles.color.center": "中心色",
|
|
||||||
"collectibles.color.edge": "边缘色",
|
|
||||||
"collectibles.color.pattern": "图案色",
|
|
||||||
"collectibles.color.text": "文字色",
|
|
||||||
"collectibles.validationReady": "属性池校验通过",
|
|
||||||
"collectibles.validationHint": "确认规范化资源无误后,即可发布这个不可变版本。",
|
|
||||||
"collectibles.publish": "发布版本",
|
|
||||||
"messages.msgIDsInvalid": "消息 ID 无效",
|
|
||||||
"auth.device": "设备",
|
|
||||||
"auth.platform": "平台",
|
|
||||||
"auth.ip": "IP",
|
|
||||||
"auth.lastActive": "最近活跃",
|
|
||||||
"auth.revokeCurrent": "撤销当前",
|
|
||||||
"auth.keepCurrent": "保留当前",
|
|
||||||
"auth.revokeAll": "撤销全部设备",
|
|
||||||
"picker.userPlaceholder": "搜索 user_id / phone / username",
|
|
||||||
"picker.channelPlaceholder": "搜索 channel_id / username / title",
|
|
||||||
"picker.verified": "认证",
|
|
||||||
"picker.regular": "普通",
|
|
||||||
"action.reasonRequired": "请填写操作原因",
|
|
||||||
"action.flow": "操作流程",
|
|
||||||
"action.close": "关闭",
|
|
||||||
"action.stepReason": "填写原因",
|
|
||||||
"action.stepDryRun": "预演检查",
|
|
||||||
"action.stepConfirm": "确认执行",
|
|
||||||
"action.reason": "操作原因",
|
|
||||||
"action.reasonPlaceholder": "说明本次操作原因",
|
|
||||||
"action.requestPreview": "请求预览",
|
|
||||||
"action.result": "操作结果",
|
|
||||||
"action.commandID": "命令 ID",
|
|
||||||
"action.status": "状态",
|
|
||||||
"action.dryRun": "预演",
|
|
||||||
"action.runAgain": "重新预演",
|
|
||||||
"action.runDry": "先预演",
|
|
||||||
"action.confirm": "确认执行",
|
|
||||||
"audit.id": "ID",
|
|
||||||
"audit.commandID": "命令 ID",
|
|
||||||
"audit.action": "动作",
|
|
||||||
"audit.actor": "操作者",
|
|
||||||
"audit.status": "状态",
|
|
||||||
"audit.dryRun": "预演",
|
|
||||||
"audit.reason": "原因",
|
|
||||||
"audit.time": "时间"
|
|
||||||
},
|
|
||||||
ru: {
|
|
||||||
"app.adminConsole": "Панель администратора",
|
|
||||||
"app.localAccess": "Локальный доступ",
|
|
||||||
"app.title": "telesrv admin",
|
|
||||||
"common.actions": "Действия",
|
|
||||||
"common.admins": "Администраторы",
|
|
||||||
"common.backToList": "Назад к списку",
|
|
||||||
"common.channel": "Канал",
|
|
||||||
"common.channelOrGroup": "Канал / Группа",
|
|
||||||
"common.clear": "Очистить",
|
|
||||||
"common.close": "Закрыть",
|
|
||||||
"common.count": "Количество",
|
|
||||||
"common.deleted": "Удалено",
|
|
||||||
"common.detail": "Детали",
|
|
||||||
"common.device": "Устройство",
|
|
||||||
"common.disabled": "Отключено",
|
|
||||||
"common.enabled": "Включено",
|
|
||||||
"common.fromPeer": "От пира",
|
|
||||||
"common.group": "Группа",
|
|
||||||
"common.id": "ID",
|
|
||||||
"common.limit": "Лимит",
|
|
||||||
"common.loading": "Загрузка...",
|
|
||||||
"common.member": "Участник",
|
|
||||||
"common.members": "Участники",
|
|
||||||
"common.messageId": "ID сообщения",
|
|
||||||
"common.name": "Имя",
|
|
||||||
"common.no": "Нет",
|
|
||||||
"common.noResults": "Нет результатов",
|
|
||||||
"common.none": "Нет",
|
|
||||||
"common.normal": "Обычный",
|
|
||||||
"common.operations": "Операции",
|
|
||||||
"common.owner": "Владелец",
|
|
||||||
"common.platform": "Платформа",
|
|
||||||
"common.refresh": "Обновить",
|
|
||||||
"common.search": "Поиск",
|
|
||||||
"common.sender": "Отправитель",
|
|
||||||
"common.status": "Статус",
|
|
||||||
"common.survived": "Уцелело",
|
|
||||||
"common.time": "Время",
|
|
||||||
"common.type": "Тип",
|
|
||||||
"common.updatedAt": "Обновлено",
|
|
||||||
"common.username": "Имя пользователя",
|
|
||||||
"common.valid": "Действителен",
|
|
||||||
"common.verified": "Подтвержден",
|
|
||||||
"common.views": "Просмотры",
|
|
||||||
"common.yes": "Да",
|
|
||||||
"route.accounts": "Аккаунты",
|
|
||||||
"route.accountsSubtitle": "Консоль / Аккаунты",
|
|
||||||
"route.channels": "Супергруппы и каналы",
|
|
||||||
"route.channelsSubtitle": "Консоль / Каналы",
|
|
||||||
"route.dashboard": "Панель управления",
|
|
||||||
"route.dashboardSubtitle": "Консоль / Обзор",
|
|
||||||
"route.messages": "Аудит сообщений",
|
|
||||||
"route.messagesSubtitle": "Консоль / Сообщения",
|
|
||||||
"route.gifts": "Звездные подарки",
|
|
||||||
"route.giftsSubtitle": "Консоль / Звездные подарки",
|
|
||||||
"layout.navigation": "Навигация",
|
|
||||||
"layout.primaryNav": "Основное меню",
|
|
||||||
"layout.dashboard": "Обзор",
|
|
||||||
"layout.accounts": "Аккаунты",
|
|
||||||
"layout.channels": "Супергруппы / Каналы",
|
|
||||||
"layout.messages": "Сообщения",
|
|
||||||
"layout.gifts": "Звездные подарки",
|
|
||||||
"layout.privateMessages": "Личные",
|
|
||||||
"layout.groupMessages": "Группы",
|
|
||||||
"layout.runtime": "Среда выполнения",
|
|
||||||
"layout.adminBackend": "Админ-бэкенд",
|
|
||||||
"layout.ready": "Готов",
|
|
||||||
"layout.pgRead": "Чтение из PG",
|
|
||||||
"layout.readOnly": "Только чтение",
|
|
||||||
"layout.writeOps": "Операции записи",
|
|
||||||
"layout.dryRun": "Тестовый запуск",
|
|
||||||
"layout.actor": "Вход выполнен как: {actor}",
|
|
||||||
"layout.logout": "Выйти",
|
|
||||||
"language.en": "EN",
|
|
||||||
"language.zh": "中文",
|
|
||||||
"language.ru": "RU",
|
|
||||||
"login.heading": "Панель администратора",
|
|
||||||
"login.body": "Введите учетные данные для входа в консоль.",
|
|
||||||
"login.secret": "Пароль или токен администратора",
|
|
||||||
"login.submit": "Войти",
|
|
||||||
"login.submitting": "Вход...",
|
|
||||||
"dashboard.eyebrow": "Состояние системы",
|
|
||||||
"dashboard.title": "Обзор консоли",
|
|
||||||
"dashboard.readPath": "Путь чтения",
|
|
||||||
"dashboard.readPathValue": "PG только для чтения",
|
|
||||||
"dashboard.writePath": "Путь записи",
|
|
||||||
"dashboard.executionPolicy": "Политика выполнения",
|
|
||||||
"dashboard.dryRunFirst": "Сначала тестовый запуск",
|
|
||||||
"dashboard.accountsText": "Статус аккаунтов, премиум, верификация, сессии.",
|
|
||||||
"dashboard.channelsText": "Публичные каналы и группы, количество участников, статус верификации.",
|
|
||||||
"dashboard.messagesText": "Ящики сообщений, обновления, состояние исходящих.",
|
|
||||||
"dashboard.strip.dryRun": "Все опасные действия начинаются с тестового запуска",
|
|
||||||
"dashboard.strip.token": "Браузер никогда не сохраняет внутренние токены",
|
|
||||||
"dashboard.strip.pagination": "Списки используют курсорную пагинацию",
|
|
||||||
"dashboard.strip.snapshot": "Детальные страницы сохраняют моментальные снимки исходного состояния",
|
|
||||||
"account.pageTitle": "Аккаунты",
|
|
||||||
"account.queryResults": "Результаты поиска",
|
|
||||||
"account.recentActive": "Недавно активные аккаунты",
|
|
||||||
"account.currentPage": "Аккаунты на странице",
|
|
||||||
"account.onlineDevices": "Активные сессии устройств",
|
|
||||||
"account.premium": "Premium",
|
|
||||||
"account.frozen": "Заморожен",
|
|
||||||
"account.searchPlaceholder": "ID пользователя / телефон / имя пользователя",
|
|
||||||
"account.userID": "ID пользователя",
|
|
||||||
"account.phone": "Телефон",
|
|
||||||
"account.lastActive": "Последняя активность",
|
|
||||||
"account.notVerified": "Не подтвержден",
|
|
||||||
"account.notPremium": "Без Premium",
|
|
||||||
"account.premiumUntil": "Premium истекает",
|
|
||||||
"account.starsBalance": "Баланс Звезд",
|
|
||||||
"account.startingGrantApplied": "стартовый бонус начислен",
|
|
||||||
"account.startingGrantPending": "ожидает стартового бонуса",
|
|
||||||
"account.activeSessions": "Авторизованные устройства",
|
|
||||||
"account.accountFlags": "Флаги аккаунта",
|
|
||||||
"account.restriction": "Ограничение",
|
|
||||||
"account.restricted": "Ограничен",
|
|
||||||
"account.createdAt": "Создан",
|
|
||||||
"account.detailTitle": "Аккаунт #{id}",
|
|
||||||
"account.profile": "Профиль аккаунта",
|
|
||||||
"account.loadingDetail": "Загрузка данных аккаунта",
|
|
||||||
"account.waitingData": "Ожидание данных",
|
|
||||||
"account.noUsername": "Нет имени пользователя",
|
|
||||||
"account.noPhone": "Нет телефона",
|
|
||||||
"account.accountFrozen": "Аккаунт заморожен",
|
|
||||||
"account.accountActive": "Аккаунт активен",
|
|
||||||
"account.authorizationsTitle": "Авторизованные устройства",
|
|
||||||
"account.authorizationsCount": "Авторизаций: {count}",
|
|
||||||
"account.recentAdminOps": "Последние действия администратора",
|
|
||||||
"account.recent30Audit": "Последние 30 записей аудита",
|
|
||||||
"account.actionDock": "Действия с аккаунтом",
|
|
||||||
"account.freezeAccount": "Заморозить аккаунт",
|
|
||||||
"account.updateFreeze": "Обновить параметры заморозки",
|
|
||||||
"account.unfreezeAccount": "Разморозить аккаунт",
|
|
||||||
"account.freezeSince": "Заморожен с",
|
|
||||||
"account.freezeUntil": "Срок подачи апелляции",
|
|
||||||
"account.freezeUntilAria": "Срок подачи апелляции на заморозку",
|
|
||||||
"account.freezeAppealURL": "URL для апелляции",
|
|
||||||
"account.freezeAppealURLAria": "URL для апелляции на заморозку",
|
|
||||||
"account.premiumMonths": "Срок действия Premium (в месяцах)",
|
|
||||||
"account.premiumMonthsAria": "Указать срок действия Premium в месяцах",
|
|
||||||
"account.setPremium": "Выдать Premium",
|
|
||||||
"account.clearPremium": "Снять Premium",
|
|
||||||
"account.starsAmount": "Количество звёзд",
|
|
||||||
"account.starsAmountAria": "Указать количество начисляемых звёзд",
|
|
||||||
"account.grantStars": "Начислить звёзды",
|
|
||||||
"account.setVerified": "Подтвердить аккаунт",
|
|
||||||
"account.clearVerified": "Снять подтверждение",
|
|
||||||
"channel.pageTitle": "Супергруппы и каналы",
|
|
||||||
"channel.recentUpdated": "Недавно обновленные",
|
|
||||||
"channel.currentPage": "Объекты на странице",
|
|
||||||
"channel.megagroups": "Супергруппы",
|
|
||||||
"channel.broadcasts": "Каналы",
|
|
||||||
"channel.verifiedCount": "Подтверждено",
|
|
||||||
"channel.searchPlaceholder": "ID канала / имя пользователя / название",
|
|
||||||
"channel.channelID": "ID канала",
|
|
||||||
"channel.kind": "Тип",
|
|
||||||
"channel.title": "Название",
|
|
||||||
"channel.pts": "PTS",
|
|
||||||
"channel.detailProfile": "Профиль канала",
|
|
||||||
"channel.loadingDetail": "Загрузка данных канала",
|
|
||||||
"channel.creator": "Создатель: {id}",
|
|
||||||
"channel.governance": "Модерация",
|
|
||||||
"channel.governanceValue": "Заблокировано {banned} / Исключено {kicked}",
|
|
||||||
"channel.flags": "Флаги канала",
|
|
||||||
"channel.rawRow": "Исходная строка БД",
|
|
||||||
"channel.rawRowText": "Снимок базы данных только для чтения",
|
|
||||||
"channel.actionDock": "Действия с каналом",
|
|
||||||
"channel.setVerified": "Подтвердить канал",
|
|
||||||
"channel.clearVerified": "Снять подтверждение",
|
|
||||||
"channel.kind.broadcast": "Канал",
|
|
||||||
"channel.kind.forum": "Супергруппа / Форум",
|
|
||||||
"channel.kind.megagroup": "Супергруппа",
|
|
||||||
"channel.kind.generic": "Канал / Группа",
|
|
||||||
"messages.privateTitle": "Личные сообщения",
|
|
||||||
"messages.privateEyebrow": "Личные ящики сообщений",
|
|
||||||
"messages.groupTitle": "Групповые сообщения",
|
|
||||||
"messages.groupEyebrow": "Сообщения супергрупп и каналов",
|
|
||||||
"messages.selectPrivatePeers": "Сначала найдите и выберите владельца и собеседника",
|
|
||||||
"messages.selectChannel": "Сначала найдите и выберите супергруппу или канал",
|
|
||||||
"messages.ownerUser": "Пользователь-владелец",
|
|
||||||
"messages.peerUser": "Собеседник",
|
|
||||||
"messages.beforeDatePlaceholder": "курсор before_date",
|
|
||||||
"messages.beforeIDPlaceholder": "курсор before_msg_id",
|
|
||||||
"messages.limitPlaceholder": "лимит <= 100",
|
|
||||||
"messages.searchMessages": "Поиск сообщений",
|
|
||||||
"messages.nextPage": "Следующая страница",
|
|
||||||
"messages.currentPage": "Сообщения на странице",
|
|
||||||
"messages.deleted": "Удалено",
|
|
||||||
"messages.outgoing": "Исходящее",
|
|
||||||
"messages.incoming": "Входящее",
|
|
||||||
"messages.ownerPeer": "Владелец / Собеседник",
|
|
||||||
"messages.deleteSelected": "Указать и удалить выбранные сообщения",
|
|
||||||
"messages.idsPlaceholder": "ID сообщений через запятую",
|
|
||||||
"messages.revoke": "Удалить для обеих сторон",
|
|
||||||
"messages.previewDelete": "Тестовое удаление",
|
|
||||||
"messages.clearHistory": "Очистить историю личной переписки",
|
|
||||||
"messages.maxIDPlaceholder": "граница max_id",
|
|
||||||
"messages.maxBatchesPlaceholder": "max_batches",
|
|
||||||
"messages.justClear": "Очистить только у себя",
|
|
||||||
"messages.previewClearHistory": "Тестовая очистка истории",
|
|
||||||
"messages.direction": "Направление",
|
|
||||||
"messages.body": "Текст сообщения",
|
|
||||||
"messages.privateDetailTitle": "Сообщение #{id}",
|
|
||||||
"messages.detailEyebrow": "Детали сообщения",
|
|
||||||
"messages.backPrivate": "Назад к личным сообщениям",
|
|
||||||
"messages.backGroup": "Назад к групповым сообщениям",
|
|
||||||
"messages.ownerPeerTitle": "Владелец {owner} · Собеседник {peer}",
|
|
||||||
"messages.senderSubtitle": "Отправитель {sender} · {date}",
|
|
||||||
"messages.boxID": "ID ящика сообщений",
|
|
||||||
"messages.privateMessageID": "ID личного сообщения",
|
|
||||||
"messages.messageSender": "Отправитель сообщения",
|
|
||||||
"messages.messageBox": "Ящик сообщений",
|
|
||||||
"messages.dialogRow": "Строка диалога",
|
|
||||||
"messages.privateRow": "Строка личного сообщения",
|
|
||||||
"messages.channelMessageRow": "Строка сообщения канала",
|
|
||||||
"messages.channelRow": "Строка канала",
|
|
||||||
"messages.userUpdateEvents": "События обновления пользователей",
|
|
||||||
"messages.channelUpdateEvents": "События обновления каналов",
|
|
||||||
"messages.eventJson": "JSON события",
|
|
||||||
"messages.dispatchOutbox": "Очередь отправки (Outbox)",
|
|
||||||
"messages.messageBoxesSnapshot": "Снимок message_boxes только для чтения",
|
|
||||||
"messages.dialogSnapshot": "Снимок dialogs только для чтения",
|
|
||||||
"messages.privateSnapshot": "Снимок private_messages только для чтения",
|
|
||||||
"messages.channelMessagesSnapshot": "Снимок channel_messages только для чтения",
|
|
||||||
"messages.channelSnapshot": "Снимок channels только для чтения",
|
|
||||||
"messages.userEventsSource": "постоянные user_update_events",
|
|
||||||
"messages.channelEventsSource": "постоянные channel_update_events",
|
|
||||||
"messages.outboxSource": "онлайн/офлайн dispatch_outbox",
|
|
||||||
"messages.attempts": "Попытки",
|
|
||||||
"messages.deleteThis": "Удалить это сообщение",
|
|
||||||
"messages.groupDetailTitle": "Групповое сообщение #{id}",
|
|
||||||
"messages.channelGroupTitle": "Канал / Группа {id}",
|
|
||||||
"messages.mediaCount": "С медиафайлами",
|
|
||||||
"messages.channelPosts": "Посты канала",
|
|
||||||
"messages.channelGroup": "Канал / Группа",
|
|
||||||
"messages.pinned": "Закреплено",
|
|
||||||
"messages.channelPost": "Пост в канале",
|
|
||||||
"gifts.pageTitle": "Каталог звездных подарков",
|
|
||||||
"gifts.eyebrow": "Каталог, неизменяемые версии и файлы анимаций",
|
|
||||||
"gifts.total": "Подарков в каталоге",
|
|
||||||
"gifts.enabled": "Включено",
|
|
||||||
"gifts.received": "Полученные подарки",
|
|
||||||
"gifts.formats": "Поддерживаемые форматы",
|
|
||||||
"gifts.add": "Добавить подарок",
|
|
||||||
"gifts.importAll": "Импортировать все официальные подарки",
|
|
||||||
"gifts.searchPlaceholder": "Поиск по ID подарка, названию или формату",
|
|
||||||
"gifts.listSummary": "Показано {shown} из {total}",
|
|
||||||
"gifts.idRevision": "ID / Версия",
|
|
||||||
"gifts.price": "Цена / Конвертация",
|
|
||||||
"gifts.importTitle": "Импорт звездного подарка",
|
|
||||||
"gifts.importEyebrow": "Управление каталогом подарков",
|
|
||||||
"gifts.newRevision": "Создать версию для подарка #{id}",
|
|
||||||
"gifts.importHint": "Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.",
|
|
||||||
"gifts.officialSource": "Официальный снимок",
|
|
||||||
"gifts.fileSource": "Загрузить файл",
|
|
||||||
"gifts.officialHint": "Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.",
|
|
||||||
"gifts.officialSearch": "Поиск по ID или названию официального подарка",
|
|
||||||
"gifts.officialSelect": "Выберите официальный подарок",
|
|
||||||
"gifts.officialRequired": "Сначала выберите официальный подарок",
|
|
||||||
"gifts.officialResults": "Показано {shown} из {total}",
|
|
||||||
"gifts.officialCategoryLabel": "Категория возможностей официального подарка",
|
|
||||||
"gifts.officialCategory.all": "Все",
|
|
||||||
"gifts.officialCategory.upgrade": "Можно улучшить",
|
|
||||||
"gifts.officialCategory.craft": "Можно создать",
|
|
||||||
"gifts.officialCategory.basic": "Нельзя улучшить",
|
|
||||||
"gifts.officialUnnamed": "Официальный подарок без названия #{id}",
|
|
||||||
"gifts.officialAttributes": "Атрибутов: {count}",
|
|
||||||
"gifts.canUpgrade": "Можно улучшить",
|
|
||||||
"gifts.cannotUpgrade": "Нельзя улучшить",
|
|
||||||
"gifts.canCraft": "Можно создать",
|
|
||||||
"gifts.cannotCraft": "Нельзя создать",
|
|
||||||
"gifts.officialEmpty": "Нет подарков, соответствующих категории и поиску.",
|
|
||||||
"gifts.includeCollectible": "Импортировать полный пул коллекционных предметов, включая созданные модели",
|
|
||||||
"gifts.animation": "Файл анимации",
|
|
||||||
"gifts.filePrompt": "Перетащите или выберите файл TGS / Lottie",
|
|
||||||
"gifts.fileHint": "TGS, JSON или Lottie · файл проверяется перед импортом",
|
|
||||||
"gifts.chooseFile": "Выбрать файл",
|
|
||||||
"gifts.changeFile": "Изменить файл",
|
|
||||||
"gifts.title": "Отображаемое название",
|
|
||||||
"gifts.titlePlaceholder": "например, Праздничная звезда",
|
|
||||||
"gifts.stars": "Цена в Звездах",
|
|
||||||
"gifts.convertStars": "Звезд при конвертации",
|
|
||||||
"gifts.sortOrder": "Порядок сортировки",
|
|
||||||
"gifts.reason": "Причина для аудита",
|
|
||||||
"gifts.reasonPlaceholder": "Кратко опишите причину импорта этого подарка",
|
|
||||||
"gifts.enableAfterImport": "Включить после импорта",
|
|
||||||
"gifts.validate": "Тестовая проверка",
|
|
||||||
"gifts.confirmImport": "Подтвердить импорт",
|
|
||||||
"gifts.stepDetails": "Файл и описание",
|
|
||||||
"gifts.stepValidate": "Тестовая проверка",
|
|
||||||
"gifts.stepImport": "Подтверждение импорта",
|
|
||||||
"gifts.fileRequired": "Сначала выберите файл TGS или Lottie",
|
|
||||||
"gifts.source": "Источник",
|
|
||||||
"gifts.replace": "Новая версия",
|
|
||||||
"gifts.disable": "Отключить",
|
|
||||||
"gifts.enable": "Включить",
|
|
||||||
"gifts.bulkSelected": "Выбрано: {count}",
|
|
||||||
"gifts.bulkSelectAll": "Выбрать все видимые подарки",
|
|
||||||
"gifts.bulkSelectOne": "Выбрать подарок {id}",
|
|
||||||
"gifts.bulkEnable": "Включить выбранные",
|
|
||||||
"gifts.bulkDisable": "Отключить выбранные",
|
|
||||||
"gifts.bulkStatusFailed": "{failed} из {total} не выполнено",
|
|
||||||
"gifts.empty": "Звездные подарки еще не импортированы.",
|
|
||||||
"gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.",
|
|
||||||
"gifts.validationReady": "Проверка пройдена",
|
|
||||||
"gifts.validationHint": "Проверьте нормализованные метаданные и подтвердите импорт.",
|
|
||||||
"gifts.confirmState": "Применить проверенные изменения состояния к подарку #{id}?",
|
|
||||||
"collectibles.manage": "Пул атрибутов",
|
|
||||||
"collectibles.title": "Пул коллекционных предметов · Подарок #{id}",
|
|
||||||
"collectibles.eyebrow": "Уникальные атрибуты подарка",
|
|
||||||
"collectibles.activeRevision": "Опубликованная версия {revision}",
|
|
||||||
"collectibles.published": "Опубликовано",
|
|
||||||
"collectibles.noPool": "Нет опубликованного пула коллекционных предметов",
|
|
||||||
"collectibles.noPoolHint": "Опубликуйте модели, узоры и фоны для активации улучшений.",
|
|
||||||
"collectibles.publishNew": "Опубликовать новую неизменяемую версию",
|
|
||||||
"collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и итоговые показатели редкости перед тем, как версия станет активной.",
|
|
||||||
"collectibles.upgradeStars": "Цена улучшения в Звездах",
|
|
||||||
"collectibles.supply": "Уникальный тираж",
|
|
||||||
"collectibles.slug": "Публичный префикс ссылки (slug)",
|
|
||||||
"collectibles.models": "Модели",
|
|
||||||
"collectibles.patterns": "Узоры",
|
|
||||||
"collectibles.backdrops": "Фоны",
|
|
||||||
"collectibles.model": "Модель",
|
|
||||||
"collectibles.pattern": "Узор",
|
|
||||||
"collectibles.backdrop": "Фон",
|
|
||||||
"collectibles.rarity": "Редкость ‰",
|
|
||||||
"collectibles.rarityHint": "Значения permille — это относительные веса обычного улучшения; их сумма не обязана равняться 1000.",
|
|
||||||
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.",
|
|
||||||
"collectibles.addAttribute": "Добавить",
|
|
||||||
"collectibles.remove": "Удалить атрибут",
|
|
||||||
"collectibles.fileRequired": "Для каждой модели и узора требуется файл TGS или Lottie.",
|
|
||||||
"collectibles.backdropID": "ID фона",
|
|
||||||
"collectibles.color.center": "Центр",
|
|
||||||
"collectibles.color.edge": "Край",
|
|
||||||
"collectibles.color.pattern": "Узор",
|
|
||||||
"collectibles.color.text": "Текст",
|
|
||||||
"collectibles.validationReady": "Пул атрибутов корректен",
|
|
||||||
"collectibles.validationHint": "Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.",
|
|
||||||
"collectibles.publish": "Опубликовать версию",
|
|
||||||
"messages.msgIDsInvalid": "Некорректные ID сообщений",
|
|
||||||
"auth.device": "Устройство",
|
|
||||||
"auth.platform": "Платформа",
|
|
||||||
"auth.ip": "IP-адрес",
|
|
||||||
"auth.lastActive": "Последняя активность",
|
|
||||||
"auth.revokeCurrent": "Отозвать текущую",
|
|
||||||
"auth.keepCurrent": "Оставить текущую",
|
|
||||||
"auth.revokeAll": "Разлогинить все устройства",
|
|
||||||
"picker.userPlaceholder": "Поиск по user_id / телефону / имени пользователя",
|
|
||||||
"picker.channelPlaceholder": "Поиск по channel_id / имени пользователя / названию",
|
|
||||||
"picker.verified": "Подтвержденные",
|
|
||||||
"picker.regular": "Обычные",
|
|
||||||
"action.reasonRequired": "Пожалуйста, укажите причину операции",
|
|
||||||
"action.flow": "Процесс выполнения",
|
|
||||||
"action.close": "Закрыть",
|
|
||||||
"action.stepReason": "Укажите причину",
|
|
||||||
"action.stepDryRun": "Тестовый запуск",
|
|
||||||
"action.stepConfirm": "Подтверждение выполнения",
|
|
||||||
"action.reason": "Причина операции",
|
|
||||||
"action.reasonPlaceholder": "Опишите, почему выполняется эта операция",
|
|
||||||
"action.requestPreview": "Запросить предпросмотр",
|
|
||||||
"action.result": "Результат действия",
|
|
||||||
"action.commandID": "ID команды",
|
|
||||||
"action.status": "Статус",
|
|
||||||
"action.dryRun": "Тестовый запуск",
|
|
||||||
"action.runAgain": "Запустить тестовый запуск снова",
|
|
||||||
"action.runDry": "Сначала выполните тестовый запуск",
|
|
||||||
"action.confirm": "Подтвердить выполнение",
|
|
||||||
"audit.id": "ID",
|
|
||||||
"audit.commandID": "ID команды",
|
|
||||||
"audit.action": "Действие",
|
|
||||||
"audit.actor": "Исполнитель",
|
|
||||||
"audit.status": "Статус",
|
|
||||||
"audit.dryRun": "Тестовый запуск",
|
|
||||||
"audit.reason": "Причина",
|
|
||||||
"audit.time": "Время"
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type I18nContextValue = {
|
type I18nContextValue = {
|
||||||
lang: Language;
|
|
||||||
setLang: (lang: Language) => void;
|
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const I18nContext = createContext<I18nContextValue | null>(null);
|
const I18nContext = createContext<I18nContextValue | null>(null);
|
||||||
|
|
||||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||||
const [lang, setLang] = useState<Language>(() => initialLanguage());
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
document.documentElement.lang = "en";
|
||||||
localStorage.setItem(storageKey, lang);
|
|
||||||
} catch {
|
|
||||||
// Language persistence is best-effort.
|
|
||||||
}
|
|
||||||
const langAttr = lang === "zh" ? "zh-CN" : lang === "ru" ? "ru" : "en";
|
|
||||||
document.documentElement.lang = langAttr;
|
|
||||||
document.documentElement.dir = "ltr";
|
document.documentElement.dir = "ltr";
|
||||||
document.documentElement.setAttribute("translate", "no");
|
document.documentElement.setAttribute("translate", "no");
|
||||||
document.body.classList.add("notranslate");
|
document.body.classList.add("notranslate");
|
||||||
document.title = translate(lang, "app.title");
|
document.title = translate("app.title");
|
||||||
}, [lang]);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo<I18nContextValue>(() => ({
|
const value = useMemo<I18nContextValue>(() => ({
|
||||||
lang,
|
t: (key, params) => translate(key, params)
|
||||||
setLang,
|
}), []);
|
||||||
t: (key, params) => translate(lang, key, params)
|
|
||||||
}), [lang]);
|
|
||||||
|
|
||||||
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
@ -1182,73 +408,10 @@ export function useI18n(): I18nContextValue {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LanguageSwitch() {
|
export function translate(key: string, params?: TranslationParams): string {
|
||||||
const { lang, setLang, t } = useI18n();
|
const value = translations[key] ?? key;
|
||||||
return (
|
|
||||||
<div className="language-switch" role="group" aria-label="Language">
|
|
||||||
{(["en", "zh", "ru"] as const).map((item) => (
|
|
||||||
<button
|
|
||||||
key={item}
|
|
||||||
className={lang === item ? "active" : ""}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={lang === item}
|
|
||||||
onClick={() => setLang(item)}
|
|
||||||
>
|
|
||||||
{t(`language.${item}`)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function translate(lang: Language, key: string, params?: TranslationParams): string {
|
|
||||||
const value = translations[lang][key] ?? translations.en[key] ?? key;
|
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return value.replace(/\{(\w+)\}/g, (_, name: string) => String(params[name] ?? ""));
|
return value.replace(/\{(\w+)\}/g, (_, name: string) => String(params[name] ?? ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialLanguage(): Language {
|
|
||||||
try {
|
|
||||||
const requested = normalizeLanguage(new URLSearchParams(window.location.search).get("lang"));
|
|
||||||
if (requested) {
|
|
||||||
return requested;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// URLSearchParams can fail in unusual embedded contexts.
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const saved = normalizeLanguage(localStorage.getItem(storageKey));
|
|
||||||
if (saved) {
|
|
||||||
return saved;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Storage is optional.
|
|
||||||
}
|
|
||||||
const browserLanguages = navigator.languages?.length ? navigator.languages : [navigator.language];
|
|
||||||
for (const item of browserLanguages) {
|
|
||||||
const normalized = normalizeLanguage(item);
|
|
||||||
if (normalized) {
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "en";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLanguage(value: string | null | undefined): Language | null {
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const tag = value.trim().toLowerCase().replace("_", "-");
|
|
||||||
if (tag === "zh" || tag.startsWith("zh-")) {
|
|
||||||
return "zh";
|
|
||||||
}
|
|
||||||
if (tag === "en" || tag.startsWith("en-")) {
|
|
||||||
return "en";
|
|
||||||
}
|
|
||||||
if (tag === "ru" || tag.startsWith("ru-")) {
|
|
||||||
return "ru";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import type { FormEvent } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { api, errorMessage } from "../api";
|
import { api, errorMessage } from "../api";
|
||||||
import { Alert } from "../components/ui";
|
import { Alert } from "../components/ui";
|
||||||
import { LanguageSwitch, useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
|
|
||||||
export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
|
export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -29,14 +29,13 @@ export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
|
||||||
<section className="login-panel">
|
<section className="login-panel">
|
||||||
<div className="login-head">
|
<div className="login-head">
|
||||||
<div className="brand brand-elevated">
|
<div className="brand brand-elevated">
|
||||||
<span className="brand-mark">T</span>
|
<span className="brand-mark"><img src="/logo.png" alt="OwpenGram" /></span>
|
||||||
<span>
|
<span>
|
||||||
<strong>telesrv</strong>
|
<strong>OwpenGram</strong>
|
||||||
<small>{t("app.adminConsole")}</small>
|
<small>{t("app.adminConsole")}</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="login-head-actions">
|
<div className="login-head-actions">
|
||||||
<LanguageSwitch />
|
|
||||||
<span className="login-chip">{t("app.localAccess")}</span>
|
<span className="login-chip">{t("app.localAccess")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,31 @@
|
||||||
|
@font-face { font-family: "Plus Jakarta Sans"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/plus-jakarta-sans-400.woff2") format("woff2"); }
|
||||||
|
@font-face { font-family: "Plus Jakarta Sans"; font-style: normal; font-weight: 500; font-display: swap; src: url("/fonts/plus-jakarta-sans-500.woff2") format("woff2"); }
|
||||||
|
@font-face { font-family: "Plus Jakarta Sans"; font-style: normal; font-weight: 600; font-display: swap; src: url("/fonts/plus-jakarta-sans-600.woff2") format("woff2"); }
|
||||||
|
@font-face { font-family: "Plus Jakarta Sans"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/plus-jakarta-sans-700.woff2") format("woff2"); }
|
||||||
|
@font-face { font-family: "Plus Jakarta Sans"; font-style: normal; font-weight: 800; font-display: swap; src: url("/fonts/plus-jakarta-sans-800.woff2") format("woff2"); }
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--bg: #f3f5f7;
|
--bg: #f7f9fc;
|
||||||
--panel: #ffffff;
|
--panel: #ffffff;
|
||||||
--panel-subtle: #f8fafb;
|
--panel-subtle: #f7f9fc;
|
||||||
--panel-strong: #eef2f5;
|
--panel-strong: #f1f5f9;
|
||||||
--line: #d9e1e8;
|
--line: #e2e8f0;
|
||||||
--line-strong: #c2ccd6;
|
--line-strong: #cbd5e1;
|
||||||
--text: #101828;
|
--text: #0f1720;
|
||||||
--muted: #667085;
|
--muted: #64748b;
|
||||||
--muted-2: #98a2b3;
|
--muted-2: #94a3b8;
|
||||||
--brand: #176d61;
|
--brand: #2563eb;
|
||||||
--brand-2: #245b9d;
|
--brand-2: #38bdf8;
|
||||||
|
--grad: linear-gradient(135deg, #38bdf8 0%, #2563eb 55%, #1e40af 100%);
|
||||||
--good: #167447;
|
--good: #167447;
|
||||||
--warn: #a15c07;
|
--warn: #a15c07;
|
||||||
--danger: #b42318;
|
--danger: #b42318;
|
||||||
--sidebar: #11161d;
|
--sidebar: #08080e;
|
||||||
--sidebar-soft: #1b222b;
|
--sidebar-soft: #12121a;
|
||||||
--sidebar-line: #2c3541;
|
--sidebar-line: #222228;
|
||||||
--focus: rgba(23, 109, 97, 0.16);
|
--focus: rgba(37, 99, 235, 0.16);
|
||||||
--shadow: 0 18px 52px rgba(16, 24, 40, 0.14);
|
--shadow: 0 28px 70px -36px rgba(5, 5, 8, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -35,7 +42,7 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
font: 13px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font: 13px/1.45 "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
|
|
@ -81,20 +88,18 @@ a {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-elevated .brand-mark {
|
|
||||||
box-shadow: 0 8px 24px rgba(23, 109, 97, 0.26);
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-mark {
|
.brand-mark {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: #ffffff;
|
}
|
||||||
background: var(--brand);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
.brand-mark img {
|
||||||
border-radius: 8px;
|
display: block;
|
||||||
font-weight: 800;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand strong {
|
.brand strong {
|
||||||
|
|
@ -261,37 +266,6 @@ a {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-switch {
|
|
||||||
display: inline-flex;
|
|
||||||
min-height: 30px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px;
|
|
||||||
background: var(--panel-subtle);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-switch button {
|
|
||||||
min-width: 42px;
|
|
||||||
min-height: 24px;
|
|
||||||
padding: 0 9px;
|
|
||||||
color: var(--muted);
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-switch button.active {
|
|
||||||
color: #ffffff;
|
|
||||||
background: var(--brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-switch button:focus-visible {
|
|
||||||
outline: 2px solid var(--brand);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actor-pill {
|
.actor-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
||||||
|
|
@ -110,8 +110,8 @@
|
||||||
height: 38px;
|
height: 38px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
background: #edf7f4;
|
background: #eaf2fd;
|
||||||
border: 1px solid #c9e2dc;
|
border: 1px solid #c7dcf9;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,9 +217,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 7px 9px;
|
padding: 7px 9px;
|
||||||
color: #0f3f38;
|
color: #1e3a8a;
|
||||||
background: #eef8f5;
|
background: #eaf2fe;
|
||||||
border: 1px solid #b9dcd3;
|
border: 1px solid #c3d9f7;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +291,7 @@
|
||||||
|
|
||||||
.picker-row:hover,
|
.picker-row:hover,
|
||||||
.picker-row.selected {
|
.picker-row.selected {
|
||||||
background: #f3f8f6;
|
background: #eef4ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-row strong,
|
.picker-row strong,
|
||||||
|
|
@ -410,7 +410,7 @@ textarea:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary:hover:not(:disabled) {
|
.btn.primary:hover:not(:disabled) {
|
||||||
background: #12594f;
|
background: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.ghost {
|
.btn.ghost {
|
||||||
|
|
|
||||||
|
|
@ -265,12 +265,12 @@
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
background: #eaf6f3;
|
background: #eaf2fd;
|
||||||
border: 1px solid #c7e3dc;
|
border: 1px solid #c7dcf9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-format-chips { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
|
.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: #1e40af; background: #eaf2fe; border: 1px solid #cbdcf7; 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; }
|
.gift-list-summary { margin-left: auto; color: var(--muted); font-size: 11px; font-weight: 700; }
|
||||||
|
|
||||||
|
|
@ -284,12 +284,12 @@
|
||||||
.official-gift-categories { display: flex; flex-wrap: wrap; gap: 7px; }
|
.official-gift-categories { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||||
.official-gift-categories button {
|
.official-gift-categories button {
|
||||||
display: inline-flex; align-items: center; gap: 7px; min-height: 32px; padding: 5px 10px;
|
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: #3f5a78; background: #f5f8fd; border: 1px solid #d7e2f4; border-radius: 999px;
|
||||||
font: inherit; font-size: 11px; font-weight: 800; cursor: pointer;
|
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;
|
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:hover { color: var(--brand); border-color: #9dc3f5; }
|
||||||
.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.active { color: #ffffff; background: var(--brand); border-color: var(--brand); box-shadow: 0 4px 12px rgba(37, 99, 235, .17); }
|
||||||
.official-gift-categories button span {
|
.official-gift-categories button span {
|
||||||
display: grid; min-width: 20px; height: 20px; padding: 0 5px; place-items: center;
|
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(255,255,255,.65); border-radius: 999px; font-size: 10px;
|
||||||
|
|
@ -298,20 +298,20 @@
|
||||||
.official-gift-list {
|
.official-gift-list {
|
||||||
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; max-height: 314px;
|
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;
|
min-height: 126px; padding: 8px; overflow: auto; border: 1px solid var(--line); border-radius: 14px;
|
||||||
background: #f6f9f8; scrollbar-gutter: stable;
|
background: #f5f8fd; scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
.official-gift-option {
|
.official-gift-option {
|
||||||
display: grid; min-width: 0; gap: 8px; padding: 11px 12px; text-align: left; color: var(--text);
|
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;
|
background: #ffffff; border: 1px solid #dbe6f7; border-radius: 11px; cursor: pointer;
|
||||||
box-shadow: 0 1px 2px rgba(32, 54, 50, .03);
|
box-shadow: 0 1px 2px rgba(30, 41, 82, .03);
|
||||||
transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
|
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:hover { border-color: #9dc3f5; box-shadow: 0 5px 14px rgba(30, 64, 175, .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.selected { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(37, 99, 235, .12), 0 5px 14px rgba(30, 64, 175, .08); }
|
||||||
.official-gift-option-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: baseline; }
|
.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 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-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: #5b6b85; font-size: 10px; font-weight: 700; }
|
||||||
.official-gift-capabilities { display: flex; flex-wrap: wrap; gap: 5px; }
|
.official-gift-capabilities { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||||
.official-gift-capabilities > span {
|
.official-gift-capabilities > span {
|
||||||
padding: 3px 7px; border: 1px solid transparent; border-radius: 999px; font-size: 9px; font-weight: 850; letter-spacing: .01em;
|
padding: 3px 7px; border: 1px solid transparent; border-radius: 999px; font-size: 9px; font-weight: 850; letter-spacing: .01em;
|
||||||
|
|
@ -342,21 +342,21 @@
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px dashed #b7ccc8;
|
border: 1px dashed #b7c9e8;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color .16s ease, background .16s ease, box-shadow .16s ease;
|
transition: border-color .16s ease, background .16s ease, box-shadow .16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-file-picker:hover,
|
.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: #f5f9ff; border-color: var(--brand); box-shadow: 0 0 0 2px rgba(37, 99, 235, .05); }
|
||||||
.gift-file-picker input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
.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: 9px; }
|
||||||
.gift-file-copy { display: grid; min-width: 0; gap: 2px; }
|
.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-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; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.gift-file-copy small { color: var(--muted); font-size: 11px; font-weight: 500; }
|
.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: #eef4ff; border: 1px solid #c7dcf9; border-radius: 7px; font-size: 11px; font-weight: 800; }
|
||||||
|
|
||||||
.gift-fields-grid {
|
.gift-fields-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -386,7 +386,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-fields-grid input:focus,
|
.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: #7bb4f0; box-shadow: 0 0 0 3px rgba(37, 99, 235, .08); 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: #344054; font-size: 12px; font-weight: 700; cursor: pointer; }
|
||||||
.gift-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
.gift-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||||
|
|
@ -394,14 +394,14 @@
|
||||||
.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-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 { background: var(--brand); }
|
||||||
.gift-switch input:checked + .gift-switch-track span { transform: translateX(15px); }
|
.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-switch input:focus-visible + .gift-switch-track { outline: 3px solid rgba(37, 99, 235, .16); outline-offset: 2px; }
|
||||||
.gift-validation { overflow: hidden; color: #d5fff5; background: #173631; border: 1px solid #24564e; border-radius: 9px; }
|
.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-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-validation-head div { display: grid; gap: 2px; }
|
.gift-validation-head div { display: grid; gap: 2px; }
|
||||||
.gift-validation-head span { color: #99cfc4; font-size: 10px; }
|
.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 pre { max-height: 180px; overflow: auto; margin: 0; padding: 11px 12px; color: #d5fff5; 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: radial-gradient(circle, #f9f3ff, #eaf2fe); }
|
||||||
.gift-animation { width: 200px; height: 200px; }
|
.gift-animation { width: 200px; height: 200px; }
|
||||||
.gift-animation canvas { width: 100% !important; height: 100% !important; }
|
.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: rgba(255,255,255,.9); border: 1px solid var(--line); border-radius: 50%; }
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
|
|
||||||
.command-step.active {
|
.command-step.active {
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
border-color: #a9d8ce;
|
border-color: #a9c8f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-step.done {
|
.command-step.done {
|
||||||
|
|
@ -201,8 +201,8 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
background: #edf7f4;
|
background: #eaf2fd;
|
||||||
border: 1px solid #c9e2dc;
|
border: 1px solid #c7dcf9;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue