diff --git a/src/renderer/index.html b/src/renderer/index.html index 5ac4e88..86ca35a 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -7,7 +7,7 @@ Exercise Reminder - +
diff --git a/src/renderer/src/ReminderApp.tsx b/src/renderer/src/ReminderApp.tsx index 27c7180..24c90f0 100644 --- a/src/renderer/src/ReminderApp.tsx +++ b/src/renderer/src/ReminderApp.tsx @@ -1,6 +1,15 @@ import { useEffect, useRef, useState } from 'react' import { motion } from 'framer-motion' -import { Check, Clock, X, Trophy, Skull, Gamepad2 } from 'lucide-react' +import { + Check, + Clock, + X, + Trophy, + Skull, + Gamepad2, + Flame, + Zap +} from 'lucide-react' import type { Exercise, MatchSummary, Settings, ChallengeResult } from '@shared/types' import { Icon } from './lib/icon' import { formatInterval } from './lib/format' @@ -37,6 +46,26 @@ export default function ReminderApp(): JSX.Element { } }, []) + // Keyboard shortcuts on reminder window + useEffect(() => { + if (mode.kind !== 'exercise') return + const ex = mode.exercise + const snoozeMin = settings?.snoozeMinutes ?? 5 + function onKey(e: KeyboardEvent): void { + if (e.key === 'Enter') { + window.api.markDone(ex.id).then(close) + } else if (e.key === ' ' || e.code === 'Space') { + e.preventDefault() + window.api.snooze(ex.id, snoozeMin).then(close) + } else if (e.key === 'Escape') { + window.api.skip(ex.id).then(close) + } + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode, settings?.snoozeMinutes]) + function close(): void { setMode({ kind: 'idle' }) window.api.reminderClose() @@ -91,11 +120,14 @@ function ExerciseReminder({ } return ( -
-
+
+
+
+ Cooldown ready +
-
-
- + {/* Outer rotating ring */} + +
+
+
+
-
Время размяться
-

{exercise.name}

-
- {exercise.reps} - раз + +
+ Время размяться
-
- Следующее напоминание через {formatInterval(exercise.intervalMinutes)} +

+ {exercise.name} +

