redesign: bento home + semantic tokens + solid cards
All checks were successful
Deploy / deploy (push) Successful in 2m43s

- introduces semantic CSS tokens (--surface-1/2/3, --border-subtle/strong,
  --hairline, --shadow-sm/md/lg/xl) with distinct dark and light values;
  fixes broken light theme caused by hardcoded rgba(255,255,255,X)
- drops glassmorphism on cards — solid var(--surface-1) with 1px border
  and layered shadows; glass kept only for aurora page background
- introduces .card/.card-raised/.card-hero utility classes
- Home page restructured into a bento grid:
  * greeting row with inline day/date
  * hero weather (64px number, large icon, ощущается/влажность/ветер)
    next to the tram widget (1fr 1.1fr)
  * forecast as a single hairline-separated band (no per-day cards)
  * events+notes in a 2-column grid; events card combines today and
    tomorrow with a divider; notes card styled via surface tokens
- TransportWidget repainted to use tokens, larger numbers (32px for the
  next arrival), imminent highlight uses color-mix against surface-2
This commit is contained in:
Cosmo
2026-04-23 08:30:03 +00:00
parent 9ad758174d
commit 121bf30ab1
3 changed files with 461 additions and 375 deletions

View File

@@ -4,34 +4,101 @@
padding: 0; padding: 0;
} }
/* ——————————————————————————————
Tokens — dark (default)
—————————————————————————————— */
:root { :root {
--bg: #0c0c18; /* Surfaces */
--bg-secondary: #12121f; --bg: #0b0b14;
--sidebar-bg: rgba(12, 12, 24, 0.8); --bg-secondary: #101020;
--card-bg: rgba(255, 255, 255, 0.04); --surface-1: #15152a;
--card-bg-hover: rgba(255, 255, 255, 0.07); --surface-2: #1c1c36;
--card-border: rgba(255, 255, 255, 0.07); --surface-3: #242444;
--card-border-hover: rgba(255, 255, 255, 0.12); --surface-hover: #1e1e3c;
/* Borders */
--border-subtle: rgba(255, 255, 255, 0.06);
--border-strong: rgba(255, 255, 255, 0.12);
--hairline: rgba(255, 255, 255, 0.08);
/* Text */
--text-primary: rgba(255, 255, 255, 0.95); --text-primary: rgba(255, 255, 255, 0.95);
--text-secondary: rgba(255, 255, 255, 0.45); --text-secondary: rgba(255, 255, 255, 0.6);
--text-tertiary: rgba(255, 255, 255, 0.25); --text-tertiary: rgba(255, 255, 255, 0.38);
/* Accents */
--accent: #818cf8; --accent: #818cf8;
--accent-secondary: #22d3ee; --accent-secondary: #22d3ee;
--accent-glow: rgba(129, 140, 248, 0.15); --accent-glow: rgba(129, 140, 248, 0.22);
--glass: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.06); /* Brand gradients */
--gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6); --gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6);
--gradient-warm: linear-gradient(135deg, #f59e0b, #ef4444); --gradient-warm: linear-gradient(135deg, #f59e0b, #ef4444);
--gradient-cool: linear-gradient(135deg, #06b6d4, #3b82f6); --gradient-cool: linear-gradient(135deg, #06b6d4, #3b82f6);
--gradient-green: linear-gradient(135deg, #10b981, #34d399); --gradient-green: linear-gradient(135deg, #10b981, #34d399);
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.45);
--shadow-xl: 0 24px 60px rgba(0, 0, 0, 0.5);
/* State */
--on-color: #818cf8; --on-color: #818cf8;
--off-color: rgba(255, 255, 255, 0.15); --off-color: rgba(255, 255, 255, 0.15);
/* Radius */
--radius-xs: 8px;
--radius-sm: 12px; --radius-sm: 12px;
--radius-md: 16px; --radius-md: 16px;
--radius-lg: 22px; --radius-lg: 22px;
--radius-xl: 28px; --radius-xl: 28px;
/* Legacy aliases */
--sidebar-bg: var(--bg);
--card-bg: var(--surface-1);
--card-bg-hover: var(--surface-2);
--card-border: var(--border-subtle);
--card-border-hover: var(--border-strong);
--glass: var(--surface-1);
--glass-border: var(--border-subtle);
} }
/* ——————————————————————————————
Tokens — light
—————————————————————————————— */
.light {
--bg: #f6f7fb;
--bg-secondary: #eef0f5;
--surface-1: #ffffff;
--surface-2: #f3f4f8;
--surface-3: #eaebf0;
--surface-hover: #f8f9fc;
--border-subtle: rgba(15, 20, 40, 0.06);
--border-strong: rgba(15, 20, 40, 0.12);
--hairline: rgba(15, 20, 40, 0.08);
--text-primary: rgba(15, 20, 40, 0.94);
--text-secondary: rgba(15, 20, 40, 0.62);
--text-tertiary: rgba(15, 20, 40, 0.38);
--accent: #5b63e0;
--accent-secondary: #0891b2;
--accent-glow: rgba(91, 99, 224, 0.14);
--shadow-sm: 0 1px 2px rgba(15, 20, 40, 0.05);
--shadow-md: 0 2px 6px rgba(15, 20, 40, 0.06), 0 12px 28px -8px rgba(15, 20, 40, 0.1);
--shadow-lg: 0 4px 10px rgba(15, 20, 40, 0.06), 0 24px 60px -12px rgba(15, 20, 40, 0.14);
--shadow-xl: 0 8px 16px rgba(15, 20, 40, 0.06), 0 40px 100px -24px rgba(15, 20, 40, 0.2);
--on-color: #5b63e0;
--off-color: rgba(15, 20, 40, 0.12);
}
/* ——————————————————————————————
Base
—————————————————————————————— */
html, body { html, body {
background: var(--bg); background: var(--bg);
color: var(--text-primary); color: var(--text-primary);
@@ -40,13 +107,14 @@ html, body {
overflow: hidden; overflow: hidden;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
transition: background 0.3s ease, color 0.3s ease;
} }
#__next, main { #__next, main { height: 100%; }
height: 100%;
}
/* Ambient background orbs */ /* ——————————————————————————————
Aurora
—————————————————————————————— */
.bg-ambient { .bg-ambient {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -54,27 +122,27 @@ html, body {
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;
} }
.bg-ambient::before { .bg-ambient::before,
content: '';
position: absolute;
width: 600px;
height: 600px;
border-radius: 50%;
background: radial-gradient(circle, rgba(99, 102, 241, 0.08) 0%, transparent 70%);
top: -200px;
right: -100px;
animation: float1 20s ease-in-out infinite;
}
.bg-ambient::after { .bg-ambient::after {
content: ''; content: '';
position: absolute; position: absolute;
width: 500px;
height: 500px;
border-radius: 50%; border-radius: 50%;
background: radial-gradient(circle, rgba(139, 92, 246, 0.06) 0%, transparent 70%); filter: blur(80px);
bottom: -150px; }
left: -50px; .bg-ambient::before {
animation: float2 25s ease-in-out infinite; width: 560px; height: 560px;
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
top: -180px; right: -80px;
animation: float1 22s ease-in-out infinite;
}
.bg-ambient::after {
width: 480px; height: 480px;
background: radial-gradient(circle, rgba(139, 92, 246, 0.14) 0%, transparent 70%);
bottom: -140px; left: -60px;
animation: float2 27s ease-in-out infinite;
}
.light .bg-ambient::after {
background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 70%);
} }
@keyframes float1 { @keyframes float1 {
@@ -88,21 +156,36 @@ html, body {
66% { transform: translate(20px, -10px) scale(0.9); } 66% { transform: translate(20px, -10px) scale(0.9); }
} }
/* Glass card base */ /* ——————————————————————————————
.glass-card { Cards
background: var(--card-bg); —————————————————————————————— */
backdrop-filter: blur(20px); .card {
-webkit-backdrop-filter: blur(20px); background: var(--surface-1);
border: 1px solid var(--card-border); border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: var(--shadow-sm);
} }
.glass-card:hover { .card-raised {
background: var(--card-bg-hover); background: var(--surface-1);
border-color: var(--card-border-hover); border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
}
.card-hero {
background: var(--surface-1);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-lg);
} }
/* Gradient text */ .glass-card {
background: var(--surface-1);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.glass-card:hover { background: var(--surface-hover); }
.gradient-text { .gradient-text {
background: var(--gradient-primary); background: var(--gradient-primary);
-webkit-background-clip: text; -webkit-background-clip: text;
@@ -118,125 +201,48 @@ button {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
touch-action: manipulation; touch-action: manipulation;
font-family: inherit; font-family: inherit;
color: inherit;
} }
button:focus-visible { button:focus-visible {
outline: 2px solid var(--accent); outline: 2px solid var(--accent);
outline-offset: 2px; outline-offset: 2px;
} }
/* Smooth scrollbar */ ::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar { ::-webkit-scrollbar-track { background: transparent; }
width: 4px; ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
height: 4px; ::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}
/* Pulse animation for active devices */ /* ——————————————————————————————
Motion
—————————————————————————————— */
@keyframes pulse-glow { @keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(129, 140, 248, 0.15); } 0%, 100% { box-shadow: 0 0 20px var(--accent-glow); }
50% { box-shadow: 0 0 30px rgba(129, 140, 248, 0.25); } 50% { box-shadow: 0 0 30px var(--accent-glow); }
} }
/* Slide in animation */
@keyframes slideUp { @keyframes slideUp {
from { opacity: 0; transform: translateY(10px); } from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.animate-slide-up { animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
.animate-slide-up { @keyframes fan-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
/* ————— Light theme ————— */
.light {
--bg: #f5f5fa;
--bg-secondary: #eeeef4;
--sidebar-bg: rgba(255, 255, 255, 0.8);
--card-bg: rgba(255, 255, 255, 0.7);
--card-bg-hover: rgba(255, 255, 255, 0.85);
--card-border: rgba(0, 0, 0, 0.06);
--card-border-hover: rgba(0, 0, 0, 0.1);
--text-primary: rgba(0, 0, 0, 0.88);
--text-secondary: rgba(0, 0, 0, 0.45);
--text-tertiary: rgba(0, 0, 0, 0.2);
--accent: #6366f1;
--accent-secondary: #0891b2;
--accent-glow: rgba(99, 102, 241, 0.12);
--glass: rgba(255, 255, 255, 0.5);
--glass-border: rgba(0, 0, 0, 0.06);
--on-color: #6366f1;
--off-color: rgba(0, 0, 0, 0.12);
}
.light .bg-ambient::before {
background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%);
}
.light .bg-ambient::after {
background: radial-gradient(circle, rgba(139, 92, 246, 0.04) 0%, transparent 70%);
}
.light ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
}
.light ::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.2);
}
/* ————— Device animations ————— */
@keyframes fan-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes light-pulse { @keyframes light-pulse {
0%, 100% { filter: drop-shadow(0 0 4px rgba(251, 191, 36, 0.3)); } 0%, 100% { filter: drop-shadow(0 0 4px rgba(251, 191, 36, 0.3)); }
50% { filter: drop-shadow(0 0 12px rgba(251, 191, 36, 0.6)); } 50% { filter: drop-shadow(0 0 12px rgba(251, 191, 36, 0.6)); }
} }
@keyframes device-breathe { 0%, 100% { opacity: 0.7; } 50% { opacity: 1; } }
.fan-spinning { animation: fan-spin 2s linear infinite; }
.light-on-pulse { animation: light-pulse 3s ease-in-out infinite; }
.device-active-breathe { animation: device-breathe 3s ease-in-out infinite; }
@keyframes device-breathe { @keyframes spin-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
0%, 100% { opacity: 0.7; } @keyframes cloud-float { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(4px); } }
50% { opacity: 1; }
}
.fan-spinning {
animation: fan-spin 2s linear infinite;
}
.light-on-pulse {
animation: light-pulse 3s ease-in-out infinite;
}
.device-active-breathe {
animation: device-breathe 3s ease-in-out infinite;
}
/* ————— Weather animations ————— */
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes cloud-float {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(4px); }
}
@keyframes rain-fall { @keyframes rain-fall {
0% { transform: translateY(0); opacity: 0.7; } 0% { transform: translateY(0); opacity: 0.7; }
80% { opacity: 0.7; } 80% { opacity: 0.7; }
100% { transform: translateY(16px); opacity: 0; } 100% { transform: translateY(16px); opacity: 0; }
} }
@keyframes snow-fall { @keyframes snow-fall {
0% { transform: translateY(0) translateX(0); opacity: 0.9; } 0% { transform: translateY(0) translateX(0); opacity: 0.9; }
25% { transform: translateY(5px) translateX(2px); opacity: 0.8; } 25% { transform: translateY(5px) translateX(2px); opacity: 0.8; }
@@ -244,7 +250,6 @@ button:focus-visible {
75% { transform: translateY(15px) translateX(3px); opacity: 0.4; } 75% { transform: translateY(15px) translateX(3px); opacity: 0.4; }
100% { transform: translateY(22px) translateX(0); opacity: 0; } 100% { transform: translateY(22px) translateX(0); opacity: 0; }
} }
@keyframes thunder-flash { @keyframes thunder-flash {
0%, 100% { opacity: 0; } 0%, 100% { opacity: 0; }
5%, 7% { opacity: 1; } 5%, 7% { opacity: 1; }
@@ -252,52 +257,21 @@ button:focus-visible {
50%, 52% { opacity: 0.8; } 50%, 52% { opacity: 0.8; }
51% { opacity: 0.2; } 51% { opacity: 0.2; }
} }
@keyframes fog-drift { @keyframes fog-drift {
0%, 100% { transform: translateX(0); opacity: 0.4; } 0%, 100% { transform: translateX(0); opacity: 0.4; }
50% { transform: translateX(8px); opacity: 0.6; } 50% { transform: translateX(8px); opacity: 0.6; }
} }
/* ————— Dynamic weather backgrounds ————— */ /* Weather ambient tints via accent-glow */
.weather-bg-clear { .weather-bg-clear { --accent-glow: rgba(251, 191, 36, 0.18); }
--orb1-color: rgba(251, 191, 36, 0.08); .weather-bg-cloudy { --accent-glow: rgba(148, 163, 184, 0.18); }
--orb2-color: rgba(245, 158, 11, 0.05); .weather-bg-rain { --accent-glow: rgba(59, 130, 246, 0.18); }
} .weather-bg-snow { --accent-glow: rgba(186, 230, 253, 0.22); }
.weather-bg-cloudy { .weather-bg-thunder { --accent-glow: rgba(139, 92, 246, 0.22); }
--orb1-color: rgba(148, 163, 184, 0.08); .weather-bg-night { --accent-glow: rgba(67, 56, 202, 0.16); }
--orb2-color: rgba(100, 116, 139, 0.06); .light.weather-bg-clear { --accent-glow: rgba(234, 169, 27, 0.16); }
} .light.weather-bg-cloudy { --accent-glow: rgba(100, 116, 139, 0.14); }
.weather-bg-rain { .light.weather-bg-rain { --accent-glow: rgba(37, 99, 235, 0.14); }
--orb1-color: rgba(59, 130, 246, 0.08); .light.weather-bg-snow { --accent-glow: rgba(125, 211, 252, 0.22); }
--orb2-color: rgba(30, 64, 175, 0.06); .light.weather-bg-thunder { --accent-glow: rgba(109, 40, 217, 0.14); }
} .light.weather-bg-night { --accent-glow: rgba(55, 48, 163, 0.14); }
.weather-bg-snow {
--orb1-color: rgba(186, 230, 253, 0.1);
--orb2-color: rgba(147, 197, 253, 0.07);
}
.weather-bg-thunder {
--orb1-color: rgba(139, 92, 246, 0.1);
--orb2-color: rgba(88, 28, 135, 0.06);
}
.weather-bg-night {
--orb1-color: rgba(67, 56, 202, 0.06);
--orb2-color: rgba(49, 46, 129, 0.05);
}
.weather-bg-clear .bg-ambient::before,
.weather-bg-cloudy .bg-ambient::before,
.weather-bg-rain .bg-ambient::before,
.weather-bg-snow .bg-ambient::before,
.weather-bg-thunder .bg-ambient::before,
.weather-bg-night .bg-ambient::before {
background: radial-gradient(circle, var(--orb1-color) 0%, transparent 70%);
}
.weather-bg-clear .bg-ambient::after,
.weather-bg-cloudy .bg-ambient::after,
.weather-bg-rain .bg-ambient::after,
.weather-bg-snow .bg-ambient::after,
.weather-bg-thunder .bg-ambient::after,
.weather-bg-night .bg-ambient::after {
background: radial-gradient(circle, var(--orb2-color) 0%, transparent 70%);
}

View File

@@ -419,94 +419,199 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
return ( return (
<div style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, padding: '20px 24px 28px', display: 'flex', flexDirection: 'column', gap: 14 }}> <div style={{
{/* Greeting + hint */} flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any,
<div> padding: '18px 22px 24px',
<h1 style={{ fontSize: 26, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-0.5px', margin: 0 }}> display: 'flex', flexDirection: 'column', gap: 14,
{greeting} 👋 }}>
{/* ───── Greeting row ───── */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
<h1 style={{
fontSize: 28, fontWeight: 800, color: 'var(--text-primary)',
letterSpacing: '-0.6px', margin: 0, lineHeight: 1.1,
}}>
{greeting} <span style={{ fontSize: 26 }}>👋</span>
</h1> </h1>
<div style={{
fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600,
textTransform: 'capitalize', textAlign: 'right',
letterSpacing: '-0.1px',
}}>
{new Date().toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' })}
</div>
</div> </div>
{/* Weather — full width compact */} {/* ───── Bento row: Hero weather + Tram ───── */}
{weather && ( <div style={{ display: 'grid', gridTemplateColumns: '1fr 1.1fr', gap: 14, minHeight: 230 }}>
<div style={{
background: 'linear-gradient(135deg, rgba(99,102,241,0.1), rgba(139,92,246,0.05))', {/* Hero weather card */}
backdropFilter: 'blur(20px)', border: '1px solid rgba(129,140,248,0.1)', {weather ? (
borderRadius: 20, padding: '18px 22px', <div
display: 'flex', alignItems: 'center', gap: 20, className="card-hero"
position: 'relative', overflow: 'hidden', onClick={() => weather?.forecast?.[0] && setSelectedDay(weather.forecast[0])}
style={{
padding: '22px 24px',
display: 'flex', flexDirection: 'column',
position: 'relative', overflow: 'hidden',
cursor: 'pointer',
}}
>
{/* Decorative animation, large, behind */}
<div style={{
position: 'absolute', top: -24, right: -12,
opacity: 0.14, pointerEvents: 'none',
}}>
<WeatherAnimation condition={weather.desc} size={160} />
</div>
<div style={{
fontSize: 11, color: 'var(--text-tertiary)', fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 8,
position: 'relative', zIndex: 1,
}}>
Сейчас
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 12,
position: 'relative', zIndex: 1, marginBottom: 6,
}}>
<div style={{
fontSize: 64, fontWeight: 800, letterSpacing: '-3px',
color: 'var(--text-primary)', lineHeight: 0.9,
fontVariantNumeric: 'tabular-nums',
}}>
{weather.temp}°
</div>
<WeatherAnimation condition={weather.desc} size={52} />
</div>
<div style={{
fontSize: 16, color: 'var(--text-primary)', fontWeight: 600,
position: 'relative', zIndex: 1, marginBottom: 12,
}}>
{weather.desc}
</div>
<div style={{
display: 'flex', gap: 16, flexWrap: 'wrap',
marginTop: 'auto', position: 'relative', zIndex: 1,
}}>
{weather.feelsLike && (
<div>
<div style={{ fontSize: 10, color: 'var(--text-tertiary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em' }}>Ощущается</div>
<div style={{ fontSize: 15, color: 'var(--text-primary)', fontWeight: 700, marginTop: 2 }}>{weather.feelsLike}°</div>
</div>
)}
{weather.humidity && (
<div>
<div style={{ fontSize: 10, color: 'var(--text-tertiary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em' }}>Влажность</div>
<div style={{ fontSize: 15, color: 'var(--text-primary)', fontWeight: 700, marginTop: 2 }}>{weather.humidity}</div>
</div>
)}
{weather.windSpeed && (
<div>
<div style={{ fontSize: 10, color: 'var(--text-tertiary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em' }}>Ветер</div>
<div style={{ fontSize: 15, color: 'var(--text-primary)', fontWeight: 700, marginTop: 2 }}>{weather.windSpeed}</div>
</div>
)}
</div>
</div>
) : (
<div className="card-hero" style={{ padding: '22px 24px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>
Загрузка погоды...
</div>
)}
{/* Tram */}
<TransportWidget />
</div>
{/* ───── Forecast band (no cards, hairline-separated) ───── */}
{weather?.forecast && (
<div className="card" style={{
padding: '12px 6px',
display: 'flex', alignItems: 'stretch',
}}> }}>
<div style={{ position: 'absolute', top: -15, right: 5, opacity: 0.1, pointerEvents: 'none' }}> {weather.forecast.map((day, idx) => {
<WeatherAnimation condition={weather.desc} size={90} /> const d = new Date(day.date)
</div> const isToday = idx === 0
return (
{/* Current */} <div key={day.date} style={{
<div onClick={() => weather?.forecast?.[0] && setSelectedDay(weather.forecast[0])} style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1, cursor: 'pointer' }}> flex: 1, display: 'flex', alignItems: 'stretch',
<WeatherAnimation condition={weather.desc} size={48} /> borderRight: idx < weather.forecast!.length - 1 ? '1px solid var(--hairline)' : 'none',
<div> }}>
<div style={{ fontSize: 32, fontWeight: 800, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div> <button
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 3, fontWeight: 500 }}>{weather.desc}</div> onClick={() => setSelectedDay(day)}
</div> style={{
</div> flex: 1, padding: '8px 4px', borderRadius: 14,
background: isToday ? 'var(--surface-2)' : 'transparent',
{/* Divider */} display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
<div style={{ width: 1, height: 50, background: 'rgba(255,255,255,0.08)', flexShrink: 0 }} />
{/* 7 day forecast */}
{weather.forecast && (
<div style={{ display: 'flex', gap: 4, flex: 1, overflow: 'hidden' }}>
{weather.forecast.map((day, idx) => {
const d = new Date(day.date)
const isToday = idx === 0
return (
<div key={day.date} onClick={() => setSelectedDay(day)} style={{
flex: 1, minWidth: 0, textAlign: 'center', padding: '4px 2px',
borderRadius: 10, cursor: 'pointer',
background: isToday ? 'rgba(99,102,241,0.1)' : 'transparent',
transition: 'background 0.2s ease', transition: 'background 0.2s ease',
}}
>
<div style={{
fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase',
color: isToday ? 'var(--accent)' : 'var(--text-tertiary)',
}}> }}>
<div style={{ fontSize: 9, color: isToday ? '#a5b4fc' : 'var(--text-secondary)', fontWeight: 600, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis' }}> {isToday ? 'Сей' : d.toLocaleDateString('ru-RU', { weekday: 'short' }).replace('.', '').slice(0, 2)}
{isToday ? 'Сей' : d.toLocaleDateString('ru-RU', { weekday: 'short' }).slice(0, 2)}
</div>
<div style={{ fontSize: 14, marginBottom: 2 }}>{getWeatherIcon(day.desc)}</div>
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-primary)' }}>{day.maxTemp}°</div>
<div style={{ fontSize: 9, color: 'var(--text-secondary)' }}>{day.minTemp}°</div>
</div> </div>
) <div style={{ fontSize: 20 }}>{getWeatherIcon(day.desc)}</div>
})} <div style={{
</div> fontSize: 14, fontWeight: 800, color: 'var(--text-primary)',
)} letterSpacing: '-0.5px', fontVariantNumeric: 'tabular-nums',
}}>
{day.maxTemp}°
</div>
<div style={{
fontSize: 11, color: 'var(--text-tertiary)', fontWeight: 500,
fontVariantNumeric: 'tabular-nums',
}}>
{day.minTemp}°
</div>
</button>
</div>
)
})}
</div> </div>
)} )}
{/* Transport: tram arrivals at Ул. Антонова-Овсеенко, both directions */} {/* ───── Events + Notes row ───── */}
<TransportWidget />
{/* Two columns: Events + Notes */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, flex: 1, minHeight: 0 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, flex: 1, minHeight: 0 }}>
{/* Left: Today + Tomorrow events */} {/* Events — today + tomorrow in one card */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}> <div className="card" style={{ padding: '18px 20px', display: 'flex', flexDirection: 'column', gap: 14, overflowY: 'auto' }}>
{/* Today */} {/* Today */}
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 20, padding: '18px 20px' }}> <div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<Calendar size={14} color="var(--text-secondary)" /> <Calendar size={13} color="var(--text-secondary)" />
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>Сегодня</span> <span style={{ fontSize: 10, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700 }}>Сегодня</span>
<span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{todayEvents.length}</span> <span style={{
fontSize: 10, color: 'var(--text-tertiary)',
background: 'var(--surface-2)', padding: '2px 7px', borderRadius: 8,
fontWeight: 700, fontVariantNumeric: 'tabular-nums',
}}>
{todayEvents.length}
</span>
</div> </div>
{calLoading ? ( {calLoading ? (
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>Загрузка...</div> <div style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Загрузка...</div>
) : todayEvents.length === 0 ? ( ) : todayEvents.length === 0 ? (
<div style={{ fontSize: 14, color: 'var(--text-secondary)', textAlign: 'center', padding: '8px 0' }}>Свободный день</div> <div style={{ fontSize: 14, color: 'var(--text-tertiary)', textAlign: 'center', padding: '6px 0' }}>Свободный день </div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{todayEvents.map(ev => ( {todayEvents.map(ev => (
<div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 12, background: `${ev.color}08`, border: `1px solid ${ev.color}12` }}> <div key={ev.id} style={{
<div style={{ width: 3, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 30, flexShrink: 0 }} /> display: 'flex', alignItems: 'center', gap: 10,
padding: '8px 10px', borderRadius: 12,
background: 'var(--surface-2)',
border: '1px solid var(--border-subtle)',
}}>
<div style={{ width: 3, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 28, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div> <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div>
<div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 2 }}> <div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 2 }}>
{ev.allDay ? 'Весь день' : formatEventTime(ev.start)} · <span style={{ color: ev.color }}>{ev.ownerName}</span> {ev.allDay ? 'Весь день' : formatEventTime(ev.start)} · <span style={{ color: ev.color, fontWeight: 600 }}>{ev.ownerName}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -515,23 +620,34 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
)} )}
</div> </div>
{/* Divider */}
{(tomorrowEvents.length > 0 || todayEvents.length > 0) && (
<div style={{ height: 1, background: 'var(--hairline)' }} />
)}
{/* Tomorrow */} {/* Tomorrow */}
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.04)', borderRadius: 20, padding: '18px 20px' }}> <div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<Calendar size={14} color="var(--text-secondary)" /> <Calendar size={13} color="var(--text-tertiary)" />
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>Завтра</span> <span style={{ fontSize: 10, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700 }}>Завтра</span>
<span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{tomorrowEvents.length}</span> <span style={{
fontSize: 10, color: 'var(--text-tertiary)',
background: 'var(--surface-2)', padding: '2px 7px', borderRadius: 8,
fontWeight: 700, fontVariantNumeric: 'tabular-nums',
}}>
{tomorrowEvents.length}
</span>
</div> </div>
{tomorrowEvents.length === 0 ? ( {tomorrowEvents.length === 0 ? (
<div style={{ fontSize: 14, color: 'var(--text-secondary)', textAlign: 'center', padding: '8px 0' }}>Нет событий</div> <div style={{ fontSize: 13, color: 'var(--text-tertiary)', textAlign: 'center', padding: '4px 0' }}></div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
{tomorrowEvents.map(ev => ( {tomorrowEvents.map(ev => (
<div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 12, background: `${ev.color}06` }}> <div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px' }}>
<div style={{ width: 3, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 24, flexShrink: 0, opacity: 0.6 }} /> <div style={{ width: 3, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 22, flexShrink: 0, opacity: 0.7 }} />
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div> <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div>
<div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 2 }}> <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 1 }}>
{ev.allDay ? 'Весь день' : formatEventTime(ev.start)} {ev.allDay ? 'Весь день' : formatEventTime(ev.start)}
</div> </div>
</div> </div>
@@ -542,53 +658,62 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
</div> </div>
</div> </div>
{/* Right: Pinned notes / shopping lists */} {/* Notes */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}> <div className="card" style={{ padding: '18px 20px', display: 'flex', flexDirection: 'column', gap: 12, overflowY: 'auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<StickyNote size={13} color="var(--text-secondary)" />
<span style={{ fontSize: 10, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700 }}>Заметки</span>
</div>
{pinnedNotes.length === 0 ? ( {pinnedNotes.length === 0 ? (
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.04)', borderRadius: 20, padding: '18px 20px', flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', color: 'var(--text-tertiary)' }}>
<div style={{ textAlign: 'center', color: 'var(--text-secondary)' }}> <div>
<StickyNote size={24} style={{ margin: '0 auto 8px', opacity: 0.3 }} /> <StickyNote size={22} style={{ margin: '0 auto 6px', opacity: 0.4 }} />
<div style={{ fontSize: 13 }}>Заметки появятся здесь</div> <div style={{ fontSize: 12 }}>Заметки появятся здесь</div>
</div> </div>
</div> </div>
) : ( ) : (
pinnedNotes.map(note => { <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
const doneCount = note.items?.filter((i: any) => i.done).length || 0 {pinnedNotes.map(note => {
const totalCount = note.items?.length || 0 const doneCount = note.items?.filter((i: any) => i.done).length || 0
return ( const totalCount = note.items?.length || 0
<div key={note.id} style={{ return (
background: `${note.color}08`, border: `1px solid ${note.color}15`, <div key={note.id} style={{
borderRadius: 20, padding: '18px 20px', padding: '10px 12px', borderRadius: 12,
}}> background: 'var(--surface-2)',
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}> border: '1px solid var(--border-subtle)',
{note.type === 'shopping' ? <ShoppingCart size={14} color={note.color} /> : <FileText size={14} color={note.color} />} borderLeft: `3px solid ${note.color}`,
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{note.title}</span> }}>
{note.type === 'shopping' && totalCount > 0 && ( <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<span style={{ fontSize: 11, color: note.color, marginLeft: 'auto' }}>{doneCount}/{totalCount}</span> {note.type === 'shopping' ? <ShoppingCart size={12} color={note.color} /> : <FileText size={12} color={note.color} />}
)} <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-primary)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{note.title}</span>
</div> {note.type === 'shopping' && totalCount > 0 && (
{note.type === 'shopping' ? ( <span style={{ fontSize: 11, color: note.color, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{doneCount}/{totalCount}</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{(note.items || []).filter((i: any) => !i.done).slice(0, 5).map((item: any) => (
<div key={item.id} style={{ fontSize: 13, color: 'var(--text-primary)', display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 6, height: 6, borderRadius: 2, border: `1.5px solid ${note.color}50`, flexShrink: 0 }} />
{item.text}
</div>
))}
{(note.items || []).filter((i: any) => !i.done).length > 5 && (
<div style={{ fontSize: 11, color: 'var(--text-secondary)' }}>
+{(note.items || []).filter((i: any) => !i.done).length - 5} ещё
</div>
)} )}
</div> </div>
) : ( {note.type === 'shopping' ? (
<div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.5, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 4, WebkitBoxOrient: 'vertical' as any }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{note.text || 'Пустая заметка'} {(note.items || []).filter((i: any) => !i.done).slice(0, 4).map((item: any) => (
</div> <div key={item.id} style={{ fontSize: 12, color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: 6 }}>
)} <div style={{ width: 5, height: 5, borderRadius: 2, background: note.color, opacity: 0.6, flexShrink: 0 }} />
</div> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.text}</span>
) </div>
}) ))}
{(note.items || []).filter((i: any) => !i.done).length > 4 && (
<div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginLeft: 11 }}>
+{(note.items || []).filter((i: any) => !i.done).length - 4} ещё
</div>
)}
</div>
) : (
<div style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.5, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical' as any }}>
{note.text || 'Пустая заметка'}
</div>
)}
</div>
)
})}
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -22,16 +22,11 @@ const DIRECTIONS: Direction[] = [
] ]
const ROUTES: { num: string; color: string; bg: string }[] = [ const ROUTES: { num: string; color: string; bg: string }[] = [
{ num: '23', color: '#34d399', bg: 'linear-gradient(135deg, #10b981, #059669)' }, { num: '23', color: '#10b981', bg: 'linear-gradient(135deg, #10b981, #059669)' },
{ num: '27', color: '#60a5fa', bg: 'linear-gradient(135deg, #3b82f6, #2563eb)' }, { num: '27', color: '#3b82f6', bg: 'linear-gradient(135deg, #3b82f6, #2563eb)' },
{ num: '39', color: '#f87171', bg: 'linear-gradient(135deg, #ef4444, #dc2626)' }, { num: '39', color: '#ef4444', bg: 'linear-gradient(135deg, #ef4444, #dc2626)' },
] ]
function formatMinutes(m: number): string {
if (m <= 0) return 'сейчас'
return `${m} мин`
}
function Cell({ arrivals, color }: { arrivals: Arrival[]; color: string }) { function Cell({ arrivals, color }: { arrivals: Arrival[]; color: string }) {
const sorted = [...arrivals].sort((a, b) => a.minutes - b.minutes).slice(0, 3) const sorted = [...arrivals].sort((a, b) => a.minutes - b.minutes).slice(0, 3)
if (sorted.length === 0) { if (sorted.length === 0) {
@@ -39,10 +34,10 @@ function Cell({ arrivals, color }: { arrivals: Arrival[]; color: string }) {
<div style={{ <div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '10px 14px', borderRadius: 12, padding: '10px 14px', borderRadius: 12,
background: 'rgba(255,255,255,0.02)', background: 'var(--surface-2)',
border: '1px dashed rgba(255,255,255,0.06)', border: '1px dashed var(--border-subtle)',
color: 'var(--text-tertiary)', fontSize: 13, fontWeight: 500, color: 'var(--text-tertiary)', fontSize: 13, fontWeight: 500,
minHeight: 52, minHeight: 56,
}}></div> }}></div>
) )
} }
@@ -50,49 +45,46 @@ function Cell({ arrivals, color }: { arrivals: Arrival[]; color: string }) {
const imminent = first.minutes <= 2 const imminent = first.minutes <= 2
return ( return (
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 10, display: 'flex', alignItems: 'center', gap: 12,
padding: '8px 14px', borderRadius: 12, padding: '10px 14px', borderRadius: 12,
background: imminent ? `${color}15` : 'rgba(255,255,255,0.03)', background: imminent ? `color-mix(in srgb, ${color} 10%, var(--surface-2))` : 'var(--surface-2)',
border: `1px solid ${imminent ? color + '35' : 'rgba(255,255,255,0.06)'}`, border: `1px solid ${imminent ? color : 'var(--border-subtle)'}`,
minHeight: 52, minHeight: 56,
transition: 'all 0.25s ease', transition: 'all 0.3s ease',
}}> }}>
{/* Primary time */} {/* Primary time — big */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, flexShrink: 0 }}> <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, flexShrink: 0 }}>
<div style={{ <div style={{
fontSize: first.minutes <= 0 ? 16 : 24, fontSize: first.minutes <= 0 ? 18 : 32,
fontWeight: 800, letterSpacing: '-1px', lineHeight: 1, fontWeight: 800, letterSpacing: '-1.5px', lineHeight: 1,
color: imminent ? color : 'var(--text-primary)', color: imminent ? color : 'var(--text-primary)',
fontVariantNumeric: 'tabular-nums', fontVariantNumeric: 'tabular-nums',
}}> }}>
{first.minutes <= 0 ? 'сейчас' : first.minutes} {first.minutes <= 0 ? 'сейчас' : first.minutes}
</div> </div>
{first.minutes > 0 && ( {first.minutes > 0 && (
<div style={{ fontSize: 11, color: 'var(--text-secondary)', fontWeight: 500 }}>мин</div> <div style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600 }}>мин</div>
)} )}
</div> </div>
{/* Divider */}
{rest.length > 0 && ( {rest.length > 0 && (
<div style={{ width: 1, alignSelf: 'stretch', background: 'rgba(255,255,255,0.06)' }} /> <>
)} <div style={{ width: 1, alignSelf: 'stretch', background: 'var(--hairline)', margin: '4px 0' }} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, flex: 1, minWidth: 0 }}>
{/* Next arrivals */} <div style={{
{rest.length > 0 && ( fontSize: 9, color: 'var(--text-tertiary)', fontWeight: 700,
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 0 }}> textTransform: 'uppercase', letterSpacing: '0.08em',
<div style={{ }}>
fontSize: 10, color: 'var(--text-tertiary)', fontWeight: 600, затем
textTransform: 'uppercase', letterSpacing: '0.06em', </div>
}}> <div style={{
затем fontSize: 13, color: 'var(--text-secondary)', fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
}}>
{rest.map(r => r.minutes <= 0 ? 'сейчас' : `${r.minutes} мин`).join(' · ')}
</div>
</div> </div>
<div style={{ </>
fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
}}>
{rest.map(r => formatMinutes(r.minutes)).join(' · ')}
</div>
</div>
)} )}
</div> </div>
) )
@@ -128,38 +120,33 @@ export default function TransportWidget() {
}, []) }, [])
return ( return (
<div style={{ <div className="card-hero" style={{
background: 'linear-gradient(135deg, rgba(99,102,241,0.08), rgba(236,72,153,0.04))', padding: '18px 20px',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)' as any,
border: '1px solid rgba(255,255,255,0.06)',
borderRadius: 20, padding: '16px 18px',
display: 'flex', flexDirection: 'column', gap: 14, display: 'flex', flexDirection: 'column', gap: 14,
position: 'relative', overflow: 'hidden', height: '100%',
}}> }}>
{/* Glow */}
<div style={{
position: 'absolute', top: -60, left: '50%', transform: 'translateX(-50%)',
width: 280, height: 120, borderRadius: '50%',
background: 'radial-gradient(ellipse, rgba(99,102,241,0.25) 0%, transparent 60%)',
opacity: 0.35, pointerEvents: 'none',
}} />
{/* Header */} {/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, position: 'relative', zIndex: 1 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ <div style={{
width: 32, height: 32, borderRadius: 10, width: 36, height: 36, borderRadius: 11,
background: 'rgba(255,255,255,0.06)', background: 'var(--surface-2)',
border: '1px solid var(--border-subtle)',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--text-primary)', flexShrink: 0, color: 'var(--text-secondary)', flexShrink: 0,
}}> }}>
<Train size={16} /> <Train size={17} />
</div> </div>
<div style={{ minWidth: 0, flex: 1 }}> <div style={{ minWidth: 0, flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-primary)', letterSpacing: '-0.2px' }}> <div style={{
fontSize: 10, color: 'var(--text-tertiary)', fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 2,
}}>
Трамвай Трамвай
</div> </div>
<div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <div style={{
fontSize: 14, fontWeight: 700, color: 'var(--text-primary)',
letterSpacing: '-0.2px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
Ул. Антонова-Овсеенко Ул. Антонова-Овсеенко
</div> </div>
</div> </div>
@@ -170,7 +157,8 @@ export default function TransportWidget() {
display: 'grid', display: 'grid',
gridTemplateColumns: '58px 1fr 1fr', gridTemplateColumns: '58px 1fr 1fr',
gap: 10, gap: 10,
position: 'relative', zIndex: 1, paddingBottom: 6,
borderBottom: '1px solid var(--hairline)',
}}> }}>
<div /> <div />
{DIRECTIONS.map(d => ( {DIRECTIONS.map(d => (
@@ -180,7 +168,7 @@ export default function TransportWidget() {
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1.1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1.1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{d.short} {d.short}
</div> </div>
<div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginTop: 1 }}> <div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginTop: 1, fontWeight: 500 }}>
{d.sub} {d.sub}
</div> </div>
</div> </div>
@@ -188,8 +176,8 @@ export default function TransportWidget() {
))} ))}
</div> </div>
{/* Rows: one per route */} {/* Rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, position: 'relative', zIndex: 1 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}>
{ROUTES.map(route => ( {ROUTES.map(route => (
<div key={route.num} style={{ <div key={route.num} style={{
display: 'grid', display: 'grid',
@@ -197,15 +185,14 @@ export default function TransportWidget() {
gap: 10, gap: 10,
alignItems: 'stretch', alignItems: 'stretch',
}}> }}>
{/* Route badge */}
<div style={{ <div style={{
background: route.bg, background: route.bg,
boxShadow: `0 6px 16px ${route.color}35`, boxShadow: `0 6px 16px -4px ${route.color}55`,
borderRadius: 12, borderRadius: 13,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'white', fontWeight: 800, fontSize: 20, color: 'white', fontWeight: 800, fontSize: 22,
letterSpacing: '-1px', letterSpacing: '-1.5px',
minHeight: 52, minHeight: 56,
}}> }}>
{route.num} {route.num}
</div> </div>
@@ -218,7 +205,7 @@ export default function TransportWidget() {
key={`${route.num}-${d.stopId}-${arrivals.map(a => a.minutes).join(',')}`} key={`${route.num}-${d.stopId}-${arrivals.map(a => a.minutes).join(',')}`}
initial={{ opacity: 0, y: 4 }} initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.25 }}
> >
<Cell arrivals={arrivals} color={route.color} /> <Cell arrivals={arrivals} color={route.color} />
</motion.div> </motion.div>
@@ -230,7 +217,7 @@ export default function TransportWidget() {
</div> </div>
{loading && Object.keys(data).length === 0 && ( {loading && Object.keys(data).length === 0 && (
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', textAlign: 'center', position: 'relative', zIndex: 1 }}> <div style={{ fontSize: 11, color: 'var(--text-tertiary)', textAlign: 'center', marginTop: 'auto' }}>
Загрузка расписания... Загрузка расписания...
</div> </div>
)} )}