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 :
globSyncsurstyles/themes/*/main.scss; chaque theme produitwwwroot/css/<theme>/main.css(viaassetFileNames: un fichierstyles-<theme>.cssest renomme en<theme>/main.css).
- JS : entree unique
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 cesjs/styles-*.js. vite-plugin-live-reload: surveillewwwroot/css/**/*.cssetwwwroot/js/**/*.jspour 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 developmentpuischokidarre-build sur changementstyles/**/*.scssetscripts/**/*.js.npm run build: build (mode par defaut).npm run build:prod: ESLint puisvite build --mode production(une erreur de lint stoppe le build).
PostCSS
Chaine appliquee aux CSS (vite.config.js) :
postcss-sort-media-queriesautoprefixerpostcss-combine-duplicated-selectors(removeDuplicatedValues)- en prod uniquement :
PurgeCSSpuiscssnano
SCSS via api: "modern-compiler", deprecations silencieuses (if-function, global-builtin, color-functions, import).
Breakpoints responsive
Utiliser les mixins de styles/abstracts/_breakpoints.scss (@use "…/abstracts/breakpoints" as bp;) plutôt que des @media en dur : @include bp.down(xl), bp.up(lg), bp.between(md, xl). Le map $breakpoints reprend les valeurs de Bootstrap (xs 0, sm 576, md 768, lg 992, xl 1200, xxl 1400) — down() soustrait 0.02px comme Bootstrap. On ne peut pas utiliser les mixins Bootstrap directement (5.3.3 en @import legacy, non @use-able sans charger tout le framework), d'où ce fichier maison = source unique de vérité. Seuls restent en dur quelques breakpoints custom hors grille repris du site source (footer APOL 1280/980/930/600/480/408 ; méga-menu 1150).
PurgeCSS
purgeCss() (vite.config.helper.js) scanne Views/**/*.cshtml et scripts/**/*.js. Safelist : standard = /^js-/, /^is-/, /^has-/, /^u-/, /^util--/, /^btn-(lavaux|...)$/ (boutons communes footer), /^btn--/ et /^sep--/ (modificateurs de blocs construits dynamiquement dans les vues : btn--@(style), sep--@(size)) ; deep = /^cookie/. Toute classe injectee dynamiquement (par JS, par une lib, ou construite par interpolation dans une vue) doit etre ajoutee a cette safelist sinon elle sera purgee en prod. Ajouts recents : animations block-grid (/^scroll-anim$/, /^show-*, /^showed$/, /^d-primary$/) et bloc Documents (/^doc-ico/, couleur d'icone construite depuis l'extension du fichier).
Icones de type de fichier (bloc Documents) : glyphes extraits des fontes icomoon/themify des sites sources vers le sprite SVG Views/Partials/layout/_fileIconsSprite.cshtml (genere par migrations/file-icons/gen-file-icons.mjs, inclus dans _svgSprite.cshtml). Symboles icon-file-<type>, references via <use> dans des pastilles doc-ico--<type> colorees par styles/base/blocks/_documents.scss.
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 partialsbase/(header, footer, cookies, a11y, sections d'exemple). Peut surcharger viawith (...).themes/<theme>/_bootstrap_variables.scss: overrides des tokens Bootstrap par theme.base/:_bootstrap_variables.scss,layout/(_header,_footer,_cookies,_a11y,_error),blocks/(styles des blocs),sections/,elements/.
Pages d'erreur (404 / 500) : mise en page partagee Views/Partials/layout/_errorPage.cshtml, incluse par NotFoundPage.cshtml / InternalServerErrorPage.cshtml (qui passent ErrorCode/ErrorTitle/ErrorDescription via ViewData). Les deux affichent une illustration : wwwroot/images/errors/404.png (« 404 » + labyrinthe) et .../500.png (« OFFLINE » + prise debranchee), portees depuis metiersdart.ch. Le code n'est pas repete en texte (present dans l'image). Titre (ErrorTitle), description et bouton « Revenir a l'accueil » (.btn.btn--primary) communs. Styles : base/layout/_error.scss (.error-page*), @use dans les 2 themes.
Couleurs = variables uniquement. Aucune couleur en dur (hex / rgb / hsl) dans les regles SCSS. Toute couleur passe par une variable ; le seul endroit ou l'on ecrit une valeur litterale est themes/<theme>/_bootstrap_variables.scss. Pour une teinte derivee, appliquer l'alpha sur une variable : rgba(vars.$black, .1).
Bootstrap 5 d'abord (utility-first). On a Bootstrap 5 : privilegier les classes utilitaires dans le HTML (d-flex, d-block, text-white-50, border-bottom border-white border-opacity-25, row-cols-md-2, position-relative, overflow-hidden, py-3, order-md-0...) plutot que d'ecrire du SCSS. Le SCSS custom est reserve a ce que les utilitaires ne peuvent pas exprimer : decors (bokeh du footer PEL), glyphes de police d'icones, :hover/transition, white-space: pre-line, tailles fixes. Regle : si une classe Bootstrap fait le travail, ne pas creer de regle SCSS.
Themes livres dans le template : default (reference), cave, piscines (exemples de variations, dont une section card-section propre a piscines). Ce sont des exemples a adapter/supprimer dans un projet aval.
Styles des blocs (base/blocks/)
Styles de base (neutres, communs aux thèmes) des blocs BlockGrid/BlockList : styles/base/blocks/_<block>.scss agreges par styles/base/blocks/_index.scss, @use dans chaque themes/<theme>/main.scss (section // -------- BLOCKS --------). Un thème peut surcharger dans themes/<theme>/blocks/.
- Convention de classes : BEM léger
block--<type>(racine du bloc), sous-elementsblock__xou<type>__x(ex.hero__title), modificateurs--<variante>(ex.btn--primary,sep--large) et hooks JSjs-(sans style). - Les modificateurs construits dynamiquement dans les vues (
btn--@(style),sep--@(size)) sont safelistes PurgeCSS (voir section PurgeCSS). - Blocs interactifs : galerie FAITE (lightbox Splide) et slider FAIT (carrousel Splide, voir plus bas) ; onglets / accordeon via Bootstrap. JS en modules lazy
scripts/modules/blocks/. Voirdocs/migration/blocks-apol-pel.md.
Header PEL (design porte depuis pel-website)
Le header du theme pel reproduit le « mainnav » de pel-website (police-el.ch).
- Markup :
Views/Partials/layout/pel/_header.cshtml(+_sosIcon.cshtml). Desktop (.main-header-desktop,d-none d-lg-block) : logo blanc/logos/pel/main-logo.svg+ nav en pilule translucide (.mainnav-desktop) sur bande navy ($header-bg), liens.nav-link.level-1(actif = pilule blanche / texte navy), dropdowns blancs (.sub-nav), bouton SOS rouge. Mobile (.main-header-mobile,sticky-top,d-lg-none) : barre blanche (logo sombre + SOS + recherche + burger) ouvrant un offcanvas plein ecran (.mainnav-mobile). Nav =Home > En-tete(headerNavMenuLinks). - Carte SOS (
#sosCard, collapse) : numeros en dur 117/144/118/145 ; « toutes les urgences » et recherche = placeholders#. L'auto-ouverture grand ecran n'est pas portee. - Styles :
styles/themes/pel/layout/_header.scss(couleurs via_bootstrap_variables.scss). Burger « squeeze » CSS pur. JS :scripts/modules/pelHeader.js(hover dropdowns + burgeris-active), lazy dansindex.js, no-op hors pel. - Assets :
wwwroot/logos/pel/main-logo.svg+main-logo-dark.svg; glyphes themifyti-search/ti-close/ti-angle-right/ti-arrow-top-rightdansthemes/pel/_fonts.scss. - Adaptation : desktop pose sur une bande navy (
$header-bg), l'intranet n'ayant pas le hero du site public. - Méga-menu (dropdown à sections) : une entrée de menu peut avoir un sous-menu en colonnes au lieu d'une liste plate. Modèle de contenu :
navMenuElementporte, en plus denavSubmenuLinks(liste plate, conservée), une BlockListnavMenuSections(datatypeCustom - Bloc List - Nav Menu Sections, max 3) de l'element typenavMenuSection=sectionTitle(Textstring) +sectionLinks(MultiURLPicker). Rendu (_header.cshtml) : si des sections existent →<div class="sub-nav sub-nav--mega">endisplay:grid(.sub-nav--mega, hérite de.sub-navla carte blanche/ombre) avec un.sub-nav__col+.sub-nav__titlepar section ; sinon le dropdown simple actuel ; mobile = sections empilées (sous-titre + liens) dans le collapse. Priorité sections > liste plate. Le JSpelHeader.jscible.sub-nav(générique) → inchangé. Voir.claude/docs/modules/content-model.md. Cycle uSync→ModelsBuilder requis avant que la vue typée (item.NavMenuSections,NavMenuSection) compile.
Footer APOL (design porte depuis apol-website)
Le footer du theme apol reproduit fidelement celui du site public apol-website (Umbraco 13) : bandeau sombre avec image de fond (montagnes), 4 colonnes separees par des bordures cyan (logo / reseaux sociaux + adresse / horaires / liens), puis credits Spektrum.
- Markup :
Views/Partials/layout/apol/_footer.cshtml. Les donnees restent branchees sur le modele intranet : adresse / horaires / contact = champsFooter*du noeud Home ; liens =FooterLinkColumns(chaque colonne rendue avec son titre.footer-nav-title+ sa liste de liens) ; reseaux =FooterSocialLinks(la classe sociale —facebook,instagram,linkedin,twitter,youtube— est deduite de l'URL). - Styles :
styles/themes/apol/layout/_footer.scss(cible le selecteurfooter; couleurs via@use "../bootstrap_variables" as vars— pas de couleur en dur ni de variable redeclaree, y compris$footer-cyan/$footer-cities-titleajoutees au_bootstrap_variables.scssdu theme). - Assets portes depuis apol-website : police d'icones
themify-icons(wwwroot/fonts/apol/,@font-facedansthemes/apol/_fonts.scss) pour les boutons ronds sociaux, images de fondwwwroot/images/apol/footer/footer_bg-{xl,lg,sm,xs}.png, et jeux d'images des boutons communeswwwroot/images/apol/footer/{lavaux,chexbres,lutry,puidoux,rivaz,stsaphorin}[-over][@2x].png. - Section « communes / police de proximite » : bandeau de boutons communes alimente par la propriete
footerCommunesdu noeud Home (voircontent-model.md). La classe CSS commune (btn-lavaux,btn-chexbres, ...) vient du contenu, donc elle est safelistee dans PurgeCSS (vite.config.helper.js) sinon les regles.btn-*seraient supprimees en prod. - Convention d'assets par theme : polices sous
wwwroot/fonts/<theme>/, images specifiques souswwwroot/images/<theme>/....
Footer PEL (design porte depuis pel-website)
Le footer du theme pel reproduit celui du site public pel-website (StarterKit V4 / Nanoxi, Umbraco 13) : fond navy avec decor bokeh (cercles flous statiques), colonne adresse a gauche (col-md-4 : logo, horaires, contacts telephone/email/adresse avec icones et bordures basses) et colonne contenus a droite (col-md-8 : navigation sur deux colonnes + reseaux), puis barre copyright Spektrum alignee a droite.
- Markup :
Views/Partials/layout/pel/_footer.cshtml, classes de pel-website (main-footer,main-footer-container,main-footer-address,main-footer-nav,main-footer-copyright,btn-copyright) + utilitaires Bootstrap. Donnees branchees sur le modele intranet (adresse / horaires / contact = noeud Home ; liens =FooterLinkColumnsvia_footerColumns.cshtml, wrappes dansrow row-cols-1 row-cols-md-2; reseaux =FooterSocialLinksvia_footerSocial.cshtml). - Bokeh :
.footer-bokeh(gradients) +.footer-bokeh__field spanx10 (cerclesradial-gradient+box-shadowglow,blur). Statique (les animations ont ete desactivees cote pel-website). Couleurs bokeh dans_bootstrap_variables.scss($bokeh-*). - Icones de contact : polices
themify-icons(.ti-email\e75a,.ti-location-pin\e693) etfont-icon/icomoon (.ico-phone\e60a), portees depuis pel-website (wwwroot/fonts/pel/*.woff2,@font-face+ glyphes dansthemes/pel/_fonts.scss). - Styles :
styles/themes/pel/layout/_footer.scss(cible.main-footer; couleurs via_bootstrap_variables.scss). Nav : titresletter-spacing+ blanc (majuscules via l'utilitairetext-uppercasedu partial), liens hovertranslateX(6px). Copyright : SVG blanc opacite 0.5 -> 1 au survol. - Non porte : les widgets Nanoxi (navigation et newsletter) ; la navigation est remplacee par
FooterLinkColumns, la newsletter est omise.
Header APOL (adapte d'apol-website)
Le header du theme apol est un port fidele (structure + valeurs exactes) du header d'apol-website. Markup : Views/Partials/layout/apol/_header.cshtml (classes de reference : header-container, top-header, left-side, logo + logo-up/logo-sticky, menu-navbar, min-logo-area, container-fluid, social-nav, nav-link-search). Styles : styles/themes/apol/layout/_header.scss (couleurs via _bootstrap_variables.scss). Reseaux = FooterSocialLinks ; menu = _headerNav.cshtml.
On reprend la version standard (page non-home) d'apol-website — pas la home (700px / logo agrandi / menu a 618) qui est un cas particulier a ne pas utiliser ici.
Desktop (>= 992px) — valeurs exactes mesurees sur une page non-home (Contact) d'apol-website :
- Cover :
header-containerde 420px. Image saisonniere aleatoire (54 images souswwwroot/images/apol/cover/{spring,summer,winter}/bg-1..18.jpg, saison selonDateTime.Now.Month) enbackgroundsur le header. - Logo :
.top-header(fixed, height 0) >.left-side(220x220,overflow:hidden,padding-left:20) >.logo(absolute top/left 20, ~150px au repos, sans scale). Le logo est dimensionne parmax-width:100%de la.left-side: quand elle retrecit, le logo suit. - Barre :
.menu-navbar(relative,top: 338px,padding-left: 130px,min-height: 82px) avec.min-logo-area(encoche 130px,translateX(-100%),::beforequi se remplit) +.container-fluid(absolute,width: calc(100% - 130px), fond$secondary) -> barre decalee, pas pleine largeur..nav-link:padding: 29px 16px 21px,line-height: 32px,font15px majuscules -> 82px.
Animation au scroll (scripts/modules/header.js, classes sur .js-header, safelist /^is-/) — seuils exacts d'apol-website :
is-logo-sm(>=70) : le.logo-sticky(rond,logo-desktop-2.svg) apparait en fondu par-dessus le.logo-up(logo-desktop.svg).is-logo-xs(>=160) :.left-side-> 122x112 (le logo passe de ~150px a 102px),.logo top: 10.is-logo-bg(>=240) : l'encoche.min-logo-area::beforese remplit ($header-social-bg).is-stuck(affix, >=338) : tout le header passeposition: fixed,header-container-> 82px,menu-navbar top: 0. Lemainrecoit unmargin-topde 420px pour eviter le saut (pose par le JS).
Méga-menu (dropdown à sections) — porté d'apol-website. Quand une entrée de menu a des navMenuSections (cf. content-model.md), _headerNav.cshtml rend <li class="nav-item dropdown full-width position-static"> + <div class="dropdown-menu dropdown-columns ..."> avec un <ul> par section (titre li.level-3 + liens a.dropdown-item). Panneau gris pleine largeur ($menu-hover-bg = #eeeeee), colonnes float:left; width:20% (5 par ligne), ouverture au hover (desktop, visibility/opacity) ; mobile = dropdown Bootstrap empilé par défaut (.show). Détails fidèles au source (vérifiés sur les computed styles en ligne) : liens en text-transform: lowercase + ::first-letter majuscule, padding-bottom: 15px, titres .level-3 18px/#151515 ; responsive <1150px (liens 14px, titres 17px). Bootstrap-first : mise en page (w-100 start-0 border-0 rounded-0 clearfix list-unstyled position-static) en classes dans le markup ; le SCSS (_header.scss) ne garde que le non-mappable (couleurs, paddings custom, width:20%, et position/display/float responsive — utilitaires !important non responsive).
Mobile (< lg) — porté d'apol-website. Le chrome desktop est masqué (d-none d-lg-block) et apol/_headerMobile.cshtml (d-lg-none) rend une barre fixe 54px (logo + recherche + burger navy \e68e → croix \e646) au-dessus d'un drawer plein largeur (.dl-menu-container, révélé par .dl-show). Navigation multi-niveaux par glissement horizontal : le JS scripts/modules/apolMobileMenu.js (vanilla, lazy, no-op hors APOL) échange le contenu d'un unique ul.dl-menu et empile l'historique, en animant translateX (.dl-animate-* / keyframes apolMove*). Les sections méga (li.level-3) sont regroupées en un niveau supplémentaire (Titre → Sections → Liens) ; un bouton RETOUR bleu (li.dl-back) est injecté à chaque niveau. Fermeture : re-clic burger, clic extérieur, Échap. Le drawer/burger/.dl-* sont stylés en SCSS (markup piloté par JS + icônes themify en pseudo-éléments) ; le chrome statique utilise des utilitaires Bootstrap. Alimenté par le même navMenuElement (navSubmenuLinks / navMenuSections) que le desktop.
Popup urgence (desktop + mobile) — portée d'apol-website. apol/_emergencyPopup.cshtml = dropdown Bootstrap natif paramétré par @@model bool (isMobile) ; data-bs-display="static" désactive Popper (positionnement en CSS). Déclencheurs (SVG source copiés dans wwwroot/images/apol/) :
- mobile :
emergency-btn-mobile.svg(combiné téléphone rouge sur fond transparent) dans la barre mobile entre recherche et burger → panneauposition:fixedpleine largeur sous la barre 54px. - desktop :
emergency-btn.svg(téléphone blanc) sur un bouton rouge$danger82px collé à droite (margin-left:auto) de la barre de nav → dropdownposition:absolutealigné à droite (min-width:380px). Flèche d'appel\e62a(pseudo-élément, le thème apol ne définit pas les classes.ti-*) → croix\e646à l'ouverture ; ondes « pulse » animées au survol (.btn-emergency-pulse, keyframeanim-dash-pulse).
Numéros en dur (verbatim source : Police Lavaux 021 791 11 21 ; 117/144/118/145/1414/143 + Médecin de garde + MalleyPrairie), aucun JS custom (Bootstrap Dropdown). Bandeaux rouge $danger / bleu $info, numéros #192f89 avec soulignement animé. Styles dans _header.scss (.dropdown-emergency partagé + variantes .site-header--apol / .site-header--apol-mobile).
Non porte : popup recherche (le bouton nav-link-search est visuel).
- Styles :
styles/themes/apol/layout/_header.scss(couleurs via_bootstrap_variables.scss, coverbackground-size: cover). Reutilise la policethemify-iconsdeja chargee pour le footer. - Le drawer mobile est porté en glissement 2D (le
perspective3D d'origine était vestigial).
Hero de page (bloc manuel) et bokeh fixe PEL
Fidele a pel-website, le hero n'est pas automatique : c'est le bloc heroBlock (sous-titre + titre + texte + image/couleur de fond + lien, tous saisis par l'editeur), place dans une BlockList dediee « Heros » portee par genericSection, au-dessus du BlockGrid blocks. Sur la page generique le rendu est donc [ Heros ] puis [ Blocs ]. Voir modules/content-model.md.
- Rendu :
sections/genericSection.cshtmlsort le hero (Model.Value<BlockListModel>("hero")) en pleine largeur viablocklist/default.cshtml->blocklist/Components/heroBlock.cshtml, puis les blocs dans.section-body.bg-white(.container-fluid py-5). - Vue du bloc :
.block--hero(Bootstrap-first :text-white py-5,container),.hero__subtitle(eyebrowtext-uppercase small fw-bold opacity-75),.hero__title(h1 fw-light),.hero__text, lienbtn btn-primary. Style dansbase/blocks/_hero.scss(transparent par defaut : seul leletter-spacingdu subtitle et lemax-widthdu texte y sont ; une image/couleur editeur le rend opaque). - Bokeh fixe PEL (mecanisme du
.bokeh-stagede pel-website) :pel/_bokeh.cshtml(.site-bokeh+__field10span) rendu par_MasterLayoutavant le header ;pel/layout/_siteTop.scssle pose enposition: fixed; inset: 0; z-index: -1(gradient exactradial(#02285a) + radial(#011e44) + linear(#02285a->#071839)+ cercles identiques au footer). Le header desktop devient transparent (.main-header-desktop { background: transparent },@useen dernier danspel/main.scsspour gagner en ordre ; le meme bloc force aussioverflow: visiblecar le port du header le clippait enoverflow: hiddenpour contenir son.header-bokeh— bokeh desactive ici, le clip ne rognait donc plus que les dropdowns du menu) et le hero est transparent : header + hero laissent voir un seul fond bokeh continu. Le.body-contentest transparent ; le fond blanc vit sur.section-body(couvre le bokeh sous le hero). _MasterLayout: plus de.containerinterne autour de@RenderBody()(chaque page/section gere sa largeur : le hero est pleine largeur,SearchPage/_errorPageont deja le leur).apol/_bokeh.cshtmlest vide (APOL a son cover-header, pas de bokeh).
Blog (listing + cartes par theme)
Le blog (blogPage -> articlePage, voir modules/content-model.md) est rendu serveur. BlogPage.cshtml filtre par tag (?tags=, OR), trie (blogSortOrder), decoupe selon blogPageSize + blogDisplay, et rend une grille .js-blog-grid de cartes. Les cartes reprennent fidelement le markup + style des sites de reference (rien d'invente) :
- PEL :
layout/pel/_articleCard.cshtml= structurefeedde pel-website (feed-cover16:9,feed-bodydate/tags/titre/intro). Style porte dansthemes/pel/layout/_blog.scss(zoom.feed-cover imgau hover du.feed-link). - APOL :
layout/apol/_articleCard.cshtml= structurecardd'apol-website (card-img-overlayen bas). Style porte dansthemes/apol/layout/_blog.scss(degrade bleu#13134fmontant, date jaunergb(228,210,26), titre blanc, image13emcover, hover scale). - Choix du partial par theme via
Html.ResolveTheme()(helperExtensions/ThemeExtensions.cs: Site:Theme + cookiedebug-themeen Development, meme logique que_MasterLayout). - Infinity scroll :
scripts/modules/blog.js(lazy-load) ; le bouton.js-blog-load-morefetch?page=N+1et append les.js-article-card. Les filtres tags et la pagination marchent sans JS (liens?tags=/?page=). - Pieges Razor rencontres : une variable nommee
pagecasse la generation (@page= directive) -> nommercurrentPage;@x@(y)colle deux expressions et corrompt la classe -> pre-construire les URLs en C# ;Umbraco.GetDictionaryValue(key, "fallback")n'existe pas (2e arg =CultureInfo) -> utiliserGetDictionaryValueOrDefault(key, altText).
Icônes SVG — sprite + <use>
Convention : aucune icône SVG en double ni en inline répété. Chaque icône reutilisable est definie une seule fois comme <symbol id="icon-..."> dans Views/Partials/layout/_svgSprite.cshtml, rendu une fois en tete de <body> par _MasterLayout. On la reference ensuite par <svg class="..." viewBox="..." aria-hidden="true"><use href="#icon-..." /></svg>.
- Symbols existants :
icon-spektrum(logo, reference 3x : footers apol/pel +_footerBottom),icon-search(loupe navbar APOL),icon-sos(bouton urgences PEL). - Couleur : le CSS externe ne traverse pas le
<use>(shadow tree). Le<symbol>ne porte donc pas defill; la couleur vient du<svg>referençant, par heritage :fill="currentColor"(search), attributfill="#BBB"(logo APOL) ou une regle CSS sur le<svg>et non surpath(.svg-logo { fill: white }en PEL). L'exceptionicon-sosgarde sesfill/strokefixes dans le symbol. - Pas de font-icon pour une icone deja disponible en SVG : l'icone de recherche APOL etait doublee (SVG inline + glyphe themify
::after \e610) ; le::aftera ete supprime, seul le SVG (via sprite) reste, dimensionne par.nav-link-search__icon { width/height: 1.4em }.
JavaScript
scripts/index.js: expose Bootstrap surwindow, puis surDOMContentLoadedchargemodules/header(sticky header, promptement) etmodules/cookiesConsentviarequestIdleCallback(fallbacksetTimeout) pour ne pas bloquer le LCP.scripts/modules/: un module par fonctionnalite, objet avecinit()(ex.header.js= header sticky APOL,cookiesConsent.js,searchFilter.js= filtre par type de la page de recherche, no-op ailleurs).- Événements :
scripts/modules/events.js(lazy) — agenda FullCalendar + carte Google Maps depuis le JSON inline#events-data; bascule?view=, filtres?y=/?cat=/?commune=; cléGoogleMaps:ApiKey(appsettings). Carteslayout/{theme}/_eventCard.cshtml(styles des cartes blog).- Plugins FullCalendar :
daygrid(Mois),timegrid(Semaine/Jour) etmultimonth(Année). Pas de pluginlist: la vue Liste de la page est rendue côté serveur (timeline), unlistMonthferait doublon. headerToolbar: l'espace sépare les éléments, la virgule les regroupe en button group. Écrireprev,title,nexttente de plier le titre dans un groupe de boutons et casse la ligne — utiliserprev title next.- FullCalendar 6 injecte son CSS au runtime : ses classes n'apparaissent jamais dans le CSS compilé. Nos surcharges
.fc-*vivent dansstyles/base/blocks/_eventsTimeline.scss(safelist PurgeCSS/^fc-/).
- Plugins FullCalendar :
- Galerie (lightbox Splide) :
scripts/modules/blocks/gallery.js(lazy). La grille reste rendue serveur (galleryBlock.cshtml,.block--gallery) ; quand l'editeur active la lightbox, la vue ajoute.js-gallery+data-lightboxsur chaque lien. Le module attache les clics ; Splide et son CSS ne sont importes qu'au premier clic (import("@splidejs/splide")+@splidejs/splide/css), et s'ouvrent en overlay plein ecran (.gallery-lightbox, fermeture croix / Escape / clic backdrop). Splide sort en chunks separes (js/chunks/splide.esm.js,css/splide/main.css) charges a la demande — pas dans les bundles de theme. Le CSS lib traverse PurgeCSS -> safelist/^splide/(vite.config.helper.js). Styles overlay dansstyles/base/blocks/_gallery.scss(markup Splide au runtime -> SCSS justifie). - Slider (carrousel Splide) :
scripts/modules/blocks/slider.js(lazy). Le markup.splide.js-sliderest rendu serveur (sliderBlock.cshtml) avec les options carrousel dansdata-splide(JSON construit côté serveur : type loop/slide, perPage + breakpoints, autoplay, fixedHeight, gap). Le module importe Splide + son CSS (@splidejs/splide+@splidejs/splide/css) seulement s'il y a un.js-slidersur la page, puisnew Splide(el, options).mount(). Réutilise les chunks Splide de la galerie ; safelist/^splide/. Stylesstyles/base/blocks/_slider.scss. - Animations block-grid :
scripts/modules/scrollAnim.js(lazy) — révèle les items.scroll-animvia IntersectionObserver (toggleshowed, rejoue sishow-loop, fallback = tout afficher). Les classes viennent des réglages du bloc (ButtonPickeranimation/delay =scroll-anim show-up/show-delay-*,isLoopAnimation→show-loop,isMobileFirstPosition→d-primary) émis sur le wrapperblockgrid/items.cshtml. Styles :styles/base/blocks/_scrollAnim.scss. Classes issues du contenu → safelistées (scroll-anim,show-*,showed,d-primary). Voirdocs/migration/blocks-apol-pel.md. - Annonces (pop-up / bande) :
scripts/modules/announcement.js(lazy, sur.js-announcement, no-op sans annonce). Le markup est rendu serveur par thème (layout/{apol,pel}/_announcement.cshtml) ; le module décide de l'affichage selon les fermetures mémorisées, puis enregistre la fermeture.oncePerVisitor→ localStorage (masquée durablement sur l'appareil),oncePerVisit→ sessionStorage (réapparaît à la session suivante), clé communeannouncement_closed_ids. Pop-up = modale Bootstrap (bootstrap.Modal) ; bande = retrait ded-none+ classehas-announcementsur<body>, repli géré nativement par Bootstrap collapse. SCSS structurel partagéstyles/base/blocks/_announcement.scss(couleur de bande =var(--bs-danger), donc rouge propre à chaque thème). La modale étant montée en JS, ses classes runtime (modal-backdrop,modal-open,show,fade) sont safelistées dansvite.config.helper.js. - Vacances (calendrier) :
scripts/modules/blocks/vacations.js(lazy, sur.js-vacations-calendar, no-op sans calendrier). Réutilise FullCalendar (mêmes chunks que les événements) : chaque période = un événement journée entière ; clic → popup de détail construit en JS (vacations-popup, fermeture croix / Escape / clic extérieur), sans navigation. Données JSON inline (#vacations-data-<key>). La vue timeline du même bloc est rendue serveur (aucun JS). SCSSstyles/base/blocks/_vacations.scss; safelist PurgeCSS/^vacations-popup/. scripts/helpers/utilityHelper.js: utilitaires partages.- Charge dans
_MasterLayoutvia<script src="/js/index.js" asp-append-version="true" defer>.
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). Editerstyles/etscripts/. /csset/jssont fingerprintes (asp-append-version) et caches en immutable (voiroverview.md).

