@zevra/ui 0.11.0 → 0.12.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.
- package/DECISIONS.md +28 -0
- package/README.md +10 -3
- package/demo/index.html +1 -0
- package/dist/css-inline.d.ts.map +1 -1
- package/dist/css-inline.js +1 -1
- package/dist/css-inline.js.map +1 -1
- package/package.json +2 -1
- package/react/css-inline.ts +1 -1
- package/src/motion.css +184 -105
- package/src/motion.js +53 -0
- package/src/surfaces.css +9 -0
package/src/motion.css
CHANGED
|
@@ -14,9 +14,24 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ─── Comment c'est construit ────────────────────────────────────────────
|
|
16
16
|
* L'état AU REPOS de chaque élément est son état FINAL : sans animation, la
|
|
17
|
-
* page est complète et lisible.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* page est complète et lisible.
|
|
18
|
+
*
|
|
19
|
+
* Les ENTRÉES se jouent UNE FOIS (arbitrage du 26/08/2026, DECISIONS.md) :
|
|
20
|
+
* l'ancien pilotage par `animation-timeline: view()` rejouait chaque entrée
|
|
21
|
+
* à l'envers dès qu'on remontait — le contenu disparaissait sous le lecteur,
|
|
22
|
+
* et une grille haute n'était jamais « contenue », donc jamais posée. Le
|
|
23
|
+
* paquet arme désormais les entrées par `html.zv-motion` (posé par
|
|
24
|
+
* motion.js, importable en `@zevra/ui/motion.js`), et chaque élément se
|
|
25
|
+
* révèle quand il entre au viewport, puis N'EN BOUGE PLUS. Sans JavaScript,
|
|
26
|
+
* rien n'est armé : la page s'affiche complète — même dégradation douce
|
|
27
|
+
* qu'avant.
|
|
28
|
+
*
|
|
29
|
+
* Un échafaudage de scènes (l'expérience épinglée d'une home) peut vouloir
|
|
30
|
+
* garder le pilotage au défilement : poser `data-zv-motion-skip` sur la
|
|
31
|
+
* scène — motion.js n'y touche pas — et re-brancher les @keyframes du
|
|
32
|
+
* paquet sur ses timelines nommées. Seuls la barre de progression
|
|
33
|
+
* (zv-anim-progress) et le point d'étape (zv-anim-dot-active) restent
|
|
34
|
+
* scroll-pilotés ici : suivre la lecture est leur raison d'être.
|
|
20
35
|
*
|
|
21
36
|
* Les deux boucles (drift, breathe) animent les propriétés `translate` et
|
|
22
37
|
* `scale` INDÉPENDANTES, jamais `transform` : les éléments concernés — halo
|
|
@@ -69,146 +84,210 @@
|
|
|
69
84
|
animation: zv-breathe 9s ease-in-out infinite;
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
/* ─── Entrées
|
|
73
|
-
*
|
|
74
|
-
*
|
|
87
|
+
/* ─── Entrées : une révélation par visite ───────────────────────────────
|
|
88
|
+
* Les @keyframes vivent au niveau racine : un échafaudage de scènes peut
|
|
89
|
+
* les re-brancher sur ses propres timelines nommées (voir l'en-tête). */
|
|
75
90
|
|
|
76
|
-
@
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
from {
|
|
81
|
-
opacity: 0;
|
|
82
|
-
transform: translateY(32px);
|
|
83
|
-
}
|
|
91
|
+
@keyframes zv-rise {
|
|
92
|
+
from {
|
|
93
|
+
opacity: 0;
|
|
94
|
+
transform: translateY(32px);
|
|
84
95
|
}
|
|
96
|
+
}
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
/* Le même mouvement, DOSÉ pour les blocs des pages intérieures : douze
|
|
99
|
+
* pixels au lieu de trente-deux — le bloc est posé avant que l'œil ait fini
|
|
100
|
+
* d'arriver, et une visite répétée ne le remarque plus. */
|
|
101
|
+
@keyframes zv-rise-doux {
|
|
102
|
+
from {
|
|
103
|
+
opacity: 0;
|
|
104
|
+
transform: translateY(12px);
|
|
90
105
|
}
|
|
106
|
+
}
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* change que l'amplitude et la plage. La home garde l'entrée
|
|
97
|
-
* pleine : c'est elle la signature, le doux est le quotidien. */
|
|
98
|
-
@keyframes zv-rise-doux {
|
|
99
|
-
from {
|
|
100
|
-
opacity: 0;
|
|
101
|
-
transform: translateY(12px);
|
|
102
|
-
}
|
|
108
|
+
@keyframes zv-glyph-in {
|
|
109
|
+
from {
|
|
110
|
+
opacity: 0;
|
|
111
|
+
transform: translateY(40px) scale(0.7);
|
|
103
112
|
}
|
|
113
|
+
}
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
/* La signature de la page d'accueil : les glyphes convergent vers le Z
|
|
116
|
+
* depuis les quatre coins — la seule entrée latérale du système, latérale
|
|
117
|
+
* parce qu'elle converge, pas parce qu'elle glisse. */
|
|
118
|
+
@keyframes zv-converge-tl {
|
|
119
|
+
from {
|
|
120
|
+
opacity: 0;
|
|
121
|
+
transform: translate(-420px, -420px) scale(1.5);
|
|
108
122
|
}
|
|
123
|
+
}
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
transform: translateY(40px) scale(0.7);
|
|
115
|
-
}
|
|
125
|
+
@keyframes zv-converge-tr {
|
|
126
|
+
from {
|
|
127
|
+
opacity: 0;
|
|
128
|
+
transform: translate(420px, -420px) scale(1.5);
|
|
116
129
|
}
|
|
130
|
+
}
|
|
117
131
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
@keyframes zv-converge-bl {
|
|
133
|
+
from {
|
|
134
|
+
opacity: 0;
|
|
135
|
+
transform: translate(-420px, 420px) scale(1.5);
|
|
122
136
|
}
|
|
137
|
+
}
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
@keyframes zv-converge-tl {
|
|
129
|
-
from {
|
|
130
|
-
opacity: 0;
|
|
131
|
-
transform: translate(-420px, -420px) scale(1.5);
|
|
132
|
-
}
|
|
139
|
+
@keyframes zv-converge-br {
|
|
140
|
+
from {
|
|
141
|
+
opacity: 0;
|
|
142
|
+
transform: translate(420px, 420px) scale(1.5);
|
|
133
143
|
}
|
|
144
|
+
}
|
|
134
145
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
146
|
+
@keyframes zv-pop {
|
|
147
|
+
from {
|
|
148
|
+
opacity: 0;
|
|
149
|
+
transform: scale(0.6);
|
|
140
150
|
}
|
|
151
|
+
}
|
|
141
152
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
/* Le filet de spectre se dessine : on anime bien la largeur, et non une
|
|
154
|
+
* mise à l'échelle — le dégradé doit se révéler, pas s'étirer. */
|
|
155
|
+
@keyframes zv-filet {
|
|
156
|
+
from {
|
|
157
|
+
width: 0;
|
|
147
158
|
}
|
|
159
|
+
}
|
|
148
160
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
/* ─── Le contrat au repos : des définitions inertes ─────────────────────
|
|
162
|
+
* Chaque classe d'entrée possède ici sa définition PROPRE, volontairement
|
|
163
|
+
* inerte tant que rien n'est armé : sans motion.js, aucune animation n'est
|
|
164
|
+
* attachée et ces propriétés n'ont pas d'effet — la page s'affiche
|
|
165
|
+
* complète. C'est aussi le point d'ancrage du dosage : le décalage d'un
|
|
166
|
+
* groupe se règle en posant --zv-delay par élément (120 ms). */
|
|
167
|
+
|
|
168
|
+
.zv-anim-rise {
|
|
169
|
+
animation-delay: var(--zv-delay);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.zv-anim-rise--doux {
|
|
173
|
+
animation-duration: 0.45s;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.zv-anim-glyph-in {
|
|
177
|
+
animation-delay: var(--zv-delay);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.zv-anim-converge {
|
|
181
|
+
animation-delay: var(--zv-delay);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.zv-anim-converge--tr {
|
|
185
|
+
animation-name: zv-converge-tr;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.zv-anim-converge--bl {
|
|
189
|
+
animation-name: zv-converge-bl;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.zv-anim-converge--br {
|
|
193
|
+
animation-name: zv-converge-br;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.zv-anim-pop {
|
|
197
|
+
animation-delay: var(--zv-delay);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Les six vagues : 70 ms d'écart — un délai en secondes retrouve son sens
|
|
201
|
+
* maintenant que l'entrée est temporelle. */
|
|
202
|
+
.zv-anim-pop--2 { animation-delay: calc(var(--zv-delay) + 0.07s); }
|
|
203
|
+
.zv-anim-pop--3 { animation-delay: calc(var(--zv-delay) + 0.14s); }
|
|
204
|
+
.zv-anim-pop--4 { animation-delay: calc(var(--zv-delay) + 0.21s); }
|
|
205
|
+
.zv-anim-pop--5 { animation-delay: calc(var(--zv-delay) + 0.28s); }
|
|
206
|
+
.zv-anim-pop--6 { animation-delay: calc(var(--zv-delay) + 0.35s); }
|
|
207
|
+
|
|
208
|
+
.zv-anim-filet {
|
|
209
|
+
animation-delay: var(--zv-delay);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ─── L'armement, puis la libération ────────────────────────────────────
|
|
213
|
+
* `:where(html.zv-motion)` garde la spécificité d'une classe simple : les
|
|
214
|
+
* définitions inertes ci-dessus (délais, noms, durées) restent lisibles à
|
|
215
|
+
* travers l'armement, et `.zv-in` — plus bas dans le fichier — gagne à la
|
|
216
|
+
* cascade sans surenchère. Armé, l'élément attend en PAUSE sur la première
|
|
217
|
+
* image de son animation (fill both) ; libéré, elle se joue une fois et
|
|
218
|
+
* s'arrête sur l'état final. Le défilement n'y reviendra jamais. */
|
|
219
|
+
|
|
220
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
221
|
+
/* 3 · Entrée standard de tout contenu texte et panneau. */
|
|
222
|
+
:where(html.zv-motion) .zv-anim-rise {
|
|
223
|
+
animation: zv-rise 0.6s var(--zv-easing) both paused;
|
|
224
|
+
animation-delay: var(--zv-delay);
|
|
154
225
|
}
|
|
155
226
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
227
|
+
/* 3 bis · Le dosage des pages intérieures : se pose AVEC zv-anim-rise,
|
|
228
|
+
* dont il ne change que l'amplitude et la durée. La home garde
|
|
229
|
+
* l'entrée pleine : c'est elle la signature, le doux est le
|
|
230
|
+
* quotidien. */
|
|
231
|
+
:where(html.zv-motion) .zv-anim-rise--doux {
|
|
232
|
+
animation-name: zv-rise-doux;
|
|
233
|
+
animation-duration: 0.45s;
|
|
160
234
|
}
|
|
161
235
|
|
|
162
|
-
.
|
|
236
|
+
/* 4 · Entrée des glyphes produit : montée et grossissement. */
|
|
237
|
+
:where(html.zv-motion) .zv-anim-glyph-in {
|
|
238
|
+
animation: zv-glyph-in 0.6s var(--zv-easing) both paused;
|
|
239
|
+
animation-delay: var(--zv-delay);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* 5 · La convergence. Direction par modificateur. */
|
|
243
|
+
:where(html.zv-motion) .zv-anim-converge {
|
|
244
|
+
animation: zv-converge-tl 0.7s var(--zv-easing) both paused;
|
|
245
|
+
animation-delay: var(--zv-delay);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
:where(html.zv-motion) .zv-anim-converge--tr {
|
|
163
249
|
animation-name: zv-converge-tr;
|
|
164
250
|
}
|
|
165
251
|
|
|
166
|
-
.zv-anim-converge--bl {
|
|
252
|
+
:where(html.zv-motion) .zv-anim-converge--bl {
|
|
167
253
|
animation-name: zv-converge-bl;
|
|
168
254
|
}
|
|
169
255
|
|
|
170
|
-
.zv-anim-converge--br {
|
|
256
|
+
:where(html.zv-motion) .zv-anim-converge--br {
|
|
171
257
|
animation-name: zv-converge-br;
|
|
172
258
|
}
|
|
173
259
|
|
|
174
|
-
/* 6 · Badges, jetons d'entités, éléments de preuve.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
opacity: 0;
|
|
179
|
-
transform: scale(0.6);
|
|
180
|
-
}
|
|
260
|
+
/* 6 · Badges, jetons d'entités, éléments de preuve — en cascade. */
|
|
261
|
+
:where(html.zv-motion) .zv-anim-pop {
|
|
262
|
+
animation: zv-pop 0.4s var(--zv-easing) both paused;
|
|
263
|
+
animation-delay: var(--zv-delay);
|
|
181
264
|
}
|
|
182
265
|
|
|
183
|
-
.zv-anim-pop {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
266
|
+
:where(html.zv-motion) .zv-anim-pop--2 { animation-delay: calc(var(--zv-delay) + 0.07s); }
|
|
267
|
+
:where(html.zv-motion) .zv-anim-pop--3 { animation-delay: calc(var(--zv-delay) + 0.14s); }
|
|
268
|
+
:where(html.zv-motion) .zv-anim-pop--4 { animation-delay: calc(var(--zv-delay) + 0.21s); }
|
|
269
|
+
:where(html.zv-motion) .zv-anim-pop--5 { animation-delay: calc(var(--zv-delay) + 0.28s); }
|
|
270
|
+
:where(html.zv-motion) .zv-anim-pop--6 { animation-delay: calc(var(--zv-delay) + 0.35s); }
|
|
188
271
|
|
|
189
|
-
/*
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.zv-anim-pop--4 { animation-range: contain 48% contain 88%; }
|
|
194
|
-
.zv-anim-pop--5 { animation-range: contain 54% contain 94%; }
|
|
195
|
-
.zv-anim-pop--6 { animation-range: contain 60% contain 100%; }
|
|
196
|
-
|
|
197
|
-
/* 7 · Le filet de spectre se dessine à l'entrée de chaque en-tête de
|
|
198
|
-
* section. On anime bien la largeur, et non une mise à l'échelle :
|
|
199
|
-
* le dégradé doit se révéler, pas s'étirer. */
|
|
200
|
-
@keyframes zv-filet {
|
|
201
|
-
from {
|
|
202
|
-
width: 0;
|
|
203
|
-
}
|
|
272
|
+
/* 7 · Le filet de spectre, à l'entrée de chaque en-tête de section. */
|
|
273
|
+
:where(html.zv-motion) .zv-anim-filet {
|
|
274
|
+
animation: zv-filet 0.5s var(--zv-easing) both paused;
|
|
275
|
+
animation-delay: var(--zv-delay);
|
|
204
276
|
}
|
|
277
|
+
}
|
|
205
278
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
279
|
+
/* La libération, commune : posée par motion.js à l'entrée au viewport, une
|
|
280
|
+
* seule fois. Sa place APRÈS l'armement fait tout : à spécificité égale,
|
|
281
|
+
* c'est elle qui a le dernier mot. */
|
|
282
|
+
.zv-in {
|
|
283
|
+
animation-play-state: running;
|
|
284
|
+
}
|
|
211
285
|
|
|
286
|
+
/* ─── Les deux lectures du défilement, conservées ───────────────────────
|
|
287
|
+
* Suivre la position de lecture est leur raison d'être : elles doivent
|
|
288
|
+
* avancer ET reculer avec le lecteur. */
|
|
289
|
+
|
|
290
|
+
@supports (animation-timeline: view()) {
|
|
212
291
|
/* 8 · Le point d'étape s'allume à l'entrée de sa scène et s'éteint à la
|
|
213
292
|
* sortie. Un seul actif à la fois. */
|
|
214
293
|
@keyframes zv-dot-active {
|
package/src/motion.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Zevra UI — motion.js : la révélation unique des entrées.
|
|
2
|
+
*
|
|
3
|
+
* Rôle exact, et rien d'autre :
|
|
4
|
+
* 1. poser `zv-motion` sur <html> — c'est cette classe qui ARME les
|
|
5
|
+
* entrées de motion.css (sans elle, la page s'affiche complète) ;
|
|
6
|
+
* 2. poser `zv-in`, UNE fois, sur chaque élément d'entrée quand il
|
|
7
|
+
* touche le viewport — l'animation se joue, s'arrête sur l'état
|
|
8
|
+
* final, et on n'observe plus l'élément : remonter ne défait rien.
|
|
9
|
+
*
|
|
10
|
+
* Un échafaudage de scènes scroll-pilotées (l'expérience épinglée d'une
|
|
11
|
+
* home) pose `data-zv-motion-skip` sur ses scènes : rien n'y est observé,
|
|
12
|
+
* ses timelines nommées gardent la main.
|
|
13
|
+
*
|
|
14
|
+
* S'importe une fois par page : `import '@zevra/ui/motion.js'`. Pour un
|
|
15
|
+
* armement avant première peinture (aucun éclair du contenu), l'app peut
|
|
16
|
+
* AUSSI poser la classe en inline dans <head> :
|
|
17
|
+
* <script>document.documentElement.classList.add('zv-motion')</script>
|
|
18
|
+
* motion.js est idempotent par-dessus.
|
|
19
|
+
*/
|
|
20
|
+
(() => {
|
|
21
|
+
if (typeof document === 'undefined' || typeof IntersectionObserver === 'undefined') return;
|
|
22
|
+
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
23
|
+
|
|
24
|
+
document.documentElement.classList.add('zv-motion');
|
|
25
|
+
|
|
26
|
+
const ENTREES = '.zv-anim-rise, .zv-anim-glyph-in, .zv-anim-converge, .zv-anim-pop, .zv-anim-filet';
|
|
27
|
+
|
|
28
|
+
const armer = () => {
|
|
29
|
+
const io = new IntersectionObserver(
|
|
30
|
+
(entries) => {
|
|
31
|
+
for (const e of entries) {
|
|
32
|
+
if (!e.isIntersecting) continue;
|
|
33
|
+
e.target.classList.add('zv-in');
|
|
34
|
+
io.unobserve(e.target);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
/* Déclenche un peu AVANT que l'élément soit franchement visible :
|
|
38
|
+
* l'entrée accompagne l'arrivée au lieu de la suivre. */
|
|
39
|
+
{ rootMargin: '0px 0px -8% 0px', threshold: 0.01 }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
for (const el of document.querySelectorAll(ENTREES)) {
|
|
43
|
+
if (el.closest('[data-zv-motion-skip]')) continue;
|
|
44
|
+
io.observe(el);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (document.readyState === 'loading') {
|
|
49
|
+
document.addEventListener('DOMContentLoaded', armer, { once: true });
|
|
50
|
+
} else {
|
|
51
|
+
armer();
|
|
52
|
+
}
|
|
53
|
+
})();
|
package/src/surfaces.css
CHANGED
|
@@ -138,6 +138,15 @@
|
|
|
138
138
|
z-index: 1;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/* L'emphase de base.css pose --ink sur les strong de zv-body/zv-lead — sur
|
|
142
|
+
* le navy du manifeste, cette encre disparaît dans le fond. Dans la bande,
|
|
143
|
+
* l'emphase hérite de la couleur ambiante (blanc), et seule la graisse la
|
|
144
|
+
* distingue. */
|
|
145
|
+
.zv-band .zv-body strong,
|
|
146
|
+
.zv-band .zv-lead strong {
|
|
147
|
+
color: inherit;
|
|
148
|
+
}
|
|
149
|
+
|
|
141
150
|
/* Filigrane : le glyphe, blanchi et presque effacé, débordant du cadre. */
|
|
142
151
|
.zv-band__watermark {
|
|
143
152
|
position: absolute;
|