+ + {/* HUD reps counter */} +
+ + + {exercise.reps} + + + REPS + +
+
+ + Next drop через {formatInterval(exercise.intervalMinutes)}
+
@@ -163,16 +236,24 @@ function MatchSummaryView({ const remainingReps = summary.results .filter((r) => !done.has(r.challengeId)) .reduce((s, r) => s + r.reps, 0) + const won = summary.won === true + const lost = summary.won === false + + const heroGradient = won + ? 'bg-gradient-victory' + : lost + ? 'bg-gradient-defeat' + : 'bg-gradient-brand' return ( -
+
-
+
{summary.gameName}
-
-
- -
-
-
- Ближайшее напоминание -
-
- {stats.nextMs === Infinity - ? 'Нет активных упражнений' - : `через ${formatCountdown(stats.nextMs)}`} -
-
- {!settings?.globalEnabled && ( -
- Напоминания на паузе -
- )} + {/* HUD stat strip */} +
+ } + label="Cooldown" + value={ + stats.nextMs === Infinity + ? '—' + : stats.nextMs <= 0 + ? 'READY' + : formatCountdown(stats.nextMs) + } + accent={stats.nextMs <= 0 && stats.nextMs !== Infinity} + /> + } + label="Активных" + value={`${stats.active}/${stats.total}`} + /> + } + label="Avg интервал" + value={stats.avgInterval ? formatInterval(stats.avgInterval) : '—'} + /> + } + label="Game tracking" + value={gamesEnabled ? 'LIVE' : 'OFF'} + accent={gamesEnabled} + tone={gamesEnabled ? 'victory' : 'muted'} + />
+ {/* Paused banner */} + {paused && ( +
+
+ +
+
+
Тренировка на паузе
+
+ Напоминания не сработают, пока не возобновишь +
+
+ +
+ )} + + {/* Challenges shortcut */} + {challenges.length > 0 && ( +
+
+ +
+
+
+ Активные челленджи +
+
+ {challenges.length} правил привязано к матчам +
+
+
+ См. вкладку Челленджи +
+
+ )} + + {/* Exercise grid */}
{exercises.map((ex) => ( @@ -128,8 +211,16 @@ export default function Dashboard(): JSX.Element {
{exercises.length === 0 && ( -
-

Нет упражнений. Добавьте первое.

+
+
+ +
+
+ Старт пуст +
+

+ Добавь первое упражнение — и поехали +

)} @@ -142,3 +233,54 @@ export default function Dashboard(): JSX.Element {
) } + +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 ( +
+ {accent && ( +
+ )} +
+
+ {icon} +
+
+
+ {label} +
+
+ {value} +
+
+
+
+ ) +} diff --git a/src/renderer/src/providers/ThemeProvider.tsx b/src/renderer/src/providers/ThemeProvider.tsx index f055d4e..4b933e9 100644 --- a/src/renderer/src/providers/ThemeProvider.tsx +++ b/src/renderer/src/providers/ThemeProvider.tsx @@ -1,14 +1,6 @@ import { ReactNode, useEffect, useState } from 'react' import { useAppStore } from '../store/appStore' -function hexToRgbString(hex: string): string { - const cleaned = hex.replace('#', '').slice(0, 6).padEnd(6, '0') - const r = parseInt(cleaned.slice(0, 2), 16) - const g = parseInt(cleaned.slice(2, 4), 16) - const b = parseInt(cleaned.slice(4, 6), 16) - return `${r} ${g} ${b}` -} - export function ThemeProvider({ children }: { children: ReactNode }): JSX.Element { const settings = useAppStore((s) => s.state?.settings) const [osTheme, setOsTheme] = useState<'light' | 'dark'>('dark') @@ -19,19 +11,8 @@ export function ThemeProvider({ children }: { children: ReactNode }): JSX.Elemen return unsub }, []) - useEffect(() => { - window.api.getAccentColor().then((color) => { - document.documentElement.style.setProperty('--accent', hexToRgbString(color)) - document.documentElement.style.setProperty( - '--accent-soft', - hexToRgbString(color) - ) - }) - const unsub = window.api.onAccentChanged((color) => { - document.documentElement.style.setProperty('--accent', hexToRgbString(color)) - }) - return unsub - }, []) + // Brand palette is fixed (cyan + violet neon). We deliberately do not + // overwrite --accent with the OS accent — keeping the esports HUD identity. useEffect(() => { const pref = settings?.theme ?? 'system' diff --git a/src/renderer/src/styles/globals.css b/src/renderer/src/styles/globals.css index 20acd55..ac99fc3 100644 --- a/src/renderer/src/styles/globals.css +++ b/src/renderer/src/styles/globals.css @@ -3,30 +3,36 @@ @tailwind utilities; :root { - /* Default accent (Windows blue), overridden at runtime via systemPreferences.getAccentColor */ - --accent: 91 141 239; - --accent-soft: 91 141 239; + /* 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 */ color-scheme: light dark; } -/* Light theme (default) */ +/* Light theme — kept clean and modern, sport vibe */ :root { - --bg: 245 247 251; + --bg: 244 246 252; + --bg-deep: 230 234 244; --surface: 255 255 255; - --surface-elevated: 255 255 255; - --border: 226 230 240; - --text: 17 24 39; - --muted: 107 114 128; + --surface-elevated: 248 250 254; + --border: 224 228 240; + --text: 13 18 32; + --muted: 102 112 134; } -/* Dark theme */ +/* Dark theme — esports HUD vibe (default for gamers) */ .dark { - --bg: 15 17 23; - --surface: 24 27 35; - --surface-elevated: 32 36 47; - --border: 45 50 64; - --text: 235 238 245; - --muted: 148 156 173; + --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; } html, @@ -39,11 +45,48 @@ body, } body { - font-family: 'Inter', 'Segoe UI', system-ui, sans-serif; - background: rgb(var(--bg)); + font-family: 'Inter', 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; color: rgb(var(--text)); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: rgb(var(--bg)); + background-image: + radial-gradient( + 1200px 600px at 85% -10%, + rgb(var(--accent) / 0.12), + transparent 60% + ), + radial-gradient( + 900px 500px at -10% 110%, + rgb(var(--accent-2) / 0.1), + transparent 60% + ); + background-attachment: fixed; +} + +.dark body { + background-image: + radial-gradient( + 1200px 600px at 85% -10%, + rgb(var(--accent) / 0.18), + transparent 60% + ), + radial-gradient( + 900px 500px at -10% 110%, + rgb(var(--accent-2) / 0.14), + transparent 60% + ), + linear-gradient(180deg, rgb(var(--bg-deep)) 0%, rgb(var(--bg)) 100%); +} + +/* Display font for big numbers / sport headers */ +.font-display { + font-family: 'Rajdhani', 'Inter', 'Segoe UI Variable', sans-serif; + letter-spacing: 0.02em; +} +.font-mono-num { + font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Code', Menlo, monospace; + font-variant-numeric: tabular-nums; } /* Custom titlebar drag region */ @@ -64,9 +107,13 @@ body { ::-webkit-scrollbar-thumb { background: rgb(var(--border)); border-radius: 8px; + border: 2px solid transparent; + background-clip: padding-box; } ::-webkit-scrollbar-thumb:hover { - background: rgb(var(--muted) / 0.5); + background: rgb(var(--accent) / 0.4); + background-clip: padding-box; + border: 2px solid transparent; } ::-webkit-scrollbar-track { background: transparent; @@ -74,20 +121,138 @@ body { /* Selection */ ::selection { - background: rgb(var(--accent) / 0.35); + background: rgb(var(--accent) / 0.4); color: rgb(var(--text)); } -/* Reminder-window root: rounded corners & subtle border */ +/* Reminder-window root: neon HUD frame */ .reminder-shell { - border: 1px solid rgb(var(--border)); - border-radius: 18px; - background: linear-gradient( - 180deg, - rgb(var(--surface-elevated)) 0%, - rgb(var(--surface)) 100% - ); - box-shadow: 0 24px 60px -20px rgb(0 0 0 / 0.55); + position: relative; + border: 1px solid rgb(var(--accent) / 0.5); + border-radius: 20px; + background: + radial-gradient( + circle at 50% -20%, + rgb(var(--accent) / 0.22), + transparent 60% + ), + linear-gradient(180deg, rgb(var(--surface-elevated)) 0%, rgb(var(--surface)) 100%); + box-shadow: + 0 0 0 1px rgb(var(--accent) / 0.15), + 0 20px 80px -20px rgb(var(--accent) / 0.45), + 0 24px 60px -20px rgb(0 0 0 / 0.6); overflow: hidden; height: 100%; } + +/* Soft scanline texture for HUD surfaces */ +.hud-scanlines { + background-image: repeating-linear-gradient( + 180deg, + rgb(var(--text) / 0.03) 0px, + rgb(var(--text) / 0.03) 1px, + transparent 1px, + transparent 3px + ); +} + +/* Gradient text and gradient brand */ +.text-gradient-brand { + background-image: linear-gradient( + 135deg, + rgb(var(--accent)) 0%, + rgb(var(--accent-2)) 100% + ); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.bg-gradient-brand { + background-image: linear-gradient( + 135deg, + rgb(var(--accent)) 0%, + rgb(var(--accent-2)) 100% + ); +} +.bg-gradient-victory { + background-image: linear-gradient( + 135deg, + rgb(var(--victory)) 0%, + rgb(var(--accent)) 100% + ); +} + +/* Neon border (animated gradient stroke for "due" / "active" cards) */ +.neon-border { + position: relative; + isolation: isolate; +} +.neon-border::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: linear-gradient( + 135deg, + rgb(var(--accent)) 0%, + rgb(var(--accent-2)) 60%, + rgb(var(--accent)) 100% + ); + background-size: 200% 200%; + animation: neon-shift 6s linear infinite; + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + z-index: 1; +} + +/* HUD pulse — soft outer glow for "due" cards */ +.hud-pulse { + animation: hud-pulse 2.4s ease-in-out infinite; +} + +@keyframes neon-shift { + 0% { + background-position: 0% 50%; + } + 100% { + background-position: 200% 50%; + } +} +@keyframes hud-pulse { + 0%, + 100% { + box-shadow: + 0 0 0 0 rgb(var(--accent) / 0.45), + 0 12px 30px -10px rgb(var(--accent) / 0.4); + } + 50% { + box-shadow: + 0 0 0 6px rgb(var(--accent) / 0), + 0 18px 40px -10px rgb(var(--accent) / 0.55); + } +} + +/* Subtle dot-grid texture (sidebar / hero strip) */ +.dot-grid { + background-image: radial-gradient( + rgb(var(--text) / 0.07) 1px, + transparent 1px + ); + background-size: 14px 14px; +} + +/* Cooldown ring SVG helpers */ +.cooldown-track { + stroke: rgb(var(--border)); +} +.cooldown-fill { + stroke: url(#cooldownGrad); + stroke-linecap: round; + filter: drop-shadow(0 0 6px rgb(var(--accent) / 0.6)); + transition: stroke-dashoffset 0.5s linear; +} diff --git a/tailwind.config.js b/tailwind.config.js index 54f3e09..4b5d204 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -7,7 +7,12 @@ export default { colors: { accent: 'rgb(var(--accent) / )', 'accent-soft': 'rgb(var(--accent-soft) / )', + 'accent-2': 'rgb(var(--accent-2) / )', + victory: 'rgb(var(--victory) / )', + defeat: 'rgb(var(--defeat) / )', + xp: 'rgb(var(--xp) / )', bg: 'rgb(var(--bg) / )', + 'bg-deep': 'rgb(var(--bg-deep) / )', surface: 'rgb(var(--surface) / )', 'surface-elevated': 'rgb(var(--surface-elevated) / )', border: 'rgb(var(--border) / )', @@ -15,19 +20,44 @@ export default { muted: 'rgb(var(--muted) / )' }, fontFamily: { - sans: ['Inter', 'Segoe UI', 'system-ui', 'sans-serif'] + sans: ['Inter', 'Segoe UI Variable', 'Segoe UI', 'system-ui', 'sans-serif'], + display: ['Rajdhani', 'Inter', 'Segoe UI Variable', 'sans-serif'], + mono: ['JetBrains Mono', 'ui-monospace', 'Cascadia Code', 'Menlo', 'monospace'] }, boxShadow: { - soft: '0 8px 30px -12px rgb(0 0 0 / 0.25)', - glow: '0 0 0 1px rgb(var(--accent) / 0.4), 0 8px 24px -8px rgb(var(--accent) / 0.5)' + soft: '0 8px 30px -12px rgb(0 0 0 / 0.35)', + glow: '0 0 0 1px rgb(var(--accent) / 0.4), 0 8px 24px -8px rgb(var(--accent) / 0.55)', + 'glow-lg': + '0 0 0 1px rgb(var(--accent) / 0.45), 0 18px 48px -12px rgb(var(--accent) / 0.7)', + 'glow-victory': + '0 0 0 1px rgb(var(--victory) / 0.45), 0 12px 32px -10px rgb(var(--victory) / 0.55)', + hud: '0 1px 0 rgb(var(--text) / 0.04) inset, 0 0 0 1px rgb(var(--border) / 0.8), 0 18px 40px -20px rgb(0 0 0 / 0.4)' + }, + backgroundImage: { + 'gradient-brand': + 'linear-gradient(135deg, rgb(var(--accent)) 0%, rgb(var(--accent-2)) 100%)', + 'gradient-victory': + 'linear-gradient(135deg, rgb(var(--victory)) 0%, rgb(var(--accent)) 100%)', + 'gradient-defeat': + 'linear-gradient(135deg, rgb(var(--defeat)) 0%, rgb(var(--accent-2)) 100%)' }, animation: { - 'pulse-ring': 'pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite' + 'pulse-ring': 'pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', + shimmer: 'shimmer 2.5s linear infinite', + 'neon-shift': 'neon-shift 6s linear infinite' }, keyframes: { 'pulse-ring': { '0%, 100%': { transform: 'scale(1)', opacity: '0.7' }, - '50%': { transform: 'scale(1.05)', opacity: '0.3' } + '50%': { transform: 'scale(1.1)', opacity: '0.25' } + }, + shimmer: { + '0%': { backgroundPosition: '-200% 0' }, + '100%': { backgroundPosition: '200% 0' } + }, + 'neon-shift': { + '0%': { backgroundPosition: '0% 50%' }, + '100%': { backgroundPosition: '200% 50%' } } } }