@wakastellar/ui 3.3.3 → 3.5.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.
Files changed (140) hide show
  1. package/dist/badge-BbwO7QeZ.js +1 -0
  2. package/dist/badge-BfiocODp.mjs +23 -0
  3. package/dist/charts.cjs.js +1 -1
  4. package/dist/charts.es.js +1 -1
  5. package/dist/chunk-14q5BKub.js +1 -0
  6. package/dist/{chunk-BH6uBOac.mjs → chunk-Cr9pTUWm.mjs} +5 -5
  7. package/dist/cn-DEtaFQsA.js +1 -0
  8. package/dist/cn-DUn6aSIQ.mjs +24 -0
  9. package/dist/doc.cjs.js +2 -2
  10. package/dist/doc.es.js +19 -19
  11. package/dist/editor.cjs.js +48 -0
  12. package/dist/editor.d.ts +1 -0
  13. package/dist/editor.es.js +6551 -0
  14. package/dist/{exceljs.min-DG9M8IZ1.mjs → exceljs.min-DL1XYDll.mjs} +1 -1
  15. package/dist/{exceljs.min-BuefmDRS.js → exceljs.min-qeIfSCbF.js} +1 -1
  16. package/dist/export.cjs.js +1 -1
  17. package/dist/export.es.js +1 -1
  18. package/dist/index.cjs.js +150 -150
  19. package/dist/index.es.js +26782 -27591
  20. package/dist/input-BfaSAGVw.js +1 -0
  21. package/dist/input-DVr_Qkl8.mjs +14 -0
  22. package/dist/rich-text.cjs.js +1 -1
  23. package/dist/rich-text.es.js +1 -1
  24. package/dist/security-CyBpuklN.mjs +122 -0
  25. package/dist/security-bFWwDrlg.js +1 -0
  26. package/dist/separator-NrkltulH.js +1 -0
  27. package/dist/separator-ibN2mycs.mjs +51 -0
  28. package/dist/src/components/editor/blocks/index.d.ts +51 -0
  29. package/dist/src/components/editor/blocks/waka-acceptance-criteria-block.d.ts +60 -0
  30. package/dist/src/components/editor/blocks/waka-ai-assist-block.d.ts +58 -0
  31. package/dist/src/components/editor/blocks/waka-api-endpoint-block.d.ts +63 -0
  32. package/dist/src/components/editor/blocks/waka-code-playground-block.d.ts +61 -0
  33. package/dist/src/components/editor/blocks/waka-comment-thread-block.d.ts +85 -0
  34. package/dist/src/components/editor/blocks/waka-diagram-block.d.ts +52 -0
  35. package/dist/src/components/editor/blocks/waka-embed-block.d.ts +58 -0
  36. package/dist/src/components/editor/blocks/waka-slash-menu-block.d.ts +67 -0
  37. package/dist/src/components/editor/blocks/waka-user-story-block.d.ts +79 -0
  38. package/dist/src/components/editor/blocks/waka-version-diff-block.d.ts +73 -0
  39. package/dist/src/components/editor/index.d.ts +66 -0
  40. package/dist/src/components/editor/waka-ai-writer.d.ts +80 -0
  41. package/dist/src/components/editor/waka-collaborative-editor.d.ts +93 -0
  42. package/dist/src/components/editor/waka-diff-viewer.d.ts +71 -0
  43. package/dist/src/components/editor/waka-dnd-editor.d.ts +64 -0
  44. package/dist/src/components/editor/waka-document-editor.d.ts +92 -0
  45. package/dist/src/components/editor/waka-editor-elements.d.ts +79 -0
  46. package/dist/src/components/editor/waka-editor-leaves.d.ts +39 -0
  47. package/dist/src/components/editor/waka-editor-plugins.d.ts +41 -0
  48. package/dist/src/components/editor/waka-editor-toolbar.d.ts +20 -0
  49. package/dist/src/components/editor/waka-editor.d.ts +59 -0
  50. package/dist/src/components/editor/waka-floating-toolbar.d.ts +47 -0
  51. package/dist/src/components/editor/waka-markdown-editor.d.ts +60 -0
  52. package/dist/src/components/editor/waka-mention-editor.d.ts +125 -0
  53. package/dist/src/components/editor/waka-slash-menu.d.ts +70 -0
  54. package/dist/src/components/editor/waka-spec-editor.d.ts +88 -0
  55. package/dist/src/components/index.d.ts +1 -15
  56. package/dist/src/editor.d.ts +26 -0
  57. package/dist/textarea-CdQWggYG.js +1 -0
  58. package/dist/textarea-DJDXJ3nd.mjs +23 -0
  59. package/dist/types-C2St0wOW.js +1 -0
  60. package/dist/{types-B6GVaSIP.mjs → types-JnqoLyuv.mjs} +214 -211
  61. package/dist/{useDataTableImport-BPvfo--2.mjs → useDataTableImport-BWUFesPi.mjs} +3 -3
  62. package/dist/{useDataTableImport-Cm_pCKnO.js → useDataTableImport-T7ddpN5k.js} +3 -3
  63. package/dist/waka-doc-renderer-CTxC7Trf.js +3 -0
  64. package/dist/{waka-doc-renderer-BkIvas3z.mjs → waka-doc-renderer-Cw-Xnyen.mjs} +264 -281
  65. package/dist/waka-editor-plugins-DR6tpsUC.mjs +135 -0
  66. package/dist/waka-editor-plugins-sGSh9hn2.js +1 -0
  67. package/dist/waka-rich-text-editor-BlIdtknG.js +1 -0
  68. package/dist/waka-rich-text-editor-D1uA3zbB.js +1 -0
  69. package/dist/waka-rich-text-editor-DgSWiXMW.mjs +342 -0
  70. package/dist/waka-rich-text-editor-DndVJuDw.mjs +2 -0
  71. package/package.json +87 -2
  72. package/src/blocks/footer/index.tsx +1 -6
  73. package/src/blocks/login/index.tsx +1 -7
  74. package/src/blocks/profile/index.tsx +3 -5
  75. package/src/components/editor/blocks/index.ts +182 -0
  76. package/src/components/editor/blocks/waka-acceptance-criteria-block.tsx +326 -0
  77. package/src/components/editor/blocks/waka-ai-assist-block.tsx +284 -0
  78. package/src/components/editor/blocks/waka-api-endpoint-block.tsx +382 -0
  79. package/src/components/editor/blocks/waka-code-playground-block.tsx +331 -0
  80. package/src/components/editor/blocks/waka-comment-thread-block.tsx +448 -0
  81. package/src/components/editor/blocks/waka-diagram-block.tsx +293 -0
  82. package/src/components/editor/blocks/waka-embed-block.tsx +416 -0
  83. package/src/components/editor/blocks/waka-slash-menu-block.tsx +432 -0
  84. package/src/components/editor/blocks/waka-user-story-block.tsx +295 -0
  85. package/src/components/editor/blocks/waka-version-diff-block.tsx +426 -0
  86. package/src/components/editor/index.ts +279 -0
  87. package/src/components/editor/waka-ai-writer.tsx +434 -0
  88. package/src/components/editor/waka-collaborative-editor.tsx +426 -0
  89. package/src/components/editor/waka-diff-viewer.tsx +352 -0
  90. package/src/components/editor/waka-dnd-editor.tsx +284 -0
  91. package/src/components/editor/waka-document-editor.tsx +502 -0
  92. package/src/components/editor/waka-editor-elements.tsx +312 -0
  93. package/src/components/editor/waka-editor-leaves.tsx +101 -0
  94. package/src/components/editor/waka-editor-plugins.ts +207 -0
  95. package/src/components/editor/waka-editor-toolbar.tsx +358 -0
  96. package/src/components/editor/waka-editor.tsx +431 -0
  97. package/src/components/editor/waka-floating-toolbar.tsx +268 -0
  98. package/src/components/editor/waka-markdown-editor.tsx +395 -0
  99. package/src/components/editor/waka-mention-editor.tsx +459 -0
  100. package/src/components/editor/waka-slash-menu.tsx +392 -0
  101. package/src/components/editor/waka-spec-editor.tsx +657 -0
  102. package/src/components/index.ts +1 -18
  103. package/dist/chunk-BDDJmn7V.js +0 -1
  104. package/dist/cn-DnPbmOCy.js +0 -1
  105. package/dist/cn-DpLcAzrf.mjs +0 -22
  106. package/dist/separator-BDReXBvI.mjs +0 -59
  107. package/dist/separator-BKjNl9sI.js +0 -1
  108. package/dist/src/components/waka-actor-badge/index.d.ts +0 -8
  109. package/dist/src/components/waka-actors-list/index.d.ts +0 -18
  110. package/dist/src/components/waka-ai-assistant-button/index.d.ts +0 -8
  111. package/dist/src/components/waka-document-flyover/index.d.ts +0 -10
  112. package/dist/src/components/waka-document-preview-popup/index.d.ts +0 -26
  113. package/dist/src/components/waka-hour-balance-badge/index.d.ts +0 -8
  114. package/dist/src/components/waka-hour-consumption-table/index.d.ts +0 -15
  115. package/dist/src/components/waka-hour-pack-dialog/index.d.ts +0 -8
  116. package/dist/src/components/waka-project-stats-header/index.d.ts +0 -15
  117. package/dist/src/components/waka-step-comment-bubble/index.d.ts +0 -13
  118. package/dist/src/components/waka-step-comment-panel/index.d.ts +0 -20
  119. package/dist/src/components/waka-step-permission-matrix/index.d.ts +0 -12
  120. package/dist/src/components/waka-time-entry-dialog/index.d.ts +0 -16
  121. package/dist/src/components/waka-time-tracking-flyover/index.d.ts +0 -11
  122. package/dist/types-BH9cQRqZ.js +0 -1
  123. package/dist/waka-doc-renderer-BZ2-SqyT.js +0 -3
  124. package/dist/waka-rich-text-editor-BJGlQgpq.js +0 -1
  125. package/dist/waka-rich-text-editor-BJzzxeP1.mjs +0 -361
  126. package/dist/waka-rich-text-editor-wnXLwvUo.js +0 -1
  127. package/src/components/waka-actor-badge/index.tsx +0 -34
  128. package/src/components/waka-actors-list/index.tsx +0 -125
  129. package/src/components/waka-ai-assistant-button/index.tsx +0 -31
  130. package/src/components/waka-document-flyover/index.tsx +0 -36
  131. package/src/components/waka-document-preview-popup/index.tsx +0 -103
  132. package/src/components/waka-hour-balance-badge/index.tsx +0 -43
  133. package/src/components/waka-hour-consumption-table/index.tsx +0 -72
  134. package/src/components/waka-hour-pack-dialog/index.tsx +0 -72
  135. package/src/components/waka-project-stats-header/index.tsx +0 -69
  136. package/src/components/waka-step-comment-bubble/index.tsx +0 -71
  137. package/src/components/waka-step-comment-panel/index.tsx +0 -106
  138. package/src/components/waka-step-permission-matrix/index.tsx +0 -65
  139. package/src/components/waka-time-entry-dialog/index.tsx +0 -131
  140. package/src/components/waka-time-tracking-flyover/index.tsx +0 -41
