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
@@ -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
+ }
@@ -0,0 +1,91 @@
1
+ import { DragEvent } from "react";
2
+ import { Icon } from "../../../icon/Icon";
3
+ import { DamierCellInterface } from "../../damierCell/damierCell.interface";
4
+
5
+ interface DamierEditorCellProps {
6
+ position: number;
7
+ cell?: DamierCellInterface;
8
+ dragging?: boolean;
9
+ dragOver?: boolean;
10
+ disabled?: boolean;
11
+ onDragStart: (position: number) => void;
12
+ onDragEnd: () => void;
13
+ onDragOver: (position: number) => void;
14
+ onDrop: (from: number, to: number) => void;
15
+ onClick: (position: number, cell?: DamierCellInterface) => void;
16
+ onDelete?: (cell: DamierCellInterface) => void;
17
+ }
18
+
19
+ // Emplacement du damier en édition : cible de dépôt, et source de glisser si une case l'occupe
20
+ export const DamierEditorCell = ({
21
+ position,
22
+ cell,
23
+ dragging = false,
24
+ dragOver = false,
25
+ disabled = false,
26
+ onDragStart,
27
+ onDragEnd,
28
+ onDragOver,
29
+ onDrop,
30
+ onClick,
31
+ onDelete,
32
+ }: DamierEditorCellProps) => {
33
+ const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
34
+ e.dataTransfer.setData("text/plain", String(position));
35
+ e.dataTransfer.effectAllowed = "move";
36
+ onDragStart(position);
37
+ };
38
+ const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
39
+ if (disabled) return;
40
+ e.preventDefault();
41
+ e.dataTransfer.dropEffect = "move";
42
+ onDragOver(position);
43
+ };
44
+ const handleDrop = (e: DragEvent<HTMLDivElement>) => {
45
+ e.preventDefault();
46
+ const from = Number(e.dataTransfer.getData("text/plain"));
47
+ if (from) onDrop(from, position);
48
+ };
49
+ const classNames = [
50
+ "damier-editor-cell",
51
+ cell ? "" : "damier-editor-cell--empty",
52
+ dragging ? "damier-editor-cell--dragging" : "",
53
+ dragOver ? "damier-editor-cell--over" : "",
54
+ ].join(" ");
55
+
56
+ return (
57
+ <div
58
+ className={classNames}
59
+ draggable={!!cell && !disabled}
60
+ title={cell ? cell.label : "Ajouter une case"}
61
+ onDragStart={handleDragStart}
62
+ onDragEnd={onDragEnd}
63
+ onDragOver={handleDragOver}
64
+ onDrop={handleDrop}
65
+ onClick={() => !disabled && onClick(position, cell)}
66
+ >
67
+ {cell ? (
68
+ <>
69
+ <div className="damier-editor-cell-content">
70
+ {cell.child ?? <span>{cell.label}</span>}
71
+ </div>
72
+ {onDelete && !disabled && (
73
+ <button
74
+ type="button"
75
+ className="damier-editor-cell-delete"
76
+ aria-label="Supprimer la case"
77
+ onClick={(e) => {
78
+ e.stopPropagation();
79
+ onDelete(cell);
80
+ }}
81
+ >
82
+ <Icon classIcon="bi-x-lg" />
83
+ </button>
84
+ )}
85
+ </>
86
+ ) : (
87
+ <Icon classIcon="bi-plus-lg damier-editor-cell-add" />
88
+ )}
89
+ </div>
90
+ );
91
+ };
@@ -0,0 +1,71 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Button } from "../../../button/Button";
3
+ import { TypeInputEnum } from "../../../form/form.enum";
4
+ import { Input } from "../../../form/input/Input";
5
+ import { DamierPageInterface } from "../../damierPage/damierPage.interface";
6
+
7
+ interface DamierEditorToolbarProps {
8
+ page: DamierPageInterface;
9
+ disabled?: boolean;
10
+ canAddCell: boolean;
11
+ onChangeDimension: (cols: number, rows: number) => void;
12
+ onAddCell: () => void;
13
+ }
14
+
15
+ // Dimension de la page courante (colonnes x lignes) et ajout rapide d'une case
16
+ export const DamierEditorToolbar = ({
17
+ page,
18
+ disabled = false,
19
+ canAddCell,
20
+ onChangeDimension,
21
+ onAddCell,
22
+ }: DamierEditorToolbarProps) => {
23
+ const [cols, setCols] = useState(page.cols);
24
+ const [rows, setRows] = useState(page.rows);
25
+ useEffect(() => {
26
+ setCols(page.cols);
27
+ setRows(page.rows);
28
+ }, [page.id, page.cols, page.rows]);
29
+ const changed = cols !== page.cols || rows !== page.rows;
30
+
31
+ return (
32
+ <div className="damier-editor-toolbar">
33
+ <label className="damier-editor-dimension">
34
+ Colonnes
35
+ <Input
36
+ id="damier-editor-cols"
37
+ name="damier-editor-cols"
38
+ type={TypeInputEnum.NUMBER}
39
+ value={cols}
40
+ disabled={disabled}
41
+ disabledDebounce
42
+ onChange={(e) => setCols(Number(e.target.value))}
43
+ />
44
+ </label>
45
+ <label className="damier-editor-dimension">
46
+ Lignes
47
+ <Input
48
+ id="damier-editor-rows"
49
+ name="damier-editor-rows"
50
+ type={TypeInputEnum.NUMBER}
51
+ value={rows}
52
+ disabled={disabled}
53
+ disabledDebounce
54
+ onChange={(e) => setRows(Number(e.target.value))}
55
+ />
56
+ </label>
57
+ <Button
58
+ title="Appliquer la dimension"
59
+ classIcon="bi-aspect-ratio"
60
+ disabled={disabled || !changed}
61
+ onClick={() => onChangeDimension(cols, rows)}
62
+ />
63
+ <Button
64
+ title="Ajouter une case"
65
+ classIcon="bi-plus-square"
66
+ disabled={disabled || !canAddCell}
67
+ onClick={onAddCell}
68
+ />
69
+ </div>
70
+ );
71
+ };
@@ -0,0 +1,134 @@
1
+ import { useMemo, useState } from "react";
2
+ import { Button } from "../../../button/Button";
3
+ import { Input } from "../../../form/input/Input";
4
+ import { DamierPageInterface } from "../../damierPage/damierPage.interface";
5
+ import { buildDamierTree } from "../../utils/buildDamierTree";
6
+ import { findDamierTreePath } from "../../utils/findDamierTreePath";
7
+ import { DamierEditorTreeNode } from "./damierEditorTreeNode/DamierEditorTreeNode";
8
+
9
+ interface DamierEditorTreeProps {
10
+ pages: DamierPageInterface[];
11
+ currentPageId: number;
12
+ rootPageId?: number;
13
+ disabled?: boolean;
14
+ onSelect: (page: DamierPageInterface) => void;
15
+ /** Sans callback, le champ d'ajout de page est masqué */
16
+ onAdd?: (title: string) => void;
17
+ onDelete?: (page: DamierPageInterface) => void;
18
+ }
19
+
20
+ // Arborescence des pages (à gauche du damier) + recherche, ajout et suppression de page
21
+ export const DamierEditorTree = ({
22
+ pages,
23
+ currentPageId,
24
+ rootPageId,
25
+ disabled = false,
26
+ onSelect,
27
+ onAdd,
28
+ onDelete,
29
+ }: DamierEditorTreeProps) => {
30
+ const [title, setTitle] = useState("");
31
+ const [search, setSearch] = useState("");
32
+ const tree = useMemo(
33
+ () => buildDamierTree(pages, rootPageId),
34
+ [pages, rootPageId],
35
+ );
36
+ const activePath = useMemo(
37
+ () => findDamierTreePath(tree, currentPageId),
38
+ [tree, currentPageId],
39
+ );
40
+ const term = search.trim().toLocaleLowerCase();
41
+ const results = term
42
+ ? pages.filter((page) => page.title.toLocaleLowerCase().includes(term))
43
+ : [];
44
+ const currentPage = pages.find((page) => page.id === currentPageId);
45
+ const add = () => {
46
+ if (!title.trim() || !onAdd) return;
47
+ onAdd(title.trim());
48
+ setTitle("");
49
+ };
50
+
51
+ return (
52
+ <nav className="damier-tree" aria-label="Pages du damier">
53
+ <div className="damier-tree-actions">
54
+ {onAdd && (
55
+ <>
56
+ <Input
57
+ id="damier-editor-page-title"
58
+ name="damier-editor-page-title"
59
+ placeholder="Nouvelle page"
60
+ value={title}
61
+ disabled={disabled}
62
+ disabledDebounce
63
+ onChange={(e) => setTitle(e.target.value)}
64
+ handleKeyDown={(e) => e.key === "Enter" && add()}
65
+ />
66
+ <Button
67
+ title=""
68
+ classIcon="bi-plus-lg"
69
+ disabled={disabled || !title.trim()}
70
+ onClick={add}
71
+ />
72
+ </>
73
+ )}
74
+ {onDelete && currentPage && pages.length > 1 && (
75
+ <Button
76
+ title=""
77
+ classIcon="bi-trash"
78
+ className="damier-editor-danger"
79
+ disabled={disabled}
80
+ onClick={() => onDelete(currentPage)}
81
+ />
82
+ )}
83
+ </div>
84
+ <Input
85
+ id="damier-editor-page-search"
86
+ name="damier-editor-page-search"
87
+ placeholder="Rechercher une page"
88
+ value={search}
89
+ disabledDebounce
90
+ showClearButton
91
+ onChange={(e) => setSearch(e.target.value)}
92
+ />
93
+ <div className="damier-tree-list">
94
+ {term ? (
95
+ <ul>
96
+ {results.map((page) => (
97
+ <li key={page.id}>
98
+ <div
99
+ className={`damier-tree-item ${
100
+ page.id === currentPageId ? "damier-tree-item--active" : ""
101
+ }`}
102
+ >
103
+ <button
104
+ type="button"
105
+ className="damier-tree-label"
106
+ title={page.title}
107
+ onClick={() => onSelect(page)}
108
+ >
109
+ {page.title}
110
+ </button>
111
+ </div>
112
+ </li>
113
+ ))}
114
+ {!results.length && (
115
+ <li className="damier-tree-empty">Aucune page</li>
116
+ )}
117
+ </ul>
118
+ ) : (
119
+ <ul role="tree">
120
+ {tree.map((node) => (
121
+ <DamierEditorTreeNode
122
+ key={node.page.id}
123
+ node={node}
124
+ currentPageId={currentPageId}
125
+ activePath={activePath}
126
+ onSelect={onSelect}
127
+ />
128
+ ))}
129
+ </ul>
130
+ )}
131
+ </div>
132
+ </nav>
133
+ );
134
+ };
@@ -0,0 +1,78 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Icon } from "../../../../icon/Icon";
3
+ import { DamierPageInterface } from "../../../damierPage/damierPage.interface";
4
+ import { DamierTreeNodeInterface } from "../damierTreeNode.interface";
5
+
6
+ interface DamierEditorTreeNodeProps {
7
+ node: DamierTreeNodeInterface;
8
+ currentPageId: number;
9
+ activePath: number[];
10
+ depth?: number;
11
+ onSelect: (page: DamierPageInterface) => void;
12
+ }
13
+
14
+ export const DamierEditorTreeNode = ({
15
+ node,
16
+ currentPageId,
17
+ activePath,
18
+ depth = 0,
19
+ onSelect,
20
+ }: DamierEditorTreeNodeProps) => {
21
+ const inActivePath = activePath.includes(node.page.id);
22
+ const [expanded, setExpanded] = useState(depth === 0 || inActivePath);
23
+ // On déplie automatiquement le chemin vers la page sélectionnée
24
+ useEffect(() => {
25
+ if (inActivePath) setExpanded(true);
26
+ }, [inActivePath]);
27
+ const hasChildren = node.children.length > 0;
28
+ const active = node.page.id === currentPageId;
29
+
30
+ return (
31
+ <li
32
+ role="treeitem"
33
+ aria-expanded={hasChildren ? expanded : undefined}
34
+ aria-selected={active}
35
+ >
36
+ <div
37
+ className={`damier-tree-item ${active ? "damier-tree-item--active" : ""}`}
38
+ style={{ paddingLeft: `${depth * 0.9}rem` }}
39
+ >
40
+ <button
41
+ type="button"
42
+ className="damier-tree-toggle"
43
+ aria-label={expanded ? "Replier" : "Déplier"}
44
+ disabled={!hasChildren}
45
+ onClick={() => setExpanded(!expanded)}
46
+ >
47
+ {hasChildren && (
48
+ <Icon
49
+ classIcon={expanded ? "bi-chevron-down" : "bi-chevron-right"}
50
+ />
51
+ )}
52
+ </button>
53
+ <button
54
+ type="button"
55
+ className="damier-tree-label"
56
+ title={node.page.title}
57
+ onClick={() => onSelect(node.page)}
58
+ >
59
+ {node.page.title}
60
+ </button>
61
+ </div>
62
+ {hasChildren && expanded && (
63
+ <ul role="group">
64
+ {node.children.map((child) => (
65
+ <DamierEditorTreeNode
66
+ key={child.page.id}
67
+ node={child}
68
+ currentPageId={currentPageId}
69
+ activePath={activePath}
70
+ depth={depth + 1}
71
+ onSelect={onSelect}
72
+ />
73
+ ))}
74
+ </ul>
75
+ )}
76
+ </li>
77
+ );
78
+ };
@@ -0,0 +1,6 @@
1
+ import { DamierPageInterface } from "../../damierPage/damierPage.interface";
2
+
3
+ export interface DamierTreeNodeInterface {
4
+ page: DamierPageInterface;
5
+ children: DamierTreeNodeInterface[];
6
+ }
@@ -0,0 +1 @@
1
+ export type MaybePromise<T> = T | Promise<T>;
@@ -0,0 +1,9 @@
1
+ import { DamierCellInterface } from "../damierCell/damierCell.interface";
2
+
3
+ export interface DamierPageInterface {
4
+ id: number;
5
+ title: string;
6
+ cols: number;
7
+ rows: number;
8
+ cells: DamierCellInterface[];
9
+ }
@@ -0,0 +1,31 @@
1
+ import { DamierTreeNodeInterface } from "../damierEditor/damierEditorTree/damierTreeNode.interface";
2
+ import { DamierPageInterface } from "../damierPage/damierPage.interface";
3
+
4
+ // Arborescence des pages à partir des liens des cases (ordre des positions).
5
+ // Chaque page n'apparaît qu'une fois (premier chemin trouvé, pas de cycle) ;
6
+ // les pages non atteignables depuis la racine deviennent des racines à leur tour.
7
+ export const buildDamierTree = (
8
+ pages: DamierPageInterface[],
9
+ rootPageId?: number,
10
+ ): DamierTreeNodeInterface[] => {
11
+ const byId = new Map(pages.map((page) => [page.id, page]));
12
+ const visited = new Set<number>();
13
+ const build = (page: DamierPageInterface): DamierTreeNodeInterface => {
14
+ visited.add(page.id);
15
+ const children: DamierPageInterface[] = [];
16
+ [...page.cells]
17
+ .sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
18
+ .forEach((cell) => {
19
+ const child = cell.linkPageId ? byId.get(cell.linkPageId) : undefined;
20
+ if (child && !visited.has(child.id)) {
21
+ visited.add(child.id);
22
+ children.push(child);
23
+ }
24
+ });
25
+ return { page, children: children.map(build) };
26
+ };
27
+ const root =
28
+ (rootPageId !== undefined ? byId.get(rootPageId) : undefined) ?? pages[0];
29
+ const ordered = root ? [root, ...pages.filter((p) => p !== root)] : pages;
30
+ return ordered.flatMap((page) => (visited.has(page.id) ? [] : [build(page)]));
31
+ };
@@ -0,0 +1,10 @@
1
+ import { DamierPageInterface } from "../damierPage/damierPage.interface";
2
+ import { DAMIER_DEFAULT_SIZE } from "./damierDefaultSize";
3
+
4
+ // Page initiale utilisée quand aucun damier n'est fourni au composant
5
+ export const createDefaultDamierPage = (
6
+ cols = DAMIER_DEFAULT_SIZE,
7
+ rows = DAMIER_DEFAULT_SIZE,
8
+ id = -1,
9
+ title = "Sommaire",
10
+ ): DamierPageInterface => ({ id, title, cols, rows, cells: [] });
@@ -0,0 +1 @@
1
+ export const DAMIER_DEFAULT_SIZE = 9;