Files
OnBudget/design/variants.jsx
T
2026-05-27 09:19:19 +03:00

744 lines
34 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 5 home-screen wireframe variations.
// Each is a function returning the inner-content of an AndroidDevice (status bar + nav are added by the frame).
// =====================================================================
// Shared sub-blocks
// =====================================================================
function MonthChip({ label = 'Май 2026', tight }) {
return (
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: tight ? '4px 8px' : '6px 10px',
border: '1px solid var(--line)', borderRadius: 999,
fontSize: 12, color: 'var(--ink)', background: 'var(--paper)',
}}>
<span>{label}</span>
<span style={{ color: 'var(--ink-2)', display: 'flex' }}>{Icons.chev}</span>
</div>
);
}
function KPI({ label, value, hint, accent, mono = true }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div style={{
fontSize: 10, color: 'var(--ink-2)',
letterSpacing: 0.6, textTransform: 'uppercase',
}}>{label}</div>
<div style={{
fontSize: 17, fontWeight: 600, color: accent || 'var(--ink)',
fontFamily: mono ? 'JetBrains Mono, monospace' : undefined,
fontVariantNumeric: 'tabular-nums', letterSpacing: -0.3,
}}>{value}</div>
{hint && (
<div style={{ fontSize: 10, color: 'var(--ink-2)' }}>{hint}</div>
)}
</div>
);
}
function CategoryChip({ cat, active, count, onClick, dense }) {
return (
<div onClick={onClick} style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: dense ? '4px 9px' : '6px 11px',
borderRadius: 999, flexShrink: 0,
border: '1px solid ' + (active ? 'var(--ink)' : 'var(--line)'),
background: active ? 'var(--ink)' : 'var(--paper)',
color: active ? 'var(--paper)' : 'var(--ink)',
fontSize: 12, fontWeight: active ? 600 : 400,
cursor: 'pointer',
}}>
{cat?.color && (
<div style={{
width: 8, height: 8, borderRadius: 99,
background: active ? 'var(--paper)' : cat.color,
}}/>
)}
<span>{cat ? cat.label : 'Все'}</span>
{count !== undefined && (
<span style={{ fontSize: 10, opacity: 0.7 }}>· {count}</span>
)}
</div>
);
}
// Horizontal scroll strip
function HScroll({ children, gap = 8, pad = 16 }) {
return (
<div style={{
display: 'flex', gap, padding: `0 ${pad}px`,
overflowX: 'auto', scrollbarWidth: 'none',
WebkitOverflowScrolling: 'touch',
}}>{children}</div>
);
}
// =====================================================================
// V1 — Tab pills + KPI strip + donut + bottomsheet-trigger filter
// + day-grouped transactions
// =====================================================================
function V1() {
const [acc, setAcc] = React.useState('all');
const [filterCat, setFilterCat] = React.useState(null);
const [active, setActive] = React.useState(null);
const visibleTx = filterCat ? TX.filter(t => t.cat === filterCat) : TX;
// Group transactions by `when` field (we'll use first comma-split as day key).
const groups = React.useMemo(() => {
const byDay = new Map();
for (const t of visibleTx) {
const day = t.when.split(',')[0].trim();
if (!byDay.has(day)) byDay.set(day, []);
byDay.get(day).push(t);
}
return Array.from(byDay, ([day, items]) => ({
day,
items,
total: items.reduce((s, t) => s + (t.amount < 0 ? -t.amount : 0), 0),
}));
}, [visibleTx]);
const activeCat = filterCat && CATS.find(c => c.id === filterCat);
return (
<div style={{ background: 'var(--paper)', minHeight: '100%', position: 'relative', paddingBottom: 80 }}>
{/* Header */}
<div style={{ padding: '14px 16px 8px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<div style={{ fontSize: 11, color: 'var(--ink-2)', letterSpacing: 0.6, textTransform: 'uppercase' }}>Бюджет</div>
<div style={{ fontSize: 20, fontWeight: 600, color: 'var(--ink)' }}>Май 2026</div>
</div>
<div style={{ display: 'flex', gap: 6, color: 'var(--ink-2)' }}>
{Icons.search}{Icons.bell}
</div>
</div>
{/* Account tabs (segmented pills, scrollable) */}
<HScroll>
{ACCOUNTS.map(a => (
<div key={a.id} onClick={() => setAcc(a.id)} style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '7px 12px', borderRadius: 999, flexShrink: 0,
border: '1px solid ' + (acc === a.id ? 'var(--ink)' : 'var(--line)'),
background: acc === a.id ? 'var(--ink)' : 'var(--paper)',
color: acc === a.id ? 'var(--paper)' : 'var(--ink)',
fontSize: 13, fontWeight: acc === a.id ? 600 : 400,
}}>
<span style={{ display: 'flex', opacity: acc === a.id ? 1 : 0.7 }}>{a.icon}</span>
<span>{a.short}</span>
</div>
))}
</HScroll>
{/* Month KPI strip — balance + доходы / расходы only */}
<div style={{
margin: '12px 16px 14px',
border: '1px solid var(--line)', borderRadius: 14,
padding: '14px 14px 14px', background: 'var(--paper)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 12 }}>
<span style={{ fontSize: 11, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Баланс</span>
<span style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 22, fontWeight: 600,
color: 'var(--ink)', letterSpacing: -0.4,
}}>184 320 </span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1px 1fr', gap: 12, alignItems: 'center' }}>
<div>
<div style={{ fontSize: 10, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 2 }}>Доходы</div>
<div style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 18, fontWeight: 600,
color: 'var(--pos)', letterSpacing: -0.3,
}}>+95 000 </div>
</div>
<div style={{ width: 1, height: 32, background: 'var(--line)' }} />
<div>
<div style={{ fontSize: 10, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 2 }}>Расходы</div>
<div style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 18, fontWeight: 600,
color: 'var(--neg)', letterSpacing: -0.3,
}}>67 200 </div>
</div>
</div>
</div>
{/* Donut + legend */}
<div style={{
margin: '0 16px 14px', padding: 14,
border: '1px solid var(--line)', borderRadius: 14,
display: 'flex', gap: 14, alignItems: 'center',
}}>
<div style={{ position: 'relative', width: 130, height: 130, flexShrink: 0 }}>
<Donut data={DONUT_DATA} size={130} thickness={22}
active={active} onSegment={(i) => {
const id = DONUT_DATA[i].id;
setFilterCat(filterCat === id ? null : id);
setActive(active === i ? null : i);
}} />
<div style={{
position: 'absolute', inset: 0, display: 'flex',
flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
pointerEvents: 'none',
}}>
<div style={{ fontSize: 9, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Расходы</div>
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 14, fontWeight: 600 }}>67 200 </div>
</div>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 6 }}>
{DONUT_DATA.slice(0, 4).map((d, i) => {
const pct = Math.round((d.value / SPEND_TOTAL) * 100);
return (
<div key={d.id} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
<div style={{ width: 8, height: 8, borderRadius: 2, background: d.color }} />
<span style={{ flex: 1, color: 'var(--ink)' }}>{d.label}</span>
<span style={{
fontFamily: 'JetBrains Mono, monospace', color: 'var(--ink-2)',
fontVariantNumeric: 'tabular-nums',
}}>{pct}%</span>
</div>
);
})}
<div style={{ fontSize: 10, color: 'var(--ink-2)' }}>+ ещё 2 категории </div>
</div>
</div>
{/* Section header */}
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0 16px 8px', alignItems: 'center' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>Транзакции</div>
<span style={{ fontSize: 11, color: 'var(--ink-2)' }}>{visibleTx.length} операций</span>
</div>
{/* Bottomsheet-trigger filter (from V2) */}
<div
onClick={() => { setFilterCat(null); setActive(null); }}
style={{
margin: '0 16px 0', padding: '10px 12px',
border: '1px solid var(--line)', borderRadius: 12,
display: 'flex', alignItems: 'center', gap: 8,
background: 'var(--paper)', cursor: 'pointer',
}}>
<span style={{ display: 'flex', color: 'var(--ink-2)' }}>{Icons.filter}</span>
<span style={{ flex: 1, fontSize: 13, color: 'var(--ink)', display: 'flex', alignItems: 'center', gap: 6 }}>
{activeCat ? (
<>
<div style={{ width: 8, height: 8, borderRadius: 2, background: activeCat.color }} />
<span>{activeCat.label}</span>
</>
) : (
<span>Все категории · все типы</span>
)}
</span>
<span style={{
fontSize: 11, padding: '2px 6px', borderRadius: 99,
background: 'var(--accent-soft)', color: 'var(--accent)', fontWeight: 600,
fontFamily: 'JetBrains Mono, monospace',
}}>{visibleTx.length}</span>
<span style={{ color: 'var(--ink-2)', display: 'flex' }}>{Icons.chev}</span>
</div>
{/* TX list grouped by day */}
<div style={{ marginTop: 4 }}>
{groups.map(g => (
<React.Fragment key={g.day}>
<DayHeader label={g.day} total={g.total} />
{g.items.map(tx => <TxRow key={tx.id} tx={tx} />)}
</React.Fragment>
))}
</div>
<FAB />
</div>
);
}
// =====================================================================
// V2 — Account dropdown + hero card + bottomsheet filter
// =====================================================================
function V2() {
const [acc] = React.useState('card');
const accObj = ACCOUNTS.find(a => a.id === acc);
return (
<div style={{ background: 'var(--paper)', minHeight: '100%', position: 'relative', paddingBottom: 80 }}>
{/* Account dropdown header */}
<div style={{ padding: '14px 16px 6px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
padding: '6px 10px 6px 8px', borderRadius: 999,
border: '1px solid var(--line)', background: 'var(--paper)',
}}>
<div style={{
width: 24, height: 24, borderRadius: 6, background: 'var(--accent-soft)',
color: 'var(--accent)', display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>{accObj.icon}</div>
<div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1.15 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>{accObj.label}</span>
<span style={{ fontSize: 10, color: 'var(--ink-2)' }}>4 счёта · переключить</span>
</div>
<span style={{ color: 'var(--ink-2)', display: 'flex' }}>{Icons.chev}</span>
</div>
<div style={{ color: 'var(--ink-2)', display: 'flex', gap: 6 }}>
{Icons.search}{Icons.bell}
</div>
</div>
{/* Hero balance card — editorial */}
<div style={{
margin: '14px 16px 16px',
padding: '18px 18px 16px',
background: 'var(--ink)', color: 'var(--paper)',
borderRadius: 18, position: 'relative', overflow: 'hidden',
}}>
<div style={{ fontSize: 10, opacity: 0.6, letterSpacing: 0.6, textTransform: 'uppercase' }}>Баланс счёта</div>
<div style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 30, fontWeight: 600,
letterSpacing: -0.6, marginTop: 2, marginBottom: 14,
}}>142 500 </div>
{/* Inline sparkline */}
<svg width="100%" height="40" viewBox="0 0 280 40" style={{ marginBottom: 10 }}>
<path d="M0 28 L20 24 L40 26 L60 18 L80 22 L100 14 L120 18 L140 10 L160 14 L180 8 L200 12 L220 6 L240 10 L260 4 L280 8"
fill="none" stroke="var(--accent)" strokeWidth="1.5" />
<path d="M0 28 L20 24 L40 26 L60 18 L80 22 L100 14 L120 18 L140 10 L160 14 L180 8 L200 12 L220 6 L240 10 L260 4 L280 8 L280 40 L0 40 Z"
fill="var(--accent)" opacity="0.18" />
</svg>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8 }}>
<div>
<div style={{ fontSize: 10, opacity: 0.6, textTransform: 'uppercase', letterSpacing: 0.6 }}>Доход</div>
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 15, fontWeight: 600 }}>+95 000</div>
</div>
<div>
<div style={{ fontSize: 10, opacity: 0.6, textTransform: 'uppercase', letterSpacing: 0.6 }}>Расход</div>
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 15, fontWeight: 600 }}>67 200</div>
</div>
<div>
<div style={{ fontSize: 10, opacity: 0.6, textTransform: 'uppercase', letterSpacing: 0.6 }}>Остаток</div>
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 15, fontWeight: 600 }}>27 800</div>
</div>
</div>
</div>
{/* Donut centered, with big center stat */}
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0 16px 8px', alignItems: 'baseline' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>Расходы по категориям</div>
<span style={{ fontSize: 11, color: 'var(--ink-2)' }}>Май</span>
</div>
<div style={{ display: 'flex', justifyContent: 'center', position: 'relative', marginBottom: 4 }}>
<div style={{ position: 'relative', width: 160, height: 160 }}>
<Donut data={DONUT_DATA} size={160} thickness={20} />
<div style={{
position: 'absolute', inset: 0, display: 'flex',
flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
}}>
<div style={{ fontSize: 9, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Всего</div>
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 19, fontWeight: 600, color: 'var(--ink)' }}>67 200 </div>
<div style={{ fontSize: 10, color: 'var(--ink-2)', marginTop: 2 }}>6 категорий</div>
</div>
</div>
</div>
{/* Filter row (sheet trigger) */}
<div style={{
margin: '6px 16px 0', padding: '10px 12px',
border: '1px solid var(--line)', borderRadius: 12,
display: 'flex', alignItems: 'center', gap: 8,
background: 'var(--paper)',
}}>
<span style={{ display: 'flex', color: 'var(--ink-2)' }}>{Icons.filter}</span>
<span style={{ flex: 1, fontSize: 13, color: 'var(--ink)' }}>Все категории · все типы</span>
<span style={{
fontSize: 11, padding: '2px 6px', borderRadius: 99,
background: 'var(--accent-soft)', color: 'var(--accent)', fontWeight: 600,
}}>132</span>
<span style={{ color: 'var(--ink-2)', display: 'flex' }}>{Icons.chev}</span>
</div>
{/* Compact TX list */}
<div style={{ marginTop: 8 }}>
{TX.slice(0, 5).map(tx => <TxRow key={tx.id} tx={tx} />)}
</div>
{/* Sheet peek note */}
<div style={{
position: 'absolute', left: 12, right: 12, bottom: 90,
display: 'flex', alignItems: 'center', gap: 6,
pointerEvents: 'none',
}}>
<NoteArrow rot={-15} len={32} />
<Note>Тап открывает bottom sheet с фильтрами</Note>
</div>
<FAB />
</div>
);
}
// =====================================================================
// V3 — Swipeable account cards carousel + stacked bar + chips
// =====================================================================
function V3() {
const [filterCat, setFilterCat] = React.useState(null);
return (
<div style={{ background: 'var(--paper)', minHeight: '100%', position: 'relative', paddingBottom: 80 }}>
{/* Title */}
<div style={{ padding: '14px 16px 4px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: 22, fontWeight: 600, color: 'var(--ink)', letterSpacing: -0.3 }}>Привет, Аня</div>
<div style={{ fontSize: 12, color: 'var(--ink-2)' }}>Май 2026 · 27 800 свободно</div>
</div>
<div style={{ display: 'flex', gap: 6, color: 'var(--ink-2)' }}>{Icons.bell}</div>
</div>
{/* Account cards carousel (peek style) */}
<div style={{ marginTop: 12, marginBottom: 12, position: 'relative' }}>
<HScroll gap={10}>
{ACCOUNTS.map((a, i) => (
<div key={a.id} style={{
width: 220, flexShrink: 0,
padding: '14px 14px 12px',
borderRadius: 16,
border: '1px solid ' + (i === 1 ? 'var(--ink)' : 'var(--line)'),
background: i === 1 ? 'var(--ink)' : 'var(--paper)',
color: i === 1 ? 'var(--paper)' : 'var(--ink)',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<div style={{
width: 28, height: 28, borderRadius: 8,
background: i === 1 ? 'rgba(255,255,255,0.12)' : 'var(--accent-soft)',
color: i === 1 ? 'var(--paper)' : 'var(--accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>{a.icon}</div>
<span style={{
fontSize: 10, opacity: i === 1 ? 0.6 : 0.5,
textTransform: 'uppercase', letterSpacing: 0.6,
}}>{a.id === 'all' ? 'сводно' : 'счёт'}</span>
</div>
<div style={{ fontSize: 12, opacity: 0.7, marginBottom: 2 }}>{a.label}</div>
<div style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 20, fontWeight: 600,
letterSpacing: -0.3,
}}>{fmtNoCur(a.balance)} </div>
</div>
))}
</HScroll>
{/* Page dots */}
<div style={{ display: 'flex', justifyContent: 'center', gap: 5, marginTop: 10 }}>
{ACCOUNTS.map((_, i) => (
<div key={i} style={{
width: i === 1 ? 14 : 5, height: 5, borderRadius: 99,
background: i === 1 ? 'var(--ink)' : 'var(--line-2)',
}} />
))}
</div>
</div>
{/* 2x2 KPI */}
<div style={{
margin: '0 16px 14px', padding: 14,
border: '1px solid var(--line)', borderRadius: 14,
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14,
}}>
<KPI label="Доходы" value="+95 000 ₽" accent="var(--pos)" hint="3 транзакции" />
<KPI label="Расходы" value="67 200 ₽" accent="var(--neg)" hint="42 транзакции" />
<KPI label="Бюджет" value="95 000 ₽" hint="лимит на месяц" />
<KPI label="Осталось" value="27 800 ₽" hint="на 9 дней" />
</div>
{/* Stacked bar — alt visualization */}
<div style={{ margin: '0 16px 14px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>Структура расходов</div>
<span style={{ fontSize: 11, color: 'var(--ink-2)' }}>67 200 </span>
</div>
<StackBar data={DONUT_DATA} />
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 10 }}>
{DONUT_DATA.map(d => {
const pct = Math.round((d.value / SPEND_TOTAL) * 100);
return (
<div key={d.id} style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 11 }}>
<div style={{ width: 8, height: 8, borderRadius: 2, background: d.color }} />
<span style={{ color: 'var(--ink)' }}>{d.label}</span>
<span style={{ color: 'var(--ink-2)', fontFamily: 'JetBrains Mono, monospace' }}>{pct}%</span>
</div>
);
})}
</div>
</div>
{/* Inline category chips */}
<div style={{ padding: '0 16px 6px', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>Последние операции</div>
<span style={{ fontSize: 11, color: 'var(--ink-2)' }}>Все </span>
</div>
<HScroll>
<CategoryChip active={!filterCat} onClick={() => setFilterCat(null)} dense />
{CATS.slice(0, 5).map(c => (
<CategoryChip key={c.id} cat={c} dense
active={filterCat === c.id}
onClick={() => setFilterCat(filterCat === c.id ? null : c.id)} />
))}
</HScroll>
<div style={{ marginTop: 8 }}>
{TX.slice(0, 4).map(tx => <TxRow key={tx.id} tx={tx} />)}
</div>
<FAB />
</div>
);
}
// =====================================================================
// V4 — Editorial title + tiny tab pills (top-right) + centered donut KPI
// =====================================================================
function V4() {
const [acc, setAcc] = React.useState('all');
const [filterCat, setFilterCat] = React.useState(null);
return (
<div style={{ background: 'var(--paper)', minHeight: '100%', position: 'relative', paddingBottom: 80 }}>
{/* Top: editorial title + tiny tabs */}
<div style={{
padding: '18px 16px 4px',
display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
}}>
<div>
<div style={{ fontSize: 11, color: 'var(--ink-2)', letterSpacing: 0.6, textTransform: 'uppercase' }}>2026</div>
<div style={{
fontSize: 38, fontWeight: 600, color: 'var(--ink)',
letterSpacing: -1, lineHeight: 1, marginTop: 2,
}}>Май.</div>
</div>
<div style={{ color: 'var(--ink-2)', display: 'flex', gap: 6 }}>
{Icons.eye}{Icons.bell}
</div>
</div>
{/* Tiny segmented tabs */}
<div style={{ padding: '14px 16px 0' }}>
<div style={{
display: 'inline-flex', padding: 3, borderRadius: 99,
background: 'var(--card-soft)', border: '1px solid var(--line)',
}}>
{ACCOUNTS.map(a => (
<div key={a.id} onClick={() => setAcc(a.id)} style={{
padding: '5px 11px', borderRadius: 99, fontSize: 12,
background: acc === a.id ? 'var(--paper)' : 'transparent',
boxShadow: acc === a.id ? '0 1px 3px rgba(0,0,0,0.08)' : 'none',
fontWeight: acc === a.id ? 600 : 400,
color: acc === a.id ? 'var(--ink)' : 'var(--ink-2)',
}}>{a.short}</div>
))}
</div>
</div>
{/* Number-first month summary */}
<div style={{ padding: '16px 16px 10px', display: 'flex', gap: 18 }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 10, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Баланс</div>
<div style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 24, fontWeight: 600,
color: 'var(--ink)', letterSpacing: -0.4,
}}>184 320 </div>
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ color: 'var(--pos)', display: 'flex' }}>{Icons.arrowUp}</span>
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>95 000 </span>
<span style={{ fontSize: 11, color: 'var(--ink-2)' }}>доход</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ color: 'var(--neg)', display: 'flex' }}>{Icons.arrowDn}</span>
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>67 200 </span>
<span style={{ fontSize: 11, color: 'var(--ink-2)' }}>расход</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 14, height: 14, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--ink-2)' }}>=</span>
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>27 800 </span>
<span style={{ fontSize: 11, color: 'var(--ink-2)' }}>остаток</span>
</div>
</div>
</div>
{/* Donut with big center number */}
<div style={{ position: 'relative', width: 130, height: 130 }}>
<Donut data={DONUT_DATA} size={130} thickness={14} />
<div style={{
position: 'absolute', inset: 0, display: 'flex',
flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
}}>
<div style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 17, fontWeight: 600,
color: 'var(--neg)', letterSpacing: -0.3,
}}>67.2K</div>
<div style={{ fontSize: 9, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6 }}>траты</div>
</div>
</div>
</div>
{/* Divider */}
<div style={{ height: 1, background: 'var(--line)', margin: '6px 16px 0' }} />
{/* Filter chips */}
<div style={{ padding: '14px 16px 8px', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>Операции</div>
<span style={{ fontSize: 11, color: 'var(--ink-2)' }}>132 в мае</span>
</div>
<HScroll>
<CategoryChip active={!filterCat} onClick={() => setFilterCat(null)} dense />
{CATS.map(c => (
<CategoryChip key={c.id} cat={c} dense
active={filterCat === c.id}
onClick={() => setFilterCat(filterCat === c.id ? null : c.id)} />
))}
</HScroll>
<div style={{ marginTop: 6 }}>
<DayHeader label="Сегодня · 24 мая" total={2882} />
{TX.slice(0, 3).map(tx => <TxRow key={tx.id} tx={tx} />)}
<DayHeader label="Вчера · 23 мая" total={1770} />
{TX.slice(3, 5).map(tx => <TxRow key={tx.id} tx={tx} />)}
</div>
<FAB />
</div>
);
}
// =====================================================================
// V5 — Mini account grid + 2x2 KPI + donut with chip-legend (dual filter)
// =====================================================================
function V5() {
const [acc, setAcc] = React.useState('card');
const [filterCat, setFilterCat] = React.useState(null);
return (
<div style={{ background: 'var(--paper)', minHeight: '100%', position: 'relative', paddingBottom: 80 }}>
<div style={{ padding: '14px 16px 4px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<div style={{ fontSize: 11, color: 'var(--ink-2)', letterSpacing: 0.6, textTransform: 'uppercase' }}>Бюджет · Май 2026</div>
<div style={{ fontSize: 20, fontWeight: 600, color: 'var(--ink)' }}>Обзор</div>
</div>
<div style={{ color: 'var(--ink-2)', display: 'flex', gap: 6 }}>{Icons.search}{Icons.bell}</div>
</div>
{/* Mini account grid 2x2 */}
<div style={{
margin: '12px 16px 0',
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8,
}}>
{ACCOUNTS.map(a => {
const isActive = a.id === acc;
return (
<div key={a.id} onClick={() => setAcc(a.id)} style={{
padding: '10px 12px', borderRadius: 12,
border: '1px solid ' + (isActive ? 'var(--ink)' : 'var(--line)'),
background: isActive ? 'var(--ink)' : 'var(--paper)',
color: isActive ? 'var(--paper)' : 'var(--ink)',
display: 'flex', flexDirection: 'column', gap: 4,
position: 'relative',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ display: 'flex', opacity: isActive ? 1 : 0.7 }}>{a.icon}</span>
<span style={{ fontSize: 11, fontWeight: 500, letterSpacing: 0.2 }}>{a.label}</span>
{isActive && (
<div style={{
position: 'absolute', top: 6, right: 8,
width: 6, height: 6, borderRadius: 99, background: 'var(--accent)',
}} />
)}
</div>
<div style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 14, fontWeight: 600,
letterSpacing: -0.2,
}}>{fmtNoCur(a.balance)} </div>
</div>
);
})}
</div>
{/* 2x2 KPI grid */}
<div style={{
margin: '12px 16px 0',
display: 'grid', gridTemplateColumns: '1fr 1fr',
border: '1px solid var(--line)', borderRadius: 12, overflow: 'hidden',
}}>
{[
{ l: 'Доходы', v: '+95 000 ₽', c: 'var(--pos)' },
{ l: 'Расходы', v: '67 200 ₽', c: 'var(--neg)' },
{ l: 'Остаток', v: '27 800 ₽', c: 'var(--ink)' },
{ l: 'Бюджет', v: '70% / 95K', c: 'var(--ink)' },
].map((k, i) => (
<div key={i} style={{
padding: 12,
borderRight: i % 2 === 0 ? '1px solid var(--line)' : 'none',
borderBottom: i < 2 ? '1px solid var(--line)' : 'none',
}}>
<div style={{ fontSize: 10, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6 }}>{k.l}</div>
<div style={{
fontFamily: 'JetBrains Mono, monospace', fontSize: 15, fontWeight: 600,
color: k.c, marginTop: 2,
}}>{k.v}</div>
</div>
))}
</div>
{/* Donut + chip-legend */}
<div style={{
margin: '12px 16px 12px', padding: 12,
border: '1px solid var(--line)', borderRadius: 14,
display: 'flex', gap: 12, alignItems: 'center',
}}>
<div style={{ position: 'relative', width: 108, height: 108, flexShrink: 0 }}>
<Donut data={DONUT_DATA} size={108} thickness={16}
active={filterCat ? DONUT_DATA.findIndex(d => d.id === filterCat) : null}
onSegment={i => setFilterCat(filterCat === DONUT_DATA[i].id ? null : DONUT_DATA[i].id)} />
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}>
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13, fontWeight: 600 }}>67.2K</div>
<div style={{ fontSize: 8, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Расходы</div>
</div>
</div>
<div style={{ flex: 1, display: 'flex', flexWrap: 'wrap', gap: 5 }}>
{DONUT_DATA.map(d => {
const active = filterCat === d.id;
return (
<div key={d.id}
onClick={() => setFilterCat(active ? null : d.id)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
padding: '3px 8px', borderRadius: 99, fontSize: 11,
border: '1px solid ' + (active ? 'var(--ink)' : 'var(--line)'),
background: active ? 'var(--ink)' : 'var(--paper)',
color: active ? 'var(--paper)' : 'var(--ink)',
cursor: 'pointer',
}}>
<div style={{ width: 6, height: 6, borderRadius: 99, background: d.color }} />
<span>{d.label}</span>
</div>
);
})}
</div>
</div>
{/* Filter + list */}
<div style={{ padding: '0 16px 6px', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>
Операции {filterCat && <span style={{ color: 'var(--ink-2)', fontWeight: 400 }}>· {CATS.find(c => c.id === filterCat)?.label}</span>}
</div>
{filterCat && (
<span onClick={() => setFilterCat(null)} style={{ fontSize: 11, color: 'var(--accent)', cursor: 'pointer' }}>Сбросить</span>
)}
</div>
<div>
{(filterCat ? TX.filter(t => t.cat === filterCat) : TX).slice(0, 4).map(tx => <TxRow key={tx.id} tx={tx} />)}
</div>
<FAB />
</div>
);
}
Object.assign(window, { V1, V2, V3, V4, V5, CategoryChip, KPI, MonthChip, HScroll });