feat: redesigned Home (weather+forecast bar, today+tomorrow, pinned notes), fix snow animation, scrollable weather modal, weather hints
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
Cosmo
2026-04-22 20:58:05 +00:00
parent 4bcfff775c
commit bce9578fa1
5 changed files with 229 additions and 109 deletions

View File

@@ -11,6 +11,7 @@ interface Note {
items?: { id: string; text: string; done: boolean }[] items?: { id: string; text: string; done: boolean }[]
text?: string text?: string
color: string color: string
pinDate: string | null
createdAt: string createdAt: string
updatedAt: string updatedAt: string
} }
@@ -42,6 +43,7 @@ export async function POST(req: Request) {
items: body.type === 'shopping' ? [] : undefined, items: body.type === 'shopping' ? [] : undefined,
text: body.type === 'note' ? '' : undefined, text: body.type === 'note' ? '' : undefined,
color: body.color || '#6366f1', color: body.color || '#6366f1',
pinDate: body.pinDate || null,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
} }

View File

@@ -238,9 +238,11 @@ button:focus-visible {
} }
@keyframes snow-fall { @keyframes snow-fall {
0% { transform: translateY(0) rotate(0deg); opacity: 0.8; } 0% { transform: translateY(0) translateX(0); opacity: 0.9; }
50% { transform: translateY(10px) translateX(3px) rotate(180deg); opacity: 0.6; } 25% { transform: translateY(5px) translateX(2px); opacity: 0.8; }
100% { transform: translateY(20px) rotate(360deg); opacity: 0; } 50% { transform: translateY(10px) translateX(-1px); opacity: 0.7; }
75% { transform: translateY(15px) translateX(3px); opacity: 0.4; }
100% { transform: translateY(22px) translateX(0); opacity: 0; }
} }
@keyframes thunder-flash { @keyframes thunder-flash {

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { Thermometer, Droplets, Wind, Calendar, Lock, Settings as SettingsIcon, LogOut, Delete, KeyRound, MapPin, Info, Check, X as XIcon } from 'lucide-react' import { Thermometer, Droplets, Wind, Calendar, Lock, Settings as SettingsIcon, LogOut, Delete, KeyRound, MapPin, Info, Check, X as XIcon, StickyNote, ShoppingCart, FileText } from 'lucide-react'
import Sidebar from '@/components/Sidebar' import Sidebar from '@/components/Sidebar'
import TopBar from '@/components/TopBar' import TopBar from '@/components/TopBar'
import RoomTabs from '@/components/RoomTabs' import RoomTabs from '@/components/RoomTabs'
@@ -287,8 +287,10 @@ function LockScreen({ onUnlock }: { onUnlock: () => void }) {
// ————— Home Tab ————— // ————— Home Tab —————
function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: SensorData | null }) { function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: SensorData | null }) {
const [todayEvents, setTodayEvents] = useState<CalendarEvent[]>([]) const [todayEvents, setTodayEvents] = useState<CalendarEvent[]>([])
const [tomorrowEvents, setTomorrowEvents] = useState<CalendarEvent[]>([])
const [calLoading, setCalLoading] = useState(true) const [calLoading, setCalLoading] = useState(true)
const [greeting, setGreeting] = useState(getGreeting()) const [greeting, setGreeting] = useState(getGreeting())
const [pinnedNotes, setPinnedNotes] = useState<any[]>([])
useEffect(() => { useEffect(() => {
fetch('/api/calendar?range=today') fetch('/api/calendar?range=today')
@@ -296,6 +298,28 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
.then(d => setTodayEvents(d.events || [])) .then(d => setTodayEvents(d.events || []))
.catch(() => setTodayEvents([])) .catch(() => setTodayEvents([]))
.finally(() => setCalLoading(false)) .finally(() => setCalLoading(false))
// Tomorrow
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
const y = tomorrow.getFullYear()
const m = tomorrow.getMonth()
fetch(`/api/calendar?range=month&year=${y}&month=${m}`)
.then(r => r.json())
.then(d => {
const tmr = (d.events || []).filter((e: any) => {
const ed = new Date(e.start)
return ed.getDate() === tomorrow.getDate() && ed.getMonth() === tomorrow.getMonth()
})
setTomorrowEvents(tmr)
})
.catch(() => {})
// Notes
fetch('/api/notes')
.then(r => r.json())
.then(d => setPinnedNotes((d.notes || []).slice(0, 3)))
.catch(() => {})
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -303,115 +327,207 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
return () => clearInterval(t) return () => clearInterval(t)
}, []) }, [])
const pm25Info = sensors ? getPm25Level(sensors.pm25) : null // Weather hint
const getWeatherHint = (): string | null => {
if (!weather) return null
const desc = weather.desc.toLowerCase()
const temp = parseInt(weather.temp)
if (desc.includes('дождь') || desc.includes('ливен') || desc.includes('морос')) return '☂️ Не забудьте зонт'
if (desc.includes('снег')) return '🧤 На улице снег, одевайтесь теплее'
if (desc.includes('гроз')) return '⛈️ Ожидается гроза'
if (temp <= 0) return '🥶 На улице мороз'
if (temp <= 5) return '🧥 Оденьтесь потеплее'
if (temp >= 30) return '🥵 Очень жарко, пейте воду'
return null
}
const hint = getWeatherHint()
return ( return (
<div style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, padding: '20px 24px 28px', display: 'flex', flexDirection: 'column', gap: 16 }}> <div style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, padding: '20px 24px 28px', display: 'flex', flexDirection: 'column', gap: 14 }}>
{/* Greeting */} {/* Greeting + hint */}
<div style={{ marginBottom: 4 }}> <div>
<h1 style={{ fontSize: 28, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-0.5px', margin: 0 }}> <h1 style={{ fontSize: 26, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-0.5px', margin: 0 }}>
{greeting} 👋 {greeting} 👋
</h1> </h1>
<p style={{ fontSize: 14, color: 'var(--text-secondary)', marginTop: 4, fontWeight: 400 }}> {hint && (
Вот что происходит дома <div style={{
</p> fontSize: 14, color: 'var(--text-primary)', marginTop: 8,
padding: '10px 16px', borderRadius: 12,
background: 'rgba(251,191,36,0.08)', border: '1px solid rgba(251,191,36,0.15)',
fontWeight: 500,
}}>
{hint}
</div>
)}
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}> {/* Weather — full width compact */}
{weather && ( {weather && (
<div style={{ <div style={{
background: 'linear-gradient(135deg, rgba(99,102,241,0.12), rgba(139,92,246,0.06))', background: 'linear-gradient(135deg, rgba(99,102,241,0.1), rgba(139,92,246,0.05))',
backdropFilter: 'blur(20px)', border: '1px solid rgba(129,140,248,0.12)', backdropFilter: 'blur(20px)', border: '1px solid rgba(129,140,248,0.1)',
borderRadius: 22, padding: '22px 24px', position: 'relative', overflow: 'hidden', borderRadius: 20, padding: '18px 22px',
}}> display: 'flex', alignItems: 'center', gap: 20,
<div style={{ position: 'absolute', top: -20, right: -10, fontSize: 80, opacity: 0.12, pointerEvents: 'none' }}>{getWeatherIcon(weather.desc)}</div> position: 'relative', overflow: 'hidden',
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, marginBottom: 16 }}>Погода</div> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 18, position: 'relative', zIndex: 1 }}> <div style={{ position: 'absolute', top: -15, right: 5, opacity: 0.1, pointerEvents: 'none' }}>
<WeatherAnimation condition={weather.desc} size={56} /> <WeatherAnimation condition={weather.desc} size={90} />
<div> </div>
<div style={{ fontSize: 36, fontWeight: 800, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div>
<div style={{ fontSize: 14, color: 'var(--text-secondary)', marginTop: 4, fontWeight: 500 }}>{weather.desc}</div> {/* Current */}
</div> <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1 }}>
<WeatherAnimation condition={weather.desc} size={48} />
<div>
<div style={{ fontSize: 32, fontWeight: 800, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div>
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 3, fontWeight: 500 }}>{weather.desc}</div>
</div> </div>
{weather.forecast && weather.forecast.length > 0 && ( </div>
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', WebkitOverflowScrolling: 'touch' as any, scrollbarWidth: 'none' as any, msOverflowStyle: 'none' as any, paddingBottom: 2 }}>
{weather.forecast.map((day, idx) => { {/* Divider */}
const d = new Date(day.date) <div style={{ width: 1, height: 50, background: 'rgba(255,255,255,0.08)', flexShrink: 0 }} />
const isToday = idx === 0
return ( {/* 7 day forecast */}
<div key={day.date} style={{ {weather.forecast && (
minWidth: 58, background: isToday ? 'rgba(99,102,241,0.1)' : 'rgba(255,255,255,0.04)', <div style={{ display: 'flex', gap: 4, flex: 1, overflow: 'hidden' }}>
borderRadius: 12, padding: '8px 6px', textAlign: 'center', {weather.forecast.map((day, idx) => {
border: isToday ? '1px solid rgba(129,140,248,0.2)' : '1px solid rgba(255,255,255,0.04)', const d = new Date(day.date)
flexShrink: 0, const isToday = idx === 0
}}> return (
<div style={{ fontSize: 9, color: isToday ? '#a5b4fc' : 'var(--text-secondary)', textTransform: 'capitalize', marginBottom: 3, fontWeight: 600 }}> <div key={day.date} style={{
{isToday ? 'Сегодня' : d.toLocaleDateString('ru-RU', { weekday: 'short' })} flex: 1, minWidth: 0, textAlign: 'center', padding: '4px 2px',
</div> borderRadius: 10,
<div style={{ fontSize: 16, marginBottom: 3 }}>{getWeatherIcon(day.desc)}</div> background: isToday ? 'rgba(99,102,241,0.1)' : 'transparent',
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-primary)' }}>{day.maxTemp}°</div> }}>
<div style={{ fontSize: 10, color: 'var(--text-secondary)' }}>{day.minTemp}°</div> <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' }).slice(0, 2)}
</div> </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>
)}
{/* Two columns: Events + Notes */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, flex: 1, minHeight: 0 }}>
{/* Left: Today + Tomorrow events */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{/* 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 style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Calendar size={14} color="var(--text-secondary)" />
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>Сегодня</span>
<span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{todayEvents.length}</span>
</div>
{calLoading ? (
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>Загрузка...</div>
) : todayEvents.length === 0 ? (
<div style={{ fontSize: 14, color: 'var(--text-secondary)', textAlign: 'center', padding: '8px 0' }}>Свободный день</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{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 style={{ width: 3, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 30, flexShrink: 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: 11, color: 'var(--text-secondary)', marginTop: 2 }}>
{ev.allDay ? 'Весь день' : formatEventTime(ev.start)} · <span style={{ color: ev.color }}>{ev.ownerName}</span>
</div>
</div>
</div>
))}
</div> </div>
)} )}
</div> </div>
)}
{sensors && ( {/* Tomorrow */}
<div style={{ background: 'rgba(255,255,255,0.03)', backdropFilter: 'blur(20px)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}> <div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.04)', borderRadius: 20, padding: '18px 20px' }}>
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, marginBottom: 16 }}>Климат в квартире</div> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}> <Calendar size={14} color="var(--text-secondary)" />
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}> <span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>Завтра</span>
<div style={{ width: 48, height: 48, borderRadius: 16, background: 'linear-gradient(135deg, rgba(251,146,60,0.15), rgba(245,158,11,0.08))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Thermometer size={22} color="#fb923c" /></div> <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{tomorrowEvents.length}</span>
<div><div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1 }}>{sensors.temperature}°C</div><div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>Температура</div></div> </div>
{tomorrowEvents.length === 0 ? (
<div style={{ fontSize: 14, color: 'var(--text-secondary)', textAlign: 'center', padding: '8px 0' }}>Нет событий</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{tomorrowEvents.map(ev => (
<div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 12, background: `${ev.color}06` }}>
<div style={{ width: 3, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 24, flexShrink: 0, opacity: 0.6 }} />
<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: 11, color: 'var(--text-secondary)', marginTop: 2 }}>
{ev.allDay ? 'Весь день' : formatEventTime(ev.start)}
</div>
</div>
</div>
))}
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}> )}
<div style={{ width: 48, height: 48, borderRadius: 16, background: 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(99,102,241,0.08))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Droplets size={22} color="#3b82f6" /></div> </div>
<div><div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1 }}>{sensors.humidity}%</div><div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>Влажность</div></div> </div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}> {/* Right: Pinned notes / shopping lists */}
<div style={{ width: 48, height: 48, borderRadius: 16, background: pm25Info?.bg || 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Wind size={22} color={pm25Info?.color || '#999'} /></div> <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ flex: 1 }}> {pinnedNotes.length === 0 ? (
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}><span style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1 }}>{sensors.pm25}</span><span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>µg/m³</span></div> <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={{ fontSize: 12, color: pm25Info?.color, marginTop: 2, fontWeight: 500 }}>PM2.5 · {pm25Info?.label}</div> <div style={{ textAlign: 'center', color: 'var(--text-secondary)' }}>
</div> <StickyNote size={24} style={{ margin: '0 auto 8px', opacity: 0.3 }} />
<div style={{ fontSize: 13 }}>Заметки появятся здесь</div>
</div> </div>
</div> </div>
</div> ) : (
)} pinnedNotes.map(note => {
</div> const doneCount = note.items?.filter((i: any) => i.done).length || 0
const totalCount = note.items?.length || 0
<div style={{ background: 'rgba(255,255,255,0.03)', backdropFilter: 'blur(20px)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}> return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}> <div key={note.id} style={{
<Calendar size={15} color="var(--text-secondary)" /> background: `${note.color}08`, border: `1px solid ${note.color}15`,
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600 }}>Сегодня</span> borderRadius: 20, padding: '18px 20px',
</div> }}>
{calLoading ? ( <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>Загрузка...</div> {note.type === 'shopping' ? <ShoppingCart size={14} color={note.color} /> : <FileText size={14} color={note.color} />}
) : todayEvents.length === 0 ? ( <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{note.title}</span>
<div style={{ fontSize: 15, color: 'var(--text-secondary)', textAlign: 'center', padding: '12px 0' }}>Нет событий на сегодня</div> {note.type === 'shopping' && totalCount > 0 && (
) : ( <span style={{ fontSize: 11, color: note.color, marginLeft: 'auto' }}>{doneCount}/{totalCount}</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> )}
{todayEvents.map(ev => (
<div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '12px 16px', borderRadius: 14, background: `${ev.color}0a`, border: `1px solid ${ev.color}18` }}>
<div style={{ width: 4, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 36, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 3, display: 'flex', gap: 8 }}>
<span>{ev.allDay ? 'Весь день' : `${formatEventTime(ev.start)}${formatEventTime(ev.end)}`}</span>
<span style={{ color: ev.color, fontWeight: 500 }}>{ev.ownerName}</span>
</div> </div>
{note.type === 'shopping' ? (
<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 style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.5, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 4, WebkitBoxOrient: 'vertical' as any }}>
{note.text || 'Пустая заметка'}
</div>
)}
</div> </div>
</div> )
))} })
</div> )}
)} </div>
</div> </div>
</div> </div>
) )
} }
// ————— Settings Tab ————— // ————— Settings Tab —————
function SettingsTab({ city, onCityChange, onLogout, theme, onThemeChange }: { city: string; onCityChange: (id: string) => void; onLogout: () => void; theme: string; onThemeChange: (t: string) => void }) { function SettingsTab({ city, onCityChange, onLogout, theme, onThemeChange }: { city: string; onCityChange: (id: string) => void; onLogout: () => void; theme: string; onThemeChange: (t: string) => void }) {
const [showPinChange, setShowPinChange] = useState(false) const [showPinChange, setShowPinChange] = useState(false)

View File

@@ -161,6 +161,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
position: 'fixed', inset: 0, position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(12px)', background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(12px)',
zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center',
overflowY: 'auto', padding: 20,
}} }}
> >
<div <div
@@ -168,7 +169,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
style={{ style={{
background: 'rgba(16,16,30,0.97)', backdropFilter: 'blur(40px)', background: 'rgba(16,16,30,0.97)', backdropFilter: 'blur(40px)',
border: '1px solid rgba(255,255,255,0.07)', borderRadius: 28, border: '1px solid rgba(255,255,255,0.07)', borderRadius: 28,
width: 480, maxWidth: '95vw', overflow: 'hidden', width: 480, maxWidth: '95vw', maxHeight: '90vh', overflow: 'auto',
boxShadow: '0 30px 90px rgba(0,0,0,0.6)', boxShadow: '0 30px 90px rgba(0,0,0,0.6)',
}} }}
> >

