744 lines
34 KiB
React
744 lines
34 KiB
React
// 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 });
|