/* ============================================================
Icons (functional, line-based)
============================================================ */
const Icon = ({ name, size = 32, stroke = 2, color = "currentColor" }) => {
const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" };
switch (name) {
case "arrow-right": return ;
case "arrow-left": return ;
case "arrow-up-right": return ;
case "check": return ;
case "x": return ;
case "alert": return ;
case "info": return ;
case "lock": return ;
case "user": return ;
case "card": return ;
case "iris": return ;
case "cash": return ;
case "mix": return ;
case "qr": return ;
case "scan": return ;
case "mail": return ;
case "printer": return ;
case "clock": return ;
case "graduation": return ;
case "school": return ;
case "help": return ;
case "language": return ;
case "refresh": return ;
case "wifi-off": return ;
case "tools": return ;
case "banknote-x": return ;
case "campus": return ;
case "shield-check": return ;
case "menu": return ;
case "eye-off": return ;
case "delete": return ;
default: return ;
}
};
window.Icon = Icon;
/* ============================================================
Logo (MC GR Portrait)
============================================================ */
const MCLogo = ({ height = 96, white = false }) => (
);
window.MCLogo = MCLogo;
/* ============================================================
Header — three variants: brand, compact, minimal
============================================================ */
const Header = ({ variant = "brand", showSteps = false, step = 0, totalSteps = 5, onCancel, hideLang = false, leftAction, rightAction }) => {
const t = useT();
const lang = window.AppState.lang;
const setLang = window.AppState.setLang;
const cls = variant === "compact" ? "screen-header compact" : variant === "minimal" ? "screen-header minimal" : "screen-header";
return (
);
};
window.Header = Header;
/* ============================================================
Big touch button — mirrors attached home screen vibe
============================================================ */
const BigButton = ({ children, onClick, variant = "primary", icon, disabled }) => (
);
window.BigButton = BigButton;
/* ============================================================
Option tile (for two- or three-up choices)
============================================================ */
const OptionTile = ({ icon, title, sub, selected, onClick, badge }) => (
);
window.OptionTile = OptionTile;
/* ============================================================
Status panel (success / error / warning / info)
============================================================ */
const StatusPanel = ({ kind = "success", icon, title, sub, children }) => (
{title}
{sub &&
{sub}
}
{children}
);
window.StatusPanel = StatusPanel;
/* ============================================================
Numeric keypad (large kiosk-friendly)
============================================================ */
const Keypad = ({ onKey, onDelete, onClear, onSubmit, submitLabel = "OK", canSubmit = true }) => {
const t = useT();
return (
{[1,2,3,4,5,6,7,8,9].map(n => (
))}
);
};
window.Keypad = Keypad;
/* ============================================================
QR placeholder — generated SVG pattern (deterministic)
============================================================ */
const QRPlaceholder = ({ seed = "MC" }) => {
const size = 25;
const cells = React.useMemo(() => {
let h = 0;
for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) | 0;
const grid = [];
for (let y = 0; y < size; y++) {
const row = [];
for (let x = 0; x < size; x++) {
// fixed-position blocks for QR markers
const isFinder = (x < 7 && y < 7) || (x > size - 8 && y < 7) || (x < 7 && y > size - 8);
if (isFinder) {
const fx = x < 7 ? x : x - (size - 7);
const fy = y < 7 ? y : y - (size - 7);
const onEdge = fx === 0 || fx === 6 || fy === 0 || fy === 6;
const onCenter = fx >= 2 && fx <= 4 && fy >= 2 && fy <= 4;
row.push(onEdge || onCenter);
} else {
h = (h * 1103515245 + 12345) | 0;
row.push(((h >>> 16) & 1) === 1);
}
}
grid.push(row);
}
return grid;
}, [seed]);
const cellSize = 100 / size;
return (
);
};
window.QRPlaceholder = QRPlaceholder;
/* ============================================================
Pulsing scan target (kiosk-style "place code here" indicator)
============================================================ */
const ScanTarget = ({ icon = "qr" }) => (
);
window.ScanTarget = ScanTarget;
/* ============================================================
Footer/secure label
============================================================ */
const SecureFooter = () => {
const t = useT();
return (
{t.secured}
·
{t.classter}
);
};
window.SecureFooter = SecureFooter;
/* ============================================================
Money formatting
============================================================ */
window.fmtEUR = (n, lang = 'el') => {
const formatted = new Intl.NumberFormat(lang === 'el' ? 'el-GR' : 'en-GB', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2 }).format(n);
return formatted;
};
/* ============================================================
Modal
============================================================ */
const Modal = ({ children, onClose }) => (
e.stopPropagation()}>{children}
);
window.Modal = Modal;