Skip to content

Frontend - Vite, themes SCSS, JS

Le pipeline frontend a remplace l'ancien Gulp (un gulpfile.js legacy subsiste mais n'est plus utilise). Tout se lance depuis src/Web avec Node 22.12.0.

Vite

Config : src/Web/vite.config.js (+ helper vite.config.helper.js).

  • Entrees decouvertes dynamiquement (vite.config.helper.js) :
    • JS : entree unique scripts/index.js -> wwwroot/js/index.js.
    • SCSS : globSync sur styles/themes/*/main.scss ; chaque theme produit wwwroot/css/<theme>/main.css (via assetFileNames : un fichier styles-<theme>.css est renomme en <theme>/main.css).
    • SCSS hors theme : config.sass.standalone (cle -> entree styles-flat-<nom>), emis a plat en wwwroot/css/<nom>.css. Une seule entree aujourd'hui : rte (voir ci-dessous).
  • emptyOutDir: false (ne vide pas wwwroot, qui contient aussi favicons/og/images).
  • Plugin maison cleanupScssJs : Vite genere un stub JS vide pour chaque entree SCSS ; ce plugin supprime ces js/styles-*.js.
  • vite-plugin-live-reload : surveille wwwroot/css/**/*.css et wwwroot/js/**/*.js pour declencher le rechargement navigateur.
  • Dev server : port 8080, proxy / -> https://localhost:44360 (l'app .NET).
  • Prod : terser (drop console), cssMinify, sourcemaps desactivees. Dev : sourcemaps + rollup-plugin-visualizer (scripts/logs/stats.html).
  • Alias : @ -> scripts, @styles -> styles.

Commandes (package.json) :

  • npm run dev : vite build --mode development puis chokidar re-build sur changement styles/**/*.scss et scripts/**/*.js.
  • npm run build : build (mode par defaut).
  • npm run build:prod : ESLint puis vite build --mode production (une erreur de lint stoppe le build).

rte.css - l'apercu du Rich Text du backoffice

wwwroot/css/rte.css est selectionne par le DataType Richtext editor ("stylesheets": ["/css/rte.css"]) : c'est ce qui donne a l'editeur, dans le backoffice, la typographie du site.

