@zevra/ui 0.42.0 → 0.43.1
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 +131 -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/dist/data-table.d.ts +45 -3
- package/dist/data-table.d.ts.map +1 -1
- package/dist/data-table.js +89 -8
- package/dist/data-table.js.map +1 -1
- package/dist/grille.d.ts +203 -0
- package/dist/grille.d.ts.map +1 -0
- package/dist/grille.js +809 -0
- package/dist/grille.js.map +1 -0
- package/dist/use-persisted-state.d.ts +14 -0
- package/dist/use-persisted-state.d.ts.map +1 -0
- package/dist/use-persisted-state.js +97 -0
- package/dist/use-persisted-state.js.map +1 -0
- package/package.json +16 -2
- package/react/css-inline.ts +1 -1
- package/react/data-table.tsx +148 -26
- package/react/grille.tsx +1480 -0
- package/react/use-persisted-state.ts +105 -0
- package/src/data.css +407 -3
- package/src/forms.css +14 -0
package/react/data-table.tsx
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
// variables. Aucune classe nouvelle n'entre par cette porte : ce sont les
|
|
16
16
|
// mêmes .zv-table / .zv-row / .zv-cell que le tableau écrit à la main.
|
|
17
17
|
|
|
18
|
-
import type
|
|
18
|
+
import { Fragment, type CSSProperties, type ReactNode } from 'react'
|
|
19
19
|
import {
|
|
20
20
|
flexRender,
|
|
21
|
+
type Column,
|
|
22
|
+
type Header,
|
|
21
23
|
type RowData,
|
|
22
24
|
type Table as TableTanstack,
|
|
23
25
|
} from '@tanstack/react-table'
|
|
@@ -40,11 +42,38 @@ declare module '@tanstack/react-table' {
|
|
|
40
42
|
/** Largeur de la colonne dans la grille (syntaxe grid-template-columns).
|
|
41
43
|
* Par défaut : `minmax(0, 1fr)`. */
|
|
42
44
|
largeur?: string
|
|
45
|
+
/** Famille du filtre de colonne dans la grille (`DataGrid`). Déduite
|
|
46
|
+
* des valeurs si absente : nombre, date (Date ou ISO), choix (peu de
|
|
47
|
+
* valeurs qui se répètent), texte. `false` retire le filtre. */
|
|
48
|
+
filtre?: 'texte' | 'nombre' | 'date' | 'choix' | false
|
|
49
|
+
/** Classe de cellule supplémentaire, réservée au paquet (la case de
|
|
50
|
+
* sélection de la grille). Une app n'y écrit rien : le CSS du paquet
|
|
51
|
+
* est hors couche, une classe d'app y serait inerte. */
|
|
52
|
+
classe?: string
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
/** Ce que reçoit un rendu d'en-tête substitué (`celluleEntete`) : la cellule
|
|
57
|
+
* telle que le tableau l'aurait rendue, classes, style et contenu compris.
|
|
58
|
+
* Le substitut l'ENVELOPPE (une poignée, un comportement), il ne la refait
|
|
59
|
+
* pas : c'est ce qui garde l'en-tête identique d'un tableau à l'autre. */
|
|
60
|
+
export interface CelluleEnteteProps<T> {
|
|
61
|
+
entete: Header<T, unknown>
|
|
62
|
+
className: string
|
|
63
|
+
style?: CSSProperties
|
|
64
|
+
'aria-sort'?: 'ascending' | 'descending'
|
|
65
|
+
children: ReactNode
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
export interface DataTableProps<T> {
|
|
47
69
|
table: TableTanstack<T>
|
|
70
|
+
/** Remplace le rendu de chaque cellule d'en-tête. Sert à la grille
|
|
71
|
+
* (poignées de déplacement et de largeur) ; un tableau simple n'en a
|
|
72
|
+
* pas besoin. */
|
|
73
|
+
celluleEntete?: (props: CelluleEnteteProps<T>) => ReactNode
|
|
74
|
+
/** Rangée posée SOUS les intitulés, avant les lignes : les filtres de
|
|
75
|
+
* colonne de la grille. */
|
|
76
|
+
sousEntete?: ReactNode
|
|
48
77
|
/** Rendu à la place des lignes quand le modèle est vide. ⚠️ C'est la LIGNE
|
|
49
78
|
* d'absence qu'on attend ici (filtre trop étroit), pas l'écran d'absence :
|
|
50
79
|
* l'en-tête doit rester visible pour qu'on puisse élargir le filtre. */
|
|
@@ -52,20 +81,90 @@ export interface DataTableProps<T> {
|
|
|
52
81
|
/** Appelé au clic sur une ligne. Sans lui, la ligne n'est pas cliquable :
|
|
53
82
|
* et elle ne doit pas en avoir l'air. */
|
|
54
83
|
onRowClick?: (ligne: T) => void
|
|
84
|
+
/** La ligne dont on regarde le détail (l'aperçu ouvert à côté de la
|
|
85
|
+
* liste). Elle porte un filet d'accent à gauche, distinct de la ligne
|
|
86
|
+
* COCHÉE, qui est une sélection pour agir. */
|
|
87
|
+
ligneActive?: (ligne: T) => boolean
|
|
55
88
|
className?: string
|
|
56
89
|
}
|
|
57
90
|
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
/** MODE GRILLE : activé par `enableColumnResizing`. Les largeurs viennent
|
|
92
|
+
* alors de TanStack en pixels (`size` = plancher, `columnSizing` = réglage
|
|
93
|
+
* de l'utilisateur), et les colonnes épinglées collent au bord du cadre.
|
|
94
|
+
* Sans ce réglage, le tableau rend exactement ce qu'il rendait avant. */
|
|
95
|
+
function piste<T>(c: Column<T, unknown>, regle: boolean): string {
|
|
96
|
+
const taille = c.getSize()
|
|
97
|
+
// Réglée ou épinglée : fixe. Sinon : un plancher, et une part de l'espace
|
|
98
|
+
// libre PROPORTIONNELLE au plancher, pour que les colonnes larges restent
|
|
99
|
+
// larges en s'étirant.
|
|
100
|
+
return regle || c.getIsPinned() ? `${taille}px` : `minmax(${taille}px, ${taille}fr)`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Classes et décalage d'une cellule épinglée (en-tête, filtres ou corps).
|
|
104
|
+
* Exporté pour la rangée de filtres de la grille, qui doit coller avec ses
|
|
105
|
+
* colonnes. */
|
|
106
|
+
export function epinglage<T>(
|
|
107
|
+
c: Column<T, unknown>,
|
|
108
|
+
table: TableTanstack<T>,
|
|
109
|
+
): { classes: string | false; style?: CSSProperties } {
|
|
110
|
+
const cote = c.getIsPinned()
|
|
111
|
+
if (!cote) return { classes: false }
|
|
112
|
+
const voisines = cote === 'left' ? table.getLeftVisibleLeafColumns() : table.getRightVisibleLeafColumns()
|
|
113
|
+
const rang = voisines.findIndex((v) => v.id === c.id)
|
|
114
|
+
// Le bord : la dernière à gauche, la première à droite, celle qui touche
|
|
115
|
+
// ce qui défile.
|
|
116
|
+
const bord = cote === 'left' ? rang === voisines.length - 1 : rang === 0
|
|
117
|
+
const decalage =
|
|
118
|
+
cote === 'left'
|
|
119
|
+
? { px: c.getStart('left'), gouttieres: rang }
|
|
120
|
+
: { px: c.getAfter('right'), gouttieres: voisines.length - 1 - rang }
|
|
121
|
+
const valeur = `calc(${decalage.px}px + ${decalage.gouttieres} * var(--zv-grille-gap))`
|
|
122
|
+
return {
|
|
123
|
+
classes: cx('zv-cell--pin', cote === 'right' && 'zv-cell--pin-droite', bord && 'zv-cell--pin-bord'),
|
|
124
|
+
style: cote === 'left' ? { left: valeur } : { right: valeur },
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Les colonnes visibles DANS L'ORDRE DU RENDU : épinglées à gauche,
|
|
129
|
+
* centrales, épinglées à droite. C'est l'ordre des en-têtes et des cellules ;
|
|
130
|
+
* `getVisibleLeafColumns()`, lui, ignore l'épinglage, et un gabarit bâti
|
|
131
|
+
* dessus donne à la première colonne la largeur d'une autre. */
|
|
132
|
+
export function colonnesAffichees<T>(table: TableTanstack<T>): Column<T, unknown>[] {
|
|
133
|
+
return [
|
|
134
|
+
...table.getLeftVisibleLeafColumns(),
|
|
135
|
+
...table.getCenterVisibleLeafColumns(),
|
|
136
|
+
...table.getRightVisibleLeafColumns(),
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function DataTable<T>({
|
|
141
|
+
table,
|
|
142
|
+
vide,
|
|
143
|
+
onRowClick,
|
|
144
|
+
celluleEntete,
|
|
145
|
+
sousEntete,
|
|
146
|
+
ligneActive,
|
|
147
|
+
className,
|
|
148
|
+
}: DataTableProps<T>) {
|
|
149
|
+
const grille = table.options.enableColumnResizing === true
|
|
150
|
+
const reglages = table.getState().columnSizing
|
|
151
|
+
const colonnes = colonnesAffichees(table)
|
|
60
152
|
const gabarit = colonnes
|
|
61
|
-
.map((c) =>
|
|
153
|
+
.map((c) =>
|
|
154
|
+
grille ? piste(c, reglages[c.id] != null) : (c.columnDef.meta?.largeur ?? 'minmax(0, 1fr)'),
|
|
155
|
+
)
|
|
62
156
|
.join(' ')
|
|
63
157
|
const lignes = table.getRowModel().rows
|
|
158
|
+
const style: Record<string, string> = { '--zv-cols': gabarit }
|
|
159
|
+
if (grille) {
|
|
160
|
+
style['--zv-cols-somme'] = `${colonnes.reduce((s, c) => s + c.getSize(), 0)}px`
|
|
161
|
+
style['--zv-cols-n'] = String(colonnes.length)
|
|
162
|
+
}
|
|
64
163
|
|
|
65
164
|
return (
|
|
66
165
|
<div
|
|
67
166
|
className={cx('zv-table', className)}
|
|
68
|
-
style={
|
|
167
|
+
style={style as CSSProperties}
|
|
69
168
|
>
|
|
70
169
|
{table.getHeaderGroups().map((groupe) => (
|
|
71
170
|
<div className="zv-table__head" key={groupe.id}>
|
|
@@ -76,42 +175,61 @@ export function DataTable<T>({ table, vide, onRowClick, className }: DataTablePr
|
|
|
76
175
|
const contenu = entete.isPlaceholder
|
|
77
176
|
? null
|
|
78
177
|
: flexRender(entete.column.columnDef.header, entete.getContext())
|
|
178
|
+
const pin = grille ? epinglage(entete.column, table) : { classes: false as const }
|
|
179
|
+
const ariaSort =
|
|
180
|
+
sens === 'asc' ? ('ascending' as const) : sens === 'desc' ? ('descending' as const) : undefined
|
|
181
|
+
const props: CelluleEnteteProps<T> = {
|
|
182
|
+
entete,
|
|
183
|
+
className: cx('zv-cell', registre === 'num' && 'zv-cell--num', pin.classes),
|
|
184
|
+
style: pin.style,
|
|
185
|
+
// La colonne triée est annoncée : sans aria-sort, le tri est
|
|
186
|
+
// une information purement visuelle.
|
|
187
|
+
'aria-sort': ariaSort,
|
|
188
|
+
children: triable ? (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="zv-sort"
|
|
192
|
+
onClick={entete.column.getToggleSortingHandler()}
|
|
193
|
+
>
|
|
194
|
+
{celluleEntete ? <span className="zv-colhead__label">{contenu}</span> : contenu}
|
|
195
|
+
<span className="zv-sort__mark" aria-hidden="true">
|
|
196
|
+
{sens === 'asc' ? '↑' : sens === 'desc' ? '↓' : '↕'}
|
|
197
|
+
</span>
|
|
198
|
+
</button>
|
|
199
|
+
) : celluleEntete ? (
|
|
200
|
+
<span className="zv-colhead__label">{contenu}</span>
|
|
201
|
+
) : (
|
|
202
|
+
contenu
|
|
203
|
+
),
|
|
204
|
+
}
|
|
205
|
+
if (celluleEntete) return <Fragment key={entete.id}>{celluleEntete(props)}</Fragment>
|
|
79
206
|
return (
|
|
80
207
|
<div
|
|
81
208
|
key={entete.id}
|
|
82
|
-
className={
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
aria-sort={
|
|
86
|
-
sens === 'asc' ? 'ascending' : sens === 'desc' ? 'descending' : undefined
|
|
87
|
-
}
|
|
209
|
+
className={props.className}
|
|
210
|
+
style={props.style}
|
|
211
|
+
aria-sort={props['aria-sort']}
|
|
88
212
|
>
|
|
89
|
-
{
|
|
90
|
-
<button
|
|
91
|
-
type="button"
|
|
92
|
-
className="zv-sort"
|
|
93
|
-
onClick={entete.column.getToggleSortingHandler()}
|
|
94
|
-
>
|
|
95
|
-
{contenu}
|
|
96
|
-
<span className="zv-sort__mark" aria-hidden="true">
|
|
97
|
-
{sens === 'asc' ? '↑' : sens === 'desc' ? '↓' : '↕'}
|
|
98
|
-
</span>
|
|
99
|
-
</button>
|
|
100
|
-
) : (
|
|
101
|
-
contenu
|
|
102
|
-
)}
|
|
213
|
+
{props.children}
|
|
103
214
|
</div>
|
|
104
215
|
)
|
|
105
216
|
})}
|
|
106
217
|
</div>
|
|
107
218
|
))}
|
|
108
219
|
|
|
220
|
+
{sousEntete}
|
|
221
|
+
|
|
109
222
|
{lignes.length === 0
|
|
110
223
|
? vide
|
|
111
224
|
: lignes.map((ligne) => (
|
|
112
225
|
<div
|
|
113
226
|
key={ligne.id}
|
|
114
|
-
className=
|
|
227
|
+
className={cx(
|
|
228
|
+
'zv-row',
|
|
229
|
+
ligne.getIsSelected() && 'zv-row--selected',
|
|
230
|
+
ligneActive?.(ligne.original) && 'zv-row--on',
|
|
231
|
+
)}
|
|
232
|
+
aria-current={ligneActive?.(ligne.original) || undefined}
|
|
115
233
|
// Le clavier suit le clic : une ligne cliquable qui n'est pas
|
|
116
234
|
// atteignable au clavier est une action inaccessible.
|
|
117
235
|
role={onRowClick ? 'button' : undefined}
|
|
@@ -131,6 +249,7 @@ export function DataTable<T>({ table, vide, onRowClick, className }: DataTablePr
|
|
|
131
249
|
>
|
|
132
250
|
{ligne.getVisibleCells().map((cellule) => {
|
|
133
251
|
const meta = cellule.column.columnDef.meta
|
|
252
|
+
const pin = grille ? epinglage(cellule.column, table) : { classes: false as const }
|
|
134
253
|
return (
|
|
135
254
|
<div
|
|
136
255
|
key={cellule.id}
|
|
@@ -138,7 +257,10 @@ export function DataTable<T>({ table, vide, onRowClick, className }: DataTablePr
|
|
|
138
257
|
'zv-cell',
|
|
139
258
|
meta?.registre === 'nom' && 'zv-cell--name',
|
|
140
259
|
meta?.registre === 'num' && 'zv-cell--num',
|
|
260
|
+
meta?.classe,
|
|
261
|
+
pin.classes,
|
|
141
262
|
)}
|
|
263
|
+
style={pin.style}
|
|
142
264
|
>
|
|
143
265
|
{meta?.label && <span className="zv-cell__label">{meta.label}</span>}
|
|
144
266
|
{flexRender(cellule.column.columnDef.cell, cellule.getContext())}
|