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

@@ -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 }) {
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: '10px 14px', borderRadius: 12,
background: 'rgba(255,255,255,0.02)',
border: '1px dashed rgba(255,255,255,0.06)',
background: 'var(--surface-2)',
border: '1px dashed var(--border-subtle)',
color: 'var(--text-tertiary)', fontSize: 13, fontWeight: 500,
minHeight: 52,
minHeight: 56,
}}></div>
)
}
@@ -50,49 +45,46 @@ function Cell({ arrivals, color }: { arrivals: Arrival[]; color: string }) {
const imminent = first.minutes <= 2
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '8px 14px', borderRadius: 12,
background: imminent ? `${color}15` : 'rgba(255,255,255,0.03)',
border: `1px solid ${imminent ? color + '35' : 'rgba(255,255,255,0.06)'}`,
minHeight: 52,
transition: 'all 0.25s ease',
display: 'flex', alignItems: 'center', gap: 12,
padding: '10px 14px', borderRadius: 12,
background: imminent ? `color-mix(in srgb, ${color} 10%, var(--surface-2))` : 'var(--surface-2)',
border: `1px solid ${imminent ? color : 'var(--border-subtle)'}`,
minHeight: 56,
transition: 'all 0.3s ease',
}}>
{/* Primary time */}
{/* Primary time — big */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, flexShrink: 0 }}>
<div style={{
fontSize: first.minutes <= 0 ? 16 : 24,
fontWeight: 800, letterSpacing: '-1px', lineHeight: 1,
fontSize: first.minutes <= 0 ? 18 : 32,
fontWeight: 800, letterSpacing: '-1.5px', lineHeight: 1,
color: imminent ? color : 'var(--text-primary)',
fontVariantNumeric: 'tabular-nums',
}}>
{first.minutes <= 0 ? 'сейчас' : first.minutes}
</div>
{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>
{/* Divider */}
{rest.length > 0 && (
<div style={{ width: 1, alignSelf: 'stretch', background: 'rgba(255,255,255,0.06)' }} />
)}
{/* Next arrivals */}
{rest.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 0 }}>
<div style={{
fontSize: 10, color: 'var(--text-tertiary)', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.06em',
}}>
затем
<>
<div style={{ width: 1, alignSelf: 'stretch', background: 'var(--hairline)', margin: '4px 0' }} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, flex: 1, minWidth: 0 }}>
<div style={{
fontSize: 9, color: 'var(--text-tertiary)', fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.08em',
}}>
затем
</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 style={{
fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
}}>
{rest.map(r => formatMinutes(r.minutes)).join(' · ')}
</div>
</div>
</>
)}
</div>
)
@@ -128,38 +120,33 @@ export default function TransportWidget() {
}, [])
return (
<div style={{
background: 'linear-gradient(135deg, rgba(99,102,241,0.08), rgba(236,72,153,0.04))',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)' as any,
border: '1px solid rgba(255,255,255,0.06)',
borderRadius: 20, padding: '16px 18px',
<div className="card-hero" style={{
padding: '18px 20px',
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 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 10, position: 'relative', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 32, height: 32, borderRadius: 10,
background: 'rgba(255,255,255,0.06)',
width: 36, height: 36, borderRadius: 11,
background: 'var(--surface-2)',
border: '1px solid var(--border-subtle)',
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 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 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>
@@ -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)',
}}>
<div />
{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' }}>
{d.short}
</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}
</div>
</div>
@@ -188,8 +176,8 @@ export default function TransportWidget() {
))}
</div>
{/* Rows: one per route */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, position: 'relative', zIndex: 1 }}>
{/* Rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}>
{ROUTES.map(route => (
<div key={route.num} style={{
display: 'grid',
@@ -197,15 +185,14 @@ export default function TransportWidget() {
gap: 10,
alignItems: 'stretch',
}}>
{/* Route badge */}
<div style={{
background: route.bg,
boxShadow: `0 6px 16px ${route.color}35`,
borderRadius: 12,
boxShadow: `0 6px 16px -4px ${route.color}55`,
borderRadius: 13,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'white', fontWeight: 800, fontSize: 20,
letterSpacing: '-1px',
minHeight: 52,
color: 'white', fontWeight: 800, fontSize: 22,
letterSpacing: '-1.5px',
minHeight: 56,
}}>
{route.num}
</div>
@@ -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 }}
>
<Cell arrivals={arrivals} color={route.color} />
</motion.div>
@@ -230,7 +217,7 @@ export default function TransportWidget() {
</div>
{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>
)}