@zevra/ui 0.42.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/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/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 +138 -26
- package/react/grille.tsx +1411 -0
- package/react/use-persisted-state.ts +105 -0
- package/src/data.css +381 -0
- package/src/forms.css +14 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Un état React qui survit au rechargement, dans `localStorage`.
|
|
4
|
+
//
|
|
5
|
+
// ⚠️ LA LECTURE A LIEU APRÈS LE MONTAGE, et c'est tout l'objet du hook. Lue
|
|
6
|
+
// pendant le rendu, la valeur différerait entre le serveur (qui n'a pas de
|
|
7
|
+
// `localStorage`) et le navigateur : React signale alors une erreur
|
|
8
|
+
// d'hydratation et Next jette le rendu serveur. Le premier rendu montre donc
|
|
9
|
+
// `initial`, puis la valeur enregistrée.
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ TOUT ACCÈS EST SOUS try/catch : navigation privée, quota plein, stockage
|
|
12
|
+
// bloqué par l'utilisateur ou une politique d'entreprise, JSON abîmé à la
|
|
13
|
+
// main. Dans chacun de ces cas l'état fonctionne, il ne persiste simplement
|
|
14
|
+
// pas. Un tableau qui plante parce que Safari refuse d'écrire est pire qu'un
|
|
15
|
+
// tableau qui oublie sa vue.
|
|
16
|
+
|
|
17
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
18
|
+
import type { Dispatch, SetStateAction } from 'react'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Comme `useState`, mais enregistré sous `key`.
|
|
22
|
+
*
|
|
23
|
+
* `reconcilier` reçoit la valeur LUE (non typée : elle vient d'une version
|
|
24
|
+
* précédente du code, ou d'ailleurs) et la valeur initiale ; il rend la
|
|
25
|
+
* valeur à employer, ou `undefined` pour s'en tenir à l'initiale. C'est là
|
|
26
|
+
* qu'une vue enregistrée s'adapte à des colonnes qui ont changé.
|
|
27
|
+
*
|
|
28
|
+
* Rend `[valeur, setValeur, reinitialiser]` : `reinitialiser` vide la clé et
|
|
29
|
+
* revient à `initial`.
|
|
30
|
+
*/
|
|
31
|
+
export function usePersistedState<S>(
|
|
32
|
+
key: string,
|
|
33
|
+
initial: S,
|
|
34
|
+
reconcilier?: (lu: unknown, initial: S) => S | undefined,
|
|
35
|
+
): [S, Dispatch<SetStateAction<S>>, () => void] {
|
|
36
|
+
const [etat, setEtat] = useState<S>(initial)
|
|
37
|
+
const [lu, setLu] = useState(false)
|
|
38
|
+
|
|
39
|
+
// Les dernières valeurs, sans en faire des dépendances : `initial` est
|
|
40
|
+
// souvent un littéral recréé à chaque rendu, et relire le stockage à
|
|
41
|
+
// chaque rendu effacerait les changements en cours.
|
|
42
|
+
const initialRef = useRef(initial)
|
|
43
|
+
initialRef.current = initial
|
|
44
|
+
const reconcilierRef = useRef(reconcilier)
|
|
45
|
+
reconcilierRef.current = reconcilier
|
|
46
|
+
|
|
47
|
+
// Ce que le stockage contient DÉJÀ, sérialisé. On n'écrit que ce qui en
|
|
48
|
+
// diffère : sans cette garde, la lecture elle-même provoquerait une
|
|
49
|
+
// écriture, et « Réinitialiser » réécrirait aussitôt la vue par défaut
|
|
50
|
+
// dans la clé qu'il vient de vider.
|
|
51
|
+
const enregistre = useRef<string | null>(null)
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
let valeur: S | undefined
|
|
55
|
+
try {
|
|
56
|
+
const brut = window.localStorage.getItem(key)
|
|
57
|
+
if (brut != null) {
|
|
58
|
+
const parse: unknown = JSON.parse(brut)
|
|
59
|
+
valeur = reconcilierRef.current
|
|
60
|
+
? reconcilierRef.current(parse, initialRef.current)
|
|
61
|
+
: (parse as S)
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
valeur = undefined
|
|
65
|
+
}
|
|
66
|
+
const retenue = valeur === undefined ? initialRef.current : valeur
|
|
67
|
+
enregistre.current = serialiser(retenue)
|
|
68
|
+
setEtat(retenue)
|
|
69
|
+
setLu(true)
|
|
70
|
+
}, [key])
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
// Avant la lecture, l'état est `initial` : l'écrire écraserait la vue
|
|
74
|
+
// enregistrée par la vue par défaut, au premier rendu de chaque page.
|
|
75
|
+
if (!lu) return
|
|
76
|
+
const texte = serialiser(etat)
|
|
77
|
+
if (texte === null || texte === enregistre.current) return
|
|
78
|
+
try {
|
|
79
|
+
window.localStorage.setItem(key, texte)
|
|
80
|
+
enregistre.current = texte
|
|
81
|
+
} catch {
|
|
82
|
+
// Stockage refusé : l'état vit en mémoire, c'est tout.
|
|
83
|
+
}
|
|
84
|
+
}, [key, etat, lu])
|
|
85
|
+
|
|
86
|
+
const reinitialiser = useCallback(() => {
|
|
87
|
+
try {
|
|
88
|
+
window.localStorage.removeItem(key)
|
|
89
|
+
} catch {
|
|
90
|
+
// Rien à vider qu'on puisse atteindre : on revient quand même au défaut.
|
|
91
|
+
}
|
|
92
|
+
enregistre.current = serialiser(initialRef.current)
|
|
93
|
+
setEtat(initialRef.current)
|
|
94
|
+
}, [key])
|
|
95
|
+
|
|
96
|
+
return [etat, setEtat, reinitialiser]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function serialiser(valeur: unknown): string | null {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.stringify(valeur)
|
|
102
|
+
} catch {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/data.css
CHANGED
|
@@ -980,3 +980,384 @@
|
|
|
980
980
|
padding: 12px;
|
|
981
981
|
}
|
|
982
982
|
}
|
|
983
|
+
|
|
984
|
+
/* ─── Grille de données (DataGrid) ─────────────────────────────────────
|
|
985
|
+
* ⚠️ HORS RELEVÉ, tout ce bloc (24/10/2026) : aucune planche ne dessine un
|
|
986
|
+
* tableau qu'on réordonne, élargit, épingle ou coche. L'arbitrage et ses
|
|
987
|
+
* raisons sont dans DECISIONS.md, « La grille de données ». Aucune valeur
|
|
988
|
+
* nouvelle n'entre : les gouttières sont celles du tableau (14 et 10 px),
|
|
989
|
+
* les fonds ceux du paquet (blanc, --panel au survol, --accent-soft pour
|
|
990
|
+
* le choisi), les filets --line et --hairline.
|
|
991
|
+
*
|
|
992
|
+
* La grille est un MODE de .zv-table, pas un autre composant : mêmes
|
|
993
|
+
* lignes, mêmes cellules, même passage en cartes sous 768 px. */
|
|
994
|
+
|
|
995
|
+
.zv-grille {
|
|
996
|
+
display: grid;
|
|
997
|
+
gap: 12px;
|
|
998
|
+
min-width: 0;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/* Recherche à gauche, actions à droite ; elles passent à la ligne plutôt
|
|
1002
|
+
* que de pousser le cadre sur un écran étroit. */
|
|
1003
|
+
.zv-grille__outils {
|
|
1004
|
+
display: flex;
|
|
1005
|
+
flex-wrap: wrap;
|
|
1006
|
+
align-items: center;
|
|
1007
|
+
justify-content: space-between;
|
|
1008
|
+
gap: 10px;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
.zv-grille__outils > .zv-search {
|
|
1012
|
+
flex: 1 1 240px;
|
|
1013
|
+
max-width: 360px;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.zv-grille__actions {
|
|
1017
|
+
display: flex;
|
|
1018
|
+
flex-wrap: wrap;
|
|
1019
|
+
align-items: center;
|
|
1020
|
+
gap: 8px;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/* Les gouttières de la rangée deviennent des PARAMÈTRES : les colonnes
|
|
1024
|
+
* épinglées en ont besoin pour calculer leur décalage, et la largeur
|
|
1025
|
+
* minimale des rangées pour additionner ses pistes. Mêmes valeurs que
|
|
1026
|
+
* .zv-row et .zv-table__head (dérivées, pas relevées : redites ici). */
|
|
1027
|
+
.zv-table--grille {
|
|
1028
|
+
--zv-grille-pad: 14px;
|
|
1029
|
+
--zv-grille-gap: 10px;
|
|
1030
|
+
/* Un seul ascenseur pour l'en-tête et les lignes : le cadre. */
|
|
1031
|
+
position: relative;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/* Une rangée de grille est au moins aussi large que la somme de ses
|
|
1035
|
+
* pistes PLANCHERS : sans cela, des colonnes réglées en pixels déborderaient
|
|
1036
|
+
* de la rangée, et le filet comme le fond de survol s'arrêteraient au bord
|
|
1037
|
+
* du cadre pendant que les cellules continuent. --zv-cols-somme et
|
|
1038
|
+
* --zv-cols-n sont posés par le composant (largeurs choisies par
|
|
1039
|
+
* l'utilisateur, pas des valeurs de charte). */
|
|
1040
|
+
.zv-table--grille > .zv-table__head,
|
|
1041
|
+
.zv-table--grille > .zv-row {
|
|
1042
|
+
min-width: calc(
|
|
1043
|
+
var(--zv-cols-somme, 0px) + 2 * var(--zv-grille-pad) +
|
|
1044
|
+
(var(--zv-cols-n, 1) - 1) * var(--zv-grille-gap)
|
|
1045
|
+
);
|
|
1046
|
+
padding-inline: var(--zv-grille-pad);
|
|
1047
|
+
column-gap: var(--zv-grille-gap);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/* Rangée de FILTRES, sous les intitulés : même fond, mais ni capitales ni
|
|
1051
|
+
* interlettrage, ce sont des champs. Le filet de l'en-tête passe sous elle. */
|
|
1052
|
+
.zv-table__head--filtres {
|
|
1053
|
+
padding-block: 6px;
|
|
1054
|
+
font-family: var(--font-text);
|
|
1055
|
+
font-weight: 400;
|
|
1056
|
+
letter-spacing: normal;
|
|
1057
|
+
text-transform: none;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.zv-table__head:has(+ .zv-table__head--filtres) {
|
|
1061
|
+
border-bottom-color: var(--hairline);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/* Le champ de filtre : le .zv-input du paquet, resserré à la hauteur d'une
|
|
1065
|
+
* rangée d'en-tête. Une seule classe suffit à l'emporter : data.css est
|
|
1066
|
+
* chargé après forms.css (index.css). */
|
|
1067
|
+
.zv-table__filtre {
|
|
1068
|
+
padding: 5px 8px;
|
|
1069
|
+
font-size: var(--app-sm);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/* ─── En-tête de colonne de grille ──────────────────────────────────── */
|
|
1073
|
+
|
|
1074
|
+
.zv-colhead {
|
|
1075
|
+
position: relative;
|
|
1076
|
+
display: flex;
|
|
1077
|
+
align-items: center;
|
|
1078
|
+
gap: 4px;
|
|
1079
|
+
/* La poignée de largeur déborde dans la gouttière : sans cela elle
|
|
1080
|
+
* serait rognée par l'overflow de .zv-cell. */
|
|
1081
|
+
overflow: visible;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/* L'intitulé tronque, pas la cellule : la poignée et la marque de tri
|
|
1085
|
+
* doivent rester visibles sur une colonne étroite. */
|
|
1086
|
+
.zv-colhead .zv-sort {
|
|
1087
|
+
min-width: 0;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.zv-colhead__label {
|
|
1091
|
+
min-width: 0;
|
|
1092
|
+
overflow: hidden;
|
|
1093
|
+
text-overflow: ellipsis;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/* Poignée de déplacement. Un <button> : c'est elle qui porte le clavier
|
|
1097
|
+
* (Espace pour saisir, flèches, Espace pour poser). `touch-action: none` :
|
|
1098
|
+
* sans lui, le doigt fait défiler le cadre au lieu de saisir la colonne. */
|
|
1099
|
+
.zv-colhead__grip {
|
|
1100
|
+
flex: none;
|
|
1101
|
+
display: inline-grid;
|
|
1102
|
+
place-items: center;
|
|
1103
|
+
margin: -2px 0 -2px -4px;
|
|
1104
|
+
padding: 2px;
|
|
1105
|
+
border: 0;
|
|
1106
|
+
background: none;
|
|
1107
|
+
color: var(--label);
|
|
1108
|
+
cursor: grab;
|
|
1109
|
+
touch-action: none;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.zv-colhead__grip:hover {
|
|
1113
|
+
color: var(--accent);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.zv-colhead__grip:focus-visible {
|
|
1117
|
+
outline-offset: -2px;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/* Tout l'en-tête se saisit : la main ouverte le dit au survol. Le bouton
|
|
1121
|
+
* de tri garde son pointeur, puisqu'un clic simple trie toujours. */
|
|
1122
|
+
.zv-colhead--deplacable {
|
|
1123
|
+
cursor: grab;
|
|
1124
|
+
touch-action: none;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/* La colonne qu'on déplace : le fond de l'entrée choisie, et elle passe
|
|
1128
|
+
* au-dessus de ses voisines qu'elle traverse. */
|
|
1129
|
+
.zv-colhead--drag {
|
|
1130
|
+
z-index: 3;
|
|
1131
|
+
background: var(--accent-soft);
|
|
1132
|
+
cursor: grabbing;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/* Poignée de largeur : une bande de saisie de la largeur de la gouttière,
|
|
1136
|
+
* centrée sur la frontière, et un filet qui ne se montre qu'à l'approche. */
|
|
1137
|
+
.zv-colhead__resize {
|
|
1138
|
+
position: absolute;
|
|
1139
|
+
top: -9px;
|
|
1140
|
+
bottom: -9px;
|
|
1141
|
+
right: calc(-1 * var(--zv-grille-gap, 10px) / 2 - 4px);
|
|
1142
|
+
z-index: 2;
|
|
1143
|
+
width: 8px;
|
|
1144
|
+
cursor: col-resize;
|
|
1145
|
+
touch-action: none;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
.zv-colhead__resize::after {
|
|
1149
|
+
content: '';
|
|
1150
|
+
position: absolute;
|
|
1151
|
+
top: 9px;
|
|
1152
|
+
bottom: 9px;
|
|
1153
|
+
left: 3px;
|
|
1154
|
+
width: 2px;
|
|
1155
|
+
background: var(--line);
|
|
1156
|
+
opacity: 0;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.zv-colhead:hover .zv-colhead__resize::after {
|
|
1160
|
+
opacity: 1;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.zv-colhead__resize:hover::after,
|
|
1164
|
+
.zv-colhead__resize:focus-visible::after,
|
|
1165
|
+
.zv-colhead__resize--actif::after {
|
|
1166
|
+
background: var(--accent);
|
|
1167
|
+
opacity: 1;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.zv-colhead__resize:focus-visible {
|
|
1171
|
+
outline: none;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/* Pendant le geste, le curseur ne doit pas changer en traversant les
|
|
1175
|
+
* cellules voisines. */
|
|
1176
|
+
.zv-colhead__resize--actif {
|
|
1177
|
+
cursor: col-resize;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/* ─── Colonnes épinglées ────────────────────────────────────────────────
|
|
1181
|
+
* Collées au bord du cadre pendant que le reste défile. Le fond est OPAQUE
|
|
1182
|
+
* et suit le contexte (blanc, survol, choisi) par une variable : une cellule
|
|
1183
|
+
* transparente laisserait passer les valeurs qui glissent dessous. L'ombre
|
|
1184
|
+
* pleine, sans flou, bouche la gouttière : c'est une géométrie, pas une
|
|
1185
|
+
* ombre de charte. */
|
|
1186
|
+
.zv-cell--pin {
|
|
1187
|
+
--zv-pin-fond: var(--white);
|
|
1188
|
+
position: sticky;
|
|
1189
|
+
z-index: 1;
|
|
1190
|
+
background: var(--zv-pin-fond);
|
|
1191
|
+
box-shadow: calc(-1 * var(--zv-grille-gap, 10px)) 0 0 0 var(--zv-pin-fond);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.zv-cell--pin-droite {
|
|
1195
|
+
box-shadow: var(--zv-grille-gap, 10px) 0 0 0 var(--zv-pin-fond);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/* La frontière entre ce qui reste et ce qui défile : un filet --line,
|
|
1199
|
+
* côté contenu. */
|
|
1200
|
+
.zv-cell--pin-bord {
|
|
1201
|
+
box-shadow:
|
|
1202
|
+
calc(-1 * var(--zv-grille-gap, 10px)) 0 0 0 var(--zv-pin-fond),
|
|
1203
|
+
calc(var(--zv-grille-gap, 10px) / 2) 0 0 0 var(--zv-pin-fond),
|
|
1204
|
+
calc(var(--zv-grille-gap, 10px) / 2 + 1px) 0 0 0 var(--line);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.zv-cell--pin-droite.zv-cell--pin-bord {
|
|
1208
|
+
box-shadow:
|
|
1209
|
+
var(--zv-grille-gap, 10px) 0 0 0 var(--zv-pin-fond),
|
|
1210
|
+
calc(-1 * var(--zv-grille-gap, 10px) / 2) 0 0 0 var(--zv-pin-fond),
|
|
1211
|
+
calc(-1 * var(--zv-grille-gap, 10px) / 2 - 1px) 0 0 0 var(--line);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.zv-table__head .zv-cell--pin,
|
|
1215
|
+
.zv-row:hover .zv-cell--pin {
|
|
1216
|
+
--zv-pin-fond: var(--panel);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/* ─── Ligne choisie ─────────────────────────────────────────────────────
|
|
1220
|
+
* Le fond de l'entrée de menu choisie. Il tient au survol : sinon, la
|
|
1221
|
+
* ligne qu'on vient de cocher perdrait sa marque sous le pointeur. */
|
|
1222
|
+
.zv-row--selected,
|
|
1223
|
+
.zv-row--selected:hover {
|
|
1224
|
+
background: var(--accent-soft);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
.zv-row--selected .zv-cell--pin,
|
|
1228
|
+
.zv-row--selected:hover .zv-cell--pin {
|
|
1229
|
+
--zv-pin-fond: var(--accent-soft);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/* La case de sélection : centrée dans sa piste, sans troncature. */
|
|
1233
|
+
.zv-cell--case {
|
|
1234
|
+
display: flex;
|
|
1235
|
+
align-items: center;
|
|
1236
|
+
overflow: visible;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/* Un nombre s'aligne à droite : son intitulé aussi. */
|
|
1240
|
+
.zv-colhead.zv-cell--num {
|
|
1241
|
+
justify-content: flex-end;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/* Le menu des colonnes s'ouvre depuis la DROITE de la barre : ancré à
|
|
1245
|
+
* gauche comme le menu de sélection, il sortirait de l'écran. Il défile
|
|
1246
|
+
* au-delà d'une douzaine de colonnes plutôt que de couvrir la page. */
|
|
1247
|
+
.zv-grille__menu {
|
|
1248
|
+
left: auto;
|
|
1249
|
+
right: 0;
|
|
1250
|
+
max-height: min(420px, 70vh);
|
|
1251
|
+
overflow-y: auto;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.zv-grille__menu-ligne {
|
|
1255
|
+
display: flex;
|
|
1256
|
+
align-items: center;
|
|
1257
|
+
gap: 4px;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/* L'entrée de menu porte ici une case et un intitulé : elle redevient une
|
|
1261
|
+
* rangée, et la case ne s'écrase pas. */
|
|
1262
|
+
.zv-grille__menu-case {
|
|
1263
|
+
display: flex;
|
|
1264
|
+
flex: 1;
|
|
1265
|
+
align-items: center;
|
|
1266
|
+
gap: 8px;
|
|
1267
|
+
min-width: 0;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/* L'épingle : un bouton d'icône à la hauteur d'une entrée de menu, pas à
|
|
1271
|
+
* celle d'une cible de barre (--tap). */
|
|
1272
|
+
.zv-grille__epingle {
|
|
1273
|
+
flex: none;
|
|
1274
|
+
width: 30px;
|
|
1275
|
+
height: 30px;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
.zv-grille__epingle[aria-pressed='true'] {
|
|
1279
|
+
color: var(--accent);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/* La taille de page : le <select> du paquet, à sa largeur de contenu. */
|
|
1283
|
+
.zv-grille__taille {
|
|
1284
|
+
width: auto;
|
|
1285
|
+
padding-block: 5px;
|
|
1286
|
+
font-size: var(--app-sm);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/* Le résumé d'une plage (nombres, dates) dans la rangée de filtres : un
|
|
1290
|
+
* déclencheur à l'apparence du champ, resserré comme lui. */
|
|
1291
|
+
.zv-table__filtre.zv-input--trigger {
|
|
1292
|
+
gap: 4px;
|
|
1293
|
+
min-width: 0;
|
|
1294
|
+
width: 100%;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/* L'enveloppe du popover de plage, rendue HORS du tableau (portail) : le
|
|
1298
|
+
* cadre défile, il rognerait le panneau. Placée en coordonnées d'écran par
|
|
1299
|
+
* le composant ; le panneau, .zv-popover, s'y pose comme sous son ancre. */
|
|
1300
|
+
.zv-grille__flottant {
|
|
1301
|
+
position: fixed;
|
|
1302
|
+
z-index: 60;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/* Raccourcis, bornes, effacement : un petit formulaire. Le popover du
|
|
1306
|
+
* paquet borne sa largeur à 320 px, trop juste pour deux champs de date
|
|
1307
|
+
* côte à côte : la grille la reprend (même spécificité, data.css est chargé
|
|
1308
|
+
* après navigation.css). */
|
|
1309
|
+
.zv-grille__plage {
|
|
1310
|
+
display: grid;
|
|
1311
|
+
gap: 14px;
|
|
1312
|
+
width: 340px;
|
|
1313
|
+
max-width: calc(100vw - 16px);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/* Les raccourcis de période : des entrées de menu (le survol et l'état
|
|
1317
|
+
* choisi du paquet), sur deux colonnes. Le filet les sépare des bornes. */
|
|
1318
|
+
.zv-grille__periodes {
|
|
1319
|
+
display: grid;
|
|
1320
|
+
grid-template-columns: 1fr 1fr;
|
|
1321
|
+
gap: 2px;
|
|
1322
|
+
margin: -6px -8px 0;
|
|
1323
|
+
padding-bottom: 12px;
|
|
1324
|
+
border-bottom: 1px solid var(--hairline);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.zv-grille__bornes {
|
|
1328
|
+
display: grid;
|
|
1329
|
+
grid-template-columns: 1fr 1fr;
|
|
1330
|
+
gap: 10px;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/* Un champ date natif a une largeur intrinsèque : sans min-width, il
|
|
1334
|
+
* pousse sa colonne hors du popover. */
|
|
1335
|
+
.zv-grille__bornes .zv-input {
|
|
1336
|
+
min-width: 0;
|
|
1337
|
+
padding-inline: 8px;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/* Le pied : compte à gauche, taille de page et sens à droite. */
|
|
1341
|
+
.zv-grille__pied {
|
|
1342
|
+
display: flex;
|
|
1343
|
+
align-items: center;
|
|
1344
|
+
gap: 14px;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
@media (max-width: 767px) {
|
|
1348
|
+
/* En cartes, l'épinglage, les poignées et la largeur minimale n'ont plus
|
|
1349
|
+
* de sens : la carte prend la largeur de l'écran. */
|
|
1350
|
+
.zv-table--grille > .zv-row {
|
|
1351
|
+
min-width: 0;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
.zv-cell--pin {
|
|
1355
|
+
position: static;
|
|
1356
|
+
box-shadow: none;
|
|
1357
|
+
background: none;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.zv-table__head--filtres {
|
|
1361
|
+
display: none;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
package/src/forms.css
CHANGED
|
@@ -514,6 +514,20 @@
|
|
|
514
514
|
background-size: 12px 12px;
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
/* Case INDÉTERMINÉE (« une partie des lignes est cochée »), en tête d'une
|
|
518
|
+
* colonne de sélection. ⚠️ HORS RELEVÉ (24/10/2026, grille de données) :
|
|
519
|
+
* aucune planche ne la dessine. Même facture que la coche : aplat accent,
|
|
520
|
+
* trait blanc carré de 1,5. L'état vient de la propriété DOM
|
|
521
|
+
* `indeterminate`, que seul le script pose : il n'a pas d'attribut HTML. */
|
|
522
|
+
.zv-check-box:indeterminate {
|
|
523
|
+
border-color: var(--accent);
|
|
524
|
+
background-color: var(--accent);
|
|
525
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23fff' stroke-width='1.5' stroke-linecap='square'%3E%3Cline x1='3' y1='6' x2='9' y2='6'/%3E%3C/svg%3E");
|
|
526
|
+
background-position: center;
|
|
527
|
+
background-repeat: no-repeat;
|
|
528
|
+
background-size: 12px 12px;
|
|
529
|
+
}
|
|
530
|
+
|
|
517
531
|
/* Le point du radio est creusé dans l'aplat par un anneau blanc interne :
|
|
518
532
|
* un pseudo-élément sur un <input> n'est pas fiable d'un moteur à l'autre.
|
|
519
533
|
* Ce box-shadow est une géométrie, pas une ombre de charte : d'où l'absence
|