narrative web version

This commit is contained in:
sarahgarcin1 2026-02-24 19:47:30 +01:00
parent 9ed028a560
commit f6032f717d
27 changed files with 1677 additions and 7 deletions

1004
public/assets/css/web.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
Uuid: 0ofsryvoc2wwnnqr
----
Template: blocks/image

View file

@ -0,0 +1,5 @@
Uuid: kjb9tgal4d7m95mt
----
Template: blocks/image

View file

@ -0,0 +1,29 @@
Title: Maison
----
Cover: - file://0ofsryvoc2wwnnqr
----
Text: [{"content":{"location":"kirby","image":["file://0ofsryvoc2wwnnqr"],"src":"","alt":"","caption":"Mon appartement","link":"","width":"","position":""},"id":"bf74d3e0-3fff-4ef6-a1bc-ba7605e67a74","isHidden":false,"type":"image"},{"content":{"text":"<p>Il fait chaud, la luminosité est chaleureuse, et l'odeur apaisante. Un chat est assis sur la table basse du salon, un autre sur le meuble télé, un dort sur le canapé, et un autre semble se diriger vers la chambre. Des toiles et des posters recouvrent les murs, et de la musique jaillit des quatre coins de la pièce. Je me sens bien ici, car je fais en sorte que ce soit le cas. Mais pourtant, depuis quelques jours, j'ai du mal à rester dans mon salon, dans ma cuisine ou même dans ma chambre. Même mon balcon devient étouffant. Il y a soit trop de silence chez moi, soit trop de bruit. Mon salon semble soit trop rangé minutieusement, soit trop en désordre. Je n'ai plus envie de rester ici, et je n'y arrive tout simplement plus. Mais je n'ai pas non plus envie d'en sortir. Alors je mets de la musique pour casser ce lourd silence, mais en même temps je change de musique toutes les secondes, car aucune ne me convient. Cette dualité mentale m'épuise et m'empêche de réfléchir, parfois même de respirer. Alors en ce moment je doute de tout. Tout me semble terne, je n'ai plus d'inspiration et je n'arrive plus a peindre. Je suis bloquée. Et plus rien ne me donne envie. J'ai mal partout, mon dos est bloqué, je suis constamment recroquevillée sur moi-même, et j'ai parfois l'impression que mes jambes ne sont plus capables de me porter.</p><p>Un de mes chats se réveille doucement de sa sieste : Fely. C'est mon chat tout gris. Le plus gourmand des quatre. Alors forcement, s'il se réveille de sa sieste c'est uniquement pour aller manger. Mais voilà que je l'entends miauler malgré la musique qui se fait retentir chez moi. Ce qui vient balayer mes pensées. Il n'y a plus de croquettes dans les gamelles, et, pire encore, plus de croquettes dans le paquet. C'est une excuse parfaite pour enfin sortir de chez moi, et pour ne penser qu'à une chose : <a href=\"https://geoproject.fr/ba/markers/mes-premiers-pas/\">faire les courses.</a></p>"},"id":"42deaa16-dcd1-4327-a4b3-875a2c61d47b","isHidden":false,"type":"text"},{"content":{"location":"kirby","image":["file://kjb9tgal4d7m95mt"],"src":"","alt":"","caption":"Fely sur mon canapé","link":"","width":"","position":""},"id":"ab6104a9-6c93-4d72-9ff9-3eb75476cf94","isHidden":false,"type":"image"}]
----
Latitude: 48.850647669811
----
Longitude: 2.3252579641452
----
Markericon:
----
Markericonsize: 40
----
Uuid: mkl0rcgmk2va5v2p

View file

@ -0,0 +1,5 @@
Uuid: ugt8nkibhgeaxws8
----
Template: blocks/image

View file

@ -0,0 +1,5 @@
Uuid: v3cgqqjcu5xi0bef
----
Template: blocks/image

View file

