kelt-ui-kit-react 2.1.8 → 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.
- package/dist/damier/damierCell/damierCell.interface.d.ts +3 -0
- package/dist/damier/damierEditor/DamierEditor.d.ts +33 -0
- package/dist/damier/damierEditor/damierEditorCell/DamierEditorCell.d.ts +16 -0
- package/dist/damier/damierEditor/damierEditorToolbar/DamierEditorToolbar.d.ts +10 -0
- package/dist/damier/damierEditor/damierEditorTree/DamierEditorTree.d.ts +13 -0
- package/dist/damier/damierEditor/damierEditorTree/damierEditorTreeNode/DamierEditorTreeNode.d.ts +11 -0
- package/dist/damier/damierEditor/damierEditorTree/damierTreeNode.interface.d.ts +5 -0
- package/dist/damier/damierEditor/maybePromise.interface.d.ts +1 -0
- package/dist/damier/damierPage/damierPage.interface.d.ts +8 -0
- package/dist/damier/utils/buildDamierTree.d.ts +3 -0
- package/dist/damier/utils/createDefaultDamierPage.d.ts +2 -0
- package/dist/damier/utils/damierDefaultSize.d.ts +1 -0
- package/dist/damier/utils/findDamierTreePath.d.ts +2 -0
- package/dist/damier/utils/getFirstFreePosition.d.ts +2 -0
- package/dist/damier/utils/moveDamierCell.d.ts +2 -0
- package/dist/form/form.interface.d.ts +2 -0
- package/dist/form/getFieldError.d.ts +2 -0
- package/dist/index.js +881 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/damier/Damier.tsx +1 -1
- package/src/damier/Damier.view.tsx +46 -0
- package/src/damier/damierCell/DamierCell.tsx +1 -1
- package/src/damier/damierCell/damierCell.interface.tsx +3 -0
- package/src/damier/damierEditor/DamierEditor.tsx +346 -0
- package/src/damier/damierEditor/damierEditor.css +255 -0
- package/src/damier/damierEditor/damierEditorCell/DamierEditorCell.tsx +91 -0
- package/src/damier/damierEditor/damierEditorToolbar/DamierEditorToolbar.tsx +71 -0
- package/src/damier/damierEditor/damierEditorTree/DamierEditorTree.tsx +134 -0
- package/src/damier/damierEditor/damierEditorTree/damierEditorTreeNode/DamierEditorTreeNode.tsx +78 -0
- package/src/damier/damierEditor/damierEditorTree/damierTreeNode.interface.ts +6 -0
- package/src/damier/damierEditor/maybePromise.interface.ts +1 -0
- package/src/damier/damierPage/damierPage.interface.tsx +9 -0
- package/src/damier/utils/buildDamierTree.ts +31 -0
- package/src/damier/utils/createDefaultDamierPage.ts +10 -0
- package/src/damier/utils/damierDefaultSize.ts +1 -0
- package/src/damier/utils/findDamierTreePath.ts +14 -0
- package/src/damier/utils/getFirstFreePosition.ts +12 -0
- package/src/damier/utils/moveDamierCell.ts +13 -0
- package/src/dataTable/DataTable.tsx +2 -2
- package/src/docs/catalog.ts +1 -1
- package/src/form/Form.tsx +68 -8
- package/src/form/form.css +9 -0
- package/src/form/form.interface.tsx +2 -0
- package/src/form/getFieldError.ts +14 -0
- package/src/select/select.css +11 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
package/src/damier/Damier.tsx
CHANGED
|
@@ -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
|
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/* La hauteur est fixée par la grille (taille de case bornée) ; l'arbre défile dans cette hauteur.
|
|
2
|
+
--damier-editor-offset : hauteur occupée autour du damier (en-têtes, barre d'outils...) */
|
|
3
|
+
.damier-editor {
|
|
4
|
+
display: flex;
|
|
5
|
+
gap: 1rem;
|
|
6
|
+
width: 100%;
|
|
7
|
+
|
|
8
|
+
.damier-editor-main {
|
|
9
|
+
flex: 1;
|
|
10
|
+
min-width: 0;
|
|
11
|
+
min-height: 0;
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: 0.75rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.damier-editor-title {
|
|
18
|
+
margin: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.damier-editor-toolbar {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-wrap: wrap;
|
|
24
|
+
align-items: flex-end;
|
|
25
|
+
gap: 0.5rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Arborescence des pages */
|
|
29
|
+
.damier-tree {
|
|
30
|
+
flex: 0 0 260px;
|
|
31
|
+
width: 260px;
|
|
32
|
+
min-width: 0;
|
|
33
|
+
min-height: 0;
|
|
34
|
+
/* Le contenu de l'arbre (potentiellement des milliers de pages) n'agrandit pas le damier */
|
|
35
|
+
contain: size;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: 0.5rem;
|
|
39
|
+
padding-right: 0.75rem;
|
|
40
|
+
border-right: 1px solid #d1c1c1;
|
|
41
|
+
|
|
42
|
+
ul {
|
|
43
|
+
list-style: none;
|
|
44
|
+
margin: 0;
|
|
45
|
+
padding: 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.damier-tree-actions {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 0.25rem;
|
|
53
|
+
|
|
54
|
+
.input-container {
|
|
55
|
+
flex: 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.damier-tree-list {
|
|
60
|
+
flex: 1;
|
|
61
|
+
min-height: 0;
|
|
62
|
+
overflow-y: auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Neutralise le style global des <button> (largeur mini, centrage, taille) */
|
|
66
|
+
.damier-tree-toggle,
|
|
67
|
+
.damier-tree-label,
|
|
68
|
+
.damier-editor-cell-delete {
|
|
69
|
+
min-width: 0;
|
|
70
|
+
max-height: none;
|
|
71
|
+
border-radius: 0;
|
|
72
|
+
font-size: 0.95rem;
|
|
73
|
+
font-weight: normal;
|
|
74
|
+
justify-content: flex-start;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.damier-tree-item {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
border-radius: 4px;
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
background: var(--light-color);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&.damier-tree-item--active,
|
|
87
|
+
&.damier-tree-item--active:hover {
|
|
88
|
+
background: var(--primary-color);
|
|
89
|
+
|
|
90
|
+
.damier-tree-label,
|
|
91
|
+
.damier-tree-toggle {
|
|
92
|
+
color: var(--white-color);
|
|
93
|
+
font-weight: bold;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.damier-tree-toggle {
|
|
99
|
+
flex: 0 0 1.5rem;
|
|
100
|
+
width: 1.5rem;
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
padding: 0.3rem 0;
|
|
105
|
+
border: 0;
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: inherit;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
|
|
110
|
+
&:disabled {
|
|
111
|
+
cursor: default;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.damier-tree-label {
|
|
116
|
+
flex: 1;
|
|
117
|
+
min-width: 0;
|
|
118
|
+
padding: 0.3rem 0.25rem;
|
|
119
|
+
border: 0;
|
|
120
|
+
background: transparent;
|
|
121
|
+
color: #1e293b;
|
|
122
|
+
text-align: left;
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
text-overflow: ellipsis;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.damier-tree-empty {
|
|
130
|
+
padding: 0.3rem;
|
|
131
|
+
opacity: 0.6;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.damier-editor-dimension {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
font-size: 0.85rem;
|
|
138
|
+
width: 6rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.damier-editor-danger {
|
|
142
|
+
background-color: var(--danger-color);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.damier-editor-error {
|
|
146
|
+
margin: 0;
|
|
147
|
+
color: var(--danger-color);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Hauteur de ligne bornée : le damier tient dans l'écran, sans dépasser 6rem par case */
|
|
151
|
+
.damier-editor-grid {
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-template-columns: repeat(var(--damier-cols, 9), minmax(0, 1fr));
|
|
154
|
+
grid-template-rows: repeat(
|
|
155
|
+
var(--damier-rows, 9),
|
|
156
|
+
clamp(
|
|
157
|
+
2.5rem,
|
|
158
|
+
calc(
|
|
159
|
+
(100vh - var(--damier-editor-offset, 16rem)) / var(--damier-rows, 9)
|
|
160
|
+
),
|
|
161
|
+
6rem
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
gap: 0.3rem;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.damier-editor-cell {
|
|
168
|
+
position: relative;
|
|
169
|
+
box-sizing: border-box;
|
|
170
|
+
min-height: 0;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
justify-content: center;
|
|
175
|
+
border: 1px solid #d1c1c1;
|
|
176
|
+
border-radius: 4px;
|
|
177
|
+
background-color: var(--white-color);
|
|
178
|
+
cursor: grab;
|
|
179
|
+
user-select: none;
|
|
180
|
+
transition: transform 0.1s, box-shadow 0.1s;
|
|
181
|
+
|
|
182
|
+
&.damier-editor-cell--empty {
|
|
183
|
+
border-style: dashed;
|
|
184
|
+
background: #f9f9f9;
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
|
|
187
|
+
.damier-editor-cell-add {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
color: var(--primary-color);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
&:hover .damier-editor-cell-add {
|
|
193
|
+
opacity: 0.6;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
&.damier-editor-cell--dragging {
|
|
198
|
+
opacity: 0.4;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
&.damier-editor-cell--over {
|
|
202
|
+
border: 2px solid var(--tertiary-color);
|
|
203
|
+
box-shadow: 0 0 0 3px rgba(48, 184, 194, 0.25);
|
|
204
|
+
transform: scale(1.03);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
&:hover .damier-editor-cell-delete {
|
|
208
|
+
opacity: 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.damier-editor-cell-content {
|
|
213
|
+
width: 100%;
|
|
214
|
+
height: 100%;
|
|
215
|
+
display: flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: center;
|
|
218
|
+
text-align: center;
|
|
219
|
+
font-size: 0.85rem;
|
|
220
|
+
/* Les images/enfants ne doivent pas capter le glisser natif */
|
|
221
|
+
pointer-events: none;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.damier-editor-cell-delete {
|
|
225
|
+
position: absolute;
|
|
226
|
+
justify-content: center;
|
|
227
|
+
top: 2px;
|
|
228
|
+
right: 2px;
|
|
229
|
+
z-index: 9;
|
|
230
|
+
display: flex;
|
|
231
|
+
padding: 0.15rem;
|
|
232
|
+
border: 0;
|
|
233
|
+
border-radius: 50%;
|
|
234
|
+
background: var(--white-color);
|
|
235
|
+
color: var(--danger-color);
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
opacity: 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@media (max-width: 767px) {
|
|
242
|
+
.damier-editor {
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
|
|
245
|
+
.damier-tree {
|
|
246
|
+
contain: none;
|
|
247
|
+
flex-basis: auto;
|
|
248
|
+
width: 100%;
|
|
249
|
+
max-height: 40vh;
|
|
250
|
+
padding-right: 0;
|
|
251
|
+
border-right: 0;
|
|
252
|
+
border-bottom: 1px solid #d1c1c1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|