redesign(ui): phase 1 — esports HUD design system + core surfaces
Сменили визуальную ДНК на dark-first гейминг-эстетику: cyan→violet
неоновая палитра, спортивный display-шрифт Rajdhani, моноширинный
HUD для всех счётчиков, градиентные CTA с glow.
Дизайн-система (globals.css + tailwind.config.js):
- Brand-токены accent (cyan), accent-2 (violet), victory (lime),
defeat (rose), xp (amber); --bg-deep слой
- Утилиты .neon-border (анимированный обводный градиент),
.hud-pulse, .hud-scanlines, .dot-grid, .text-gradient-brand,
.bg-gradient-brand/-victory/-defeat
- Радиальные градиенты на body (cyan/violet glow по углам)
- Шрифты Rajdhani (display) и JetBrains Mono подключены через CDN
Компоненты:
- Sidebar: gradient-логотип, активный пункт с вертикальной gradient-
полосой и shadow-glow, статус-чип GSI tracking
- Titlebar: glass + scanlines, моноширинный лейбл, defeat hover на X
- Button: primary = bg-gradient-brand + shadow-glow; новый variant
victory (lime-gradient)
- ExerciseCard: SVG cooldown-ring как у способностей в MOBA,
градиентный stroke с drop-shadow, .neon-border на due, hover lift
- Dashboard: hero с gradient-text заголовком, HUD-полоса из 4 stat-
карточек (Cooldown / Active / Avg / Game tracking LIVE/OFF)
- ReminderApp: вращающийся conic-gradient вокруг иконки, HUD-блок
reps в моноширинном шрифте, кнопки с подписями хоткеев,
Match summary с heroGradient по результату (victory/defeat/brand)
ThemeProvider больше не перезаписывает --accent системным —
у laude теперь стабильная brand-идентичность.
Бонус: хоткеи на reminder-окне (Enter=done, Space=snooze, Esc=skip).
Откат: git revert HEAD или git reset --hard 688a86b
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,21 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { AnimatePresence } from 'framer-motion'
|
||||
import { Plus, Pause, Play, Clock } from 'lucide-react'
|
||||
import {
|
||||
Plus,
|
||||
Pause,
|
||||
Play,
|
||||
Timer,
|
||||
Flame,
|
||||
Activity,
|
||||
Gamepad2,
|
||||
Trophy
|
||||
} from 'lucide-react'
|
||||
import { useAppStore } from '../store/appStore'
|
||||
import { ExerciseCard } from '../components/ExerciseCard'
|
||||
import { ExerciseEditor } from '../components/ExerciseEditor'
|
||||
import { Button } from '../components/ui/Button'
|
||||
import type { Exercise } from '@shared/types'
|
||||
import { formatCountdown } from '../lib/format'
|
||||
import { formatCountdown, formatInterval } from '../lib/format'
|
||||
|
||||
export default function Dashboard(): JSX.Element {
|
||||
const state = useAppStore((s) => s.state)
|
||||
@@ -16,16 +25,27 @@ export default function Dashboard(): JSX.Element {
|
||||
|
||||
const exercises = state?.exercises ?? []
|
||||
const settings = state?.settings
|
||||
const challenges = state?.challenges ?? []
|
||||
const gamesEnabled = Object.values(state?.gamesEnabled ?? {}).some(Boolean)
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const enabled = exercises.filter((e) => e.enabled)
|
||||
const next = enabled
|
||||
.map((e) => ({ id: e.id, ms: e.nextFireAt - Date.now() }))
|
||||
.sort((a, b) => a.ms - b.ms)[0]
|
||||
const totalReps = enabled.reduce((s, e) => s + e.reps, 0)
|
||||
const avgInterval =
|
||||
enabled.length > 0
|
||||
? Math.round(
|
||||
enabled.reduce((s, e) => s + e.intervalMinutes, 0) / enabled.length
|
||||
)
|
||||
: 0
|
||||
return {
|
||||
total: exercises.length,
|
||||
active: enabled.length,
|
||||
nextMs: next?.ms ?? Infinity
|
||||
nextMs: next?.ms ?? Infinity,
|
||||
totalReps,
|
||||
avgInterval
|
||||
}
|
||||
}, [exercises, ticks])
|
||||
|
||||
@@ -63,18 +83,30 @@ export default function Dashboard(): JSX.Element {
|
||||
await window.api.updateSettings({ globalEnabled: !settings.globalEnabled })
|
||||
}
|
||||
|
||||
const paused = !settings?.globalEnabled
|
||||
|
||||
return (
|
||||
<div className="p-8 overflow-y-auto h-full">
|
||||
{/* Hero header */}
|
||||
<div className="flex items-end justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Дашборд</h1>
|
||||
<p className="text-sm text-muted mt-1">
|
||||
{stats.active} активных из {stats.total} упражнений
|
||||
<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">
|
||||
<span className="text-gradient-brand">Дашборд</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
{stats.active} активных из {stats.total} упражнений ·{' '}
|
||||
<span className="text-text font-mono-num font-semibold">
|
||||
{stats.totalReps}
|
||||
</span>{' '}
|
||||
повторов за цикл
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" onClick={togglePause}>
|
||||
{settings?.globalEnabled ? (
|
||||
{!paused ? (
|
||||
<>
|
||||
<Pause size={16} /> Пауза
|
||||
</>
|
||||
@@ -90,27 +122,78 @@ export default function Dashboard(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 rounded-2xl border border-border bg-surface px-5 py-4 flex items-center gap-4">
|
||||
<div className="w-11 h-11 rounded-xl bg-accent/15 text-accent grid place-items-center">
|
||||
<Clock size={20} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-muted uppercase tracking-wider">
|
||||
Ближайшее напоминание
|
||||
</div>
|
||||
<div className="text-lg font-semibold mt-0.5">
|
||||
{stats.nextMs === Infinity
|
||||
? 'Нет активных упражнений'
|
||||
: `через ${formatCountdown(stats.nextMs)}`}
|
||||
</div>
|
||||
</div>
|
||||
{!settings?.globalEnabled && (
|
||||
<div className="px-3 py-1.5 rounded-full bg-amber-500/15 text-amber-500 text-xs font-medium">
|
||||
Напоминания на паузе
|
||||
</div>
|
||||
)}
|
||||
{/* HUD stat strip */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3 mb-6">
|
||||
<HudStat
|
||||
icon={<Timer size={18} />}
|
||||
label="Cooldown"
|
||||
value={
|
||||
stats.nextMs === Infinity
|
||||
? '—'
|
||||
: stats.nextMs <= 0
|
||||
? 'READY'
|
||||
: formatCountdown(stats.nextMs)
|
||||
}
|
||||
accent={stats.nextMs <= 0 && stats.nextMs !== Infinity}
|
||||
/>
|
||||
<HudStat
|
||||
icon={<Activity size={18} />}
|
||||
label="Активных"
|
||||
value={`${stats.active}/${stats.total}`}
|
||||
/>
|
||||
<HudStat
|
||||
icon={<Flame size={18} />}
|
||||
label="Avg интервал"
|
||||
value={stats.avgInterval ? formatInterval(stats.avgInterval) : '—'}
|
||||
/>
|
||||
<HudStat
|
||||
icon={<Gamepad2 size={18} />}
|
||||
label="Game tracking"
|
||||
value={gamesEnabled ? 'LIVE' : 'OFF'}
|
||||
accent={gamesEnabled}
|
||||
tone={gamesEnabled ? 'victory' : 'muted'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Paused banner */}
|
||||
{paused && (
|
||||
<div className="mb-6 rounded-2xl border border-xp/30 bg-xp/10 px-5 py-3 flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-xp/20 text-xp grid place-items-center">
|
||||
<Pause size={16} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-sm">Тренировка на паузе</div>
|
||||
<div className="text-xs text-muted mt-0.5">
|
||||
Напоминания не сработают, пока не возобновишь
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="victory" size="sm" onClick={togglePause}>
|
||||
<Play size={14} /> GO
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Challenges shortcut */}
|
||||
{challenges.length > 0 && (
|
||||
<div className="mb-6 rounded-2xl border border-border/70 bg-surface/60 backdrop-blur-sm px-5 py-4 flex items-center gap-4">
|
||||
<div className="w-11 h-11 rounded-xl bg-accent-2/15 text-accent-2 grid place-items-center">
|
||||
<Trophy size={20} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-[10px] text-muted uppercase tracking-[0.18em] font-semibold">
|
||||
Активные челленджи
|
||||
</div>
|
||||
<div className="text-base font-display font-semibold mt-0.5">
|
||||
{challenges.length} правил привязано к матчам
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted">
|
||||
См. вкладку <span className="text-accent font-semibold">Челленджи</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Exercise grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<AnimatePresence>
|
||||
{exercises.map((ex) => (
|
||||
@@ -128,8 +211,16 @@ export default function Dashboard(): JSX.Element {
|
||||
</div>
|
||||
|
||||
{exercises.length === 0 && (
|
||||
<div className="mt-10 text-center text-muted">
|
||||
<p>Нет упражнений. Добавьте первое.</p>
|
||||
<div className="mt-12 text-center">
|
||||
<div className="inline-flex w-16 h-16 rounded-2xl bg-gradient-brand items-center justify-center text-white shadow-glow mb-4">
|
||||
<Plus size={28} />
|
||||
</div>
|
||||
<div className="font-display text-xl font-semibold uppercase tracking-wider mb-1">
|
||||
Старт пуст
|
||||
</div>
|
||||
<p className="text-sm text-muted">
|
||||
Добавь первое упражнение — и поехали
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -142,3 +233,54 @@ export default function Dashboard(): JSX.Element {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HudStat({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
accent,
|
||||
tone = 'accent'
|
||||
}: {
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
value: string
|
||||
accent?: boolean
|
||||
tone?: 'accent' | 'victory' | 'muted'
|
||||
}): JSX.Element {
|
||||
const toneClasses =
|
||||
tone === 'victory'
|
||||
? 'text-victory bg-victory/15'
|
||||
: tone === 'muted'
|
||||
? 'text-muted bg-surface-elevated'
|
||||
: 'text-accent bg-accent/15'
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'relative rounded-2xl border bg-surface/60 backdrop-blur-sm px-4 py-3 overflow-hidden',
|
||||
accent ? 'border-accent/40 shadow-glow' : 'border-border/70'
|
||||
].join(' ')}
|
||||
>
|
||||
{accent && (
|
||||
<div className="absolute -top-8 -right-8 w-24 h-24 rounded-full bg-accent/20 blur-2xl pointer-events-none" />
|
||||
)}
|
||||
<div className="relative flex items-center gap-3">
|
||||
<div
|
||||
className={[
|
||||
'w-10 h-10 rounded-xl grid place-items-center shrink-0',
|
||||
toneClasses
|
||||
].join(' ')}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[10px] uppercase tracking-[0.18em] text-muted font-semibold">
|
||||
{label}
|
||||
</div>
|
||||
<div className="font-display font-bold text-xl tracking-wide truncate">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user