décors ponctuels (oiseaux, fleurs) sur home, pages internes et footer

This commit is contained in:
2026-06-05 19:34:15 +02:00
parent 0f1e8bab18
commit 8e5a0c936e
16 changed files with 580 additions and 33 deletions

162
CLAUDE.md Normal file
View File

@@ -0,0 +1,162 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository layout
This directory (`src/`) is a **git submodule** of the parent `docker-erable` repo. It holds the Drupal 10 application; the parent repo holds the Docker environment (`Docker/`, `docker-compose.yml`, `Makefile`, `ressources/db.sql`).
- `src/` (this repo, `drupal-erable.git`) — Drupal codebase, custom theme & modules, exported config.
- `../` (parent repo, `docker-erable.git`) — Docker setup, DB seed, Makefile commands.
Branches: `master` (dev), `stage` (preprod), `prod` (prod). A `refonte-graphique` branch is currently active for the ongoing visual redesign — **all work happens there**, not on `master`. `git push origin master` from `src/` will appear up-to-date even after committing on `refonte-graphique`; push the branch by name.
The parent repo references the submodule under its own branch set (`master`, `prod`). After pulling parent changes, run `git submodule update --recursive --checkout`.
## Commands
All commands are run **from the parent directory** (`docker-erable/`), never directly from `src/`. Never use `docker-compose` directly — always go through the Makefile.
```sh
make build # first-time build (uses host UID/GID via build args — do not skip)
make up # start the stack (php, nginx, mysql, redis, phpmyadmin)
make down # stop
make logs # follow logs
make ps # status
make exec_php # shell into the PHP container (where drush lives)
make exec_mysql # shell into the MySQL container
make dump_db # dump erable DB to ressources/erable-<date>-local.sql
```
After pulling code changes that affect Drupal config/schema:
```sh
make maj_config # composer install + drush updb + drush cim + drush cr + drush pag
```
Individual Drupal ops (also available as Make targets):
```sh
make composer_install # composer install --no-dev in the php container
make updb # drush updb -y
make cim # drush config-import -y
make cr # drush cr (cache rebuild)
make pag # drush pag all for nodes & taxonomy terms
```
For ad-hoc drush use, `make exec_php` then run `drush ...` from `/var/www/erable/`.
### Drush gotcha (Make targets broken)
The Make targets that wrap drush (`cr`, `cim`, `updb`, `crd`, `pag`, and the composite `maj_config`) are currently broken because the container's global `drush` is the Drush Launcher, which errors out against Drush 12+ (`Drush Launcher is not compatible with Drush 12+`).
Until the parent `Makefile` is fixed, call the vendor binary directly:
```sh
docker exec erable-php-1 /var/www/erable/vendor/bin/drush cr
docker exec erable-php-1 /var/www/erable/vendor/bin/drush cim -y
docker exec erable-php-1 /var/www/erable/vendor/bin/drush updb -y
```
Inside an `exec_php` shell, use `vendor/bin/drush` (or `./vendor/bin/drush` from `/var/www/erable/`) — not the bare `drush`.
### When to run drush cr
**Every Twig template edit must be followed by a cache rebuild**, otherwise Drupal keeps serving the previously rendered markup. This applies to anything under `web/themes/erabletheme/templates/` and any custom module template (e.g. `web/modules/custom/erable_mod/templates/`). SCSS/CSS changes do NOT require `drush cr` (the CSS file is served as-is by Drupal). Run:
```sh
docker exec erable-php-1 /var/www/erable/vendor/bin/drush cr
```
after every `.twig` edit, then hard-reload the browser.
Site URL (via reverse proxy on the host): `http://dev.erable.fr` (container exposed on `:8980`). phpMyAdmin on `http://dev.phpmyadmin.erable.fr` (`:8981`). See parent `README.md` for the nginx/apache vhost snippets.
## Theme build
The custom theme is `web/themes/erabletheme/`. SCSS is compiled to CSS via **Dart Sass** (`sass` package, not `node-sass`):
```sh
cd web/themes/erabletheme
npm install # first time only
npm run sass # runs: sass --watch --no-source-map scss/styles.scss:css/styles.css
```
The compiled `css/styles.css` is what Drupal serves (see `erabletheme.libraries.yml`). The watch must be running while editing SCSS, or styles won't update. If `npm run sass` ever appears to output into `scss/` itself, the script has been mis-set — the entry must be the `IN:OUT` form `scss/styles.scss:css/styles.css`, not two positional args.
Compile failures surface in the rendered page as garbage in `body::before { content: "..." }` (the Sass watcher injects the error there). To see them from a script: `curl -s http://dev.erable.fr | grep -A2 'body::before'`.
## Custom code
- **`web/themes/erabletheme/`** — base theme: `stark`. SCSS entry is `scss/styles.scss`, which imports `global/`, `partials/`, and per-page files (`_home.scss`, `_projets.scss`, `_actualites.scss`, …). Twig templates override blocks, regions, nodes and views; node templates live under `templates/node/` per content type (`actualite`, `projet`, `partenaire`, `equipe`, `ressource`, `meetup`). The single JS file is `js/erabletheme.js`. Slick carousel is vendored under `librairies/slick-1.8.1/`. Rellax is also vendored under `librairies/` (powers the parallax background).
- **`web/modules/custom/erable_mod/`** — site-specific module (routes, blocks, controllers under `src/`, templates under `templates/`). Notable: `SitesMap` block (`src/Plugin/Block/SitesMap.php`) renders project markers on the home map; its SVG marker template is at `assets/svg/point.svg` and the Twig wrapper at `templates/svg-mapsites.html.twig`.
- **`web/modules/custom/loginregisterblock/`** — combined login + register block.
## Theme design system (post-refonte)
The redesign on `refonte-graphique` introduces a token-driven design system. **All typography sizes are in `rem`**, never `px` (one exception: deliberate "magic numbers" tied to a slick-carousel structural quirk in `_carousel.scss`, commented as such).
### Tokens (single source of truth)
- `scss/global/variables/_colors.scss` — brand colors. `$fluo_green: #33ffc4` is the new accent; `$teal` is aliased to `$fluo_green` so legacy `$teal` usages propagate automatically. Other tokens: `$page_bg: #f9f9f9` (site background), `$text_grey: #4a4a49`, `$dark_green`, `$beige`. Gradient mixins `beige_gradient()` and `white_beige_gradient()` live here too.
- `scss/global/variables/_typography.scss` — font-size scale + line-heights + weights + semantic mixins. All sizes in `rem`:
- `$fs_xs: 0.8125rem` (≈13px), `$fs_sm: 0.9375rem` (≈15px), `$fs_md: 1.0625rem` (≈17px), `$fs_lg: 1.375rem` (≈22px), `$fs_xl: 2.25rem` (≈36px), `$fs_2xl: 3.5625rem` (≈57px).
- Mixins (**use these, do not hardcode font properties**): `main_title()`, `sous_titre()`, `sous_titre_alt()` (intermediate Vogun 22→36px), `main_text_content()`, `meta_text()`, `cta_text()`, `fluo_button()` (the universal CTA — propagates to `.more-link` and `footer.fluo_links` site-wide).
- `scss/global/_fonts.scss``@font-face` declarations. **Vogun** (titles/CTA, `fonts/Vogun/Vogun-Regular.woff2`) and **Marianne** (body) are the only typefaces. Barlow has been removed.
- `scss/global/variables/_layout.scss` — breakpoints: `$breakpoint_tablet: 760px`, `$breakpoint_desktop: 1080px`, `$breakpoint_desktop_large: 1600px`. Plus `$x_margin` for horizontal page padding.
### Responsive philosophy
Mobile-first via `vw` for proportional sizing + `max-width` (or aspect ratios) to cap on large screens. Avoid hard `px` breakpoints in component sizing — use the breakpoint variables for layout shifts only.
### Refonte branch conventions
- This is a redesign branch, so **replace** legacy code rather than stacking new styles on top. Removing the old `$teal` value, renaming Barlow out, deleting unused SVGs (e.g. `feuille.svg`) is the norm here.
- Semantic mixin names (`main_title`, `sous_titre`) are kept even when their visual content is overhauled — this preserves call-sites across all per-page SCSS files.
- It's OK to edit Twig block templates (e.g. `block--views-block--home-blocks-block-1.html.twig`, `block--sitesmap-block.html.twig`) when restructuring layouts. Avoid editing core templates like `node.html.twig` or `field--body.html.twig` to limit side effects.
- The CTA "En savoir plus" on the home is reparented by JS (see `js/erabletheme.js``intro.appendChild(moreLink)`) because `smart_trim` renders `.more-link` deep inside `article > div`. This is intentional and lets the separator stop at text-end instead of button-end.
### Décors parallax v2
All decor systems are JS-driven (injection + positioning + parallax where applicable). Common entry points live in `Drupal.behaviors.erabletheme.attach` inside `erabletheme.js`; positions recompute on `window.load`, `window.resize` (debounced 200ms). Body scroll is on `body` (not `html`) — see `_global.scss` (`body { overflow: scroll }`, `html { overflow: hidden }`). Rellax's `wrapper: 'body'` and the custom parallax scroll listener both depend on this.
Assets live in `assets/new-bg-shapes/` (frises, mid-left, home-diapo) and `assets/drawings/` (oiseaux, fleurs ponctuels).
1. **Side frises** (`.bgImg.decor-left/right`) — single `.decor-tile` in `page.html.twig` cloned by JS (`setupBackgroundTiles`) to fill `layout-container.offsetHeight`, then animated by Rellax (`new Rellax('.bgImg', { wrapper: 'body' })`). SCSS: `_background.scss` (positions in `%` of tile). `.decor-once` images (cut-at-top) are removed from clones.
2. **Mid-left column decors** (`.col-decor`, `mid-left-1.png` + `mid-left-2.png`) — `setupColDecor` / `positionColDecor` / `applyColDecorParallax`. Two cibles:
- `.fullpage:not(.large-container)` (internal pages): stack of up to 2 PNG at the bottom-left outer edge of the column. Sizes + gaps proportional to colH, min/max clamped. Display thresholds: 50% colH for both, 30% for one.
- `.map-projets` (home "projets" block): single PNG via `singleOnly: true`, threshold 50%.
Custom parallax (subtle upward translate, capped ±30px). Bord droit of PNG calé on bord gauche extérieur via `translateX(-100%)`. Visibility responsive: `_background.scss`.
3. **Footer ornements** (`.footer-decor`, `bird-3.png` + `flower-2.png`) — Twig in `page.html.twig` inside `<footer>`. CSS-only positioning in `_footer.scss`: `position: absolute; bottom: 100%` posed on the footer top edge. Footer raised to `z-index: 3` (cf. `_global.scss`) so the decors overlay the column bottom. `pointer-events: none`.
4. **Random page decors** (`.page-decor`) — `setupPageDecor` / `positionPageDecor`. One PNG per `.fullpage` (excluding `aside .fullpage` sidebars), tirage déterministe par colonne (index stocked on `col.dataset.pageDecorIdx`). Pool: `bird-1, bird-2, flower-1, flower-3` (flower-2 + bird-3 réservés au footer). Width by breakpoint, anchor switches to `.page-header-outside` on mobile. `flower-3` overlaps 40% inside (vs 20% default) because of thin stem.
5. **Home decors above carousel** (`.home-above-decor`, `flower-1` + `bird-1`) — `setupHomeAboveDecor`. Static (no parallax), debord `overlap` px on the carousel cards top edge, horizontal `offsetVw` from viewport edges.
6. **Home decors on carousel** (`.home-diapo-decor`, `home-diapo-left.png` + `home-diapo-right.png`) — `setupHomeDiapoDecor`. Anchored to `.slick-list` top (not `.carousel_container` to avoid catching the dots). Custom parallax shared with col-decor (same `applyColDecorParallax` siblings: `applyHomeDiapoParallax`). Width × `decorWidthScale()` (1× desktop, 1.5× tablet, 1.8× mobile).
7. **Home decors around projets block** (`.home-projet-decor`, `bird-2` + `flower-3`) — `setupHomeProjetDecor`. Anchored to `.map-projets`. `bird-2` at top-left (debord on the block left edge in desktop; `-3vw` from viewport in mobile/tablet). `flower-3` truncated by the viewport right edge (`right: -20px`), centered at 1/3 from block top. Static (no parallax).
The `decorWidthScale()` helper (mobile 1.8× / tablette 1.5× / desktop 1×) is shared by home-diapo, home-above, home-projet to keep widths visually comparable across screen sizes. Footer decors do NOT use it (pure CSS).
## Drupal configuration
- Exported config lives in `config/sync/` (and `config/tmp/`). Edits to config in the UI must be exported with `drush cex -y` and committed; conversely, after pulling config changes, run `drush cim -y` (via the vendor binary — see Drush gotcha).
- The project uses `wikimedia/composer-merge-plugin` to merge `web/profiles/drupal-starterkit-profile/composer.json` (itself a submodule from `figureslibres.io`).
- Drupal core is `^10.2`. Key contrib modules: `paragraphs`, `webform`, `panels`/`page_manager`, `geofield`+`leaflet` (project map), `smart_date`, `smart_trim` (responsible for `.more-link` rendering deep inside fields — see refonte conventions), `social_media_links`, `view_unpublished`.
## Database
- DB is auto-seeded from `${DB_IMPORTE_FILE}` (set in `.env` in the parent repo) on **first** `make up` only. To reload a dump later, copy the SQL into the mysql container and re-import manually — see "Updating manualy the mysql db" in the parent README.
- Credentials default to `root` / `erable` (DB name `erable`).
## Twig debugging
Twig theme debug is currently ON in this dev environment: every rendered region/block/field has `<!-- FILE NAME SUGGESTIONS -->` HTML comments listing candidate template names in priority order. To find which template controls a region of the page: View Source → search for the markup → read the suggestions above it. The active template is the first one that exists.
## Patterns to know
- Do not commit `web/sites/default/settings.local.php`, `salt.txt`, or the `files/` directory — they are environment-specific (copies of templates live in parent `ressources/drupal/`).
- The `php` container runs as the **host user** (USER_UID/USER_GID baked at build time) so file ownership stays clean. If you re-`make build` on a different machine, files written by the container will switch UID.
- xdebug is wired (see `Docker/php-8.1-fpm/` and the `XDEBUG_INI` mount). `host.docker.internal` resolves to the host on Linux via the `extra_hosts` mapping.
- Penpot is the design source of truth for the refonte. Frames of interest in the connected file: `1 - accueil - integration` (id `8997888b-7408-8014-8008-0cf456861e09`) for the home, and `3 - Single projet` (id `cb2f4013-441f-800a-8007-d81a04408009`) for the side-decor reference. Use `mcp__penpot__export_shape` to pull PNG/SVG references.

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

