@zevra/ui 0.38.0 → 0.39.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 +45 -0
- package/dist/catalogue.d.ts.map +1 -1
- package/dist/catalogue.js +28 -6
- package/dist/catalogue.js.map +1 -1
- 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/dist/overlays.d.ts +26 -0
- package/dist/overlays.d.ts.map +1 -1
- package/dist/overlays.js +13 -1
- package/dist/overlays.js.map +1 -1
- package/package.json +1 -1
- package/react/catalogue.tsx +65 -5
- package/react/css-inline.ts +1 -1
- package/react/overlays.tsx +79 -1
- package/src/overlays.css +140 -2
package/react/overlays.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
// Enrobages : voile, tiroir, modale, notification flottante.
|
|
3
|
+
// Enrobages : voile, tiroir, volet, modale, notification flottante.
|
|
4
4
|
//
|
|
5
5
|
// SEUL fichier du paquet marqué « use client », avec un hook. Tout ce qui vit
|
|
6
6
|
// ici vole le regard à la page : il faut une touche Échap, un piège de focus
|
|
@@ -209,6 +209,84 @@ export function Modal({
|
|
|
209
209
|
)
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/* ─── Volet ────────────────────────────────────────────────────────── */
|
|
213
|
+
|
|
214
|
+
export interface VoletProps extends Omit<Div, 'title'> {
|
|
215
|
+
ouvert: boolean
|
|
216
|
+
onClose: () => void
|
|
217
|
+
titre?: ReactNode
|
|
218
|
+
/** Nom du dialogue quand le titre n'est pas une chaîne. */
|
|
219
|
+
label?: string
|
|
220
|
+
/** Actions de tête, à droite du titre (la croix, en général). */
|
|
221
|
+
actions?: ReactNode
|
|
222
|
+
/** Pied d'actions, fixe quand le corps défile. */
|
|
223
|
+
pied?: ReactNode
|
|
224
|
+
/** `normale` pour une fiche, `large` pour un document à lire. */
|
|
225
|
+
largeur?: 'normale' | 'large'
|
|
226
|
+
/** Vrai par défaut : dialogue modal, page voilée et figée. Faux : la page
|
|
227
|
+
* reste utilisable derrière (passer d'une pièce à l'autre d'une liste sans
|
|
228
|
+
* refermer). Échap ferme dans les deux cas. */
|
|
229
|
+
voile?: boolean
|
|
230
|
+
/** `nu` : corps sans marge, pour un document bord à bord (PDF, image). */
|
|
231
|
+
corps?: 'normal' | 'nu'
|
|
232
|
+
className?: string
|
|
233
|
+
children?: ReactNode
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Panneau latéral DROIT, à toutes les largeurs : un contenu qu'on consulte
|
|
237
|
+
* à côté d'une liste (un document, une fiche). Ne remplace ni le tiroir,
|
|
238
|
+
* qui est la navigation mobile, ni la modale, qui interrompt pour une
|
|
239
|
+
* décision. Reste monté fermé, comme le tiroir, pour glisser dehors. */
|
|
240
|
+
export function Volet({
|
|
241
|
+
ouvert,
|
|
242
|
+
onClose,
|
|
243
|
+
titre,
|
|
244
|
+
label,
|
|
245
|
+
actions,
|
|
246
|
+
pied,
|
|
247
|
+
largeur = 'normale',
|
|
248
|
+
voile = true,
|
|
249
|
+
corps = 'normal',
|
|
250
|
+
className,
|
|
251
|
+
children,
|
|
252
|
+
...reste
|
|
253
|
+
}: VoletProps) {
|
|
254
|
+
const panneau = useRef<HTMLDivElement>(null)
|
|
255
|
+
// Sans voile, la page reste à l'utilisateur : ni verrou de défilement, ni
|
|
256
|
+
// piège de focus (Tab doit pouvoir revenir à la liste). Échap ferme.
|
|
257
|
+
useOverlay({ ouvert, onClose, panneau: voile ? panneau : undefined, verrouille: voile })
|
|
258
|
+
const fermer = useCallback(() => onClose(), [onClose])
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
role="dialog"
|
|
263
|
+
aria-modal={voile ? 'true' : undefined}
|
|
264
|
+
aria-label={label ?? (typeof titre === 'string' ? titre : undefined)}
|
|
265
|
+
aria-hidden={!ouvert}
|
|
266
|
+
className={cx(
|
|
267
|
+
'zv-volet',
|
|
268
|
+
ouvert && 'zv-volet--open',
|
|
269
|
+
largeur === 'large' && 'zv-volet--large',
|
|
270
|
+
!voile && 'zv-volet--sans-voile',
|
|
271
|
+
className,
|
|
272
|
+
)}
|
|
273
|
+
{...reste}
|
|
274
|
+
>
|
|
275
|
+
{voile && <div className="zv-scrim" onClick={fermer} aria-hidden="true" />}
|
|
276
|
+
<div className="zv-volet__panel" ref={panneau}>
|
|
277
|
+
{(titre || actions) && (
|
|
278
|
+
<div className="zv-volet__head">
|
|
279
|
+
{titre && <h2 className="zv-volet__title">{titre}</h2>}
|
|
280
|
+
{actions && <div className="zv-volet__actions">{actions}</div>}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
<div className={cx('zv-volet__body', corps === 'nu' && 'zv-volet__body--nu')}>{children}</div>
|
|
284
|
+
{pied && <div className="zv-volet__foot">{pied}</div>}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
212
290
|
/* ─── Notification flottante ───────────────────────────────────────── */
|
|
213
291
|
|
|
214
292
|
/** Le conteneur ne capte RIEN : seules les notifications le font, sans quoi
|
package/src/overlays.css
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* l'échappement au clavier est du ressort du gabarit, que le CSS ne peut pas
|
|
13
13
|
* assurer seul.
|
|
14
14
|
*
|
|
15
|
-
* Empilement, dans l'ordre : voile 60, tiroir 80, modale 90,
|
|
15
|
+
* Empilement, dans l'ordre : voile 60, tiroir 80, volet 85, modale 90,
|
|
16
|
+
* notification 100.
|
|
16
17
|
* Les couches inférieures (2, 5, 40, 50) sont dans layout.css.
|
|
17
18
|
*
|
|
18
19
|
* OUVERTURE, deux écritures équivalentes, au choix du gabarit :
|
|
@@ -51,7 +52,8 @@
|
|
|
51
52
|
html:has(.zv-drawer[data-open]),
|
|
52
53
|
html:has(.zv-drawer--open),
|
|
53
54
|
html:has(.zv-modal[data-open]),
|
|
54
|
-
html:has(.zv-modal--open)
|
|
55
|
+
html:has(.zv-modal--open),
|
|
56
|
+
html:has(.zv-volet--open:not(.zv-volet--sans-voile)) {
|
|
55
57
|
overflow: hidden;
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -255,6 +257,135 @@ html:has(.zv-modal--open) {
|
|
|
255
257
|
border-top: 3px solid var(--err);
|
|
256
258
|
}
|
|
257
259
|
|
|
260
|
+
/* ─── Volet ─────────────────────────────────────────────────────────────
|
|
261
|
+
* ⚠️ HORS RELEVÉ (DECISIONS.md, 26/09/2026). Panneau latéral DROIT, à toutes
|
|
262
|
+
* les largeurs : il porte un contenu (un document, une fiche) qu'on consulte
|
|
263
|
+
* à côté d'une liste. Dérivé du tiroir (le glissement, le voile) et de la
|
|
264
|
+
* modale (tête, corps qui défile, pied fixe) ; seules ses deux largeurs sont
|
|
265
|
+
* nouvelles.
|
|
266
|
+
*
|
|
267
|
+
* Empilement 85 : devant le tiroir (80), derrière la modale (90), pour
|
|
268
|
+
* qu'une confirmation ouverte depuis le volet passe devant lui.
|
|
269
|
+
*
|
|
270
|
+
* Sans voile (`zv-volet--sans-voile`), le conteneur ne capte rien : seul le
|
|
271
|
+
* panneau reçoit les clics, et la page reste utilisable derrière. */
|
|
272
|
+
|
|
273
|
+
.zv-volet {
|
|
274
|
+
--zv-volet-largeur: min(560px, 92vw);
|
|
275
|
+
position: fixed;
|
|
276
|
+
inset: 0;
|
|
277
|
+
z-index: 85;
|
|
278
|
+
/* `visibility` plutôt que `display`, comme le tiroir : le panneau reste
|
|
279
|
+
* à l'écran le temps qu'il glisse dehors. */
|
|
280
|
+
visibility: hidden;
|
|
281
|
+
transition: visibility 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.zv-volet--large {
|
|
285
|
+
--zv-volet-largeur: min(960px, 96vw);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.zv-volet[data-open],
|
|
289
|
+
.zv-volet--open {
|
|
290
|
+
visibility: visible;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.zv-volet--sans-voile {
|
|
294
|
+
pointer-events: none;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Le voile du volet existe à toutes les largeurs, comme celui de la modale. */
|
|
298
|
+
.zv-volet .zv-scrim {
|
|
299
|
+
z-index: 0;
|
|
300
|
+
display: block;
|
|
301
|
+
opacity: 0;
|
|
302
|
+
transition: opacity 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.zv-volet[data-open] .zv-scrim,
|
|
306
|
+
.zv-volet--open .zv-scrim {
|
|
307
|
+
opacity: 1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.zv-volet__panel {
|
|
311
|
+
pointer-events: auto;
|
|
312
|
+
position: absolute;
|
|
313
|
+
z-index: 1;
|
|
314
|
+
inset-block: 0;
|
|
315
|
+
right: 0;
|
|
316
|
+
display: flex;
|
|
317
|
+
width: var(--zv-volet-largeur);
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
border-left: 1px solid var(--line);
|
|
320
|
+
border-radius: 0;
|
|
321
|
+
background: var(--white);
|
|
322
|
+
box-shadow: var(--shadow-panel);
|
|
323
|
+
transform: translateX(100%);
|
|
324
|
+
transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.zv-volet[data-open] .zv-volet__panel,
|
|
328
|
+
.zv-volet--open .zv-volet__panel {
|
|
329
|
+
transform: translateX(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.zv-volet__head {
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: space-between;
|
|
336
|
+
gap: 12px;
|
|
337
|
+
border-bottom: 1px solid var(--hairline);
|
|
338
|
+
padding: 14px 18px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.zv-volet__title {
|
|
342
|
+
min-width: 0;
|
|
343
|
+
margin: 0;
|
|
344
|
+
overflow: hidden;
|
|
345
|
+
text-overflow: ellipsis;
|
|
346
|
+
white-space: nowrap;
|
|
347
|
+
font-family: var(--font-display);
|
|
348
|
+
letter-spacing: -0.02em;
|
|
349
|
+
font-size: var(--app-h2);
|
|
350
|
+
font-weight: 400;
|
|
351
|
+
color: var(--ink);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.zv-volet__actions {
|
|
355
|
+
display: flex;
|
|
356
|
+
flex-shrink: 0;
|
|
357
|
+
align-items: center;
|
|
358
|
+
gap: 6px;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Le corps seul défile : tête et pied restent, les actions ne sortent
|
|
362
|
+
* jamais de l'écran. */
|
|
363
|
+
.zv-volet__body {
|
|
364
|
+
display: flex;
|
|
365
|
+
min-height: 0;
|
|
366
|
+
flex: 1;
|
|
367
|
+
flex-direction: column;
|
|
368
|
+
overflow: auto;
|
|
369
|
+
padding: 14px 18px;
|
|
370
|
+
font-size: var(--app);
|
|
371
|
+
line-height: 1.6;
|
|
372
|
+
color: var(--muted);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* Un document qui doit aller bord à bord (PDF, image) : pas de marge. */
|
|
376
|
+
.zv-volet__body--nu {
|
|
377
|
+
padding: 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.zv-volet__foot {
|
|
381
|
+
display: flex;
|
|
382
|
+
justify-content: flex-end;
|
|
383
|
+
gap: 10px;
|
|
384
|
+
border-top: 1px solid var(--hairline);
|
|
385
|
+
background: var(--white);
|
|
386
|
+
padding: 12px 18px;
|
|
387
|
+
}
|
|
388
|
+
|
|
258
389
|
/* ─── Notification flottante ────────────────────────────────────────────
|
|
259
390
|
* ⚠️ HORS RELEVÉ, même raison : Mémoire signale tout en place, par une
|
|
260
391
|
* alerte de bloc. Cette notification est l'alerte du module de retours,
|
|
@@ -318,6 +449,13 @@ html:has(.zv-modal--open) {
|
|
|
318
449
|
display: block;
|
|
319
450
|
}
|
|
320
451
|
|
|
452
|
+
/* Sous le seuil, le volet prend toute la largeur : une bande de page de
|
|
453
|
+
* 30 px à gauche ne sert à rien, le voile se ferme par Échap ou la croix. */
|
|
454
|
+
.zv-volet,
|
|
455
|
+
.zv-volet--large {
|
|
456
|
+
--zv-volet-largeur: 100vw;
|
|
457
|
+
}
|
|
458
|
+
|
|
321
459
|
/* La notification prend la largeur : un coin de 380px sur un écran de 390
|
|
322
460
|
* n'est plus un coin. */
|
|
323
461
|
.zv-toasts {
|