// Shared atoms used across all 5 variations.
// Compact and self-contained — no external imports.
const { useState, useMemo } = React;
const fmtKRW = (n, short = false) => {
if (n == null) return "—";
const abs = Math.abs(n);
if (short) {
if (abs >= 1e8) return `${(n / 1e8).toFixed(2)}억`;
if (abs >= 1e4) {
// One decimal under ~100만 so small figures (fees, deltas, harvest) don't
// round to a misleading single-digit '만' (₩12,345 → '1.2만', not '1만').
const man = n / 1e4;
return abs < 1e6 ? `${man.toFixed(1)}만` : `${Math.round(man).toLocaleString()}만`;
}
}
return n.toLocaleString("ko-KR");
};
const fmtPct = (n, signed = true) => {
if (n == null || !Number.isFinite(n)) return "—";
const s = signed && n >= 0 ? "+" : "";
return `${s}${n.toFixed(2)}%`;
};
const fmtSigned = (n, short = false) => {
if (n == null || !Number.isFinite(n)) return "—";
const s = n >= 0 ? "+" : "−";
return `${s}₩${fmtKRW(Math.abs(n), short)}`;
};
const upDown = n => (Number.isFinite(n) ? (n >= 0 ? "up" : "down") : "");
// ─── Toss top app bar ───
function AppBar({ title, leading = "‹", trailing, onLeading }) {
return (
{leading ? (
) : null}
{title}
{trailing}
);
}
// ─── Bottom tab bar (5 tabs matching Streamlit nav) ───
function TabBar({ active = "home", onTab }) {
const tabs = [
{ id: "home", label: "홈", icon: "M3 11l9-8 9 8v10a1 1 0 01-1 1h-5v-7h-6v7H4a1 1 0 01-1-1V11z" },
{ id: "accounts", label: "계좌", icon: "M3 7h18v12H3zM3 7l3-3h12l3 3" },
{ id: "tax", label: "세금", icon: "M5 4h11l3 3v13H5z M9 9h6 M9 13h6 M9 17h4" },
{ id: "flow", label: "자금", icon: "M4 12h12 M12 8l4 4-4 4 M20 6v12" },
{ id: "history", label: "내역", icon: "M12 8v4l3 2 M3 12a9 9 0 109-9" },
];
return (
{tabs.map(t => (
))}
);
}
// ─── Account avatar (initial-style colored badge) ───
function AccountAvatar({ name, kind = "domestic" }) {
const colorMap = {
domestic: "linear-gradient(135deg,#3b82f6,#1e40af)",
us: "linear-gradient(135deg,#ef4444,#991b1b)",
isa: "linear-gradient(135deg,#8a4dab,#6f2b8f)",
};
const ch = (name || "?").slice(0, 2);
return (
{ch}
);
}
// ─── Flow row icon by category ───
const FLOW_META = {
deposit: { cls: "dep", ico: "↓", label: "입금" },
withdrawal: { cls: "wd", ico: "↑", label: "출금" },
internal_transfer: { cls: "int", ico: "⇄", label: "계좌이체" },
internal_in: { cls: "int", ico: "⇄", label: "이체 입금" },
internal_out: { cls: "int", ico: "⇄", label: "이체 출금" },
fx_krw_to_usd: { cls: "fx", ico: "₩→$", label: "환전" },
fx_usd_to_krw: { cls: "fx", ico: "$→₩", label: "환전" },
krx_buy: { cls: "buy", ico: "B", label: "매수" },
krx_sell: { cls: "sell", ico: "S", label: "매도" },
us_buy: { cls: "buy", ico: "B", label: "매수" },
us_sell: { cls: "sell", ico: "S", label: "매도" },
dividend: { cls: "div", ico: "₩", label: "배당" },
interest: { cls: "int", ico: "%", label: "이자" },
};
function FlowIcon({ category }) {
const m = FLOW_META[category] || FLOW_META.deposit;
return {m.ico}
;
}
Object.assign(window, {
fmtKRW, fmtPct, fmtSigned, upDown,
AppBar, TabBar, AccountAvatar, FlowIcon, FLOW_META,
});