@agence-zef/astro 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,9 @@
8
8
  * - un autre bouton du même popover pendant qu'il est ouvert : le popover se déplace vers lui
9
9
  * tout de suite, sans se fermer ni se rouvrir (le site met son contenu à jour dans son propre
10
10
  * `click`) ; le même bouton : il se ferme ;
11
- * - clic en dehors ou Échap : il se ferme (sauf `manual`).
11
+ * - clic en dehors ou Échap : il se ferme (sauf `manual`) ;
12
+ * - `popovertargetaction="hide"` (croix de fermeture, placée dans le popover ou ailleurs) : il se
13
+ * ferme ; `popovertargetaction="show"` : il s'ouvre contre ce bouton sans jamais se fermer.
12
14
  */
13
15
  interface Props {
14
16
  id: string;
@@ -59,7 +61,12 @@ const { id, manual = false, placement = "bottom", class: className } = Astro.pro
59
61
  if (trigger && pop?.hasAttribute("data-zef-popover")) {
60
62
  // Le comportement natif (basculer) est remplacé : déplacer plutôt que fermer.
61
63
  event.preventDefault();
62
- if (isOpen(pop) && opener.get(pop) === trigger) return pop.hidePopover();
64
+ const action = trigger.getAttribute("popovertargetaction");
65
+ if (action === "hide") {
66
+ if (isOpen(pop)) pop.hidePopover();
67
+ return;
68
+ }
69
+ if (action !== "show" && isOpen(pop) && opener.get(pop) === trigger) return pop.hidePopover();
63
70
  anchorTo(pop, trigger);
64
71
  if (!isOpen(pop)) pop.showPopover();
65
72
  return;
package/dist/index.d.ts CHANGED
@@ -32,6 +32,17 @@ export type ZefOptions = {
32
32
  checkout?: string;
33
33
  confirmation?: string;
34
34
  };
35
+ /**
36
+ * Apparitions au défilement (`data-reveal` dans les sections, styles dans `styles.css`) :
37
+ * actives par défaut. `false` les coupe sur tout le site ; `once: false` les rejoue à chaque
38
+ * entrée dans l'écran ; `threshold` (part visible, 0 à 1) et `rootMargin` règlent le moment
39
+ * où elles partent.
40
+ */
41
+ animations?: false | {
42
+ once?: boolean;
43
+ threshold?: number;
44
+ rootMargin?: string;
45
+ };
35
46
  };
36
47
  /**
37
48
  * Intégration Astro du CMS ZEF (cahier des charges § 10) : route attrape-tout des pages
package/dist/index.js CHANGED
@@ -7,6 +7,15 @@ export * from "./accounts.js";
7
7
  export * from "./helpers.js";
8
8
  export * from "./seo.js";
9
9
  export * from "./shop.js";
10
+ /**
11
+ * Script en tête de page : active les apparitions (`data-zef-reveal` sur `<html>`, avec leurs
12
+ * réglages) avant le premier affichage, sauf dans les routes de l'éditeur (`/__zef/…`). Filet de
13
+ * sécurité : si le script des apparitions n'a pas démarré après 2,5 s, tout redevient visible.
14
+ */
15
+ function revealHead(animations) {
16
+ const settings = JSON.stringify(animations || {});
17
+ return `(function(){var d=document.documentElement;if(location.pathname.indexOf("/__zef/")===0)return;d.setAttribute("data-zef-reveal",${JSON.stringify(settings)});setTimeout(function(){if(!window.__zefReveal)d.removeAttribute("data-zef-reveal")},2500)})();`;
18
+ }
10
19
  const MEDIA_URL = "https://medias.agence-zef.fr";
11
20
  /** `rendering` de `zef.config.ts` (lu comme la CLI, avec jiti) ; `static` en cas de doute. */
12
21
  async function readRendering(file) {
@@ -72,7 +81,7 @@ export default function zef(options = {}) {
72
81
  return {
73
82
  name: "@agence-zef/astro",
74
83
  hooks: {
75
- "astro:config:setup": async ({ config, command, injectRoute, updateConfig, addWatchFile, addMiddleware, logger, }) => {
84
+ "astro:config:setup": async ({ config, command, injectRoute, injectScript, updateConfig, addWatchFile, addMiddleware, logger, }) => {
76
85
  const root = fileURLToPath(config.root);
77
86
  loadZefEnv(root);
78
87
  // Redirections du CMS (C-17 : slugs modifiés, pages déplacées, redirections manuelles).
@@ -104,6 +113,11 @@ export default function zef(options = {}) {
104
113
  if (config.build.inlineStylesheets === "auto") {
105
114
  updateConfig({ build: { inlineStylesheets: "always" } });
106
115
  }
116
+ // Apparitions au défilement (`data-reveal`) : actives sauf `animations: false`.
117
+ if (options.animations !== false) {
118
+ injectScript("head-inline", revealHead(options.animations));
119
+ injectScript("page", 'import "@agence-zef/astro/reveal";');
120
+ }
107
121
  addWatchFile(join(root, ".env"));
108
122
  const layout = join(root, options.layout ?? "src/layouts/Page.astro");
109
123
  const configFile = join(root, "zef.config.ts");
@@ -0,0 +1,2 @@
1
+ /** Observe les nouveaux `data-reveal` de la page (rang dans la cascade, délai). */
2
+ export declare function scanReveals(scope?: ParentNode): void;
package/dist/reveal.js ADDED
@@ -0,0 +1,75 @@
1
+ /*
2
+ * Apparitions au défilement (`data-reveal`), chargé sur chaque page par l'intégration (option
3
+ * `animations`). Le visuel est dans `styles.css` ; ce script pose seulement `data-zef-visible`
4
+ * quand un élément entre dans l'écran, puis `data-zef-done` une fois l'effet joué (l'élément
5
+ * retrouve alors ses propres transitions). Ne fait rien sans `data-zef-reveal` sur `<html>`
6
+ * (aperçu de l'éditeur, animations coupées).
7
+ *
8
+ * Attributs : `data-reveal` (effet), `data-reveal-delay` (ms), `data-reveal-stagger` sur un
9
+ * parent (ses `data-reveal` arrivent en cascade), `data-reveal-scroll` (100 % CSS, ignoré ici).
10
+ */
11
+ const root = document.documentElement;
12
+ const observed = new WeakSet();
13
+ let observer = null;
14
+ let options = { once: true, threshold: 0.15, rootMargin: "0px 0px -5% 0px" };
15
+ /** Fin de l'effet : l'élément ne porte plus la transition d'apparition. */
16
+ function done(el) {
17
+ const style = getComputedStyle(el);
18
+ const seconds = (v) => Math.max(...v.split(",").map((s) => Number.parseFloat(s) || 0));
19
+ const ms = (seconds(style.transitionDuration) + seconds(style.transitionDelay)) * 1000;
20
+ window.setTimeout(() => el.setAttribute("data-zef-done", ""), ms + 50);
21
+ }
22
+ function onIntersect(entries) {
23
+ for (const { target, isIntersecting } of entries) {
24
+ const el = target;
25
+ if (isIntersecting) {
26
+ el.setAttribute("data-zef-visible", "");
27
+ if (options.once) {
28
+ observer?.unobserve(el);
29
+ done(el);
30
+ }
31
+ }
32
+ else if (!options.once) {
33
+ el.removeAttribute("data-zef-visible");
34
+ }
35
+ }
36
+ }
37
+ /** Observe les nouveaux `data-reveal` de la page (rang dans la cascade, délai). */
38
+ export function scanReveals(scope = document) {
39
+ if (!observer)
40
+ return;
41
+ for (const parent of scope.querySelectorAll("[data-reveal-stagger]")) {
42
+ let index = 0;
43
+ for (const el of parent.querySelectorAll("[data-reveal]")) {
44
+ if (el.closest("[data-reveal-stagger]") !== parent)
45
+ continue;
46
+ el.style.setProperty("--zef-reveal-index", String(index++));
47
+ }
48
+ }
49
+ for (const el of scope.querySelectorAll("[data-reveal]:not([data-reveal-scroll])")) {
50
+ if (observed.has(el))
51
+ continue;
52
+ observed.add(el);
53
+ const delay = Number(el.dataset.revealDelay);
54
+ if (delay > 0)
55
+ el.style.setProperty("--zef-reveal-delay", `${delay}ms`);
56
+ observer.observe(el);
57
+ }
58
+ }
59
+ function start() {
60
+ const raw = root.getAttribute("data-zef-reveal");
61
+ if (raw === null || observer)
62
+ return;
63
+ window.__zefReveal = true;
64
+ try {
65
+ options = { ...options, ...JSON.parse(raw || "{}") };
66
+ }
67
+ catch { }
68
+ observer = new IntersectionObserver(onIntersect, {
69
+ threshold: options.threshold,
70
+ rootMargin: options.rootMargin,
71
+ });
72
+ scanReveals();
73
+ }
74
+ start();
75
+ document.addEventListener("astro:page-load", () => scanReveals());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agence-zef/astro",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Intégration Astro du CMS ZEF : routes, rendu des sections, aperçu, composants réutilisables sans style.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -65,6 +65,10 @@
65
65
  "types": "./dist/ui.d.ts",
66
66
  "import": "./dist/ui.js"
67
67
  },
68
+ "./reveal": {
69
+ "types": "./dist/reveal.d.ts",
70
+ "import": "./dist/reveal.js"
71
+ },
68
72
  "./routes/page-cached.astro": "./components/routes/page-cached.astro",
69
73
  "./routes/revalidate.ts": "./components/routes/revalidate.ts"
70
74
  },
@@ -86,8 +90,8 @@
86
90
  "dependencies": {
87
91
  "jiti": "^2.6.1",
88
92
  "vanilla-cookieconsent": "3.1.0",
89
- "@agence-zef/schema": "0.10.0",
90
- "@agence-zef/sdk": "0.10.0"
93
+ "@agence-zef/schema": "0.11.0",
94
+ "@agence-zef/sdk": "0.11.0"
91
95
  },
92
96
  "homepage": "https://agence-zef.fr",
93
97
  "scripts": {
package/styles.css CHANGED
@@ -686,3 +686,130 @@
686
686
  flex-basis: 100%;
687
687
  }
688
688
  }
689
+
690
+ /*
691
+ * Apparitions au défilement (`data-reveal`) : le visuel est ici, en CSS ; le script de
692
+ * l'intégration (`src/reveal.ts`) pose seulement `data-zef-visible` quand l'élément entre dans
693
+ * l'écran. L'état masqué ne s'applique que sous `:root[data-zef-reveal]`, posé en tête de page
694
+ * par l'intégration (option `animations`) : sans JavaScript, dans l'aperçu de l'éditeur ou si le
695
+ * script ne se charge pas, tout reste visible.
696
+ *
697
+ * Réglages par site : les variables `--zef-reveal-*` dans le `:root` du site ; un effet maison
698
+ * se déclare avec les variables `--zef-reveal-from-*` (voir `CLAUDE.md` du kit).
699
+ */
700
+ :where(:root) {
701
+ --zef-reveal-duration: 0.7s;
702
+ --zef-reveal-easing: cubic-bezier(0.22, 1, 0.36, 1);
703
+ --zef-reveal-distance: 1.5rem;
704
+ --zef-reveal-scale: 0.94;
705
+ --zef-reveal-blur: 8px;
706
+ --zef-reveal-stagger: 80ms;
707
+ --zef-reveal-scroll-range: entry 0% cover 35%;
708
+ }
709
+
710
+ /* Point de départ de chaque effet (fade-up par défaut) */
711
+ :where([data-reveal]) {
712
+ --zef-reveal-from-opacity: 0;
713
+ --zef-reveal-from-translate: 0 var(--zef-reveal-distance);
714
+ --zef-reveal-from-scale: none;
715
+ --zef-reveal-from-filter: none;
716
+ }
717
+ :where([data-reveal="fade"]) {
718
+ --zef-reveal-from-translate: none;
719
+ }
720
+ :where([data-reveal="fade-down"]) {
721
+ --zef-reveal-from-translate: 0 calc(var(--zef-reveal-distance) * -1);
722
+ }
723
+ :where([data-reveal="slide-left"]) {
724
+ --zef-reveal-from-translate: var(--zef-reveal-distance) 0;
725
+ }
726
+ :where([data-reveal="slide-right"]) {
727
+ --zef-reveal-from-translate: calc(var(--zef-reveal-distance) * -1) 0;
728
+ }
729
+ :where([data-reveal="zoom"]) {
730
+ --zef-reveal-from-translate: none;
731
+ --zef-reveal-from-scale: var(--zef-reveal-scale);
732
+ }
733
+ :where([data-reveal="blur"]) {
734
+ --zef-reveal-from-translate: none;
735
+ --zef-reveal-from-filter: blur(var(--zef-reveal-blur));
736
+ }
737
+ :where([data-reveal="clip"]) {
738
+ --zef-reveal-from-opacity: 1;
739
+ --zef-reveal-from-translate: none;
740
+ }
741
+
742
+ /* Déclenché à l'entrée dans l'écran : transition de l'état de départ à l'état normal */
743
+ :root[data-zef-reveal] [data-reveal]:not([data-reveal-scroll], [data-zef-done]) {
744
+ transition-property: opacity, translate, scale, filter, clip-path;
745
+ transition-duration: var(--zef-reveal-duration);
746
+ transition-timing-function: var(--zef-reveal-easing);
747
+ transition-delay: calc(
748
+ var(--zef-reveal-delay, 0ms) +
749
+ var(--zef-reveal-index, 0) *
750
+ var(--zef-reveal-stagger)
751
+ );
752
+ }
753
+ :root[data-zef-reveal] [data-reveal]:not([data-reveal-scroll], [data-zef-visible]) {
754
+ opacity: var(--zef-reveal-from-opacity);
755
+ translate: var(--zef-reveal-from-translate);
756
+ scale: var(--zef-reveal-from-scale);
757
+ filter: var(--zef-reveal-from-filter);
758
+ }
759
+ /* Dévoilement par masque : seul effet qui touche à clip-path (les autres gardent celui du site) */
760
+ :root[data-zef-reveal] [data-reveal="clip"]:not([data-reveal-scroll], [data-zef-visible]) {
761
+ clip-path: inset(0 100% 0 0);
762
+ }
763
+ :root[data-zef-reveal] [data-reveal="clip"][data-zef-visible]:not([data-zef-done]) {
764
+ clip-path: inset(0 0 0 0);
765
+ }
766
+
767
+ /*
768
+ * Variante 100 % CSS liée au défilement (`data-reveal-scroll`) : l'effet avance et recule avec
769
+ * le défilement. Navigateurs sans `animation-timeline` : rien n'est animé, tout est visible.
770
+ */
771
+ @keyframes zef-reveal {
772
+ from {
773
+ opacity: var(--zef-reveal-from-opacity);
774
+ translate: var(--zef-reveal-from-translate);
775
+ scale: var(--zef-reveal-from-scale);
776
+ filter: var(--zef-reveal-from-filter);
777
+ }
778
+ }
779
+ @keyframes zef-reveal-clip {
780
+ from {
781
+ clip-path: inset(0 100% 0 0);
782
+ }
783
+ to {
784
+ clip-path: inset(0 0 0 0);
785
+ }
786
+ }
787
+ @supports (animation-timeline: view()) {
788
+ @media (prefers-reduced-motion: no-preference) {
789
+ :root[data-zef-reveal] [data-reveal][data-reveal-scroll] {
790
+ animation: zef-reveal linear both;
791
+ animation-timeline: view();
792
+ animation-range: var(--zef-reveal-scroll-range);
793
+ }
794
+ :root[data-zef-reveal] [data-reveal="clip"][data-reveal-scroll] {
795
+ animation-name: zef-reveal-clip;
796
+ }
797
+ }
798
+ }
799
+
800
+ /* Mouvement réduit : un fondu court, sans déplacement ni flou */
801
+ @media (prefers-reduced-motion: reduce) {
802
+ :where([data-reveal]) {
803
+ --zef-reveal-from-translate: none;
804
+ --zef-reveal-from-scale: none;
805
+ --zef-reveal-from-filter: none;
806
+ --zef-reveal-stagger: 0ms;
807
+ --zef-reveal-duration: 0.3s;
808
+ }
809
+ :where([data-reveal="clip"]) {
810
+ --zef-reveal-from-opacity: 0;
811
+ }
812
+ :root[data-zef-reveal] [data-reveal="clip"]:not([data-reveal-scroll], [data-zef-visible]) {
813
+ clip-path: none;
814
+ }
815
+ }