redesign(ui): спортивная палитра Strava + полная адаптивность
Some checks failed
CI / Typecheck + Tests (push) Has been cancelled
CI / Build (Windows) (push) Has been cancelled

== Цветовая гамма ==
Сменили 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:
AnRil
2026-05-17 12:50:20 +07:00
parent 92e15e69a3
commit cec146ae3a
11 changed files with 235 additions and 109 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom' import { HashRouter, Route, Routes, useLocation } from 'react-router-dom'
import { Sidebar } from './components/Sidebar' import { Sidebar } from './components/Sidebar'
import { Titlebar } from './components/Titlebar' import { Titlebar } from './components/Titlebar'
import Dashboard from './pages/Dashboard' import Dashboard from './pages/Dashboard'
@@ -11,6 +11,7 @@ import { subscribeToBackend, useAppStore } from './store/appStore'
export default function App(): JSX.Element { export default function App(): JSX.Element {
const hydrated = useAppStore((s) => s.hydrated) const hydrated = useAppStore((s) => s.hydrated)
const [mobileNavOpen, setMobileNavOpen] = useState(false)
useEffect(() => { useEffect(() => {
const unsub = subscribeToBackend() const unsub = subscribeToBackend()
@@ -20,18 +21,18 @@ export default function App(): JSX.Element {
return ( return (
<HashRouter> <HashRouter>
<div className="h-screen w-screen flex flex-col bg-bg"> <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"> <div className="flex-1 flex overflow-hidden">
<Sidebar /> <Sidebar
<main className="flex-1 overflow-hidden"> mobileOpen={mobileNavOpen}
onMobileClose={() => setMobileNavOpen(false)}
/>
<main className="flex-1 overflow-hidden min-w-0">
{hydrated ? ( {hydrated ? (
<Routes> <RoutesWithCloseOnNav onClose={() => setMobileNavOpen(false)} />
<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>
) : ( ) : (
<div className="p-8 text-muted">Загрузка</div> <div className="p-8 text-muted">Загрузка</div>
)} )}
@@ -41,3 +42,21 @@ export default function App(): JSX.Element {
</HashRouter> </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>
)
}

View File

@@ -123,7 +123,7 @@ function ExerciseReminder({
<div className="reminder-shell flex flex-col h-full hud-scanlines"> <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="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"> <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> </div>
<button <button
onClick={onClose} onClick={onClose}
@@ -158,7 +158,7 @@ function ExerciseReminder({
</motion.div> </motion.div>
<div className="text-[10px] uppercase tracking-[0.28em] text-muted font-semibold"> <div className="text-[10px] uppercase tracking-[0.28em] text-muted font-semibold">
Время размяться Двигайся
</div> </div>
<h1 className="font-display text-3xl font-bold mt-2 mb-3 uppercase tracking-wide"> <h1 className="font-display text-3xl font-bold mt-2 mb-3 uppercase tracking-wide">
{exercise.name} {exercise.name}
@@ -176,7 +176,7 @@ function ExerciseReminder({
</div> </div>
<div className="text-[11px] text-muted mt-4 inline-flex items-center gap-1.5"> <div className="text-[11px] text-muted mt-4 inline-flex items-center gap-1.5">
<Clock size={10} /> <Clock size={10} />
Next drop через {formatInterval(exercise.intervalMinutes)} Следующее через {formatInterval(exercise.intervalMinutes)}
</div> </div>
</div> </div>
@@ -280,9 +280,9 @@ function MatchSummaryView({
</motion.div> </motion.div>
<h1 className="font-display font-bold text-xl uppercase tracking-wide"> <h1 className="font-display font-bold text-xl uppercase tracking-wide">
{won {won
? 'Victory · упражнения заработаны' ? 'Победа · тренировка заработана'
: lost : lost
? 'Defeat · но тело — нет' ? 'Проигрыш · но тело не сдаётся'
: 'Матч завершён'} : 'Матч завершён'}
</h1> </h1>
<p className="text-[11px] text-muted mt-1.5"> <p className="text-[11px] text-muted mt-1.5">
@@ -293,7 +293,7 @@ function MatchSummaryView({
челлендж{summary.results.length === 1 ? '' : 'а'} ·{' '} челлендж{summary.results.length === 1 ? '' : 'а'} ·{' '}
{allDone ? ( {allDone ? (
<span className="text-victory font-semibold uppercase tracking-wider"> <span className="text-victory font-semibold uppercase tracking-wider">
all clear готово
</span> </span>
) : ( ) : (
<span className="text-accent font-bold font-mono-num"> <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="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"> <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"> <span className="text-gradient-brand font-mono-num text-base font-bold tracking-normal">
{totalReps} {totalReps}
</span>{' '} </span>{' '}
reps повторов
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
@@ -331,7 +331,7 @@ function MatchSummaryView({
> >
{allDone ? ( {allDone ? (
<> <>
<Check size={16} /> GG <Check size={16} /> Готово
</> </>
) : ( ) : (
'Позже' 'Позже'

View File

@@ -130,7 +130,7 @@ export function ExerciseCard({
<div className="relative"> <div className="relative">
<div className="flex items-baseline justify-between mb-1.5"> <div className="flex items-baseline justify-between mb-1.5">
<span className="text-[10px] uppercase tracking-[0.18em] text-muted font-semibold"> <span className="text-[10px] uppercase tracking-[0.18em] text-muted font-semibold">
Cooldown Через
</span> </span>
<span <span
className={[ className={[
@@ -138,7 +138,7 @@ export function ExerciseCard({
isDue ? 'text-accent' : 'text-text' isDue ? 'text-accent' : 'text-text'
].join(' ')} ].join(' ')}
> >
{exercise.enabled ? formatCountdown(ms) : 'PAUSED'} {exercise.enabled ? formatCountdown(ms) : 'пауза'}
</span> </span>
</div> </div>
<div className="h-1.5 rounded-full bg-surface-elevated/80 overflow-hidden"> <div className="h-1.5 rounded-full bg-surface-elevated/80 overflow-hidden">
@@ -153,7 +153,7 @@ export function ExerciseCard({
</div> </div>
</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 <button
onClick={onMarkDone} 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" 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"

View File

@@ -1,11 +1,13 @@
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { AnimatePresence, motion } from 'framer-motion'
import { import {
LayoutDashboard, LayoutDashboard,
ListChecks, ListChecks,
Gamepad2, Gamepad2,
Target, Target,
Settings as SettingsIcon, Settings as SettingsIcon,
Dumbbell Dumbbell,
X
} from 'lucide-react' } from 'lucide-react'
const links = [ const links = [
@@ -16,42 +18,111 @@ const links = [
{ to: '/settings', label: 'Настройки', icon: SettingsIcon } { 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 ( 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="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="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="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"> <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} /> <Dumbbell size={20} strokeWidth={2.5} />
</div> </div>
</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"> <div className="font-display font-bold text-lg leading-none uppercase tracking-wider">
<span className="text-gradient-brand">Laude</span> <span className="text-gradient-brand">Laude</span>
</div> </div>
<div className="text-[10px] text-muted uppercase tracking-[0.18em] mt-1"> <div className="text-[10px] text-muted uppercase tracking-[0.18em] mt-1">
Play hard · Train harder Move every day
</div> </div>
</div> </div>
</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 className="h-px bg-gradient-to-r from-transparent via-border to-transparent" />
</div> </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 }) => ( {links.map(({ to, label, icon: Icon, end }) => (
<NavLink <NavLink
key={to} key={to}
to={to} to={to}
end={end} end={end}
onClick={onNav}
title={compact ? label : undefined}
className={({ isActive }) => 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 isActive
? 'text-text bg-surface-elevated/80' ? 'text-text bg-surface-elevated/80'
: 'text-muted hover:text-text hover:bg-surface-elevated/50' : 'text-muted hover:text-text hover:bg-surface-elevated/50'
@@ -73,14 +144,27 @@ export function Sidebar(): JSX.Element {
className={isActive ? 'text-accent' : ''} className={isActive ? 'text-accent' : ''}
strokeWidth={isActive ? 2.4 : 2} 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> </NavLink>
))} ))}
</nav> </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="rounded-xl border border-border/60 bg-surface-elevated/60 p-3">
<div className="flex items-center gap-2 mb-1.5"> <div className="flex items-center gap-2 mb-1.5">
<span className="relative flex h-2 w-2"> <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 className="relative inline-flex rounded-full h-2 w-2 bg-victory" />
</span> </span>
<div className="text-[10px] uppercase tracking-[0.18em] text-muted"> <div className="text-[10px] uppercase tracking-[0.18em] text-muted">
Online · v0.1 Online
</div> </div>
</div> </div>
<div className="text-xs text-muted/80 leading-snug"> <div className="text-xs text-muted/80 leading-snug">
GSI-трекинг матчей активен Трекинг активности включён
</div> </div>
</div> </div>
</div> </div>
</aside> </>
) )
} }

View File

@@ -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 ( 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="flex items-center gap-2 text-xs font-medium">
<div className="relative"> <div className="relative">
<span className="absolute inset-0 rounded-full bg-accent blur-[6px] opacity-70" /> <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} strokeWidth={2.5}
/> />
</div> </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} {title}
</span> </span>
</div> </div>
</div>
<div className="titlebar-nodrag flex items-center gap-1"> <div className="titlebar-nodrag flex items-center gap-1">
<button <button
onClick={() => window.api.minimizeMain()} onClick={() => window.api.minimizeMain()}

View File

@@ -46,13 +46,13 @@ export default function ChallengesPage(): JSX.Element {
const activeCount = challenges.filter((c) => c.enabled).length const activeCount = challenges.filter((c) => c.enabled).length
return ( return (
<div className="p-8 overflow-y-auto h-full max-w-3xl"> <div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full max-w-3xl">
<div className="flex items-end justify-between mb-6"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
<div> <div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2"> <div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Match rules Правила за матч
</div> </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> <span className="text-gradient-brand">Челленджи</span>
</h1> </h1>
<p className="text-sm text-muted mt-2"> <p className="text-sm text-muted mt-2">
@@ -68,6 +68,7 @@ export default function ChallengesPage(): JSX.Element {
</p> </p>
</div> </div>
<Button <Button
className="self-start sm:self-auto flex-shrink-0"
onClick={() => { onClick={() => {
setEditing(null) setEditing(null)
setEditorOpen(true) setEditorOpen(true)
@@ -127,7 +128,7 @@ export default function ChallengesPage(): JSX.Element {
checked={c.enabled} checked={c.enabled}
onChange={(v) => window.api.toggleChallenge(c.id, v)} 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 <button
onClick={() => { onClick={() => {
setEditing(c) setEditing(c)

View File

@@ -86,14 +86,14 @@ export default function Dashboard(): JSX.Element {
const paused = !settings?.globalEnabled const paused = !settings?.globalEnabled
return ( 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 */} {/* Hero header */}
<div className="flex items-end justify-between mb-6"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
<div> <div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2"> <div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Mission control Тренировка дня
</div> </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> <span className="text-gradient-brand">Дашборд</span>
</h1> </h1>
<p className="text-sm text-muted mt-2"> <p className="text-sm text-muted mt-2">
@@ -104,7 +104,7 @@ export default function Dashboard(): JSX.Element {
повторов за цикл повторов за цикл
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-shrink-0">
<Button variant="secondary" onClick={togglePause}> <Button variant="secondary" onClick={togglePause}>
{!paused ? ( {!paused ? (
<> <>
@@ -123,15 +123,15 @@ export default function Dashboard(): JSX.Element {
</div> </div>
{/* HUD stat strip */} {/* 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 <HudStat
icon={<Timer size={18} />} icon={<Timer size={18} />}
label="Cooldown" label="До следующего"
value={ value={
stats.nextMs === Infinity stats.nextMs === Infinity
? '—' ? '—'
: stats.nextMs <= 0 : stats.nextMs <= 0
? 'READY' ? 'СЕЙЧАС'
: formatCountdown(stats.nextMs) : formatCountdown(stats.nextMs)
} }
accent={stats.nextMs <= 0 && stats.nextMs !== Infinity} accent={stats.nextMs <= 0 && stats.nextMs !== Infinity}
@@ -148,7 +148,7 @@ export default function Dashboard(): JSX.Element {
/> />
<HudStat <HudStat
icon={<Gamepad2 size={18} />} icon={<Gamepad2 size={18} />}
label="Game tracking" label="Трекинг матчей"
value={gamesEnabled ? 'LIVE' : 'OFF'} value={gamesEnabled ? 'LIVE' : 'OFF'}
accent={gamesEnabled} accent={gamesEnabled}
tone={gamesEnabled ? 'victory' : 'muted'} tone={gamesEnabled ? 'victory' : 'muted'}
@@ -187,7 +187,7 @@ export default function Dashboard(): JSX.Element {
{challenges.length} правил привязано к матчам {challenges.length} правил привязано к матчам
</div> </div>
</div> </div>
<div className="text-xs text-muted"> <div className="hidden sm:block text-xs text-muted">
См. вкладку <span className="text-accent font-semibold">Челленджи</span> См. вкладку <span className="text-accent font-semibold">Челленджи</span>
</div> </div>
</div> </div>

View File

@@ -20,13 +20,13 @@ export default function Exercises(): JSX.Element {
.reduce((s, e) => s + e.reps, 0) .reduce((s, e) => s + e.reps, 0)
return ( 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">
<div className="flex items-end justify-between mb-6"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
<div> <div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2"> <div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Loadout Программа
</div> </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> <span className="text-gradient-brand">Упражнения</span>
</h1> </h1>
<p className="text-sm text-muted mt-2"> <p className="text-sm text-muted mt-2">
@@ -41,6 +41,7 @@ export default function Exercises(): JSX.Element {
</p> </p>
</div> </div>
<Button <Button
className="self-start sm:self-auto flex-shrink-0"
onClick={() => { onClick={() => {
setEditing(null) setEditing(null)
setEditorOpen(true) setEditorOpen(true)
@@ -99,7 +100,7 @@ export default function Exercises(): JSX.Element {
checked={ex.enabled} checked={ex.enabled}
onChange={(v) => window.api.toggleExercise(ex.id, v)} 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 <button
onClick={() => { onClick={() => {
setEditing(ex) setEditing(ex)

View File

@@ -58,13 +58,13 @@ export default function GamesPage(): JSX.Element {
const liveCount = games.filter((g) => g.enabled && g.integrationActive).length const liveCount = games.filter((g) => g.enabled && g.integrationActive).length
return ( return (
<div className="p-8 overflow-y-auto h-full max-w-3xl"> <div className="p-4 sm:p-6 lg:p-8 overflow-y-auto h-full max-w-3xl">
<div className="flex items-end justify-between mb-6"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-6">
<div> <div className="min-w-0">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2"> <div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Game integrations Трекинг матчей
</div> </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> <span className="text-gradient-brand">Игры</span>
</h1> </h1>
<p className="text-sm text-muted mt-2"> <p className="text-sm text-muted mt-2">
@@ -79,7 +79,11 @@ export default function GamesPage(): JSX.Element {
)} )}
</p> </p>
</div> </div>
<Button variant="secondary" onClick={refresh}> <Button
variant="secondary"
className="self-start sm:self-auto flex-shrink-0"
onClick={refresh}
>
<RefreshCw size={16} /> Обновить <RefreshCw size={16} /> Обновить
</Button> </Button>
</div> </div>
@@ -224,7 +228,7 @@ function GameRow({
</div> </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 && ( {game.installed && !game.integrationActive && (
<Button onClick={onInstall} disabled={busy}> <Button onClick={onInstall} disabled={busy}>
<Download size={16} /> Установить интеграцию <Download size={16} /> Установить интеграцию

View File

@@ -18,12 +18,12 @@ export default function SettingsPage(): JSX.Element {
} }
return ( 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="mb-8">
<div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2"> <div className="text-[10px] uppercase tracking-[0.22em] text-muted font-semibold mb-2">
Config Конфигурация
</div> </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> <span className="text-gradient-brand">Настройки</span>
</h1> </h1>
<p className="text-sm text-muted mt-2"> <p className="text-sm text-muted mt-2">
@@ -89,7 +89,7 @@ export default function SettingsPage(): JSX.Element {
<Section title="Внешний вид" icon={<Palette size={14} />}> <Section title="Внешний вид" icon={<Palette size={14} />}>
<SelectRow <SelectRow
label="Тема" label="Тема"
hint="Тёмная — родная esports-эстетика приложения" hint="Тёмная подходит к спортивной эстетике приложения"
value={settings.theme} value={settings.theme}
onChange={(v) => patch({ theme: v as Theme })} onChange={(v) => patch({ theme: v as Theme })}
options={[ options={[

View File

@@ -3,36 +3,36 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
/* Brand neon palette — overridden at runtime if user picks OS accent */ /* Sport palette — Strava-inspired energy orange + intense rose pair */
--accent: 34 211 238; /* cyan-400 — primary energy */ --accent: 249 115 22; /* orange-500 — primary energy / "GO" */
--accent-soft: 34 211 238; --accent-soft: 249 115 22;
--accent-2: 168 85 247; /* violet-500 — gradient pair */ --accent-2: 244 63 94; /* rose-500 — gradient pair, intensity */
--victory: 132 204 22; /* lime-500 — sport / done */ --victory: 132 204 22; /* lime-500 done / personal best */
--defeat: 244 63 94; /* rose-500 — danger */ --defeat: 220 38 38; /* red-600 — danger */
--xp: 250 204 21; /* amber-400 — streak */ --xp: 245 158 11; /* amber-500 — streak / XP */
color-scheme: light dark; color-scheme: light dark;
} }
/* Light theme — kept clean and modern, sport vibe */ /* Light theme — clean athletic paper feel */
:root { :root {
--bg: 244 246 252; --bg: 250 250 251;
--bg-deep: 230 234 244; --bg-deep: 240 240 244;
--surface: 255 255 255; --surface: 255 255 255;
--surface-elevated: 248 250 254; --surface-elevated: 248 248 250;
--border: 224 228 240; --border: 226 228 234;
--text: 13 18 32; --text: 17 18 24;
--muted: 102 112 134; --muted: 102 105 120;
} }
/* Dark theme — esports HUD vibe (default for gamers) */ /* Dark theme — warm graphite (not cool navy) */
.dark { .dark {
--bg: 8 11 20; --bg: 13 14 18;
--bg-deep: 4 6 12; --bg-deep: 7 8 11;
--surface: 16 20 33; --surface: 22 24 30;
--surface-elevated: 22 27 44; --surface-elevated: 30 32 40;
--border: 38 46 70; --border: 50 53 64;
--text: 232 237 250; --text: 236 238 244;
--muted: 138 150 178; --muted: 148 152 165;
} }
html, html,