@zevra/ui 0.43.0 → 0.44.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/dist/grille.js CHANGED
@@ -22,10 +22,12 @@ import { DataTable, colonnesAffichees, epinglage } from './data-table.js';
22
22
  import { usePersistedState } from './use-persisted-state.js';
23
23
  import { Button } from './actions.js';
24
24
  import { Icon, IconButton } from './icon.js';
25
- import { Checkbox, Field, Input, Search, Select, SelectAnchor } from './forms.js';
25
+ import { Checkbox, Field, Input, Search, SelectAnchor } from './forms.js';
26
+ import { SelectMenu } from './select-menu.js';
26
27
  import { Menu, MenuItem, Pager, PagerArrow, Popover } from './navigation.js';
27
28
  import { Empty } from './feedback.js';
28
29
  import { Meta } from './data.js';
30
+ import { Mono } from './typography.js';
29
31
  import { cx } from './cx.js';
30
32
  export { usePersistedState };
31
33
  /** Identifiant réservé de la colonne de cases. */
@@ -45,6 +47,11 @@ export const TEXTES_GRILLE = {
45
47
  min: 'Min.',
46
48
  max: 'Max.',
47
49
  effacer: 'Effacer le filtre',
50
+ effacerFiltres: 'Effacer les filtres',
51
+ toutAfficher: 'Tout afficher',
52
+ aucun: 'Aucun',
53
+ rechercherValeur: 'Rechercher une valeur',
54
+ aucuneValeur: 'Aucune valeur.',
48
55
  periodes: 'Périodes',
49
56
  aujourdhui: "Aujourd'hui",
50
57
  septJours: '7 derniers jours',
@@ -191,15 +198,22 @@ const nombre = (s) => {
191
198
  const n = Number(s.replace(',', '.'));
192
199
  return Number.isFinite(n) ? n : null;
193
200
  };
201
+ /** Sans casse ni accents : « decision » trouve « Décision ». */
202
+ export function normaliser(v) {
203
+ return String(v ?? '')
204
+ .toLowerCase()
205
+ .normalize('NFD')
206
+ .replace(/[\u0300-\u036f]/g, '');
207
+ }
194
208
  /** Le filtre de toutes les colonnes de la grille : il lit la FORME de la
195
209
  * valeur, pas la colonne. Bornes comprises. */
196
210
  export const filtreGrille = (ligne, colonne, filtre) => {
197
211
  const brut = ligne.getValue(colonne);
198
212
  switch (filtre?.type) {
199
213
  case 'texte':
200
- return String(brut ?? '').toLowerCase().includes(filtre.valeur.trim().toLowerCase());
214
+ return normaliser(brut).includes(normaliser(filtre.valeur.trim()));
201
215
  case 'choix':
202
- return String(brut ?? '') === filtre.valeur;
216
+ return filtre.valeurs.includes(String(brut ?? ''));
203
217
  case 'nombre': {
204
218
  const v = typeof brut === 'number' ? brut : nombre(String(brut ?? ''));
205
219
  const min = nombre(filtre.min);
@@ -220,7 +234,7 @@ export const filtreGrille = (ligne, colonne, filtre) => {
220
234
  };
221
235
  filtreGrille.autoRemove = (f) => !f ||
222
236
  (f.type === 'texte' && f.valeur.trim() === '') ||
223
- (f.type === 'choix' && f.valeur === '') ||
237
+ (f.type === 'choix' && f.valeurs.length === 0) ||
224
238
  (f.type === 'nombre' && f.min === '' && f.max === '') ||
225
239
  (f.type === 'date' && f.du === '' && f.au === '');
226
240
  /** Ne garde d'une saisie que ce qui peut faire un nombre : chiffres, un
@@ -393,21 +407,22 @@ function PoigneeLargeur({ colonne, table, nom, t, }) {
393
407
  // une valeur, et les flèches pour la changer.
394
408
  role: "separator", "aria-orientation": "vertical", "aria-label": t.largeur(nom), "aria-valuenow": Math.round(reglee ?? colonne.getSize()), "aria-valuemin": min, tabIndex: 0, title: t.largeurAide, className: cx('zv-colhead__resize', actif && 'zv-colhead__resize--actif'), onPointerDown: appui, onPointerMove: mouvement, onPointerUp: fin, onPointerCancel: fin, onDoubleClick: rendre, onKeyDown: clavier, onClick: (e) => e.stopPropagation() }));
395
409
  }
396
- /* ─── Cases de sélection ─────────────────────────────────────────────── */
397
- function CaseTout({ table, t }) {
410
+ function CaseTout({ table, t, options }) {
398
411
  // Une enveloppe plutôt qu'une ref sur la case : `Checkbox` ne transmet
399
412
  // pas de ref sous React 18, que le paquet accepte encore.
400
413
  const ref = useRef(null);
401
414
  const partiel = table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected();
402
415
  // `indeterminate` n'a pas d'attribut HTML : seule la propriété DOM existe.
416
+ // Sans elle, « 3 sur 40 cochées » ressemble à « aucune ».
403
417
  useEffect(() => {
404
418
  const caseTout = ref.current?.querySelector('input');
405
419
  if (caseTout)
406
420
  caseTout.indeterminate = partiel;
407
421
  }, [partiel]);
408
- return (_jsx("span", { ref: ref, className: "zv-cell--case", children: _jsx(Checkbox, { checked: table.getIsAllRowsSelected(), onChange: table.getToggleAllRowsSelectedHandler(), "aria-label": t.toutSelectionner }) }));
422
+ const aucune = !table.getPrePaginationRowModel().rows.some((r) => r.getCanSelect());
423
+ return (_jsx("span", { ref: ref, className: "zv-cell--case", children: _jsx(Checkbox, { checked: table.getIsAllRowsSelected(), disabled: aucune || options.current?.desactivee, onChange: table.getToggleAllRowsSelectedHandler(), "aria-label": t.toutSelectionner, title: t.toutSelectionner }) }));
409
424
  }
410
- function colonneSelection(t) {
425
+ function colonneSelection(t, options) {
411
426
  return {
412
427
  id: COLONNE_SELECTION,
413
428
  size: 40,
@@ -418,11 +433,13 @@ function colonneSelection(t) {
418
433
  enableColumnFilter: false,
419
434
  enableGlobalFilter: false,
420
435
  enablePinning: false,
421
- header: ({ table }) => _jsx(CaseTout, { table: table, t: t }),
422
- cell: ({ row }) => (_jsx(Checkbox, { checked: row.getIsSelected(), disabled: !row.getCanSelect(), onChange: row.getToggleSelectedHandler(),
423
- // La case vit dans une ligne cliquable : sans ces deux arrêts, cocher
424
- // ouvrirait aussi la ligne, et Espace l'ouvrirait au clavier.
425
- onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), "aria-label": t.selectionnerLigne })),
436
+ header: ({ table }) => _jsx(CaseTout, { table: table, t: t, options: options }),
437
+ // Une ligne qui ne se sélectionne pas n'a PAS de case : une case grisée
438
+ // promettrait une action qui n'existe pas pour elle.
439
+ cell: ({ row }) => row.getCanSelect() ? (_jsx(Checkbox, { checked: row.getIsSelected(), disabled: options.current?.desactivee, onChange: row.getToggleSelectedHandler(),
440
+ // La case vit dans une ligne cliquable : sans ces deux arrêts,
441
+ // cocher ouvrirait aussi la ligne, et Espace l'ouvrirait au clavier.
442
+ onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), "aria-label": options.current?.label?.(row.original) ?? t.selectionnerLigne })) : null,
426
443
  meta: { classe: 'zv-cell--case' },
427
444
  };
428
445
  }
@@ -454,74 +471,51 @@ function MenuColonnes({ table, t }) {
454
471
  };
455
472
  }, [ouvert]);
456
473
  const colonnes = table.getAllLeafColumns().filter((c) => c.id !== COLONNE_SELECTION);
457
- return (_jsx("div", { ref: ancre, children: _jsxs(SelectAnchor, { children: [_jsx(Button, { variante: "fantome", compact: true, icone: "reglages", "aria-expanded": ouvert, "aria-haspopup": "true", onClick: () => setOuvert((o) => !o), children: t.colonnes }), ouvert && (_jsx(Menu, { role: "group", "aria-label": t.colonnesAffichees, className: "zv-grille__menu", children: colonnes.map((c) => {
458
- const nom = libelleColonne(c);
459
- const cote = c.getIsPinned();
460
- const suivant = cote === false ? 'left' : cote === 'left' ? 'right' : false;
461
- return (_jsxs("div", { className: "zv-grille__menu-ligne", children: [_jsxs("label", { className: "zv-menu__item zv-grille__menu-case", children: [_jsx(Checkbox, { checked: c.getIsVisible(), disabled: !c.getCanHide(), onChange: c.getToggleVisibilityHandler() }), _jsx("span", { children: nom }), cote && (_jsx(Meta, { taille: "sm", children: cote === 'left' ? t.aGauche : t.aDroite }))] }), c.getCanPin() && (_jsx(IconButton, { icone: "epingle", taille: "sm", className: "zv-grille__epingle", "aria-pressed": cote !== false, label: suivant === 'left'
462
- ? t.epinglerGauche(nom)
463
- : suivant === 'right'
464
- ? t.epinglerDroite(nom)
465
- : t.detacher(nom), onClick: () => c.pin(suivant) }))] }, c.id));
466
- }) }))] }) }));
474
+ return (_jsx("div", { ref: ancre, children: _jsxs(SelectAnchor, { children: [_jsx(Button, { variante: "fantome", compact: true, icone: "reglages", "aria-expanded": ouvert, "aria-haspopup": "true", onClick: () => setOuvert((o) => !o), children: t.colonnes }), ouvert && (_jsxs(Menu, { role: "group", "aria-label": t.colonnesAffichees, className: "zv-grille__menu", children: [colonnes.some((c) => !c.getIsVisible()) && (_jsx(Button, { variante: "fantome", compact: true, onClick: () => table.setColumnVisibility((v) => Object.fromEntries(Object.keys(v).map((k) => [k, true]))), children: t.toutAfficher })), colonnes.map((c) => {
475
+ const nom = libelleColonne(c);
476
+ const cote = c.getIsPinned();
477
+ const suivant = cote === false ? 'left' : cote === 'left' ? 'right' : false;
478
+ return (_jsxs("div", { className: "zv-grille__menu-ligne", children: [_jsxs("label", { className: "zv-menu__item zv-grille__menu-case", children: [_jsx(Checkbox, { checked: c.getIsVisible(), disabled: !c.getCanHide(), onChange: c.getToggleVisibilityHandler() }), _jsx("span", { children: nom }), cote && (_jsx(Meta, { taille: "sm", children: cote === 'left' ? t.aGauche : t.aDroite }))] }), c.getCanPin() && (_jsx(IconButton, { icone: "epingle", taille: "sm", className: "zv-grille__epingle", "aria-pressed": cote !== false, label: suivant === 'left'
479
+ ? t.epinglerGauche(nom)
480
+ : suivant === 'right'
481
+ ? t.epinglerDroite(nom)
482
+ : t.detacher(nom), onClick: () => c.pin(suivant) }))] }, c.id));
483
+ })] }))] }) }));
467
484
  }
468
485
  /* ─── Filtres de colonne ────────────────────────────────────────────── */
469
486
  function FiltreColonne({ colonne, type, t, }) {
470
487
  const nom = libelleColonne(colonne);
471
488
  const filtre = colonne.getFilterValue();
472
- if (type === 'choix') {
473
- // Les valeurs du modèle FACETTÉ : celles que les autres filtres laissent
474
- // encore passer. Une option qui ne rendrait aucune ligne n'est pas offerte.
475
- const options = [...colonne.getFacetedUniqueValues().keys()]
476
- .filter((v) => v != null && v !== '')
477
- .map(String)
478
- .sort((a, b) => a.localeCompare(b, 'fr'));
479
- const valeur = filtre?.type === 'choix' ? filtre.valeur : '';
480
- // L'option choisie reste dans la liste même si un autre filtre l'a
481
- // fait disparaître : sinon le <select> afficherait « Tous » en mentant.
482
- if (valeur && !options.includes(valeur))
483
- options.unshift(valeur);
484
- return (_jsxs(Select, { className: "zv-table__filtre", value: valeur, onChange: (e) => colonne.setFilterValue({ type: 'choix', valeur: e.target.value }), "aria-label": t.filtrerColonne(nom), children: [_jsx("option", { value: "", children: t.tous }), options.map((o) => (_jsx("option", { value: o, children: o }, o)))] }));
485
- }
489
+ if (type === 'choix')
490
+ return _jsx(FiltreChoix, { colonne: colonne, nom: nom, t: t });
486
491
  if (type === 'nombre' || type === 'date') {
487
492
  return _jsx(FiltrePlage, { colonne: colonne, type: type, nom: nom, t: t });
488
493
  }
489
494
  return (_jsx(Input, { className: "zv-table__filtre", value: filtre?.type === 'texte' ? filtre.valeur : '', onChange: (e) => colonne.setFilterValue({ type: 'texte', valeur: e.target.value }), placeholder: t.filtrer, "aria-label": t.filtrerColonne(nom) }));
490
495
  }
491
496
  /**
492
- * Une plage (deux bornes) ne tient pas dans une cellule de 90 px : le champ
493
- * de la rangée RÉSUME la plage, et l'ouvre dans un popover.
497
+ * Un filtre qui s'ouvre : le champ de la rangée RÉSUME la valeur, et ouvre
498
+ * un panneau (une liste de choix, une plage) sous lui.
494
499
  *
495
- * ⚠️ LE POPOVER SORT DU CADRE, par un portail. `.zv-table` défile
496
- * horizontalement, et `overflow-x: auto` force `overflow-y` : un popover
500
+ * ⚠️ LE PANNEAU SORT DU CADRE, par un portail. `.zv-table` défile
501
+ * horizontalement, et `overflow-x: auto` force `overflow-y` : un panneau
497
502
  * posé dans la cellule serait rogné par le bas du tableau, invisible sur un
498
- * tableau de trois lignes. Il est donc rendu dans le <body> et placé sous
499
- * son déclencheur, en coordonnées d'écran.
503
+ * tableau de trois lignes. Il est rendu dans le <body> et placé sous son
504
+ * déclencheur, en coordonnées d'écran.
505
+ *
506
+ * ⚠️ AUCUN <select> NATIF ICI : sa liste est peinte par le SYSTÈME, hors
507
+ * charte (menu noir sous macOS sombre), et certaines apps l'interdisent.
500
508
  */
501
- function FiltrePlage({ colonne, type, nom, t, }) {
509
+ function FiltreFlottant({ libelle, resume, vide, icone, role, panneau: contenu, }) {
502
510
  const [ouvert, setOuvert] = useState(false);
503
511
  const [position, setPosition] = useState(null);
504
512
  const declencheur = useRef(null);
505
513
  const panneau = useRef(null);
506
- const idChamp = useId();
507
- const filtre = colonne.getFilterValue();
508
- const [a, b] = filtre?.type === 'nombre' ? [filtre.min, filtre.max] : filtre?.type === 'date' ? [filtre.du, filtre.au] : ['', ''];
509
- const ecrire = (bas, haut) => colonne.setFilterValue(type === 'nombre' ? { type, min: bas, max: haut } : { type, du: bas, au: haut });
510
- // Ce qui est TAPÉ, distinct de ce qui est APPLIQUÉ : une date en cours de
511
- // saisie s'affiche dans le champ sans filtrer le tableau. Resynchronisé
512
- // quand le filtre change d'ailleurs (« Réinitialiser la vue »).
513
- const [brouillon, setBrouillon] = useState([a, b]);
514
- useEffect(() => {
515
- setBrouillon([a, b]);
516
- }, [a, b]);
517
- const saisir = (bas, haut) => {
518
- setBrouillon([bas, haut]);
519
- if (type === 'nombre' || (dateComplete(bas) && dateComplete(haut)))
520
- ecrire(bas, haut);
521
- };
522
- const resume = type === 'nombre' ? t.resumeNombres(a, b) : t.resumeDates(a, b);
523
- const vide = !a && !b;
524
- // Placé AVANT l'affichage (layout effect) : sinon le popover apparaît un
514
+ const fermer = useCallback(() => {
515
+ setOuvert(false);
516
+ declencheur.current?.focus();
517
+ }, []);
518
+ // Placé AVANT l'affichage (layout effect) : sinon le panneau apparaît un
525
519
  // instant en haut à gauche de la page.
526
520
  useLayoutEffect(() => {
527
521
  if (!ouvert) {
@@ -537,8 +531,8 @@ function FiltrePlage({ colonne, type, nom, t, }) {
537
531
  const largeur = panneau.current?.firstElementChild?.offsetWidth ?? 0;
538
532
  const vue = declencheur.current?.ownerDocument.defaultView;
539
533
  const droite = vue ? vue.innerWidth : r.right + largeur;
540
- // Le décalage de 5 px sous le déclencheur est celui de .zv-popover
541
- // (top: calc(100% + 5px)) : l'enveloppe se pose au ras du champ.
534
+ // Le décalage de 5 px sous le déclencheur est celui de .zv-popover et
535
+ // de .zv-menu (top: calc(100% + 5px)) : l'enveloppe se pose au ras.
542
536
  setPosition({ top: r.bottom, left: Math.max(0, Math.min(r.left, droite - largeur - 8)) });
543
537
  };
544
538
  placer();
@@ -561,10 +555,8 @@ function FiltrePlage({ colonne, type, nom, t, }) {
561
555
  setOuvert(false);
562
556
  };
563
557
  const echap = (e) => {
564
- if (e.key === 'Escape') {
565
- setOuvert(false);
566
- declencheur.current?.focus();
567
- }
558
+ if (e.key === 'Escape')
559
+ fermer();
568
560
  };
569
561
  doc.addEventListener('mousedown', dehors);
570
562
  doc.addEventListener('keydown', echap);
@@ -572,15 +564,70 @@ function FiltrePlage({ colonne, type, nom, t, }) {
572
564
  doc.removeEventListener('mousedown', dehors);
573
565
  doc.removeEventListener('keydown', echap);
574
566
  };
575
- }, [ouvert]);
576
- // Le focus entre dans le premier champ une fois le panneau PLACÉ : avant,
577
- // il est en `visibility: hidden`, et le navigateur refuse le focus à un
578
- // élément invisible. La frappe partait alors dans le vide.
567
+ }, [ouvert, fermer]);
568
+ // Le focus entre dans le panneau une fois PLACÉ : avant, il est en
569
+ // `visibility: hidden`, et le navigateur refuse le focus à un élément
570
+ // invisible. La frappe partait alors dans le vide. L'option choisie
571
+ // d'abord, sinon le premier champ ou la première option.
579
572
  const place = position !== null;
580
573
  useEffect(() => {
581
- if (ouvert && place)
582
- panneau.current?.querySelector('input')?.focus();
574
+ if (!ouvert || !place)
575
+ return;
576
+ const p = panneau.current;
577
+ (p?.querySelector('[aria-selected="true"]') ?? p?.querySelector('input, button'))?.focus();
583
578
  }, [ouvert, place]);
579
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: declencheur, type: "button", className: "zv-input zv-input--trigger zv-table__filtre", "data-vide": vide ? 'oui' : undefined, "aria-expanded": ouvert, "aria-haspopup": role, "aria-label": `${libelle} : ${resume}`, onClick: () => setOuvert((o) => !o), children: [_jsx("span", { className: "zv-colhead__label", children: resume }), _jsx(Icon, { nom: icone, taille: "sm", discret: true })] }), ouvert &&
580
+ createPortal(_jsx("div", { ref: panneau, className: "zv-grille__flottant", style: position ? { top: position.top, left: position.left } : { visibility: 'hidden' }, children: contenu(fermer) }), declencheur.current?.ownerDocument.body ?? document.body)] }));
581
+ }
582
+ function FiltreChoix({ colonne, nom, t }) {
583
+ const filtre = colonne.getFilterValue();
584
+ const choisies = filtre?.type === 'choix' ? filtre.valeurs : [];
585
+ // Les valeurs du modèle FACETTÉ, avec leur nombre de lignes : celles que
586
+ // les autres filtres laissent encore passer. Une option qui ne rendrait
587
+ // aucune ligne n'est pas offerte, sauf si elle est déjà cochée : sinon
588
+ // la liste mentirait sur ce qui filtre.
589
+ const comptes = colonne.getFacetedUniqueValues();
590
+ const options = [...comptes.keys()]
591
+ .filter((v) => v != null && v !== '')
592
+ .map(String)
593
+ .sort((a, b) => a.localeCompare(b, 'fr', { numeric: true }));
594
+ for (const c of choisies)
595
+ if (!options.includes(c))
596
+ options.unshift(c);
597
+ const ecrire = (valeurs) => colonne.setFilterValue({ type: 'choix', valeurs });
598
+ const resume = choisies.length === 0 ? t.tous : choisies.length === 1 ? (choisies[0] ?? t.tous) : `${choisies.length} / ${options.length}`;
599
+ return (_jsx(FiltreFlottant, { libelle: t.filtrerColonne(nom), resume: resume, vide: choisies.length === 0, icone: "chevron-bas", role: "dialog", panneau: () => _jsx(ListeChoix, { options: options, comptes: comptes, choisies: choisies, ecrire: ecrire, nom: nom, t: t }) }));
600
+ }
601
+ /** Plusieurs valeurs à la fois (« Acte » OU « Contrat ») : des cases, avec
602
+ * le nombre de lignes de chacune, une recherche au-delà de huit options,
603
+ * et « Tout » / « Aucun » sur ce que la recherche montre. */
604
+ function ListeChoix({ options, comptes, choisies, ecrire, nom, t, }) {
605
+ const [q, setQ] = useState('');
606
+ const vues = q.trim() ? options.filter((o) => normaliser(o).includes(normaliser(q.trim()))) : options;
607
+ return (_jsxs(Popover, { className: "zv-grille__plage", role: "dialog", "aria-label": t.filtrerColonne(nom), children: [options.length > 8 && (_jsx(Search, { value: q, onChange: (e) => setQ(e.target.value), placeholder: t.rechercherValeur, "aria-label": t.rechercherValeur })), _jsxs("div", { className: "zv-grille__menu-ligne", children: [_jsx(Button, { variante: "fantome", compact: true, onClick: () => ecrire([...new Set([...choisies, ...vues])]), children: t.tous }), _jsx(Button, { variante: "fantome", compact: true, onClick: () => ecrire(choisies.filter((c) => !vues.includes(c))), children: t.aucun })] }), _jsxs("div", { className: "zv-grille__liste", role: "group", "aria-label": t.filtrerColonne(nom), children: [vues.map((o) => {
608
+ const pris = choisies.includes(o);
609
+ return (_jsxs("label", { className: "zv-menu__item zv-grille__menu-case", children: [_jsx(Checkbox, { checked: pris, onChange: () => ecrire(pris ? choisies.filter((c) => c !== o) : [...choisies, o]) }), _jsx("span", { className: "zv-colhead__label", children: o }), _jsx(Meta, { taille: "sm", discret: true, className: "zv-grille__compte", children: comptes.get(o) ?? 0 })] }, o));
610
+ }), vues.length === 0 && _jsx(Meta, { taille: "sm", children: t.aucuneValeur })] }), choisies.length > 0 && (_jsx(Button, { variante: "fantome", compact: true, icone: "fermer", onClick: () => ecrire([]), children: t.effacer }))] }));
611
+ }
612
+ function FiltrePlage({ colonne, type, nom, t, }) {
613
+ const idChamp = useId();
614
+ const filtre = colonne.getFilterValue();
615
+ const [a, b] = filtre?.type === 'nombre' ? [filtre.min, filtre.max] : filtre?.type === 'date' ? [filtre.du, filtre.au] : ['', ''];
616
+ const ecrire = (bas, haut) => colonne.setFilterValue(type === 'nombre' ? { type, min: bas, max: haut } : { type, du: bas, au: haut });
617
+ // Ce qui est TAPÉ, distinct de ce qui est APPLIQUÉ : une date en cours de
618
+ // saisie s'affiche dans le champ sans filtrer le tableau. Resynchronisé
619
+ // quand le filtre change d'ailleurs (« Réinitialiser la vue »).
620
+ const [brouillon, setBrouillon] = useState([a, b]);
621
+ useEffect(() => {
622
+ setBrouillon([a, b]);
623
+ }, [a, b]);
624
+ const saisir = (bas, haut) => {
625
+ setBrouillon([bas, haut]);
626
+ if (type === 'nombre' || (dateComplete(bas) && dateComplete(haut)))
627
+ ecrire(bas, haut);
628
+ };
629
+ const resume = type === 'nombre' ? t.resumeNombres(a, b) : t.resumeDates(a, b);
630
+ const vide = !a && !b;
584
631
  const champ = (valeur, changer, suffixe, bornes) => type === 'date' ? (_jsx(Input, { id: idChamp + suffixe, type: "date", value: valeur,
585
632
  // Le calendrier natif grise ce qui rendrait la plage incohérente :
586
633
  // « Au » ne peut pas précéder « Du ».
@@ -589,15 +636,13 @@ function FiltrePlage({ colonne, type, nom, t, }) {
589
636
  // française selon la langue du système. Le clavier numérique vient
590
637
  // d'`inputMode`, le refus des lettres de `saisieNombre`.
591
638
  inputMode: "decimal", autoComplete: "off", value: valeur, onChange: (e) => changer(saisieNombre(e.target.value)) }));
592
- return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: declencheur, type: "button", className: "zv-input zv-input--trigger zv-table__filtre", "data-vide": vide ? 'oui' : undefined, "aria-expanded": ouvert, "aria-haspopup": "dialog", "aria-label": `${t.filtrerColonne(nom)} : ${resume}`, onClick: () => setOuvert((o) => !o), children: [_jsx("span", { className: "zv-colhead__label", children: resume }), _jsx(Icon, { nom: type === 'date' ? 'calendrier' : 'filtre', taille: "sm", discret: true })] }), ouvert &&
593
- createPortal(_jsx("div", { ref: panneau, role: "dialog", "aria-label": t.filtrerColonne(nom), className: "zv-grille__flottant", style: position ? { top: position.top, left: position.left } : { visibility: 'hidden' }, children: _jsxs(Popover, { className: "zv-grille__plage", children: [type === 'date' && (_jsx("div", { className: "zv-grille__periodes", role: "group", "aria-label": t.periodes, children: ['aujourdhui', 'septJours', 'trenteJours', 'ceMois', 'cetteAnnee'].map((cle) => {
594
- const [du, au] = periodes(new Date())[cle];
595
- const actif = a === du && b === au;
596
- return (_jsx(MenuItem, { actif: actif, onClick: () => saisir(du, au), children: t[cle] }, cle));
597
- }) })), _jsxs("div", { className: "zv-grille__bornes", children: [_jsx(Field, { label: type === 'date' ? t.du : t.min, htmlFor: idChamp + 'a', children: champ(brouillon[0], (v) => saisir(v, brouillon[1]), 'a', { max: brouillon[1] }) }), _jsx(Field, { label: type === 'date' ? t.au : t.max, htmlFor: idChamp + 'b', children: champ(brouillon[1], (v) => saisir(brouillon[0], v), 'b', { min: brouillon[0] }) })] }), !vide && (_jsx(Button, { variante: "fantome", compact: true, icone: "fermer", onClick: () => saisir('', ''), children: t.effacer }))] }) }), declencheur.current?.ownerDocument.body ?? document.body)] }));
639
+ return (_jsx(FiltreFlottant, { libelle: t.filtrerColonne(nom), resume: resume, vide: vide, icone: type === 'date' ? 'calendrier' : 'filtre', role: "dialog", panneau: () => (_jsxs(Popover, { className: "zv-grille__plage", role: "dialog", "aria-label": t.filtrerColonne(nom), children: [type === 'date' && (_jsx("div", { className: "zv-grille__periodes", role: "group", "aria-label": t.periodes, children: ['aujourdhui', 'septJours', 'trenteJours', 'ceMois', 'cetteAnnee'].map((cle) => {
640
+ const [du, au] = periodes(new Date())[cle];
641
+ return (_jsx(MenuItem, { role: "button", actif: a === du && b === au, "aria-selected": undefined, "aria-pressed": a === du && b === au, onClick: () => saisir(du, au), children: t[cle] }, cle));
642
+ }) })), _jsxs("div", { className: "zv-grille__bornes", children: [_jsx(Field, { label: type === 'date' ? t.du : t.min, htmlFor: idChamp + 'a', children: champ(brouillon[0], (v) => saisir(v, brouillon[1]), 'a', { max: brouillon[1] }) }), _jsx(Field, { label: type === 'date' ? t.au : t.max, htmlFor: idChamp + 'b', children: champ(brouillon[1], (v) => saisir(brouillon[0], v), 'b', { min: brouillon[0] }) })] }), !vide && (_jsx(Button, { variante: "fantome", compact: true, icone: "fermer", onClick: () => saisir('', ''), children: t.effacer }))] })) }));
598
643
  }
