Patterns responsive utilises dans Speedcube Master : approche mobile-first, breakpoints, layouts adaptatifs, scaling du texte et navigation conditionnelle.
Tous les styles sont ecrits pour mobile en priorite (base), puis enrichis avec des prefixes de breakpoint pour les ecrans plus grands. On ne doit jamais ecrire de style desktop en base puis le surcharger pour mobile.
// Correct : mobile-first (base = mobile, sm/md/lg = enrichissements)
<div className="px-4 sm:px-6 lg:px-8">
<h1 className="text-xl sm:text-2xl lg:text-3xl">
Titre
</h1>
</div>
// Incorrect : desktop-first (surcouche mobile)
// Ne PAS faire ceci :
<div className="px-8 max-sm:px-4"> {/* Anti-pattern */}
<h1 className="text-3xl max-lg:text-xl"> {/* Anti-pattern */}
Titre
</h1>
</div>Tailwind CSS utilise des breakpoints min-width. Le style sans prefixe s'applique a toutes les tailles, et chaque prefixe s'active a partir de la largeur indiquee.
(base)0pxTous les ecrans (mobile en premier)xs375pxPetits mobiles (iPhone SE, etc.)sm640pxGrands mobiles / petites tablettesmd768pxTablettes (bascule sidebar/bottom nav)lg1024pxDesktop (3 colonnes, layout elargi)xl1280pxGrand desktop2xl1536pxTres grand ecran / ultra-wideLe pattern le plus courant : les elements sont empiles verticalement sur mobile puis disposes en grille sur les ecrans plus larges.
// 1 colonne → 2 colonnes → 3 colonnes
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Card>Carte 1</Card>
<Card>Carte 2</Card>
<Card>Carte 3</Card>
</div>
// Flex column → row
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">Element 1</div>
<div className="flex-1">Element 2</div>
</div>
// Stats : 2 colonnes → 4 colonnes
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard label="PB" value="8.42" />
<StatCard label="Ao5" value="12.67" />
<StatCard label="Ao12" value="13.45" />
<StatCard label="Total" value="1,247" />
</div>
// Form layout : pleine largeur → 2 colonnes
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField name="firstName" />
<FormField name="lastName" />
<FormField name="email" className="md:col-span-2" />
</div>Extrait de src/components/timer-stats.tsx. La grille de stats du timer utilise un layout fixe a 4 colonnes avec des tailles de texte et d'espacement qui s'adaptent. Sur mobile, le texte est tres compact (10px pour les labels, 16px pour les valeurs). Sur desktop, les valeurs passent a 30px. Ce pattern evite de changer le nombre de colonnes tout en restant lisible.
// Pattern : grille fixe 4 colonnes avec scaling du contenu
<div className="grid grid-cols-4 gap-2 sm:gap-6">
{/* PB */}
<div className="space-y-0.5 sm:space-y-2 text-center">
{/* Label : 10px sur mobile, 14px sur desktop */}
<div className="text-[10px] sm:text-sm font-medium
text-muted-foreground uppercase tracking-wide">
PB
</div>
{/* Valeur : 16px sur mobile, 30px sur desktop */}
<div className="text-base sm:text-3xl font-bold text-primary font-mono">
8.42
</div>
</div>
{/* Ao5 */}
<div className="space-y-0.5 sm:space-y-2 text-center">
<div className="text-[10px] sm:text-sm font-medium
text-muted-foreground uppercase tracking-wide">
Ao5
</div>
<div className="text-base sm:text-3xl font-bold text-foreground font-mono">
12.67
</div>
</div>
{/* Ao12 */}
<div className="space-y-0.5 sm:space-y-2 text-center">
<div className="text-[10px] sm:text-sm font-medium
text-muted-foreground uppercase tracking-wide">
Ao12
</div>
<div className="text-base sm:text-3xl font-bold text-accent font-mono">
13.45
</div>
</div>
{/* Tendance : texte abrege sur mobile */}
<div className="space-y-0.5 sm:space-y-2 text-center">
<div className="text-[10px] sm:text-sm font-medium
text-muted-foreground uppercase tracking-wide">
<span className="hidden sm:inline">Tendance</span>
<span className="sm:hidden">Trend</span>
</div>
<div className="flex items-center justify-center gap-0.5 sm:gap-1.5">
<TrendingUp className="h-3 w-3 sm:h-5 sm:w-5 text-green-500" />
<span className="text-sm sm:text-xl font-bold text-green-500">
{/* Pourcentage sur desktop, fleche sur mobile */}
<span className="hidden sm:inline">-2.3%</span>
<span className="sm:hidden">▲</span>
</span>
</div>
</div>
</div>Extrait de src/components/timer-display.tsx. Le temps du timer utilise 5 breakpoints de taille de texte pour occuper l'espace de maniere proportionnelle, de 48px (xs mobile) a 128px (xl desktop). Le breakpoint custom xs (375px) est utilise pour les tres petits ecrans.
// Timer display : scaling progressif sur 5 breakpoints
<div className="text-5xl xs:text-6xl sm:text-7xl lg:text-8xl xl:text-9xl
font-mono font-bold">
{formatTime(time)}
</div>
// Correspondance des tailles :
// text-5xl = 48px (mobile < 375px)
// text-6xl = 60px (xs: 375px - petit iPhone)
// text-7xl = 72px (sm: 640px - grand mobile)
// text-8xl = 96px (lg: 1024px - desktop)
// text-9xl = 128px (xl: 1280px - grand desktop)
// Le breakpoint custom xs est defini dans tailwind.config :
// theme: {
// screens: {
// xs: "375px",
// // ... breakpoints Tailwind par defaut
// }
// }Certains elements sont affiches ou masques selon la taille de l'ecran. On utilise hidden pour masquer et le prefixe du breakpoint pour afficher.
// Visible uniquement sur mobile
<MobileBottomNav className="lg:hidden" />
// Visible uniquement sur desktop
<Sidebar className="hidden lg:flex" />
// Texte abrege sur mobile, complet sur desktop
<span className="sm:hidden">PB</span>
<span className="hidden sm:inline">Personal Best</span>
// Header simplifie sur mobile
<header className="flex items-center justify-between">
<Logo />
{/* Navigation complete sur desktop */}
<nav className="hidden lg:flex items-center gap-4">
<NavLink href="/timer">Timer</NavLink>
<NavLink href="/algos">Algorithmes</NavLink>
<NavLink href="/learning">Apprentissage</NavLink>
<NavLink href="/dashboard">Dashboard</NavLink>
</nav>
{/* Menu hamburger sur mobile */}
<Button variant="ghost" size="icon" className="lg:hidden">
<Menu className="h-5 w-5" />
</Button>
</header>
// Informations secondaires masquees sur mobile
<div className="flex items-center gap-4">
<span className="font-mono text-lg">{time}</span>
<span className="hidden sm:inline text-sm text-muted-foreground">
{scramble}
</span>
<span className="hidden lg:inline text-xs text-muted-foreground">
{date}
</span>
</div>Extrait de src/app/page.tsx. La page d'accueil utilise des grilles qui s'adaptent de 1 a 3 colonnes selon la taille de l'ecran. Les skeleton de chargement reproduisent cette meme grille pour eviter les sauts de layout.
// Page d'accueil : grille d'articles 1 → 3 colonnes
<section className="py-20 px-3 sm:px-4 lg:px-6">
<div className="mx-auto max-w-6xl">
{/* Grille responsive : empilee sur mobile, 3 colonnes sur desktop */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
{articles.map((article) => (
<Card key={article.id}>
<div className="h-48 w-full bg-muted rounded-t-lg overflow-hidden">
<img
src={article.cover_url}
alt={article.title}
className="w-full h-full object-cover"
/>
</div>
<CardContent className="p-4">
<h3 className="font-semibold mb-2 line-clamp-2">
{article.title}
</h3>
<p className="text-sm text-muted-foreground line-clamp-3">
{article.excerpt}
</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
// Section features : layout flex qui bascule colonne → ligne
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
{/* Texte a gauche (pleine largeur sur mobile) */}
<div className="space-y-6">
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-bold">
Timer precis au millieme
</h2>
<p className="text-muted-foreground text-sm sm:text-base">
Chronometre professionnel avec inspection WCA,
stackmat et smart cube bluetooth.
</p>
<div className="flex flex-col sm:flex-row gap-3">
<Button asChild>
<Link href="/timer">Essayer le timer</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/algos">Voir les algorithmes</Link>
</Button>
</div>
</div>
{/* Preview a droite (sous le texte sur mobile) */}
<div className="relative mt-8 lg:mt-0 h-[300px] sm:h-[350px] lg:h-[400px]">
<HeroPreview />
</div>
</div>Extrait de src/components/bottom-nav.tsx. Le menu "Plus" de la bottom nav affiche les liens en grille 2 colonnes avec une hauteur minimale de 48px pour chaque element (accessibilite tactile). Les items sont organises par sections avec des titres en majuscules.
// Grille 2 colonnes dans le menu "Plus" de la bottom nav
<div className="flex-1 overflow-y-auto overscroll-contain px-4 py-4 space-y-6">
{/* Section Competition */}
<div className="space-y-2">
<h3 className="text-xs uppercase tracking-wide
text-muted-foreground font-semibold px-2">
Competition
</h3>
<div className="grid grid-cols-2 gap-2">
{competitionItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-xl",
"transition-colors touch-feedback",
"min-h-[48px]", // Taille minimum tactile
isActive(item.href)
? "bg-primary/10 text-primary"
: "bg-muted/30 hover:bg-muted"
)}
>
<item.icon className="h-5 w-5 flex-shrink-0" />
<span className="text-sm font-medium truncate">
{item.label}
</span>
{/* Badge optionnel pour les notifications */}
{item.badge !== undefined && item.badge > 0 && (
<span className="ml-auto bg-red-500 text-white text-xs
rounded-full h-5 min-w-[20px] flex items-center
justify-center font-semibold px-1">
{item.badge > 99 ? "99+" : item.badge}
</span>
)}
</Link>
))}
</div>
</div>
{/* Section Utilisateur (aussi en grille 2 colonnes) */}
<div className="space-y-2">
{/* Carte profil pleine largeur */}
<div className="flex items-center gap-3 p-3 rounded-xl bg-muted/50">
<img
src={profile.avatar_url}
className="w-12 h-12 rounded-full object-cover border-2 border-border"
/>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{profile.username}</p>
<p className="text-sm text-muted-foreground truncate">{email}</p>
</div>
</div>
{/* Liens utilisateur en grille */}
<div className="grid grid-cols-2 gap-2">
{userItems.map((item) => (
<Link key={item.href} href={item.href} className="...">
<item.icon className="h-5 w-5" />
<span className="text-sm font-medium truncate">{item.label}</span>
</Link>
))}
</div>
</div>
</div>Les tailles de texte augmentent progressivement sur les ecrans plus grands pour occuper l'espace de maniere proportionnelle.
// Titres de page
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold">
Titre de la page
</h1>
// Sous-titres
<h2 className="text-lg sm:text-xl lg:text-2xl font-semibold">
Section
</h2>
// Timer (tres grand sur desktop)
<div className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-mono font-bold">
12.34
</div>
// Texte de corps
<p className="text-sm sm:text-base">
Description du contenu
</p>
// Texte muted (labels, hints)
<span className="text-xs sm:text-sm text-muted-foreground">
Information secondaire
</span>Le padding augmente avec la taille de l'ecran pour eviter un contenu trop colle aux bords sur mobile tout en utilisant l'espace sur desktop.
// Container principal
<main className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
{children}
</main>
// Cartes
<Card>
<CardContent className="p-4 sm:p-6">
{/* Contenu */}
</CardContent>
</Card>
// Sections de page
<section className="py-8 sm:py-12 lg:py-16">
{/* Contenu */}
</section>
// Modales
<DialogContent className="p-4 sm:p-6 max-w-[calc(100vw-2rem)] sm:max-w-lg">
{/* Contenu */}
</DialogContent>
// Hero section
<div className="px-4 py-12 sm:px-6 sm:py-16 lg:px-8 lg:py-24">
<h1 className="text-3xl sm:text-4xl lg:text-5xl">
Speedcube Master
</h1>
</div>Le container principal utilise max-w-6xl (1152px) avec un centrage automatique et un padding horizontal responsive. Ce pattern est utilise sur toutes les pages publiques.
// Container standard
<div className="max-w-6xl mx-auto px-4 sm:px-6">
{children}
</div>
// Container etroit (formulaires, articles)
<div className="max-w-2xl mx-auto px-4 sm:px-6">
{children}
</div>
// Container pleine largeur avec padding (landing pages)
<div className="w-full px-4 sm:px-6 lg:px-8">
{children}
</div>
// Container responsive composant reutilisable
function Container({
children,
size = "default",
}: {
children: React.ReactNode;
size?: "narrow" | "default" | "wide" | "full";
}) {
const maxWidths = {
narrow: "max-w-2xl",
default: "max-w-6xl",
wide: "max-w-7xl",
full: "w-full",
};
return (
<div className={cn(
maxWidths[size],
"mx-auto px-4 sm:px-6",
size === "full" && "lg:px-8"
)}>
{children}
</div>
);
}La navigation change de forme selon la taille de l'ecran. Sur mobile, c'est une barre en bas de l'ecran. Sur desktop, c'est un header ou une sidebar fixe.
// Pattern : bottom nav mobile ↔ header desktop
// Extrait reel de l'app Speedcube Master
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen">
{/* Header (toujours visible) */}
<header className="h-16 border-b border-border bg-background/95 backdrop-blur-md">
<div className="max-w-6xl mx-auto px-4 sm:px-6 h-full flex items-center">
<Logo />
{/* Nav desktop dans le header (hidden sur mobile) */}
<nav className="hidden lg:flex items-center gap-6 ml-8">
<NavLink href="/timer">Timer</NavLink>
<NavLink href="/algos">Algos</NavLink>
<NavLink href="/dashboard">Dashboard</NavLink>
</nav>
</div>
</header>
{/* Contenu principal */}
<main className="flex-1 pb-20 lg:pb-0">
{/* pb-20 pour la bottom nav sur mobile */}
{/* pb-0 sur desktop car pas de bottom nav */}
{children}
</main>
{/* Bottom nav mobile (hidden sur desktop) */}
<BottomNav /> {/* Contient lg:hidden en interne */}
</div>
);
}Grillegrid-cols-1 sm:grid-cols-2 lg:grid-cols-3Containermax-w-6xl mx-auto px-4 sm:px-6Textetext-sm sm:text-base lg:text-lgPaddingp-4 sm:p-6 lg:p-8Masquerhidden sm:block ou lg:hiddenDirectionflex-col sm:flex-rowNavBottom nav mobile (lg:hidden) + header desktop (hidden lg:flex)Timertext-5xl xs:text-6xl sm:text-7xl lg:text-8xl xl:text-9xl