View File

@@ -79,7 +79,7 @@ body .layout-container main {
} }
} }
body .layout-container footer { body .layout-container footer {
z-index: 0; z-index: 3;
} }
body .layout-container #background { body .layout-container #background {
z-index: -1; z-index: -1;
@@ -399,6 +399,7 @@ footer.fluo_links p {
margin-top: 50px; margin-top: 50px;
padding: 1rem 3vw; padding: 1rem 3vw;
box-sizing: border-box; box-sizing: border-box;
position: relative;
} }
@media (min-width: 1080px) { @media (min-width: 1080px) {
.layout-container > footer { .layout-container > footer {
@@ -407,6 +408,33 @@ footer.fluo_links p {
padding: 1rem; padding: 1rem;
} }
} }
.layout-container > footer > .footer-decor {
position: absolute;
bottom: 100%;
width: 18vw;
height: auto;
transform: translateY(15%);
pointer-events: none;
}
@media (min-width: 1080px) {
.layout-container > footer > .footer-decor {
width: 8vw;
}
}
.layout-container > footer > .footer-decor-bird {
left: 0;
}
.layout-container > footer > .footer-decor-flower {
right: 0;
}
@media (min-width: 1080px) {
.layout-container > footer > .footer-decor-bird {
left: -4vw;
}
.layout-container > footer > .footer-decor-flower {
right: -4vw;
}
}
.layout-container > footer #footer_middle { .layout-container > footer #footer_middle {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -1182,11 +1210,12 @@ main.main-login .login > div > div:not(.hidden) form .button:hover, main.main-lo
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 5vh;
} }
@media (min-width: 1080px) { @media (min-width: 1080px) {
.map-projets-section { .map-projets-section {
margin-top: 10vh; margin-top: 10vh;
margin-bottom: 10vh; margin-bottom: 4vh;
} }
} }
@@ -1441,6 +1470,11 @@ main:not(:has(#block-erabletheme-leprogramme-2)):not(:has(#block-erabletheme-vie
border-bottom: 7px solid #33ffc4; border-bottom: 7px solid #33ffc4;
position: relative; position: relative;
} }
@media (min-width: 1080px) {
.layout-content .fullpage {
margin-bottom: 4vh;
}
}
.layout-content .fullpage h2 { .layout-content .fullpage h2 {
margin-top: 5vh !important; margin-top: 5vh !important;
margin-bottom: 7vh; margin-bottom: 7vh;
@@ -1492,7 +1526,7 @@ main:not(:has(#block-erabletheme-leprogramme-2)):not(:has(#block-erabletheme-vie
color: #4a4a49; color: #4a4a49;
line-height: 1.3; line-height: 1.3;
} }
.layout-content .fullpage .fullpage_content p:first-of-type { .layout-content .fullpage .fullpage_content :is(p, h1, h2, h3, h4, h5, h6):first-child {
margin-top: 7vh; margin-top: 7vh;
} }
.layout-content .fullpage .fullpage_content .liens_fixed > div > div, .layout-content .fullpage .fullpage_content .file_fixed > div > div, .layout-content .fullpage .fullpage_content .liens_fixed > div > div, .layout-content .fullpage .fullpage_content .file_fixed > div > div,

View File

@@ -440,14 +440,14 @@
} }
// //
// Décors mid-left : 0, 1 ou 2 PNG empilés au pied gauche extérieur // Décors mid-left (col-decor) : PNG empilés au pied gauche
// de chaque .fullpage interne (pages non-index). Tailles et gaps // extérieur d'une colonne. Position absolue dans .layout-container,
// proportionnels à colH, bornés min/max. On n'affiche un motif
// que si la pile tient sous 50% colH (les deux) ou 30% (un seul),
// sinon la forme déborderait dans le contenu en haut.
// Position absolue calculée en px dans .layout-container ;
// bord droit du PNG calé sur bord gauche colonne via // bord droit du PNG calé sur bord gauche colonne via
// translateX(-100%). Parallax custom plus bas (sans Rellax). // translateX(-100%). Tailles + gaps proportionnels à colH (bornés
// min/max). Affichage conditionnel : seuils 50%/30% colH.
// - .fullpage:not(.large-container) : pile de 2 (mid-left-1+2).
// - .map-projets (home) : 1 seul (mid-left-1) via singleOnly.
// Parallax custom (sans Rellax) plus bas.
// Visibilité responsive : _background.scss. // Visibilité responsive : _background.scss.
// //
const layoutContainer = document.querySelector('.layout-container'); const layoutContainer = document.querySelector('.layout-container');
@@ -455,6 +455,17 @@
function clamp(min, val, max) { return Math.max(min, Math.min(val, max)); } function clamp(min, val, max) { return Math.max(min, Math.min(val, max)); }
// Coefficient appliqué aux décors home (oiseaux + fleurs) hors
// desktop : on grossit les images pour qu'elles restent lisibles
// sur écrans étroits (le viewport rétrécit mais les éléments
// décoratifs en vw deviennent visuellement trop petits).
function decorWidthScale() {
const w = window.innerWidth;
if (w < 760) return 1.8; // mobile
if (w < 1080) return 1.5; // tablette
return 1; // desktop
}
function colDecorMetrics(colH) { function colDecorMetrics(colH) {
return { return {
gapBottom: clamp(120, 0.08 * colH, 320), gapBottom: clamp(120, 0.08 * colH, 320),
@@ -478,6 +489,7 @@
if (!layoutContainer) return; if (!layoutContainer) return;
layoutContainer.querySelectorAll('.col-decor').forEach(n => n.remove()); layoutContainer.querySelectorAll('.col-decor').forEach(n => n.remove());
colDecorState.length = 0; colDecorState.length = 0;
// Colonnes .fullpage internes : pile de 2 PNG (mid-left-1 et -2).
document.querySelectorAll('.fullpage:not(.large-container)').forEach(col => { document.querySelectorAll('.fullpage:not(.large-container)').forEach(col => {
const img1 = createColDecorImg(1); const img1 = createColDecorImg(1);
const img2 = createColDecorImg(2); const img2 = createColDecorImg(2);
@@ -485,6 +497,14 @@
layoutContainer.appendChild(img2); layoutContainer.appendChild(img2);
colDecorState.push({ img1, img2, col }); colDecorState.push({ img1, img2, col });
}); });
// Bloc .map-projets (home) : un seul PNG via singleOnly.
document.querySelectorAll('.map-projets').forEach(col => {
const img1 = createColDecorImg(1);
const img2 = createColDecorImg(2);
layoutContainer.appendChild(img1);
layoutContainer.appendChild(img2);
colDecorState.push({ img1, img2, col, singleOnly: true });
});
positionColDecor(); positionColDecor();
} }
@@ -494,26 +514,34 @@
const lcTopAbs = lcRect.top + window.scrollY; const lcTopAbs = lcRect.top + window.scrollY;
const lcLeftAbs = lcRect.left + window.scrollX; const lcLeftAbs = lcRect.left + window.scrollX;
for (const entry of colDecorState) { for (const entry of colDecorState) {
const { img1, img2, col } = entry; const { img1, img2, col, singleOnly } = entry;
const r = col.getBoundingClientRect(); const r = col.getBoundingClientRect();
const colH = r.height; const colH = r.height;
const m = colDecorMetrics(colH); const m = colDecorMetrics(colH);
const stack2H = m.gapBottom + m.h2; const stack1H = m.gapBottom + m.h2 + m.gapBetween + m.h1;
const stack1H = stack2H + m.gapBetween + m.h1; // singleOnly : un seul motif (img1) si la place le permet
const showBoth = stack1H <= colH * 0.5; // (= gapBottom + h1 sous 50% colH).
const showOne = !showBoth && stack2H <= colH * 0.3; const showBoth = !singleOnly && stack1H <= colH * 0.5;
img1.style.display = showBoth ? 'block' : 'none'; const showOne = !showBoth && (m.gapBottom + m.h2) <= colH * 0.3;
img2.style.display = (showBoth || showOne) ? 'block' : 'none'; const showSingle = singleOnly && (m.gapBottom + m.h1) <= colH * 0.5;
entry.visible1 = showBoth; entry.visible1 = showBoth || showSingle;
entry.visible2 = showBoth || showOne; entry.visible2 = !singleOnly && (showBoth || showOne);
if (!showBoth && !showOne) continue; img1.style.display = entry.visible1 ? 'block' : 'none';
img2.style.display = entry.visible2 ? 'block' : 'none';
if (!entry.visible1 && !entry.visible2) continue;
const colBottomAbs = r.bottom + window.scrollY; const colBottomAbs = r.bottom + window.scrollY;
const leftRel = (r.left + window.scrollX) - lcLeftAbs; const leftRel = (r.left + window.scrollX) - lcLeftAbs;
if (singleOnly) {
const top1 = colBottomAbs - m.gapBottom - m.h1;
img1.style.top = (top1 - lcTopAbs) + 'px';
img1.style.left = leftRel + 'px';
img1.style.height = m.h1 + 'px';
entry.scrollRef1 = top1 + m.h1 / 2 - window.innerHeight / 2;
} else {
const top2 = colBottomAbs - m.gapBottom - m.h2; const top2 = colBottomAbs - m.gapBottom - m.h2;
img2.style.top = (top2 - lcTopAbs) + 'px'; img2.style.top = (top2 - lcTopAbs) + 'px';
img2.style.left = leftRel + 'px'; img2.style.left = leftRel + 'px';
img2.style.height = m.h2 + 'px'; img2.style.height = m.h2 + 'px';
// Référence scroll : motif centré dans le viewport.
entry.scrollRef2 = top2 + m.h2 / 2 - window.innerHeight / 2; entry.scrollRef2 = top2 + m.h2 / 2 - window.innerHeight / 2;
if (showBoth) { if (showBoth) {
const top1 = top2 - m.gapBetween - m.h1; const top1 = top2 - m.gapBetween - m.h1;
@@ -523,7 +551,7 @@
entry.scrollRef1 = top1 + m.h1 / 2 - window.innerHeight / 2; entry.scrollRef1 = top1 + m.h1 / 2 - window.innerHeight / 2;
} }
} }
// Premier rendu du parallax après (re)position. }
applyColDecorParallax(); applyColDecorParallax();
} }
@@ -559,6 +587,7 @@
requestAnimationFrame(() => { requestAnimationFrame(() => {
colDecorRafPending = false; colDecorRafPending = false;
applyColDecorParallax(); applyColDecorParallax();
applyHomeDiapoParallax();
}); });
} }
if (!document.body.dataset.colDecorScrollWired) { if (!document.body.dataset.colDecorScrollWired) {
@@ -566,6 +595,279 @@
document.body.dataset.colDecorScrollWired = 'true'; document.body.dataset.colDecorScrollWired = 'true';
} }
//
// Décor aléatoire en haut à droite de chaque .fullpage. Tirage
// parmi drawings/ (excluant flower-2 + bird-3 déjà au footer).
// Bas de l'image overlap le haut de l'ancre (20%, sauf flower-3
// à 40% car tige fine). Mobile : ancre = .page-header-outside.
// Largeur par breakpoint.
//
const PAGE_DECOR_POOL = ['bird-1.png', 'bird-2.png', 'flower-1.png', 'flower-3.png'];
const pageDecorState = [];
function pageDecorWidthVw() {
const w = window.innerWidth;
if (w < 1080) return 22; // mobile + tablette
return 10; // desktop
}
function setupPageDecor() {
if (!layoutContainer) return;
layoutContainer.querySelectorAll('.page-decor').forEach(n => n.remove());
pageDecorState.length = 0;
document.querySelectorAll('.fullpage').forEach(col => {
// Exclure les .fullpage situées dans une sidebar (vue projets
// imbriquée). On ne décore que les colonnes de contenu.
if (col.closest('aside')) return;
// Tirage déterministe par colonne : on stocke l'index sur le
// dataset pour qu'un resize ne change pas l'image affichée.
let idx = col.dataset.pageDecorIdx;
if (idx == null) {
idx = Math.floor(Math.random() * PAGE_DECOR_POOL.length);
col.dataset.pageDecorIdx = String(idx);
} else {
idx = parseInt(idx, 10);
}
const img = document.createElement('img');
img.className = 'page-decor';
img.setAttribute('aria-hidden', 'true');
img.setAttribute('alt', '');
img.src = `/themes/erabletheme/assets/drawings/${PAGE_DECOR_POOL[idx]}`;
img.style.cssText = 'position:absolute;width:auto;height:auto;pointer-events:none;z-index:0;';
layoutContainer.appendChild(img);
pageDecorState.push({ img, col });
});
positionPageDecor();
}
function positionPageDecor() {
if (!pageDecorState.length) return;
const lcRect = layoutContainer.getBoundingClientRect();
const lcTopAbs = lcRect.top + window.scrollY;
const lcLeftAbs = lcRect.left + window.scrollX;
const widthVw = pageDecorWidthVw();
const widthPx = (widthVw / 100) * window.innerWidth;
const isMobile = window.innerWidth < 760;
for (const entry of pageDecorState) {
const img = entry.img;
if (!img.naturalWidth) {
img.addEventListener('load', positionPageDecor, { once: true });
continue;
}
const heightPx = widthPx * (img.naturalHeight / img.naturalWidth);
// Mobile : ancre = .page-header-outside (titre/retour) si présent,
// sinon la colonne elle-même.
let anchorEl = entry.col;
if (isMobile) {
const layoutContent = entry.col.closest('.layout-content');
const phOutside = layoutContent && layoutContent.querySelector('.page-header-outside');
if (phOutside) anchorEl = phOutside;
}
const r = anchorEl.getBoundingClientRect();
const anchorTopAbs = r.top + window.scrollY;
const anchorRightAbs = r.right + window.scrollX;
// flower-3 a une tige fine en haut → on la pousse plus dans
// l'ancre (40%) pour qu'elle ne soit pas perdue dans la marge.
const overlapRatio = img.src.includes('flower-3') ? 0.4 : 0.2;
const top = anchorTopAbs + heightPx * overlapRatio - heightPx;
const left = anchorRightAbs - widthPx * 0.7; // 30% dépasse à droite
img.style.width = widthPx + 'px';
img.style.top = (top - lcTopAbs) + 'px';
img.style.left = (left - lcLeftAbs) + 'px';
}
}
//
// Décors home-diapo-{right,left} : ancrés aux bords droit/gauche
// du viewport, leur haut dépassant au-dessus du haut des cards
// du carousel home (overlapTop px). Parallax custom.
//
const HOME_DIAPO_CONFIGS = [
{ side: 'right', file: 'home-diapo-right.png', widthVw: 30, overlapTop: 160 },
{ side: 'left', file: 'home-diapo-left.png', widthVw: 28, overlapTop: 80 },
];
const homeDiapoState = [];
// Ancre cible : .slick-list (cards visibles), avec fallbacks.
// .carousel_container inclut les dots → bas trop bas, à éviter.
function getCarouselCardsBox() {
return document.querySelector('.carousel_container .slick-list')
|| document.querySelector('.carousel_container .slick-container')
|| document.querySelector('.carousel_container');
}
function setupHomeDiapoDecor() {
if (!layoutContainer) return;
layoutContainer.querySelectorAll('.home-diapo-decor').forEach(n => n.remove());
homeDiapoState.length = 0;
const cardsBox = getCarouselCardsBox();
if (!cardsBox) return;
for (const cfg of HOME_DIAPO_CONFIGS) {
const img = document.createElement('img');
img.className = `home-diapo-decor home-diapo-decor-${cfg.side}`;
img.setAttribute('aria-hidden', 'true');
img.setAttribute('alt', '');
img.src = `/themes/erabletheme/assets/new-bg-shapes/${cfg.file}`;
img.style.cssText = `position:absolute;${cfg.side}:0;width:auto;height:auto;pointer-events:none;z-index:0;`;
layoutContainer.appendChild(img);
homeDiapoState.push({ img, widthVw: cfg.widthVw, overlapTop: cfg.overlapTop, cardsBox });
}
positionHomeDiapoDecor();
}
function positionHomeDiapoDecor() {
if (!homeDiapoState.length) return;
const lcRect = layoutContainer.getBoundingClientRect();
const lcTopAbs = lcRect.top + window.scrollY;
const scale = decorWidthScale();
for (const entry of homeDiapoState) {
const widthPx = (entry.widthVw / 100) * window.innerWidth * scale;
const cRect = entry.cardsBox.getBoundingClientRect();
const cardsTopAbs = cRect.top + window.scrollY;
// Top = haut des cards - overlapTop : le haut de l'image
// dépasse de cette valeur au-dessus du haut des cards.
const top = cardsTopAbs - entry.overlapTop;
entry.img.style.width = widthPx + 'px';
entry.img.style.top = (top - lcTopAbs) + 'px';
entry.scrollRef = top - window.innerHeight / 2;
}
applyHomeDiapoParallax();
}
function applyHomeDiapoParallax() {
if (!homeDiapoState.length) return;
const scrollY = window.scrollY || document.body.scrollTop || 0;
for (const { img, scrollRef } of homeDiapoState) {
if (scrollRef == null) continue;
const dY = clamp(-COL_DECOR_PARALLAX_AMP,
-(scrollY - scrollRef) * COL_DECOR_PARALLAX_SPEED,
COL_DECOR_PARALLAX_AMP);
img.style.transform = `translate3d(0, ${dY}px, 0)`;
}
}
//
// Décors home-above : flower-1 (gauche) et bird-1 (droite),
// au-dessus des cards, leur bas débordant de `overlap` px sur le
// haut des cards. offsetVw : décalage horizontal vers l'intérieur.
// Pas de parallax.
//
const HOME_ABOVE_CONFIGS = [
{ side: 'left', file: 'drawings/flower-1.png', widthVw: 15, overlap: 40, offsetVw: 8 },
{ side: 'right', file: 'drawings/bird-1.png', widthVw: 15, overlap: 40, offsetVw: 4 },
];
const homeAboveState = [];
function setupHomeAboveDecor() {
if (!layoutContainer) return;
layoutContainer.querySelectorAll('.home-above-decor').forEach(n => n.remove());
homeAboveState.length = 0;
const cardsBox = getCarouselCardsBox();
if (!cardsBox) return;
for (const cfg of HOME_ABOVE_CONFIGS) {
const img = document.createElement('img');
img.className = `home-above-decor home-above-decor-${cfg.side}`;
img.setAttribute('aria-hidden', 'true');
img.setAttribute('alt', '');
img.src = `/themes/erabletheme/assets/${cfg.file}`;
img.style.cssText = `position:absolute;${cfg.side}:${cfg.offsetVw}vw;width:auto;height:auto;pointer-events:none;z-index:0;`;
layoutContainer.appendChild(img);
homeAboveState.push({ img, widthVw: cfg.widthVw, overlap: cfg.overlap, cardsBox });
}
positionHomeAboveDecor();
}
function positionHomeAboveDecor() {
if (!homeAboveState.length) return;
const lcRect = layoutContainer.getBoundingClientRect();
const lcTopAbs = lcRect.top + window.scrollY;
const lcHeight = lcRect.height;
const scale = decorWidthScale();
for (const entry of homeAboveState) {
const widthPx = (entry.widthVw / 100) * window.innerWidth * scale;
const cRect = entry.cardsBox.getBoundingClientRect();
const cardsTopAbs = cRect.top + window.scrollY;
// Le BAS de l'image est à `overlap` px sous le haut des cards :
// l'image est essentiellement au-dessus du carousel, débord en bas.
const imgBottom = cardsTopAbs + entry.overlap;
const bottomFromLcBottom = (lcTopAbs + lcHeight) - imgBottom;
entry.img.style.width = widthPx + 'px';
entry.img.style.bottom = bottomFromLcBottom + 'px';
}
}
//
// Décors home-projet, ancrés au bloc .map-projets :
// - bird-2 en haut à gauche, débordant à gauche du bloc en
// desktop, ou collé à -3vw du viewport en mobile/tablette.
// - flower-3 collée au bord droit du viewport (tronquée), centre
// vertical à 1/3 depuis le haut du bloc.
// Pas de parallax.
//
const HOME_PROJET_CONFIGS = [
{ file: 'drawings/bird-2.png', widthVw: 15, anchor: 'block-left-top' },
{ file: 'drawings/flower-3.png', widthVw: 7, anchor: 'viewport-right' },
];
const homeProjetState = [];
function setupHomeProjetDecor() {
if (!layoutContainer) return;
layoutContainer.querySelectorAll('.home-projet-decor').forEach(n => n.remove());
homeProjetState.length = 0;
const block = document.querySelector('.map-projets');
if (!block) return;
for (const cfg of HOME_PROJET_CONFIGS) {
const img = document.createElement('img');
img.className = `home-projet-decor home-projet-decor-${cfg.anchor}`;
img.setAttribute('aria-hidden', 'true');
img.setAttribute('alt', '');
img.src = `/themes/erabletheme/assets/${cfg.file}`;
img.style.cssText = 'position:absolute;width:auto;height:auto;pointer-events:none;z-index:0;';
layoutContainer.appendChild(img);
homeProjetState.push({ img, anchor: cfg.anchor, widthVw: cfg.widthVw, block });
}
positionHomeProjetDecor();
}
function positionHomeProjetDecor() {
if (!homeProjetState.length) return;
const lcRect = layoutContainer.getBoundingClientRect();
const lcTopAbs = lcRect.top + window.scrollY;
const lcLeftAbs = lcRect.left + window.scrollX;
const scale = decorWidthScale();
for (const entry of homeProjetState) {
const img = entry.img;
if (!img.naturalWidth) {
img.addEventListener('load', positionHomeProjetDecor, { once: true });
continue;
}
const widthPx = (entry.widthVw / 100) * window.innerWidth * scale;
const heightPx = widthPx * (img.naturalHeight / img.naturalWidth);
const bRect = entry.block.getBoundingClientRect();
const blockTopAbs = bRect.top + window.scrollY;
const blockLeftAbs = bRect.left + window.scrollX;
const blockHeightPx = bRect.height;
img.style.width = widthPx + 'px';
if (entry.anchor === 'block-left-top') {
// bird-2 remontée de 0.6× sa hauteur au-dessus du bloc.
const top = blockTopAbs - heightPx * 0.6;
if (window.innerWidth < 1080) {
img.style.left = '-3vw';
} else {
img.style.left = (blockLeftAbs - widthPx * 0.5 - lcLeftAbs) + 'px';
}
img.style.top = (top - lcTopAbs) + 'px';
img.style.right = '';
} else if (entry.anchor === 'viewport-right') {
// flower-3 : tronquée par le bord droit du viewport.
const top = blockTopAbs + blockHeightPx / 3 - heightPx / 2;
img.style.right = '-20px';
img.style.left = '';
img.style.top = (top - lcTopAbs) + 'px';
}
}
}
// //
// Décors latéraux parallax v2. // Décors latéraux parallax v2.
// Cloner le .decor-tile autant de fois que nécessaire pour couvrir // Cloner le .decor-tile autant de fois que nécessaire pour couvrir
@@ -618,10 +920,14 @@
window.addEventListener('load', () => { window.addEventListener('load', () => {
setupBackgroundTiles(); setupBackgroundTiles();
setupColDecor(); setupColDecor();
setupHomeDiapoDecor();
setupHomeAboveDecor();
setupHomeProjetDecor();
setupPageDecor();
initRellax(); initRellax();
}); });
// Resize debouncé : recalcule le nombre de tiles + col-decor, // Resize debouncé : recalcule le nombre de tiles + tous les décors,
// repart Rellax propre. // repart Rellax propre.
let bgResizeTimer = null; let bgResizeTimer = null;
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
@@ -629,6 +935,10 @@
bgResizeTimer = setTimeout(() => { bgResizeTimer = setTimeout(() => {
setupBackgroundTiles(); setupBackgroundTiles();
setupColDecor(); setupColDecor();
setupHomeDiapoDecor();
setupHomeAboveDecor();
setupHomeProjetDecor();
setupPageDecor();
initRellax(); initRellax();
}, 200); }, 200);
}); });

