Import source: global/src/global-ui

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:27:20 +03:30
parent 69cb684ba0
commit e033130ec4
+498
View File
@@ -0,0 +1,498 @@
// global-ui.jsx — Shared UI components for Teknav Global (English, LTR)
import { useState, useEffect, useRef, useCallback, createContext, useContext } from 'react';
import { authors, categories, siteConfig } from '../global-data.js';
import { api } from './lib/api.js';
// ── Toast System ────────────────────────────────────────────────────────────
const ToastCtx = createContext(null);
function ToastProvider({ children }) {
const [toasts, setToasts] = useState([]);
const add = useCallback((msg, type = 'success') => {
const id = Date.now();
setToasts(t => [...t, { id, msg, type }]);
setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3200);
}, []);
return (
<ToastCtx.Provider value={add}>
{children}
<div style={{ position: 'fixed', bottom: 28, right: 24, zIndex: 9999, display: 'flex', flexDirection: 'column', gap: 10 }}>
{toasts.map(t => (
<div key={t.id} style={{
padding: '11px 20px', borderRadius: 8, color: '#fff', fontSize: 14, fontFamily: 'Inter,sans-serif',
background: t.type === 'error' ? '#C94C4C' : t.type === 'warning' ? '#D08A22' : '#2F8F6B',
boxShadow: '0 4px 20px rgba(0,0,0,0.18)',
animation: 'slideUp 0.25s ease',
}}>{t.msg}</div>
))}
</div>
</ToastCtx.Provider>
);
}
const useToast = () => useContext(ToastCtx);
// ── Navigation Context ──────────────────────────────────────────────────────
const NavCtx = createContext(null);
function getCurrentPage() {
const pathname = window.location.pathname;
const cleanPath = pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
const path = `${cleanPath}${window.location.search}`;
return path === '' ? '/' : path;
}
function NavProvider({ children }) {
const [page, setPage] = useState(getCurrentPage);
const navigate = useCallback((to) => {
if (`${window.location.pathname}${window.location.search}` !== to) {
window.history.pushState(null, '', to);
}
setPage(to);
window.scrollTo(0, 0);
}, []);
useEffect(() => {
const handler = () => setPage(getCurrentPage());
window.addEventListener('popstate', handler);
return () => window.removeEventListener('popstate', handler);
}, []);
return <NavCtx.Provider value={{ page, navigate }}>{children}</NavCtx.Provider>;
}
const useNav = () => useContext(NavCtx);
// ── Auth Context ────────────────────────────────────────────────────────────
const AuthCtx = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [bootstrapping, setBootstrapping] = useState(true);
useEffect(() => {
let cancelled = false;
api.get('/api/auth/me')
.then((res) => { if (!cancelled) setUser(res?.user ?? null); })
.catch(() => {})
.finally(() => { if (!cancelled) setBootstrapping(false); });
return () => { cancelled = true; };
}, []);
const logout = async () => {
try { await api.post('/api/auth/logout', {}); } catch (e) { console.warn('[auth/logout]', e); }
setUser(null);
};
return <AuthCtx.Provider value={{ user, logout, bootstrapping }}>{children}</AuthCtx.Provider>;
}
const useAuth = () => useContext(AuthCtx);
// ── Badges ──────────────────────────────────────────────────────────────────
const TYPE_COLORS = {
'Deep Analysis': { bg: '#0F6B73', text: '#fff' },
'Technical Guide': { bg: '#D49A2A', text: '#fff' },
'Data Viz': { bg: '#C76D4A', text: '#fff' },
'Research': { bg: '#2F8F6B', text: '#fff' },
'Fact-Checked': { bg: '#263238', text: '#fff' },
'Breaking': { bg: '#C94C4C', text: '#fff' },
'Analysis': { bg: '#5F6B6D', text: '#fff' },
'Technical Analysis': { bg: '#0F6B73', text: '#fff' },
'Guide': { bg: '#D49A2A', text: '#fff' },
'default': { bg: '#E4DDD2', text: '#263238' },
};
function CategoryBadge({ name, color, small }) {
return (
<span style={{
display: 'inline-block', padding: small ? '2px 8px' : '3px 10px',
borderRadius: 4, fontSize: small ? 11 : 12, fontWeight: 600,
background: color ? color + '18' : '#0F6B7318', color: color || '#0F6B73',
border: `1px solid ${color || '#0F6B73'}30`,
}}>{name}</span>
);
}
function TypeBadge({ type, small }) {
const c = TYPE_COLORS[type] || TYPE_COLORS.default;
return (
<span style={{
display: 'inline-block', padding: small ? '2px 8px' : '3px 10px',
borderRadius: 3, fontSize: small ? 10 : 11, fontWeight: 700, letterSpacing: '0.03em',
background: c.bg, color: c.text,
}}>{type}</span>
);
}
// ── Header ──────────────────────────────────────────────────────────────────
const NAV_ITEMS = [
{ label: 'Home', path: '/' },
{ label: 'AI', path: '/category/ai' },
{ label: 'Security', path: '/category/security' },
{ label: 'Software', path: '/category/software' },
{ label: 'Hardware', path: '/category/hardware' },
{ label: 'Data', path: '/category/data' },
{ label: 'Policy', path: '/category/policy' },
{ label: 'Articles', path: '/articles' },
];
function Header() {
const { navigate, page } = useNav();
const { user, logout } = useAuth();
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const [searchQ, setSearchQ] = useState('');
const searchRef = useRef();
useEffect(() => {
const h = () => setScrolled(window.scrollY > 20);
window.addEventListener('scroll', h);
return () => window.removeEventListener('scroll', h);
}, []);
useEffect(() => {
if (searchOpen) setTimeout(() => searchRef.current?.focus(), 100);
}, [searchOpen]);
const handleSearch = (e) => {
e.preventDefault();
if (searchQ.trim()) { navigate('/articles?q=' + encodeURIComponent(searchQ)); setSearchOpen(false); setSearchQ(''); }
};
return (
<>
<header role="banner" style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 100, direction: 'ltr',
background: scrolled ? 'rgba(250,247,240,0.95)' : 'rgba(250,247,240,0.85)',
backdropFilter: 'blur(12px)', borderBottom: scrolled ? '1px solid #E4DDD2' : '1px solid transparent',
transition: 'all 0.3s',
}}>
<div className="site-header-inner" style={{ maxWidth: 1280, margin: '0 auto', padding: '0 24px', display: 'flex', alignItems: 'center', height: 64, gap: 32 }}>
{/* Logo */}
<button onClick={() => navigate('/')} style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', flexDirection: 'column', lineHeight: 1 }} aria-label="Teknav home">
<span style={{ fontSize: 22, fontWeight: 900, color: '#0F6B73', fontFamily: 'Inter,sans-serif', letterSpacing: '-0.02em' }}>Teknav</span>
<span style={{ fontSize: 9, color: '#5F6B6D', letterSpacing: '0.15em', fontFamily: 'sans-serif' }}>GLOBAL</span>
</button>
{/* Nav */}
<nav role="navigation" style={{ display: 'flex', gap: 0, flex: 1, overflowX: 'auto' }} className="desktop-nav" aria-label="Main navigation">
{NAV_ITEMS.map(item => (
<button key={item.path} onClick={() => navigate(item.path)} style={{
background: 'none', border: 'none', cursor: 'pointer', padding: '8px 12px',
fontSize: 13, fontFamily: 'Inter,sans-serif', whiteSpace: 'nowrap',
color: page === item.path ? '#0F6B73' : '#263238',
fontWeight: page === item.path ? 700 : 400,
borderBottom: page === item.path ? '2px solid #0F6B73' : '2px solid transparent',
transition: 'all 0.15s',
}} aria-current={page === item.path ? 'page' : undefined}>{item.label}</button>
))}
</nav>
{/* Actions */}
<div className="site-header-actions" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button onClick={() => setSearchOpen(s => !s)} style={iconBtnStyle} aria-label="Search Teknav" aria-expanded={searchOpen}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#263238" strokeWidth="2" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
</button>
<a href="https://www.teknav.ir" target="_blank" rel="noopener noreferrer" style={{ ...pillBtn, background: '#F4EFE6', color: '#263238', border: '1px solid #E4DDD2', fontSize: 12 }}>
فارسی
</a>
{user ? (
<div className="site-auth-actions" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{user.username && (
<span style={{ fontSize: 13, color: '#5F6B6D', fontWeight: 600 }}>@{user.username}</span>
)}
<button onClick={() => logout()} style={{ ...pillBtn, background: '#F4EFE6', color: '#263238' }}>Sign out</button>
</div>
) : (
<button onClick={() => navigate('/login')} style={{ ...pillBtn, background: '#C46A4D', color: '#fff' }}>Sign in</button>
)}
<button onClick={() => setMobileOpen(o => !o)} style={{ ...iconBtnStyle, display: 'none' }} className="mobile-menu-btn" aria-label="Open mobile menu" aria-expanded={mobileOpen}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#263238" strokeWidth="2" aria-hidden="true"><path d="M3 12h18M3 6h18M3 18h18"/></svg>
</button>
</div>
</div>
{/* Search bar */}
{searchOpen && (
<div className="site-search-strip" style={{ borderTop: '1px solid #E4DDD2', padding: '12px 24px', background: '#FAF7F0' }}>
<form className="site-search-form" role="search" onSubmit={handleSearch} style={{ maxWidth: 600, margin: '0 auto', display: 'flex', gap: 8 }}>
<input ref={searchRef} value={searchQ} onChange={e => setSearchQ(e.target.value)}
placeholder="Search articles, authors, topics..."
style={{ flex: 1, ...inputStyle, fontSize: 14 }} aria-label="Search query" />
<button type="submit" style={{ ...pillBtn, background: '#0F6B73', color: '#fff' }}>Search</button>
</form>
</div>
)}
</header>
{/* Mobile Drawer */}
{mobileOpen && (
<div style={{ position: 'fixed', inset: 0, zIndex: 200, direction: 'ltr' }}>
<div onClick={() => setMobileOpen(false)} style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.4)' }} />
<div className="mobile-drawer-panel" style={{ position: 'absolute', top: 0, left: 0, width: 280, height: '100%', background: '#FAF7F0', padding: 24, overflowY: 'auto', boxShadow: '4px 0 24px rgba(0,0,0,0.12)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<span style={{ fontSize: 20, fontWeight: 900, color: '#0F6B73' }}>Teknav</span>
<button onClick={() => setMobileOpen(false)} style={iconBtnStyle}>✕</button>
</div>
{NAV_ITEMS.map(item => (
<button key={item.path} onClick={() => { navigate(item.path); setMobileOpen(false); }} style={{
display: 'block', width: '100%', textAlign: 'left', background: 'none', border: 'none',
padding: '12px 0', fontSize: 15, fontFamily: 'Inter,sans-serif', cursor: 'pointer',
color: page === item.path ? '#0F6B73' : '#263238', fontWeight: page === item.path ? 700 : 400,
borderBottom: '1px solid #E4DDD2',
}}>{item.label}</button>
))}
<a href="https://www.teknav.ir" style={{ display: 'block', marginTop: 20, ...pillBtn, background: '#F4EFE6', color: '#263238', textAlign: 'center', justifyContent: 'center' }}>فارسی (Persian)</a>
</div>
</div>
)}
<style>{`
.desktop-nav { display: flex !important; }
.mobile-menu-btn { display: none !important; }
@media (max-width: 768px) {
.desktop-nav { display: none !important; }
.mobile-menu-btn { display: flex !important; }
}
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
* { box-sizing: border-box; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #FAF7F0; }
::-webkit-scrollbar-thumb { background: #E4DDD2; border-radius: 3px; }
`}</style>
</>
);
}
// ── Footer ──────────────────────────────────────────────────────────────────
function Footer() {
const { navigate } = useNav();
return (
<footer role="contentinfo" style={{ background: '#ebe3d2', color: '#4A5A5C', direction: 'ltr', padding: '56px 24px 28px', marginTop: 80, position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 3, background: 'linear-gradient(90deg, transparent 0%, #0F6B73 25%, #D49A2A 50%, #C46A4D 75%, transparent 100%)' }} />
<div style={{ maxWidth: 1280, margin: '0 auto', position: 'relative' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(200px,1fr))', gap: 40, marginBottom: 40 }}>
<section aria-label="About Teknav">
<div style={{ fontSize: 28, fontWeight: 900, color: '#0F2A2E', marginBottom: 6, fontFamily: "Inter,sans-serif", letterSpacing: '-0.02em' }}>Teknav</div>
<div style={{ fontSize: 11, color: '#C46A4D', letterSpacing: '0.18em', marginBottom: 14, fontWeight: 700 }}>GLOBAL</div>
<p style={{ fontSize: 13, lineHeight: 1.9, color: '#5F6B6D' }}>Deep technology analysis. Independent, rigorous coverage of AI engineering, security, and the future of software for builders worldwide.</p>
</section>
<nav aria-label="Category links">
<div style={{ fontSize: 14, fontWeight: 700, color: '#0F2A2E', marginBottom: 16, fontFamily: "Inter,sans-serif" }}>Topics</div>
{[
['AI Engineering', '/category/ai'],
['Security', '/category/security'],
['Software Architecture', '/category/software'],
['Hardware & Compute', '/category/hardware'],
['AI Policy', '/category/policy'],
].map(([label, path]) => (
<div key={path} onClick={() => navigate(path)} style={{ fontSize: 13, marginBottom: 9, cursor: 'pointer', color: '#5F6B6D', transition: 'color 0.15s, transform 0.15s' }}
onMouseEnter={e => { e.currentTarget.style.color = '#0F6B73'; e.currentTarget.style.transform = 'translateX(4px)'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#5F6B6D'; e.currentTarget.style.transform = 'translateX(0)'; }}>{label}</div>
))}
</nav>
<nav aria-label="Site links">
<div style={{ fontSize: 14, fontWeight: 700, color: '#0F2A2E', marginBottom: 16, fontFamily: "Inter,sans-serif" }}>Teknav</div>
{[
['About Us', '/about'],
['Privacy Policy', '/privacy'],
['Terms of Use', '/terms'],
['Editorial Policy', '/editorial-policy'],
['Persian Edition', 'https://www.teknav.ir'],
].map(([label, path]) => (
path.startsWith('http') ? (
<a key={path} href={path} target="_blank" rel="noopener noreferrer" style={{ display: 'block', fontSize: 13, marginBottom: 9, color: '#5F6B6D', transition: 'color 0.15s' }}
onMouseEnter={e => e.currentTarget.style.color = '#0F6B73'}
onMouseLeave={e => e.currentTarget.style.color = '#5F6B6D'}>{label}</a>
) : (
<div key={path} onClick={() => navigate(path)} style={{ fontSize: 13, marginBottom: 9, cursor: 'pointer', color: '#5F6B6D', transition: 'color 0.15s, transform 0.15s' }}
onMouseEnter={e => { e.currentTarget.style.color = '#0F6B73'; e.currentTarget.style.transform = 'translateX(4px)'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#5F6B6D'; e.currentTarget.style.transform = 'translateX(0)'; }}>{label}</div>
)
))}
</nav>
<section aria-label="Connect">
<div style={{ fontSize: 14, fontWeight: 700, color: '#0F2A2E', marginBottom: 16, fontFamily: "Inter,sans-serif" }}>Connect</div>
<div style={{ display: 'flex', gap: 10, marginBottom: 16 }}>
{[['Twitter/X', 'https://x.com/teknavir'], ['LinkedIn', 'https://linkedin.com/company/teknav'], ['Telegram', 'https://t.me/teknavir']].map(([s, href]) => (
<a key={s} href={href} target="_blank" rel="noopener noreferrer" style={{ background: 'rgba(15,107,115,0.08)', border: '1px solid rgba(15,107,115,0.25)', color: '#0F6B73', padding: '7px 14px', borderRadius: 8, cursor: 'pointer', fontSize: 12, fontWeight: 600, fontFamily: 'Inter,sans-serif', transition: 'all 0.18s', textDecoration: 'none', display: 'inline-block' }}
onMouseEnter={e => { e.currentTarget.style.background = '#0F6B73'; e.currentTarget.style.color = '#fff'; e.currentTarget.style.transform = 'translateY(-2px)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(15,107,115,0.08)'; e.currentTarget.style.color = '#0F6B73'; e.currentTarget.style.transform = 'translateY(0)'; }}>{s}</a>
))}
</div>
<div style={{ fontSize: 12, color: '#C46A4D', fontWeight: 600 }}>info@teknav.ir</div>
</section>
</div>
<div style={{ borderTop: '1px solid rgba(15,42,46,0.12)', paddingTop: 22, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
<span style={{ fontSize: 12, color: '#5F6B6D' }}>© 2026 Teknav — All rights reserved</span>
<span style={{ fontSize: 12, color: '#5F6B6D', letterSpacing: '0.04em' }}>
Designed By <a href="https://www.asabbagh.com" target="_blank" rel="noopener noreferrer" style={{ color: '#0F6B73', fontWeight: 700, letterSpacing: '0.08em', textDecoration: 'none' }}>Coaxys</a>
</span>
</div>
</div>
</footer>
);
}
// ── Newsletter Form ─────────────────────────────────────────────────────────
function NewsletterForm() {
const [email, setEmail] = useState('');
const [state, setState] = useState('idle');
const toast = useToast();
const submit = async (e) => {
e.preventDefault();
if (!email.includes('@') || !email.includes('.')) { toast('Please enter a valid email address', 'error'); return; }
try {
await api.post('/api/newsletter/subscribe', { email });
setState('done');
toast('You\'ve subscribed to Teknav!', 'success');
} catch {
toast('Subscription error — please try again', 'error');
}
};
if (state === 'done') return (
<div style={{ textAlign: 'center', padding: '32px 0' }}>
<div style={{ fontSize: 32, marginBottom: 8 }}>✓</div>
<div style={{ fontSize: 16, fontWeight: 600, color: '#2F8F6B' }}>You're subscribed!</div>
<div style={{ fontSize: 13, color: '#5F6B6D', marginTop: 4 }}>You'll receive Teknav's latest analysis in your inbox.</div>
</div>
);
return (
<form onSubmit={submit} style={{ display: 'flex', gap: 10, maxWidth: 460, margin: '0 auto' }}>
<input value={email} onChange={e => setEmail(e.target.value)} type="email"
placeholder="Your email address"
style={{ flex: 1, minWidth: 200, ...inputStyle }} />
<button type="submit" style={{ ...pillBtn, background: '#C46A4D', color: '#fff', padding: '10px 24px', whiteSpace: 'nowrap' }}>Subscribe</button>
</form>
);
}
// ── Skeleton Loader ─────────────────────────────────────────────────────────
function SkeletonLoader({ lines = 3, height = 16 }) {
return (
<div>
{Array.from({ length: lines }).map((_, i) => (
<div key={i} style={{ height, borderRadius: 6, background: 'linear-gradient(90deg,#E4DDD2 0%,#F4EFE6 50%,#E4DDD2 100%)', backgroundSize: '200% 100%', animation: 'shimmer 1.4s infinite', marginBottom: 10, width: i === lines - 1 ? '60%' : '100%' }} />
))}
</div>
);
}
// ── Empty State ─────────────────────────────────────────────────────────────
function EmptyState({ title = 'No results found', subtitle = 'Try different filters or search terms' }) {
return (
<div style={{ textAlign: 'center', padding: '60px 24px', color: '#5F6B6D' }}>
<div style={{ fontSize: 48, marginBottom: 12, opacity: 0.4 }}>◎</div>
<div style={{ fontSize: 16, fontWeight: 600, marginBottom: 6, color: '#263238' }}>{title}</div>
<div style={{ fontSize: 13 }}>{subtitle}</div>
</div>
);
}
// ── Reading Progress ────────────────────────────────────────────────────────
function ReadingProgress() {
const [pct, setPct] = useState(0);
useEffect(() => {
const h = () => {
const el = document.documentElement;
const scroll = el.scrollTop || document.body.scrollTop;
const total = el.scrollHeight - el.clientHeight;
setPct(total > 0 ? (scroll / total) * 100 : 0);
};
window.addEventListener('scroll', h);
return () => window.removeEventListener('scroll', h);
}, []);
return (
<div style={{ position: 'fixed', top: 64, left: 0, right: 0, height: 3, zIndex: 99, background: '#E4DDD2' }}>
<div style={{ height: '100%', width: pct + '%', background: '#0F6B73', transition: 'width 0.1s linear' }} />
</div>
);
}
// ── Author Avatar ───────────────────────────────────────────────────────────
function AuthorAvatar({ author, size = 40 }) {
const a = typeof author === 'string' ? authors.find(x => x.id === author) : author;
if (!a) return null;
return (
<div style={{
width: size, height: size, borderRadius: '50%', background: a.color || '#0F6B73',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: size * 0.32, fontWeight: 700, flexShrink: 0,
}}>{a.initials || a.name.slice(0, 2)}</div>
);
}
// ── Push Notification Prompt ────────────────────────────────────────────────
function PushPrompt() {
const [visible, setVisible] = useState(false);
const toast = useToast();
useEffect(() => {
if (
'PushManager' in window &&
'serviceWorker' in navigator &&
typeof Notification !== 'undefined' &&
Notification.permission === 'default' &&
!localStorage.getItem('push_prompt_dismissed_en')
) {
const t = setTimeout(() => setVisible(true), 8000);
return () => clearTimeout(t);
}
}, []);
if (!visible) return null;
const handleEnable = async () => {
try {
const perm = await Notification.requestPermission();
if (perm === 'granted') toast('Browser notifications enabled');
} catch { /* ignore */ }
setVisible(false);
};
const dismiss = () => {
localStorage.setItem('push_prompt_dismissed_en', '1');
setVisible(false);
};
return (
<div style={{
position: 'fixed', bottom: 100, right: 24, zIndex: 490,
background: '#1A2E30', border: '1px solid #2A4045', borderRadius: 16,
padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 14,
boxShadow: '0 12px 40px rgba(0,0,0,0.22)', animation: 'slideUp 0.3s ease',
maxWidth: 320,
}}>
<span style={{ fontSize: 20 }}>🔔</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: '#FAF7F0', marginBottom: 3 }}>Browser notifications</div>
<div style={{ fontSize: 11, color: '#8AA5A8' }}>Never miss a Teknav article</div>
</div>
<div style={{ display: 'flex', gap: 6 }}>
<button onClick={dismiss} style={{ ...pillBtn, background: 'transparent', color: '#8AA5A8', border: '1px solid #2A4045', padding: '5px 10px', fontSize: 11 }}>Later</button>
<button onClick={handleEnable} style={{ ...pillBtn, background: '#C46A4D', color: '#fff', padding: '5px 12px', fontSize: 11 }}>Enable</button>
</div>
</div>
);
}
// ── Shared styles ───────────────────────────────────────────────────────────
const iconBtnStyle = {
background: 'none', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
width: 36, height: 36, borderRadius: 8, transition: 'background 0.15s',
};
const pillBtn = {
border: 'none', cursor: 'pointer', padding: '8px 18px', borderRadius: 8,
fontSize: 13, fontFamily: 'Inter,sans-serif', fontWeight: 600,
transition: 'all 0.15s', display: 'inline-flex', alignItems: 'center', gap: 6,
};
const inputStyle = {
border: '1px solid #E4DDD2', borderRadius: 8, padding: '9px 14px',
fontSize: 13, fontFamily: 'Inter,sans-serif', background: '#fff',
color: '#263238', outline: 'none', width: '100%',
};
export {
ToastProvider, useToast, NavProvider, useNav, AuthProvider, useAuth,
CategoryBadge, TypeBadge, AuthorAvatar,
Header, Footer, NewsletterForm, SkeletonLoader, EmptyState, ReadingProgress,
PushPrompt,
iconBtnStyle, pillBtn, inputStyle,
};