diff --git a/src/components/DemoMode.tsx b/src/components/DemoMode.tsx index 44178da..d489fee 100644 --- a/src/components/DemoMode.tsx +++ b/src/components/DemoMode.tsx @@ -1,10 +1,20 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { PhoneFrame } from './sketchy'; import { screens, getScreen } from '../screens'; import { getStoriesForScreen, categoryLabels, categoryColors, priorityColors } from '../data'; import { getStoryUrl } from '../router'; import { ThemeToggle } from './ThemeToggle'; +function useIsMobile(breakpoint = 768) { + const [isMobile, setIsMobile] = useState(window.innerWidth < breakpoint); + useEffect(() => { + const handleResize = () => setIsMobile(window.innerWidth < breakpoint); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [breakpoint]); + return isMobile; +} + interface DemoModeProps { initialScreenId: string; onBack: () => void; @@ -15,6 +25,8 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod const [currentScreenId, setCurrentScreenId] = useState(initialScreenId); const [history, setHistory] = useState([initialScreenId]); const [historyIndex, setHistoryIndex] = useState(0); + const [sidebarOpen, setSidebarOpen] = useState(false); + const isMobile = useIsMobile(); const currentScreen = getScreen(currentScreenId); const ScreenComponent = currentScreen?.component; @@ -56,7 +68,21 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod background: 'var(--tool-bg)', overflow: 'hidden', transition: 'background-color 0.2s ease', + position: 'relative', }}> + {/* Mobile overlay */} + {isMobile && sidebarOpen && ( +
setSidebarOpen(false)} + style={{ + position: 'fixed', + inset: 0, + background: 'rgba(0,0,0,0.5)', + zIndex: 40, + }} + /> + )} + {/* Left Sidebar */}
{/* Back button and theme toggle */}
@@ -253,24 +287,87 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod
+ {/* Mobile header */} + {isMobile && ( +
+ + + {currentScreen?.name} + + +
+ )}
- - {ScreenComponent && } - +
+ + {ScreenComponent && } + +
@@ -278,7 +375,7 @@ export function DemoMode({ initialScreenId, onBack, onNavigateToStory }: DemoMod ); } -function ScaledPhoneFrame({ children }: { children: React.ReactNode }) { +function ScaledPhoneFrame({ children, isMobile = false }: { children: React.ReactNode; isMobile?: boolean }) { const phoneWidth = 375; const phoneHeight = 812; @@ -287,20 +384,24 @@ function ScaledPhoneFrame({ children }: { children: React.ReactNode }) { React.useEffect(() => { const calculateScale = () => { - const availableHeight = window.innerHeight - 48; // 24px padding on each side - const availableWidth = window.innerWidth - 280 - 48; // sidebar + padding + const mobileHeaderHeight = isMobile ? 56 : 0; + const padding = isMobile ? 24 : 48; + const sidebarWidth = isMobile ? 0 : 280; + + const availableHeight = window.innerHeight - padding - mobileHeaderHeight; + const availableWidth = window.innerWidth - sidebarWidth - padding; const scaleByHeight = availableHeight / phoneHeight; const scaleByWidth = availableWidth / phoneWidth; const newScale = Math.min(scaleByHeight, scaleByWidth, 1); - setScale(Math.max(0.5, newScale)); // minimum 50% scale + setScale(Math.max(0.4, newScale)); // minimum 40% scale for mobile }; calculateScale(); window.addEventListener('resize', calculateScale); return () => window.removeEventListener('resize', calculateScale); - }, []); + }, [isMobile]); return (
{ + const handleResize = () => setIsMobile(window.innerWidth < breakpoint); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [breakpoint]); + return isMobile; +} + interface GalleryProps { onSelectScreen: (screenId: string) => void; onShowStories: () => void; @@ -15,20 +25,27 @@ const DEFAULT_SCALE = 0.5; export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryProps) { const [scale, setScale] = useState(DEFAULT_SCALE); + const isMobile = useIsMobile(); return (
-
+

@@ -36,7 +53,7 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP

@@ -47,7 +64,8 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP

{/* User Stories button */} )} - {/* Zoom control */} -
- Zoom - setScale(Number(e.target.value) / 100)} - style={{ - width: 100, - accentColor: 'var(--tool-text)', - }} - /> - {Math.round(scale * 100)}% -
+ {/* Zoom control - hide on mobile */} + {!isMobile && ( +
+ Zoom + setScale(Number(e.target.value) / 100)} + style={{ + width: 100, + accentColor: 'var(--tool-text)', + }} + /> + {Math.round(scale * 100)}% +
+ )} {/* Theme toggle */} @@ -113,14 +133,14 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP
-
+
{screenGroups.map((group) => ( -
+
{/* Group header */}

{group.name} @@ -129,9 +149,9 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP {/* Horizontal scrolling row */}
@@ -139,7 +159,7 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP onSelectScreen(screen.id)} /> ))} diff --git a/src/components/UserStoriesPage.tsx b/src/components/UserStoriesPage.tsx index 64a1430..107fdc2 100644 --- a/src/components/UserStoriesPage.tsx +++ b/src/components/UserStoriesPage.tsx @@ -12,6 +12,16 @@ import { import { getScreen, screens } from '../screens'; import { ThemeToggle } from './ThemeToggle'; +function useIsMobile(breakpoint = 768) { + const [isMobile, setIsMobile] = useState(window.innerWidth < breakpoint); + useEffect(() => { + const handleResize = () => setIsMobile(window.innerWidth < breakpoint); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [breakpoint]); + return isMobile; +} + interface UserStoriesPageProps { selectedStoryId?: string; onBack: () => void; @@ -24,7 +34,9 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use const [selectedCategories, setSelectedCategories] = useState>(new Set()); const [selectedPriorities, setSelectedPriorities] = useState>(new Set()); const [selectedScreens, setSelectedScreens] = useState>(new Set()); + const [filtersExpanded, setFiltersExpanded] = useState(false); const storyRefs = useRef>(new Map()); + const isMobile = useIsMobile(); // Scroll to selected story on mount useEffect(() => { @@ -105,34 +117,39 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use
{/* Header */}
-
- +
+
+ + {isMobile && } +

@@ -140,113 +157,241 @@ export function UserStoriesPage({ selectedStoryId, onBack, onSelectScreen }: Use

- {filteredStories.length} / {userStories.length} stories · Cliquez sur un écran pour voir le mockup + {filteredStories.length} / {userStories.length} stories

- + {!isMobile && }
{/* Filter bar */} -
- {/* Category filters */} -
- - Catégorie - - {categories.map(cat => ( - toggleCategory(cat)} - /> - ))} -
- - {/* Priority filters */} -
- - Priorité - - {[0, 1, 2, 3].map(p => ( - togglePriority(p)} - /> - ))} -
- - {/* Screen filters */} -
- - Écran - - {screensWithStories.map(screen => ( - toggleScreen(screen.id)} - /> - ))} -
- - {/* Clear filters */} - {hasFilters && ( + {isMobile ? ( + /* Mobile: Collapsible filter bar */ +
+ {/* Filter toggle button */} - )} -
+ + {/* Expandable filter panel */} + {filtersExpanded && ( +
+ {/* Category filters */} +
+ + Catégorie + +
+ {categories.map(cat => ( + toggleCategory(cat)} + /> + ))} +
+
+ + {/* Priority filters */} +
+ + Priorité + +
+ {[0, 1, 2, 3].map(p => ( + togglePriority(p)} + /> + ))} +
+
+ + {/* Clear filters */} + {hasFilters && ( + + )} +
+ )} +
+ ) : ( + /* Desktop: Full filter bar */ +
+ {/* Category filters */} +
+ + Catégorie + + {categories.map(cat => ( + toggleCategory(cat)} + /> + ))} +
+ + {/* Priority filters */} +
+ + Priorité + + {[0, 1, 2, 3].map(p => ( + togglePriority(p)} + /> + ))} +
+ + {/* Screen filters */} +
+ + Écran + + {screensWithStories.map(screen => ( + toggleScreen(screen.id)} + /> + ))} +
+ + {/* Clear filters */} + {hasFilters && ( + + )} +
+ )} {/* Stories by priority */} -
+
{storiesByPriority.length === 0 ? (

{ + const handleResize = () => setIsMobile(window.innerWidth < breakpoint); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [breakpoint]); + return isMobile; +} + interface FeatureFilterProps { selectedCategories: Set; onCategoriesChange: (categories: Set) => void; @@ -22,6 +33,9 @@ export function FeatureFilter({ searchQuery, onSearchChange, }: FeatureFilterProps) { + const [filtersExpanded, setFiltersExpanded] = useState(false); + const isMobile = useIsMobile(); + const toggleCategory = (cat: string) => { const newSet = new Set(selectedCategories); if (newSet.has(cat)) { @@ -49,14 +63,104 @@ export function FeatureFilter({ }; const hasFilters = selectedCategories.size > 0 || selectedPriorities.size > 0 || searchQuery; + const activeFilterCount = selectedCategories.size + selectedPriorities.size + (searchQuery ? 1 : 0); + // On mobile, show compact filter bar with expand button + if (isMobile) { + return ( +

+ {/* Compact header with search and filter toggle */} +
+
+ onSearchChange(e.target.value)} + className="bg-background text-sm h-9" + /> +
+ +
+ + {/* Expandable filter panel */} + {filtersExpanded && ( +
+ {/* Category filters */} +
+ Catégorie +
+ {categories.map(cat => ( + + ))} +
+
+ + {/* Priority filters */} +
+ Priorité +
+ {[0, 1, 2, 3].map(p => ( + + ))} +
+
+ + {/* Clear filters */} + {hasFilters && ( + + )} +
+ )} +
+ ); + } + + // Desktop layout return (
{/* Search */}
onSearchChange(e.target.value)} className="bg-background" @@ -64,8 +168,8 @@ export function FeatureFilter({
{/* Category filters */} -
- Categorie +
+ Catégorie
{categories.map(cat => (
-

Specifications BDD

-

- {filteredFeatures.length} / {parsedFeatures.length} fonctionnalites - {filteredFeatures.reduce((acc, f) => acc + f.scenarios.length, 0)} scenarios +

Specs BDD

+

+ {filteredFeatures.length} / {parsedFeatures.length} fonctionnalités

+
+ +
{/* Test Results Summary */} {testSummary.totalScenarios > 0 && ( -
-
- - {testSummary.passed} passes +
+
+ + {testSummary.passed}
{testSummary.failed > 0 && ( -
- - {testSummary.failed} echecs +
+ + {testSummary.failed}
)} {testSummary.skipped > 0 && ( -
- - {testSummary.skipped} ignores +
+ + {testSummary.skipped}
)} - {testSummary.lastRun && ( - - {testSummary.lastRun.toLocaleString('fr-FR', { dateStyle: 'short', timeStyle: 'short' })} - - )} - + - +
+ +
)} - {testSummary.totalScenarios === 0 && } + {testSummary.totalScenarios === 0 &&
}
@@ -128,7 +128,7 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS /> {/* Feature list */} -
+
{featuresByPriority.map(({ priority, features }) => (
@@ -146,7 +146,7 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS
-
+
{features.map(feature => (