feat: adaptation mobile — drawer bottom + layout responsive
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Sur smartphone (< 640px) : panneau stats masqué par défaut, accessible via un bottom drawer animé (bouton ☰). PeriodSelector passe en flex-wrap avec padding tactile 44px. AnimationPlayer s'adapte à la largeur écran. Badge ville focus affiché directement sur la carte en mode mobile. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+61
-15
@@ -11,6 +11,7 @@ import type { TransactionArc } from './data/arcData';
|
||||
import { computeStats } from './data/mockData';
|
||||
import { computeFlowStats } from './data/arcData';
|
||||
import { useAnimation } from './hooks/useAnimation';
|
||||
import { useMediaQuery } from './hooks/useMediaQuery';
|
||||
|
||||
export default function App() {
|
||||
const [periodDays, setPeriodDays] = useState(7);
|
||||
@@ -25,6 +26,8 @@ export default function App() {
|
||||
const [allTimestamps, setAllTimestamps] = useState<number[]>([]);
|
||||
const [viewMode, setViewMode] = useState<'heatmap' | 'flow'>('heatmap');
|
||||
const [focusCity, setFocusCity] = useState<string | null>(null);
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
const isMobile = useMediaQuery('(max-width: 639px)');
|
||||
|
||||
const animation = useAnimation(transactions, arcs, periodDays, allTimestamps);
|
||||
|
||||
@@ -86,20 +89,22 @@ export default function App() {
|
||||
[arcs, animation.visibleArcs, animation.active],
|
||||
);
|
||||
|
||||
const statsPanelProps = {
|
||||
stats: visibleStats,
|
||||
loading,
|
||||
periodDays,
|
||||
source,
|
||||
currentUD,
|
||||
animationLabel: animation.active ? (animation.currentFrame?.label ?? undefined) : undefined,
|
||||
viewMode,
|
||||
flowStats,
|
||||
focusCity,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-svh w-full overflow-hidden bg-[#0a0b0f] text-white">
|
||||
{/* Side panel */}
|
||||
<StatsPanel
|
||||
stats={visibleStats}
|
||||
loading={loading}
|
||||
periodDays={periodDays}
|
||||
source={source}
|
||||
currentUD={currentUD}
|
||||
animationLabel={animation.active ? (animation.currentFrame?.label ?? undefined) : undefined}
|
||||
viewMode={viewMode}
|
||||
flowStats={flowStats}
|
||||
focusCity={focusCity}
|
||||
/>
|
||||
{/* Side panel — desktop uniquement */}
|
||||
{!isMobile && <StatsPanel {...statsPanelProps} />}
|
||||
|
||||
{/* Map area */}
|
||||
<div className="relative flex-1 min-w-0">
|
||||
@@ -113,8 +118,19 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Bouton menu — mobile uniquement */}
|
||||
{isMobile && (
|
||||
<button
|
||||
onClick={() => setPanelOpen(true)}
|
||||
className="absolute top-4 left-4 z-[1001] w-10 h-10 bg-[#0a0b0f]/90 backdrop-blur-sm border border-[#2e2f3a] rounded-xl flex items-center justify-center text-[#d4a843] text-lg"
|
||||
aria-label="Ouvrir le panneau"
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Period selector — floating over map */}
|
||||
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-[1000]">
|
||||
<div className={`absolute top-4 z-[1000] ${isMobile ? 'left-16 right-4' : 'left-1/2 -translate-x-1/2'}`}>
|
||||
<PeriodSelector
|
||||
value={periodDays}
|
||||
onChange={handlePeriodChange}
|
||||
@@ -125,8 +141,16 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Transaction count + source badge (masqués en mode animation) */}
|
||||
{!loading && !animation.active && (
|
||||
{/* Badge ville focus — mobile uniquement */}
|
||||
{isMobile && focusCity && (
|
||||
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-[1001] flex items-center gap-2 bg-[#0a0b0f]/90 backdrop-blur-sm border border-[#d4a843]/40 rounded-full px-3 py-1.5">
|
||||
<span className="text-[#d4a843] text-xs font-medium">{focusCity}</span>
|
||||
<button onClick={() => setFocusCity(null)} className="text-[#4b5563] hover:text-white text-xs leading-none">✕</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Transaction count + source badge (masqués sur mobile et en mode animation) */}
|
||||
{!loading && !animation.active && !isMobile && (
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-[1000] flex items-center gap-2">
|
||||
<div className="bg-[#0a0b0f]/80 backdrop-blur-sm border border-[#2e2f3a] rounded-full px-4 py-1.5 text-xs text-[#6b7280]">
|
||||
<span className="text-[#d4a843] font-medium">{transactions.length}</span> transactions affichées
|
||||
@@ -168,6 +192,28 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom drawer — mobile uniquement */}
|
||||
{isMobile && (
|
||||
<>
|
||||
{/* Overlay */}
|
||||
{panelOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-[1009] bg-black/50"
|
||||
onClick={() => setPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{/* Drawer */}
|
||||
<div
|
||||
className={`fixed bottom-0 left-0 right-0 z-[1010] h-[85vh] flex flex-col transition-transform duration-300 ${panelOpen ? 'translate-y-0' : 'translate-y-full'}`}
|
||||
>
|
||||
<div className="flex justify-center pt-2 pb-1 bg-[#0a0b0f] rounded-t-2xl border-t border-x border-[#2e2f3a] shrink-0">
|
||||
<div className="w-10 h-1 rounded-full bg-[#2e2f3a]" />
|
||||
</div>
|
||||
<StatsPanel {...statsPanelProps} onClose={() => setPanelOpen(false)} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user