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:
Christian Vidal Wolf
2026-02-16 09:27:46 +01:00
co-authored by Claude Opus 4.6
parent fecc7264c8
commit 9895545910
11 changed files with 172 additions and 72 deletions
+39 -16
View File
@@ -652,11 +652,11 @@ const App: React.FC = () => {
<div className="min-h-screen flex flex-col bg-background text-slate-200">
{/* 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="flex items-center gap-8">
{/* Logo Container (Horizontal Box) - Persistent User Image */}
<div className="h-16 w-64 relative flex-shrink-0">
<div className="flex items-center gap-3 md:gap-8">
{/* Logo Container - Smaller on mobile */}
<div className="h-10 w-32 md:h-16 md:w-64 relative flex-shrink-0">
<CrazeLogo />
</div>
@@ -677,63 +677,62 @@ const App: React.FC = () => {
</div>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-3 md:gap-6">
{/* Force Sync Action */}
<button
onClick={() => { handleDataFetch(); handleAdsFetch(); handleTrafficFetch(); handleVendorStockFetch(); handleBuyBoxFetch(); }}
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
? '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'}`}
>
<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>
{/* View Switcher is the priority now */}
{/* View Switcher */}
<div className="flex bg-slate-900 rounded-lg p-1 border border-border">
{/* View Switcher - Hidden on mobile (shown in bottom nav instead) */}
<div className="hidden md:flex bg-slate-900 rounded-lg p-1 border border-border">
<button
onClick={() => setView('dashboard')}
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'}`}
>
<ChartIcon /> <span className="hidden sm:inline">Dashboard</span>
<ChartIcon /> <span className="hidden lg:inline">Dashboard</span>
</button>
<button
onClick={() => setView('table')}
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'}`}
>
<TableIcon /> <span className="hidden sm:inline">Grid</span>
<TableIcon /> <span className="hidden lg:inline">Grid</span>
</button>
<button
onClick={() => setView('weekly')}
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'}`}
>
<TrendingIcon /> <span className="hidden sm:inline">Weekly Sales</span>
<TrendingIcon /> <span className="hidden lg:inline">Weekly Sales</span>
</button>
<button
onClick={() => setView('movers')}
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'}`}
>
<TrendingIcon /> <span className="hidden sm:inline">Movers</span>
<TrendingIcon /> <span className="hidden lg:inline">Movers</span>
</button>
<button
onClick={() => setView('ads')}
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'}`}
>
<MegaphoneIcon /> <span className="hidden sm:inline">Ads</span>
<MegaphoneIcon /> <span className="hidden lg:inline">Ads</span>
</button>
<button
onClick={() => setView('forecast')}
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'}`}
>
<ChartIcon /> <span className="hidden sm:inline">Fc 26</span>
<ChartIcon /> <span className="hidden lg:inline">Fc 26</span>
</button>
</div>
</div>
@@ -850,6 +849,30 @@ const App: React.FC = () => {
{/* Chat Assistant */}
<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 */}
{
isDataModalOpen && (
+6 -6
View File
@@ -93,21 +93,21 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
return (
<>
{/* Trigger Button */}
{/* Trigger Button - above bottom nav on mobile */}
<button
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'}`}
>
<ChatIcon />
</button>
{/* Chat Window */}
{/* Chat Window - fullscreen on mobile, floating on desktop */}
<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
? 'bottom-6 right-6 w-96 h-[600px] rounded-2xl opacity-100 translate-y-0'
: 'bottom-6 right-6 w-96 h-0 opacity-0 translate-y-10 pointer-events-none'}`}
? '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-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 */}
<div className="bg-primary/10 p-4 border-b border-border flex justify-between items-center backdrop-blur">
+6 -6
View File
@@ -363,10 +363,10 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
}, [filteredAndSorted]);
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>
<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">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
<span>
@@ -376,7 +376,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
</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="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" />
@@ -395,7 +395,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
</div>
<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>
<h2 className="text-sm font-bold text-white uppercase tracking-wider">Product Inventory</h2>
<p className="text-[10px] text-slate-400 mt-0.5">
@@ -428,8 +428,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
</div>
</div>
<div className="overflow-x-auto flex-1 custom-scrollbar">
<table className="w-full text-left border-collapse text-[11px]">
<div className="overflow-x-auto flex-1 custom-scrollbar scroll-touch">
<table className="w-full text-left border-collapse text-[10px] md:text-[11px]">
<thead className="sticky top-0 z-20 bg-[#1A1F29]">
<tr className="border-b border-white/5">
<th className="p-4 font-black text-slate-400 uppercase tracking-widest min-w-[250px]">
+14 -14
View File
@@ -34,12 +34,12 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
if (isExpanded) {
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
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"
>
<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>
</button>
<div className="flex justify-between items-center mb-4 border-b border-slate-800 pb-4 shrink-0">
<h3 className="text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
<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-base md:text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
</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}
</div>
</div>
@@ -60,13 +60,13 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
return (
<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">
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
<div className="flex justify-between items-start mb-3 md:mb-4">
<h3 className="text-xs md:text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
<button
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"
>
<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>
</button>
</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>
);
};
@@ -102,8 +102,8 @@ const MultiYearKPICard: React.FC<{
const displayTitle = contextData ? `${title} (Selected Item)` : title;
return (
<div className="bg-gradient-to-br from-surface to-slate-900 border border-border rounded-xl p-6 flex flex-col justify-center">
<h3 className="text-sm font-medium text-slate-400 mb-3">{displayTitle}</h3>
<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-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>}
@@ -508,7 +508,7 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData, adsData = [] }
const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300);
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 */}
{adsKPIsByYear && availableAdsYears.length > 0 && (
+3 -3
View File
@@ -738,7 +738,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
}, [rowFilters, data, effectiveDimensions, searchTerm]);
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 */}
{showChart && chartData.length > 0 && (
@@ -1173,8 +1173,8 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
)}
{/* Data Table */}
<div className="overflow-x-auto min-h-[400px]">
<table className="w-full text-left text-sm border-collapse">
<div className="overflow-x-auto min-h-[400px] scroll-touch">
<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">
{/* NEW: Filtered Totals Header Row */}
<tr className="bg-indigo-950/20 border-b border-indigo-500/30">
+41 -12
View File
@@ -1,7 +1,8 @@
import React from 'react';
import React, { useState, useMemo } from 'react';
import { FilterState } from '../types';
import MultiSelectDropdown from './MultiSelectDropdown';
import { FunnelIcon } from './Icons';
interface FilterBarProps {
filters: FilterState;
@@ -20,36 +21,64 @@ interface FilterBarProps {
}
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 (
<div className="bg-slate-950 border-b border-border sticky top-0 z-[60] p-4 shadow-xl">
<div className="max-w-7xl mx-auto flex flex-wrap gap-4 items-end">
<div className="bg-slate-950 border-b border-border sticky top-0 z-[60] p-2 md:p-4 shadow-xl">
{/* 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
label="Customer"
selected={filters.customer}
options={options.customer}
onChange={(v) => onFilterChange('customer', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Year"
selected={filters.year}
options={options.year}
onChange={(v) => onFilterChange('year', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Month"
selected={filters.month}
options={options.month}
onChange={(v) => onFilterChange('month', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Week"
selected={filters.week}
options={options.week}
onChange={(v) => onFilterChange('week', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
enableRangeSelect={true}
/>
<MultiSelectDropdown
@@ -57,35 +86,35 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
selected={filters.line}
options={options.line}
onChange={(v) => onFilterChange('line', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="SKU"
selected={filters.sku}
options={options.sku}
onChange={(v) => onFilterChange('sku', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Title"
selected={filters.title}
options={options.title}
onChange={(v) => onFilterChange('title', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="ASIN"
selected={filters.asin}
options={options.asin}
onChange={(v) => onFilterChange('asin', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Stock"
selected={filters.stock}
options={options.stock}
onChange={(v) => onFilterChange('stock', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
enableRangeSelect={true}
/>
</div>
+6 -6
View File
@@ -414,8 +414,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
};
return (
<div className="flex flex-col gap-6 animate-fade-in 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 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-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="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>
@@ -478,8 +478,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
</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="p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-4 bg-slate-800/20 shrink-0">
<h3 className="text-lg font-bold text-white uppercase tracking-tight">
<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-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>
</h3>
<div className="flex items-center gap-4">
@@ -536,8 +536,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
</div>
</div>
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar relative">
<table className="w-full text-left border-collapse relative">
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar scroll-touch 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">
<tr>
<th className="px-6 py-4 text-left w-[30%]">
+27 -4
View File
@@ -38,6 +38,14 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
}
}, [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(() => {
if (!searchTerm) return options;
return options.filter(opt => opt.toLowerCase().includes(searchTerm.toLowerCase()));
@@ -139,8 +147,23 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
</svg>
</button>
{/* Mobile overlay backdrop */}
{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 */}
<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>
</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) => (
<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
type="checkbox"
checked={selected.includes(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'}`}>
{opt}
+1 -1
View File
@@ -241,7 +241,7 @@ const TopMovers: React.FC<TopMoversProps> = ({ data }) => {
}
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 */}
<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">
+4 -4
View File
@@ -632,9 +632,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
}, [sortedRows, allWeeks, weekTotals]);
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 */}
<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">
{/* Search Input */}
<div className="relative w-full md:w-80">
@@ -739,8 +739,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
</div>
<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">
<table className="w-full text-left border-collapse min-w-[max-content]">
<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] text-[10px] md:text-sm">
<thead className="sticky top-0 z-20 bg-slate-900">
<tr className="bg-slate-900 border-b border-white/10 relative z-30">
<th
+25
View File
@@ -44,6 +44,14 @@
background: #475569;
}
/* Thinner scrollbar on mobile */
@media (max-width: 767px) {
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
}
@keyframes bounce-subtle {
0%,
@@ -59,6 +67,23 @@
.animate-bounce-subtle {
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>
</head>