Avant le 2026-07-29 c'etait un fichier de 2 lignes de commentaire ecrit a la main directement dans wwwroot - a la fois une violation de la regle 2 (ne jamais editer la sortie generee) et un apercu qui n'apercevait rien. Il est desormais construit depuis styles/rte/main.scss, qui reprend base/sections/_single.scss .single__content (le corps d'article, la propriete RTE ou un editeur passe le plus de temps).

Deux particularites, toutes deux deliberees :

  • Il est neutre vis-a-vis de la marque. Le DataType prend une seule liste de feuilles et les applique toutes en meme temps : on ne peut pas servir un rte.css par marque. Il previsualise donc la structure (echelle des titres, traitement des liens, rythme des listes et des figure), pas les couleurs d'une marque. Ses valeurs sont declarees une fois dans styles/rte/_variables.scss et consommees par @use - le backoffice n'a ni <body class="theme-*"> ni les custom properties --g-*, donc @use est ici la seule option conforme a la regle 11.

  • PurgeCSS ne le traite pas (PURGE_SKIP dans vite.config.helper.js). Il style des elements nus qu'un editeur tape (blockquote, table, th, ol) et qui n'apparaissent dans aucun .cshtml : purge, le rte.css de prod ne contenait presque plus rien - le meme mode de panne silencieuse et prod-seulement que le reset figure.

    ⚠️ Ne pas "corriger" ca en ajoutant la feuille au content de PurgeCSS. Ca marche aussi, et c'est le mauvais correctif : les noms d'elements survivent alors dans toutes les feuilles. Mesure - ca ajoutait ~0,35 ko de selecteurs nus ressuscites a chacun des trois themes, soit une modification reelle du CSS livre au site en echange d'un apercu qui ne concerne que le backoffice. Sauter le fichier garde le rayon d'action au fichier.

PostCSS

Chaine appliquee aux CSS (vite.config.js) :

  1. postcss-sort-media-queries
  2. autoprefixer
  3. postcss-combine-duplicated-selectors (removeDuplicatedValues)
  4. en prod uniquement : PurgeCSS puis cssnano

SCSS via api: "modern-compiler", deprecations silencieuses (if-function, global-builtin, color-functions, import).

PurgeCSS

purgeCss() (vite.config.helper.js) scanne Views/**/*.cshtml, scripts/**/*.js et le dist de vanilla-cookieconsent (qui construit ses classes au runtime).

Safelist reelle (voir vite.config.helper.js, seule source de verite) :

  • standard : /^js-/, /^is-/, /^has-/, /^u-/, /^util--/ ; les classes Bootstrap injectees par les ButtonPickers (/^btn/, /^shadow/, /^rounded/, /^py-/, /^container/, /^text-bg-/, /^justify-content-/, /^align-items-/, /^row-cols/) ; /^swiper/ ; /^umb-block-grid/ ; les animations ButtonPicker (fade-in, slide-up, zoom-in) ; job-list ; et les modificateurs d'affichage de blocs (/^card-collection--/, /^media-gallery--/, /^child-listing--/, /^media-text--/).
  • deep + greedy : /^cc/, /^cm/, /^pm/ (selecteurs cookieconsent v3) ; /social-ico/ (les glyphes sociaux du footer sont choisis par [data-net="facebook"] etc., mais la valeur data-net est composee en Razor - le litteral n'apparait dans aucune source, donc le selecteur avec attribut serait purge sans cette entree greedy).

⚠️ Piege : une classe composee en Razor (card-collection--@display) n'apparait dans aucun .cshtml sous sa forme litterale — sa valeur vit dans le DataType, que PurgeCSS ne lit pas. Elle sera purgee en prod (et seulement en prod : le build dev la garde) si elle n'est pas safelistee. C'est la raison des quatre regex -- ci-dessus. Regle generale : toute classe injectee dynamiquement (par JS, par une lib, ou composee en Razor) doit etre safelistee. Corollaire pratique : ajouter un ButtonPicker d'affichage a un bloc = ajouter sa regex ici (/^media-text--/ a ete ajoute avec mediaText.display le 2026-07-17). Le gate de fidelite tourne sur le CSS construit justement pour attraper ce cas.

Le gate de fidelite tourne contre le CSS builde precisement pour attraper ce cas.

Themes SCSS

Arborescence sous src/Web/styles/ :

  • themes/<theme>/main.scss : point d'entree d'un theme. Importe Bootstrap (@use ".../node_modules/bootstrap/scss/bootstrap") et les partials base/ (header, footer, cookies, a11y, sections d'exemple). Peut surcharger via with (...).
  • themes/<theme>/_bootstrap_variables.scss : overrides des tokens Bootstrap par theme.
  • base/ : _bootstrap_variables.scss, layout/ (_header, _footer, _cookies, _a11y), sections/, elements/.

Un theme par marque (multi-sites). config.sass.src = "./styles/themes/*/main.scss" : getSassEntries() glob et nomme chaque entree d'apres le dossier parent, assetFileNames mappe styles-<nom>.css -> wwwroot/css/<nom>/main.css. Ajouter une marque = ajouter son dossier, aucune modif de build.

Contrat de tokens :rootbase/** ne code JAMAIS une surface en dur

base/** est partage par les 4 marques, dont deux sont quasi-noires (portedenovembre.ch, gilliarday.ch). Un background: #fff enfoui dans un partial partage est invisible jusqu'au jour ou ce theme s'affiche. Toutes les surfaces / titres / bordures / ombres de base/ passent donc par des custom properties definies dans le :root de chaque themes/<theme>/main.scss.

TokenRoleValeur Gilliard
--g-body-bgfond de page#fff
--g-surfacesurface en relief : cartes, champs, panneaux de menu#fff
--g-surface-mutedsurface en creux : placeholders#f5f4f2
--g-headingtitres + texte fort (pas le corps : c'est --g-ink)#000
--g-borderfiletsrgba(0,0,0,.12)
--g-shadow / --g-shadow-hoverombre de carte au repos / au survol0 0 30px rgba(0,0,0,.14) / 0 10px 40px rgba(0,0,0,.18)
--sec-title-size / -tracking / -transformrythme des titres de section2rem / 8px / uppercase
--sec-pretitle-size / -trackingpre-titres1.125rem / 3px
--card-radius, --btn-radius, --btn-trackingformes0, 0, 1px

Les majuscules + le tracking large sont une signature Gilliard, pas un defaut partage : les 3 autres marques ne l'ont pas. D'ou --sec-title-transform/-tracking — un theme s'en affranchit sans toucher un seul composant. Verifie : en surchargeant uniquement :root avec la palette PDN, .g-card passe a #111, les titres a blanc et le text-transform a none, zero edition de composant.

Regle : si deux themes en ont besoin -> token dans base/. Si un seul -> surcharge dans themes/<theme>/_overrides.scss. Les valeurs Gilliard ci-dessus sont inchangees par ce refactor (harnais byte-identique).

⚠️ var() ne se resout PAS dans une data-URI SVG. background: url("data:image/svg+xml,… fill='%238e784c'…") a l'or Gilliard cuit dedans — 3 occurrences dans base/sections/_card-collection.scss (pin d'adresse, chevrons). Un theme qui veut une autre couleur doit redeclarer la declaration background entiere dans son _overrides.scss : on ne tokenise pas l'interieur d'une data-URI.

Quel theme pour quelle requete ? (Extensions/ThemeExtensions.cs)

GetThemeName() : switch explicite sur l'alias du doctype de la racine du site -> slug de theme (homePage -> gilliard, chevaliersHome -> chevaliers, pdnHome -> pdn, gilliardayHome -> gilliarday), defaut gilliard. _MasterLayout resout la racine, appelle GetThemeName() et emet /css/<theme>/main.css, <body class="theme-<theme>"> et passe le slug a _header via ViewData["Theme"] (logo /images/<theme>/logo.svg).

Pourquoi pas un champ de contenu : un theme n'existe que si quelqu'un a ecrit le SCSS. Laisser un editeur saisir une valeur qui pointe vers une feuille inexistante ferait un ecran blanc. Chaque marque a de toute facon deja son doctype d'accueil (il porte sa propre Block Grid) : l'alias est un discriminant gratuit et fiable — zero DataType, zero migration, zero regen. Un switch est greppable et testable (tests/Web/Extensions/ThemeExtensionsTests.cs, 17 tests dont le slug reste un segment de chemin sur — il est interpole dans une URL).

Ajouter une marque : ajouter le case dans ThemeExtensions, styles/themes/<slug>/main.scss, wwwroot/images/<slug>/logo.svg. Rien d'autre.

Aucun nom de marque ni chemin d'asset code en dur dans une vue partagee. Quatre marques partagent _header/_footer/map : le nom vient de siteSettings.brandName (contenu), le chemin du slug de theme.

Themes livres : gilliard (+ chevaliers, portedenovembre, gilliarday a venir). Les themes d'exemple du template (default, cave, piscines) et les partials d'exemple (base/elements/_exampleElement.scss, base/sections/_exampleSection.scss) ont ete supprimes (nettoyage 2026-07-15). Dupliquer themes/gilliard/ comme point de depart.

wwwroot/css/default/main.css subsiste : artefact perime d'un theme supprime (build.emptyOutDir: false). wwwroot/css/cookiesConsent/main.css en revanche est normal — c'est le CSS de vanilla-cookieconsent importe par scripts/modules/cookiesConsent.js, nomme d'apres le chunk JS, pas un theme.

Theme gilliard (themes/gilliard/main.scss) : importe en plus base/layout/_page-hero (banniere de titre des pages interieures) et les sections base/sections/ _hero-slider, _card-collection, _media-text (rangees centrees + emblème + image carree), _rich-intro (intro de section soulignee or), _cta-banner (bande CTA pleine largeur), _form-block, _map, _newsletter-band, _insta-feed, _swiper, et pour les pages structures : _listing (cartes de liste .g-card--listing/--blog + filtre blog), _team-grid (grille d'equipe), _single (corps de detail centre + faits event/vacancy), _text-page (pages legales), _vacancies (liste de postes .positions). Valeurs alignees sur les styles calcules du live gilliard.ch @1440.

Tokens couleurs neutres (CSS custom properties) — les partials base/** ne peuvent pas @use les variables SCSS du theme (elles vivent dans themes/gilliard/). Les neutres de la charte sont donc exposes comme variables CSS sur le bloc :root de themes/gilliard/main.scss, derivees des tokens SCSS de themes/gilliard/_bootstrap_variables.scss : --g-ink (#323232, texte), --g-gray (#9b9b9b, texte secondaire), --g-gray-light (#e4e4e4, bordures), --g-off-white (#f5f4f2, fonds), --g-wine (#bf425c, accent). Les partials base/** referencent var(--g-…) au lieu de reecrire les hex (nettoyage duplication 2026-07-16). L'or reste var(--bs-primary) ; ne pas convertir les hex dans les data-URI SVG (%23…var() n'y resout pas). Les valeurs CSS doivent correspondre exactement aux tokens SCSS.

Responsive (theme gilliard) — desktop-first, verifie @1024/768/375 contre le live (voir memoire gilliard-migration.md). Breakpoints Bootstrap : xl 1199.98 · lg 991.98 · sm 575.98. Regles cles :

  • Header (_header.scss) : le menu horizontal passe en panneau coulissant + burger a <=1199.98 (navbar-expand-xl, comme le live qui montre le burger des 1024). Bascule 100 % CSS (checkbox #siteNavToggle + transform), aucune modif JS.
  • Cartes (_card-collection.scss) : .card-collection__grid = var(--card-cols,3) desktop, 2-up <=lg, 1-up <=sm. La variante produits (--products, bouteilles) reste dense : 3-up <=xl, 2-up <=sm. Les cartes compactes (.g-card--activity des pages Activites, .g-card--no-media du "Nos Activites" home) gardent 3-up en tablette via &:has(.g-card--activity), &:has(.g-card--no-media) (min-width 576) — seules les grosses cartes-image (hero-boxes) tombent a 2-up, comme le live.
  • Titres bannieres : .page-hero__title (48px/12px) et .cta-banner__title (32px/8px) reduisent taille + interlettrage en lg/sm et ont overflow-wrap: break-word (un long mot majuscule ne doit jamais forcer un scroll horizontal — cause de debordement corrigee).
  • Footer (_footer.scss) : 4 colonnes desktop, 2 <=lg, 1 colonne centree <=sm.
  • Team (_team-grid.scss) : 4-up / 2-up (<=lg) / 1-up (<=sm) — identique au live. Regle de verification : aucune page ne doit avoir de scroll horizontal (scrollWidth === clientWidth) a 375, 768, 1024.

Couleurs et valeurs de design - toujours une variable, jamais une valeur en dur

Règle : une couleur ne s'écrit qu'une fois, dans une variable. Partout ailleurs, on référence la variable. Aucun #3214a8, rgb(...) ou hsl(...) littéral dans une règle de style, une vue, un fichier backoffice ou un module JS.

Vaut aussi pour les autres valeurs de design répétées : rayons, ombres, durées de transition, points de rupture, graisses.

Pourquoi

Une couleur en dur ne casse rien tout de suite. Elle coûte plus tard, de trois façons :

  1. On ne peut plus la changer d'un geste. La même valeur finit copiée dans plusieurs fichiers et une seule occurrence est oubliée au moment du changement de charte.
  2. Elle casse les thèmes. styles/base/ est partagé par tous les thèmes. Un color: #000 enfoui dans un partial partagé est invisible jusqu'au jour où un thème sombre s'affiche.
  3. Elle survit aux merges upstream. base/ vient du template : une valeur en dur ajoutée là revient à chaque git merge upstream.

La forme par défaut : variable SASS + @use

C'est du SASS : on utilise le mécanisme du langage. Un fichier de variables, importé par @use dans chaque fichier qui en a besoin. Pas de custom property CSS par défaut.

Nature de la valeurFichierConsommé par
Tout ce qui est propre à un thème : couleurs de marque, neutres, couleurs de composants, réglages de champs, header, footerstyles/themes/<theme>/_bootstrap_variables.scssthemes/<theme>/main.scss et les partials du thème
Valeur partagée par tous les thèmes et non colorée : points de rupture, échellesstyles/abstracts/_<sujet>.scss (dossier absent du template, à créer au premier besoin)n'importe quel partial, via @use
Valeur locale à un composant, surchargeableen tête du partial$nom: valeur !default;

Le fichier de variables du thème ne se limite pas aux overrides Bootstrap : il porte toutes les valeurs du thème, avec des noms sémantiques.

scss
// styles/themes/<theme>/_bootstrap_variables.scss

// Overrides Bootstrap
$primary:   #ee4305;
$secondary: #13134f;
$body-color: #13134f;

// Neutres
$grey:       #999999;
$grey-dark:  #555555;
$near-black: #151515;

// Alias semantiques, composes a partir des precedents
$field-dark:   $near-black;
$input-placeholder-color: $grey;
$footer-bg:    #04041a;
$footer-text:  $grey;

Composer plutôt que recopier. Une couleur brute est déclarée une seule fois, puis les usages la référencent ($field-dark: $near-black). Changer le neutre met à jour tous ses usages.

Consommer

scss
// styles/themes/<theme>/layout/_footer.scss
@use "../bootstrap_variables" as vars;

.footer {
  background: vars.$footer-bg;
  color: vars.$footer-text;
}

Le main.scss du thème charge le fichier une fois et passe les overrides à Bootstrap :

scss
@use "bootstrap_variables" as customBootstrapVars;
@use "../../../node_modules/bootstrap/scss/bootstrap" with (
  $primary: customBootstrapVars.$primary,
  $body-color: customBootstrapVars.$body-color
);

Pour une valeur partagée non thématique, même mécanique avec un alias court :

scss
@use "../../abstracts/breakpoints" as bp;

.card { padding: 1rem; @include bp.up(md) { padding: 2rem; } }

Le seul cas où une custom property reste nécessaire

Un partial de styles/base/ est chargé par tous les thèmes : il ne peut pas savoir lequel, donc il ne peut pas faire @use d'un fichier de variables de thème. Deux issues, dans cet ordre :

  1. Le partial ne pose pas de couleur. Il ne fait que du structurel (grille, espacements, positionnement) et laisse la couleur au thème. C'est la solution à préférer, et elle suffit dans la grande majorité des cas.
  2. Le partial a réellement besoin d'une couleur variable : là seulement, une custom property CSS que chaque thème définit dans son :root. C'est le seul mécanisme lu au runtime, donc le seul qui laisse un fichier compilé une fois servir plusieurs thèmes.

Autrement dit, var() n'est pas le défaut : c'est le recours quand @use est structurellement impossible.

Trois pièges

Le fichier de variables du thème ne remonte pas dans base/. Déclarer $grey dans themes/apol/_bootstrap_variables.scss ne le rend pas disponible dans base/layout/_footer.scss. Un color: #999 dans un partial de base/ alors que $grey: #999999 existe dans le thème est un cas réel, présent dans plusieurs dépôts : la variable existe, mais elle n'est pas atteignable depuis là. C'est le signal qu'il faut soit remonter la règle dans le thème, soit passer par une custom property.

var() ne se résout pas dans une data-URI SVG. Un background: url("data:image/svg+xml,... fill='%23206bff'...") a la couleur cuite dedans, et aucune variable n'y changera quoi que ce soit. On ne tokenise pas l'intérieur d'une data-URI : un thème qui veut une autre couleur redéclare la déclaration background entière. Dans un SVG inline en Razor, en revanche, fill="var(--nom)" fonctionne normalement.

Le backoffice n'a pas de SASS. Les CSS d'App_Plugins/ sont hors du pipeline Vite, donc @use n'y est pas disponible. La règle s'y applique quand même, avec le seul outil disponible : déclarer en :root et référencer. Une couleur de marque écrite en dur dans deux fichiers d'App_Plugins est exactement ce que cette règle existe pour empêcher.

Détecter

bash
# Valeurs en dur dans le SCSS, hors declarations de variables.
LC_ALL=C grep -rn "#[0-9a-fA-F]\{3,8\}\b" src/Web/styles --include="*.scss" | grep -v '^\s*\$'

# Une meme couleur ecrite dans plusieurs fichiers = a factoriser.
LC_ALL=C grep -rhio "#[0-9a-f]\{6\}" src/Web/styles src/Web/App_Plugins | sort | uniq -c | sort -rn

Bootstrap d'abord - le SCSS custom est le dernier recours

Le template livre Bootstrap 5.3. Objectif : écrire le moins de SCSS custom possible. La mise en page, les espacements, la typographie et les états courants passent par les classes utilitaires dans le HTML, pas par des règles SCSS.

Règle stricte : ne jamais écrire en SCSS une propriété qui a une classe utilitaire exacte. Avant d'écrire une déclaration, vérifier s'il existe un utilitaire.

Propriété CSSClasse utilitaire
display: blockd-block
display: flexd-flex
display: inline-flex + align-items: center + justify-content: centerd-inline-flex align-items-center justify-content-center
font-weight: 700 / 600 / 500fw-bold / fw-semibold / fw-medium
text-transform: uppercasetext-uppercase
text-decoration: nonetext-decoration-none
position: relativeposition-relative
padding / marginp-* / m-* (py-3, mb-4, ...)
bordure simpleborder, border-bottom, border-0
colonnes de grillerow-cols-md-2, col-lg-6, ...

Ces propriétés vont dans le HTML, pas dans le .scss.

Ce qui justifie du SCSS custom

Ce que les utilitaires ne couvrent pas : décors et dégradés, glyphes de police d'icônes (::before), effets de survol composés, white-space, animations, sélecteurs d'état complexes, et tout ce qui relève du contrat de tokens du thème.

La seule exception à la règle stricte

Les éléments dont on ne contrôle pas le markup parce qu'une bibliothèque JavaScript les rend : vanilla-cookieconsent (.cc-*), un calendrier, un carrousel, une carte. Il n'y a pas de HTML à éditer, donc leur style passe forcément par du SCSS qui cible leurs classes. C'est le seul cas où redéclarer en SCSS une propriété qui a un utilitaire est admis.

Pourquoi cette règle compte dans ce dépôt

Deux effets, en plus de la lisibilité :

  • PurgeCSS garde les utilitaires trouvés dans le markup et supprime le SCSS mort. Moins de SCSS custom donne une feuille plus petite sans réglage de safelist.
  • Contrat upstream : styles/base/ vient du template et se fait merger dans chaque projet aval. Chaque règle custom ajoutée là est une surface de conflit à chaque git merge upstream. Un utilitaire dans une vue ne crée aucun conflit.

Contrats de mise en page partages (mesures sur le live, les deux marques)

Ces valeurs ne sont pas des choix : elles sont mesurees sur gilliard.ch et chevaliers.ch et identiques sur les deux marques, donc elles vivent dans base/, jamais par theme.

  • Gouttiere de grille = 16px ($grid-gutter-width), soit 8px de padding de conteneur. Un .row du live vaut donc conteneur + 16 (1200 / 1350 / 1736) — et beaucoup de colonnes du live sont un pourcentage de CETTE largeur, pas de l'interieur du conteneur. D'ou les margin-inline: -8px (ou -4px) sur certaines grilles : ils restituent la rangee du live.
  • Ladder de l'en-tete (base/layout/_header.scss) : le live ne declare aucune hauteur, c'est 15px de padding autour du logo (5px une fois scrolle, a partir de scrollY > 20). >=1400 110/logo 80 · 1200-1399 95/65 · 560-1199 80/50 · 380-559 85/55 · <380 logo 35. Etat scrolle : 67 au-dessus de 1200. Le logo est dimensionne par la hauteur (width:auto + max-width), jamais par une largeur fixe.
  • Decalage du body : il ne suit PAS la hauteur de l'en-tete. Mesure a 375/768/1024/1280/1440/1920 — gilliard 73/79/79/95/114/114, chevaliers 77/79/79/79/114/114 (a 1280 le decalage de chevaliers est plus PETIT que sa barre). Chaque theme reproduit son ladder tel quel.
  • em et non rem. Le corps de chevaliers fait 20px et celui de gilliard 16px : un 1.2rem fige donne a chevaliers la taille de gilliard. La quasi-totalite des valeurs du live sont des em du corps de la marque — titres de cartes, champs de formulaire, legendes. Une valeur en rem dans base/** est presque toujours un bug latent.
  • Survol : le live ne change que l'ombre d'une carte (rgba(0,0,0,.16) 0 0 99px -> rgba(0,0,0,.1) 0 5px 25px), jamais sa position ; les liens de navigation ne changent rien ; les tuiles Instagram passent en opacity: .85. Verifiable avec tools/fidelity/probes/hover-sweep.mjs.

Pieges CSS qui ont produit des decalages systematiques

  • Un element flex est un contexte de formatage de bloc : les marges de ses enfants ne peuvent plus s'echapper ni fusionner. C'est +2em sur chaque carte contact (.g-card__rich) et +34px par separateur dans la frise d'evenements (.child-listing__rows etait flex). A l'inverse, le formulaire est volontairement flex pour que les marges des champs ne fusionnent PAS (le live enveloppe chaque champ dans sa propre .row).
  • Les images en ligne du live laissent ~6px sous elles (descendante). Nos boites sont des background, donc ce 6px doit etre remis explicitement — sinon chaque bande est courte.
  • Ordre de declaration a specificite egale : .g-card--listing est declare plus BAS dans _listing.scss, donc une regle .child-listing--x .g-card__title ecrite plus haut perd. Quand une regle « ne s'applique pas », verifier l'ordre avant la specificite.
  • :has() capture plus large qu'on croit : les cartes du bandeau facts portent aussi .g-card--info, donc la regle des cartes contact les a happees et a produit un depassement horizontal. Exclure explicitement (:not(.card-collection--facts)).

JavaScript

  • scripts/index.js : expose Bootstrap sur window, puis sur DOMContentLoaded -> load -> requestAnimationFrame charge en lazy modules/heroSlider + modules/cardCarousel + modules/videoHero + modules/stickyHeader + modules/reveal ; charge modules/cookiesConsent via requestIdleCallback (fallback setTimeout) pour ne pas bloquer le LCP.
  • scripts/modules/ : un module par fonctionnalite, objet avec init() (ex. heroSlider.js, cardCarousel.js, videoHero.js, cookiesConsent.js). Le module d'exemple exampleSection.js et le helper inutilise helpers/utilityHelper.js ont ete supprimes (nettoyage 2026-07-15).

Contrat des modules multi-sites : un module est charge sur les 4 marques, donc il doit etre gate par une classe-hook (.js-*) et s'auto-neutraliser quand elle est absente (if (!els.length) return;). C'est ce qui permet un index.js unique sans branchement par site.

  • scripts/helpers/motion.jsprefersReducedMotion() / onMotionPreferenceChange(). Point de passage unique pour toute animation : chaque composant Wix de gilliarday porte un reducedMotion stateRef, donc respecter le reglage OS est une exigence, pas un bonus. Lu a chaud (pas de cache) pour qu'un changement de reglage soit pris en compte sans reload.
  • scripts/modules/videoHero.js — hook .js-video-hero (emis par ctaBanner quand video est renseignee). L'autoplay est pilote en JS (data-autoplay) et non par l'attribut autoplay : avec l'attribut, le navigateur demarre la lecture avant tout script et le poster n'aurait jamais sa chance sous prefers-reduced-motion. Sous motion reduite, la video reste sur son poster et les controles sont exposes. Pause hors viewport (IntersectionObserver) : une boucle derriere une section deja depassee coute batterie et decodage pour rien.
  • scripts/modules/stickyHeader.js — hook .js-sticky-header, pilote par data-attributs pour servir les 4 marques sans copie par theme. data-min-width="992" (PDN : sticky desktop only — sous le seuil le module se detache completement et nettoie ses classes), data-transparent-until=".page-hero" (chevaliers : en-tete transparent au-dessus du hero), data-scrolled-after (defaut 1). Classes emises pour le theme : .is-scrolled, .is-transparent. Hauteurs mesurees (getBoundingClientRect), jamais supposees. L'en-tete Gilliard porte .js-site-header et pas .js-sticky-header : le module y est un no-op assume, ajouter ces modules ne doit pas toucher le seul site fini.
  • scripts/modules/reveal.js — hook .js-reveal, IntersectionObserver, ajoute .is-revealedune seule fois (unobserve ensuite : rejouer a chaque passage se lit comme un bug, et une page de listing longue reste peu couteuse). data-reveal-delay pour l'echelonnement. Le theme possede l'apparence, le module ne decide que du quand.

⚠️ L'etat cache de .js-reveal s'ecrit html.js .js-reveal:not(.is-revealed), jamais .js-reveal seul. Le theme cache ces elements et reveal.js est la seule chose qui les reaffiche : sans le garde html.js, si le module ne tourne pas (JS desactive, chunk 404, erreur plus tot dans le bundle), le contenu reste invisible sans aucune erreur. Meme raison pour laquelle reveal.js affiche tout sous prefers-reduced-motion et sans IntersectionObserver au lieu de sortir tot : sortir tot laisserait la page blanche — ici, tout mode degrade doit tomber du cote contenu visible.

Le garde est pose en inline dans le <head> de _MasterLayout (document.documentElement.classList.add("js")), pas depuis le bundle : le bundle est differe, il pourrait arriver apres le premier paint et provoquer un flash.

  • Charge dans _MasterLayout via <script src="/js/index.js" asp-append-version="true" type="module">.

⚠️ type="module" est obligatoire, ne pas repasser a defer seul. Vite construit cette entree en format: "es". Des qu'un chunk lazy en importe dynamiquement un autre, Rollup hisse un helper partage dans l'entree et lui ajoute un export de haut niveau (export{e as _}). Charge en script classique, c'est une SyntaxError qui tue tout le bundle (plus de consentement, plus de slider) — silencieusement, et uniquement dans le build de prod : le dev, non bundle, n'a pas ce export. C'est arrive en ajoutant import('./consentEmbed') dans cookiesConsent.js. Les scripts type="module" sont differes par defaut, donc defer devient inutile.

ESLint

eslint.config.js (flat-config) cible scripts/**/*.js. Regles en error : prefer-const, no-var, eqeqeq, prefer-template, no-duplicate-imports, no-implicit-globals, default-case.

Important

  • Ne jamais editer src/Web/wwwroot/css|js (genere). Editer styles/ et scripts/.
  • /css et /js sont fingerprintes (asp-append-version) et caches en immutable (voir overview.md).

Contributors

No contributors

Changelog

No recent changes