View File

@@ -106,23 +106,22 @@ export default function WeatherAnimation({ condition, size = 64 }: WeatherAnimat
{c === 'snow' && ( {c === 'snow' && (
<g> <g>
{[ {[
{ x: 42, delay: 0 }, { x: 44, delay: 0, size: 3 },
{ x: 52, delay: 0.4 }, { x: 54, delay: 0.5, size: 2.5 },
{ x: 62, delay: 0.8 }, { x: 64, delay: 1.0, size: 2 },
{ x: 47, delay: 1.2 }, { x: 49, delay: 1.5, size: 2.5 },
{ x: 57, delay: 0.2 }, { x: 59, delay: 0.3, size: 3 },
].map((flake, i) => ( ].map((flake, i) => (
<circle <g key={i} style={{ animation: `snow-fall 2.5s linear infinite`, animationDelay: `${flake.delay}s` }}>
key={i} <text
cx={flake.x} cy={72} x={flake.x} y={70}
r={2.5} fontSize={flake.size * 4}
fill="white" fill="white"
opacity={0.8} opacity={0.85}
style={{ textAnchor="middle"
animation: `snow-fall 2s ease-in-out infinite`, dominantBaseline="central"
animationDelay: `${flake.delay}s`, ></text>
}} </g>
/>
))} ))}
</g> </g>
)} )}