View File

@@ -168,6 +168,9 @@ main:has(#block-erabletheme-views-block-projets-block-1) {
background-color: white; background-color: white;
border-bottom: 7px solid $fluo_green; border-bottom: 7px solid $fluo_green;
position: relative; position: relative;
@media (min-width: $breakpoint_desktop) {
margin-bottom: 4vh;
}
h2 { h2 {
margin-top: 5vh !important; margin-top: 5vh !important;
margin-bottom: 7vh; margin-bottom: 7vh;
@@ -201,7 +204,12 @@ main:has(#block-erabletheme-views-block-projets-block-1) {
@include main_text_content(); @include main_text_content();
} }
p:first-of-type { // Margin-top sur le tout premier élément textuel du contenu,
// peu importe son type (p / h1-h6) ET peu importe la profondeur
// de wrapping (Drupal wrappe souvent les champs dans une ou deux
// <div>). :first-child garantit qu'on ne matche que le tout
// premier (vs :first-of-type qui matche le 1er de chaque type).
:is(p, h1, h2, h3, h4, h5, h6):first-child {
margin-top: 7vh; margin-top: 7vh;
} }

View File

@@ -146,9 +146,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-bottom: 5vh;
@media (min-width: $breakpoint_desktop) { @media (min-width: $breakpoint_desktop) {
margin-top: 10vh; margin-top: 10vh;
margin-bottom: 10vh; margin-bottom: 4vh;
} }
} }

