Design system
Le design system du web tient en deux endroits : les variables CSS et les utilitaires de frontend-web/src/app/globals.css, et une douzaine de composants dans frontend-web/src/components/ui. Il reprend la charte de l'application mobile, issue de la maquette Figma : fond gris très clair, cartes blanches très arrondies, jaune « soleil » #FFDA38 pour les mises en avant, encre bleu nuit #1D2637 pour le texte et les boutons sombres, teintes pastel (lavande, menthe, pêche) pour distinguer les familles d'information, police Inter et icônes SVG (voir Thème et composants du mobile). Les valeurs sont recopiées depuis mobile-app/src/theme/colors.ts : les deux applications ne partagent aucun fichier, la parité se maintient à la main. Aucune maquette du web n'est versionnée dans le dépôt : globals.css fait foi.
Tailwind CSS 4 est chargé par @import 'tailwindcss' et le plugin PostCSS @tailwindcss/postcss (postcss.config.mjs). Il n'y a pas de fichier tailwind.config : la configuration passe par le bloc @theme inline du CSS.
Tokens de couleur
Les couleurs sont définies deux fois : sur :root pour le thème clair et sur [data-theme='dark'] pour le thème sombre. Le bloc @theme inline les expose ensuite comme couleurs Tailwind, ce qui permet d'écrire bg-surface, bg-sun ou text-fg-muted.
Base
| Variable CSS | Classe Tailwind | Clair | Sombre | Usage |
|---|---|---|---|---|
--bg |
bg-background |
#f6f7fa |
#0f1626 |
Fond de page |
--surface |
bg-surface |
#ffffff |
#1a2333 |
Cartes, barre latérale, feuilles |
--surface-elevated |
bg-surface-elevated |
#f0f2f6 |
#222c3e |
Zones en retrait, survol, squelettes, bouton désactivé |
--surface-muted |
bg-surface-muted |
#f5f6f9 |
#222c3e |
Fonds secondaires |
--primary |
bg-primary, text-primary |
#1d2637 |
#f3f5f9 |
Encre : bouton secondary, liens forts. S'inverse en sombre |
--primary-light |
bg-primary-light |
#2c3a52 |
#d5dae3 |
Survol du bouton secondary |
--on-primary |
text-on-primary |
#ffffff |
#1d2637 |
Texte posé sur l'encre |
--secondary |
text-secondary |
#a95837 |
#f0a07f |
Terracotta, gardé pour compatibilité |
--text |
text-fg |
#1d2637 |
#f3f5f9 |
Texte principal |
--text-secondary |
text-fg-secondary |
#657084 |
#a3acbd |
Texte courant atténué |
--text-muted |
text-fg-muted |
#8a93a5 |
#7d879a |
Mentions, horodatages |
--border |
border-line |
#e2e5eb |
#2c3649 |
Bordures des champs et séparateurs |
--success |
bg-success |
#356c58 |
#7fc4a6 |
Pastilles d'état |
--warning |
text-warning |
#a95837 |
#f0a07f |
Mode démonstration, pause |
--error |
text-error |
#b4472b |
#f0a07f |
Erreurs, actions destructrices |
--overlay |
bg-[var(--overlay)] |
rgba(15, 23, 38, 0.5) |
noir à 60 % | Fond des feuilles modales |
Famille soleil
| Variable CSS | Classe Tailwind | Clair | Sombre | Usage |
|---|---|---|---|---|
--sun |
bg-sun |
#ffda38 |
#ffda38 |
Bouton principal, carte du tirage, pastille des points, onglet actif des sélecteurs |
--sun-light |
bg-sun-light |
#ffe979 |
#ffe979 |
Survol du bouton principal |
--sun-soft |
bg-sun-soft |
#fff4b8 |
#fff4b8 |
Encarts posés sur la carte jaune |
--sun-chip |
bg-sun-chip |
#fff7d3 |
#3a3420 |
Surtitres en pilule, badges d'avertissement, paliers débloqués |
--sun-nav |
bg-sun-nav |
#fff3b5 |
#3a3420 |
Entrée de navigation active |
--on-sun, --on-sun-muted |
text-on-sun, text-on-sun-muted |
#1d2637, #4a5263 |
identiques | Texte posé sur le jaune |
--on-sun-nav |
text-on-sun-nav |
#1d2637 |
#ffe979 |
Texte de l'entrée de navigation active |
En thème sombre, --sun-chip devient un brun foncé : le texte posé dessus passe en jaune grâce à la classe dark:text-sun ajoutée à chaque pilule. Le mobile garde au contraire sunChip clair dans les deux thèmes.
Teintes d'accompagnement
| Variable CSS | Classe Tailwind | Clair | Sombre | Usage |
|---|---|---|---|---|
--lavender, --violet |
bg-lavender, text-violet |
#eee9ff, #7357b1 |
#2a2546, #b9a6f0 |
Informations, EtherPoints, bouton soft, contour de focus |
--mint, --green |
bg-mint, text-green |
#e6efea, #356c58 |
#1e3329, #7fc4a6 |
Succès, âge vérifié, portefeuille |
--peach, --terracotta |
bg-peach, text-terracotta |
#ffe5d8, #a95837 |
#3d2a22, #f0a07f |
Erreurs, accès refusé |
--coral |
bg-coral |
#e87f69 |
#e87f69 |
Pastille des notifications non lues, accent ponctuel |
Ombres
| Variable CSS | Clair | Sombre | Usage |
|---|---|---|---|
--card-shadow |
0 8px 24px rgba(29, 38, 55, 0.06) |
aucune | Ombre douce des cartes et des pilules flottantes |
--card-border |
transparent | #2c3649 |
Bordure des cartes : invisible en clair, elle remplace l'ombre en sombre |
Les cartes jaunes (compte à rebours, roue) portent en plus une ombre dorée 0 14px 32px rgba(172, 141, 10, 0.22).
Chaque thème déclare aussi color-scheme, ce qui adapte les contrôles natifs (barres de défilement, champs) au mode courant.
Le layout racine déclare les mêmes fonds dans viewport.themeColor (#F6F7FA en clair, #0F1626 en sombre) pour colorer la barre du navigateur mobile.
Rayons, typographie et polices
| Token | Valeur | Classe |
|---|---|---|
--radius-sm |
10 px | rounded-sm |
--radius-md |
14 px | rounded-md |
--radius-lg |
20 px | rounded-lg |
--radius-xl |
24 px | rounded-xl |
--radius-2xl |
28 px | rounded-2xl |
Les composants écrivent aussi souvent leur rayon en valeur fixe (rounded-[16px], rounded-[24px], rounded-[28px]) pour coller à la maquette mobile : 24 px pour les cartes, 16 px pour les boutons et les encarts, 28 px pour les feuilles, la barre latérale et la carte du tirage, rounded-full pour les pilules.
| Token | Taille et interligne | Classe | Usage typique |
|---|---|---|---|
--text-caption |
12 / 16 px | text-caption |
Libellés, mentions, badges |
--text-small |
13 / 18 px | text-small |
Navigation latérale |
--text-body |
15 / 22 px | text-body |
Texte courant (taille du body) |
--text-subtitle |
17 / 24 px | text-subtitle |
Titres de carte |
--text-heading |
20 / 28 px | text-heading |
Chiffres mis en avant |
--text-title |
24 / 32 px | text-title |
Titre de page sur mobile |
--text-hero |
30 / 38 px | text-hero |
Titre de page à partir de md |
--text-display |
44 / 52 px | text-display |
Accroche de la page publique, cagnotte |
La police est Inter, servie par le site lui-même. layout.tsx la charge avec next/font/local depuis frontend-web/src/app/fonts en cinq fichiers : 400, 500, 600, 700 et 700 italique (ce dernier sert au « Lucky » du logo). Next.js expose la famille dans la variable --font-inter, posée sur <html>, et --font-sans la place en tête de la pile de polices système (ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto...), qui sert de repli pendant le chargement (display: 'swap'). Les fichiers sont recopiés depuis le paquet @expo-google-fonts/inter du mobile, avec la licence SIL Open Font License dans LICENSE-inter.txt. Aucune requête ne part vers un service de polices tiers. ui-monospace reste utilisé pour les adresses et les hash.
Le body applique un espacement des lettres de -0,1 px et le lissage antialiased. Les titres de page sont en gras avec un espacement négatif (-0,4 px).
Icônes et logo
Icon.tsx affiche une icône SVG en ligne à partir de iconData.ts, copie du jeu d'icônes du mobile (47 icônes : onglets, cloche, étoile, ticket, roue, bouclier, logo, haut-parleur volume et volumeOff...). Il reçoit name (type IconName), size (20 px par défaut, la hauteur suit les proportions de l'icône) et className. Les tracés utilisent currentColor : l'icône prend la couleur du texte du parent. Elle est toujours aria-hidden, le libellé doit donc être porté par le bouton ou le lien qui la contient. Aucune bibliothèque d'icônes n'est installée.
Le logo est le « B » encre sur carré jaune #FFDA38 du mobile (icône logoSymbol, identique à mobile-app/assets/icons/logo-symbol.svg). Il sert aussi de favicon par frontend-web/src/app/icon.svg, que Next.js déclare automatiquement. Brand l'accompagne du mot « BlockLucky » avec « Lucky » en italique, comme BrandLogo sur mobile.
Utilitaires et animations
globals.css définit trois utilitaires Tailwind (@utility) :
| Utilitaire | Effet |
|---|---|
card |
Fond --surface, bordure --card-border, rayon 24 px, ombre --card-shadow |
eyebrow-caps |
Majuscules et espacement de 0,6 px pour les surtitres |
tabular |
Chiffres à chasse fixe pour les montants et les compteurs |
Et cinq animations préfixées bl- : animate-bl-spin (indicateur de chargement, rotation de la roue du tirage), animate-bl-shimmer (squelettes, reçu en attente, pastille « Tirage... »), animate-bl-slide-up (ouverture des feuilles), animate-bl-fade-in (fond des feuilles), animate-bl-drop-in (bandeau de notification). Toutes sont réduites à 0,01 ms quand prefers-reduced-motion: reduce est actif (voir Accessibilité et responsive).
La couche base fixe le fond, la couleur et la police du body, la sélection de texte (fond --sun, texte --on-sun), le contour :focus-visible (2 px, couleur --violet, décalé de 2 px) et le curseur des boutons. Les champs de saisie remplacent ce contour par une bordure couleur --text et un halo jaune (ring-4 ring-sun/30). La classe no-print masque un élément à l'impression.
Thèmes clair et sombre
Le thème est un attribut data-theme sur <html>, posé par deux mécanismes :
themeInitScript, injecté dans<head>parlayout.tsx, s'exécute avant le rendu. Il litbl.pref.themedanslocalStorage; à défaut, il suitprefers-color-scheme. Il pose aussilangselonbl.pref.language.frontend-web/src/theme/ThemeProvider.tsxobserve ces attributs avec unMutationObserverviauseSyncExternalStore, et exposemode,toggleThemeetsetThemeMode.
flowchart LR LS["localStorage bl.pref.theme"] --> INIT["themeInitScript"] MQ["prefers-color-scheme"] --> INIT INIT --> HTML["html data-theme"] BTN["PreferenceToggles ou profil"] -->|"setThemeMode"| LS BTN --> HTML HTML -->|"MutationObserver"| TP["ThemeProvider : mode"] HTML --> CSS["variables de globals.css"]
La variante Tailwind dark: est redéfinie par @custom-variant dark pour suivre [data-theme='dark'] et non la préférence système. Le rendu serveur suppose toujours LIGHT|FR ; suppressHydrationWarning sur <html> évite l'avertissement de différence d'attributs.
Le choix est mémorisé par navigateur. Depuis /profil, il est aussi envoyé au backend par PUT /user/preferences, mais cette préférence n'est jamais relue à la connexion. Le thème SYSTEM accepté par l'API n'est pas proposé dans l'interface.
Composants de src/components/ui
| Composant | Fichier | Rôle et props principales |
|---|---|---|
Button |
Button.tsx |
Variantes primary (jaune soleil, texte encre, ombre dorée), secondary (encre, texte blanc), outline (surface bordée), danger (surface bordée, texte rouge, survol pêche), ghost (transparent), soft (lavande, texte violet) ; tailles md (52 px de haut, rayon 16 px) et sm (40 px, rayon 12 px) ; loading remplace le texte par un Spinner, désactive le bouton et pose aria-busy ; pleine largeur par défaut |
LinkButton |
même fichier | Même apparence sur un Link Next.js, ou un <a target="_blank" rel="noopener noreferrer"> si external |
buttonClasses |
même fichier | Fonction qui renvoie les classes, pour styler un autre élément comme un bouton |
Card, CardTitle |
Card.tsx |
Conteneur de rayon 24 px avec tone (surface avec ombre douce, elevated, sun, sunSoft, lavender, mint, peach, ink sur fond #1d2637), padding (sm, md, lg) et balise configurable (as) |
Icon |
Icon.tsx |
Icône SVG en ligne, voir Icônes et logo |
Input, Textarea, Select |
Input.tsx |
Champ de rayon 14 px avec libellé associé, hint, errorText relié par aria-describedby, aria-invalid en cas d'erreur ; au focus, bordure encre et halo jaune |
Loader, Skeleton |
Loader.tsx |
Indicateur avec message (aria-live="polite") et squelettes de 52 px de haut, rayon 16 px |
Spinner |
Spinner.tsx |
Cercle animé, role="status", libellé « Chargement » |
PageHeader |
Misc.tsx |
Titre h1 de page, sous-titre, surtitre en pilule sun-chip et zone d'actions |
EmptyState |
même fichier | Carte d'état vide avec pastille jaune, titre, description et action |
Alert |
même fichier | Encart arrondi (16 px) teinté selon le ton : success menthe, info lavande, warning jaune pâle, error pêche ; role="alert" pour une erreur, role="status" sinon |
Badge, StatusDot |
même fichier | Pilule teintée (neutral, primary menthe, secondary lavande, danger pêche, warning jaune) et pastille d'état décorative |
CheckboxCard |
même fichier | Case à cocher native dans une carte cliquable de rayon 16 px, bordure jaune et fond sun-chip une fois cochée |
ProgressBar |
ProgressBar.tsx |
Jauge role="progressbar" remplie en jaune, compteur en pilule ; paliers (milestones) affichés en tuiles, sur deux colonnes à partir de sm, jaunes une fois débloqués |
SegmentedTabs |
SegmentedTabs.tsx |
Onglets role="tablist" / role="tab" dans une pilule blanche, onglet actif en jaune |
ChoicePills |
même fichier | Choix unique role="radiogroup" / role="radio" (quantités, tranches d'âge), pilules dont l'option choisie passe en encre |
Sheet |
Sheet.tsx |
Feuille modale de rayon 28 px : basse sur mobile avec une poignée, centrée à partir de md, bouton de fermeture rond avec l'icône close, surtitre violet (rouge en ton danger), piège de focus, Échap, retour du focus |
BackButton |
BackButton.tsx |
Pilule blanche avec une flèche dans un rond ; lien vers href, ou router.back() sans href |
Un bouton désactivé (disabled) prend toujours le fond surface-elevated et le texte fg-muted, quelle que soit sa variante. Un bouton en loading garde sa couleur.
Exemple d'usage courant :
<Button onClick={handleBuyForSelf} loading={isBuying} disabled={!understood || !isOpen || isPaused}>
{t('Confirmer', 'Confirm')}
</Button>Composants de mise en page et métier
Ils s'appuient sur les composants de base :
| Dossier | Composants |
|---|---|
components/layout |
AppShell (barre latérale, en-tête, onglets bas, liens légaux), PublicShell, LegalPage (avec LegalDocumentPage, legalSections et LegalLinks), ConsentBanner et ConsentControls, Brand, SecurityFooter, PreferenceToggles, RequireRole, AccessDenied, GlobalOverlays |
components/lottery |
WalletCard, WinnerClaimCard, DemoModeBadge, NetworkPill, CountdownTimer, RouletteAnimation, SoundToggle, ChatBox, TicketCard, LeaderboardList, TransparencyCharter |
components/marketplace |
PointsBalanceCard, RewardCard, ScratchCanvas (carte à gratter sur <canvas>) |
components/charts |
BarList, RingMeter, KpiTile, sans dépendance externe |
Coquilles
AppShell (AppShell.tsx) reprend la structure des écrans mobiles :
| Zone | Rendu |
|---|---|
Barre latérale (à partir de lg) |
Carte flottante de 272 px de large, rayon 28 px, décollée des bords de 16 px ; logo, nom de l'espace en pilule sun-chip, entrées avec leur icône, entrée active sur fond sun-nav ; en bas, thème et langue, adresse du portefeuille avec un bouton rond de déconnexion, SecurityFooter, puis la ligne légale ShellLegalLinks (titres des six documents et bouton « Gérer mes traceurs », en 11 px) |
Contenu (sous lg) |
La même ligne légale est affichée sous le contenu de chaque page |
| En-tête | Bandeau translucide flouté ; pour l'espace citoyen, pilule du portefeuille (icône ticket sur fond menthe, adresse raccourcie, masquée sous sm), pilule jaune des points avec une étoile, bouton rond de la cloche avec une pastille corail quand des notifications sont non lues |
Barre basse (sous lg) |
Barre blanche aux coins supérieurs arrondis (24 px), icône au-dessus du libellé, onglet actif sur fond sun-nav, menu « Plus » (icône settings) au-delà de cinq entrées, SecurityFooter en dessous |
Chaque entrée de navigation (NavItem) accepte un champ facultatif icon, de type IconName. Les trois layouts d'espace le renseignent : par exemple tabHome pour Accueil et Pilotage, ticket, trophy et bag pour Tickets, Tirage et Boutique, idCard, shield et document pour Identités, Sécurité et Audit. Une entrée sans icône garde la place vide dans la barre basse.
SecurityFooter.tsx affiche la mention « Application sécurisée · Aucun argent réel » avec un bouclier, comme le composant du même nom sur mobile. On la retrouve dans AppShell, PublicShell, AccessDenied et les pages de connexion.
PublicShell garde son en-tête (logo, lien vers les règles, préférences, bouton « Participer » ou « Mon espace ») et met son pied de page dans une carte, avec des liens en pilules : les six documents légaux dans l'ordre de LEGAL_DOC_ORDER (CGU, règlement, mentions légales, confidentialité, traceurs, transparence) et le bouton « Gérer mes traceurs ». LegalPage ouvre sur un surtitre en pilule jaune ; LegalDocumentPage y affiche un document de content/legal.ts (surtitre, titre, version du 12 novembre 2026, introduction, sections et éléments en liste de définitions), éventuellement suivi de sections propres à la page, puis LegalLinks, la liste des autres documents avec le document courant mis en évidence (aria-current="page").
ConsentBanner est une carte fixe en bas de l'écran (au-dessus des autres contenus, z-[70], 36 rem de large au plus) ; ses deux boutons de choix ont la même variante outline. SoundToggle est un bouton rond de 36 px, bordé, avec l'icône volume ou volumeOff et le libellé « Couper les sons » ou « Activer les sons ». AccessDenied présente une carte avec une icône sur fond pêche. GlobalOverlays utilise les mêmes feuilles et pilules que le reste de l'interface.
Graphiques
Charts.tsx exporte CHART_COLORS, une palette de cinq tons (sun, violet, green, terracotta, coral). BarList et RingMeter acceptent un tone facultatif (sun par défaut). KpiTile accepte en plus de label, value, hint et tone (default, primary sur fond jaune, secondary en violet) trois props facultatives : icon (pastille ronde en tête de tuile), accent (sun, lavender, mint ou peach, couleur de cette pastille) et className.
Les composants de loterie sont décrits dans Portefeuille et loterie.
Modifier la charte
Pour changer une couleur, modifier la variable dans les deux blocs (:root et [data-theme='dark']) de globals.css. Les jetons du web étant recopiés depuis mobile-app/src/theme/colors.ts, reporter la même valeur côté mobile pour garder les deux supports identiques. Quelques composants écrivent encore l'encre en dur (#1d2637 dans Card ton ink, ChoicePills, CheckboxCard, la roue et le texte de la carte à gratter) : les modifier aussi si l'encre change. Vérifier ensuite le contraste dans les deux thèmes : quelques combinaisons restent sous le seuil AA pour le petit texte (détail dans Accessibilité et responsive).