540 lines
21 KiB
React
540 lines
21 KiB
React
import { useRef, useState, useCallback, useContext, createContext } from "react";
|
||
import { EMOJI_OPTIONS } from "../../constants/index.js";
|
||
import { useApp } from "../../context/AppContext.jsx";
|
||
|
||
const DragContext = createContext(null);
|
||
|
||
// ── VCard ─────────────────────────────────────────────────────────────────────
|
||
export function VCard({ children, style = {} }) {
|
||
return (
|
||
<div style={{
|
||
borderRadius: 12, padding: "16px 18px",
|
||
boxShadow: "none",
|
||
...style,
|
||
}}>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── VBadge ────────────────────────────────────────────────────────────────────
|
||
export function VBadge({ label, color }) {
|
||
const { T } = useApp();
|
||
const kleur = color ?? T.accentTekst;
|
||
return (
|
||
<span style={{
|
||
background: `${kleur}22`, color: kleur,
|
||
border: `1px solid ${kleur}44`,
|
||
borderRadius: 99, padding: "2px 10px",
|
||
fontSize: 11, fontWeight: 700,
|
||
}}>
|
||
{label}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
// ── TabBtn ────────────────────────────────────────────────────────────────────
|
||
export function TabBtn({ active, onClick, children }) {
|
||
const { T } = useApp();
|
||
return (
|
||
<button
|
||
onClick={onClick}
|
||
style={{
|
||
padding: "8px 16px", border: "none", borderRadius: 8, cursor: "pointer",
|
||
fontSize: 13, fontWeight: active ? 700 : 500,
|
||
background: active ? `${T.accent}22` : "transparent",
|
||
color: active ? T.accentTekst : T.muted,
|
||
borderBottom: active ? `2px solid ${T.accentTekst}` : "2px solid transparent",
|
||
transition: "all 0.15s",
|
||
}}
|
||
>
|
||
{children}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
// ── VProgressBar ──────────────────────────────────────────────────────────────
|
||
export function VProgressBar({ value, max, color }) {
|
||
const { T } = useApp();
|
||
const kleur = color ?? T.accent;
|
||
const p = Math.min((value / (max || 1)) * 100, 100);
|
||
return (
|
||
<div style={{ height: 8, background: "rgba(255,255,255,0.07)", borderRadius: 99, overflow: "hidden" }}>
|
||
<div style={{
|
||
height: "100%", width: `${p}%`, background: kleur,
|
||
borderRadius: 99, transition: "width 0.5s ease",
|
||
}} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── DonutChart ────────────────────────────────────────────────────────────────
|
||
export function DonutChart({ segments, total, size = 120, label, centerVal }) {
|
||
const { darkMode, T } = useApp();
|
||
const wrapRef = useRef(null);
|
||
const [hover, setHover] = useState(null); // { i, x, y } — actieve categorie + cursor
|
||
|
||
const strokeW = size * 0.155;
|
||
const r = (size - strokeW) / 2 - 1;
|
||
const cx = size / 2, cy = size / 2;
|
||
const circ = 2 * Math.PI * r;
|
||
const active = segments.filter((s) => s.val > 0);
|
||
const multi = active.length > 1;
|
||
|
||
// Middenwaarde schaalt mee met de beschikbare ruimte binnen de ring, zodat
|
||
// lange bedragen niet uit verhouding raken.
|
||
const innerW = (2 * r - strokeW) * 0.92;
|
||
const valLen = centerVal ? String(centerVal).length : 0;
|
||
const valFont = valLen
|
||
? Math.min(size * 0.125, innerW / (valLen * 0.6))
|
||
: 0;
|
||
|
||
// Segmenten sluiten naadloos aan (geen tussenruimte).
|
||
let off = 0;
|
||
const slices = active.map((s) => {
|
||
const raw = (s.val / total) * circ;
|
||
const sl = { ...s, dash: Math.max(raw, 1), offset: off, share: total ? s.val / total : 0 };
|
||
off += raw;
|
||
return sl;
|
||
});
|
||
|
||
const track = (i) => (e) => {
|
||
const box = wrapRef.current?.getBoundingClientRect();
|
||
setHover({
|
||
i,
|
||
x: box ? e.clientX - box.left : cx,
|
||
y: box ? e.clientY - box.top : cy,
|
||
});
|
||
};
|
||
const hovered = hover ? slices[hover.i] : null;
|
||
const fmtPct = (v) => `${(v * 100).toFixed(1).replace(".", ",")}%`;
|
||
const fmtEur = (v) =>
|
||
new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(v || 0);
|
||
|
||
return (
|
||
<div ref={wrapRef} style={{ position: "relative", width: size, height: size, lineHeight: 0 }}>
|
||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
||
{/* Baan — zelfde toon als de voortgangsbalken */}
|
||
<circle cx={cx} cy={cy} r={r} fill="none"
|
||
stroke={darkMode ? "rgba(255,255,255,0.07)" : "rgba(15,23,42,0.07)"}
|
||
strokeWidth={strokeW} />
|
||
|
||
{/* Segmenten */}
|
||
{slices.map((s, i) => (
|
||
<circle key={`c${i}`} cx={cx} cy={cy} r={r} fill="none"
|
||
stroke={s.color} strokeWidth={hover && hover.i === i ? strokeW * 1.1 : strokeW}
|
||
strokeDasharray={`${s.dash} ${circ - s.dash}`}
|
||
strokeDashoffset={-s.offset + circ / 4}
|
||
strokeLinecap={multi ? "butt" : "round"}
|
||
opacity={hover && hover.i !== i ? 0.35 : 1}
|
||
style={{ cursor: "pointer", transition: "opacity 0.15s ease, stroke-width 0.15s ease" }}
|
||
onMouseEnter={track(i)}
|
||
onMouseMove={track(i)}
|
||
onMouseLeave={() => setHover(null)}
|
||
/>
|
||
))}
|
||
|
||
{/* Center label */}
|
||
{label && (
|
||
<text x={cx} y={cy - valFont * 0.62} fill={T.muted}
|
||
fontSize={size * 0.085} textAnchor="middle" dominantBaseline="middle"
|
||
fontFamily="inherit" style={{ pointerEvents: "none" }}>
|
||
{label}
|
||
</text>
|
||
)}
|
||
{/* Center value */}
|
||
{centerVal && (
|
||
<text x={cx} y={label ? cy + valFont * 0.45 : cy} fill={T.text}
|
||
fontSize={valFont} fontWeight="800" textAnchor="middle" dominantBaseline="middle"
|
||
fontFamily="inherit" style={{ pointerEvents: "none" }}>
|
||
{centerVal}
|
||
</text>
|
||
)}
|
||
</svg>
|
||
|
||
{/* Tooltip: categorie, bedrag en aandeel */}
|
||
{hovered && (
|
||
<div style={{
|
||
position: "absolute", left: hover.x, top: hover.y - 12,
|
||
transform: "translate(-50%, -100%)", pointerEvents: "none",
|
||
background: T.popupBg, border: `1px solid ${T.border}`,
|
||
borderRadius: 10, padding: "6px 10px", boxShadow: T.shadow,
|
||
whiteSpace: "nowrap", lineHeight: 1.35, zIndex: 5,
|
||
}}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||
<span style={{ width: 8, height: 8, borderRadius: "50%", background: hovered.color }} />
|
||
<span style={{ fontSize: 11, fontWeight: 700, color: T.text }}>
|
||
{hovered.naam || hovered.label || "—"}
|
||
</span>
|
||
</div>
|
||
<div style={{ fontSize: 11, color: T.subtext, marginTop: 2 }}>
|
||
{fmtEur(hovered.val)} · <span style={{ fontWeight: 700, color: T.text }}>{fmtPct(hovered.share)}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── SvgLineChart ──────────────────────────────────────────────────────────────
|
||
export function SvgLineChart({ pts, lines, T, H = 180 }) {
|
||
if (pts.length < 2) {
|
||
return (
|
||
<div style={{ color: T.muted, fontSize: 12, textAlign: "center", padding: "24px 0" }}>
|
||
Minimaal 2 datapunten nodig
|
||
</div>
|
||
);
|
||
}
|
||
const W = 580, pl = 64, pr = 14, pt2 = 16, pb = 30;
|
||
const iW = W - pl - pr, iH = H - pt2 - pb;
|
||
const allVals = lines.flatMap((l) => pts.map((p) => p[l.key]));
|
||
const mn = Math.min(0, ...allVals);
|
||
const mx = Math.max(...allVals) * 1.08 || 1;
|
||
const rng = mx - mn || 1;
|
||
const xp = (i) => pl + (i / (pts.length - 1)) * iW;
|
||
const yp = (v) => pt2 + (1 - (v - mn) / rng) * iH;
|
||
const gc = T.bg === "#0f1117" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)";
|
||
|
||
|
||
return (
|
||
<div style={{ overflowX: "auto" }}>
|
||
<svg viewBox={`0 0 ${W} ${H}`}
|
||
style={{ width: "100%", minWidth: 340, height: "auto", display: "block" }}>
|
||
<defs>
|
||
{lines.map((l) => (
|
||
<linearGradient key={l.key} id={`g_${l.key}`} x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor={l.color} stopOpacity="0.25" />
|
||
<stop offset="100%" stopColor={l.color} stopOpacity="0.02" />
|
||
</linearGradient>
|
||
))}
|
||
</defs>
|
||
{[0, 0.25, 0.5, 0.75, 1].map((f) => {
|
||
const gy = pt2 + (1 - f) * iH;
|
||
const v = mn + f * rng;
|
||
return (
|
||
<g key={f}>
|
||
<line x1={pl} y1={gy} x2={W - pr} y2={gy} stroke={gc} strokeWidth="1" />
|
||
<text x={pl - 4} y={gy + 4} fill={T.muted} fontSize="8" textAnchor="end">
|
||
{new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }).format(v)}
|
||
</text>
|
||
</g>
|
||
);
|
||
})}
|
||
{lines.map((l) => {
|
||
const xs = pts.map((_, i) => xp(i));
|
||
const ys = pts.map((p) => yp(p[l.key]));
|
||
const ls = xs.map((x, i) => `${x},${ys[i]}`).join(" ");
|
||
const ar = `${xs[0]},${pt2 + iH} ${ls} ${xs[xs.length - 1]},${pt2 + iH}`;
|
||
return (
|
||
<g key={l.key}>
|
||
<polygon points={ar} fill={`url(#g_${l.key})`} />
|
||
<polyline points={ls} fill="none" stroke={l.color} strokeWidth="2.5"
|
||
strokeLinejoin="round" strokeLinecap="round" />
|
||
{xs.map((x, i) => (
|
||
<circle key={i} cx={x} cy={ys[i]} r="4"
|
||
fill={l.color} stroke={T.card} strokeWidth="2" />
|
||
))}
|
||
</g>
|
||
);
|
||
})}
|
||
{pts.map((p, i) => {
|
||
const [y, m, d] = (p.datum || "").split("-");
|
||
const label = d ? `${d}-${m}-${y}` : p.datum;
|
||
return (
|
||
<text key={i} x={xp(i)} y={H - 8} fill={T.muted} fontSize="8" textAnchor="middle">
|
||
{label}
|
||
</text>
|
||
);
|
||
})}
|
||
</svg>
|
||
<div style={{ display: "flex", gap: 20, justifyContent: "center", marginTop: 10 }}>
|
||
{lines.map((l) => (
|
||
<div key={l.key} style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||
<div style={{ width: 18, height: 3, background: l.color, borderRadius: 2 }} />
|
||
<div style={{ width: 8, height: 8, borderRadius: "50%", background: l.color }} />
|
||
<span style={{ fontSize: 12, color: T.muted }}>{l.label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── EthIcon ───────────────────────────────────────────────────────────────────
|
||
export function EthIcon({ size = 18, color = "#3b82f6" }) {
|
||
const w = size * 0.6, h = size;
|
||
return (
|
||
<svg width={w} height={h} viewBox="0 0 60 100" xmlns="http://www.w3.org/2000/svg"
|
||
style={{ display: "inline-block", verticalAlign: "middle" }}>
|
||
<polygon points="30,2 58,50 30,38" fill={color} opacity="1" />
|
||
<polygon points="2,50 30,2 30,38" fill={color} opacity="0.6" />
|
||
<polygon points="30,62 58,50 30,38" fill={color} opacity="0.6" />
|
||
<polygon points="2,50 30,38 30,62" fill={color} opacity="0.3" />
|
||
<polygon points="30,98 58,50 30,62" fill={color} opacity="0.6" />
|
||
<polygon points="2,50 30,62 30,98" fill={color} opacity="1" />
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
// ── BtcIcon ───────────────────────────────────────────────────────────────────
|
||
export function BtcIcon({ size = 18, color = "#f59e0b" }) {
|
||
return (
|
||
<svg width={size} height={size} viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"
|
||
style={{ display: "inline-block", verticalAlign: "middle" }}>
|
||
<circle cx="32" cy="32" r="30" fill={color} />
|
||
<text x="32" y="44" textAnchor="middle" fontSize="36" fontWeight="bold"
|
||
fontFamily="Arial, sans-serif" fill="#fff">₿</text>
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
// ── GoldIcon ──────────────────────────────────────────────────────────────────
|
||
export function GoldIcon({ size = 24 }) {
|
||
return (
|
||
<svg width={size} height={size * 0.72} viewBox="0 0 100 72" xmlns="http://www.w3.org/2000/svg"
|
||
style={{ display: "inline-block", verticalAlign: "middle" }}>
|
||
{/* Bovenkant */}
|
||
<polygon points="18,4 82,4 100,20 0,20" fill="#fcd34d" />
|
||
{/* Voorkant */}
|
||
<rect x="0" y="20" width="100" height="40" fill="#f59e0b" />
|
||
{/* Rechterkant */}
|
||
<polygon points="100,20 100,60 82,72 82,36" fill="#d97706" />
|
||
{/* Linkeronderkant */}
|
||
<polygon points="0,60 0,20 18,4 18,44" fill="#fbbf24" />
|
||
{/* Onderkant schaduw */}
|
||
<polygon points="0,60 82,60 100,72 18,72" fill="#b45309" />
|
||
{/* Streep highlight bovenkant */}
|
||
<polygon points="28,8 72,8 88,18 12,18" fill="#fde68a" opacity="0.5" />
|
||
{/* GOLD tekst */}
|
||
<text x="50" y="46" textAnchor="middle" fontSize="18" fontWeight="800"
|
||
fontFamily="Arial, sans-serif" fill="#92400e" letterSpacing="2">GOLD</text>
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
// ── Modal primitieven ─────────────────────────────────────────────────────────
|
||
export function Overlay({ children, onClose, zIndex = 400 }) {
|
||
return (
|
||
<div
|
||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||
style={{
|
||
position: "fixed", inset: 0,
|
||
background: "rgba(0,0,0,0.45)",
|
||
zIndex,
|
||
}}
|
||
>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ModalBox({ children, maxWidth = 480 }) {
|
||
const { T } = useApp();
|
||
const ref = useRef(null);
|
||
const [pos, setPos] = useState(null);
|
||
const dragState = useRef(null);
|
||
|
||
const startDrag = useCallback((e) => {
|
||
if (e.button !== 0) return;
|
||
e.preventDefault();
|
||
const rect = ref.current.getBoundingClientRect();
|
||
dragState.current = {
|
||
startX: e.clientX,
|
||
startY: e.clientY,
|
||
startLeft: rect.left,
|
||
startTop: rect.top,
|
||
};
|
||
|
||
const onMove = (e) => {
|
||
if (!dragState.current) return;
|
||
setPos({
|
||
x: dragState.current.startLeft + (e.clientX - dragState.current.startX),
|
||
y: dragState.current.startTop + (e.clientY - dragState.current.startY),
|
||
});
|
||
};
|
||
|
||
const onUp = () => {
|
||
dragState.current = null;
|
||
window.removeEventListener("mousemove", onMove);
|
||
window.removeEventListener("mouseup", onUp);
|
||
};
|
||
|
||
window.addEventListener("mousemove", onMove);
|
||
window.addEventListener("mouseup", onUp);
|
||
}, []);
|
||
|
||
const posStyle = pos
|
||
? { left: pos.x, top: pos.y, transform: "none" }
|
||
: { left: "50%", top: "50%", transform: "translate(-50%, -50%)" };
|
||
|
||
return (
|
||
<DragContext.Provider value={startDrag}>
|
||
<div ref={ref} style={{
|
||
position: "fixed",
|
||
...posStyle,
|
||
background: T.popupBg,
|
||
border: `1px solid ${T.border}`,
|
||
borderRadius: 18, width: "100%", maxWidth,
|
||
boxShadow: "0 24px 64px rgba(0,0,0,0.5)",
|
||
overflow: "hidden",
|
||
}}>
|
||
{children}
|
||
</div>
|
||
</DragContext.Provider>
|
||
);
|
||
}
|
||
|
||
export function ModalHeader({ icon, iconBg, title, subtitle, subtitleColor, onClose, headerBg, T }) {
|
||
const startDrag = useContext(DragContext);
|
||
return (
|
||
<div
|
||
onMouseDown={startDrag}
|
||
style={{
|
||
display: "flex", alignItems: "center",
|
||
justifyContent: "space-between",
|
||
padding: "18px 22px",
|
||
borderBottom: `1px solid ${T.border}`,
|
||
background: headerBg,
|
||
cursor: "grab",
|
||
userSelect: "none",
|
||
}}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||
<div style={{
|
||
width: 32, height: 32, borderRadius: 8,
|
||
background: `${iconBg}33`,
|
||
display: "flex", alignItems: "center",
|
||
justifyContent: "center", fontSize: 16,
|
||
}}>
|
||
{icon}
|
||
</div>
|
||
<div>
|
||
<div style={{ fontWeight: 700, fontSize: 15, color: T.text }}>{title}</div>
|
||
<div style={{ fontSize: 11, color: subtitleColor, fontWeight: 600 }}>{subtitle}</div>
|
||
</div>
|
||
</div>
|
||
<button onClick={onClose} onMouseDown={(e) => e.stopPropagation()} style={{
|
||
background: "transparent", border: "none",
|
||
color: T.muted, cursor: "pointer",
|
||
fontSize: 22, lineHeight: 1, padding: 4,
|
||
}}>×</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ModalFooter({ children, T }) {
|
||
return (
|
||
<div style={{
|
||
display: "flex", gap: 10,
|
||
padding: "14px 22px",
|
||
borderTop: `1px solid ${T.border}`,
|
||
}}>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── AppIcon — rendert emoji-string of speciale SVG-iconen ────────────────────
|
||
export function AppIcon({ value, fallback = "🏷", size = 20 }) {
|
||
if (value === "__BTC__") return <BtcIcon size={size} color="#f59e0b" />;
|
||
if (value === "__ETH__") return <EthIcon size={size} color="#627eea" />;
|
||
if (value === "__GOLD__") return <GoldIcon size={size * 1.4} />;
|
||
if (value) return <span style={{ fontSize: size, lineHeight: 1 }}>{value}</span>;
|
||
return <span style={{ fontSize: size, lineHeight: 1 }}>{fallback}</span>;
|
||
}
|
||
|
||
// ── EmojiPicker ───────────────────────────────────────────────────────────────
|
||
const SPECIAL_ICONS = [
|
||
{ key: "__BTC__", node: <BtcIcon size={24} color="#f59e0b" /> },
|
||
{ key: "__ETH__", node: <EthIcon size={18} color="#627eea" /> },
|
||
{ key: "__GOLD__", node: <GoldIcon size={28} /> },
|
||
];
|
||
|
||
export function EmojiPicker({ selected, T, accentColor, onSelect, onClear }) {
|
||
const ac = accentColor || "#f59e0b";
|
||
const btnStyle = (active) => ({
|
||
width: 34, height: 34, borderRadius: 8,
|
||
border: `2px solid ${active ? ac : T.border}`,
|
||
background: active ? `${ac}22` : "transparent",
|
||
cursor: "pointer", fontSize: 18,
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
});
|
||
return (
|
||
<div style={{
|
||
background: T.inputBg, border: `1px solid ${T.border}`,
|
||
borderRadius: 12, padding: 10, marginTop: 8,
|
||
}}>
|
||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6, maxHeight: 200, overflowY: "auto" }}>
|
||
{selected && (
|
||
<button onClick={onClear} style={btnStyle(false)}>
|
||
<span style={{ fontSize: 13, color: T.muted }}>✕</span>
|
||
</button>
|
||
)}
|
||
{SPECIAL_ICONS.map(({ key, node }) => (
|
||
<button key={key} onClick={() => onSelect(key)} style={btnStyle(selected === key)}>
|
||
{node}
|
||
</button>
|
||
))}
|
||
{EMOJI_OPTIONS.map((e) => (
|
||
<button key={e} onClick={() => onSelect(e)} style={btnStyle(selected === e)}>
|
||
{e}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── ConfirmDeleteDialog ───────────────────────────────────────────────────────
|
||
export function ConfirmDeleteDialog({ naam, T, onCancel, onConfirm }) {
|
||
const RED = "#ef4444";
|
||
return (
|
||
<div style={{
|
||
background: "rgba(239,68,68,0.08)",
|
||
border: "1px solid rgba(239,68,68,0.25)",
|
||
borderRadius: 10, padding: "12px 14px",
|
||
}}>
|
||
<div style={{ fontSize: 13, color: RED, fontWeight: 600, marginBottom: 10 }}>
|
||
Weet je zeker dat je <strong>{naam}</strong> wilt verwijderen?
|
||
</div>
|
||
<div style={{ display: "flex", gap: 8 }}>
|
||
<button onClick={onCancel} style={{
|
||
flex: 1, padding: "8px",
|
||
background: "transparent",
|
||
border: `1px solid ${T.border}`,
|
||
borderRadius: 8, color: T.muted,
|
||
cursor: "pointer", fontWeight: 600, fontSize: 13,
|
||
}}>Annuleren</button>
|
||
<button onClick={onConfirm} style={{
|
||
flex: 1, padding: "8px",
|
||
background: "rgba(239,68,68,0.15)",
|
||
border: "1px solid rgba(239,68,68,0.4)",
|
||
borderRadius: 8, color: RED,
|
||
cursor: "pointer", fontWeight: 700, fontSize: 13,
|
||
}}>Ja, verwijderen</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── EmptyState ────────────────────────────────────────────────────────────────
|
||
export function EmptyState({ icon, text, hint, onClick, locked, T }) {
|
||
return (
|
||
<div
|
||
onClick={!locked ? onClick : undefined}
|
||
style={{
|
||
textAlign: "center", padding: "36px 0",
|
||
color: T.muted,
|
||
cursor: locked ? "default" : "pointer",
|
||
border: `2px dashed ${T.border}`,
|
||
borderRadius: 12,
|
||
}}
|
||
>
|
||
<div style={{ fontSize: 32, marginBottom: 10 }}>{icon}</div>
|
||
<div style={{ fontSize: 13, marginBottom: 4 }}>{text}</div>
|
||
{!locked && hint && (
|
||
<div style={{ fontSize: 12, color: T.accentTekst }}>{hint}</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|