-
-
-
-
-
- Ближайшее напоминание
-
-
- {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%' }
}
}
}