View File

@@ -25,7 +25,9 @@ body {
} }
} }
footer { footer {
z-index: 0; // Au-dessus de main pour que les ornements posés sur le footer
// (cf. .footer-decor) passent par-dessus le bas de la colonne.
z-index: 3;
} }
#background { #background {
z-index: -1; z-index: -1;

View File

@@ -19,12 +19,38 @@
margin-top: 50px; margin-top: 50px;
padding: 1rem $x_margin; padding: 1rem $x_margin;
box-sizing: border-box; box-sizing: border-box;
position: relative;
@media (min-width: $breakpoint_desktop) { @media (min-width: $breakpoint_desktop) {
width: 50vw; width: 50vw;
margin-left: 25vw; margin-left: 25vw;
padding: 1rem; padding: 1rem;
} }
// Ornements décoratifs (oiseau / fleur) "posés" sur le bord haut du
// footer : bord bas légèrement entré dans le footer, le reste dépasse
// au-dessus. En desktop le footer fait 50vw centré ; les ornements
// sont à cheval sur les bords du footer (moitié dedans, moitié hors).
// Le footer est passé au-dessus de main (cf. _global.scss) pour que
// les ornements recouvrent le bas de la colonne.
// pointer-events:none pour ne pas bloquer les liens du footer.
> .footer-decor {
position: absolute;
bottom: 100%;
width: 18vw;
height: auto;
transform: translateY(15%);
pointer-events: none;
@media (min-width: $breakpoint_desktop) {
width: 8vw;
}
}
> .footer-decor-bird { left: 0; }
> .footer-decor-flower { right: 0; }
@media (min-width: $breakpoint_desktop) {
> .footer-decor-bird { left: -4vw; }
> .footer-decor-flower { right: -4vw; }
}
#footer_middle { #footer_middle {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -86,6 +86,10 @@
</main> </main>
<footer role="contentinfo"> <footer role="contentinfo">
{# Ornements décoratifs au-dessus du footer (à cheval entre fullpage
et footer). Purement visuels. #}
<img class="footer-decor footer-decor-bird" src="/themes/erabletheme/assets/drawings/bird-3.png" aria-hidden="true" alt="">
<img class="footer-decor footer-decor-flower" src="/themes/erabletheme/assets/drawings/flower-2.png" aria-hidden="true" alt="">
<section id="footer_top">{{ page.footer_top }}</section> <section id="footer_top">{{ page.footer_top }}</section>
<section id="footer_middle"> <section id="footer_middle">
<section id="footer_left">{{ page.footer_left }}</section> <section id="footer_left">{{ page.footer_left }}</section>