@@ -0,0 +1,352 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Label } from "../label"
6
+ import { Badge } from "../badge"
7
+
8
+ // ─── Types ───────────────────────────────────────────────────────────────────
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ type SlateNode = any
12
+
13
+ /** Statistics about the diff between two documents */
14
+ export interface DiffStats {
15
+ /** Number of nodes added */
16
+ added: number
17
+ /** Number of nodes removed */
18
+ removed: number
19
+ /** Number of nodes modified */
20
+ modified: number
21
+ /** Number of unchanged nodes */
22
+ unchanged: number
23
+ }
24
+
25
+ export interface WakaDiffViewerProps {
26
+ /** Original (before) Slate document */
27
+ original: SlateNode[]
28
+ /** Modified (after) Slate document */
29
+ modified: SlateNode[]
30
+ /** Display mode */
31
+ mode?: "inline" | "side-by-side"
32
+ /** Label */
33
+ label?: string
34
+ /** Description */
35
+ description?: string
36
+ /** CSS class */
37
+ className?: string
38
+ /** Editor area CSS class */
39
+ editorClassName?: string
40
+ /** Minimum height in px */
41
+ minHeight?: number
42
+ /** Show diff statistics header */
43
+ showStats?: boolean
44
+ /** Label for the original document */
45
+ originalLabel?: string
46
+ /** Label for the modified document */
47
+ modifiedLabel?: string
48
+ /** Callback with diff stats when diff is computed */
49
+ onDiffComputed?: (stats: DiffStats) => void
50
+ }
51
+
52
+ // ─── Component ───────────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * WakaDiffViewer -- Visual diff between two document versions.
56
+ *
57
+ * Uses `@platejs/diff` to compute and render differences between
58
+ * two Slate document trees. Supports inline (unified) and
59
+ * side-by-side diff views.
60
+ *
61
+ * Rendering:
62
+ * - Added text is highlighted in green
63
+ * - Removed text is highlighted in red with strikethrough
64
+ * - Modified blocks show both states
65
+ *
66
+ * Useful for:
67
+ * - Specification version comparison (ws-serv-specs)
68
+ * - Audit trail diff review (ws-serv-audit)
69
+ * - Contract amendment review (WakaSign)
70
+ * - Content editorial review (WakaPress)
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * <WakaDiffViewer
75
+ * original={previousVersion}
76
+ * modified={currentVersion}
77
+ * mode="side-by-side"
78
+ * showStats
79
+ * originalLabel="v1.2.0"
80
+ * modifiedLabel="v1.3.0"
81
+ * />
82
+ * ```
83
+ */
84
+ export const WakaDiffViewer = React.forwardRef<HTMLDivElement, WakaDiffViewerProps>(
85
+ (
86
+ {
87
+ original,
88
+ modified,
89
+ mode = "inline",
90
+ label,
91
+ description,
92
+ className,
93
+ editorClassName,
94
+ minHeight = 200,
95
+ showStats = true,
96
+ originalLabel = "Original",
97
+ modifiedLabel = "Modifie",
98
+ onDiffComputed,
99
+ },
100
+ ref
101
+ ) => {
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ const [diffResult, setDiffResult] = React.useState<any>(null)
104
+ const [stats, setStats] = React.useState<DiffStats>({ added: 0, removed: 0, modified: 0, unchanged: 0 })
105
+ const [loadError, setLoadError] = React.useState(false)
106
+
107
+ // Compute diff
108
+ React.useEffect(() => {
109
+ let cancelled = false
110
+
111
+ const computeDiff = async () => {
112
+ try {
113
+ const diffModule = await import("@platejs/diff")
114
+
115
+ if (diffModule.computeDiff) {
116
+ const result = diffModule.computeDiff(original, modified)
117
+ if (!cancelled) {
118
+ setDiffResult(result)
119
+
120
+ // Compute stats
121
+ const s: DiffStats = { added: 0, removed: 0, modified: 0, unchanged: 0 }
122
+ if (Array.isArray(result)) {
123
+ for (const node of result) {
124
+ if (node.diff === "insert" || node.diffOperation?.type === "insert") s.added++
125
+ else if (node.diff === "delete" || node.diffOperation?.type === "delete") s.removed++
126
+ else if (node.diff === "update" || node.diffOperation?.type === "update") s.modified++
127
+ else s.unchanged++
128
+ }
129
+ }
130
+ setStats(s)
131
+ onDiffComputed?.(s)
132
+ }
133
+ }
134
+ } catch (err) {
135
+ console.error("[WakaDiffViewer] Failed to compute diff:", err)
136
+ if (!cancelled) setLoadError(true)
137
+ }
138
+ }
139
+
140
+ computeDiff()
141
+ return () => { cancelled = true }
142
+ }, [original, modified, onDiffComputed])
143
+
144
+ // ── Fallback: simple text diff ────────────────────────────────────────
145
+ if (loadError || !diffResult) {
146
+ return (
147
+ <div ref={ref} className={cn("space-y-1.5", className)}>
148
+ {label && <Label>{label}</Label>}
149
+ {description && <p className="text-sm text-muted-foreground">{description}</p>}
150
+
151
+ {loadError ? (
152
+ <SimpleDiffView
153
+ original={original}
154
+ modified={modified}
155
+ mode={mode}
156
+ originalLabel={originalLabel}
157
+ modifiedLabel={modifiedLabel}
158
+ minHeight={minHeight}
159
+ editorClassName={editorClassName}
160
+ />
161
+ ) : (
162
+ <div className="flex items-center justify-center border rounded-lg bg-muted/30" style={{ minHeight }}>
163
+ <span className="text-sm text-muted-foreground animate-pulse">
164
+ Calcul des differences...
165
+ </span>
166
+ </div>
167
+ )}
168
+ </div>
169
+ )
170
+ }
171
+
172
+ // ── Render diff ───────────────────────────────────────────────────────
173
+ return (
174
+ <div ref={ref} className={cn("space-y-1.5", className)}>
175
+ {label && <Label>{label}</Label>}
176
+ {description && <p className="text-sm text-muted-foreground">{description}</p>}
177
+
178
+ <div className={cn("border rounded-lg overflow-hidden")}>
179
+ {/* Stats header */}
180
+ {showStats && (
181
+ <div className="flex items-center gap-3 px-4 py-2 border-b bg-muted/30">
182
+ <span className="text-xs text-muted-foreground font-medium">Differences :</span>
183
+ {stats.added > 0 && (
184
+ <Badge variant="secondary" className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 text-[10px] h-5">
185
+ +{stats.added} ajout{stats.added > 1 ? "s" : ""}
186
+ </Badge>
187
+ )}
188
+ {stats.removed > 0 && (
189
+ <Badge variant="secondary" className="bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 text-[10px] h-5">
190
+ -{stats.removed} suppression{stats.removed > 1 ? "s" : ""}
191
+ </Badge>
192
+ )}
193
+ {stats.modified > 0 && (
194
+ <Badge variant="secondary" className="bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 text-[10px] h-5">
195
+ ~{stats.modified} modification{stats.modified > 1 ? "s" : ""}
196
+ </Badge>
197
+ )}
198
+ {stats.added === 0 && stats.removed === 0 && stats.modified === 0 && (
199
+ <span className="text-xs text-muted-foreground">Aucune difference</span>
200
+ )}
201
+ </div>
202
+ )}
203
+
204
+ {/* Diff content */}
205
+ {mode === "side-by-side" ? (
206
+ <div className="flex divide-x" style={{ minHeight }}>
207
+ <div className="flex-1 min-w-0">
208
+ <div className="px-3 py-1.5 border-b bg-red-50/50 dark:bg-red-950/10 text-xs font-medium text-muted-foreground">
209
+ {originalLabel}
210
+ </div>
211
+ <DiffDocumentView
212
+ nodes={original}
213
+ diffType="original"
214
+ className={editorClassName}
215
+ />
216
+ </div>
217
+ <div className="flex-1 min-w-0">
218
+ <div className="px-3 py-1.5 border-b bg-green-50/50 dark:bg-green-950/10 text-xs font-medium text-muted-foreground">
219
+ {modifiedLabel}
220
+ </div>
221
+ <DiffDocumentView
222
+ nodes={modified}
223
+ diffType="modified"
224
+ className={editorClassName}
225
+ />
226
+ </div>
227
+ </div>
228
+ ) : (
229
+ <div style={{ minHeight }}>
230
+ <DiffInlineView
231
+ diffNodes={diffResult}
232
+ className={editorClassName}
233
+ />
234
+ </div>
235
+ )}
236
+ </div>
237
+ </div>
238
+ )
239
+ }
240
+ )
241
+
242
+ WakaDiffViewer.displayName = "WakaDiffViewer"
243
+
244
+ // ─── Diff rendering helpers ─────────────────────────────────────────────────
245
+
246
+ function DiffDocumentView({
247
+ nodes,
248
+ diffType,
249
+ className,
250
+ }: {
251
+ nodes: SlateNode[]
252
+ diffType: "original" | "modified"
253
+ className?: string
254
+ }) {
255
+ return (
256
+ <div className={cn("prose prose-sm max-w-none p-4 text-sm", className)}>
257
+ {nodes.map((node: SlateNode, i: number) => (
258
+ <DiffNodeRenderer key={i} node={node} diffType={diffType} />
259
+ ))}
260
+ </div>
261
+ )
262
+ }
263
+
264
+ function DiffInlineView({
265
+ diffNodes,
266
+ className,
267
+ }: {
268
+ diffNodes: SlateNode[]
269
+ className?: string
270
+ }) {
271
+ return (
272
+ <div className={cn("prose prose-sm max-w-none p-4 text-sm", className)}>
273
+ {diffNodes.map((node: SlateNode, i: number) => {
274
+ const diffOp = node.diff || node.diffOperation?.type
275
+ let bgClass = ""
276
+ if (diffOp === "insert") bgClass = "bg-green-100/60 dark:bg-green-900/20"
277
+ else if (diffOp === "delete") bgClass = "bg-red-100/60 dark:bg-red-900/20 line-through opacity-60"
278
+ else if (diffOp === "update") bgClass = "bg-amber-100/60 dark:bg-amber-900/20"
279
+
280
+ return (
281
+ <div key={i} className={cn("rounded px-1 -mx-1", bgClass)}>
282
+ <DiffNodeRenderer node={node} diffType="modified" />
283
+ </div>
284
+ )
285
+ })}
286
+ </div>
287
+ )
288
+ }
289
+
290
+ function DiffNodeRenderer({ node, diffType }: { node: SlateNode; diffType: "original" | "modified" }) {
291
+ if (!node) return null
292
+
293
+ const text = extractText(node)
294
+ const type = node.type || "p"
295
+
296
+ switch (type) {
297
+ case "h1": return <h1 className="text-xl font-bold mt-4 mb-1">{text}</h1>
298
+ case "h2": return <h2 className="text-lg font-semibold mt-3 mb-1">{text}</h2>
299
+ case "h3": return <h3 className="text-base font-semibold mt-2 mb-1">{text}</h3>
300
+ case "blockquote": return <blockquote className="border-l-2 border-muted-foreground/30 pl-3 italic text-muted-foreground">{text}</blockquote>
301
+ default: return <p className="my-0.5 leading-relaxed">{text || "\u00A0"}</p>
302
+ }
303
+ }
304
+
305
+ function SimpleDiffView({
306
+ original,
307
+ modified,
308
+ mode,
309
+ originalLabel,
310
+ modifiedLabel,
311
+ minHeight,
312
+ editorClassName,
313
+ }: {
314
+ original: SlateNode[]
315
+ modified: SlateNode[]
316
+ mode: "inline" | "side-by-side"
317
+ originalLabel: string
318
+ modifiedLabel: string
319
+ minHeight?: number
320
+ editorClassName?: string
321
+ }) {
322
+ if (mode === "side-by-side") {
323
+ return (
324
+ <div className="flex divide-x border rounded-lg overflow-hidden" style={{ minHeight }}>
325
+ <div className="flex-1 min-w-0">
326
+ <div className="px-3 py-1.5 border-b bg-red-50/50 dark:bg-red-950/10 text-xs font-medium text-muted-foreground">{originalLabel}</div>
327
+ <DiffDocumentView nodes={original} diffType="original" className={editorClassName} />
328
+ </div>
329
+ <div className="flex-1 min-w-0">
330
+ <div className="px-3 py-1.5 border-b bg-green-50/50 dark:bg-green-950/10 text-xs font-medium text-muted-foreground">{modifiedLabel}</div>
331
+ <DiffDocumentView nodes={modified} diffType="modified" className={editorClassName} />
332
+ </div>
333
+ </div>
334
+ )
335
+ }
336
+
337
+ return (
338
+ <div className="border rounded-lg overflow-hidden" style={{ minHeight }}>
339
+ <div className="p-4 text-sm text-muted-foreground">
340
+ Installez <code className="bg-muted px-1 rounded">@platejs/diff</code> pour la visualisation unifiee des differences.
341
+ </div>
342
+ </div>
343
+ )
344
+ }
345
+
346
+ function extractText(node: SlateNode): string {
347
+ if (typeof node?.text === "string") return node.text
348
+ if (Array.isArray(node?.children)) {
349
+ return node.children.map((c: SlateNode) => extractText(c)).join("")
350
+ }
351
+ return ""
352
+ }
@@ -0,0 +1,284 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Label } from "../label"
6
+ import { Textarea } from "../textarea"
7
+
8
+ // ─── Types ───────────────────────────────────────────────────────────────────
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ type SlateNode = any
12
+
13
+ export interface WakaDnDEditorProps {
14
+ /** Initial Slate content */
15
+ value?: SlateNode[]
16
+ /** Change callback */
17
+ onChange?: (value: SlateNode[]) => void
18
+ /** Read-only mode */
19
+ readOnly?: boolean
20
+ /** Placeholder */
21
+ placeholder?: string
22
+ /** CSS class */
23
+ className?: string
24
+ /** Editor CSS class */
25
+ editorClassName?: string
26
+ /** Minimum height in px */
27
+ minHeight?: number
28
+ /** Label */
29
+ label?: string
30
+ /** Description */
31
+ description?: string
32
+ /** Error */
33
+ error?: string
34
+ /**
35
+ * Show a drag handle gutter on the left side of each block.
36
+ * When false, DnD still works but handles are hidden.
37
+ */
38
+ showDragHandles?: boolean
39
+ /**
40
+ * Show a drop line indicator when dragging blocks.
41
+ * Defaults to true.
42
+ */
43
+ showDropIndicator?: boolean
44
+ /** Extra Plate plugins */
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ extraPlugins?: any[]
47
+ /** Ref to editor instance */
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ editorRef?: React.MutableRefObject<any>
50
+ }
51
+
52
+ // ─── Component ───────────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * WakaDnDEditor -- Plate editor with drag-and-drop block reordering.
56
+ *
57
+ * Users can grab any block by its drag handle and reorder it within
58
+ * the document. Built on top of `@platejs/dnd` and `@dnd-kit`.
59
+ *
60
+ * Includes block selection via `@platejs/selection` so users can
61
+ * select multiple blocks and drag them as a group.
62
+ *
63
+ * Useful for:
64
+ * - Kanban-style content builders
65
+ * - Form builders (WakaStart app wizard)
66
+ * - Reorderable document sections
67
+ * - Spec editor with rearrangeable requirements
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * <WakaDnDEditor
72
+ * value={blocks}
73
+ * onChange={setBlocks}
74
+ * showDragHandles
75
+ * />
76
+ * ```
77
+ */
78
+ export const WakaDnDEditor = React.forwardRef<HTMLDivElement, WakaDnDEditorProps>(
79
+ (
80
+ {
81
+ value,
82
+ onChange,
83
+ readOnly = false,
84
+ placeholder = "Glissez-deposez les blocs pour les reorganiser...",
85
+ className,
86
+ editorClassName,
87
+ minHeight = 300,
88
+ label,
89
+ description,
90
+ error,
91
+ showDragHandles = true,
92
+ extraPlugins,
93
+ editorRef,
94
+ },
95
+ ref
96
+ ) => {
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ const [bundle, setBundle] = React.useState<any>(null)
99
+ const [loadError, setLoadError] = React.useState(false)
100
+
101
+ React.useEffect(() => {
102
+ let cancelled = false
103
+
104
+ const load = async () => {
105
+ try {
106
+ const [plateReact, basicNodes, tableModule, layoutModule, calloutModule, linkModule, dndModule, selectionModule] =
107
+ await Promise.all([
108
+ import("platejs/react"),
109
+ import("@platejs/basic-nodes/react"),
110
+ import("@platejs/table/react"),
111
+ import("@platejs/layout/react"),
112
+ import("@platejs/callout/react"),
113
+ import("@platejs/link/react"),
114
+ import("@platejs/dnd/react"),
115
+ import("@platejs/selection/react"),
116
+ ])
117
+
118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
+ const plugins: any[] = [
120
+ basicNodes.BoldPlugin, basicNodes.ItalicPlugin, basicNodes.UnderlinePlugin,
121
+ basicNodes.StrikethroughPlugin, basicNodes.CodePlugin, basicNodes.HighlightPlugin,
122
+ basicNodes.H1Plugin, basicNodes.H2Plugin, basicNodes.H3Plugin,
123
+ basicNodes.H4Plugin, basicNodes.BlockquotePlugin,
124
+ tableModule.TablePlugin, tableModule.TableRowPlugin,
125
+ tableModule.TableCellPlugin, tableModule.TableCellHeaderPlugin,
126
+ layoutModule.ColumnPlugin, layoutModule.ColumnItemPlugin,
127
+ calloutModule.CalloutPlugin,
128
+ linkModule.LinkPlugin.configure({
129
+ options: { allowedSchemes: ["http", "https", "mailto", "tel"] },
130
+ }),
131
+ // Selection (multi-block select)
132
+ selectionModule.BlockSelectionPlugin,
133
+ // DnD
134
+ dndModule.DndPlugin.configure({
135
+ options: {
136
+ enableScroller: true,
137
+ },
138
+ }),
139
+ ]
140
+
141
+ if (!cancelled) {
142
+ setBundle({
143
+ Plate: plateReact.Plate,
144
+ PlateContent: plateReact.PlateContent,
145
+ usePlateEditor: plateReact.usePlateEditor,
146
+ plugins,
147
+ })
148
+ }
149
+ } catch (err) {
150
+ console.error("[WakaDnDEditor] Failed to load:", err)
151
+ if (!cancelled) setLoadError(true)
152
+ }
153
+ }
154
+
155
+ load()
156
+ return () => { cancelled = true }
157
+ }, [])
158
+
159
+ if (bundle) {
160
+ return (
161
+ <DnDEditorInner
162
+ ref={ref}
163
+ bundle={bundle}
164
+ value={value}
165
+ onChange={onChange}
166
+ readOnly={readOnly}
167
+ placeholder={placeholder}
168
+ className={className}
169
+ editorClassName={editorClassName}
170
+ minHeight={minHeight}
171
+ label={label}
172
+ description={description}
173
+ error={error}
174
+ showDragHandles={showDragHandles}
175
+ extraPlugins={extraPlugins}
176
+ editorRef={editorRef}
177
+ />
178
+ )
179
+ }
180
+
181
+ return (
182
+ <div ref={ref} className={cn("space-y-1.5", className)}>
183
+ {label && <Label>{label}</Label>}
184
+ {description && <p className="text-sm text-muted-foreground">{description}</p>}
185
+ <div className="relative">
186
+ <Textarea placeholder={placeholder} disabled style={{ minHeight }} />
187
+ <div className="absolute inset-0 flex items-center justify-center bg-background/50 rounded-lg">
188
+ <span className="text-sm text-muted-foreground animate-pulse">
189
+ {loadError ? "Erreur de chargement" : "Chargement..."}
190
+ </span>
191
+ </div>
192
+ </div>
193
+ {error && <p className="text-sm text-destructive">{error}</p>}
194
+ </div>
195
+ )
196
+ }
197
+ )
198
+
199
+ WakaDnDEditor.displayName = "WakaDnDEditor"
200
+
201
+ // ─── Inner component ────────────────────────────────────────────────────────
202
+
203
+ const DnDEditorInner = React.forwardRef<HTMLDivElement, WakaDnDEditorProps & {
204
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
+ bundle: any
206
+ }>(
207
+ (
208
+ {
209
+ bundle: { Plate, PlateContent, usePlateEditor, plugins },
210
+ value,
211
+ onChange,
212
+ readOnly,
213
+ placeholder,
214
+ className,
215
+ editorClassName,
216
+ minHeight,
217
+ label,
218
+ description,
219
+ error,
220
+ showDragHandles,
221
+ extraPlugins,
222
+ editorRef,
223
+ },
224
+ ref
225
+ ) => {
226
+ const allPlugins = React.useMemo(
227
+ () => [...plugins, ...(extraPlugins ?? [])],
228
+ [plugins, extraPlugins]
229
+ )
230
+
231
+ const editor = usePlateEditor({
232
+ plugins: allPlugins,
233
+ value: value ?? [{ type: "p", children: [{ text: "" }] }],
234
+ })
235
+
236
+ React.useEffect(() => {
237
+ if (editorRef) editorRef.current = editor
238
+ }, [editor, editorRef])
239
+
240
+ return (
241
+ <div ref={ref} className={cn("space-y-1.5", className)}>
242
+ {label && <Label>{label}</Label>}
243
+ {description && <p className="text-sm text-muted-foreground">{description}</p>}
244
+
245
+ <Plate
246
+ editor={editor}
247
+ onChange={onChange ? ({ value: v }: { value: SlateNode[] }) => onChange(v) : undefined}
248
+ readOnly={readOnly}
249
+ >
250
+ <div className={cn(
251
+ "border rounded-lg overflow-hidden",
252
+ error && "border-destructive",
253
+ )}>
254
+ {/* DnD hint bar */}
255
+ {!readOnly && showDragHandles && (
256
+ <div className="flex items-center gap-2 px-3 py-1.5 border-b bg-muted/30 text-xs text-muted-foreground">
257
+ <span className="inline-block w-4 text-center">&#x2630;</span>
258
+ <span>Survolez un bloc pour voir la poignee de glissement</span>
259
+ </div>
260
+ )}
261
+
262
+ <div style={{ minHeight }}>
263
+ <PlateContent
264
+ placeholder={placeholder}
265
+ readOnly={readOnly}
266
+ className={cn(
267
+ "prose prose-sm max-w-none p-4",
268
+ "focus-within:outline-none",
269
+ "[&_[data-slate-editor]]:outline-none [&_[data-slate-editor]]:min-h-[inherit]",
270
+ showDragHandles && "[&_[data-slate-node='element']]:relative [&_[data-slate-node='element']]:pl-6",
271
+ editorClassName,
272
+ )}
273
+ />
274
+ </div>
275
+ </div>
276
+ </Plate>
277
+
278
+ {error && <p className="text-sm text-destructive">{error}</p>}
279
+ </div>
280
+ )
281
+ }
282
+ )
283
+
284
+ DnDEditorInner.displayName = "DnDEditorInner"