redesign(ui): спортивная палитра Strava + полная адаптивность
== Цветовая гамма == Сменили esports cyan+violet на спортивную Strava-палитру: - accent: orange-500 (#F97316) — энергия, движение, "GO" - accent-2: rose-500 (#F43F5E) — интенсивность, gradient pair - victory: lime-500 — личный рекорд / готово - defeat: red-600 - xp: amber-500 Dark theme переведена с холодного navy на тёплый графит, light theme очищена в "athletic paper" feel. == Репозиционирование текста == Убрали esports-сленг в пользу спортивной лексики: - Sidebar: "Play hard · Train harder" → "Move every day" - Sidebar footer: "GSI-трекинг матчей" → "Трекинг активности" - Dashboard микрозаголовок: "Mission control" → "Тренировка дня" - Dashboard HUD: "Cooldown / READY" → "До следующего / СЕЙЧАС", "Game tracking / LIVE / OFF" → "Трекинг матчей / LIVE / OFF" - Exercises: "Loadout" → "Программа" - Games: "Game integrations" → "Трекинг матчей" - Challenges: "Match rules" → "Правила за матч" - Settings: "Config" → "Конфигурация" - ExerciseCard: "Cooldown / PAUSED" → "Через / пауза" - ReminderApp: "Cooldown ready" → "Время тренировки", "Время размяться" → "Двигайся", "Next drop" → "Следующее", "Victory · упражнения заработаны" → "Победа · тренировка заработана", "Defeat · но тело — нет" → "Проигрыш · но тело не сдаётся", "all clear" → "готово", "Total / reps" → "Всего / повторов", "GG" → "Готово" == Адаптивная вёрстка == - App.tsx: state mobileNavOpen, авто-закрытие drawer на route change - Sidebar: три режима через CSS breakpoints — * lg+ (≥1024px): полная ширина w-60 с лейблами * md (768-1023px): icon-only w-16 с title-тултипами * <md (<768px): скрыта, открывается drawer-ом по hamburger - Titlebar: hamburger button слева на <md, title скрывается на <sm - Все hero-блоки страниц: flex-col на sm, flex-row sm:items-end sm:justify-between с stack gap-4 - Padding страниц: p-4 sm:p-6 lg:p-8 вместо p-8 - Hero h1: text-3xl sm:text-4xl - Dashboard HUD strip: grid-cols-2 lg:grid-cols-4 (было 1/2/4) - Action buttons в карточках/строках: opacity-100 lg:opacity-0 lg:group-hover (на узких экранах всегда видны) - GameRow buttons: flex-wrap для длинных лейблов - Dashboard challenges shortcut: hint hidden sm:block - Sidebar mobile drawer: framer-motion слайд + backdrop с blur Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react'
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HashRouter, Route, Routes, useLocation } from 'react-router-dom'
|
||||
import { Sidebar } from './components/Sidebar'
|
||||
import { Titlebar } from './components/Titlebar'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
@@ -11,6 +11,7 @@ import { subscribeToBackend, useAppStore } from './store/appStore'
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const hydrated = useAppStore((s) => s.hydrated)
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = subscribeToBackend()
|
||||
@@ -20,18 +21,18 @@ export default function App(): JSX.Element {
|
||||
return (
|
||||
<HashRouter>
|
||||
<div className="h-screen w-screen flex flex-col bg-bg">
|
||||
<Titlebar title="Exercise Reminder" />
|
||||
<Titlebar
|
||||
title="Exercise Reminder"
|
||||
onMenuClick={() => setMobileNavOpen(true)}
|
||||
/>
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-hidden">
|
||||
<Sidebar
|
||||
mobileOpen={mobileNavOpen}
|
||||
onMobileClose={() => setMobileNavOpen(false)}
|
||||
/>
|
||||
<main className="flex-1 overflow-hidden min-w-0">
|
||||
{hydrated ? (
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/exercises" element={<Exercises />} />
|
||||
<Route path="/games" element={<GamesPage />} />
|
||||
<Route path="/challenges" element={<ChallengesPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
<RoutesWithCloseOnNav onClose={() => setMobileNavOpen(false)} />
|
||||
) : (
|
||||
<div className="p-8 text-muted">Загрузка…</div>
|
||||
)}
|
||||
@@ -41,3 +42,21 @@ export default function App(): JSX.Element {
|
||||
</HashRouter>
|
||||
)
|
||||
}
|
||||
|
||||
// Close mobile drawer whenever the route changes.
|
||||
function RoutesWithCloseOnNav({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
const location = useLocation()
|
||||
useEffect(() => {
|
||||
onClose()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname])
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/exercises" element={<Exercises />} />
|
||||
<Route path="/games" element={<GamesPage />} />
|
||||
<Route path="/challenges" element={<ChallengesPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ function ExerciseReminder({
|
||||
<div className="reminder-shell flex flex-col h-full hud-scanlines">
|
||||
<div className="titlebar-drag h-9 px-3 flex items-center justify-between">
|
||||
<div className="text-[10px] uppercase tracking-[0.2em] text-accent font-display font-semibold inline-flex items-center gap-1.5 px-2">
|
||||
<Flame size={11} /> Cooldown ready
|
||||
<Flame size={11} /> Время тренировки
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -158,7 +158,7 @@ function ExerciseReminder({
|
||||
</motion.div>
|
||||
|
||||
<div className="text-[10px] uppercase tracking-[0.28em] text-muted font-semibold">
|
||||
Время размяться
|
||||
Двигайся
|
||||
</div>
|
||||
<h1 className="font-display text-3xl font-bold mt-2 mb-3 uppercase tracking-wide">
|
||||
{exercise.name}
|
||||
@@ -176,7 +176,7 @@ function ExerciseReminder({
|
||||
</div>
|
||||
<div className="text-[11px] text-muted mt-4 inline-flex items-center gap-1.5">
|
||||
<Clock size={10} />
|
||||
Next drop через {formatInterval(exercise.intervalMinutes)}
|
||||
Следующее через {formatInterval(exercise.intervalMinutes)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -280,9 +280,9 @@ function MatchSummaryView({
|
||||
</motion.div>
|
||||
<h1 className="font-display font-bold text-xl uppercase tracking-wide">
|
||||
{won
|
||||
? 'Victory · упражнения заработаны'
|
||||
? 'Победа · тренировка заработана'
|
||||
: lost
|
||||
? 'Defeat · но тело — нет'
|
||||
? 'Проигрыш · но тело не сдаётся'
|
||||
: 'Матч завершён'}
|
||||
</h1>
|
||||
<p className="text-[11px] text-muted mt-1.5">
|
||||
@@ -293,7 +293,7 @@ function MatchSummaryView({
|
||||
челлендж{summary.results.length === 1 ? '' : 'а'} ·{' '}
|
||||
{allDone ? (
|
||||
<span className="text-victory font-semibold uppercase tracking-wider">
|
||||
all clear
|
||||
готово
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-accent font-bold font-mono-num">
|
||||
@@ -316,11 +316,11 @@ function MatchSummaryView({
|
||||
|
||||
<div className="px-6 pb-6 pt-3 flex items-center gap-3 border-t border-border/40">
|
||||
<div className="flex-1 text-[11px] text-muted uppercase tracking-[0.15em] font-semibold">
|
||||
Total ·{' '}
|
||||
Всего ·{' '}
|
||||
<span className="text-gradient-brand font-mono-num text-base font-bold tracking-normal">
|
||||
{totalReps}
|
||||
</span>{' '}
|
||||
reps
|
||||
повторов
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -331,7 +331,7 @@ function MatchSummaryView({
|
||||
>
|
||||
{allDone ? (
|
||||
<>
|
||||
<Check size={16} /> GG
|
||||
<Check size={16} /> Готово
|
||||
</>
|
||||
) : (
|
||||
'Позже'
|
||||
|
||||
@@ -130,7 +130,7 @@ export function ExerciseCard({
|
||||
<div className="relative">
|
||||
<div className="flex items-baseline justify-between mb-1.5">
|
||||
<span className="text-[10px] uppercase tracking-[0.18em] text-muted font-semibold">
|
||||
Cooldown
|
||||
Через
|
||||
</span>
|
||||
<span
|
||||
className={[
|
||||
@@ -138,7 +138,7 @@ export function ExerciseCard({
|
||||
isDue ? 'text-accent' : 'text-text'
|
||||
].join(' ')}
|
||||
>
|
||||
{exercise.enabled ? formatCountdown(ms) : 'PAUSED'}
|
||||
{exercise.enabled ? formatCountdown(ms) : 'пауза'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-surface-elevated/80 overflow-hidden">
|
||||
@@ -153,7 +153,7 @@ export function ExerciseCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="relative flex items-center gap-2 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={onMarkDone}
|
||||
className="flex-1 h-9 rounded-lg bg-victory/15 hover:bg-victory/25 text-victory text-xs font-semibold inline-flex items-center justify-center gap-1.5 transition-colors"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
ListChecks,
|
||||
Gamepad2,
|
||||
Target,
|
||||
Settings as SettingsIcon,
|
||||
Dumbbell
|
||||
Dumbbell,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
|
||||
const links = [
|
||||
@@ -16,42 +18,111 @@ const links = [
|
||||
{ to: '/settings', label: 'Настройки', icon: SettingsIcon }
|
||||
]
|
||||
|
||||
export function Sidebar(): JSX.Element {
|
||||
type Props = {
|
||||
mobileOpen?: boolean
|
||||
onMobileClose?: () => void
|
||||
}
|
||||
|
||||
export function Sidebar({ mobileOpen = false, onMobileClose }: Props): JSX.Element {
|
||||
return (
|
||||
<aside className="w-60 shrink-0 border-r border-border/60 bg-surface/40 backdrop-blur-sm flex flex-col relative">
|
||||
<>
|
||||
{/* Desktop sidebar: hidden on <md, icon-only on md, full on lg+ */}
|
||||
<aside className="hidden md:flex w-16 lg:w-60 shrink-0 border-r border-border/60 bg-surface/40 backdrop-blur-sm flex-col relative">
|
||||
<SidebarContent compact />
|
||||
</aside>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
<AnimatePresence>
|
||||
{mobileOpen && (
|
||||
<motion.div
|
||||
className="md:hidden fixed inset-0 z-50 flex"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={onMobileClose}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
/>
|
||||
<motion.aside
|
||||
className="relative w-64 max-w-[80vw] h-full border-r border-border/60 bg-surface flex flex-col"
|
||||
initial={{ x: '-100%' }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: '-100%' }}
|
||||
transition={{ type: 'spring', stiffness: 320, damping: 32 }}
|
||||
>
|
||||
<button
|
||||
onClick={onMobileClose}
|
||||
className="absolute top-3 right-3 w-8 h-8 grid place-items-center rounded-md hover:bg-defeat/80 hover:text-white text-muted transition-colors"
|
||||
aria-label="Закрыть меню"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
<SidebarContent compact={false} onNav={onMobileClose} />
|
||||
</motion.aside>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarContent({
|
||||
compact,
|
||||
onNav
|
||||
}: {
|
||||
compact: boolean
|
||||
onNav?: () => void
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="absolute inset-0 dot-grid opacity-40 pointer-events-none" />
|
||||
<div className="relative px-5 py-5">
|
||||
|
||||
{/* Brand */}
|
||||
<div className="relative px-3 lg:px-5 py-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="relative shrink-0">
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-brand blur-md opacity-60" />
|
||||
<div className="relative w-11 h-11 rounded-2xl bg-gradient-brand grid place-items-center text-white shadow-glow">
|
||||
<Dumbbell size={20} strokeWidth={2.5} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={[
|
||||
'min-w-0',
|
||||
compact ? 'hidden lg:block' : 'block'
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="font-display font-bold text-lg leading-none uppercase tracking-wider">
|
||||
<span className="text-gradient-brand">Laude</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted uppercase tracking-[0.18em] mt-1">
|
||||
Play hard · Train harder
|
||||
Move every day
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative px-5 pb-3">
|
||||
<div className="relative px-3 lg:px-5 pb-3">
|
||||
<div className="h-px bg-gradient-to-r from-transparent via-border to-transparent" />
|
||||
</div>
|
||||
|
||||
<nav className="relative px-3 flex flex-col gap-0.5">
|
||||
{/* Nav */}
|
||||
<nav className="relative px-2 lg:px-3 flex flex-col gap-0.5">
|
||||
{links.map(({ to, label, icon: Icon, end }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={end}
|
||||
onClick={onNav}
|
||||
title={compact ? label : undefined}
|
||||
className={({ isActive }) =>
|
||||
[
|
||||
'group relative flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all',
|
||||
'group relative flex items-center gap-3 px-2.5 lg:px-3 py-2.5 rounded-xl text-sm font-medium transition-all',
|
||||
compact ? 'justify-center lg:justify-start' : '',
|
||||
isActive
|
||||
? 'text-text bg-surface-elevated/80'
|
||||
: 'text-muted hover:text-text hover:bg-surface-elevated/50'
|
||||
@@ -73,14 +144,27 @@ export function Sidebar(): JSX.Element {
|
||||
className={isActive ? 'text-accent' : ''}
|
||||
strokeWidth={isActive ? 2.4 : 2}
|
||||
/>
|
||||
<span className={isActive ? 'font-semibold' : ''}>{label}</span>
|
||||
<span
|
||||
className={[
|
||||
compact ? 'hidden lg:inline' : 'inline',
|
||||
isActive ? 'font-semibold' : ''
|
||||
].join(' ')}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="relative mt-auto p-4">
|
||||
{/* Status footer — hidden on icon-only desktop */}
|
||||
<div
|
||||
className={[
|
||||
'relative mt-auto p-4',
|
||||
compact ? 'hidden lg:block' : 'block'
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="rounded-xl border border-border/60 bg-surface-elevated/60 p-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="relative flex h-2 w-2">
|
||||
@@ -88,14 +172,14 @@ export function Sidebar(): JSX.Element {
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-victory" />
|
||||
</span>
|
||||
<div className="text-[10px] uppercase tracking-[0.18em] text-muted">
|
||||
Online · v0.1
|
||||
Online
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted/80 leading-snug">
|
||||
GSI-трекинг матчей активен
|
||||
Трекинг активности включён
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import { Minus, X, Square, Activity } from 'lucide-react'
|
||||
import { Minus, X, Square, Activity, Menu } from 'lucide-react'
|
||||
|
||||
export function Titlebar({ title }: { title: string }): JSX.Element {
|
||||
type Props = {
|
||||
title: string
|
||||
onMenuClick?: () => void
|
||||
}
|
||||
|
||||
export function Titlebar({ title, onMenuClick }: Props): JSX.Element {
|
||||
return (
|
||||
<div className="titlebar-drag relative h-10 px-4 flex items-center justify-between border-b border-border/60 bg-surface/50 backdrop-blur-md hud-scanlines">
|
||||
<div className="titlebar-drag relative h-10 px-2 sm:px-4 flex items-center justify-between border-b border-border/60 bg-surface/50 backdrop-blur-md hud-scanlines">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Mobile menu — only visible on <md (where sidebar is hidden) */}
|
||||
{onMenuClick && (
|
||||
<button
|
||||
onClick={onMenuClick}
|
||||
className="titlebar-nodrag md:hidden w-8 h-7 grid place-items-center rounded-md hover:bg-surface-elevated text-muted hover:text-text transition-colors"
|
||||
aria-label="Открыть меню"
|
||||
>
|
||||
<Menu size={15} />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs font-medium">
|
||||
<div className="relative">
|
||||
<span className="absolute inset-0 rounded-full bg-accent blur-[6px] opacity-70" />
|
||||
@@ -12,10 +28,11 @@ export function Titlebar({ title }: { title: string }): JSX.Element {
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</div>
|
||||
<span className="uppercase tracking-[0.18em] text-muted font-display font-semibold">
|
||||
<span className="hidden sm:inline uppercase tracking-[0.18em] text-muted font-display font-semibold">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="titlebar-nodrag flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => window.api.minimizeMain()}
|
||||
|
||||
@@ -46,13 +46,13 @@ export default function ChallengesPage(): JSX.Element {
|
||||
const activeCount = challenges.filter((c) => c.enabled).length
|
||||
|
||||
return (
|
||||
<div className="p-8 overflow-y-auto h-full max-w-3xl">
|
||||
<div className="flex items-end justify-between mb-6">
|
||||
<div>
|
||||
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full max-w-3xl">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
|
||||
Match rules
|
||||
Правила за матч
|
||||
</div>
|
||||
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
|
||||
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
|
||||
<span className="text-gradient-brand">Челленджи</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
@@ -68,6 +68,7 @@ export default function ChallengesPage(): JSX.Element {
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="self-start sm:self-auto flex-shrink-0"
|
||||
onClick={() => {
|
||||
setEditing(null)
|
||||
setEditorOpen(true)
|
||||
@@ -127,7 +128,7 @@ export default function ChallengesPage(): JSX.Element {
|
||||
checked={c.enabled}
|
||||
onChange={(v) => window.api.toggleChallenge(c.id, v)}
|
||||
/>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex items-center gap-1 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditing(c)
|
||||
|
||||
@@ -86,14 +86,14 @@ export default function Dashboard(): JSX.Element {
|
||||
const paused = !settings?.globalEnabled
|
||||
|
||||
return (
|
||||
<div className="p-8 overflow-y-auto h-full">
|
||||
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full">
|
||||
{/* Hero header */}
|
||||
<div className="flex items-end justify-between mb-6">
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
|
||||
Mission control
|
||||
Тренировка дня
|
||||
</div>
|
||||
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
|
||||
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
|
||||
<span className="text-gradient-brand">Дашборд</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
@@ -104,7 +104,7 @@ export default function Dashboard(): JSX.Element {
|
||||
повторов за цикл
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Button variant="secondary" onClick={togglePause}>
|
||||
{!paused ? (
|
||||
<>
|
||||
@@ -123,15 +123,15 @@ export default function Dashboard(): JSX.Element {
|
||||
</div>
|
||||
|
||||
{/* HUD stat strip */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3 mb-6">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
|
||||
<HudStat
|
||||
icon={<Timer size={18} />}
|
||||
label="Cooldown"
|
||||
label="До следующего"
|
||||
value={
|
||||
stats.nextMs === Infinity
|
||||
? '—'
|
||||
: stats.nextMs <= 0
|
||||
? 'READY'
|
||||
? 'СЕЙЧАС'
|
||||
: formatCountdown(stats.nextMs)
|
||||
}
|
||||
accent={stats.nextMs <= 0 && stats.nextMs !== Infinity}
|
||||
@@ -148,7 +148,7 @@ export default function Dashboard(): JSX.Element {
|
||||
/>
|
||||
<HudStat
|
||||
icon={<Gamepad2 size={18} />}
|
||||
label="Game tracking"
|
||||
label="Трекинг матчей"
|
||||
value={gamesEnabled ? 'LIVE' : 'OFF'}
|
||||
accent={gamesEnabled}
|
||||
tone={gamesEnabled ? 'victory' : 'muted'}
|
||||
@@ -187,7 +187,7 @@ export default function Dashboard(): JSX.Element {
|
||||
{challenges.length} правил привязано к матчам
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted">
|
||||
<div className="hidden sm:block text-xs text-muted">
|
||||
См. вкладку <span className="text-accent font-semibold">Челленджи</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,13 +20,13 @@ export default function Exercises(): JSX.Element {
|
||||
.reduce((s, e) => s + e.reps, 0)
|
||||
|
||||
return (
|
||||
<div className="p-8 overflow-y-auto h-full">
|
||||
<div className="flex items-end justify-between mb-6">
|
||||
<div>
|
||||
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
|
||||
Loadout
|
||||
Программа
|
||||
</div>
|
||||
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
|
||||
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
|
||||
<span className="text-gradient-brand">Упражнения</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
@@ -41,6 +41,7 @@ export default function Exercises(): JSX.Element {
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="self-start sm:self-auto flex-shrink-0"
|
||||
onClick={() => {
|
||||
setEditing(null)
|
||||
setEditorOpen(true)
|
||||
@@ -99,7 +100,7 @@ export default function Exercises(): JSX.Element {
|
||||
checked={ex.enabled}
|
||||
onChange={(v) => window.api.toggleExercise(ex.id, v)}
|
||||
/>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex items-center gap-1 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditing(ex)
|
||||
|
||||
@@ -58,13 +58,13 @@ export default function GamesPage(): JSX.Element {
|
||||
const liveCount = games.filter((g) => g.enabled && g.integrationActive).length
|
||||
|
||||
return (
|
||||
<div className="p-8 overflow-y-auto h-full max-w-3xl">
|
||||
<div className="flex items-end justify-between mb-6">
|
||||
<div>
|
||||
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full max-w-3xl">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
|
||||
Game integrations
|
||||
Трекинг матчей
|
||||
</div>
|
||||
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
|
||||
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
|
||||
<span className="text-gradient-brand">Игры</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
@@ -79,7 +79,11 @@ export default function GamesPage(): JSX.Element {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="secondary" onClick={refresh}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="self-start sm:self-auto flex-shrink-0"
|
||||
onClick={refresh}
|
||||
>
|
||||
<RefreshCw size={16} /> Обновить
|
||||
</Button>
|
||||
</div>
|
||||
@@ -224,7 +228,7 @@ function GameRow({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative flex items-center gap-2 mt-4">
|
||||
<div className="relative flex items-center flex-wrap gap-2 mt-4">
|
||||
{game.installed && !game.integrationActive && (
|
||||
<Button onClick={onInstall} disabled={busy}>
|
||||
<Download size={16} /> Установить интеграцию
|
||||
|
||||
@@ -18,12 +18,12 @@ export default function SettingsPage(): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 overflow-y-auto h-full max-w-2xl">
|
||||
<div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full max-w-2xl">
|
||||
<div className="mb-8">
|
||||
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
|
||||
Config
|
||||
Конфигурация
|
||||
</div>
|
||||
<h1 className="font-display font-bold text-4xl leading-none uppercase tracking-wide">
|
||||
<h1 className="font-display font-bold text-3xl sm:text-4xl leading-none uppercase tracking-wide">
|
||||
<span className="text-gradient-brand">Настройки</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
@@ -89,7 +89,7 @@ export default function SettingsPage(): JSX.Element {
|
||||
<Section title="Внешний вид" icon={<Palette size={14} />}>
|
||||
<SelectRow
|
||||
label="Тема"
|
||||
hint="Тёмная — родная esports-эстетика приложения"
|
||||
hint="Тёмная подходит к спортивной эстетике приложения"
|
||||
value={settings.theme}
|
||||
onChange={(v) => patch({ theme: v as Theme })}
|
||||
options={[
|
||||
|
||||
@@ -3,36 +3,36 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
/* Brand neon palette — overridden at runtime if user picks OS accent */
|
||||
--accent: 34 211 238; /* cyan-400 — primary energy */
|
||||
--accent-soft: 34 211 238;
|
||||
--accent-2: 168 85 247; /* violet-500 — gradient pair */
|
||||
--victory: 132 204 22; /* lime-500 — sport / done */
|
||||
--defeat: 244 63 94; /* rose-500 — danger */
|
||||
--xp: 250 204 21; /* amber-400 — streak */
|
||||
/* Sport palette — Strava-inspired energy orange + intense rose pair */
|
||||
--accent: 249 115 22; /* orange-500 — primary energy / "GO" */
|
||||
--accent-soft: 249 115 22;
|
||||
--accent-2: 244 63 94; /* rose-500 — gradient pair, intensity */
|
||||
--victory: 132 204 22; /* lime-500 — done / personal best */
|
||||
--defeat: 220 38 38; /* red-600 — danger */
|
||||
--xp: 245 158 11; /* amber-500 — streak / XP */
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* Light theme — kept clean and modern, sport vibe */
|
||||
/* Light theme — clean athletic paper feel */
|
||||
:root {
|
||||
--bg: 244 246 252;
|
||||
--bg-deep: 230 234 244;
|
||||
--bg: 250 250 251;
|
||||
--bg-deep: 240 240 244;
|
||||
--surface: 255 255 255;
|
||||
--surface-elevated: 248 250 254;
|
||||
--border: 224 228 240;
|
||||
--text: 13 18 32;
|
||||
--muted: 102 112 134;
|
||||
--surface-elevated: 248 248 250;
|
||||
--border: 226 228 234;
|
||||
--text: 17 18 24;
|
||||
--muted: 102 105 120;
|
||||
}
|
||||
|
||||
/* Dark theme — esports HUD vibe (default for gamers) */
|
||||
/* Dark theme — warm graphite (not cool navy) */
|
||||
.dark {
|
||||
--bg: 8 11 20;
|
||||
--bg-deep: 4 6 12;
|
||||
--surface: 16 20 33;
|
||||
--surface-elevated: 22 27 44;
|
||||
--border: 38 46 70;
|
||||
--text: 232 237 250;
|
||||
--muted: 138 150 178;
|
||||
--bg: 13 14 18;
|
||||
--bg-deep: 7 8 11;
|
||||
--surface: 22 24 30;
|
||||
--surface-elevated: 30 32 40;
|
||||
--border: 50 53 64;
|
||||
--text: 236 238 244;
|
||||
--muted: 148 152 165;
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
Reference in New Issue
Block a user