@zevra/ui 0.41.0 → 0.43.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 +114 -0
- package/dist/catalogue.js +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/data-table.d.ts +41 -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 +201 -0
- package/dist/grille.d.ts.map +1 -0
- package/dist/grille.js +780 -0
- package/dist/grille.js.map +1 -0
- package/dist/icon-data.d.ts +1 -1
- package/dist/icon-data.d.ts.map +1 -1
- package/dist/icon-data.js +2 -0
- package/dist/icon-data.js.map +1 -1
- package/dist/icon.d.ts +1 -1
- package/dist/icon.js +1 -1
- 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/icons/index.json +6 -0
- package/icons/sprite.svg +1 -0
- package/package.json +16 -2
- package/react/catalogue.tsx +1 -1
- package/react/css-inline.ts +1 -1
- package/react/data-table.tsx +138 -26
- package/react/grille.tsx +1411 -0
- package/react/icon-data.ts +3 -0
- package/react/icon.tsx +2 -2
- package/react/use-persisted-state.ts +105 -0
- package/src/data.css +381 -0
- 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. */
|
|
@@ -55,17 +84,82 @@ export interface DataTableProps<T> {
|
|
|
55
84
|
className?: string
|
|
56
85
|
}
|
|
57
86
|
|
|
58
|
-
|
|
59
|
-
|
|
87
|
+
/** MODE GRILLE : activé par `enableColumnResizing`. Les largeurs viennent
|
|
88
|
+
* alors de TanStack en pixels (`size` = plancher, `columnSizing` = réglage
|
|
89
|
+
* de l'utilisateur), et les colonnes épinglées collent au bord du cadre.
|
|
90
|
+
* Sans ce réglage, le tableau rend exactement ce qu'il rendait avant. */
|
|
91
|
+
function piste<T>(c: Column<T, unknown>, regle: boolean): string {
|
|
92
|
+
const taille = c.getSize()
|
|
93
|
+
// Réglée ou épinglée : fixe. Sinon : un plancher, et une part de l'espace
|
|
94
|
+
// libre PROPORTIONNELLE au plancher, pour que les colonnes larges restent
|
|
95
|
+
// larges en s'étirant.
|
|
96
|
+
return regle || c.getIsPinned() ? `${taille}px` : `minmax(${taille}px, ${taille}fr)`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Classes et décalage d'une cellule épinglée (en-tête, filtres ou corps).
|
|
100
|
+
* Exporté pour la rangée de filtres de la grille, qui doit coller avec ses
|
|
101
|
+
* colonnes. */
|
|
102
|
+
export function epinglage<T>(
|
|
103
|
+
c: Column<T, unknown>,
|
|
104
|
+
table: TableTanstack<T>,
|
|
105
|
+
): { classes: string | false; style?: CSSProperties } {
|
|
106
|
+
const cote = c.getIsPinned()
|
|
107
|
+
if (!cote) return { classes: false }
|
|
108
|
+
const voisines = cote === 'left' ? table.getLeftVisibleLeafColumns() : table.getRightVisibleLeafColumns()
|
|
109
|
+
const rang = voisines.findIndex((v) => v.id === c.id)
|
|
110
|
+
// Le bord : la dernière à gauche, la première à droite, celle qui touche
|
|
111
|
+
// ce qui défile.
|
|
112
|
+
const bord = cote === 'left' ? rang === voisines.length - 1 : rang === 0
|
|
113
|
+
const decalage =
|
|
114
|
+
cote === 'left'
|
|
115
|
+
? { px: c.getStart('left'), gouttieres: rang }
|
|
116
|
+
: { px: c.getAfter('right'), gouttieres: voisines.length - 1 - rang }
|
|
117
|
+
const valeur = `calc(${decalage.px}px + ${decalage.gouttieres} * var(--zv-grille-gap))`
|
|
118
|
+
return {
|
|
119
|
+
classes: cx('zv-cell--pin', cote === 'right' && 'zv-cell--pin-droite', bord && 'zv-cell--pin-bord'),
|
|
120
|
+
style: cote === 'left' ? { left: valeur } : { right: valeur },
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Les colonnes visibles DANS L'ORDRE DU RENDU : épinglées à gauche,
|
|
125
|
+
* centrales, épinglées à droite. C'est l'ordre des en-têtes et des cellules ;
|
|
126
|
+
* `getVisibleLeafColumns()`, lui, ignore l'épinglage, et un gabarit bâti
|
|
127
|
+
* dessus donne à la première colonne la largeur d'une autre. */
|
|
128
|
+
export function colonnesAffichees<T>(table: TableTanstack<T>): Column<T, unknown>[] {
|
|
129
|
+
return [
|
|
130
|
+
...table.getLeftVisibleLeafColumns(),
|
|
131
|
+
...table.getCenterVisibleLeafColumns(),
|
|
132
|
+
...table.getRightVisibleLeafColumns(),
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function DataTable<T>({
|
|
137
|
+
table,
|
|
138
|
+
vide,
|
|
139
|
+
onRowClick,
|
|
140
|
+
celluleEntete,
|
|
141
|
+
sousEntete,
|
|
142
|
+
className,
|
|
143
|
+
}: DataTableProps<T>) {
|
|
144
|
+
const grille = table.options.enableColumnResizing === true
|
|
145
|
+
const reglages = table.getState().columnSizing
|
|
146
|
+
const colonnes = colonnesAffichees(table)
|
|
60
147
|
const gabarit = colonnes
|
|
61
|
-
.map((c) =>
|
|
148
|
+
.map((c) =>
|
|
149
|
+
grille ? piste(c, reglages[c.id] != null) : (c.columnDef.meta?.largeur ?? 'minmax(0, 1fr)'),
|
|
150
|
+
)
|
|
62
151
|
.join(' ')
|
|
63
152
|
const lignes = table.getRowModel().rows
|
|
153
|
+
const style: Record<string, string> = { '--zv-cols': gabarit }
|
|
154
|
+
if (grille) {
|
|
155
|
+
style['--zv-cols-somme'] = `${colonnes.reduce((s, c) => s + c.getSize(), 0)}px`
|
|
156
|
+
style['--zv-cols-n'] = String(colonnes.length)
|
|
157
|
+
}
|
|
64
158
|
|
|
65
159
|
return (
|
|
66
160
|
<div
|
|
67
161
|
className={cx('zv-table', className)}
|
|
68
|
-
style={
|
|
162
|
+
style={style as CSSProperties}
|
|
69
163
|
>
|
|
70
164
|
{table.getHeaderGroups().map((groupe) => (
|
|
71
165
|
<div className="zv-table__head" key={groupe.id}>
|
|
@@ -76,42 +170,56 @@ export function DataTable<T>({ table, vide, onRowClick, className }: DataTablePr
|
|
|
76
170
|
const contenu = entete.isPlaceholder
|
|
77
171
|
? null
|
|
78
172
|
: flexRender(entete.column.columnDef.header, entete.getContext())
|
|
173
|
+
const pin = grille ? epinglage(entete.column, table) : { classes: false as const }
|
|
174
|
+
const ariaSort =
|
|
175
|
+
sens === 'asc' ? ('ascending' as const) : sens === 'desc' ? ('descending' as const) : undefined
|
|
176
|
+
const props: CelluleEnteteProps<T> = {
|
|
177
|
+
entete,
|
|
178
|
+
className: cx('zv-cell', registre === 'num' && 'zv-cell--num', pin.classes),
|
|
179
|
+
style: pin.style,
|
|
180
|
+
// La colonne triée est annoncée : sans aria-sort, le tri est
|
|
181
|
+
// une information purement visuelle.
|
|
182
|
+
'aria-sort': ariaSort,
|
|
183
|
+
children: triable ? (
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
className="zv-sort"
|
|
187
|
+
onClick={entete.column.getToggleSortingHandler()}
|
|
188
|
+
>
|
|
189
|
+
{celluleEntete ? <span className="zv-colhead__label">{contenu}</span> : contenu}
|
|
190
|
+
<span className="zv-sort__mark" aria-hidden="true">
|
|
191
|
+
{sens === 'asc' ? '↑' : sens === 'desc' ? '↓' : '↕'}
|
|
192
|
+
</span>
|
|
193
|
+
</button>
|
|
194
|
+
) : celluleEntete ? (
|
|
195
|
+
<span className="zv-colhead__label">{contenu}</span>
|
|
196
|
+
) : (
|
|
197
|
+
contenu
|
|
198
|
+
),
|
|
199
|
+
}
|
|
200
|
+
if (celluleEntete) return <Fragment key={entete.id}>{celluleEntete(props)}</Fragment>
|
|
79
201
|
return (
|
|
80
202
|
<div
|
|
81
203
|
key={entete.id}
|
|
82
|
-
className={
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
aria-sort={
|
|
86
|
-
sens === 'asc' ? 'ascending' : sens === 'desc' ? 'descending' : undefined
|
|
87
|
-
}
|
|
204
|
+
className={props.className}
|
|
205
|
+
style={props.style}
|
|
206
|
+
aria-sort={props['aria-sort']}
|
|
88
207
|
>
|
|
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
|
-
)}
|
|
208
|
+
{props.children}
|
|
103
209
|
</div>
|
|
104
210
|
)
|
|
105
211
|
})}
|
|
106
212
|
</div>
|
|
107
213
|
))}
|
|
108
214
|
|
|
215
|
+
{sousEntete}
|
|
216
|
+
|
|
109
217
|
{lignes.length === 0
|
|
110
218
|
? vide
|
|
111
219
|
: lignes.map((ligne) => (
|
|
112
220
|
<div
|
|
113
221
|
key={ligne.id}
|
|
114
|
-
className=
|
|
222
|
+
className={cx('zv-row', ligne.getIsSelected() && 'zv-row--selected')}
|
|
115
223
|
// Le clavier suit le clic : une ligne cliquable qui n'est pas
|
|
116
224
|
// atteignable au clavier est une action inaccessible.
|
|
117
225
|
role={onRowClick ? 'button' : undefined}
|
|
@@ -131,6 +239,7 @@ export function DataTable<T>({ table, vide, onRowClick, className }: DataTablePr
|
|
|
131
239
|
>
|
|
132
240
|
{ligne.getVisibleCells().map((cellule) => {
|
|
133
241
|
const meta = cellule.column.columnDef.meta
|
|
242
|
+
const pin = grille ? epinglage(cellule.column, table) : { classes: false as const }
|
|
134
243
|
return (
|
|
135
244
|
<div
|
|
136
245
|
key={cellule.id}
|
|
@@ -138,7 +247,10 @@ export function DataTable<T>({ table, vide, onRowClick, className }: DataTablePr
|
|
|
138
247
|
'zv-cell',
|
|
139
248
|
meta?.registre === 'nom' && 'zv-cell--name',
|
|
140
249
|
meta?.registre === 'num' && 'zv-cell--num',
|
|
250
|
+
meta?.classe,
|
|
251
|
+
pin.classes,
|
|
141
252
|
)}
|
|
253
|
+
style={pin.style}
|
|
142
254
|
>
|
|
143
255
|
{meta?.label && <span className="zv-cell__label">{meta.label}</span>}
|
|
144
256
|
{flexRender(cellule.column.columnDef.cell, cellule.getContext())}
|