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 { 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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} /> Готово
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Позже'
|
'Позже'
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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} /> Установить интеграцию
|
||||||
|
|||||||
@@ -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={[
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user