599
644
  /* ─── La grille ──────────────────────────────────────────────────────── */
600
- export function DataGrid({ tableId, data, columns, onRowClick, getRowId, selection = false, onSelectionChange, parPage = 25, recherche = true, filtres = true, exportCsv = true, vueInitiale, vide, outils, textes, className, }) {
645
+ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getRowId, selection = false, onSelectionChange, triServeur, onRowContextMenu, onRowDoubleClick, onRowDragStart, ligneSaisie, groupe, rang, pied, dense, remplir, apresLignes, envelopperTableau, barre = true, champRecherche, visibilite, onVisibilite, onLignesAffichees, parPage = 25, recherche = true, filtres = true, exportCsv = true, vueInitiale, vide, outils, textes, className, }) {
601
646
  const t = { ...TEXTES_GRILLE, ...textes };
602
647
  const ids = useMemo(() => columns.map((c) => idDeColonne(c)).filter((id) => id != null), [columns]);
603
648
  const cleIds = ids.join('\u0000');
@@ -622,34 +667,73 @@ export function DataGrid({ tableId, data, columns, onRowClick, getRowId, selecti
622
667
  const [vue, setVue, reinitialiserVue] = usePersistedState(`table:${tableId}:v1`, defaut, reconcilier);
623
668
  const [columnFilters, setColumnFilters] = useState([]);
624
669
  const [globalFilter, setGlobalFilter] = useState('');
625
- const [rowSelection, setRowSelection] = useState({});
670
+ const [selectionInterne, setSelectionInterne] = useState({});
626
671
  const [pageIndex, setPageIndex] = useState(0);
627
672
  const champ = (k) => (u) => setVue((v) => ({ ...v, [k]: appliquer(u, v[k]) }));
673
+ /* Sélection. Tenue par la grille (`true`) ou par l'app (un objet) : dans le
674
+ * second cas, l'état de TanStack se DÉDUIT des clés de l'app à chaque rendu,
675
+ * et chaque changement lui est rendu en clés. */
676
+ const avecCases = selection !== false;
677
+ const externe = typeof selection === 'object' ? selection : null;
678
+ const idLigne = (l, i) => getRowId?.(l, i) ?? String(i);
679
+ const cleDe = (l, i) => externe?.cle?.(l) ?? idLigne(l, i);
680
+ const rowSelection = externe
681
+ ? Object.fromEntries(
682
+ // Une ligne sans case ne se peint pas cochée, même si sa clé est
683
+ // dans l'ensemble de l'app : on la verrait choisie sans pouvoir
684
+ // la décocher.
685
+ data.flatMap((l, i) => externe.choisies.has(cleDe(l, i)) && externe.selectionnable?.(l) !== false
686
+ ? [[idLigne(l, i), true]]
687
+ : []))
688
+ : selectionInterne;
689
+ const surSelectionLignes = (u) => {
690
+ if (!externe)
691
+ return setSelectionInterne(u);
692
+ const n = appliquer(u, rowSelection);
693
+ const suite = new Set(externe.choisies);
694
+ // Seules les clés des lignes PRÉSENTES changent : une ligne cochée puis
695
+ // filtrée hors de l'écran, ou sur une autre page, reste cochée.
696
+ data.forEach((l, i) => {
697
+ if (externe.selectionnable?.(l) === false)
698
+ return;
699
+ const cle = cleDe(l, i);
700
+ if (n[idLigne(l, i)])
701
+ suite.add(cle);
702
+ else
703
+ suite.delete(cle);
704
+ });
705
+ externe.onChoisir(suite);
706
+ };
628
707
  // La colonne de cases est épinglée à gauche, toujours, et n'entre pas dans
629
708
  // la vue enregistrée : elle dépend de la prop, pas de l'utilisateur.
630
709
  const pinning = {
631
- left: selection ? [COLONNE_SELECTION, ...(vue.columnPinning.left ?? [])] : vue.columnPinning.left,
710
+ left: avecCases ? [COLONNE_SELECTION, ...(vue.columnPinning.left ?? [])] : vue.columnPinning.left,
632
711
  right: vue.columnPinning.right,
633
712
  };
634
713
  const pagination = {
635
714
  pageIndex,
636
715
  pageSize: parPage === false ? Math.max(data.length, 1) : vue.pageSize,
637
716
  };
717
+ // L'option de sélection est lue AU RENDU par les cases : les colonnes n'ont
718
+ // pas à se recalculer à chaque case cochée.
719
+ const optionsCases = useRef(externe);
720
+ optionsCases.current = externe;
638
721
  const colonnes = useMemo(() => {
639
722
  const avecFiltre = columns.map((c) => (c.filterFn ? c : { ...c, filterFn: filtreGrille }));
640
- return selection ? [colonneSelection(t), ...avecFiltre] : avecFiltre;
723
+ return avecCases ? [colonneSelection(t, optionsCases), ...avecFiltre] : avecFiltre;
641
724
  // eslint-disable-next-line react-hooks/exhaustive-deps
642
- }, [columns, selection, t.toutSelectionner, t.selectionnerLigne]);
725
+ }, [columns, avecCases, t.toutSelectionner, t.selectionnerLigne]);
726
+ const visibiliteCourante = visibilite ?? vue.columnVisibility;
643
727
  const table = useReactTable({
644
728
  data,
645
729
  columns: colonnes,
646
730
  getRowId,
647
731
  state: {
648
732
  columnOrder: vue.columnOrder,
649
- columnVisibility: vue.columnVisibility,
733
+ columnVisibility: visibiliteCourante,
650
734
  columnSizing: vue.columnSizing,
651
735
  columnPinning: pinning,
652
- sorting: vue.sorting,
736
+ sorting: triServeur ? triServeur.tri : vue.sorting,
653
737
  columnFilters,
654
738
  globalFilter,
655
739
  rowSelection,
@@ -657,12 +741,17 @@ export function DataGrid({ tableId, data, columns, onRowClick, getRowId, selecti
657
741
  },
658
742
  defaultColumn: { size: 150, minSize: 60 },
659
743
  onColumnOrderChange: champ('columnOrder'),
660
- onColumnVisibilityChange: champ('columnVisibility'),
744
+ onColumnVisibilityChange: onVisibilite
745
+ ? (u) => onVisibilite((v) => appliquer(u, v))
746
+ : champ('columnVisibility'),
661
747
  onColumnSizingChange: champ('columnSizing'),
662
- onSortingChange: champ('sorting'),
748
+ onSortingChange: triServeur
749
+ ? (u) => triServeur.onTri(appliquer(u, triServeur.tri))
750
+ : champ('sorting'),
751
+ manualSorting: Boolean(triServeur),
663
752
  onColumnPinningChange: (u) => setVue((v) => {
664
753
  const actuel = {
665
- left: selection ? [COLONNE_SELECTION, ...(v.columnPinning.left ?? [])] : v.columnPinning.left,
754
+ left: avecCases ? [COLONNE_SELECTION, ...(v.columnPinning.left ?? [])] : v.columnPinning.left,
666
755
  right: v.columnPinning.right,
667
756
  };
668
757
  const n = appliquer(u, actuel);
@@ -676,7 +765,7 @@ export function DataGrid({ tableId, data, columns, onRowClick, getRowId, selecti
676
765
  }),
677
766
  onColumnFiltersChange: setColumnFilters,
678
767
  onGlobalFilterChange: setGlobalFilter,
679
- onRowSelectionChange: setRowSelection,
768
+ onRowSelectionChange: surSelectionLignes,
680
769
  onPaginationChange: (u) => {
681
770
  const n = appliquer(u, pagination);
682
771
  setPageIndex(n.pageIndex);
@@ -684,14 +773,17 @@ export function DataGrid({ tableId, data, columns, onRowClick, getRowId, selecti
684
773
  setVue((v) => ({ ...v, pageSize: n.pageSize }));
685
774
  }
686
775
  },
687
- enableRowSelection: selection,
776
+ enableRowSelection: externe?.selectionnable
777
+ ? (r) => externe.selectionnable(r.original)
778
+ : avecCases,
688
779
  enableMultiSort: true,
689
780
  enableColumnResizing: true,
690
781
  columnResizeMode: 'onChange',
691
782
  enableColumnPinning: true,
692
- globalFilterFn: 'includesString',
783
+ // Sans casse ni accents, comme les filtres de colonne.
784
+ globalFilterFn: (ligne, colonne, q) => normaliser(ligne.getValue(colonne)).includes(normaliser(String(q).trim())),
693
785
  getCoreRowModel: getCoreRowModel(),
694
- getSortedRowModel: getSortedRowModel(),
786
+ getSortedRowModel: triServeur ? undefined : getSortedRowModel(),
695
787
  getFilteredRowModel: getFilteredRowModel(),
696
788
  getPaginationRowModel: getPaginationRowModel(),
697
789
  getFacetedRowModel: getFacetedRowModel(),
@@ -702,9 +794,11 @@ export function DataGrid({ tableId, data, columns, onRowClick, getRowId, selecti
702
794
  const surSelection = useRef(onSelectionChange);
703
795
  surSelection.current = onSelectionChange;
704
796
  useEffect(() => {
797
+ if (externe)
798
+ return;
705
799
  surSelection.current?.(table.getSelectedRowModel().rows.map((r) => r.original));
706
800
  // eslint-disable-next-line react-hooks/exhaustive-deps
707
- }, [rowSelection]);
801
+ }, [selectionInterne]);
708
802
  // Une page au-delà de la dernière (filtre resserré, données rechargées)
709
803
  // afficherait la ligne d'absence alors que des lignes existent.
710
804
  const nbPages = table.getPageCount();
@@ -712,17 +806,22 @@ export function DataGrid({ tableId, data, columns, onRowClick, getRowId, selecti
712
806
  if (pageIndex > 0 && pageIndex >= nbPages)
713
807
  setPageIndex(Math.max(0, nbPages - 1));
714
808
  }, [pageIndex, nbPages]);
715
- const reinitialiser = () => {
716
- reinitialiserVue();
809
+ const effacerFiltres = () => {
717
810
  setColumnFilters([]);
718
811
  setGlobalFilter('');
812
+ };
813
+ const reinitialiser = () => {
814
+ reinitialiserVue();
815
+ if (onVisibilite)
816
+ onVisibilite(() => defaut.columnVisibility);
817
+ effacerFiltres();
719
818
  setPageIndex(0);
720
819
  };
721
820
  /* Glisser-déposer des en-têtes. */
722
821
  const idDnd = useId();
723
822
  const capteurs = useSensors(
724
- // 4 px avant de saisir : sans ce seuil, un simple clic sur la poignée
725
- // démarre un déplacement.
823
+ // 4 px avant de saisir : sans ce seuil, un simple clic sur l'en-tête
824
+ // démarre un déplacement au lieu de trier.
726
825
  useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
727
826
  const deplacables = table.getCenterVisibleLeafColumns().map((c) => c.id);
728
827
  const nomDe = (id) => {
@@ -759,22 +858,62 @@ export function DataGrid({ tableId, data, columns, onRowClick, getRowId, selecti
759
858
  // eslint-disable-next-line react-hooks/exhaustive-deps
760
859
  }, [lignesBase, colonnes]);
761
860
  const filtrables = table.getVisibleLeafColumns().some((c) => c.getCanFilter() && types[c.id]);
762
- const rangeeFiltres = filtres && filtrables ? (_jsx("div", { className: "zv-table__head zv-table__head--filtres", children: colonnesAffichees(table).map((c) => {
861
+ /** Une rangée qui suit les colonnes affichées (ordre, épinglage) : les
862
+ * filtres, la ligne de saisie, le pied. */
863
+ const rangee = (classe, contenu) => (_jsx("div", { className: classe, children: colonnesAffichees(table).map((c) => {
763
864
  const pin = epinglage(c, table);
865
+ const registre = c.columnDef.meta?.registre;
866
+ return (_jsx("div", { className: cx('zv-cell', registre === 'num' && 'zv-cell--num', registre === 'nom' && 'zv-cell--name', pin.classes), style: pin.style, children: contenu(c) }, c.id));
867
+ }) }));
868
+ const rangeeFiltres = filtres && filtrables
869
+ ? rangee('zv-table__head zv-table__head--filtres', (c) => {
764
870
  const type = c.getCanFilter() ? types[c.id] : false;
765
- return (_jsx("div", { className: cx('zv-cell', pin.classes), style: pin.style, children: type && _jsx(FiltreColonne, { colonne: c, type: type, t: t }) }, c.id));
766
- }) })) : null;
767
- const total = table.getPrePaginationRowModel().rows.length;
871
+ return type ? _jsx(FiltreColonne, { colonne: c, type: type, t: t }) : null;
872
+ })
873
+ : null;
874
+ /* Lignes : le rang (des lignes gardées en tête), puis les intitulés de
875
+ * groupe et le pied, calculés sur ce qui est AFFICHÉ. */
876
+ const lignesTriees = table.getRowModel().rows;
877
+ const lignesRendues = useMemo(() => {
878
+ if (!rang)
879
+ return lignesTriees;
880
+ return lignesTriees
881
+ .map((r, i) => ({ r, i, k: rang(r.original) }))
882
+ .sort((a, b) => a.k - b.k || a.i - b.i)
883
+ .map((x) => x.r);
884
+ // eslint-disable-next-line react-hooks/exhaustive-deps
885
+ }, [lignesTriees, rang]);
886
+ const affichees = table.getPrePaginationRowModel().rows.map((r) => r.original);
887
+ const filtre = columnFilters.length > 0 || globalFilter.trim() !== '';
888
+ const surAffichees = useRef(onLignesAffichees);
889
+ surAffichees.current = onLignesAffichees;
890
+ const signatureAffichees = `${filtre}|${lignesRendues.map((r) => r.id).join('\u0001')}`;
891
+ useEffect(() => {
892
+ surAffichees.current?.(lignesRendues.map((r) => r.original), filtre);
893
+ // eslint-disable-next-line react-hooks/exhaustive-deps
894
+ }, [signatureAffichees]);
895
+ const intercalaire = groupe
896
+ ? (ligne, precedente) => {
897
+ const cle = groupe.de(ligne.original);
898
+ if (precedente && groupe.de(precedente.original) === cle)
899
+ return null;
900
+ const membres = lignesRendues.filter((r) => groupe.de(r.original) === cle).map((r) => r.original);
901
+ return (_jsx("div", { className: "zv-row zv-row--groupe", children: groupe.rendu ? groupe.rendu(cle, membres) : _jsx(Mono, { ton: "label", children: cle }) }));
902
+ }
903
+ : undefined;
904
+ const casesPied = pied && lignesRendues.length > 0 ? pied(affichees) : null;
905
+ const total = affichees.length;
768
906
  const { pageIndex: page, pageSize } = table.getState().pagination;
769
907
  const debut = total === 0 ? 0 : page * pageSize + 1;
770
908
  const fin = Math.min(total, (page + 1) * pageSize);
771
- const nbChoisies = Object.keys(rowSelection).length;
772
- const filtre = columnFilters.length > 0 || globalFilter !== '';
773
- return (_jsxs("div", { className: cx('zv-grille', className), children: [_jsxs("div", { className: "zv-grille__outils", children: [recherche ? (_jsx(Search, { value: globalFilter, onChange: (e) => setGlobalFilter(e.target.value), placeholder: t.rechercher, "aria-label": t.rechercher })) : (_jsx("span", {})), _jsxs("div", { className: "zv-grille__actions", children: [selection && nbChoisies > 0 && (_jsx(Meta, { children: t.choisies(nbChoisies) })), outils, _jsx(MenuColonnes, { table: table, t: t }), exportCsv !== false && (_jsx(Button, { variante: "fantome", compact: true, icone: "telecharger", onClick: () => telecharger(versCsv(table), typeof exportCsv === 'string' ? exportCsv : tableId), children: t.exporter })), _jsx(Button, { variante: "fantome", compact: true, icone: "rembobiner", onClick: reinitialiser, children: t.reinitialiser })] })] }), _jsx(DndContext, { id: idDnd, sensors: capteurs, collisionDetection: closestCenter, modifiers: [axeHorizontal], onDragEnd: finDeplacement, accessibility: {
774
- announcements: annonces,
775
- screenReaderInstructions: {
776
- draggable: t.dndInstructions,
777
- },
778
- }, children: _jsx(SortableContext, { items: deplacables, strategy: horizontalListSortingStrategy, children: _jsx(DataTable, { table: table, className: "zv-table--grille", celluleEntete: (p) => _jsx(EnteteColonne, { ...p, t: t }), sousEntete: rangeeFiltres, onRowClick: onRowClick, vide: vide ?? (_jsx(Empty, { children: data.length === 0 ? t.aucuneDonnee : filtre ? t.aucunResultat : t.aucuneLigne })) }) }) }), parPage !== false && (_jsxs(Pager, { pied: true, "aria-label": t.pagination, children: [_jsx(Meta, { children: t.compte(debut, fin, total) }), _jsxs("div", { className: "zv-grille__pied", children: [_jsx(Select, { className: "zv-grille__taille", "aria-label": t.lignesParPage, value: pageSize, onChange: (e) => table.setPageSize(Number(e.target.value)), children: TAILLES_PAGE.map((taille) => (_jsx("option", { value: taille, children: t.parPage(taille) }, taille))) }), _jsx(PagerArrow, { "aria-label": t.pagePrecedente, desactive: !table.getCanPreviousPage(), onClick: () => table.getCanPreviousPage() && table.previousPage(), children: "\u2039" }), _jsx(Meta, { children: t.page(nbPages === 0 ? 0 : page + 1, nbPages) }), _jsx(PagerArrow, { "aria-label": t.pageSuivante, desactive: !table.getCanNextPage(), onClick: () => table.getCanNextPage() && table.nextPage(), children: "\u203A" })] })] }))] }));
909
+ const nbChoisies = externe ? externe.choisies.size : Object.keys(selectionInterne).length;
910
+ const tableau = (_jsx(DndContext, { id: idDnd, sensors: capteurs, collisionDetection: closestCenter, modifiers: [axeHorizontal], onDragEnd: finDeplacement, accessibility: {
911
+ announcements: annonces,
912
+ screenReaderInstructions: {
913
+ draggable: t.dndInstructions,
914
+ },
915
+ }, children: _jsx(SortableContext, { items: deplacables, strategy: horizontalListSortingStrategy, children: _jsx(DataTable, { table: table, className: "zv-table--grille", celluleEntete: (p) => _jsx(EnteteColonne, { ...p, t: t }), sousEntete: rangeeFiltres, onRowClick: onRowClick, ligneActive: ligneActive, onRowContextMenu: onRowContextMenu, onRowDoubleClick: onRowDoubleClick, onRowDragStart: onRowDragStart, lignes: rang ? lignesRendues : undefined, intercalaire: intercalaire, avantLignes: ligneSaisie ? rangee('zv-row zv-row--saisie', (c) => ligneSaisie[c.id] ?? null) : undefined, apresLignes: _jsxs(_Fragment, { children: [casesPied && rangee('zv-row zv-row--pied', (c) => casesPied[c.id] ?? null), apresLignes] }), dense: dense, remplir: remplir, vide: vide && !filtre ? (vide) : (_jsxs(Empty, { children: [data.length === 0 ? t.aucuneDonnee : filtre ? t.aucunResultat : t.aucuneLigne, ' ', filtre && (_jsx(Button, { variante: "fantome", compact: true, onClick: effacerFiltres, children: t.effacerFiltres }))] })) }) }) }));
916
+ return (_jsxs("div", { className: cx('zv-grille', remplir && 'zv-grille--remplir', className), children: [barre && (_jsxs("div", { className: "zv-grille__outils", children: [champRecherche ??
917
+ (recherche !== false ? (_jsx(Search, { value: globalFilter, onChange: (e) => setGlobalFilter(e.target.value), placeholder: typeof recherche === 'string' ? recherche : t.rechercher, "aria-label": t.rechercher })) : (_jsx("span", {}))), _jsxs("div", { className: "zv-grille__actions", children: [avecCases && nbChoisies > 0 && _jsx(Meta, { children: t.choisies(nbChoisies) }), outils, !onVisibilite && _jsx(MenuColonnes, { table: table, t: t }), exportCsv !== false && (_jsx(Button, { variante: "fantome", compact: true, icone: "telecharger", onClick: () => telecharger(versCsv(table), typeof exportCsv === 'string' ? exportCsv : tableId), children: t.exporter })), _jsx(Button, { variante: "fantome", compact: true, icone: "rembobiner", onClick: reinitialiser, children: t.reinitialiser })] })] })), envelopperTableau ? envelopperTableau(tableau) : tableau, parPage !== false && (_jsxs(Pager, { pied: true, "aria-label": t.pagination, children: [_jsx(Meta, { children: t.compte(debut, fin, total) }), _jsxs("div", { className: "zv-grille__pied", children: [_jsx(SelectMenu, { className: "zv-grille__taille", label: t.lignesParPage, valeur: String(pageSize), options: TAILLES_PAGE.map((taille) => ({ valeur: String(taille), label: t.parPage(taille) })), onChange: (v) => table.setPageSize(Number(v)) }), _jsx(PagerArrow, { "aria-label": t.pagePrecedente, desactive: !table.getCanPreviousPage(), onClick: () => table.getCanPreviousPage() && table.previousPage(), children: "\u2039" }), _jsx(Meta, { children: t.page(nbPages === 0 ? 0 : page + 1, nbPages) }), _jsx(PagerArrow, { "aria-label": t.pageSuivante, desactive: !table.getCanNextPage(), onClick: () => table.getCanNextPage() && table.nextPage(), children: "\u203A" })] })] }))] }));
779
918
  }
780
919
  //# sourceMappingURL=grille.js.map