feat: Complete mobile UX/UI overhaul
Major improvements: - New CSS design system with custom properties (tokens) - Consistent spacing scale (4px base) - Touch-friendly sizing (44px min targets) - Improved Carousel with better touch handling and rubber-band effect - Enhanced FilterControls (auto-collapse on mobile) - Better stat card styling with change indicators - Refined chart cards and toggle switches - Smoother transitions and micro-interactions - Better RTL support - Print styles - Responsive breakpoints for tablet (1024px), mobile (768px), and small mobile (375px) - Cleaner typography hierarchy - Subtle shadows and depth Components updated: - App.css: Complete rewrite with design tokens - Carousel.jsx: Better touch gestures with velocity detection - StatCard.jsx: Improved change indicator styling - FilterControls.jsx: Auto-collapse on mobile - EmptyState.jsx: Better accessibility - ChartExport.js: Cleaned up unused imports
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useCallback } from 'react';
|
||||
import React, { useRef, useCallback, useState } from 'react';
|
||||
|
||||
function Carousel({
|
||||
children,
|
||||
@@ -8,25 +8,66 @@ function Carousel({
|
||||
showLabels = true,
|
||||
className = ''
|
||||
}) {
|
||||
const touchStart = useRef(null);
|
||||
const touchStartX = useRef(null);
|
||||
const touchStartY = useRef(null);
|
||||
const trackRef = useRef(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragOffset, setDragOffset] = useState(0);
|
||||
const itemCount = React.Children.count(children);
|
||||
|
||||
|
||||
// Threshold for swipe detection
|
||||
const SWIPE_THRESHOLD = 50;
|
||||
const VELOCITY_THRESHOLD = 0.3;
|
||||
|
||||
const handleTouchStart = useCallback((e) => {
|
||||
touchStart.current = e.touches[0].clientX;
|
||||
touchStartX.current = e.touches[0].clientX;
|
||||
touchStartY.current = e.touches[0].clientY;
|
||||
setIsDragging(true);
|
||||
setDragOffset(0);
|
||||
}, []);
|
||||
|
||||
const handleTouchMove = useCallback((e) => {
|
||||
if (!touchStartX.current || !isDragging) return;
|
||||
|
||||
const currentX = e.touches[0].clientX;
|
||||
const currentY = e.touches[0].clientY;
|
||||
const diffX = currentX - touchStartX.current;
|
||||
const diffY = currentY - touchStartY.current;
|
||||
|
||||
// Only handle horizontal swipes
|
||||
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||
e.preventDefault();
|
||||
// Add resistance at edges
|
||||
let offset = diffX;
|
||||
if ((activeIndex === 0 && diffX > 0) || (activeIndex === itemCount - 1 && diffX < 0)) {
|
||||
offset = diffX * 0.3; // Rubber band effect
|
||||
}
|
||||
setDragOffset(offset);
|
||||
}
|
||||
}, [isDragging, activeIndex, itemCount]);
|
||||
|
||||
const handleTouchEnd = useCallback((e) => {
|
||||
if (!touchStart.current) return;
|
||||
const diff = touchStart.current - e.changedTouches[0].clientX;
|
||||
if (Math.abs(diff) > 50) {
|
||||
if (!touchStartX.current || !isDragging) return;
|
||||
|
||||
const endX = e.changedTouches[0].clientX;
|
||||
const diff = touchStartX.current - endX;
|
||||
const velocity = Math.abs(diff) / 200; // Rough velocity calc
|
||||
|
||||
// Determine if we should change slide
|
||||
if (Math.abs(diff) > SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) {
|
||||
if (diff > 0 && activeIndex < itemCount - 1) {
|
||||
setActiveIndex(activeIndex + 1);
|
||||
} else if (diff < 0 && activeIndex > 0) {
|
||||
setActiveIndex(activeIndex - 1);
|
||||
}
|
||||
}
|
||||
touchStart.current = null;
|
||||
}, [activeIndex, setActiveIndex, itemCount]);
|
||||
|
||||
// Reset
|
||||
touchStartX.current = null;
|
||||
touchStartY.current = null;
|
||||
setIsDragging(false);
|
||||
setDragOffset(0);
|
||||
}, [isDragging, activeIndex, setActiveIndex, itemCount]);
|
||||
|
||||
const handleKeyDown = useCallback((e) => {
|
||||
if (e.key === 'ArrowLeft' && activeIndex > 0) {
|
||||
@@ -36,18 +77,40 @@ function Carousel({
|
||||
}
|
||||
}, [activeIndex, setActiveIndex, itemCount]);
|
||||
|
||||
// Calculate transform
|
||||
const baseTransform = -(activeIndex * 100);
|
||||
const dragPercentage = trackRef.current ? (dragOffset / trackRef.current.offsetWidth) * 100 : 0;
|
||||
const transform = baseTransform + dragPercentage;
|
||||
|
||||
return (
|
||||
<div className={`carousel ${className}`} onKeyDown={handleKeyDown} tabIndex={0}>
|
||||
<div
|
||||
className={`carousel ${className}`}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label="Carousel"
|
||||
>
|
||||
<div className="carousel-container">
|
||||
<div className="carousel-viewport">
|
||||
<div
|
||||
ref={trackRef}
|
||||
className="carousel-track"
|
||||
style={{ transform: `translateX(-${activeIndex * 100}%)` }}
|
||||
style={{
|
||||
transform: `translateX(${transform}%)`,
|
||||
transition: isDragging ? 'none' : 'transform 400ms cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||
}}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{React.Children.map(children, (child, i) => (
|
||||
<div className="carousel-slide" key={i}>
|
||||
<div
|
||||
className="carousel-slide"
|
||||
key={i}
|
||||
role="tabpanel"
|
||||
aria-hidden={activeIndex !== i}
|
||||
aria-label={labels[i] || `Slide ${i + 1}`}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
@@ -64,6 +127,7 @@ function Carousel({
|
||||
role="tab"
|
||||
aria-selected={activeIndex === i}
|
||||
aria-label={labels[i] || `Slide ${i + 1}`}
|
||||
aria-controls={`slide-${i}`}
|
||||
>
|
||||
{showLabels && labels[i] && (
|
||||
<span className="dot-label">{labels[i]}</span>
|
||||
|
||||
Reference in New Issue
Block a user