// ============================================
// MariusVPN — UI primitives
// Exposes: Card, Button, IconButton, Badge, Toggle, Toast, ToastHost,
// Modal, Sheet, ProgressBar, Skeleton, Logo, Field, Spinner, Copy
// ============================================
const { useState, useEffect, useRef, useCallback, createContext, useContext, useMemo } = React;
const { Icon, FMT } = window;
// ---------- Logo ----------
const Logo = ({ size = 28, withText = true, className = '' }) => (
);
// ---------- Card ----------
const Card = ({ className = '', children, padding = 'p-5', as: As = 'div', ...rest }) => (
{children}
);
// ---------- Button ----------
const Button = ({
children, variant = 'primary', size = 'md', icon, iconRight, loading, disabled,
fullWidth = false, className = '', as: As = 'button', ...rest
}) => {
const sizes = {
sm: 'h-9 px-3.5 text-[13px] gap-1.5 rounded-xl',
md: 'h-11 px-4 text-sm gap-2 rounded-xl',
lg: 'h-12 px-5 text-[15px] gap-2 rounded-xl font-medium',
};
const variants = {
primary: 'bg-m-accent text-m-bg hover:brightness-110 active:brightness-95 font-medium shadow-glow disabled:bg-m-line2 disabled:text-m-mute disabled:shadow-none',
secondary:'bg-white/[0.06] text-m-text hover:bg-white/[0.10] border border-white/5',
ghost: 'text-m-text hover:bg-white/[0.06]',
danger: 'bg-m-err/15 text-m-err hover:bg-m-err/25 border border-m-err/30',
gold: 'bg-m-gold text-m-bg hover:brightness-110 font-medium',
outline: 'border border-white/10 text-m-text hover:bg-white/[0.05]',
};
const cls = `inline-flex items-center justify-center transition-all duration-150 active:scale-[0.98]
disabled:cursor-not-allowed disabled:active:scale-100
${sizes[size]} ${variants[variant]} ${fullWidth ? 'w-full' : ''} ${className}`;
return (
{loading ? : icon && }
{children}
{iconRight && }
);
};
const IconButton = ({ icon, size = 'md', variant = 'ghost', className = '', dot = false, ...rest }) => {
const dims = size === 'sm' ? 'w-9 h-9' : 'w-10 h-10';
const variants = {
ghost: 'bg-white/[0.04] hover:bg-white/[0.08] border border-white/5',
solid: 'bg-m-card border border-white/5 hover:bg-m-card-hi',
};
return (
);
};
// ---------- Badge ----------
const Badge = ({ children, tone = 'neutral', size = 'md', icon, className = '' }) => {
const tones = {
neutral: 'bg-white/[0.06] text-m-mute border-white/5',
accent: 'bg-m-accent/15 text-m-accent border-m-accent/30',
ok: 'bg-m-ok/15 text-m-ok border-m-ok/30',
err: 'bg-m-err/15 text-m-err border-m-err/30',
gold: 'bg-m-gold/15 text-m-gold border-m-gold/30',
warn: 'bg-m-gold/15 text-m-gold border-m-gold/30',
};
const sizes = { sm: 'h-5 px-2 text-[10px] gap-1', md: 'h-6 px-2.5 text-[11px] gap-1.5' };
return (
{icon && }
{children}
);
};
// ---------- Toggle ----------
const Toggle = ({ checked, onChange, disabled = false, size = 'md' }) => {
const dims = size === 'sm' ? { w: 40, h: 24, k: 18 } : { w: 48, h: 28, k: 22 };
return (
);
};
// ---------- ProgressBar ----------
const ProgressBar = ({ value, max = 100, tone, height = 6, className = '' }) => {
const pct = Math.min(100, Math.max(0, (value / max) * 100));
// Auto-tone: cyan → gold → red as it approaches max
let auto = 'bg-gradient-to-r from-m-accent to-cyan-300';
if (pct >= 90) auto = 'bg-gradient-to-r from-m-err to-rose-400';
else if (pct >= 75) auto = 'bg-gradient-to-r from-m-gold to-amber-300';
const toneClass = tone || auto;
return (
);
};
// ---------- Skeleton ----------
const Skeleton = ({ className = 'h-4 w-full', rounded = 'rounded-md' }) => (
);
// ---------- Spinner ----------
const Spinner = ({ size = 16, className = '' }) => (
);
// ---------- Field (input wrapper) ----------
const Field = ({ label, hint, error, children, className = '' }) => (
);
const Input = ({ className = '', ...rest }) => (
);
const Select = ({ className = '', children, ...rest }) => (
);
// ---------- Copy field ----------
const CopyField = ({ value, mono = true, className = '' }) => {
const [copied, setCopied] = useState(false);
const onCopy = () => {
navigator.clipboard?.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1400);
};
return (
{value}
);
};
// ---------- Modal ----------
const Modal = ({ open, onClose, title, children, footer, maxWidth = 'max-w-md', dismissible = true }) => {
useEffect(() => {
if (!open) return;
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const onKey = (e) => e.key === 'Escape' && dismissible && onClose?.();
document.addEventListener('keydown', onKey);
return () => {
document.body.style.overflow = prev;
document.removeEventListener('keydown', onKey);
};
}, [open, dismissible, onClose]);
if (!open) return null;
return (
e.target === e.currentTarget && dismissible && onClose?.()}>
{title && (
{title}
{dismissible && (
)}
)}
{children}
{footer &&
{footer}
}
);
};
// ---------- Bottom Sheet (mobile) / side drawer (desktop) ----------
const Sheet = ({ open, onClose, title, children, side = 'auto' }) => {
useEffect(() => {
if (!open) return;
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const onKey = (e) => e.key === 'Escape' && onClose?.();
document.addEventListener('keydown', onKey);
return () => { document.body.style.overflow = prev; document.removeEventListener('keydown', onKey); };
}, [open, onClose]);
if (!open) return null;
return (
{/* Mobile: bottom; Desktop: right */}
);
};
// ---------- Toast ----------
const ToastCtx = createContext(null);
const useToast = () => useContext(ToastCtx);
const ToastHost = ({ children }) => {
const [toasts, setToasts] = useState([]);
const push = useCallback((t) => {
const id = Math.random().toString(36).slice(2);
setToasts((arr) => [...arr, { id, tone: 'neutral', ...t }]);
setTimeout(() => setToasts((arr) => arr.filter((x) => x.id !== id)), t.duration || 3000);
}, []);
const ctx = useMemo(() => ({
show: push,
success: (msg) => push({ tone: 'ok', msg }),
error: (msg) => push({ tone: 'err', msg }),
info: (msg) => push({ tone: 'accent', msg }),
}), [push]);
return (
{children}
);
};
// ---------- ScreenHeader ----------
const ScreenHeader = ({ title, subtitle, right, onBack }) => (
{onBack && (
)}
{title}
{subtitle &&
{subtitle}
}
{right && {right}
}
);
// ---------- KV Row (a label/value row for lists) ----------
const KV = ({ label, value, mono = false, className = '' }) => (
);
// expose
Object.assign(window, {
Logo, Card, Button, IconButton, Badge, Toggle, ProgressBar, Skeleton, Spinner,
Field, Input, Select, CopyField,
Modal, Sheet, ToastHost, useToast,
ScreenHeader, KV,
});