@ -0,0 +1,29 @@
Title: Mes premiers pas
----
Cover: - file://nrzg8aiibnsxecfl
----
Text: [{"content":{"location":"kirby","image":["file://ugt8nkibhgeaxws8"],"src":"","alt":"","caption":"Vue de la rue","link":"","width":"","position":""},"id":"480e0e09-5554-420e-b058-ea82e3985a6c","isHidden":false,"type":"image"},{"content":{"text":"<p>J'ai enfilé un pull un peu au hasard, mes baskets aux pieds, et mes écouteurs diffusent mon son du moment : <em>Rue de Sèvres</em> de Dinos. Me voilà en route vers le supermarché le plus proche. La rue semble silencieuse à côté de ce qu'il se passe dans ma tête. Même en me trouvant à l'extérieur, je me sens bloquée et j'ai de nouveau l'impression d'étouffer. Je me sens coincée dans ma vie. Plus rien ne me fait vibrer. Même cette rue que j'apprécie tant, à fini par me blaser. Je crois que j'attends quelque chose, un signe, un événement pour avoir un renouveau d'inspiration. J'essaie de me concentrer uniquement sur le chemin et sur ma liste de courses, mais ces pensées ne cessent de revenir. Pourquoi est-ce que je n'arrive plus à peindre ? Pourquoi est-ce que cette activité qui me faisait du bien autrefois devient un réel calvaire ? Pourquoi est-ce que même mon propre appartement ne me convient plus ? Pourquoi ? Pourquoi ? Pourquoi ?!</p><p><code>Une pause. Il me faut une pause.</code></p>"},"id":"33bbfdfd-f92a-475a-b175-440fb50fb1b4","isHidden":false,"type":"text"},{"content":{"location":"kirby","image":["file://v3cgqqjcu5xi0bef"],"src":"","alt":"","caption":"Mes pas","link":"","width":"","position":""},"id":"4d8d7e98-4b6a-4166-8077-3c728a7d8ee4","isHidden":false,"type":"image"},{"content":{"text":"<p>Je vais aller m'asseoir sur ce banc, dans <a href=\"https://geoproject.fr/ba/markers/le-banc-du-parc/\">ce parc</a> dans lequel je n'ai pas mis les pieds depuis des années.</p>"},"id":"8d7cf47a-20c4-4a90-9b91-fc470b67fc09","isHidden":false,"type":"text"}]
----
Latitude: 48.850268819336
----
Longitude: 2.3242447746227
----
Markericon:
----
Markericonsize: 40
----
Uuid: ttylgpb9rz43bncb

View file

@ -0,0 +1,5 @@
Uuid: ublkpzmpvyfnraw8
----
Template: blocks/image

View file

@ -0,0 +1,5 @@
Uuid: yk4i3gznft3k8mcs
----
Template: blocks/image

View file

