diff --git a/src/App.css b/src/App.css index 4bc0169..142de2f 100644 --- a/src/App.css +++ b/src/App.css @@ -325,6 +325,37 @@ body { font-size: 0.875rem; } +/* Page Title with Actions (Labels toggle) */ +.page-title-with-actions { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 24px; +} + +.page-title-with-actions .page-title { + margin-bottom: 0; +} + +.page-title-with-actions .toggle-with-label { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; +} + +.toggle-with-label .toggle-text { + font-size: 0.6875rem; + font-weight: 500; + color: var(--text-muted); +} + +.page-title-with-actions .toggle-switch button { + padding: 3px 8px; + font-size: 0.6875rem; +} + /* Filters - now uses .controls for consistency */ /* Stats Grid */ @@ -599,6 +630,58 @@ table tbody tr:hover { font-weight: 500; } +/* Period Display Banner */ +.period-display-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 24px; + background: var(--bg); + padding: 20px 32px; + border-radius: 12px; + margin-bottom: 24px; +} + +.period-display-banner .period-box { + text-align: center; + min-width: 180px; +} + +.period-display-banner .period-box.prev { + color: var(--text-secondary); +} + +.period-display-banner .period-box.curr { + color: var(--text-primary); +} + +.period-display-banner .period-label { + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + margin-bottom: 4px; +} + +.period-display-banner .period-value { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 2px; +} + +.period-display-banner .period-dates { + font-size: 0.8125rem; + color: var(--text-muted); +} + +.period-display-banner .period-vs { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-muted); + padding: 0 8px; +} + /* Comparison Metrics */ .comparison-grid { display: grid; @@ -1812,244 +1895,3 @@ table tbody tr:hover { width: 100%; height: 100%; } - -/* ======================================== - MOBILE EXCELLENCE ENHANCEMENTS - These rules ONLY apply to mobile (≤768px) - and do not affect desktop layout - ======================================== */ - -@media (max-width: 768px) { - /* Enhanced touch targets - minimum 44px */ - .mobile-nav-item { - min-height: 44px; - min-width: 60px; - } - - .carousel-dot { - min-height: 36px; - min-width: 36px; - } - - .toggle-switch button { - min-height: 32px; - padding: 6px 12px; - } - - .chart-metric-selector button { - min-height: 28px; - padding: 4px 8px; - } - - /* Smoother carousel transitions */ - .carousel-track { - transition: transform 350ms cubic-bezier(0.25, 0.46, 0.45, 0.94); - will-change: transform; - } - - /* Better visual feedback on touch */ - .carousel-dot:active, - .mobile-nav-item:active, - .toggle-switch button:active, - .stat-card:active { - transform: scale(0.96); - transition: transform 100ms ease; - } - - /* Active indicator for bottom nav */ - .mobile-nav-item.active { - position: relative; - } - - .mobile-nav-item.active::after { - content: ''; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 20px; - height: 3px; - background: var(--primary); - border-radius: 0 0 3px 3px; - } - - /* Improved stat cards in carousel */ - .stats-carousel .stat-card { - min-height: 100px; - display: flex; - flex-direction: column; - justify-content: center; - } - - /* Better chart card spacing */ - .charts-carousel .chart-card { - padding: 16px; - } - - .charts-carousel .chart-card h2 { - font-size: 0.875rem; - margin-bottom: 12px; - } - - /* Ensure toggle corner doesn't overlap title */ - .charts-carousel .chart-card h2 { - padding-right: 90px; - } - - .charts-carousel .toggle-corner { - top: 12px; - right: 12px; - } - - /* Improved carousel dot labels */ - .carousel-dots.labeled .carousel-dot { - padding: 6px 12px; - border-radius: 16px; - } - - .carousel-dots.labeled .carousel-dot.active { - background: var(--primary); - border-color: var(--primary); - } - - .carousel-dots.labeled .carousel-dot.active .dot-label { - color: white; - } - - /* Filters - more compact on mobile */ - .controls { - padding: 12px 14px; - margin-bottom: 16px; - } - - .controls h3 { - font-size: 0.6875rem; - } - - .control-group label { - font-size: 0.625rem; - } - - .control-group select, - .control-group input[type="date"] { - font-size: 0.8125rem; - padding: 10px 12px; - min-height: 44px; - } - - /* Period banner - stacked on mobile */ - .period-display-banner { - flex-direction: column; - gap: 16px; - padding: 16px; - } - - .period-display-banner .period-box { - min-width: unset; - } - - .period-display-banner .period-value { - font-size: 1.25rem; - } - - .period-display-banner .period-dates { - font-size: 0.75rem; - } - - /* Table scroll hint - fade at edges */ - .table-container { - position: relative; - } - - .table-container::after { - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 20px; - background: linear-gradient(to left, var(--surface), transparent); - pointer-events: none; - } - - /* Page title adjustments */ - .page-title { - margin-bottom: 16px; - } - - .page-title h1 { - font-size: 1.25rem; - line-height: 1.3; - } - - .page-title p { - font-size: 0.8125rem; - line-height: 1.4; - } - - /* Compact header toggle */ - .page-title-with-actions .toggle-switch button { - padding: 4px 8px; - font-size: 0.625rem; - min-height: 26px; - } - - .toggle-with-label .toggle-text { - font-size: 0.625rem; - } -} - -/* Extra small screens (≤375px) */ -@media (max-width: 375px) { - .dashboard, - .comparison { - padding: 12px; - padding-bottom: 80px; - } - - .page-title h1 { - font-size: 1.125rem; - } - - .stats-carousel .stat-value { - font-size: 1.5rem; - } - - .charts-carousel .chart-container { - height: 200px; - } - - .carousel-dot { - min-height: 32px; - min-width: 32px; - padding: 4px 8px; - } - - .carousel-dot .dot-label { - font-size: 0.5625rem; - } - - .mobile-nav-item { - padding: 6px 10px; - font-size: 0.5625rem; - } - - .mobile-nav-item svg { - width: 20px; - height: 20px; - } - - /* Smaller table text */ - table { - font-size: 0.6875rem; - } - - table th { - font-size: 0.5625rem; - padding: 8px 4px; - } - - table td { - padding: 8px 4px; - } -} diff --git a/src/components/shared/Carousel.jsx b/src/components/shared/Carousel.jsx index 4a9e963..7d6637c 100644 --- a/src/components/shared/Carousel.jsx +++ b/src/components/shared/Carousel.jsx @@ -1,4 +1,4 @@ -import React, { useRef, useCallback, useState } from 'react'; +import React, { useRef, useCallback } from 'react'; function Carousel({ children, @@ -8,62 +8,25 @@ function Carousel({ showLabels = true, className = '' }) { - const touchStartX = useRef(null); - const touchStartY = useRef(null); - const trackRef = useRef(null); - const [isDragging, setIsDragging] = useState(false); - const [dragOffset, setDragOffset] = useState(0); + const touchStart = useRef(null); const itemCount = React.Children.count(children); - - const SWIPE_THRESHOLD = 50; const handleTouchStart = useCallback((e) => { - touchStartX.current = e.touches[0].clientX; - touchStartY.current = e.touches[0].clientY; - setIsDragging(true); - setDragOffset(0); + touchStart.current = e.touches[0].clientX; }, []); - 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 = Math.abs(currentY - touchStartY.current); - - // Only handle horizontal swipes (prevent vertical scroll interference) - if (Math.abs(diffX) > diffY) { - // Add resistance at edges (rubber band effect) - let offset = diffX; - if ((activeIndex === 0 && diffX > 0) || (activeIndex === itemCount - 1 && diffX < 0)) { - offset = diffX * 0.25; - } - setDragOffset(offset); - } - }, [isDragging, activeIndex, itemCount]); - const handleTouchEnd = useCallback((e) => { - if (!touchStartX.current || !isDragging) return; - - const endX = e.changedTouches[0].clientX; - const diff = touchStartX.current - endX; - - // Change slide if swipe exceeds threshold - if (Math.abs(diff) > SWIPE_THRESHOLD) { + if (!touchStart.current) return; + const diff = touchStart.current - e.changedTouches[0].clientX; + if (Math.abs(diff) > 50) { if (diff > 0 && activeIndex < itemCount - 1) { setActiveIndex(activeIndex + 1); } else if (diff < 0 && activeIndex > 0) { setActiveIndex(activeIndex - 1); } } - - // Reset - touchStartX.current = null; - touchStartY.current = null; - setIsDragging(false); - setDragOffset(0); - }, [isDragging, activeIndex, setActiveIndex, itemCount]); + touchStart.current = null; + }, [activeIndex, setActiveIndex, itemCount]); const handleKeyDown = useCallback((e) => { if (e.key === 'ArrowLeft' && activeIndex > 0) { @@ -73,41 +36,18 @@ function Carousel({ } }, [activeIndex, setActiveIndex, itemCount]); - // Calculate transform with drag offset - const baseTransform = -(activeIndex * 100); - const dragPercentage = trackRef.current - ? (dragOffset / trackRef.current.offsetWidth) * 100 - : 0; - const transform = baseTransform + dragPercentage; - return ( -