From 121bf30ab1859c22b68e4178bd801694e909c201 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 23 Apr 2026 08:30:03 +0000 Subject: [PATCH] redesign: bento home + semantic tokens + solid cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/globals.css | 342 +++++++++++++++---------------- app/page.tsx | 359 ++++++++++++++++++++++----------- components/TransportWidget.tsx | 135 ++++++------- 3 files changed, 461 insertions(+), 375 deletions(-) diff --git a/app/globals.css b/app/globals.css index f3ab2d9..5c4266c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,34 +4,101 @@ padding: 0; } +/* —————————————————————————————— + Tokens — dark (default) + —————————————————————————————— */ :root { - --bg: #0c0c18; - --bg-secondary: #12121f; - --sidebar-bg: rgba(12, 12, 24, 0.8); - --card-bg: rgba(255, 255, 255, 0.04); - --card-bg-hover: rgba(255, 255, 255, 0.07); - --card-border: rgba(255, 255, 255, 0.07); - --card-border-hover: rgba(255, 255, 255, 0.12); + /* Surfaces */ + --bg: #0b0b14; + --bg-secondary: #101020; + --surface-1: #15152a; + --surface-2: #1c1c36; + --surface-3: #242444; + --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-secondary: rgba(255, 255, 255, 0.45); - --text-tertiary: rgba(255, 255, 255, 0.25); + --text-secondary: rgba(255, 255, 255, 0.6); + --text-tertiary: rgba(255, 255, 255, 0.38); + + /* Accents */ --accent: #818cf8; --accent-secondary: #22d3ee; - --accent-glow: rgba(129, 140, 248, 0.15); - --glass: rgba(255, 255, 255, 0.03); - --glass-border: rgba(255, 255, 255, 0.06); + --accent-glow: rgba(129, 140, 248, 0.22); + + /* Brand gradients */ --gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6); --gradient-warm: linear-gradient(135deg, #f59e0b, #ef4444); --gradient-cool: linear-gradient(135deg, #06b6d4, #3b82f6); --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; --off-color: rgba(255, 255, 255, 0.15); + + /* Radius */ + --radius-xs: 8px; --radius-sm: 12px; --radius-md: 16px; --radius-lg: 22px; --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 { background: var(--bg); color: var(--text-primary); @@ -40,13 +107,14 @@ html, body { overflow: hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + transition: background 0.3s ease, color 0.3s ease; } -#__next, main { - height: 100%; -} +#__next, main { height: 100%; } -/* Ambient background orbs */ +/* —————————————————————————————— + Aurora + —————————————————————————————— */ .bg-ambient { position: fixed; inset: 0; @@ -54,27 +122,27 @@ html, body { overflow: hidden; pointer-events: none; } -.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::before, .bg-ambient::after { content: ''; position: absolute; - width: 500px; - height: 500px; border-radius: 50%; - background: radial-gradient(circle, rgba(139, 92, 246, 0.06) 0%, transparent 70%); - bottom: -150px; - left: -50px; - animation: float2 25s ease-in-out infinite; + filter: blur(80px); +} +.bg-ambient::before { + 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 { @@ -88,21 +156,36 @@ html, body { 66% { transform: translate(20px, -10px) scale(0.9); } } -/* Glass card base */ -.glass-card { - background: var(--card-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--card-border); +/* —————————————————————————————— + Cards + —————————————————————————————— */ +.card { + background: var(--surface-1); + border: 1px solid var(--border-subtle); 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 { - background: var(--card-bg-hover); - border-color: var(--card-border-hover); +.card-raised { + background: var(--surface-1); + 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 { background: var(--gradient-primary); -webkit-background-clip: text; @@ -118,125 +201,48 @@ button { -webkit-tap-highlight-color: transparent; touch-action: manipulation; font-family: inherit; + color: inherit; } - button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } -/* Smooth scrollbar */ -::-webkit-scrollbar { - width: 4px; - height: 4px; -} -::-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); -} +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); } -/* Pulse animation for active devices */ +/* —————————————————————————————— + Motion + —————————————————————————————— */ @keyframes pulse-glow { - 0%, 100% { box-shadow: 0 0 20px rgba(129, 140, 248, 0.15); } - 50% { box-shadow: 0 0 30px rgba(129, 140, 248, 0.25); } + 0%, 100% { box-shadow: 0 0 20px var(--accent-glow); } + 50% { box-shadow: 0 0 30px var(--accent-glow); } } - -/* Slide in animation */ @keyframes slideUp { from { opacity: 0; transform: translateY(10px); } 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 { - 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 fan-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes light-pulse { 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)); } } +@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 { - 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; -} - -/* ————— 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 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 { 0% { transform: translateY(0); opacity: 0.7; } 80% { opacity: 0.7; } 100% { transform: translateY(16px); opacity: 0; } } - @keyframes snow-fall { 0% { transform: translateY(0) translateX(0); opacity: 0.9; } 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; } 100% { transform: translateY(22px) translateX(0); opacity: 0; } } - @keyframes thunder-flash { 0%, 100% { opacity: 0; } 5%, 7% { opacity: 1; } @@ -252,52 +257,21 @@ button:focus-visible { 50%, 52% { opacity: 0.8; } 51% { opacity: 0.2; } } - @keyframes fog-drift { 0%, 100% { transform: translateX(0); opacity: 0.4; } 50% { transform: translateX(8px); opacity: 0.6; } } -/* ————— Dynamic weather backgrounds ————— */ -.weather-bg-clear { - --orb1-color: rgba(251, 191, 36, 0.08); - --orb2-color: rgba(245, 158, 11, 0.05); -} -.weather-bg-cloudy { - --orb1-color: rgba(148, 163, 184, 0.08); - --orb2-color: rgba(100, 116, 139, 0.06); -} -.weather-bg-rain { - --orb1-color: rgba(59, 130, 246, 0.08); - --orb2-color: rgba(30, 64, 175, 0.06); -} -.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%); -} +/* Weather ambient tints via accent-glow */ +.weather-bg-clear { --accent-glow: rgba(251, 191, 36, 0.18); } +.weather-bg-cloudy { --accent-glow: rgba(148, 163, 184, 0.18); } +.weather-bg-rain { --accent-glow: rgba(59, 130, 246, 0.18); } +.weather-bg-snow { --accent-glow: rgba(186, 230, 253, 0.22); } +.weather-bg-thunder { --accent-glow: rgba(139, 92, 246, 0.22); } +.weather-bg-night { --accent-glow: rgba(67, 56, 202, 0.16); } +.light.weather-bg-clear { --accent-glow: rgba(234, 169, 27, 0.16); } +.light.weather-bg-cloudy { --accent-glow: rgba(100, 116, 139, 0.14); } +.light.weather-bg-rain { --accent-glow: rgba(37, 99, 235, 0.14); } +.light.weather-bg-snow { --accent-glow: rgba(125, 211, 252, 0.22); } +.light.weather-bg-thunder { --accent-glow: rgba(109, 40, 217, 0.14); } +.light.weather-bg-night { --accent-glow: rgba(55, 48, 163, 0.14); } diff --git a/app/page.tsx b/app/page.tsx index b5bf8b7..d8b4f4e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -419,94 +419,199 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S return ( -
- {/* Greeting + hint */} -
-

- {greeting} 👋 +
+ {/* ───── Greeting row ───── */} +
+

+ {greeting} 👋

+
+ {new Date().toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' })} +
- {/* Weather — full width compact */} - {weather && ( -
+ + {/* Hero weather card */} + {weather ? ( +
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 */} +
+ +
+ +
+ Сейчас +
+ +
+
+ {weather.temp}° +
+ +
+ +
+ {weather.desc} +
+ +
+ {weather.feelsLike && ( +
+
Ощущается
+
{weather.feelsLike}°
+
+ )} + {weather.humidity && ( +
+
Влажность
+
{weather.humidity}
+
+ )} + {weather.windSpeed && ( +
+
Ветер
+
{weather.windSpeed}
+
+ )} +
+
+ ) : ( +
+ Загрузка погоды... +
+ )} + + {/* Tram */} + +
+ + {/* ───── Forecast band (no cards, hairline-separated) ───── */} + {weather?.forecast && ( +
-
- -
- - {/* Current */} -
weather?.forecast?.[0] && setSelectedDay(weather.forecast[0])} style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1, cursor: 'pointer' }}> - -
-
{weather.temp}°
-
{weather.desc}
-
-
- - {/* Divider */} -
- - {/* 7 day forecast */} - {weather.forecast && ( -
- {weather.forecast.map((day, idx) => { - const d = new Date(day.date) - const isToday = idx === 0 - return ( -
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', + {weather.forecast.map((day, idx) => { + const d = new Date(day.date) + const isToday = idx === 0 + return ( +
+
- )} +
{getWeatherIcon(day.desc)}
+
+ {day.maxTemp}° +
+
+ {day.minTemp}° +
+ +
+ ) + })}
)} - {/* Transport: tram arrivals at Ул. Антонова-Овсеенко, both directions */} - - - {/* Two columns: Events + Notes */} + {/* ───── Events + Notes row ───── */}
- {/* Left: Today + Tomorrow events */} -
+ {/* Events — today + tomorrow in one card */} +
{/* Today */} -
-
- - Сегодня - {todayEvents.length} +
+
+ + Сегодня + + {todayEvents.length} +
{calLoading ? ( -
Загрузка...
+
Загрузка...
) : todayEvents.length === 0 ? ( -
Свободный день
+
Свободный день ✨
) : (
{todayEvents.map(ev => ( -
-
+
+
{ev.title}
- {ev.allDay ? 'Весь день' : formatEventTime(ev.start)} · {ev.ownerName} + {ev.allDay ? 'Весь день' : formatEventTime(ev.start)} · {ev.ownerName}
@@ -515,23 +620,34 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S )}
+ {/* Divider */} + {(tomorrowEvents.length > 0 || todayEvents.length > 0) && ( +
+ )} + {/* Tomorrow */} -
-
- - Завтра - {tomorrowEvents.length} +
+
+ + Завтра + + {tomorrowEvents.length} +
{tomorrowEvents.length === 0 ? ( -
Нет событий
+
) : ( -
+
{tomorrowEvents.map(ev => ( -
-
+
+
{ev.title}
-
+
{ev.allDay ? 'Весь день' : formatEventTime(ev.start)}
@@ -542,53 +658,62 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
- {/* Right: Pinned notes / shopping lists */} -
+ {/* Notes */} +
+
+ + Заметки +
+ {pinnedNotes.length === 0 ? ( -
-
- -
Заметки появятся здесь
+
+
+ +
Заметки появятся здесь
) : ( - pinnedNotes.map(note => { - const doneCount = note.items?.filter((i: any) => i.done).length || 0 - const totalCount = note.items?.length || 0 - return ( -
-
- {note.type === 'shopping' ? : } - {note.title} - {note.type === 'shopping' && totalCount > 0 && ( - {doneCount}/{totalCount} - )} -
- {note.type === 'shopping' ? ( -
- {(note.items || []).filter((i: any) => !i.done).slice(0, 5).map((item: any) => ( -
-
- {item.text} -
- ))} - {(note.items || []).filter((i: any) => !i.done).length > 5 && ( -
- +{(note.items || []).filter((i: any) => !i.done).length - 5} ещё -
+
+ {pinnedNotes.map(note => { + const doneCount = note.items?.filter((i: any) => i.done).length || 0 + const totalCount = note.items?.length || 0 + return ( +
+
+ {note.type === 'shopping' ? : } + {note.title} + {note.type === 'shopping' && totalCount > 0 && ( + {doneCount}/{totalCount} )}
- ) : ( -
- {note.text || 'Пустая заметка'} -
- )} -
- ) - }) + {note.type === 'shopping' ? ( +
+ {(note.items || []).filter((i: any) => !i.done).slice(0, 4).map((item: any) => ( +
+
+ {item.text} +
+ ))} + {(note.items || []).filter((i: any) => !i.done).length > 4 && ( +
+ +{(note.items || []).filter((i: any) => !i.done).length - 4} ещё +
+ )} +
+ ) : ( +
+ {note.text || 'Пустая заметка'} +
+ )} +
+ ) + })} +
)}
diff --git a/components/TransportWidget.tsx b/components/TransportWidget.tsx index 0942abe..5c746b8 100644 --- a/components/TransportWidget.tsx +++ b/components/TransportWidget.tsx @@ -22,16 +22,11 @@ const DIRECTIONS: Direction[] = [ ] const ROUTES: { num: string; color: string; bg: string }[] = [ - { num: '23', color: '#34d399', bg: 'linear-gradient(135deg, #10b981, #059669)' }, - { num: '27', color: '#60a5fa', bg: 'linear-gradient(135deg, #3b82f6, #2563eb)' }, - { num: '39', color: '#f87171', bg: 'linear-gradient(135deg, #ef4444, #dc2626)' }, + { num: '23', color: '#10b981', bg: 'linear-gradient(135deg, #10b981, #059669)' }, + { num: '27', color: '#3b82f6', bg: 'linear-gradient(135deg, #3b82f6, #2563eb)' }, + { 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 }) { const sorted = [...arrivals].sort((a, b) => a.minutes - b.minutes).slice(0, 3) if (sorted.length === 0) { @@ -39,10 +34,10 @@ function Cell({ arrivals, color }: { arrivals: Arrival[]; color: string }) {
) } @@ -50,49 +45,46 @@ function Cell({ arrivals, color }: { arrivals: Arrival[]; color: string }) { const imminent = first.minutes <= 2 return (
- {/* Primary time */} + {/* Primary time — big */}
{first.minutes <= 0 ? 'сейчас' : first.minutes}
{first.minutes > 0 && ( -
мин
+
мин
)}
- {/* Divider */} {rest.length > 0 && ( -
- )} - - {/* Next arrivals */} - {rest.length > 0 && ( -
-
- затем + <> +
+
+
+ затем +
+
+ {rest.map(r => r.minutes <= 0 ? 'сейчас' : `${r.minutes} мин`).join(' · ')} +
-
- {rest.map(r => formatMinutes(r.minutes)).join(' · ')} -
-
+ )}
) @@ -128,38 +120,33 @@ export default function TransportWidget() { }, []) return ( -
- {/* Glow */} -
- {/* Header */} -
+
- +
-
+
Трамвай
-
+
Ул. Антонова-Овсеенко
@@ -170,7 +157,8 @@ export default function TransportWidget() { display: 'grid', gridTemplateColumns: '58px 1fr 1fr', gap: 10, - position: 'relative', zIndex: 1, + paddingBottom: 6, + borderBottom: '1px solid var(--hairline)', }}>
{DIRECTIONS.map(d => ( @@ -180,7 +168,7 @@ export default function TransportWidget() {
{d.short}
-
+
{d.sub}
@@ -188,8 +176,8 @@ export default function TransportWidget() { ))}
- {/* Rows: one per route */} -
+ {/* Rows */} +
{ROUTES.map(route => (
- {/* Route badge */}
{route.num}
@@ -218,7 +205,7 @@ export default function TransportWidget() { key={`${route.num}-${d.stopId}-${arrivals.map(a => a.minutes).join(',')}`} initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} - transition={{ duration: 0.2 }} + transition={{ duration: 0.25 }} > @@ -230,7 +217,7 @@ export default function TransportWidget() {
{loading && Object.keys(data).length === 0 && ( -
+
Загрузка расписания...
)}