@ -0,0 +1,29 @@
Title: Le banc du parc
----
Cover: - file://ih9crswtmcuvhnlr
----
Text: [{"content":{"location":"kirby","image":["file://ublkpzmpvyfnraw8"],"src":"","alt":"","caption":"Assise sur le banc dans la charmille","link":"","width":"","position":""},"id":"2747a850-f19f-44fe-a017-bf3f9ba4a9e9","isHidden":false,"type":"image"},{"content":{"text":"<p>Je suis assise, et j'ai retiré mes écouteurs. Je suis sur ce banc, dans cette charmille. Je ne savais pas qu'il y avait autant d'oiseaux dans ce parc, et qu'on pouvait si bien les entendre. Ce son apaise un peu mes pensées. Et ce lieu est tellement réconfortant. Je me sens enveloppée dans ces feuilles, j'ai l'étrange impression qu'elles prennent soin de moi. Je me sens portée par ces dernières. Comme si elles recouvraient tout mon corps pour me soigner. Tout doucement, l'idée que cela puisse se faire réellement, commence a se former. Je les imagine une à une se coller contre moi et me créer une deuxième peau. Certaines pourraient être jaunâtres, d'autres avec des reflets rouges, certaines même pourraient avoir la même couleur que ma peau. Et alors toutes les feuilles de la charmille se décolleraient pour ne former qu'un. Rien quen y pensant, je me sens rassurée et protégée.</p>"},"id":"0dc64c8d-b490-4ae9-9db8-ee2bd6e5ec73","isHidden":false,"type":"text"},{"content":{"location":"kirby","image":["file://yk4i3gznft3k8mcs"],"src":"","alt":"","caption":"Dans mon imagination","link":"","width":"","position":""},"id":"41801548-58df-4b9f-9e5f-fccfc300d714","isHidden":false,"type":"image"},{"content":{"text":"<p>Mais c'est le bruit agaçant d'un gyrophare de police qui me ramène à la raison. En une fraction de seconde, cet état de bien-être a quitté mon corps. Je me sentais pourtant si bien... C'était <em>ce</em> sentiment que je ne retrouvais plus. J'étais enfin apaisée.</p><p>Il faut que je me reconcentre sur mon objectif premier : <a href=\"https://geoproject.fr/ba/markers/le-declic/\">faire les courses.</a></p>"},"id":"eaf743f3-75da-4aab-84ac-3a7d89c88d59","isHidden":false,"type":"text"}]
----
Latitude: 48.85065574496
----
Longitude: 2.3214011237332
----
Markericon:
----
Markericonsize: 40
----
Uuid: xne8cgpoal5t9fkn

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -2,10 +2,6 @@ Title: Le trajet des courses
---- ----
Author:
----
Tags: chemin, paris, Rue de Sèvres Tags: chemin, paris, Rue de Sèvres
---- ----
@ -14,4 +10,19 @@ Text: <p>Dans le 7ème arrondissement, à l'arrêt de métro Saint-Placide, se t
---- ----
Mapdata:
background:
type: osm
center:
lat: 48.850072183852575
lon: 2.32508823039484
zoom: 16.154464167854993
----
Author:
----
Uuid: qdofat9jfhj50hqx Uuid: qdofat9jfhj50hqx

View file

