mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:05:23 +02:00
feat: full mobile responsive optimization for Android/iPhone
- Add bottom navigation bar (6 tabs) for mobile, hidden on desktop - Compact header with smaller logo and reduced padding on mobile - Collapsible filter bar with active filter count badge on mobile - Bottom-sheet style dropdowns with overlay and larger touch targets - Fullscreen AI chat on mobile, floating window on desktop - Responsive dashboard cards with always-visible expand button - Smaller table text and touch-friendly scroll on all data grids - iPhone safe-area support and thinner scrollbars on mobile Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
fecc7264c8
commit
9895545910
@@ -652,11 +652,11 @@ const App: React.FC = () => {
|
|||||||
<div className="min-h-screen flex flex-col bg-background text-slate-200">
|
<div className="min-h-screen flex flex-col bg-background text-slate-200">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="bg-slate-950/90 backdrop-blur border-b border-border py-4 px-6 relative z-40 shadow-2xl">
|
<header className="bg-slate-950/90 backdrop-blur border-b border-border py-2 px-3 md:py-4 md:px-6 relative z-40 shadow-2xl">
|
||||||
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
<div className="max-w-7xl mx-auto flex justify-between items-center">
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-3 md:gap-8">
|
||||||
{/* Logo Container (Horizontal Box) - Persistent User Image */}
|
{/* Logo Container - Smaller on mobile */}
|
||||||
<div className="h-16 w-64 relative flex-shrink-0">
|
<div className="h-10 w-32 md:h-16 md:w-64 relative flex-shrink-0">
|
||||||
<CrazeLogo />
|
<CrazeLogo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -677,63 +677,62 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-3 md:gap-6">
|
||||||
{/* Force Sync Action */}
|
{/* Force Sync Action */}
|
||||||
<button
|
<button
|
||||||
onClick={() => { handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); handleBuyBoxFetch(); }}
|
onClick={() => { handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); handleBuyBoxFetch(); }}
|
||||||
disabled={syncing}
|
disabled={syncing}
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-black uppercase tracking-widest transition-all
|
className={`flex items-center gap-2 px-3 py-1.5 md:px-4 md:py-2 rounded-xl text-sm font-black uppercase tracking-widest transition-all
|
||||||
${syncing
|
${syncing
|
||||||
? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/30'
|
? 'bg-indigo-500/20 text-indigo-400 border border-indigo-500/30'
|
||||||
: 'bg-slate-900 text-slate-300 border border-border hover:border-indigo-500/50 hover:text-white shadow-lg active:scale-95'}`}
|
: 'bg-slate-900 text-slate-300 border border-border hover:border-indigo-500/50 hover:text-white shadow-lg active:scale-95'}`}
|
||||||
>
|
>
|
||||||
<RefreshIcon className={`w-4 h-4 ${syncing ? 'animate-spin' : ''}`} />
|
<RefreshIcon className={`w-4 h-4 ${syncing ? 'animate-spin' : ''}`} />
|
||||||
<span>{syncing ? 'Syncing...' : 'Sync Now'}</span>
|
<span className="hidden sm:inline">{syncing ? 'Syncing...' : 'Sync Now'}</span>
|
||||||
</button>
|
</button>
|
||||||
{/* View Switcher is the priority now */}
|
{/* View Switcher - Hidden on mobile (shown in bottom nav instead) */}
|
||||||
{/* View Switcher */}
|
<div className="hidden md:flex bg-slate-900 rounded-lg p-1 border border-border">
|
||||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-border">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('dashboard')}
|
onClick={() => setView('dashboard')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||||
${view === 'dashboard' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'dashboard' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
||||||
>
|
>
|
||||||
<ChartIcon /> <span className="hidden sm:inline">Dashboard</span>
|
<ChartIcon /> <span className="hidden lg:inline">Dashboard</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('table')}
|
onClick={() => setView('table')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||||
${view === 'table' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'table' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
||||||
>
|
>
|
||||||
<TableIcon /> <span className="hidden sm:inline">Grid</span>
|
<TableIcon /> <span className="hidden lg:inline">Grid</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('weekly')}
|
onClick={() => setView('weekly')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||||
${view === 'weekly' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'weekly' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
||||||
>
|
>
|
||||||
<TrendingIcon /> <span className="hidden sm:inline">Weekly Sales</span>
|
<TrendingIcon /> <span className="hidden lg:inline">Weekly Sales</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('movers')}
|
onClick={() => setView('movers')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||||
${view === 'movers' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'movers' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
||||||
>
|
>
|
||||||
<TrendingIcon /> <span className="hidden sm:inline">Movers</span>
|
<TrendingIcon /> <span className="hidden lg:inline">Movers</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('ads')}
|
onClick={() => setView('ads')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||||
${view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
||||||
>
|
>
|
||||||
<MegaphoneIcon /> <span className="hidden sm:inline">Ads</span>
|
<MegaphoneIcon /> <span className="hidden lg:inline">Ads</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('forecast')}
|
onClick={() => setView('forecast')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||||
${view === 'forecast' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
${view === 'forecast' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
||||||
>
|
>
|
||||||
<ChartIcon /> <span className="hidden sm:inline">Fc 26</span>
|
<ChartIcon /> <span className="hidden lg:inline">Fc 26</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -850,6 +849,30 @@ const App: React.FC = () => {
|
|||||||
{/* Chat Assistant */}
|
{/* Chat Assistant */}
|
||||||
<AIChat onSendMessage={handleAskGemini} isOpen={isChatOpen} setIsOpen={setIsChatOpen} />
|
<AIChat onSendMessage={handleAskGemini} isOpen={isChatOpen} setIsOpen={setIsChatOpen} />
|
||||||
|
|
||||||
|
{/* Mobile Bottom Navigation */}
|
||||||
|
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-slate-950/95 backdrop-blur border-t border-border md:hidden pb-safe">
|
||||||
|
<div className="grid grid-cols-6 gap-0">
|
||||||
|
{([
|
||||||
|
{ key: 'dashboard' as const, icon: <ChartIcon />, label: 'Home' },
|
||||||
|
{ key: 'table' as const, icon: <TableIcon />, label: 'Grid' },
|
||||||
|
{ key: 'weekly' as const, icon: <TrendingIcon />, label: 'Weekly' },
|
||||||
|
{ key: 'movers' as const, icon: <TrendingIcon />, label: 'Movers' },
|
||||||
|
{ key: 'ads' as const, icon: <MegaphoneIcon />, label: 'Ads' },
|
||||||
|
{ key: 'forecast' as const, icon: <ChartIcon />, label: 'Fc 26' },
|
||||||
|
]).map(({ key, icon, label }) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => setView(key)}
|
||||||
|
className={`flex flex-col items-center justify-center py-2 gap-0.5 transition-colors
|
||||||
|
${view === key ? 'text-primary' : 'text-slate-500 active:text-slate-300'}`}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<span className="text-[9px] font-bold uppercase tracking-wider">{label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
{/* DATA MODAL */}
|
{/* DATA MODAL */}
|
||||||
{
|
{
|
||||||
isDataModalOpen && (
|
isDataModalOpen && (
|
||||||
|
|||||||
@@ -93,21 +93,21 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Trigger Button */}
|
{/* Trigger Button - above bottom nav on mobile */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className={`fixed bottom-6 right-6 z-50 p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-110
|
className={`fixed bottom-20 right-4 md:bottom-6 md:right-6 z-50 p-3 md:p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-110
|
||||||
${isOpen ? 'bg-slate-700 rotate-90 opacity-0 pointer-events-none' : 'bg-primary text-white rotate-0 opacity-100'}`}
|
${isOpen ? 'bg-slate-700 rotate-90 opacity-0 pointer-events-none' : 'bg-primary text-white rotate-0 opacity-100'}`}
|
||||||
>
|
>
|
||||||
<ChatIcon />
|
<ChatIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Chat Window */}
|
{/* Chat Window - fullscreen on mobile, floating on desktop */}
|
||||||
<div
|
<div
|
||||||
className={`fixed z-50 bg-slate-900 border border-border shadow-2xl transition-all duration-300 flex flex-col overflow-hidden
|
className={`fixed z-[60] bg-slate-900 border border-border shadow-2xl transition-all duration-300 flex flex-col overflow-hidden
|
||||||
${isOpen
|
${isOpen
|
||||||
? 'bottom-6 right-6 w-96 h-[600px] rounded-2xl opacity-100 translate-y-0'
|
? 'inset-0 rounded-none md:inset-auto md:bottom-6 md:right-6 md:w-96 md:h-[600px] md:rounded-2xl opacity-100 translate-y-0'
|
||||||
: 'bottom-6 right-6 w-96 h-0 opacity-0 translate-y-10 pointer-events-none'}`}
|
: 'bottom-0 right-0 w-full h-0 md:bottom-6 md:right-6 md:w-96 opacity-0 translate-y-10 pointer-events-none'}`}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-primary/10 p-4 border-b border-border flex justify-between items-center backdrop-blur">
|
<div className="bg-primary/10 p-4 border-b border-border flex justify-between items-center backdrop-blur">
|
||||||
|
|||||||
@@ -363,10 +363,10 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
|
|||||||
}, [filteredAndSorted]);
|
}, [filteredAndSorted]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 animate-fade-in p-6 bg-[#0B0E14] min-h-screen text-slate-300">
|
<div className="flex flex-col gap-4 md:gap-6 animate-fade-in p-3 md:p-6 bg-[#0B0E14] min-h-screen text-slate-300">
|
||||||
<div className="flex justify-between items-end">
|
<div className="flex justify-between items-end">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white mb-1">Performance Overview</h1>
|
<h1 className="text-lg md:text-2xl font-bold text-white mb-1">Performance Overview</h1>
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||||
<span>
|
<span>
|
||||||
@@ -376,7 +376,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 xl:grid-cols-13 gap-4 bg-[#141820] p-4 rounded-xl border border-white/5 shadow-xl">
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 xl:grid-cols-13 gap-2 md:gap-4 bg-[#141820] p-2 md:p-4 rounded-xl border border-white/5 shadow-xl">
|
||||||
<SummaryCard label="TOTAL SALES" value={totals.curr.salesTotal} prevValue={totals.prev?.salesTotal} format="currency" color="text-emerald-400" />
|
<SummaryCard label="TOTAL SALES" value={totals.curr.salesTotal} prevValue={totals.prev?.salesTotal} format="currency" color="text-emerald-400" />
|
||||||
<SummaryCard label="ADS SALES" value={totals.curr.salesAds} prevValue={totals.prev?.salesAds} format="currency" color="text-orange-400" />
|
<SummaryCard label="ADS SALES" value={totals.curr.salesAds} prevValue={totals.prev?.salesAds} format="currency" color="text-orange-400" />
|
||||||
<SummaryCard label="ORGANIC SALES" value={totals.curr.salesOrganic} prevValue={totals.prev?.salesOrganic} format="currency" color="text-blue-400" />
|
<SummaryCard label="ORGANIC SALES" value={totals.curr.salesOrganic} prevValue={totals.prev?.salesOrganic} format="currency" color="text-blue-400" />
|
||||||
@@ -395,7 +395,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-[#141820] rounded-xl border border-white/10 overflow-hidden shadow-2xl flex-1 flex flex-col">
|
<div className="bg-[#141820] rounded-xl border border-white/10 overflow-hidden shadow-2xl flex-1 flex flex-col">
|
||||||
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-[#1A1F29]">
|
<div className="p-2 md:p-4 border-b border-white/5 flex flex-wrap justify-between items-center gap-2 bg-[#1A1F29]">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-bold text-white uppercase tracking-wider">Product Inventory</h2>
|
<h2 className="text-sm font-bold text-white uppercase tracking-wider">Product Inventory</h2>
|
||||||
<p className="text-[10px] text-slate-400 mt-0.5">
|
<p className="text-[10px] text-slate-400 mt-0.5">
|
||||||
@@ -428,8 +428,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto flex-1 custom-scrollbar">
|
<div className="overflow-x-auto flex-1 custom-scrollbar scroll-touch">
|
||||||
<table className="w-full text-left border-collapse text-[11px]">
|
<table className="w-full text-left border-collapse text-[10px] md:text-[11px]">
|
||||||
<thead className="sticky top-0 z-20 bg-[#1A1F29]">
|
<thead className="sticky top-0 z-20 bg-[#1A1F29]">
|
||||||
<tr className="border-b border-white/5">
|
<tr className="border-b border-white/5">
|
||||||
<th className="p-4 font-black text-slate-400 uppercase tracking-widest min-w-[250px]">
|
<th className="p-4 font-black text-slate-400 uppercase tracking-widest min-w-[250px]">
|
||||||
|
|||||||
+14
-14
@@ -34,12 +34,12 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
|
|||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-slate-950 px-6 pb-6 pt-32 flex flex-col animate-fade-in overflow-hidden">
|
<div className="fixed inset-0 z-50 bg-slate-950 px-3 pb-16 pt-16 md:px-6 md:pb-6 md:pt-32 flex flex-col animate-fade-in overflow-hidden">
|
||||||
|
|
||||||
{/* Fixed Close Button - Positioned TOP RIGHT ON TOP OF FILTER BAR with z-[100] */}
|
{/* Fixed Close Button */}
|
||||||
<button
|
<button
|
||||||
onClick={toggleExpand}
|
onClick={toggleExpand}
|
||||||
className="fixed top-3 right-3 z-[100] p-2 bg-red-600/90 hover:bg-red-500 border border-red-400 rounded-full text-white transition-all shadow-2xl hover:scale-110 flex items-center gap-2 group"
|
className="fixed top-3 right-3 z-[100] p-2 bg-red-600/90 hover:bg-red-500 active:bg-red-500 border border-red-400 rounded-full text-white transition-all shadow-2xl hover:scale-110 flex items-center gap-2 group"
|
||||||
title="Exit Fullscreen"
|
title="Exit Fullscreen"
|
||||||
>
|
>
|
||||||
<span className="text-xs font-bold hidden group-hover:inline pr-1">CLOSE</span>
|
<span className="text-xs font-bold hidden group-hover:inline pr-1">CLOSE</span>
|
||||||
@@ -48,10 +48,10 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex justify-between items-center mb-4 border-b border-slate-800 pb-4 shrink-0">
|
<div className="flex justify-between items-center mb-3 md:mb-4 border-b border-slate-800 pb-3 md:pb-4 shrink-0">
|
||||||
<h3 className="text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
|
<h3 className="text-base md:text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto bg-slate-900 rounded-xl p-6 border border-border custom-scrollbar">
|
<div className="flex-1 overflow-auto bg-slate-900 rounded-xl p-3 md:p-6 border border-border custom-scrollbar scroll-touch">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,13 +60,13 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-surface border border-border rounded-xl p-6 shadow-sm flex flex-col group relative transition-all duration-300 hover:shadow-primary/5 ${className}`}
|
className={`bg-surface border border-border rounded-xl p-3 md:p-6 shadow-sm flex flex-col group relative transition-all duration-300 hover:shadow-primary/5 ${className}`}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-3 md:mb-4">
|
||||||
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
|
<h3 className="text-xs md:text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
|
||||||
<button
|
<button
|
||||||
onClick={toggleExpand}
|
onClick={toggleExpand}
|
||||||
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-primary transition-opacity"
|
className="opacity-100 md:opacity-0 md:group-hover:opacity-100 text-slate-500 hover:text-primary active:text-primary transition-opacity"
|
||||||
title="Expand to Fullscreen"
|
title="Expand to Fullscreen"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
||||||
@@ -74,7 +74,7 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-h-[250px] cursor-pointer" onClick={toggleExpand}>{children}</div>
|
<div className="flex-1 min-h-[200px] md:min-h-[250px] cursor-pointer" onClick={toggleExpand}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -102,8 +102,8 @@ const MultiYearKPICard: React.FC<{
|
|||||||
const displayTitle = contextData ? `${title} (Selected Item)` : title;
|
const displayTitle = contextData ? `${title} (Selected Item)` : title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gradient-to-br from-surface to-slate-900 border border-border rounded-xl p-6 flex flex-col justify-center">
|
<div className="bg-gradient-to-br from-surface to-slate-900 border border-border rounded-xl p-3 md:p-6 flex flex-col justify-center">
|
||||||
<h3 className="text-sm font-medium text-slate-400 mb-3">{displayTitle}</h3>
|
<h3 className="text-xs md:text-sm font-medium text-slate-400 mb-2 md:mb-3">{displayTitle}</h3>
|
||||||
|
|
||||||
{sortedYears.length === 0 && <p className="text-3xl font-bold text-white">0</p>}
|
{sortedYears.length === 0 && <p className="text-3xl font-bold text-white">0</p>}
|
||||||
|
|
||||||
@@ -508,7 +508,7 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData, adsData = [] }
|
|||||||
const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300);
|
const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
|
<div className="p-3 md:p-6 space-y-4 md:space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
|
||||||
|
|
||||||
{/* Ads Performance Section - Condensed Layout */}
|
{/* Ads Performance Section - Condensed Layout */}
|
||||||
{adsKPIsByYear && availableAdsYears.length > 0 && (
|
{adsKPIsByYear && availableAdsYears.length > 0 && (
|
||||||
|
|||||||
@@ -738,7 +738,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
|
|||||||
}, [rowFilters, data, effectiveDimensions, searchTerm]);
|
}, [rowFilters, data, effectiveDimensions, searchTerm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-[95vw] mx-auto animate-fade-in pb-24">
|
<div className="space-y-4 md:space-y-6 max-w-[98vw] md:max-w-[95vw] mx-auto animate-fade-in px-1 md:px-0 pb-24">
|
||||||
|
|
||||||
{/* 1. Time Series Chart Section */}
|
{/* 1. Time Series Chart Section */}
|
||||||
{showChart && chartData.length > 0 && (
|
{showChart && chartData.length > 0 && (
|
||||||
@@ -1173,8 +1173,8 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Data Table */}
|
{/* Data Table */}
|
||||||
<div className="overflow-x-auto min-h-[400px]">
|
<div className="overflow-x-auto min-h-[400px] scroll-touch">
|
||||||
<table className="w-full text-left text-sm border-collapse">
|
<table className="w-full text-left text-xs md:text-sm border-collapse">
|
||||||
<thead className="bg-slate-950 text-slate-400 uppercase text-xs font-semibold tracking-wider sticky top-0 z-20 shadow-sm border-b border-white/10">
|
<thead className="bg-slate-950 text-slate-400 uppercase text-xs font-semibold tracking-wider sticky top-0 z-20 shadow-sm border-b border-white/10">
|
||||||
{/* NEW: Filtered Totals Header Row */}
|
{/* NEW: Filtered Totals Header Row */}
|
||||||
<tr className="bg-indigo-950/20 border-b border-indigo-500/30">
|
<tr className="bg-indigo-950/20 border-b border-indigo-500/30">
|
||||||
|
|||||||
+41
-12
@@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { FilterState } from '../types';
|
import { FilterState } from '../types';
|
||||||
import MultiSelectDropdown from './MultiSelectDropdown';
|
import MultiSelectDropdown from './MultiSelectDropdown';
|
||||||
|
import { FunnelIcon } from './Icons';
|
||||||
|
|
||||||
interface FilterBarProps {
|
interface FilterBarProps {
|
||||||
filters: FilterState;
|
filters: FilterState;
|
||||||
@@ -20,36 +21,64 @@ interface FilterBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options }) => {
|
const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options }) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const activeFilterCount = useMemo(() => {
|
||||||
|
return filters.customer.length + filters.year.length + filters.month.length +
|
||||||
|
filters.week.length + filters.line.length + filters.sku.length +
|
||||||
|
filters.title.length + filters.asin.length + filters.stock.length;
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-950 border-b border-border sticky top-0 z-[60] p-4 shadow-xl">
|
<div className="bg-slate-950 border-b border-border sticky top-0 z-[60] p-2 md:p-4 shadow-xl">
|
||||||
<div className="max-w-7xl mx-auto flex flex-wrap gap-4 items-end">
|
{/* Mobile toggle button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="md:hidden w-full flex items-center justify-between px-3 py-2 bg-surface border border-border rounded-lg text-sm text-slate-300 active:bg-slate-800 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FunnelIcon />
|
||||||
|
<span className="font-medium">Filters</span>
|
||||||
|
{activeFilterCount > 0 && (
|
||||||
|
<span className="bg-primary text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[20px] text-center">
|
||||||
|
{activeFilterCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<svg className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Filter dropdowns - always visible on desktop, toggle on mobile */}
|
||||||
|
<div className={`max-w-7xl mx-auto flex-wrap gap-2 md:gap-4 items-end ${isExpanded ? 'flex flex-col md:flex-row mt-3' : 'hidden md:flex md:flex-row'}`}>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Customer"
|
label="Customer"
|
||||||
selected={filters.customer}
|
selected={filters.customer}
|
||||||
options={options.customer}
|
options={options.customer}
|
||||||
onChange={(v) => onFilterChange('customer', v)}
|
onChange={(v) => onFilterChange('customer', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Year"
|
label="Year"
|
||||||
selected={filters.year}
|
selected={filters.year}
|
||||||
options={options.year}
|
options={options.year}
|
||||||
onChange={(v) => onFilterChange('year', v)}
|
onChange={(v) => onFilterChange('year', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Month"
|
label="Month"
|
||||||
selected={filters.month}
|
selected={filters.month}
|
||||||
options={options.month}
|
options={options.month}
|
||||||
onChange={(v) => onFilterChange('month', v)}
|
onChange={(v) => onFilterChange('month', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Week"
|
label="Week"
|
||||||
selected={filters.week}
|
selected={filters.week}
|
||||||
options={options.week}
|
options={options.week}
|
||||||
onChange={(v) => onFilterChange('week', v)}
|
onChange={(v) => onFilterChange('week', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
enableRangeSelect={true}
|
enableRangeSelect={true}
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
@@ -57,35 +86,35 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
|
|||||||
selected={filters.line}
|
selected={filters.line}
|
||||||
options={options.line}
|
options={options.line}
|
||||||
onChange={(v) => onFilterChange('line', v)}
|
onChange={(v) => onFilterChange('line', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="SKU"
|
label="SKU"
|
||||||
selected={filters.sku}
|
selected={filters.sku}
|
||||||
options={options.sku}
|
options={options.sku}
|
||||||
onChange={(v) => onFilterChange('sku', v)}
|
onChange={(v) => onFilterChange('sku', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Title"
|
label="Title"
|
||||||
selected={filters.title}
|
selected={filters.title}
|
||||||
options={options.title}
|
options={options.title}
|
||||||
onChange={(v) => onFilterChange('title', v)}
|
onChange={(v) => onFilterChange('title', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="ASIN"
|
label="ASIN"
|
||||||
selected={filters.asin}
|
selected={filters.asin}
|
||||||
options={options.asin}
|
options={options.asin}
|
||||||
onChange={(v) => onFilterChange('asin', v)}
|
onChange={(v) => onFilterChange('asin', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
/>
|
/>
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
label="Stock"
|
label="Stock"
|
||||||
selected={filters.stock}
|
selected={filters.stock}
|
||||||
options={options.stock}
|
options={options.stock}
|
||||||
onChange={(v) => onFilterChange('stock', v)}
|
onChange={(v) => onFilterChange('stock', v)}
|
||||||
className="flex-1"
|
className="w-full md:w-auto md:flex-1"
|
||||||
enableRangeSelect={true}
|
enableRangeSelect={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -414,8 +414,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 animate-fade-in p-6 h-full overflow-hidden">
|
<div className="flex flex-col gap-4 md:gap-6 animate-fade-in p-3 md:p-6 h-full overflow-hidden">
|
||||||
<div className="flex flex-col xl:flex-row gap-6 h-[400px] shrink-0">
|
<div className="flex flex-col xl:flex-row gap-4 md:gap-6 h-auto xl:h-[400px] shrink-0">
|
||||||
<div className="flex flex-col gap-4 min-w-[300px] xl:w-[25%]">
|
<div className="flex flex-col gap-4 min-w-[300px] xl:w-[25%]">
|
||||||
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
|
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Annual Forecast Total</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Annual Forecast Total</span>
|
||||||
@@ -478,8 +478,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex-1 flex flex-col min-h-0 relative">
|
<div className="bg-slate-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex-1 flex flex-col min-h-0 relative">
|
||||||
<div className="p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-4 bg-slate-800/20 shrink-0">
|
<div className="p-2 md:p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-2 md:gap-4 bg-slate-800/20 shrink-0">
|
||||||
<h3 className="text-lg font-bold text-white uppercase tracking-tight">
|
<h3 className="text-sm md:text-lg font-bold text-white uppercase tracking-tight">
|
||||||
Product Performance Comparison <span className="text-xs text-slate-500 font-normal normal-case ml-2">(v2.5 optimized)</span>
|
Product Performance Comparison <span className="text-xs text-slate-500 font-normal normal-case ml-2">(v2.5 optimized)</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -536,8 +536,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar relative">
|
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar scroll-touch relative">
|
||||||
<table className="w-full text-left border-collapse relative">
|
<table className="w-full text-left border-collapse relative text-[10px] md:text-sm">
|
||||||
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-[10px] font-black text-slate-500 uppercase tracking-wider">
|
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-[10px] font-black text-slate-500 uppercase tracking-wider">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4 text-left w-[30%]">
|
<th className="px-6 py-4 text-left w-[30%]">
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
|
|||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Prevent body scroll when dropdown is open on mobile
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && window.innerWidth < 768) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => { document.body.style.overflow = ''; };
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const filteredOptions = useMemo(() => {
|
const filteredOptions = useMemo(() => {
|
||||||
if (!searchTerm) return options;
|
if (!searchTerm) return options;
|
||||||
return options.filter(opt => opt.toLowerCase().includes(searchTerm.toLowerCase()));
|
return options.filter(opt => opt.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
@@ -139,8 +147,23 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Mobile overlay backdrop */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute top-[calc(100%+4px)] left-0 w-64 max-h-96 overflow-hidden bg-slate-900 border border-slate-700 rounded-xl shadow-2xl z-[100] animate-fade-in flex flex-col">
|
<div className="fixed inset-0 bg-black/50 z-[99] md:hidden" onClick={() => setIsOpen(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-x-3 bottom-3 max-h-[75vh] md:absolute md:inset-auto md:top-[calc(100%+4px)] md:left-0 md:bottom-auto md:w-64 md:max-h-96 overflow-hidden bg-slate-900 border border-slate-700 rounded-xl shadow-2xl z-[100] animate-fade-in flex flex-col">
|
||||||
|
|
||||||
|
{/* Mobile header with close */}
|
||||||
|
<div className="flex items-center justify-between p-3 border-b border-slate-800 md:hidden">
|
||||||
|
<h3 className="text-sm font-bold text-white">{label}</h3>
|
||||||
|
<button onClick={() => setIsOpen(false)} className="text-slate-400 active:text-white p-1">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<div className="p-2 border-b border-slate-800 sticky top-0 bg-slate-900 z-10">
|
<div className="p-2 border-b border-slate-800 sticky top-0 bg-slate-900 z-10">
|
||||||
@@ -209,14 +232,14 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 overflow-y-auto custom-scrollbar p-2 max-h-60">
|
<div className="space-y-1 overflow-y-auto custom-scrollbar scroll-touch p-2 max-h-60 md:max-h-60">
|
||||||
{filteredOptions.map((opt) => (
|
{filteredOptions.map((opt) => (
|
||||||
<label key={opt} className="flex items-center space-x-3 p-2 rounded hover:bg-slate-800 cursor-pointer group">
|
<label key={opt} className="flex items-center space-x-3 p-2.5 md:p-2 rounded hover:bg-slate-800 active:bg-slate-700 cursor-pointer group">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selected.includes(opt)}
|
checked={selected.includes(opt)}
|
||||||
onChange={() => toggleOption(opt)}
|
onChange={() => toggleOption(opt)}
|
||||||
className="form-checkbox h-4 w-4 text-primary rounded border-slate-600 bg-slate-800 focus:ring-primary focus:ring-offset-slate-900 transition duration-150 ease-in-out"
|
className="form-checkbox h-5 w-5 md:h-4 md:w-4 text-primary rounded border-slate-600 bg-slate-800 focus:ring-primary focus:ring-offset-slate-900 transition duration-150 ease-in-out"
|
||||||
/>
|
/>
|
||||||
<span className={`text-sm break-all group-hover:text-white ${selected.includes(opt) ? 'text-white' : 'text-slate-400'}`}>
|
<span className={`text-sm break-all group-hover:text-white ${selected.includes(opt) ? 'text-white' : 'text-slate-400'}`}>
|
||||||
{opt}
|
{opt}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ const TopMovers: React.FC<TopMoversProps> = ({ data }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-7xl mx-auto pb-24 animate-fade-in">
|
<div className="space-y-4 md:space-y-6 max-w-7xl mx-auto px-1 md:px-0 pb-24 animate-fade-in">
|
||||||
|
|
||||||
{/* Controls Header */}
|
{/* Controls Header */}
|
||||||
<div className="bg-surface border border-border rounded-xl p-6 shadow-sm flex flex-col md:flex-row justify-between items-center gap-4">
|
<div className="bg-surface border border-border rounded-xl p-6 shadow-sm flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
|
|||||||
@@ -632,9 +632,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
|||||||
}, [sortedRows, allWeeks, weekTotals]);
|
}, [sortedRows, allWeeks, weekTotals]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 animate-fade-in">
|
<div className="flex flex-col gap-3 md:gap-4 animate-fade-in px-1 md:px-0">
|
||||||
{/* Toolbar: Search & Pagination */}
|
{/* Toolbar: Search & Pagination */}
|
||||||
<div className="flex flex-col lg:flex-row justify-between items-center gap-4 bg-slate-900 border border-white/10 p-4 rounded-xl shadow-lg">
|
<div className="flex flex-col lg:flex-row justify-between items-center gap-3 md:gap-4 bg-slate-900 border border-white/10 p-2 md:p-4 rounded-xl shadow-lg">
|
||||||
<div className="flex flex-wrap items-center gap-4 w-full lg:w-auto">
|
<div className="flex flex-wrap items-center gap-4 w-full lg:w-auto">
|
||||||
{/* Search Input */}
|
{/* Search Input */}
|
||||||
<div className="relative w-full md:w-80">
|
<div className="relative w-full md:w-80">
|
||||||
@@ -739,8 +739,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-900 border border-white/10 rounded-xl overflow-hidden shadow-2xl flex-1 min-h-0">
|
<div className="bg-slate-900 border border-white/10 rounded-xl overflow-hidden shadow-2xl flex-1 min-h-0">
|
||||||
<div className="overflow-x-auto overflow-y-auto h-full max-h-[calc(100vh-220px)] custom-scrollbar">
|
<div className="overflow-x-auto overflow-y-auto h-full max-h-[calc(100vh-220px)] custom-scrollbar scroll-touch">
|
||||||
<table className="w-full text-left border-collapse min-w-[max-content]">
|
<table className="w-full text-left border-collapse min-w-[max-content] text-[10px] md:text-sm">
|
||||||
<thead className="sticky top-0 z-20 bg-slate-900">
|
<thead className="sticky top-0 z-20 bg-slate-900">
|
||||||
<tr className="bg-slate-900 border-b border-white/10 relative z-30">
|
<tr className="bg-slate-900 border-b border-white/10 relative z-30">
|
||||||
<th
|
<th
|
||||||
|
|||||||
+25
@@ -44,6 +44,14 @@
|
|||||||
background: #475569;
|
background: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Thinner scrollbar on mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes bounce-subtle {
|
@keyframes bounce-subtle {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
@@ -59,6 +67,23 @@
|
|||||||
.animate-bounce-subtle {
|
.animate-bounce-subtle {
|
||||||
animation: bounce-subtle 2s ease-in-out infinite;
|
animation: bounce-subtle 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bottom nav safe area for iPhones with notch */
|
||||||
|
.pb-safe {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth momentum scrolling on iOS */
|
||||||
|
.scroll-touch {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bottom nav spacing - adds padding so content isn't hidden behind fixed bottom nav */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
body {
|
||||||
|
padding-bottom: calc(4rem + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Reference in New Issue
Block a user