This commit is contained in:
onysd 2026-07-24 22:46:05 +03:00
parent b443ff0c73
commit 3325e793f9
10 changed files with 76 additions and 121 deletions

17
.claude/launch.json Normal file
View file

@ -0,0 +1,17 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "telesrv-admin-web",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev", "--prefix", "cmd/telesrv-admin/web"],
"port": 2410
},
{
"name": "owpengram-site-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev", "--prefix", "../owpengram-site"],
"port": 5173
}
]
}

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

@ -23,8 +23,8 @@
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-ilFwQ32E.js"></script> <script type="module" crossorigin src="/assets/index-Bdm4PGkp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B998Ff2K.css"> <link rel="stylesheet" crossorigin href="/assets/index-DWxQF996.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -16,7 +16,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 { ThemeSwitch } from "../theme"; import { ThemeSwitch } from "../theme";
import { AppLink } from "./AppLink"; import { AppLink } from "./AppLink";
@ -133,7 +133,6 @@ export function Shell({
</div> </div>
<div className="topbar-actions"> <div className="topbar-actions">
<ThemeSwitch /> <ThemeSwitch />
<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")}

View file

@ -1564,25 +1564,6 @@ export function useI18n(): I18nContextValue {
return value; return value;
} }
export function LanguageSwitch() {
const { lang, setLang, t } = useI18n();
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 { export function translate(lang: Language, key: string, params?: TranslationParams): string {
const value = translations[lang][key] ?? translations.en[key] ?? key; const value = translations[lang][key] ?? translations.en[key] ?? key;
if (!params) { if (!params) {

View file

@ -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";
import { ThemeSwitch } from "../theme"; import { ThemeSwitch } from "../theme";
export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) { export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
@ -38,7 +38,6 @@ export function LoginPage({ onLogin }: { onLogin: (actor: string) => void }) {
</div> </div>
<div className="login-head-actions"> <div className="login-head-actions">
<ThemeSwitch /> <ThemeSwitch />
<LanguageSwitch />
<span className="login-chip">{t("app.localAccess")}</span> <span className="login-chip">{t("app.localAccess")}</span>
</div> </div>
</div> </div>

View file

@ -8,42 +8,43 @@
color-scheme: light; color-scheme: light;
/* Surfaces */ /* Surfaces */
--bg: #eef1f5; --bg: #f7f9fc;
--bg-accent: #e7ecf1; --bg-accent: #eef1f5;
--panel: #ffffff; --panel: #ffffff;
--panel-subtle: #f5f8fb; --panel-subtle: #f7f9fc;
--panel-strong: #eef2f6; --panel-strong: #f1f5f9;
--surface-soft: #f2f7f6; --surface-soft: #f2f7fd;
--overlay: rgba(24, 34, 47, 0.42); --overlay: rgba(24, 34, 47, 0.42);
--topbar-bg: rgba(255, 255, 255, 0.86); --topbar-bg: rgba(255, 255, 255, 0.94);
/* Lines */ /* Lines */
--line: #e5eaf0; --line: #e2e8f0;
--line-strong: #d3dce4; --line-strong: #cbd5e1;
/* Text */ /* Text */
--heading: #253040; --heading: #101828;
--text: #333f4d; --text: #0f1720;
--text-soft: #45525f; --text-soft: #344054;
--muted: #6d7885; --muted: #64748b;
--muted-2: #9aa4b1; --muted-2: #94a3b8;
/* Brand */ /* Brand */
--brand: #1f7d6f; --brand: #2563eb;
--brand-strong: #196155; --brand-strong: #1d4ed8;
--brand-2: #3a6cae; --brand-2: #38bdf8;
--brand-tint: #e8f4f0; --grad: linear-gradient(135deg, #38bdf8 0%, #2563eb 55%, #1e40af 100%);
--brand-tint-border: #c8e2db; --brand-tint: #eaf2fd;
--brand-tint-text: #235d53; --brand-tint-border: #c7dcf9;
--brand-tint-text: #1e3a8a;
/* Semantic */ /* Semantic */
--good: #1f8a57; --good: #167447;
--good-tint: #eaf6ef; --good-tint: #eaf6ef;
--good-border: #c1e1cf; --good-border: #c1e1cf;
--warn: #a86a12; --warn: #a15c07;
--warn-tint: #fcf4e4; --warn-tint: #fcf4e4;
--warn-border: #e7d09e; --warn-border: #e7d09e;
--danger: #c0392b; --danger: #b42318;
--danger-tint: #fcefec; --danger-tint: #fcefec;
--danger-border: #eecac3; --danger-border: #eecac3;
--danger-text: #8f2f27; --danger-text: #8f2f27;
@ -67,20 +68,20 @@
--code-border: #2b3a49; --code-border: #2b3a49;
/* Sidebar */ /* Sidebar */
--sidebar: #1c2530; --sidebar: #08080e;
--sidebar-soft: #26313d; --sidebar-soft: #12121a;
--sidebar-line: #313c4a; --sidebar-line: #222228;
--sidebar-row: #232d38; --sidebar-row: #17171f;
--sidebar-text: #dbe3ec; --sidebar-text: #c6d0dc;
--sidebar-muted: #8b98a8; --sidebar-muted: #8fa0b4;
--sidebar-faint: #7c8a9a; --sidebar-faint: #8492a6;
--sidebar-heading: #ffffff; --sidebar-heading: #ffffff;
/* Effects */ /* Effects */
--focus: rgba(31, 125, 111, 0.16); --focus: rgba(37, 99, 235, 0.16);
--shadow: 0 12px 34px rgba(24, 39, 56, 0.1); --shadow: 0 28px 70px -36px rgba(5, 5, 8, 0.35);
--shadow-sm: 0 2px 10px rgba(24, 39, 56, 0.05); --shadow-sm: 0 2px 10px rgba(24, 39, 56, 0.05);
--shadow-brand: 0 8px 22px rgba(31, 125, 111, 0.22); --shadow-brand: 0 8px 22px rgba(37, 99, 235, 0.22);
/* Radii */ /* Radii */
--radius-xs: 8px; --radius-xs: 8px;
@ -110,12 +111,12 @@
--muted: #98a4b1; --muted: #98a4b1;
--muted-2: #6d7885; --muted-2: #6d7885;
--brand: #37a596; --brand: #5b9dff;
--brand-strong: #45b6a6; --brand-strong: #7db4ff;
--brand-2: #6fa8e6; --brand-2: #7cd1fb;
--brand-tint: #14322d; --brand-tint: #142a4a;
--brand-tint-border: #245349; --brand-tint-border: #24466e;
--brand-tint-text: #7fd3c4; --brand-tint-text: #9dc3f5;
--good: #47c281; --good: #47c281;
--good-tint: #12301f; --good-tint: #12301f;
@ -152,10 +153,10 @@
--sidebar-faint: #6f7b88; --sidebar-faint: #6f7b88;
--sidebar-heading: #f0f4f8; --sidebar-heading: #f0f4f8;
--focus: rgba(55, 165, 150, 0.24); --focus: rgba(91, 157, 255, 0.24);
--shadow: 0 16px 40px rgba(0, 0, 0, 0.46); --shadow: 0 16px 40px rgba(0, 0, 0, 0.46);
--shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.38); --shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.38);
--shadow-brand: 0 8px 22px rgba(55, 165, 150, 0.26); --shadow-brand: 0 8px 22px rgba(91, 157, 255, 0.26);
} }
* { * {
@ -221,20 +222,11 @@ a {
justify-content: center; justify-content: center;
} }
.brand-elevated .brand-mark {
box-shadow: var(--shadow-brand);
}
.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);
border-radius: var(--radius-sm);
font-weight: 800;
} }
.brand-mark img { .brand-mark img {
@ -299,7 +291,7 @@ a {
.nav-section.active .nav-section-toggle { .nav-section.active .nav-section-toggle {
color: var(--sidebar-heading); color: var(--sidebar-heading);
background: var(--sidebar-soft); background: var(--sidebar-soft);
border-color: var(--sidebar-line); border-color: #34404d;
} }
.nav-section-chevron { .nav-section-chevron {
@ -343,7 +335,7 @@ a {
.nav-item.active { .nav-item.active {
color: var(--sidebar-heading); color: var(--sidebar-heading);
background: var(--sidebar-soft); background: var(--sidebar-soft);
border-color: var(--sidebar-line); border-color: #34404d;
} }
.nav-item.active .nav-dot { .nav-item.active .nav-dot {
@ -365,7 +357,7 @@ a {
padding: 0 8px; padding: 0 8px;
color: var(--sidebar-text); color: var(--sidebar-text);
background: var(--sidebar-row); background: var(--sidebar-row);
border: 1px solid var(--sidebar-line); border: 1px solid #27313c;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
@ -412,39 +404,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;
transition: color 140ms ease, background-color 140ms ease;
}
.language-switch button.active {
color: #ffffff;
background: var(--brand);
}
.language-switch button:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.theme-toggle { .theme-toggle {
display: inline-grid; display: inline-grid;
width: 34px; width: 34px;