@ -25,8 +25,8 @@ Customcss:
body { body {
font-family: "DM Sans", sans-serif; font-family: "DM Sans", sans-serif;
text-align: left; text-align: left;
color: rgb(190, 9, 9); color: black;
background: blue; background: transparent;
} }
p { p {

View file

@ -11,7 +11,11 @@ tabs:
fields: fields:
type: fields type: fields
fields: fields:
content: cover:
label: Image de couverture
type: files
multiple: false
text:
label: Contenu label: Contenu
type: blocks type: blocks
fieldsets: fieldsets:

View file

@ -0,0 +1,46 @@
<?php
use Kirby\Http\Response;
return [
'debug' => true,
'routes' => [
[
'pattern' => '(:all)/web',
'action' => function ($path) {
$realPage = page($path);
if (!$realPage) {
return false;
}
return new Page([
'slug' => $realPage->slug() . '-web',
'template' => 'narrative-web',
// 'content' => $realPage->content()->toArray(),
'content' => array_merge(
$realPage->content()->toArray(),
['originalUri' => $realPage->uri()]
),
]);
}
]
]
// [
// 'pattern' => '(:all)/web',
// 'method' => 'GET',
// 'before' => true,
// 'action' => function (string $path) {
// $page = kirby()->page($path);
// if (!$page) {
// return false;
// }
// return $page->render([], 'narrative-web');
// }
// ]
// ]
];

View file

@ -0,0 +1,485 @@
<?php
/**
* Template: narrative-web.php
* Affichage web storytelling d'une narrative et de ses sous-pages
* Structure : narrative > geoformat > chapitre / map > marker
*/
// Collect all subpages for lateral navigation
$original = page($page->originalUri());
$subpages = $original->children()->listed();
$navItems = [];
foreach ($subpages as $subpage) {
$item = [
'id' => $subpage->uid(),
'title' => $subpage->title()->value(),
'template' => $subpage->intendedTemplate()->name(),
'children' => [],
];
// Pour les cartes : inclure les marqueurs comme sous-items de navigation
if ($subpage->intendedTemplate()->name() === 'map') {
foreach ($subpage->children()->listed()->filterBy('intendedTemplate', 'marker') as $mk) {
$item['children'][] = [
'id' => $mk->uid(),
'title' => $mk->title()->value(),
];
}
}
$navItems[] = $item;
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e($original->isHomePage() != true, $original->title() . ' ') . $site->title() ?></title>
<!-- Styles globaux existants -->
<link rel="stylesheet" href="<?= url('assets/css/style.css') ?>">
<!-- Styles narrative web -->
<link rel="stylesheet" href="<?= url('assets/css/web.css') ?>">
<!-- MapLibre GL -->
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<!-- Custom CSS éventuel depuis le panel -->
<?php if ($original->customCss()->isNotEmpty()): ?>
<style><?= $original->customCss() ?></style>
<?php endif ?>
<!-- À SUPPRIMER EN PRODUCTION -->
<meta name="robots" content="noindex, nofollow, noarchive">
<?php if ($kirby->user()): ?>
<meta name="csrf" content="<?= csrf() ?>">
<?php endif ?>
</head>
<body data-template="<?= $original->template() ?>">
<!-- ═══════════════════════════════════════════
NAVIGATION LATÉRALE (ancres)
════════════════════════════════════════════ -->
<?php if (!empty($navItems)): ?>
<nav class="nw-sidenav" aria-label="Navigation sections">
<ul class="nw-sidenav__list">
<li class="nw-sidenav__item nw-sidenav__item--intro">
<a class="nw-sidenav__link" href="#nw-intro">
<span class="nw-sidenav__dot"></span>
<span class="nw-sidenav__label">Introduction</span>
</a>
</li>
<?php foreach ($navItems as $item): ?>
<li class="nw-sidenav__item nw-sidenav__item--<?= $item['template'] ?>">
<a class="nw-sidenav__link" href="#section-<?= $item['id'] ?>">
<span class="nw-sidenav__dot"></span>
<span class="nw-sidenav__label"><?= html($item['title']) ?></span>
</a>
<?php if (!empty($item['children'])): ?>
<ul class="nw-sidenav__sub">
<?php foreach ($item['children'] as $child): ?>
<li class="nw-sidenav__sub-item">
<a class="nw-sidenav__sub-link" href="#marker-<?= $child['id'] ?>">
<span class="nw-sidenav__sub-dot"></span>
<span class="nw-sidenav__sub-label"><?= html($child['title']) ?></span>
</a>
</li>
<?php endforeach ?>
</ul>
<?php endif ?>
</li>
<?php endforeach ?>
</ul>
</nav>
<?php endif ?>
<!-- ═══════════════════════════════════════════
HERO / INTRODUCTION
════════════════════════════════════════════ -->
<header class="nw-hero" id="nw-intro">
<?php if ($original->cover()->isNotEmpty() && $cover = $original->cover()->toFile()): ?>
<div class="nw-hero__bg" style="background-image: url('<?= $cover->url() ?>')"></div>
<div class="nw-hero__overlay"></div>
<?php endif ?>
<div class="nw-hero__content">
<h1 class="nw-hero__title"><?= html($original->title()) ?></h1>
<?php if ($original->author()->isNotEmpty()): ?>
<p class="nw-hero__author">Par <span><?= html($original->author()) ?></span></p>
<?php endif ?>
</div>
</header>
<!-- Introduction -->
<?php if ($original->introduction()->isNotEmpty()): ?>
<section class="nw-introduction">
<div class="nw-container nw-container--narrow">
<div class="nw-introduction__body">
<?= $original->introduction() ?>
</div>
</div>
</section>
<?php endif ?>
<!-- ═══════════════════════════════════════════
SOUS-PAGES
════════════════════════════════════════════ -->
<main class="nw-main">
<?php foreach ($subpages as $subpage):
$tpl = $subpage->intendedTemplate()->name();
?>
<!-- ─────────────────────────────────────────
CARTE (map.yaml > marker.yaml)
───────────────────────────────────────── -->
<?php if ($tpl === 'map'): ?>
<section class="nw-section nw-section--map" id="section-<?= $subpage->uid() ?>">
<div class="nw-container">
<header class="nw-section__header">
<h2 class="nw-section__title"><?= html($subpage->title()) ?></h2>
<?php if ($subpage->tags()->isNotEmpty()): ?>
<div class="nw-tags">
<?php foreach ($subpage->tags()->split(',') as $tag): ?>
<span class="nw-tag"><?= html(trim($tag)) ?></span>
<?php endforeach ?>
</div>
<?php endif ?>
</header>
<?php if ($subpage->text()->isNotEmpty()): ?>
<div class="nw-prose"><?= $subpage->text() ?></div>
<?php endif ?>
</div>
<!-- Carte MapLibre inline -->
<?php
$markers = $subpage->children()->listed()->filterBy('intendedTemplate', 'marker');
$mapId = 'map-' . $subpage->uid();
?>
<div class="nw-map-wrap">
<div id="<?= $mapId ?>" class="nw-map"></div>
</div>
<!-- Fichiers de la carte -->
<?php if ($subpage->files()->isNotEmpty()): ?>
<div class="nw-container">
<div class="nw-files">
<?php foreach ($subpage->files() as $file): ?>
<a href="<?= $file->url() ?>" class="nw-file" target="_blank">
<span class="nw-file__icon"></span>
<span class="nw-file__name"><?= html($file->filename()) ?></span>
</a>
<?php endforeach ?>
</div>
</div>
<?php endif ?>
<!-- ── Liste des marqueurs sous la carte ── -->
<?php if ($markers->isNotEmpty()): ?>
<div class="nw-map-marker-list nw-container">
<?php $mNum = 1; foreach ($markers as $marker): ?>
<article
class="nw-map-marker"
id="marker-<?= $marker->uid() ?>"
data-lat="<?= $marker->latitude()->value() ?>"
data-lng="<?= $marker->longitude()->value() ?>"
>
<?php if ($marker->cover()->isNotEmpty() && $mCover = $marker->cover()->toFile()): ?>
<div class="nw-map-marker__cover">
<img src="<?= $mCover->url() ?>" alt="<?= html($marker->title()) ?>">
<span class="nw-map-marker__num"><?= $mNum ?></span>
</div>
<?php else: ?>
<span class="nw-map-marker__num nw-map-marker__num--no-cover"><?= $mNum ?></span>
<?php endif ?>
<div class="nw-map-marker__body">
<h3 class="nw-map-marker__title"><?= html($marker->title()) ?></h3>
<?php if ($marker->text()->isNotEmpty()): ?>
<div class="nw-map-marker__content">
<?php foreach ($marker->text()->toBlocks() as $block): ?>
<div class="block block-type-<?= $block->type() ?>">
<?= $block ?>
</div>
<?php endforeach ?>
</div>
<?php endif ?>
</div>
</article>
<?php $mNum++; endforeach ?>
</div>
<?php endif ?>
<script>
(function() {
var markers = <?php
$mData = [];
foreach ($markers as $m) {
if ($m->latitude()->isNotEmpty() && $m->longitude()->isNotEmpty()) {
$coverUrl = '';
if ($m->cover()->isNotEmpty() && $mCov = $m->cover()->toFile()) {
$coverUrl = $mCov->url();
}
$iconUrl = '';
$iconSize = (int)$m->markerIconSize()->or(40)->value();
if ($m->markerIcon()->isNotEmpty() && $icon = $m->markerIcon()->toFile()) {
$iconUrl = $icon->url();
}
$mData[] = [
'uid' => $m->uid(),
'lat' => (float)$m->latitude()->value(),
'lng' => (float)$m->longitude()->value(),
'title' => $m->title()->value(),
'coverUrl' => $coverUrl,
'iconUrl' => $iconUrl,
'iconSize' => $iconSize,
];
}
}
echo json_encode($mData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>;
// Centre de la carte
var mapdata = <?php
$defaultCenter = [43.836699, 4.360054];
$defaultZoom = 13;
if ($subpage->mapdata()->isNotEmpty()) {
$raw = $subpage->mapdata()->value();
$decoded = json_decode($raw, true);
if ($decoded) {
echo json_encode($decoded);
} else {
echo json_encode(['center' => $defaultCenter, 'zoom' => $defaultZoom]);
}
} else {
echo json_encode(['center' => $defaultCenter, 'zoom' => $defaultZoom]);
}
?>;
document.addEventListener('DOMContentLoaded', function() {
var center = mapdata.center || [<?= $defaultCenter[1] ?>, <?= $defaultCenter[0] ?>];
var zoom = mapdata.zoom || 13;
// MapLibre attend [lng, lat]
if (Array.isArray(center) && center.length === 2) {
// Si stocké [lat, lng], inverser
if (center[0] > 90 || center[0] < -90) {
// déjà [lng, lat]
} else {
center = [center[1], center[0]];
}
}
var map = new maplibregl.Map({
container: '<?= $mapId ?>',
style: 'https://api.maptiler.com/maps/dataviz/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
center: center,
zoom: zoom,
attributionControl: true
});
// Fallback style OSM si pas de clé MapTiler
map.on('error', function() {
map.setStyle({
version: 8,
sources: {
osm: {
type: 'raster',
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap contributors'
}
},
layers: [{ id: 'osm', type: 'raster', source: 'osm' }]
});
});
markers.forEach(function(m) {
var el = null;
if (m.iconUrl) {
el = document.createElement('div');
el.className = 'nw-marker-icon';
el.style.backgroundImage = 'url(' + m.iconUrl + ')';
el.style.width = m.iconSize + 'px';
el.style.height = m.iconSize + 'px';
}
var markerOpts = { color: 'var(--nw-accent, #c0392b)' };
if (el) markerOpts.element = el;
// Construire le HTML de la popup : cover + titre + ancre vers le texte
var popupHtml = '';
if (m.coverUrl) {
popupHtml += '<div class="nw-popup-cover"><img src="' + m.coverUrl + '" alt="' + m.title + '"></div>';
}
popupHtml += '<div class="nw-popup-body">';
if (m.title) {
popupHtml += '<strong class="nw-popup-title">' + m.title + '</strong>';
}
popupHtml += '<a class="nw-popup-anchor" href="#marker-' + m.uid + '">Lire la fiche ↓</a>';
popupHtml += '</div>';
var marker = new maplibregl.Marker(markerOpts)
.setLngLat([m.lng, m.lat])
.setPopup(new maplibregl.Popup({ offset: 25, maxWidth: '280px' }).setHTML(popupHtml))
.addTo(map);
});
// Adapter les bounds si on a des markers
if (markers.length > 1) {
var bounds = new maplibregl.LngLatBounds();
markers.forEach(function(m) { bounds.extend([m.lng, m.lat]); });
map.fitBounds(bounds, { padding: 60, maxZoom: 16 });
}
});
})();
</script>
</section>
<!-- ─────────────────────────────────────────
GÉOFORMAT (geoformat.yaml > chapitre.yaml)
───────────────────────────────────────── -->
<?php elseif ($tpl === 'geoformat'): ?>
<section class="nw-section nw-section--geoformat" id="section-<?= $subpage->uid() ?>">
<!-- En-tête géoformat -->
<div class="nw-geoformat-hero">
<?php if ($subpage->cover()->isNotEmpty() && $gCover = $subpage->cover()->toFile()): ?>
<div class="nw-geoformat-hero__bg" style="background-image: url('<?= $gCover->url() ?>')"></div>
<div class="nw-geoformat-hero__overlay"></div>
<?php endif ?>
<div class="nw-geoformat-hero__content nw-container">
<h2 class="nw-geoformat-hero__title"><?= html($subpage->title()) ?></h2>
<?php if ($subpage->subtitle()->isNotEmpty()): ?>
<p class="nw-geoformat-hero__subtitle"><?= html($subpage->subtitle()) ?></p>
<?php endif ?>
<?php if ($subpage->tags()->isNotEmpty()): ?>
<div class="nw-tags">
<?php foreach ($subpage->tags()->split(',') as $tag): ?>
<span class="nw-tag"><?= html(trim($tag)) ?></span>
<?php endforeach ?>
</div>
<?php endif ?>
</div>
</div>
<!-- Chapeau -->
<?php if ($subpage->text()->isNotEmpty()): ?>
<div class="nw-container nw-container--narrow">
<div class="nw-chapeau"><?= $subpage->text() ?></div>
</div>
<?php endif ?>
<!-- Chapitres -->
<?php
$chapitres = $subpage->children()->listed()->filterBy('intendedTemplate', 'chapitre');
$chapCount = $chapitres->count();
if ($chapCount > 0): ?>
<!-- Navigation chapitres locale -->
<?php if ($chapCount > 1): ?>
<div class="nw-container">
<nav class="nw-chap-nav" aria-label="Chapitres">
<?php $ci = 1; foreach ($chapitres as $chap): ?>
<a class="nw-chap-nav__link" href="#chap-<?= $chap->uid() ?>">
<span class="nw-chap-nav__num"><?= $ci ?></span>
<span class="nw-chap-nav__title"><?= html($chap->title()) ?></span>
</a>
<?php $ci++; endforeach ?>
</nav>
</div>
<?php endif ?>
<?php $chapNum = 1; foreach ($chapitres as $chap): ?>
<article class="nw-chapitre" id="chap-<?= $chap->uid() ?>">
<div class="nw-container nw-container--narrow">
<header class="nw-chapitre__header">
<span class="nw-chapitre__num">Chapitre <?= $chapNum ?></span>
<h3 class="nw-chapitre__title"><?= html($chap->title()) ?></h3>
</header>
<div class="nw-chapitre__body">
<?= $chap->text()->toBlocks() ?>
</div>
</div>
</article>
<?php $chapNum++; endforeach ?>
<?php endif ?>
</section>
<?php endif ?>
<?php endforeach ?>
</main>
<!-- ═══════════════════════════════════════════
FOOTER
════════════════════════════════════════════ -->
<?php snippet('footer') ?>
<!-- Script animations sections -->
<script>
(function() {
var sections = document.querySelectorAll('.nw-section');
if (!sections.length) return;
var io = new IntersectionObserver(function(entries) {
entries.forEach(function(e) {
if (e.isIntersecting) {
e.target.classList.add('is-visible');
io.unobserve(e.target);
}
});
}, { threshold: 0.08 });
sections.forEach(function(s) { io.observe(s); });
})();
</script>
<!-- Script navigation latérale (active state au scroll) -->
<script>
(function() {
var links = document.querySelectorAll('.nw-sidenav__link');
var subLinks = document.querySelectorAll('.nw-sidenav__sub-link');
var targets = document.querySelectorAll('[id^="section-"], #nw-intro');
var markerTargets = document.querySelectorAll('[id^="marker-"]');
if (!links.length || !targets.length) return;
// Observer sections principales
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
links.forEach(function(l) { l.classList.remove('is-active'); });
document.querySelectorAll('.nw-sidenav__item').forEach(function(li) {
li.classList.remove('is-parent-active');
});
var active = document.querySelector('.nw-sidenav__link[href="#' + entry.target.id + '"]');
if (active) {
active.classList.add('is-active');
var parentLi = active.closest('.nw-sidenav__item');
if (parentLi) parentLi.classList.add('is-parent-active');
}
}
});
}, { rootMargin: '-20% 0px -70% 0px' });
targets.forEach(function(t) { observer.observe(t); });
// Observer marqueurs pour les sous-liens
if (markerTargets.length && subLinks.length) {
var subObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
subLinks.forEach(function(l) { l.classList.remove('is-active'); });
var active = document.querySelector('.nw-sidenav__sub-link[href="#' + entry.target.id + '"]');
if (active) active.classList.add('is-active');
}
});
}, { rootMargin: '-15% 0px -65% 0px' });
markerTargets.forEach(function(t) { subObserver.observe(t); });
}
})();
</script>
</body>
</html>