@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.
- package/dist/badge-BbwO7QeZ.js +1 -0
- package/dist/badge-BfiocODp.mjs +23 -0
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunk-14q5BKub.js +1 -0
- package/dist/{chunk-BH6uBOac.mjs → chunk-Cr9pTUWm.mjs} +5 -5
- package/dist/cn-DEtaFQsA.js +1 -0
- package/dist/cn-DUn6aSIQ.mjs +24 -0
- package/dist/doc.cjs.js +2 -2
- package/dist/doc.es.js +19 -19
- package/dist/editor.cjs.js +48 -0
- package/dist/editor.d.ts +1 -0
- package/dist/editor.es.js +6551 -0
- package/dist/{exceljs.min-DG9M8IZ1.mjs → exceljs.min-DL1XYDll.mjs} +1 -1
- package/dist/{exceljs.min-BuefmDRS.js → exceljs.min-qeIfSCbF.js} +1 -1
- package/dist/export.cjs.js +1 -1
- package/dist/export.es.js +1 -1
- package/dist/index.cjs.js +150 -150
- package/dist/index.es.js +26782 -27591
- package/dist/input-BfaSAGVw.js +1 -0
- package/dist/input-DVr_Qkl8.mjs +14 -0
- package/dist/rich-text.cjs.js +1 -1
- package/dist/rich-text.es.js +1 -1
- package/dist/security-CyBpuklN.mjs +122 -0
- package/dist/security-bFWwDrlg.js +1 -0
- package/dist/separator-NrkltulH.js +1 -0
- package/dist/separator-ibN2mycs.mjs +51 -0
- package/dist/src/components/editor/blocks/index.d.ts +51 -0
- package/dist/src/components/editor/blocks/waka-acceptance-criteria-block.d.ts +60 -0
- package/dist/src/components/editor/blocks/waka-ai-assist-block.d.ts +58 -0
- package/dist/src/components/editor/blocks/waka-api-endpoint-block.d.ts +63 -0
- package/dist/src/components/editor/blocks/waka-code-playground-block.d.ts +61 -0
- package/dist/src/components/editor/blocks/waka-comment-thread-block.d.ts +85 -0
- package/dist/src/components/editor/blocks/waka-diagram-block.d.ts +52 -0
- package/dist/src/components/editor/blocks/waka-embed-block.d.ts +58 -0
- package/dist/src/components/editor/blocks/waka-slash-menu-block.d.ts +67 -0
- package/dist/src/components/editor/blocks/waka-user-story-block.d.ts +79 -0
- package/dist/src/components/editor/blocks/waka-version-diff-block.d.ts +73 -0
- package/dist/src/components/editor/index.d.ts +66 -0
- package/dist/src/components/editor/waka-ai-writer.d.ts +80 -0
- package/dist/src/components/editor/waka-collaborative-editor.d.ts +93 -0
- package/dist/src/components/editor/waka-diff-viewer.d.ts +71 -0
- package/dist/src/components/editor/waka-dnd-editor.d.ts +64 -0
- package/dist/src/components/editor/waka-document-editor.d.ts +92 -0
- package/dist/src/components/editor/waka-editor-elements.d.ts +79 -0
- package/dist/src/components/editor/waka-editor-leaves.d.ts +39 -0
- package/dist/src/components/editor/waka-editor-plugins.d.ts +41 -0
- package/dist/src/components/editor/waka-editor-toolbar.d.ts +20 -0
- package/dist/src/components/editor/waka-editor.d.ts +59 -0
- package/dist/src/components/editor/waka-floating-toolbar.d.ts +47 -0
- package/dist/src/components/editor/waka-markdown-editor.d.ts +60 -0
- package/dist/src/components/editor/waka-mention-editor.d.ts +125 -0
- package/dist/src/components/editor/waka-slash-menu.d.ts +70 -0
- package/dist/src/components/editor/waka-spec-editor.d.ts +88 -0
- package/dist/src/components/index.d.ts +1 -15
- package/dist/src/editor.d.ts +26 -0
- package/dist/textarea-CdQWggYG.js +1 -0
- package/dist/textarea-DJDXJ3nd.mjs +23 -0
- package/dist/types-C2St0wOW.js +1 -0
- package/dist/{types-B6GVaSIP.mjs → types-JnqoLyuv.mjs} +214 -211
- package/dist/{useDataTableImport-BPvfo--2.mjs → useDataTableImport-BWUFesPi.mjs} +3 -3
- package/dist/{useDataTableImport-Cm_pCKnO.js → useDataTableImport-T7ddpN5k.js} +3 -3
- package/dist/waka-doc-renderer-CTxC7Trf.js +3 -0
- package/dist/{waka-doc-renderer-BkIvas3z.mjs → waka-doc-renderer-Cw-Xnyen.mjs} +264 -281
- package/dist/waka-editor-plugins-DR6tpsUC.mjs +135 -0
- package/dist/waka-editor-plugins-sGSh9hn2.js +1 -0
- package/dist/waka-rich-text-editor-BlIdtknG.js +1 -0
- package/dist/waka-rich-text-editor-D1uA3zbB.js +1 -0
- package/dist/waka-rich-text-editor-DgSWiXMW.mjs +342 -0
- package/dist/waka-rich-text-editor-DndVJuDw.mjs +2 -0
- package/package.json +87 -2
- package/src/blocks/footer/index.tsx +1 -6
- package/src/blocks/login/index.tsx +1 -7
- package/src/blocks/profile/index.tsx +3 -5
- package/src/components/editor/blocks/index.ts +182 -0
- package/src/components/editor/blocks/waka-acceptance-criteria-block.tsx +326 -0
- package/src/components/editor/blocks/waka-ai-assist-block.tsx +284 -0
- package/src/components/editor/blocks/waka-api-endpoint-block.tsx +382 -0
- package/src/components/editor/blocks/waka-code-playground-block.tsx +331 -0
- package/src/components/editor/blocks/waka-comment-thread-block.tsx +448 -0
- package/src/components/editor/blocks/waka-diagram-block.tsx +293 -0
- package/src/components/editor/blocks/waka-embed-block.tsx +416 -0
- package/src/components/editor/blocks/waka-slash-menu-block.tsx +432 -0
- package/src/components/editor/blocks/waka-user-story-block.tsx +295 -0
- package/src/components/editor/blocks/waka-version-diff-block.tsx +426 -0
- package/src/components/editor/index.ts +279 -0
- package/src/components/editor/waka-ai-writer.tsx +434 -0
- package/src/components/editor/waka-collaborative-editor.tsx +426 -0
- package/src/components/editor/waka-diff-viewer.tsx +352 -0
- package/src/components/editor/waka-dnd-editor.tsx +284 -0
- package/src/components/editor/waka-document-editor.tsx +502 -0
- package/src/components/editor/waka-editor-elements.tsx +312 -0
- package/src/components/editor/waka-editor-leaves.tsx +101 -0
- package/src/components/editor/waka-editor-plugins.ts +207 -0
- package/src/components/editor/waka-editor-toolbar.tsx +358 -0
- package/src/components/editor/waka-editor.tsx +431 -0
- package/src/components/editor/waka-floating-toolbar.tsx +268 -0
- package/src/components/editor/waka-markdown-editor.tsx +395 -0
- package/src/components/editor/waka-mention-editor.tsx +459 -0
- package/src/components/editor/waka-slash-menu.tsx +392 -0
- package/src/components/editor/waka-spec-editor.tsx +657 -0
- package/src/components/index.ts +1 -18
- package/dist/chunk-BDDJmn7V.js +0 -1
- package/dist/cn-DnPbmOCy.js +0 -1
- package/dist/cn-DpLcAzrf.mjs +0 -22
- package/dist/separator-BDReXBvI.mjs +0 -59
- package/dist/separator-BKjNl9sI.js +0 -1
- package/dist/src/components/waka-actor-badge/index.d.ts +0 -8
- package/dist/src/components/waka-actors-list/index.d.ts +0 -18
- package/dist/src/components/waka-ai-assistant-button/index.d.ts +0 -8
- package/dist/src/components/waka-document-flyover/index.d.ts +0 -10
- package/dist/src/components/waka-document-preview-popup/index.d.ts +0 -26
- package/dist/src/components/waka-hour-balance-badge/index.d.ts +0 -8
- package/dist/src/components/waka-hour-consumption-table/index.d.ts +0 -15
- package/dist/src/components/waka-hour-pack-dialog/index.d.ts +0 -8
- package/dist/src/components/waka-project-stats-header/index.d.ts +0 -15
- package/dist/src/components/waka-step-comment-bubble/index.d.ts +0 -13
- package/dist/src/components/waka-step-comment-panel/index.d.ts +0 -20
- package/dist/src/components/waka-step-permission-matrix/index.d.ts +0 -12
- package/dist/src/components/waka-time-entry-dialog/index.d.ts +0 -16
- package/dist/src/components/waka-time-tracking-flyover/index.d.ts +0 -11
- package/dist/types-BH9cQRqZ.js +0 -1
- package/dist/waka-doc-renderer-BZ2-SqyT.js +0 -3
- package/dist/waka-rich-text-editor-BJGlQgpq.js +0 -1
- package/dist/waka-rich-text-editor-BJzzxeP1.mjs +0 -361
- package/dist/waka-rich-text-editor-wnXLwvUo.js +0 -1
- package/src/components/waka-actor-badge/index.tsx +0 -34
- package/src/components/waka-actors-list/index.tsx +0 -125
- package/src/components/waka-ai-assistant-button/index.tsx +0 -31
- package/src/components/waka-document-flyover/index.tsx +0 -36
- package/src/components/waka-document-preview-popup/index.tsx +0 -103
- package/src/components/waka-hour-balance-badge/index.tsx +0 -43
- package/src/components/waka-hour-consumption-table/index.tsx +0 -72
- package/src/components/waka-hour-pack-dialog/index.tsx +0 -72
- package/src/components/waka-project-stats-header/index.tsx +0 -69
- package/src/components/waka-step-comment-bubble/index.tsx +0 -71
- package/src/components/waka-step-comment-panel/index.tsx +0 -106
- package/src/components/waka-step-permission-matrix/index.tsx +0 -65
- package/src/components/waka-time-entry-dialog/index.tsx +0 -131
- 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">☰</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"
|