diff --git a/src/App.tsx b/src/App.tsx index 1ffa0da..6fa4608 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,6 @@ import { RouterProvider, useRouter } from './router'; import { ThemeProvider } from './context/ThemeContext'; import { Gallery } from './components/Gallery'; import { DemoMode } from './components/DemoMode'; -import { UserStoriesPage } from './components/UserStoriesPage'; import { SpecsPage } from './components/specs'; function AppContent() { @@ -14,17 +13,7 @@ function AppContent() { navigate({ page: 'stories', storyId })} - /> - ); - } - - if (route.page === 'stories') { - return ( - navigate({ page: 'demo', screenId })} + onNavigateToStory={(storyId) => navigate({ page: 'specs', storyId })} /> ); } @@ -33,9 +22,10 @@ function AppContent() { return ( navigate({ page: 'demo', screenId })} - onSelectStory={(storyId) => navigate({ page: 'stories', storyId })} + onSelectStory={(storyId) => navigate({ page: 'specs', storyId })} /> ); } @@ -43,7 +33,6 @@ function AppContent() { return ( navigate({ page: 'demo', screenId })} - onShowStories={() => navigate({ page: 'stories' })} onShowSpecs={() => navigate({ page: 'specs' })} /> ); diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index 0bc341d..53f5e36 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -15,15 +15,14 @@ function useIsMobile(breakpoint = 768) { interface GalleryProps { onSelectScreen: (screenId: string) => void; - onShowStories: () => void; - onShowSpecs?: () => void; + onShowSpecs: () => void; } const MIN_SCALE = 0.32; const MAX_SCALE = 0.75; const DEFAULT_SCALE = 0.5; -export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryProps) { +export function Gallery({ onSelectScreen, onShowSpecs }: GalleryProps) { const [scale, setScale] = useState(DEFAULT_SCALE); const isMobile = useIsMobile(); @@ -67,42 +66,23 @@ export function Gallery({ onSelectScreen, onShowStories, onShowSpecs }: GalleryP gap: isMobile ? 8 : 24, flexWrap: 'wrap', }}> - {/* User Stories button */} + {/* Specs BDD button */} - {/* Specs BDD button */} - {onShowSpecs && ( - - )} - {/* Zoom control - hide on mobile */} {!isMobile && (
; onCategoriesChange: (categories: Set) => void; selectedPriorities: Set; onPrioritiesChange: (priorities: Set) => void; + selectedScreens: Set; + onScreensChange: (screens: Set) => void; + screensWithStories: ScreenInfo[]; searchQuery: string; onSearchChange: (query: string) => void; } @@ -30,6 +38,9 @@ export function FeatureFilter({ onCategoriesChange, selectedPriorities, onPrioritiesChange, + selectedScreens, + onScreensChange, + screensWithStories, searchQuery, onSearchChange, }: FeatureFilterProps) { @@ -56,14 +67,25 @@ export function FeatureFilter({ onPrioritiesChange(newSet); }; + const toggleScreen = (screenId: string) => { + const newSet = new Set(selectedScreens); + if (newSet.has(screenId)) { + newSet.delete(screenId); + } else { + newSet.add(screenId); + } + onScreensChange(newSet); + }; + const clearFilters = () => { onCategoriesChange(new Set()); onPrioritiesChange(new Set()); + onScreensChange(new Set()); onSearchChange(''); }; - const hasFilters = selectedCategories.size > 0 || selectedPriorities.size > 0 || searchQuery; - const activeFilterCount = selectedCategories.size + selectedPriorities.size + (searchQuery ? 1 : 0); + const hasFilters = selectedCategories.size > 0 || selectedPriorities.size > 0 || selectedScreens.size > 0 || searchQuery; + const activeFilterCount = selectedCategories.size + selectedPriorities.size + selectedScreens.size + (searchQuery ? 1 : 0); // On mobile, show compact filter bar with expand button if (isMobile) { @@ -141,6 +163,26 @@ export function FeatureFilter({
+ {/* Screen filters */} + {screensWithStories.length > 0 && ( +
+ Écran +
+ {screensWithStories.map(({ id, screen }) => ( + + ))} +
+
+ )} + {/* Clear filters */} {hasFilters && ( + ))} + + + )} + {/* Clear filters */} {hasFilters && (
diff --git a/src/components/specs/SpecsPage.tsx b/src/components/specs/SpecsPage.tsx index d896373..2ac99f3 100644 --- a/src/components/specs/SpecsPage.tsx +++ b/src/components/specs/SpecsPage.tsx @@ -1,7 +1,8 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useRef, useEffect } from 'react'; import { parsedFeatures, getFeatureById } from '../../data/features'; -import { categoryLabels, categoryColors, priorityLabels, priorityColors, getStoryById, type StoryCategory } from '../../data'; +import { categoryLabels, categoryColors, priorityLabels, priorityColors, getStoryById, getScreenIdsWithStories, type StoryCategory } from '../../data'; import { getTestStatus, getTestSummary } from '../../data/testResults'; +import { getScreen } from '../../screens'; import { FeatureView } from './FeatureView'; import { FeatureFilter } from './FeatureFilter'; import { Card, CardHeader, CardTitle, CardContent } from '../ui/card'; @@ -12,15 +13,36 @@ import { ThemeToggle } from '../ThemeToggle'; interface SpecsPageProps { selectedFeatureId?: string; + selectedStoryId?: string; onBack: () => void; onSelectScreen: (screenId: string) => void; onSelectStory: (storyId: string) => void; } -export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectStory }: SpecsPageProps) { +export function SpecsPage({ selectedFeatureId, selectedStoryId, onBack, onSelectScreen, onSelectStory }: SpecsPageProps) { const [selectedCategories, setSelectedCategories] = useState>(new Set()); const [selectedPriorities, setSelectedPriorities] = useState>(new Set()); + const [selectedScreens, setSelectedScreens] = useState>(new Set()); const [searchQuery, setSearchQuery] = useState(''); + const featureRefs = useRef>(new Map()); + + // Get screens that have linked stories for the filter + const screensWithStories = useMemo(() => { + const screenIds = getScreenIdsWithStories(); + return screenIds + .map(id => ({ id, screen: getScreen(id) })) + .filter(({ screen }) => screen !== undefined); + }, []); + + // Scroll to selected story on mount + useEffect(() => { + if (selectedStoryId) { + const element = featureRefs.current.get(selectedStoryId); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + }, [selectedStoryId]); // Filter features - must be before any conditional returns to respect hooks rules const filteredFeatures = useMemo(() => { @@ -31,6 +53,12 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS if (selectedPriorities.size > 0 && !selectedPriorities.has(feature.priority)) { return false; } + if (selectedScreens.size > 0) { + const linkedStory = getStoryById(feature.id); + if (!linkedStory || !linkedStory.screenIds.some(id => selectedScreens.has(id))) { + return false; + } + } if (searchQuery) { const query = searchQuery.toLowerCase(); return feature.name.toLowerCase().includes(query) || @@ -38,7 +66,7 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS } return true; }); - }, [selectedCategories, selectedPriorities, searchQuery]); + }, [selectedCategories, selectedPriorities, selectedScreens, searchQuery]); // Group by priority const featuresByPriority = [0, 1, 2, 3].map(priority => ({ @@ -123,6 +151,9 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS onCategoriesChange={setSelectedCategories} selectedPriorities={selectedPriorities} onPrioritiesChange={setSelectedPriorities} + selectedScreens={selectedScreens} + onScreensChange={setSelectedScreens} + screensWithStories={screensWithStories} searchQuery={searchQuery} onSearchChange={setSearchQuery} /> @@ -150,8 +181,13 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS {features.map(feature => ( { + if (el) featureRefs.current.set(feature.id, el); + }} feature={feature} + isSelected={feature.id === selectedStoryId} onClick={() => window.location.hash = `#/specs/${feature.id}`} + onSelectScreen={onSelectScreen} /> ))}
@@ -179,11 +215,17 @@ function formatUserStory(description: string): string[] { interface FeatureCardProps { feature: ParsedFeature; + isSelected?: boolean; onClick: () => void; + onSelectScreen: (screenId: string) => void; } -function FeatureCard({ feature, onClick }: FeatureCardProps) { +const FeatureCard = React.forwardRef( + function FeatureCard({ feature, isSelected, onClick, onSelectScreen }, ref) { const linkedStory = getStoryById(feature.id); + const linkedScreens = linkedStory?.screenIds + .map(id => ({ id, screen: getScreen(id) })) + .filter(({ screen }) => screen !== undefined) || []; const testStatus = getTestStatus(feature.id); const getStatusIcon = () => { @@ -210,7 +252,10 @@ function FeatureCard({ feature, onClick }: FeatureCardProps) { return ( @@ -247,19 +292,42 @@ function FeatureCard({ feature, onClick }: FeatureCardProps) { ))} )} -
+
{feature.scenarios.length} scenarios - {linkedStory && linkedStory.screenIds.length > 0 && ( + {linkedScreens.length > 0 && ( - {linkedStory.screenIds.length} ecrans + {linkedScreens.length} ecrans )}
+ {/* Screen buttons */} + {linkedScreens.length > 0 ? ( +
+ {linkedScreens.map(({ id, screen }) => ( + + ))} +
+ ) : ( +

+ Pas encore de mockup +

+ )} ); -} +}); diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 730e50f..5166f24 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { diff --git a/src/router.tsx b/src/router.tsx index a6ae87c..32e7206 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -2,9 +2,8 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr type Route = | { page: 'gallery' } - | { page: 'stories'; storyId?: string } | { page: 'demo'; screenId: string } - | { page: 'specs'; featureId?: string }; + | { page: 'specs'; featureId?: string; storyId?: string }; interface RouterContextValue { route: Route; @@ -21,14 +20,16 @@ function parseHash(hash: string): Route { return { page: 'gallery' }; } + // Redirect /stories to /specs (backward compatibility) if (path === 'stories') { - return { page: 'stories' }; + return { page: 'specs' }; } + // Redirect /stories/{id} to /specs with storyId (backward compatibility) if (path.startsWith('stories/')) { const storyId = path.replace('stories/', ''); if (storyId) { - return { page: 'stories', storyId }; + return { page: 'specs', storyId }; } } @@ -57,12 +58,12 @@ function routeToHash(route: Route): string { switch (route.page) { case 'gallery': return '#/'; - case 'stories': - return route.storyId ? `#/stories/${route.storyId}` : '#/stories'; case 'demo': return `#/demo/${route.screenId}`; case 'specs': - return route.featureId ? `#/specs/${route.featureId}` : '#/specs'; + if (route.featureId) return `#/specs/${route.featureId}`; + if (route.storyId) return `#/specs/${route.storyId}`; + return '#/specs'; } } @@ -112,10 +113,10 @@ export function useGoBack() { } /** - * Generate a URL for a specific story + * Generate a URL for a specific story (now redirects to specs) */ export function getStoryUrl(storyId: string): string { - return `#/stories/${storyId}`; + return `#/specs/${storyId}`; } /**