Skip to content

Instantly share code, notes, and snippets.

@annguyenwasd
Created April 25, 2026 09:41
Show Gist options
  • Select an option

  • Save annguyenwasd/0e890feb9309bbdf8afd97797fe2672d to your computer and use it in GitHub Desktop.

Select an option

Save annguyenwasd/0e890feb9309bbdf8afd97797fe2672d to your computer and use it in GitHub Desktop.
Chung Chi — Budget Multiple Periods mockup (screens 19–22)
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Chung Chi — Budget Multiple Periods (screens 19–22)</title>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
html, body { margin:0; padding:0; font-family:'Outfit', system-ui, sans-serif; background:#f0eee9; }
.canvas { display:flex; gap:48px; padding:60px; align-items:flex-start; min-height:100vh; flex-wrap:wrap; }
.artboard { display:flex; flex-direction:column; align-items:center; gap:12px; }
.artboard-label { font-size:13px; font-weight:600; color:rgba(60,50,40,0.7); letter-spacing:0.02em; }
.section-title { font-size:22px; font-weight:700; color:rgba(40,30,20,0.85); letter-spacing:-0.3px; margin-bottom:4px; width:100%; padding:0 60px; }
.section-sub { font-size:14px; color:rgba(60,50,40,0.6); padding:0 60px; margin-bottom:32px; }
.section { width:100%; margin-bottom:60px; }
.row { display:flex; gap:48px; padding:0 60px; align-items:flex-start; flex-wrap:wrap; }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script type="text/babel" data-presets="env,react">
// ── IOSDevice ──────────────────────────────────────────────────
function IOSStatusBar({ dark = false }) {
const c = dark ? '#fff' : '#000';
return (
<div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'21px 24px 19px', boxSizing:'border-box', position:'relative', zIndex:20, width:'100%' }}>
<span style={{ fontFamily:'-apple-system,"SF Pro",system-ui', fontWeight:590, fontSize:17, color:c }}>9:41</span>
<div style={{ display:'flex', alignItems:'center', gap:7 }}>
<svg width="19" height="12" viewBox="0 0 19 12"><rect x="0" y="7.5" width="3.2" height="4.5" rx="0.7" fill={c}/><rect x="4.8" y="5" width="3.2" height="7" rx="0.7" fill={c}/><rect x="9.6" y="2.5" width="3.2" height="9.5" rx="0.7" fill={c}/><rect x="14.4" y="0" width="3.2" height="12" rx="0.7" fill={c}/></svg>
<svg width="27" height="13" viewBox="0 0 27 13"><rect x="0.5" y="0.5" width="23" height="12" rx="3.5" stroke={c} strokeOpacity="0.35" fill="none"/><rect x="2" y="2" width="20" height="9" rx="2" fill={c}/><path d="M25 4.5V8.5C25.8 8.2 26.5 7.2 26.5 6.5C26.5 5.8 25.8 4.8 25 4.5Z" fill={c} fillOpacity="0.4"/></svg>
</div>
</div>
);
}
function IOSDevice({ children, width=390, height=844, dark=false }) {
return (
<div style={{ width, height, borderRadius:48, overflow:'hidden', position:'relative',
background: dark?'#000':'#F2F2F7',
boxShadow:'0 40px 80px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.12)',
fontFamily:'-apple-system,system-ui,sans-serif', WebkitFontSmoothing:'antialiased',
}}>
<div style={{ position:'absolute', top:11, left:'50%', transform:'translateX(-50%)', width:126, height:37, borderRadius:24, background:'#000', zIndex:50 }}/>
<div style={{ position:'absolute', top:0, left:0, right:0, zIndex:10 }}><IOSStatusBar dark={dark}/></div>
<div style={{ height:'100%', display:'flex', flexDirection:'column' }}>
<div style={{ flex:1, overflow:'auto' }}>{children}</div>
</div>
<div style={{ position:'absolute', bottom:0, left:0, right:0, zIndex:60, height:34, display:'flex', justifyContent:'center', alignItems:'flex-end', paddingBottom:8, pointerEvents:'none' }}>
<div style={{ width:139, height:5, borderRadius:100, background: dark?'rgba(255,255,255,0.7)':'rgba(0,0,0,0.25)' }}/>
</div>
</div>
);
}
// ── Budget screens ─────────────────────────────────────────────
const T = {
crimson:'#AE2F34', coral:'#FF6B6B',
cream:'#FFF8F6', warm:'#FFF3F0', tonal:'#F9F2F0', muted:'#F5EDE9',
surface:'#FFFFFF', ink:'#1A1210', inkSoft:'#7A5C56', inkMuted:'#B89890',
border:'#EDD9D3',
fontDisplay:"'Chakra Petch', system-ui, sans-serif",
fontBody:"'Outfit', system-ui, sans-serif",
gradient:'linear-gradient(135deg, #AE2F34 0%, #FF6B6B 100%)',
shadowCard:'0 2px 16px rgba(255,107,107,0.06)',
shadowFab:'0 8px 24px rgba(255,107,107,0.40)',
shadowModal:'0 -8px 40px rgba(26,18,16,0.18)',
};
const DARK = { cream:'#1A1210', warm:'#231815', tonal:'#2A1A17', muted:'#2D1F1C', surface:'#231815', ink:'#FFF0EC', inkSoft:'#C4908A', inkMuted:'#7A5550', border:'#3D2420' };
const palette = (dark) => dark ? {...T,...DARK} : T;
const fmt = (n) => n.toLocaleString('vi-VN');
const short = (n) => { if(n>=1_000_000) return (n/1_000_000).toFixed(n%1_000_000===0?0:1).replace('.0','')+'tr'; if(n>=1_000) return (n/1_000)+'k'; return String(n); };
const CATS = {
food:{ name:'Ăn uống', color:'#FF9F43', emoji:'🍜' },
transport:{ name:'Di chuyển', color:'#54A0FF', emoji:'🚗' },
bills:{ name:'Hoá đơn', color:'#AE2F34', emoji:'💡' },
entertainment:{ name:'Giải trí', color:'#5F27CD', emoji:'🎬' },
health:{ name:'Sức khoẻ', color:'#00D2D3', emoji:'💊' },
shopping:{ name:'Mua sắm', color:'#1DD1A1', emoji:'🛒' },
family:{ name:'Nuôi bé ❤', color:'#FF6B6B', emoji:'👶' },
travel:{ name:'Về Đồng Nai 🌸', color:'#E91E8C', emoji:'✈️' },
};
function BottomNav({ active, dark }) {
const P = palette(dark);
const tabs = [
{ id:'expenses', label:'Chi tiêu', d:'M19.5 3.5L18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2v14H3v3c0 1.66 1.34 3 3 3h12c1.66 0 3-1.34 3-3V2l-1.5 1.5zM19 19c0 .55-.45 1-1 1s-1-.45-1-1v-3H8V5h11v14z' },
{ id:'report', label:'Báo cáo', d:'M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z' },
{ id:'budget', label:'Ngân sách', d:'M21 7.5V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h11L21 7.5zM17 14a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z' },
{ id:'account', label:'Tài khoản', d:'M12 12c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0 2c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4z' },
];
return (
<div style={{ position:'absolute', bottom:0, left:0, right:0, height:78, background:P.surface, display:'flex', paddingBottom:14, borderTop:`0.5px solid ${P.border}` }}>
{tabs.map(t => {
const on = t.id === active;
return (
<div key={t.id} style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:2, color:on?T.crimson:P.inkMuted, fontFamily:T.fontBody, fontSize:11, fontWeight:on?600:500, transform:on?'translateY(-2px)':'none' }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d={t.d}/></svg>
{t.label}
</div>
);
})}
</div>
);
}
function Donut({ size=180, segments, remaining, total, dark, innerText=true }) {
const P = palette(dark);
const R = size/2-14;
const C = 2*Math.PI*R;
const totalSpent = segments.reduce((s,x)=>s+x.value,0);
const cap = Math.max(totalSpent,total);
const gap = size>=140?1.8:0.8;
let acc = 0;
return (
<div style={{ position:'relative', width:size, height:size, margin:'0 auto' }}>
{size>=140 && <div style={{ position:'absolute', inset:12, borderRadius:'50%', background:'radial-gradient(circle, rgba(255,107,107,0.18), transparent 70%)', filter:'blur(8px)' }}/>}
<svg width={size} height={size} style={{ transform:'rotate(-90deg)', filter:size>=140?'drop-shadow(0 4px 10px rgba(174,47,52,0.15))':'none' }}>
<circle cx={size/2} cy={size/2} r={R} fill="none" stroke={P.muted} strokeWidth={size>=140?18:14}/>
{segments.map((seg,i) => {
const frac=seg.value/cap; const len=Math.max(0,frac*C-gap);
const el=(<circle key={i} cx={size/2} cy={size/2} r={R} fill="none" stroke={seg.color} strokeWidth={size>=140?18:14} strokeDasharray={`${len} ${C-len}`} strokeDashoffset={-acc} strokeLinecap="round"/>);
acc+=frac*C; return el;
})}
</svg>
{innerText && (
<div style={{ position:'absolute', inset:0, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center' }}>
{size>=140 && (<>
<div style={{ fontFamily:T.fontBody, fontSize:11, color:P.inkSoft, marginBottom:2, whiteSpace:'nowrap', letterSpacing:'0.04em' }}>Còn lại</div>
<div style={{ fontFamily:T.fontDisplay, fontWeight:700, fontSize:34, letterSpacing:'-0.015em', whiteSpace:'nowrap', lineHeight:1, background:remaining<0?T.crimson:T.gradient, WebkitBackgroundClip:'text', WebkitTextFillColor:'transparent', backgroundClip:'text' }}>{remaining<0?'-':''}{short(Math.abs(remaining))}</div>
<div style={{ fontFamily:T.fontBody, fontSize:10, color:P.inkMuted, marginTop:4, whiteSpace:'nowrap' }}>/ {short(total)}</div>
</>)}
</div>
)}
</div>
);
}
// ── Screen 19 · Add budget step 1 ────────────────────────────
function CCBudgetAddStep1({ dark=false }) {
const P = palette(dark);
return (
<div style={{ position:'absolute', inset:0, background:P.cream, overflow:'hidden' }}>
<div style={{ position:'absolute', inset:0, background:'#000', opacity:0.25 }}/>
<div style={{ position:'absolute', left:0, right:0, bottom:0, background:P.cream, borderTopLeftRadius:24, borderTopRightRadius:24, boxShadow:T.shadowModal, height:'92%', display:'flex', flexDirection:'column', overflow:'hidden' }}>
<div style={{ display:'flex', justifyContent:'center', padding:'10px 0 4px' }}>
<div style={{ width:38, height:4, borderRadius:9999, background:P.inkMuted, opacity:0.5 }}/>
</div>
<div style={{ height:48, display:'flex', alignItems:'center', padding:'0 16px' }}>
<button style={{ border:0, background:'transparent', color:T.crimson, fontSize:15, fontWeight:500, fontFamily:T.fontBody }}>Huỷ</button>
<div style={{ flex:1, fontFamily:T.fontDisplay, fontWeight:600, fontSize:17, textAlign:'center', color:P.ink }}>Ngân sách mới</div>
<button style={{ border:0, background:'transparent', color:P.inkMuted, fontSize:15, fontWeight:500, fontFamily:T.fontBody }}>Tiếp</button>
</div>
<div style={{ padding:'8px 20px', display:'flex', flexDirection:'column', gap:18, overflow:'auto' }}>
<div style={{ display:'flex', gap:6, alignItems:'center', justifyContent:'center', padding:'2px 0 4px' }}>
<span style={{ width:22, height:4, borderRadius:9999, background:T.crimson }}/>
<span style={{ width:22, height:4, borderRadius:9999, background:P.muted }}/>
<span style={{ fontSize:11, color:P.inkMuted, marginLeft:4 }}>1 / 2 · Thời gian & tổng</span>
</div>
{/* Budget name (NEW field) */}
<div>
<div style={{ fontSize:11, color:P.inkSoft, paddingLeft:14, marginBottom:6, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase' }}>Tên ngân sách</div>
<div style={{ background:P.surface, border:`1px solid ${P.border}`, borderRadius:12, padding:'12px 14px', fontSize:15, color:P.inkMuted, fontFamily:T.fontBody, fontWeight:500 }}>Du lịch Đà Nẵng</div>
</div>
{/* Period */}
<div>
<div style={{ fontSize:11, color:P.inkSoft, paddingLeft:14, marginBottom:6, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase' }}>Khoảng thời gian</div>
<div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:10 }}>
<div style={{ background:P.surface, border:`1.5px solid ${T.crimson}`, borderRadius:12, padding:'10px 14px' }}>
<div style={{ fontSize:10, color:P.inkSoft, marginBottom:2 }}>Bắt đầu</div>
<div style={{ fontSize:14, fontWeight:600, color:P.ink, fontFamily:T.fontDisplay, fontVariantNumeric:'tabular-nums' }}>02/05/2025</div>
</div>
<div style={{ background:P.surface, border:`1px solid ${P.border}`, borderRadius:12, padding:'10px 14px' }}>
<div style={{ fontSize:10, color:P.inkSoft, marginBottom:2 }}>Kết thúc</div>
<div style={{ fontSize:14, fontWeight:600, color:P.ink, fontFamily:T.fontDisplay, fontVariantNumeric:'tabular-nums' }}>09/05/2025</div>
</div>
</div>
{/* Recurring toggle + cadence selector */}
<div style={{ background:P.surface, borderRadius:12, boxShadow:T.shadowCard, padding:'12px 14px', display:'flex', alignItems:'center', gap:12 }}>
<div style={{ width:30, height:30, borderRadius:8, background:T.crimson+'15', color:T.crimson, display:'flex', alignItems:'center', justifyContent:'center' }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4a8 8 0 1 0 7.75 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
</div>
<div style={{ flex:1 }}>
<div style={{ fontSize:13, fontWeight:500, color:P.ink }}>Lặp lại tự động</div>
<div style={{ fontSize:11, color:P.inkSoft, marginTop:1 }}>
<span style={{ background:T.crimson+'15', color:T.crimson, borderRadius:6, padding:'2px 8px', fontSize:11, fontWeight:600 }}>Hàng tháng ▾</span>
</div>
</div>
{/* Toggle ON */}
<div style={{ width:42, height:24, borderRadius:9999, background:T.crimson, position:'relative' }}>
<span style={{ position:'absolute', top:2, left:20, width:20, height:20, borderRadius:'50%', background:'#fff' }}/>
</div>
</div>
</div>
{/* Category scope (NEW field) */}
<div>
<div style={{ fontSize:11, color:P.inkSoft, paddingLeft:14, marginBottom:6, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase' }}>Phạm vi danh mục</div>
<div style={{ background:P.surface, border:`1px solid ${P.border}`, borderRadius:12, padding:'12px 14px', display:'flex', alignItems:'center', justifyContent:'space-between' }}>
<div style={{ display:'flex', gap:6, flexWrap:'wrap' }}>
{['Du lịch','Ăn uống'].map(c => (
<span key={c} style={{ background:T.crimson+'15', color:T.crimson, borderRadius:99, padding:'3px 10px', fontSize:12, fontWeight:600 }}>{c}</span>
))}
</div>
<svg width="16" height="16" viewBox="0 0 24 24" fill={P.inkMuted}><path d="M19 6.4L17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z"/></svg>
</div>
<div style={{ fontSize:11, color:P.inkMuted, padding:'6px 14px 0', lineHeight:1.5 }}>Chỉ tính chi tiêu thuộc các danh mục này.</div>
</div>
{/* Total */}
<div>
<div style={{ fontSize:11, color:P.inkSoft, paddingLeft:14, marginBottom:6, fontWeight:600, letterSpacing:'0.05em', textTransform:'uppercase' }}>Tổng ngân sách</div>
<div style={{ background:P.surface, borderRadius:14, padding:'18px 20px', boxShadow:T.shadowCard, textAlign:'center' }}>
<div style={{ fontFamily:T.fontDisplay, fontWeight:700, fontSize:34, color:T.crimson, letterSpacing:'-0.01em', fontVariantNumeric:'tabular-nums' }}>8.000.000</div>
<div style={{ fontSize:11, color:P.inkSoft, marginTop:4, letterSpacing:'0.08em', textTransform:'uppercase', fontWeight:600 }}>đồng</div>
</div>
</div>
</div>
</div>
</div>
);
}
// ── Screen 20 · Add budget step 2 ────────────────────────────
function CCBudgetAddStep2({ dark=false }) {
const P = palette(dark);
const rows = [
{ cat:'family', amount:4_500_000, active:false },
{ cat:'travel', amount:3_000_000, active:true },
{ cat:'food', amount:null, active:false },
{ cat:'bills', amount:null, active:false },
{ cat:'transport', amount:null, active:false },
];
const allocated = rows.reduce((s,r)=>s+(r.amount||0),0);
const total = 30_000_000;
const remain = total - allocated;
return (
<div style={{ position:'absolute', inset:0, background:P.cream, overflow:'hidden' }}>
<div style={{ height:'46%', overflow:'hidden', position:'relative', background:P.cream }}>
<div style={{ height:60 }}/>
<div style={{ height:48, display:'flex', alignItems:'center', padding:'0 16px' }}>
<button style={{ border:0, background:'transparent', color:P.ink, width:30, height:30, display:'flex', alignItems:'center', justifyContent:'center' }}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.4L17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z"/></svg>
</button>
<div style={{ flex:1, fontFamily:T.fontDisplay, fontWeight:600, fontSize:17, color:P.ink, marginLeft:4 }}>Ngân sách danh mục</div>
<button style={{ background:T.crimson, color:'#fff', border:0, padding:'7px 18px', borderRadius:9999, fontFamily:T.fontBody, fontWeight:600, fontSize:13 }}>Xong</button>
</div>
<div style={{ padding:'4px 20px 10px', display:'flex', alignItems:'center', gap:16 }}>
<div style={{ width:80, height:80 }}>
<Donut size={80} segments={rows.filter(r=>r.amount).map(r=>({ color:CATS[r.cat].color, value:r.amount }))} remaining={remain} total={total} dark={dark} innerText={false}/>
</div>
<div style={{ flex:1 }}>
<div style={{ fontSize:10, color:P.inkSoft, letterSpacing:'0.08em', textTransform:'uppercase', fontWeight:600, marginBottom:3 }}>Chưa phân bổ</div>
<div style={{ fontFamily:T.fontDisplay, fontWeight:700, fontSize:22, color:T.crimson, fontVariantNumeric:'tabular-nums', letterSpacing:'-0.01em' }}>{fmt(remain)} đ</div>
<div style={{ fontSize:11, color:P.inkMuted, marginTop:2 }}>/ {fmt(total)} đ tổng</div>
</div>
</div>
<div style={{ padding:'0 20px', display:'flex', flexDirection:'column', gap:0 }}>
{rows.slice(0,2).map((r,i) => {
const c = CATS[r.cat];
return (
<div key={i} style={{ display:'flex', alignItems:'center', gap:10, padding:'10px 2px', borderBottom:`0.5px solid ${P.border}` }}>
<span style={{ width:10, height:10, borderRadius:'50%', background:c.color }}/>
<div style={{ flex:1, fontSize:14, color:r.active?T.crimson:P.ink, fontWeight:r.active?600:500 }}>{c.name}</div>
{r.amount ? <div style={{ fontSize:14, fontWeight:600, color:r.active?T.crimson:P.ink, fontVariantNumeric:'tabular-nums' }}>{short(r.amount)}</div> : <div style={{ fontSize:13, color:P.inkMuted }}>Đặt giới hạn</div>}
</div>
);
})}
</div>
</div>
<div style={{ position:'absolute', left:0, right:0, bottom:0, height:'54%', background:P.surface, borderTopLeftRadius:20, borderTopRightRadius:20, boxShadow:T.shadowModal, display:'flex', flexDirection:'column' }}>
<div style={{ padding:'10px 20px 6px' }}>
<div style={{ display:'flex', justifyContent:'center', marginBottom:8 }}>
<div style={{ width:38, height:4, borderRadius:9999, background:P.inkMuted, opacity:0.4 }}/>
</div>
<div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:10 }}>
<span style={{ width:8, height:8, borderRadius:'50%', background:CATS.travel.color }}/>
<div style={{ fontSize:14, fontWeight:600, color:P.ink }}>{CATS.travel.name}</div>
</div>
<div style={{ background:P.muted, borderRadius:14, padding:'14px 18px', textAlign:'right', fontFamily:T.fontDisplay, fontWeight:700, fontSize:30, color:P.ink, fontVariantNumeric:'tabular-nums', letterSpacing:'-0.005em' }}>3.000.000</div>
</div>
<div style={{ flex:1, padding:'4px 14px 10px', display:'grid', gridTemplateColumns:'repeat(4, 1fr)', gap:6 }}>
{[{l:'C',kind:'op'},{l:'÷',kind:'op'},{l:'×',kind:'op'},{l:'⌫',kind:'op'},{l:'7'},{l:'8'},{l:'9'},{l:'−',kind:'op'},{l:'4'},{l:'5'},{l:'6'},{l:'+',kind:'op'},{l:'1'},{l:'2'},{l:'3'},{l:'Xong',kind:'done',rowSpan:2},{l:'0'},{l:'000'},{l:'.'}].map((k,i) => {
if(k.kind==='done') return (
<div key={i} style={{ gridRow:'span 2', background:T.crimson, color:'#fff', borderRadius:12, display:'flex', alignItems:'center', justifyContent:'center', fontFamily:T.fontDisplay, fontWeight:700, fontSize:15, letterSpacing:'0.02em' }}>{k.l}</div>
);
const isOp = k.kind==='op';
return (<div key={i} style={{ background:P.surface, border:`1px solid ${P.border}`, borderRadius:12, display:'flex', alignItems:'center', justifyContent:'center', fontFamily:isOp?T.fontDisplay:T.fontBody, fontWeight:isOp?600:500, fontSize:isOp?18:17, color:isOp?T.crimson:P.ink, height:44 }}>{k.l}</div>);
})}
</div>
</div>
</div>
);
}
// ── Screen 21 · Budget swipe to delete ───────────────────────
function CCBudgetSwipeDelete({ dark=false }) {
const P = palette(dark);
const rows = [
{ cat:'family', budget:4_500_000, spent:3_200_000, swipe:0 },
{ cat:'travel', budget:3_000_000, spent:4_215_000, swipe:110 },
{ cat:'food', budget:6_000_000, spent:4_439_000, swipe:0 },
];
return (
<div style={{ position:'absolute', inset:0, background:P.cream, overflow:'hidden' }}>
<div style={{ height:60 }}/>
<div style={{ height:52, display:'flex', alignItems:'center', padding:'0 20px' }}>
<div style={{ flex:1, fontFamily:T.fontDisplay, fontWeight:700, fontSize:24, color:P.ink, letterSpacing:'-0.01em' }}>Ngân sách</div>
<button style={{ border:0, background:T.gradient, color:'#fff', height:34, padding:'0 16px', borderRadius:9999, display:'flex', alignItems:'center', justifyContent:'center', gap:5, boxShadow:T.shadowFab, fontFamily:T.fontDisplay, fontWeight:600, fontSize:13 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
Thêm
</button>
</div>
<div style={{ padding:'4px 20px 10px', fontSize:13, color:P.inkSoft, textAlign:'center', fontVariantNumeric:'tabular-nums' }}>25/03 – 25/04 · còn 1.1tr</div>
<div style={{ padding:'4px 20px', display:'flex', flexDirection:'column', gap:10 }}>
{rows.map((r,i) => {
const c = CATS[r.cat];
const pct = Math.round((r.spent/r.budget)*100);
const over = pct>100;
return (
<div key={i} style={{ position:'relative', height:88 }}>
<div style={{ position:'absolute', inset:0, background:'#E53935', borderRadius:14, display:'flex', alignItems:'center', justifyContent:'flex-end', paddingRight:24, gap:10, color:'#fff', fontFamily:T.fontBody, fontWeight:600, fontSize:13 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6zM19 4h-3.5l-1-1h-5l-1 1H5v2h14z"/></svg>
<span>Xoá</span>
</div>
<div style={{ position:'absolute', inset:0, width:r.swipe?`calc(100% - ${r.swipe}px)`:'100%', overflow:'hidden', borderTopLeftRadius:14, borderBottomLeftRadius:14, borderTopRightRadius:r.swipe?0:14, borderBottomRightRadius:r.swipe?0:14 }}>
<div style={{ width:350, boxSizing:'border-box', background:P.surface, borderRadius:14, padding:'12px 14px', boxShadow:T.shadowCard, height:88 }}>
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:8 }}>
<div style={{ width:10, height:10, borderRadius:'50%', background:c.color, flexShrink:0 }}/>
<div style={{ flex:1, fontSize:14, fontWeight:500, color:P.ink }}>{c.name}</div>
<div style={{ fontSize:13, fontWeight:600, color:over?T.crimson:'#F5A623', fontVariantNumeric:'tabular-nums' }}>{pct}%</div>
</div>
<div style={{ height:6, borderRadius:9999, background:P.muted, overflow:'hidden', marginBottom:6 }}>
<div style={{ height:'100%', width:`${Math.min(100,pct)}%`, background:over?T.crimson:c.color, borderRadius:9999 }}/>
</div>
<div style={{ display:'flex', justifyContent:'space-between' }}>
<div style={{ fontSize:11, color:P.inkSoft, fontVariantNumeric:'tabular-nums' }}>{fmt(r.spent)} đ</div>
<div style={{ fontSize:10, color:P.inkMuted, letterSpacing:'0.05em', textTransform:'uppercase', fontWeight:600 }}>Tối đa {short(r.budget)}</div>
</div>
</div>
</div>
</div>
);
})}
<div style={{ marginTop:6, alignSelf:'flex-end', background:P.ink, color:P.cream, borderRadius:10, padding:'7px 13px', fontSize:11, fontFamily:T.fontBody, fontWeight:500, whiteSpace:'nowrap', boxShadow:'0 4px 14px rgba(0,0,0,0.24)', position:'relative' }}>
← Vuốt để xoá ngân sách
<div style={{ position:'absolute', right:22, top:-4, width:8, height:8, background:P.ink, transform:'rotate(45deg)' }}/>
</div>
</div>
<BottomNav active="budget" dark={dark}/>
</div>
);
}
// ── Screen 22 · Budget multiple periods ──────────────────────
function CCBudgetMulti({ dark=false }) {
const P = palette(dark);
const budgets = [
{ id:'apr', title:'Tháng 4 · Cùng nhau', period:'25/03 – 25/04', recurring:true, total:16_000_000, spent:13_882_000, accent:T.crimson, accent2:T.coral, gradient:T.gradient },
{ id:'travel', title:'Du lịch Đà Nẵng', period:'02/05 – 09/05', recurring:false, total:8_000_000, spent:1_240_000, accent:'#5F27CD', accent2:'#54A0FF', gradient:'linear-gradient(135deg, #5F27CD 0%, #54A0FF 100%)' },
{ id:'wedding', title:'Quỹ cưới', period:'Đến 12/12', recurring:false, total:80_000_000, spent:22_500_000, accent:'#1DD1A1', accent2:'#00D2D3', gradient:'linear-gradient(135deg, #1DD1A1 0%, #00D2D3 100%)' },
];
const active = budgets[0];
const remaining = active.total - active.spent;
const pct = Math.round((active.spent/active.total)*100);
const allocations = [
{ name:'Nuôi bé ❤', budget:4_500_000, spent:3_200_000, color:'#FF6B6B' },
{ name:'Về Đồng Nai 🌸', budget:3_000_000, spent:4_215_000, color:'#E91E8C' },
{ name:'Ăn uống', budget:6_000_000, spent:4_439_000, color:'#FF9F43' },
];
return (
<div style={{ position:'absolute', inset:0, background:P.cream, overflow:'hidden', fontFamily:T.fontBody, color:P.ink }}>
<div style={{ height:60 }}/>
<div style={{ height:52, display:'flex', alignItems:'center', padding:'0 20px' }}>
<div style={{ flex:1, fontFamily:T.fontDisplay, fontWeight:700, fontSize:26, color:P.ink, letterSpacing:'-0.015em' }}>Ngân sách</div>
<button style={{ border:0, background:T.gradient, color:'#fff', height:34, padding:'0 16px', borderRadius:9999, display:'flex', alignItems:'center', justifyContent:'center', gap:5, boxShadow:T.shadowFab, fontFamily:T.fontDisplay, fontWeight:600, fontSize:13 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
Thêm
</button>
</div>
<div style={{ padding:'8px 0 14px', overflow:'hidden', position:'relative' }}>
<div style={{ display:'flex', gap:12, padding:'0 20px' }}>
{/* Active card */}
<div style={{ flex:'0 0 calc(100% - 56px)', background:active.gradient, color:'#fff', borderRadius:22, padding:'18px 20px 20px', boxShadow:'0 12px 32px rgba(174,47,52,0.30)', position:'relative', overflow:'hidden' }}>
<div style={{ position:'absolute', right:-16, bottom:-50, fontFamily:T.fontDisplay, fontSize:180, fontWeight:700, opacity:0.10, lineHeight:1, color:'#fff', pointerEvents:'none' }}></div>
<div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:10 }}>
{active.recurring && (
<span style={{ display:'inline-flex', alignItems:'center', gap:4, background:'rgba(255,255,255,0.22)', color:'#fff', fontSize:10, fontWeight:600, letterSpacing:'0.06em', padding:'3px 8px', borderRadius:9999, fontFamily:T.fontBody }}>
<svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4a8 8 0 1 0 7.75 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
HÀNG THÁNG
</span>
)}
<span style={{ fontSize:11, opacity:0.85, fontVariantNumeric:'tabular-nums' }}>{active.period}</span>
</div>
<div style={{ fontFamily:T.fontDisplay, fontWeight:700, fontSize:19, marginBottom:14, letterSpacing:'-0.01em' }}>{active.title}</div>
<div style={{ display:'flex', alignItems:'flex-end', gap:10, marginBottom:8 }}>
<div>
<div style={{ fontSize:10, opacity:0.78, marginBottom:2, letterSpacing:'0.06em', textTransform:'uppercase', fontWeight:600 }}>Còn lại</div>
<div style={{ fontFamily:T.fontDisplay, fontSize:32, fontWeight:700, letterSpacing:'-0.015em', fontVariantNumeric:'tabular-nums', lineHeight:1 }}>{short(remaining)}</div>
</div>
<div style={{ flex:1 }}/>
<div style={{ textAlign:'right' }}>
<div style={{ fontSize:10, opacity:0.78, marginBottom:2, letterSpacing:'0.06em', textTransform:'uppercase', fontWeight:600 }}>Đã chi</div>
<div style={{ fontFamily:T.fontDisplay, fontSize:16, fontWeight:600, fontVariantNumeric:'tabular-nums' }}>{short(active.spent)} / {short(active.total)}</div>
</div>
</div>
<div style={{ height:8, borderRadius:9999, background:'rgba(255,255,255,0.22)', overflow:'hidden' }}>
<div style={{ height:'100%', width:`${pct}%`, background:'#fff', borderRadius:9999, boxShadow:'0 0 8px rgba(255,255,255,0.6)' }}/>
</div>
</div>
{/* Peek of next card */}
<div style={{ flex:'0 0 44px', height:200, background:budgets[1].gradient, borderRadius:22, boxShadow:'0 8px 20px rgba(95,39,205,0.20)', opacity:0.85 }}/>
</div>
{/* Pagination dots */}
<div style={{ display:'flex', justifyContent:'center', gap:6, marginTop:12 }}>
{budgets.map((b,i) => (
<span key={b.id} style={{ width:i===0?18:6, height:6, borderRadius:9999, background:i===0?T.crimson:P.inkMuted, opacity:i===0?1:0.4 }}/>
))}
</div>
</div>
{/* Allocations list */}
<div style={{ padding:'0 20px 12px', display:'flex', flexDirection:'column', gap:8, overflow:'hidden', flex:1 }}>
<div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', padding:'0 2px 4px' }}>
<div style={{ fontSize:11, fontWeight:700, color:P.inkSoft, letterSpacing:'0.08em', textTransform:'uppercase' }}>Theo danh mục</div>
<div style={{ fontSize:11, color:P.inkMuted }}>{allocations.length} danh mục</div>
</div>
{allocations.slice(0,2).map((a,i) => {
const p = Math.round((a.spent/a.budget)*100);
const over = p>100;
return (
<div key={i} style={{ background:P.surface, borderRadius:14, padding:'11px 14px', boxShadow:'0 2px 10px rgba(174,47,52,0.05)', border:`1px solid ${P.border}` }}>
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:7 }}>
<div style={{ width:10, height:10, borderRadius:'50%', background:a.color }}/>
<div style={{ flex:1, fontSize:13, fontWeight:600, color:P.ink }}>{a.name}</div>
<div style={{ fontSize:12, fontWeight:700, color:over?T.crimson:'#F5A623', fontFamily:T.fontDisplay, fontVariantNumeric:'tabular-nums' }}>{p}%</div>
</div>
<div style={{ height:5, borderRadius:9999, background:P.muted, overflow:'hidden' }}>
<div style={{ height:'100%', width:`${Math.min(100,p)}%`, background:over?T.crimson:`linear-gradient(90deg, ${a.color}, ${a.color}cc)`, borderRadius:9999 }}/>
</div>
</div>
);
})}
</div>
<BottomNav active="budget" dark={dark}/>
</div>
);
}
// ── App ────────────────────────────────────────────────────────
function Phone({ dark, children, label }) {
return (
<div className="artboard">
<IOSDevice width={390} height={844} dark={dark}>{children}</IOSDevice>
<div className="artboard-label">{label}</div>
</div>
);
}
function App() {
const [dark, setDark] = React.useState(false);
return (
<div>
{/* Tweaks bar */}
<div style={{ position:'fixed', top:16, right:16, zIndex:100, background:'#FFF8F6', border:'1px solid #EDD9D3', borderRadius:12, padding:'10px 14px', display:'flex', alignItems:'center', gap:10, boxShadow:'0 4px 16px rgba(174,47,52,0.12)' }}>
<span style={{ fontFamily:"'Chakra Petch',sans-serif", fontSize:11, fontWeight:600, letterSpacing:'0.12em', textTransform:'uppercase', color:'#AE2F34' }}>Theme</span>
{['light','dark'].map(v => (
<button key={v} onClick={() => setDark(v==='dark')} style={{ border:0, background:(dark?'dark':'light')===v?'#AE2F34':'transparent', color:(dark?'dark':'light')===v?'#fff':'#7A5C56', fontFamily:"'Outfit',sans-serif", fontSize:11, fontWeight:500, padding:'4px 10px', borderRadius:9999, cursor:'pointer' }}>{v}</button>
))}
</div>
<div style={{ padding:'60px 60px 80px' }}>
<div style={{ marginBottom:60 }}>
<div className="section-title">Budget · add flow</div>
<div className="section-sub">Step 1: period, name, scope, cadence → Step 2: per-category allocations with keypad</div>
<div className="row" style={{ marginTop:32 }}>
<Phone dark={dark} label="19 · Add budget · period + total"><CCBudgetAddStep1 dark={dark}/></Phone>
<Phone dark={dark} label="20 · Add budget · allocate + keypad"><CCBudgetAddStep2 dark={dark}/></Phone>
</div>
</div>
<div>
<div className="section-title">Budget · multiple periods</div>
<div className="section-sub">Carousel switcher when N≥2 active budgets · swipe-to-delete on individual cards</div>
<div className="row" style={{ marginTop:32 }}>
<Phone dark={dark} label="22 · Budget · multiple periods"><CCBudgetMulti dark={dark}/></Phone>
<Phone dark={dark} label="21 · Budget · swipe to delete"><CCBudgetSwipeDelete dark={dark}/></Phone>
</div>
</div>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment