kelt-ui-kit-react 2.1.7 → 2.1.9

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.
Files changed (50) hide show
  1. package/dist/badge/Badge.d.ts +2 -1
  2. package/dist/damier/damierCell/damierCell.interface.d.ts +3 -0
  3. package/dist/damier/damierEditor/DamierEditor.d.ts +33 -0
  4. package/dist/damier/damierEditor/damierEditorCell/DamierEditorCell.d.ts +16 -0
  5. package/dist/damier/damierEditor/damierEditorToolbar/DamierEditorToolbar.d.ts +10 -0
  6. package/dist/damier/damierEditor/damierEditorTree/DamierEditorTree.d.ts +13 -0
  7. package/dist/damier/damierEditor/damierEditorTree/damierEditorTreeNode/DamierEditorTreeNode.d.ts +11 -0
  8. package/dist/damier/damierEditor/damierEditorTree/damierTreeNode.interface.d.ts +5 -0
  9. package/dist/damier/damierEditor/maybePromise.interface.d.ts +1 -0
  10. package/dist/damier/damierPage/damierPage.interface.d.ts +8 -0
  11. package/dist/damier/utils/buildDamierTree.d.ts +3 -0
  12. package/dist/damier/utils/createDefaultDamierPage.d.ts +2 -0
  13. package/dist/damier/utils/damierDefaultSize.d.ts +1 -0
  14. package/dist/damier/utils/findDamierTreePath.d.ts +2 -0
  15. package/dist/damier/utils/getFirstFreePosition.d.ts +2 -0
  16. package/dist/damier/utils/moveDamierCell.d.ts +2 -0
  17. package/dist/form/form.interface.d.ts +2 -0
  18. package/dist/form/getFieldError.d.ts +2 -0
  19. package/dist/index.js +889 -858
  20. package/dist/style.css +1 -1
  21. package/package.json +1 -1
  22. package/src/badge/Badge.tsx +3 -0
  23. package/src/badge/Badge.view.tsx +1 -0
  24. package/src/damier/Damier.tsx +1 -1
  25. package/src/damier/Damier.view.tsx +46 -0
  26. package/src/damier/damierCell/DamierCell.tsx +1 -1
  27. package/src/damier/damierCell/damierCell.interface.tsx +3 -0
  28. package/src/damier/damierEditor/DamierEditor.tsx +346 -0
  29. package/src/damier/damierEditor/damierEditor.css +255 -0
  30. package/src/damier/damierEditor/damierEditorCell/DamierEditorCell.tsx +91 -0
  31. package/src/damier/damierEditor/damierEditorToolbar/DamierEditorToolbar.tsx +71 -0
  32. package/src/damier/damierEditor/damierEditorTree/DamierEditorTree.tsx +134 -0
  33. package/src/damier/damierEditor/damierEditorTree/damierEditorTreeNode/DamierEditorTreeNode.tsx +78 -0
  34. package/src/damier/damierEditor/damierEditorTree/damierTreeNode.interface.ts +6 -0
  35. package/src/damier/damierEditor/maybePromise.interface.ts +1 -0
  36. package/src/damier/damierPage/damierPage.interface.tsx +9 -0
  37. package/src/damier/utils/buildDamierTree.ts +31 -0
  38. package/src/damier/utils/createDefaultDamierPage.ts +10 -0
  39. package/src/damier/utils/damierDefaultSize.ts +1 -0
  40. package/src/damier/utils/findDamierTreePath.ts +14 -0
  41. package/src/damier/utils/getFirstFreePosition.ts +12 -0
  42. package/src/damier/utils/moveDamierCell.ts +13 -0
  43. package/src/dataTable/DataTable.tsx +2 -2
  44. package/src/docs/catalog.ts +1 -1
  45. package/src/form/Form.tsx +68 -8
  46. package/src/form/form.css +9 -0
  47. package/src/form/form.interface.tsx +2 -0
  48. package/src/form/getFieldError.ts +14 -0
  49. package/src/select/select.css +11 -1
  50. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kelt-ui-kit-react",
3
3
  "type": "module",
4
- "version": "2.1.7",
4
+ "version": "2.1.9",
5
5
  "private": false,
6
6
  "repository": {
7
7
  "type": "git",
@@ -6,6 +6,7 @@ export type BadgeInterface = {
6
6
  className?: string;
7
7
  onClick?: (e: React.MouseEvent) => void;
8
8
  title?: string;
9
+ tooltip?: string;
9
10
  sizeIcon?: IconSizeEnum;
10
11
  classIcon?: string;
11
12
  color?: string;
@@ -15,6 +16,7 @@ export const Badge = ({
15
16
  className,
16
17
  onClick,
17
18
  title,
19
+ tooltip,
18
20
  sizeIcon,
19
21
  classIcon,
20
22
  color,
@@ -29,6 +31,7 @@ export const Badge = ({
29
31
  return (
30
32
  <div
31
33
  onClick={handleClick}
34
+ title={tooltip}
32
35
  style={color ? { backgroundColor: color } : undefined}
33
36
  className={`badge ${className ? className : ""}`}
34
37
  >
@@ -6,6 +6,7 @@ export const BadgeView = (): JSX.Element => {
6
6
  <Badge
7
7
  className="badge--primary"
8
8
  title="Badge"
9
+ tooltip="Texte affiché au survol"
9
10
  sizeIcon={undefined}
10
11
  classIcon="bi-check-circle"
11
12
  onClick={() => console.log("Badge clicked")}
@@ -49,7 +49,7 @@ export const Damier = ({
49
49
  );
50
50
  }
51
51
  return rowsArray;
52
- }, [nbCols, nbRows, currentPage]);
52
+ }, [nbCols, nbRows, currentPage, damierCells, onClick]);
53
53
 
54
54
  const damierStyle = useMemo(
55
55
  () => ({ "--rows": String(nbRows) }) as CSSProperties,
@@ -2,9 +2,12 @@ import { useState } from "react";
2
2
  import { Button } from "../button/Button";
3
3
  import { Damier } from "./Damier";
4
4
  import { DamierCellInterface } from "./damierCell/damierCell.interface";
5
+ import { DamierEditor } from "./damierEditor/DamierEditor";
6
+ import { DamierPageInterface } from "./damierPage/damierPage.interface";
5
7
 
6
8
  export const DamierView = () => {
7
9
  const [page, setPage] = useState(1);
10
+ const [lastAction, setLastAction] = useState("");
8
11
  const items: DamierCellInterface[] = [
9
12
  {
10
13
  position: 2,
@@ -19,13 +22,56 @@ export const DamierView = () => {
19
22
  child: <div>82</div>,
20
23
  },
21
24
  ];
25
+ // État stable : un nouveau tableau à chaque rendu réinitialiserait l'éditeur
26
+ const [pages] = useState<DamierPageInterface[]>(() => [
27
+ {
28
+ id: 1,
29
+ title: "Sommaire",
30
+ cols: 6,
31
+ rows: 4,
32
+ cells: [
33
+ { id: 1, position: 1, label: "Accessoires", linkPageId: 2 },
34
+ { id: 2, position: 2, label: "Cheveux", linkPageId: 3 },
35
+ { id: 3, position: 8, label: "Sacs" },
36
+ ],
37
+ },
38
+ {
39
+ id: 2,
40
+ title: "Accessoires",
41
+ cols: 6,
42
+ rows: 4,
43
+ cells: [{ id: 4, position: 1, label: "Chouchous", linkPageId: 4 }],
44
+ },
45
+ { id: 3, title: "Cheveux", cols: 6, rows: 4, cells: [] },
46
+ { id: 4, title: "Chouchous", cols: 6, rows: 4, cells: [] },
47
+ { id: 5, title: "Non associé", cols: 6, rows: 4, cells: [] },
48
+ ]);
22
49
 
23
50
  return (
24
51
  <>
52
+ <h3>Affichage</h3>
25
53
  <Button onClick={() => setPage(page - 1)} title="previous" />
26
54
  <Button onClick={() => setPage(page + 1)} title="next" />
27
55
 
28
56
  <Damier damierCells={items} page={page} />
57
+
58
+ <h3>Édition (glisser-déposer)</h3>
59
+ <p>{lastAction || "Glissez une case, cliquez sur un emplacement vide..."}</p>
60
+ <DamierEditor
61
+ pages={pages}
62
+ onAddPage={(added) => setLastAction(`Page ajoutée : ${added.title}`)}
63
+ onAddCell={(cell) => setLastAction(`Case ajoutée en ${cell.position}`)}
64
+ onMoveCells={(cells) =>
65
+ setLastAction(
66
+ `Déplacement : ${cells.map((c) => `${c.label} → ${c.position}`).join(", ")}`,
67
+ )
68
+ }
69
+ onClickCell={(cell) => setLastAction(`Clic sur ${cell.label}`)}
70
+ onDeleteCell={(cell) => setLastAction(`Case supprimée : ${cell.label}`)}
71
+ />
72
+
73
+ <h3>Édition avec init par défaut</h3>
74
+ <DamierEditor defaultCols={5} defaultRows={3} />
29
75
  </>
30
76
  );
31
77
  };
@@ -12,7 +12,7 @@ export const DamierCell = ({ cell, onClick }: DamierCellProps) => {
12
12
  };
13
13
  return (
14
14
  <div className="cell" onClick={handleClick}>
15
- {cell.child}
15
+ {cell.child ?? cell.label}
16
16
  </div>
17
17
  );
18
18
  };
@@ -1,5 +1,8 @@
1
1
  export interface DamierCellInterface {
2
2
  child?: JSX.Element;
3
+ label?: string;
3
4
  position?: number;
4
5
  id?: number;
6
+ /** Page ouverte par la case : sert à construire l'arborescence des pages */
7
+ linkPageId?: number;
5
8
  }
@@ -0,0 +1,346 @@
1
+ import { CSSProperties, useEffect, useMemo, useRef, useState } from "react";
2
+ import { DamierCellInterface } from "../damierCell/damierCell.interface";
3
+ import { DamierPageInterface } from "../damierPage/damierPage.interface";
4
+ import { createDefaultDamierPage } from "../utils/createDefaultDamierPage";
5
+ import { DAMIER_DEFAULT_SIZE } from "../utils/damierDefaultSize";
6
+ import { getFirstFreePosition } from "../utils/getFirstFreePosition";
7
+ import { moveDamierCell } from "../utils/moveDamierCell";
8
+ import { DamierEditorCell } from "./damierEditorCell/DamierEditorCell";
9
+ import { DamierEditorTree } from "./damierEditorTree/DamierEditorTree";
10
+ import { DamierEditorToolbar } from "./damierEditorToolbar/DamierEditorToolbar";
11
+ import "./damierEditor.css";
12
+ import { MaybePromise } from "./maybePromise.interface";
13
+
14
+ export type DamierEditorProps = {
15
+ /** Pages du damier ; sans valeur, le composant démarre sur une page "Sommaire" vide */
16
+ pages?: DamierPageInterface[];
17
+ /** Page affichée (contrôlée) */
18
+ currentPageId?: number;
19
+ /** Masque le champ d'ajout de page de l'arborescence (ajout géré par le parent) */
20
+ hideAddPage?: boolean;
21
+ /** Racine de l'arborescence des pages (ex. sommaire), la première page par défaut */
22
+ rootPageId?: number;
23
+ /** Dimension de la page créée par défaut, 9 x 9 par défaut */
24
+ defaultCols?: number;
25
+ defaultRows?: number;
26
+ /** Dimension maximale autorisée (colonnes et lignes), 30 par défaut */
27
+ maxSize?: number;
28
+ className?: string;
29
+ onChangePage?: (page: DamierPageInterface) => void;
30
+ /** Retourner la page enregistrée (id définitif) pour remplacer l'id temporaire (négatif) */
31
+ onAddPage?: (
32
+ page: DamierPageInterface,
33
+ ) => MaybePromise<DamierPageInterface | void>;
34
+ /** Sans callback, la suppression de page n'est pas proposée */
35
+ onDeletePage?: (page: DamierPageInterface) => MaybePromise<void>;
36
+ onChangeDimension?: (
37
+ page: DamierPageInterface,
38
+ ) => MaybePromise<DamierPageInterface | void>;
39
+ /** Retourner la case enregistrée (id définitif) pour remplacer l'id temporaire (négatif) */
40
+ onAddCell?: (
41
+ cell: DamierCellInterface,
42
+ page: DamierPageInterface,
43
+ ) => MaybePromise<DamierCellInterface | void>;
44
+ /** Cases dont la position a changé (2 cases en cas d'échange) */
45
+ onMoveCells?: (
46
+ cells: DamierCellInterface[],
47
+ page: DamierPageInterface,
48
+ ) => MaybePromise<void>;
49
+ /** Sans callback, la suppression de case n'est pas proposée */
50
+ onDeleteCell?: (
51
+ cell: DamierCellInterface,
52
+ page: DamierPageInterface,
53
+ ) => MaybePromise<void>;
54
+ onClickCell?: (cell: DamierCellInterface, page: DamierPageInterface) => void;
55
+ };
56
+
57
+ export const DamierEditor = ({
58
+ pages: initialPages,
59
+ currentPageId,
60
+ rootPageId,
61
+ hideAddPage = false,
62
+ defaultCols = DAMIER_DEFAULT_SIZE,
63
+ defaultRows = DAMIER_DEFAULT_SIZE,
64
+ maxSize = 30,
65
+ className = "",
66
+ onChangePage,
67
+ onAddPage,
68
+ onDeletePage,
69
+ onChangeDimension,
70
+ onAddCell,
71
+ onMoveCells,
72
+ onDeleteCell,
73
+ onClickCell,
74
+ }: DamierEditorProps) => {
75
+ const [pages, setPages] = useState<DamierPageInterface[]>(() =>
76
+ initialPages?.length
77
+ ? initialPages
78
+ : [createDefaultDamierPage(defaultCols, defaultRows)],
79
+ );
80
+ const [activeId, setActiveId] = useState<number>(
81
+ currentPageId ?? rootPageId ?? pages[0].id,
82
+ );
83
+ const [dragFrom, setDragFrom] = useState<number>();
84
+ const [dragOver, setDragOver] = useState<number>();
85
+ const [busy, setBusy] = useState(false);
86
+ const [error, setError] = useState("");
87
+ // Ids temporaires négatifs pour les éléments pas encore enregistrés (-1 = page par défaut)
88
+ const tempId = useRef(-1);
89
+ const nextTempId = () => --tempId.current;
90
+ const propsRef = useRef(initialPages);
91
+ propsRef.current = initialPages;
92
+
93
+ useEffect(() => {
94
+ if (initialPages?.length) setPages(initialPages);
95
+ }, [initialPages]);
96
+ useEffect(() => {
97
+ if (currentPageId !== undefined) setActiveId(currentPageId);
98
+ }, [currentPageId]);
99
+
100
+ // Page active introuvable (pages rechargées, page par défaut remplacée...) : on revient à la racine
101
+ const page =
102
+ pages.find((item) => item.id === activeId) ??
103
+ pages.find((item) => item.id === rootPageId) ??
104
+ pages[0];
105
+ const freePosition = getFirstFreePosition(page);
106
+
107
+ const replacePage = (id: number, next: DamierPageInterface) => {
108
+ setPages((prev) => prev.map((item) => (item.id === id ? next : item)));
109
+ setActiveId((current) => (current === id ? next.id : current));
110
+ };
111
+
112
+ // Applique la modification localement, appelle l'API puis annule en cas d'échec
113
+ const run = async (
114
+ optimistic: DamierPageInterface[],
115
+ action: () => MaybePromise<void>,
116
+ ) => {
117
+ const previous = pages;
118
+ const propsAtStart = propsRef.current;
119
+ setPages(optimistic);
120
+ setBusy(true);
121
+ setError("");
122
+ try {
123
+ await action();
124
+ } catch (e) {
125
+ console.error("Damier : enregistrement impossible", e);
126
+ // Si le parent a rechargé les pages entre-temps, elles font foi
127
+ const reloaded = propsRef.current;
128
+ setPages(
129
+ reloaded?.length && reloaded !== propsAtStart ? reloaded : previous,
130
+ );
131
+ setError("La modification n'a pas pu être enregistrée.");
132
+ } finally {
133
+ setBusy(false);
134
+ }
135
+ };
136
+
137
+ // Une page temporaire (ex. page par défaut) est enregistrée avant d'y ajouter quoi que ce soit
138
+ const persistPage = async (
139
+ target: DamierPageInterface,
140
+ ): Promise<DamierPageInterface> => {
141
+ if (target.id >= 0 || !onAddPage) return target;
142
+ const saved = await onAddPage(target);
143
+ if (!saved) return target;
144
+ const next = { ...saved, cells: target.cells };
145
+ replacePage(target.id, next);
146
+ return next;
147
+ };
148
+
149
+ const selectPage = (next: DamierPageInterface) => {
150
+ setActiveId(next.id);
151
+ onChangePage?.(next);
152
+ };
153
+
154
+ const addPage = (title: string) => {
155
+ const created = createDefaultDamierPage(
156
+ page.cols,
157
+ page.rows,
158
+ nextTempId(),
159
+ title,
160
+ );
161
+ setActiveId(created.id);
162
+ run([...pages, created], async () => {
163
+ const saved = await onAddPage?.(created);
164
+ if (saved) replacePage(created.id, { ...saved, cells: [] });
165
+ });
166
+ };
167
+
168
+ const deletePage = (target: DamierPageInterface) => {
169
+ const remaining = pages.filter((item) => item.id !== target.id);
170
+ setActiveId(remaining[0].id);
171
+ run(remaining, async () => {
172
+ if (target.id >= 0) await onDeletePage?.(target);
173
+ });
174
+ };
175
+
176
+ const changeDimension = (cols: number, rows: number) => {
177
+ if (
178
+ !Number.isInteger(cols) ||
179
+ !Number.isInteger(rows) ||
180
+ cols < 1 ||
181
+ rows < 1 ||
182
+ cols > maxSize ||
183
+ rows > maxSize
184
+ ) {
185
+ setError(`La dimension doit être comprise entre 1 et ${maxSize}.`);
186
+ return;
187
+ }
188
+ const lastPosition = Math.max(0, ...page.cells.map((c) => c.position ?? 0));
189
+ if (cols * rows < lastPosition) {
190
+ setError(
191
+ "Impossible de réduire le damier : des cases sont placées au-delà de la nouvelle dimension.",
192
+ );
193
+ return;
194
+ }
195
+ const updated = { ...page, cols, rows };
196
+ run(
197
+ pages.map((item) => (item.id === page.id ? updated : item)),
198
+ async () => {
199
+ const persisted = await persistPage(updated);
200
+ if (persisted.id === updated.id) {
201
+ const saved = await onChangeDimension?.(updated);
202
+ if (saved)
203
+ replacePage(updated.id, { ...saved, cells: updated.cells });
204
+ }
205
+ },
206
+ );
207
+ };
208
+
209
+ const addCell = (position: number) => {
210
+ const cell: DamierCellInterface = {
211
+ id: nextTempId(),
212
+ position,
213
+ label: "Nouvelle case",
214
+ };
215
+ const updated = { ...page, cells: [...page.cells, cell] };
216
+ run(
217
+ pages.map((item) => (item.id === page.id ? updated : item)),
218
+ async () => {
219
+ const persisted = await persistPage(updated);
220
+ const saved = await onAddCell?.(cell, persisted);
221
+ if (!saved) return;
222
+ setPages((prev) =>
223
+ prev.map((item) =>
224
+ item.id !== persisted.id
225
+ ? item
226
+ : {
227
+ ...item,
228
+ cells: item.cells.map((c) => (c.id === cell.id ? saved : c)),
229
+ },
230
+ ),
231
+ );
232
+ },
233
+ );
234
+ };
235
+
236
+ const moveCell = (from: number, to: number) => {
237
+ setDragFrom(undefined);
238
+ setDragOver(undefined);
239
+ if (from === to) return;
240
+ const cells = moveDamierCell(page.cells, from, to);
241
+ const moved = cells.filter(
242
+ (cell, index) => cell.position !== page.cells[index].position,
243
+ );
244
+ if (!moved.length) return;
245
+ const updated = { ...page, cells };
246
+ run(
247
+ pages.map((item) => (item.id === page.id ? updated : item)),
248
+ async () => {
249
+ await onMoveCells?.(moved, updated);
250
+ },
251
+ );
252
+ };
253
+
254
+ const deleteCell = (cell: DamierCellInterface) => {
255
+ const updated = {
256
+ ...page,
257
+ cells: page.cells.filter((c) => c.position !== cell.position),
258
+ };
259
+ run(
260
+ pages.map((item) => (item.id === page.id ? updated : item)),
261
+ async () => {
262
+ if ((cell.id ?? -1) >= 0) await onDeleteCell?.(cell, updated);
263
+ },
264
+ );
265
+ };
266
+
267
+ const clickCell = (position: number, cell?: DamierCellInterface) => {
268
+ if (cell) onClickCell?.(cell, page);
269
+ else addCell(position);
270
+ };
271
+
272
+ const cellsByPosition = useMemo(
273
+ () => new Map(page.cells.map((cell) => [cell.position, cell])),
274
+ [page.cells],
275
+ );
276
+ const gridStyle = useMemo(
277
+ () =>
278
+ ({
279
+ "--damier-cols": String(page.cols),
280
+ "--damier-rows": String(page.rows),
281
+ }) as CSSProperties,
282
+ [page.cols, page.rows],
283
+ );
284
+
285
+ return (
286
+ <div className={`damier-editor ${className}`}>
287
+ <DamierEditorToolbar
288
+ page={page}
289
+ disabled={busy}
290
+ canAddCell={freePosition !== undefined}
291
+ onChangeDimension={changeDimension}
292
+ onAddCell={() => freePosition && addCell(freePosition)}
293
+ />
294
+ <div>
295
+ <DamierEditorTree
296
+ pages={pages}
297
+ currentPageId={page.id}
298
+ rootPageId={rootPageId}
299
+ disabled={busy}
300
+ onSelect={selectPage}
301
+ onAdd={hideAddPage ? undefined : addPage}
302
+ onDelete={onDeletePage ? deletePage : undefined}
303
+ />
304
+ <div className="damier-editor-main">
305
+ {error && (
306
+ <p role="alert" className="damier-editor-error">
307
+ {error}
308
+ </p>
309
+ )}
310
+ <div
311
+ className="damier-editor-grid"
312
+ style={gridStyle}
313
+ onDragLeave={(e) => {
314
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
315
+ setDragOver(undefined);
316
+ }
317
+ }}
318
+ >
319
+ {Array.from({ length: page.cols * page.rows }, (_, index) => {
320
+ const position = index + 1;
321
+ return (
322
+ <DamierEditorCell
323
+ key={position}
324
+ position={position}
325
+ cell={cellsByPosition.get(position)}
326
+ disabled={busy}
327
+ dragging={dragFrom === position}
328
+ dragOver={dragFrom !== undefined && dragOver === position}
329
+ onDragStart={setDragFrom}
330
+ onDragEnd={() => {
331
+ setDragFrom(undefined);
332
+ setDragOver(undefined);
333
+ }}
334
+ onDragOver={(over) => over !== dragOver && setDragOver(over)}
335
+ onDrop={moveCell}
336
+ onClick={clickCell}
337
+ onDelete={onDeleteCell ? deleteCell : undefined}
338
+ />
339
+ );
340
+ })}
341
+ </div>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ );
346
+ };