// ============================================ // 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 = '' }) => (
{withText && (
MariusVPN
)}
); // ---------- 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}
{toasts.map((t) => (
{t.msg}
))}
); }; // ---------- 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 = '' }) => (
{label}
{value}
); // expose Object.assign(window, { Logo, Card, Button, IconButton, Badge, Toggle, ProgressBar, Skeleton, Spinner, Field, Input, Select, CopyField, Modal, Sheet, ToastHost, useToast, ScreenHeader, KV, });