Aller au contenu

Showroom

@mister-guiiug/dev-pwa-config
Habiller
Générique — preset seul
Préférences
Schéma de couleurs
Langue
Densité
Inspecteur

Le design system de la famille miss-* / mister-*

Ce paquet partage une structure, pas une identité : échelles fluides, points de rupture, safe-areas iOS, cible tactile, et des composants React livrés non stylés. La couleur, la typographie de titrage et les arrondis appartiennent à chaque application.

Générique — preset seul
Ce que le preset fournit réellement : de la structure, pas de couleur. Palette monochrome de référence. Voir les couleurs →

Comparer deux apps

Scènes

Enregistrez l’état courant (habillage, schéma, section, paire) sous un nom, puis rouvrez-le ou partagez ?scene=….

    Les applications de la famille

    Seize dépôts publics, dont quinze consomment ce paquet — l’application desktop n’en importe rien, et c’est le champ « sous-chemins » qui l’a révélé. Cette grille n’est pas une liste tenue à la main dans la page : elle est engendrée depuis apps-catalog.js — le fichier qu’importent les applications elles-mêmes pour s’afficher les unes les autres. Ce qui serait faux ici serait faux dans leurs pieds de page.

    Recherche unifiée en haut de page (CtrlK)

    Affichage

    Le domaine et la maturité sont éditoriaux, saisis à la main : ils disent une intention, pas une version publiée. La persistance et les sous-chemins sont relevés dans le code de chaque app — les import et les extends réels, pas une intention —, et laissés vides quand ils ne l’ont pas été : un filtre qui affiche « non relevé » vaut mieux qu’une donnée inventée. Le filtre Consomme répond ainsi à la question qu’un design system doit se poser en premier : qui utilise vraiment quoi ? components.css n’a qu’un adoptant sur seize. Les pastilles sont peintes avec la primaire réelle de chaque application, jamais avec une icône distante : le showroom ne fait aucune requête réseau.

    Catalogue

    Tout ce que le paquet exporte, en une grille. Un test compare cette liste aux exports réels de react/index.js : ce qui n’est pas ici est soit couvert par une autre fiche, soit nommément exclu — c’est ainsi que les neuf hooks, absents de leur propre vitrine, ont été repérés.

    Filtrer par catégorie

    Fondations

    Tous les tokens ci-dessous viennent de tailwind-preset.css et sont donc identiques dans toutes les applications de la famille. Les valeurs affichées sont calculées en direct : redimensionnez la fenêtre pour voir jouer les clamp().

    Typographie fluide

    Token Valeur Calculé Rendu
    --text-fluid-xs clamp(0.7rem, 1.6vw, 0.8125rem) — Aa — Portez ce vieux whisky
    --text-fluid-sm clamp(0.8125rem, 1.9vw, 0.95rem) — Aa — Portez ce vieux whisky
    --text-fluid-base clamp(0.9rem, 2.2vw, 1.05rem) — Aa — Portez ce vieux whisky
    --text-fluid-lg clamp(1rem, 2.6vw, 1.25rem) — Aa — Portez ce vieux whisky
    --text-fluid-xl clamp(1.15rem, 3vw, 1.5rem) — Aa — Portez ce vieux
    --text-fluid-2xl clamp(1.35rem, 4.2vw, 2rem) — Aa — Portez ce vieux

    Côté Tailwind : text-fluid-sm, text-fluid-2xl… La colonne « Rendu » utilise la police de titrage du thème courant.

    Viewport jumeau

    Même échelle fluide, calculée à 390 px et 1280 px — sans redimensionner la fenêtre.

    Téléphone 390 px

      Bureau 1280 px

        Familles de police

        Token Rendu
        --font-sans Interface système — 0123456789
        --font-mono const total = 42; // 0O1lI
        --ds-font-display (app) Titrage de l’application

        Le preset ne fournit que sans et mono (piles système, zéro requête réseau). Les polices de titrage (Fredoka, Plus Jakarta Sans, DM Sans, Inter…) sont chargées par chaque app ; ce showroom ne les télécharge pas et retombe sur la pile système si elles ne sont pas installées localement.

        Espacement fluide

        Token Valeur Calculé Aperçu
        --spacing-fluid-sm clamp(0.5rem, 1.4vw, 0.75rem) —
        --spacing-fluid-md clamp(0.75rem, 2.4vw, 1.25rem) —
        --spacing-fluid-lg clamp(1rem, 3.2vw, 1.75rem) —

        Points de rupture

        breakpoint : —

        En rem et non en px : les ruptures suivent la taille de police du navigateur. Une police par défaut agrandie décale donc bien la mise en page — sinon c’est une régression d’accessibilité.

        Safe-areas iOS

        Zone de contenu à l’intérieur des env(safe-area-inset-*). Utilitaires : pt-safe, pb-safe, px-safe, py-safe, top-safe, bottom-safe-3…
        Inset Valeur mesurée
        safe-area-inset-top —
        safe-area-inset-bottom —
        safe-area-inset-left —
        safe-area-inset-right —

        Ces valeurs valent 0 partout sauf sur un appareil à encoche / barre d’accueil, et uniquement si le document déclare viewport-fit=cover. Les variantes *-safe-3 ajoutent 0,75 rem à l’inset système.

        Cible tactile & focus

        Le pointillé rouge matérialise le minimum de 2,75 rem.

        Le preset pose aussi :focus-visible → outline: 2px solid currentColor avec outline-offset: 2px. Parcourez la page au clavier (Tab) : l’anneau suit la couleur du texte, donc reste lisible dans tous les thèmes.

        Couleurs

        Le preset partagé n’expose aucune couleur. C’est volontaire : chaque application définit sa palette dans son propre @theme. Le showroom mappe ces palettes sur un jeu de rôles sémantiques communs — c’est ce jeu que la bascule de thème réécrit.

        Clair et sombre, côte à côte

        Valider une palette en alternant les deux schémas est un mauvais outil. Les voici simultanément, pour le thème sélectionné.

        Deux applications, côte à côte

        Choisir deux habillages pour comparer leurs primaires et surfaces dans le schéma courant — utile avant une revue visuelle croisée.

        Écarts entre les deux apps

        Seuls les rôles qui diffèrent — pour une revue croisée sans lire deux panneaux entiers.

        Rôle App A App B

        Palette du thème courant, lue via getComputedStyle.

        Primitives d’interface

        Ces six composants n’ont pas été inventés : ils ont été extraits de ce que plusieurs apps avaient déjà réécrit chacune de leur côté. Les matrices ci-dessous montrent les combinaisons — c’est là que vivent les régressions, pas dans l’instance isolée.

        Button — variantes × tailles × états (4 apps avaient la même API)

        Matrice des variantes de bouton par taille et par état

        loading pose aria-busy et désactive le bouton : afficher un indicateur sans bloquer laissait passer le double-clic dans deux apps.

        Badge — tons sémantiques × variantes

        Matrice des tons de pastille par variante

        Le tone dit une intention, pas une couleur — la teinte réelle vient du thème de l’app.

        Field — états de validation

        Sert aux relances de cotisation.

        En euros, deux décimales.

        En erreur, aria-describedby référence l’aide et le message — les copies locales remplaçaient l’une par l’autre, faisant disparaître la consigne au pire moment.

        Stat — chiffres-clés

        Adhérents
        128
        12 en hausse
        Trésorerie
        4 210 €
        320 € en baisse
        Licences
        96

        La tendance porte une flèche et un libellé lu par les lecteurs d’écran : la couleur ne suffit pas à distinguer une hausse d’une baisse.

        Card — la surface que dix apps avaient

        Cotisations 2026

        12 adhérents en retard

        Le clic est sur le bouton, pas sur la carte : le clavier et le lecteur d’écran y accèdent.

        padding={false} : l’image touche les bords.

        Une carte est une surface, pas un contrôle : l’action va dans action (un élément focusable à l’intérieur), jamais sur la carte entière. CardHeader rend un vrai titre, pour que la structure du document survive à la mise en page.

        Skeleton — chargement

        Chargement des écritures

        Les barres sont aria-hidden ; seul le conteneur est annoncé (role="status"). Sinon un lecteur d’écran énumère « chargement » une fois par barre.

        Sheet — feuille modale

        Essayez au clavier : Tab boucle dans le panneau, Échap ferme, et le focus revient sur le bouton d’origine — les trois comportements qu’aucune copie locale n’avait au complet.

        SegmentedControl — onglets « pilule »

        Bac à sable — composer et copier

        Les matrices montrent des combinaisons choisies ; celle dont vous avez besoin n’y est pas forcément. Réglez les props ci-dessous : le rendu suit, et l’appel React à copier se réécrit avec — y compris les attributs d’accessibilité qu’ils entraînent.

        Le DOM engendré ici est écrit à la main, comme dans toute cette page — le showroom n’embarque pas React. C’est bien le même balisage que produit le composant, et il est habillé par le components.css réel du paquet ; mais c’est l’extrait de code, pas l’aperçu, qui fait foi.

        Composants applicatifs

        Les composants du paquet ne posent que des attributs data-dwc : aucune classe, aucun style imposé. Chaque app les habille avec sa propre CSS. Ci-dessous, le DOM exact produit par chaque composant, habillé par le thème courant.

        EmptyState

        État vide avec action suivante.

        Aucune donnée pour l’instant

        Créez une première entrée pour voir apparaître vos statistiques.

        Sélecteurs CSS
        • [data-dwc="empty-state"] — conteneur
        • [data-dwc="empty-state-icon"]
        • [data-dwc="empty-state-title"]
        • [data-dwc="empty-state-desc"]
        • [data-dwc="empty-state-action"]

        ErrorBanner

        Erreur récupérable. severity distingue le temporaire du permanent ; role passe de alert à status.

        Impossible d’enregistrer la saison.
        Connexion instable : les données peuvent être en retard.
        Sauvegarde locale effectuée.
        Sélecteurs CSS
        • [data-dwc="error-banner"][data-severity="error|warning|info"]
        • [data-dwc="error-banner-message"]
        • [data-dwc="error-banner-retry"]
        • [data-dwc="error-banner-dismiss"]

        SyncStatusBadge

        État de synchronisation. Le compteur n’apparaît que sur pending.

        Synchronisé En attente (3) Hors ligne Erreur
        Sélecteurs CSS
        Hors ligne — reconnexion…
        • [data-dwc="sync-status"][data-status="synced|pending|offline|error"]

        ErrorBoundary

        UI de repli par défaut (évite l’écran blanc). Le bouton « sauvegarde » n’apparaît que si onDownloadBackup est fourni.

        Une erreur est survenue.

        Sélecteurs CSS
        • [data-dwc="error-boundary"]
        • [data-dwc="error-boundary-title"]
        • [data-dwc="error-boundary-reset"]
        • [data-dwc="error-boundary-backup"]

        PwaInstallPrompt

        Bannière passive (role="region", pas dialog : elle ne piège pas le focus). Deux visages : le bouton natif, et la marche à suivre là où le navigateur n’en expose aucun.

        Installer l’application

        Ajoutez cette application à votre écran d’accueil : accès rapide, hors-ligne.

        Installer l’application

        Touchez le bouton Partager, puis « Sur l’écran d’accueil ».

        Sélecteurs CSS
        • [data-dwc="pwa-install-prompt"]
        • [data-dwc="pwa-install-title"]
        • [data-dwc="pwa-install-desc"]
        • [data-dwc="pwa-install-actions"]
        • [data-method="prompt|instructions"]
        • [data-platform="ios|safari|generic"]

        ShareButton

        Partage natif, repli presse-papiers. L’annulation n’affiche rien : fermer la feuille de partage n’est pas un échec.

        Lien copié
        Sélecteurs CSS
        • [data-dwc="share-button-group"]
        • [data-dwc="share-button"]
        • [data-dwc="share-button-status"]

        Sparkline · BarChart · Gauge

        Le tracé est aria-hidden : la donnée est dite en toutes lettres dans un texte à côté. Un graphique qui ne dit rien ne dit rien à personne.

        trafic : de 4 à 28 vues, en hausse
        Sélecteurs CSS
        • [data-dwc="sparkline"]
        • [data-dwc="sparkline-text"]
        • [data-dwc="bars"]
        • [data-dwc="gauge"]

        UpdatePromptBanner

        registerSW est reçu en prop, plus importé en dur : le module a rejoint le barrel. Le bouton UpdateButton des réglages, lui, s’en passe.

        Mise à jour disponible
        Sélecteurs CSS
        • [data-dwc="update-banner"]
        • [data-dwc="update-banner-title"]
        • [data-dwc="update-banner-actions"]
        • [data-dwc="update-banner-update"]
        • [data-dwc="update-banner-dismiss"]

        ConfirmDialog

        Sept apps sur seize en avaient un, tous différents. Le focus s’ouvre sur Annuler — jamais sur la confirmation.

        Supprimer la partie ?

        Cette action est définitive.
        Sélecteurs CSS
        • [data-dwc="confirm"]
        • [data-dwc="confirm-panel"]
        • [data-dwc="confirm-title"]
        • [data-dwc="confirm-body"]
        • [data-dwc="confirm-actions"]
        • [data-dwc="confirm-cancel"]
        • [data-dwc="confirm-confirm"]

        Toast

        Deux régions vivantes montées en permanence, et aucun rôle sur le message : c’est ce qui évite la double annonce constatée dans deux apps. Une action facultative y met un bouton — « Annuler » plutôt que « êtes-vous sûr ? » — et porte la durée de vie à huit secondes.

        Fiche enregistrée
        Note supprimée
        Envoi impossible
        Sélecteurs CSS
        • [data-dwc="toast-viewport"]
        • [data-dwc="toast-region"]
        • [data-dwc="toast"][data-tone]
        • [data-dwc="toast-message"]
        • [data-dwc="toast-action"]
        • [data-dwc="toast-close"]

        BottomNav

        Sept apps sur seize en ont une. L’onglet courant n’est jamais distingué par la seule couleur : aria-current, un trait, et un « Page actuelle » lu mais non vu.

        Sélecteurs CSS
        • [data-dwc="bottom-nav"]
        • [data-dwc="bottom-nav-item"][data-current]
        • [data-dwc="bottom-nav-icon"]
        • [data-dwc="bottom-nav-label"]
        • [data-dwc="bottom-nav-badge"]
        • [data-dwc="bottom-nav-more"]
        • [data-dwc="bottom-nav-drawer"]

        AppHeader

        Neuf apps ont un en-tête. Le titre EST le h1 de la page ; le retour est un lien quand il a une destination, un bouton quand il n’a qu’une action — nommé « Retour » en sept langues.

        Trésorerie

        Saison 2025 – 2026 · 128 adhérents

        Sélecteurs CSS
        • [data-dwc="app-header"][data-sticky]
        • [data-dwc="app-header-row"]
        • [data-dwc="app-header-back"]
        • [data-dwc="app-header-leading"]
        • [data-dwc="app-header-title"]
        • [data-dwc="app-header-actions"]
        • [data-dwc="app-header-extra"]

        AppShell

        La coquille assemblée : lien d’évitement, en-tête, contenu borné, barre basse. Le pied de page n’y est PAS — Accueil et À propos seulement.

        Aller au contenu

        Notes

        Contenu de la vue.

        Sélecteurs CSS
        • [data-dwc="app-shell-skip"]
        • [data-dwc="app-shell"]
        • [data-dwc="chrome-prefs"]

        PageContainer

        Centré, borné à un palier de largeur, zones sûres iOS comprises — celle du bas surtout, sans laquelle le dernier bouton d’une vue colle à la barre d’onglets. Promu de badminton et molkky.

        width="sm" — 28 rem, centré, marges sûres.

        Sélecteurs CSS
        • [data-dwc="page-container"][data-width="sm|md|lg|xl|full"]
        • [data-dwc="page-container"][data-padding="none"]

        LoginForm

        Quatre apps avaient le même écran : deux champs, un bouton, une erreur traduite. Le formulaire est présentationnel — l’appelant fait signIn et redonne busy et error ; children et footer portent le reste (inscription, passkey, mot de passe oublié).

        Connexion

        Identifiants invalides.

        Sélecteurs CSS
        • [data-dwc="login-form"][data-mode="signin|signup"]
        • [data-dwc="login-form-title"]
        • [data-dwc="login-form-error"]
        • [data-dwc="login-form-footer"]

        MfaChallenge

        L’étape TOTP au login, promue de mister-doc et miss-uwh : clavier numérique, code reçu proposé par le système, voie de secours et déconnexion seulement quand l’appelant les fournit.

        Vérification en deux étapes

        Saisissez le code de votre application d’authentification.

        Sélecteurs CSS
        • [data-dwc="mfa-challenge"][data-mode="totp|recovery"]
        • [data-dwc="mfa-challenge-hint"]
        • [data-dwc="mfa-challenge-error"]
        • [data-dwc="mfa-challenge"] [data-action="switch"]
        • [data-dwc="mfa-challenge"] [data-action="sign-out"]

        ThemeToggle

        Cinq apps en ont un. Celui-ci parcourt les trois états de useTheme : une bascule à deux états interdit de revenir à « système ».

        Sélecteurs CSS
        • [data-dwc="theme-toggle"]
        • [data-dwc="theme-toggle"][data-theme-state]
        • [data-dwc="theme-toggle-icon"]
        • [data-dwc="theme-toggle-label"]

        ChromePrefs

        Groupe accessible thème + langue (ou densités) : une seule étiquette pour ce que le hub et plusieurs apps juxtaposent déjà.

        Sélecteurs CSS
        • [data-dwc="chrome-prefs"]

        AppFooter

        Lien code source + lien sponsor, ouverture sécurisée.

        Sélecteurs CSS
        • [data-dwc="app-footer"]
        • [data-dwc="footer-source"]
        • [data-dwc="footer-sponsor"]

        FamilyAbout

        L’écran À propos famille : installation, intro métier, grille des sœurs (showSource={false}), pied de page. Promu du squelette.

        Intro métier (carte titre) — puis la grille, puis le pied.

        Nos autres apps

        Sélecteurs CSS
        • [data-dwc="family-about"]

        AppVersion

        Le numéro de version, injecté au build par vite-version : ce qui tourne, ce qui vient d’être installé, ce qui est en ligne.

        Version  3.13.0 Compilée le 26 août 2026 · 104c944 Version 3.14.0 disponible

        Sélecteurs CSS
        • [data-dwc="app-version"]
        • [data-dwc="app-version-label"]
        • [data-dwc="app-version-value"]
        • [data-dwc="app-version-details"]
        • [data-dwc="app-version-updated"]
        • [data-dwc="app-version-available"]

        FamilyApps

        Grille « nos autres applications », alimentée par apps-catalog.js, avec badge de maturité.

        Nos autres applications

          Sélecteurs CSS
          • [data-dwc="family-apps"]
          • [data-dwc="family-links"], [data-dwc="family-source"], [data-dwc="family-sponsor"]
          • [data-dwc="family-app-list"], [data-dwc="family-app-item"], [data-dwc="family-app"], [data-dwc="family-app-repo"]
          • [data-dwc="family-app-icon"], [data-dwc="family-app-body"], [data-dwc="family-app-name"], [data-dwc="family-app-desc"], [data-dwc="family-app-arrow"]
          • [data-dwc="maturity"][data-maturity="…"]

          Les icônes réelles sont chargées depuis les GitHub Pages de chaque app. Ce showroom reste hors ligne : il exerce le chemin de repli (initiale du nom) du composant.

          Lequel choisir ?

          Les sections précédentes montrent chaque composant seul. Elles ne disent jamais lequel prendre quand deux conviennent — et c’est pourtant là qu’on hésite. Chaque arbre tient en deux à quatre branches ; au-delà, ce n’est pas l’arbre qui manque de place, c’est l’API qui est sous-spécifiée.

          Hooks et utilitaires

          Neuf exports du barrel, soit près de la moitié de la surface React du paquet - et aucun n’apparaissait ici jusqu’à présent. Ils n’ont pas de démo : un hook ne se regarde pas. Ils ont une signature, une phrase, et le piège qui va avec.

          Hooks et utilitaires exportés par le paquet

          useTheme mérite une mention à part : c’est le contrat que reproduit le sélecteur en haut de cette page - light | dark | system persisté sous dwc_theme, attribut data-theme posé sur <html>.

          Contrôles d’accessibilité

          Mesurés sur cette page, dans le thème courant, à chaque changement de thème ou de taille de fenêtre. Ce ne sont pas des valeurs recopiées : elles sortent de getBoundingClientRect et de getComputedStyle.

          La chaîne d’outils

          Quatre filets successifs, du clavier du développeur au navigateur de l’utilisateur. Aucun ne suffit seul, et c’est pour ça qu’ils sont quatre.

          Outil Quand Ce qu’il attrape Réglage partagé
          eslint-plugin-jsx-a11y À l’écriture Les fautes visibles dans le JSX : image sans alt, gestionnaire de clic sur un <div>, label orphelin. Règles recommended en warn. eslint-react
          axe-core + Playwright En CI, navigateur réel Violations WCAG sur le DOM rendu, tags wcag2a, wcag2aa, wcag21a, wcag21aa. Suite présente dans 11 apps. playwright-a11y, templates/e2e/a11y.spec.ts
          Lighthouse CI En CI, sur le build Score d’accessibilité global, en error sous 0,9 — la PR est bloquée. Configuré dans 12 apps. pwa-lighthouse (workflow)
          Le design system À l’exécution Ce qu’un audit ne rattrape pas parce que ça se décide à la conception : cible tactile de 2,75 rem imposée, :focus-visible qui suit la couleur du texte, breakpoints en rem, prefers-reduced-motion, sévérités jamais portées par la seule couleur. tailwind-preset.css, components.css

          axe-core ne détecte que 30 à 50 % des défauts, et Lighthouse encore moins. Un score de 100 ne dit pas qu’une appli est utilisable au clavier ni qu’un lecteur d’écran l’annonce correctement — d’où les deux dernières lignes du tableau, et les mesures ci-dessous.

          Cible tactile

          Hauteur mesurée des commandes interactives

          Seuil de 2,75 rem (≈ 44 px), recommandation WCAG 2.5.5 / iOS. Le Button partagé le tient même en taille sm — les variantes locales des apps descendaient à 32 px.

          Contraste du texte

          Ratios de contraste calculés par paire premier plan / arrière-plan

          Seuil AA : 4,5:1 pour le texte courant, 3:1 pour le grand texte et les éléments d’interface. Un thème d’app qui échoue ici échoue aussi dans l’app.

          Campagne contraste multi-apps

          Ratios calculés sur les palettes déclarées (sans habiller la page) — pour prioriser une campagne, pas une app à la fois. Seuil AA 4,5:1.

          App text / surface primary / on soft / text

          Contraste forcé

          Sous « contraste élevé » de Windows, le navigateur remplace d’autorité les couleurs de texte, de fond et de bordure, et supprime les ombres. Tout components.css reposant sur des variables et des color-mix(), ce rendu-là ne se déduit pas : il se regarde.

          Sans les correctifs

          Avec les correctifs livrés

          Ces deux panneaux sont une émulation, pas le vrai mode : la page reproduit à la main ce que fait le navigateur — couleurs ramenées à la palette système « Aquatique » de Windows, ombres supprimées, et transparent laissé intact, car le forçage ne le touche pas. C’est précisément ce dernier point qui vide le bouton primaire à gauche. Le rendu authentique s’obtient en activant le thème de contraste dans le système, ou via Rendering › Emulate CSS media (forced-colors) dans les outils de développement.

          Régressions du contraste forcé et correctifs livrés

          Démo par application

          Chaque tuile est peinte avec la palette réelle d’une application, dans le schéma en cours. Un clic habille la page entière — le même geste que Habiller la page sur une carte de la vitrine. L’aperçu en grand reprend alors les composants partagés dans cet univers. C’est la même page, le même CSS : seules les treize variables du contrat changent.

          Ce sont des aperçus générés, pas des captures d’écran. Ils sont peints en direct avec la palette réelle de chaque app et les composants réels du paquet — ils montrent donc fidèlement le design system dans chaque univers, mais pas les écrans des applications. Une vraie capture déposée dans showroom/screenshots/ et déclarée dans screenshots.js prend automatiquement la place de l’aperçu.

          Stack de la famille

          Relevé dans le package.json et le code source des applications, pas dans une note d’intention. Ce qui suit décrit ce qui est réellement installé et appelé.

          Bases de données

          Trois familles de persistance coexistent, et c’est délibéré : une app qui n’a pas besoin de compte utilisateur n’embarque pas de backend.

          Persistance Applications Fonctionnalités utilisées
          Supabase 9 apps — carbook, lookhouse, uwh, doc, footcoach, mölkky, family-map, miss-koh, settle Postgres + RLS (deny-by-default), Auth, Realtime (.channel()), fonctions SQL (.rpc()), Storage, Edge Functions (functions.invoke), migrations versionnées — de 3 (footcoach) à 26 (doc).
          Firebase 3 apps — miss-ticket, puzzle, qowa Realtime Database pour le temps réel à faible latence (puzzle, qowa), Firestore pour les données structurées (miss-ticket, qowa), Auth, Storage, Cloud Functions.
          Local-first 6 apps — badminton, contraction, devises, dice, genius, cim10 localStorage / IndexedDB seuls. Aucun compte, aucune donnée qui sort du téléphone — souvent le bon choix, et la meilleure réponse RGPD.

          miss-supaboss est à part : elle pilote d’autres comptes Supabase via un backend Node et un jeton personnel, sans client Supabase côté navigateur. Le paquet fournit par ailleurs un keep-alive (workflow réutilisable + templates/supabase/keep-alive.sql) contre la mise en pause automatique du plan gratuit.

          Icônes

          lucide-react est le standard famille : 10 apps sur 15 l’utilisent. Icônes SVG tree-shakées (seules celles importées sont embarquées), strokeWidth et size ajustables, et currentColor qui suit les tokens du thème.

          Deux réserves apprises à l’usage : lucide 1.x a retiré les icônes de marque — le logo GitHub des composants partagés est un SVG inline ; et une icône décorative doit porter aria-hidden, le libellé accessible vivant sur le bouton qui la contient. Les 5 apps sans lucide utilisent des SVG inline maison.

          Cartes et géographie

          Une seule app en a besoin : miss-lookhouse (veille immobilière), avec Leaflet 1.9 — affichage des biens et éditeur de zone par polygone. Tuiles OpenStreetMap, avec attribution et sans clé d’API : pas de compte tiers, pas de quota, pas de traceur.

          Pas de standard famille imposé ici — un seul cas d’usage ne justifie pas une règle. Si une deuxième app cartographie, Leaflet + OSM est le point de départ à reprendre.

          Animations — Rive

          3 apps sur 15 intègrent Rive — animations vectorielles pilotées par machines à états, plutôt que par des Lottie/GIF joués en boucle. Et sur deux runtimes différents : @rive-app/react-canvas pour miss-badminton et mister-mölkky, @rive-app/react-webgl2 pour miss-genius.

          Application Runtime Garde-fou avant de charger le moteur
          miss-badminton react-canvas 4.28 import.meta.glob à la compilation : fichier absent = aucun fetch, donc aucun 404. Puis vérification des 4 octets d’en-tête (RIVE) avant de monter le canvas.
          mister-mölkky react-canvas 4.28 Même sonde d’en-tête, plus un React.lazy sur le module du canvas : le WASM (~200 ko) ne descend que si un fichier valide existe.
          miss-genius react-webgl2 4.18 Trois sorties vers le visuel statique : pas de src, événement loaderror, ou prefers-reduced-motion. Le moteur WebGL n’est alors jamais chargé.

          Deux constats qui ne se voient pas dans un package.json. Aucun fichier .riv n’est livré — les trois dossiers ne contiennent qu’un README : en production, les trois apps affichent aujourd’hui leur repli statique, et c’est le chemin le mieux testé. Et aucune n’utilise le wrapper du paquet (RiveAnimation, sous-chemin /react/rive) : il enveloppe le composant <Rive>, alors que les trois ont besoin du hook useRive pour atteindre l’instance — entrées de machine à états, événements d’erreur, cleanup(). Un wrapper qui ne donne pas accès à l’instance ne sert à rien ici ; c’est une inadéquation d’API, pas un oubli.

          Outils de test

          Outil Portée Config partagée
          Vitest 4 Unitaire et composants, en jsdom. Présent dans les 13 apps, de 2 à 54 fichiers de test. vitest-base, vitest-setup
          Playwright Parcours critiques de bout en bout, dans un vrai navigateur. 11 apps. playwright-base
          axe-core Audit d’accessibilité automatisé, branché sur Playwright. 10 apps. playwright-a11y, templates/e2e/a11y.spec.ts
          Vitest Browser Mode Composants dans un navigateur réel plutôt qu’en jsdom. Disponible, utilisé par aucune app à ce jour. vitest-browser-base

          axe-core ne détecte qu’une partie des défauts d’accessibilité — typiquement 30 à 50 %. Il ne remplace ni le parcours au clavier, ni les contrôles de contraste de la section précédente.

          Intégration

          Deux lignes dans le CSS principal de l’app suffisent à hériter de tout ce qui précède. La palette, elle, se déclare ensuite dans le @theme du projet.

          /* src/index.css */
          @import 'tailwindcss';
          @import '@mister-guiiug/dev-pwa-config/tailwind-preset.css';
          
          /* Identité PROPRE à l'application — jamais dans le preset partagé. */
          @theme {
            --color-primary: #1758ba;
            --color-accent: #edc715;
            --font-display: 'Fredoka', system-ui, sans-serif;
            --radius-card: 1.1rem;
          }

          Rappel de priorité : un @theme placé après l’import écrase le preset ; du CSS non « layered » l’emporte sur la couche base.

          Le schéma clair/sombre est piloté par le hook useTheme() (light | dark | system), qui pose data-theme sur <html> — ou la classe .dark avec l’option attribute: 'class'. Le sélecteur en haut de cette page reproduit exactement ce contrat.

          Raccourcis

          CtrlK
          Recherche unifiée
          /
          Focus recherche
          i
          Basculer l’inspecteur
          d
          Densité compacte
          Alt+clic sommaire
          Épingler une section
          ?
          Cette aide
          p
          Mode présentation
          Esc
          Fermer panneau / aide