@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,426 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../../utils/cn"
5
+ import type { PlateElementProps } from "../waka-editor-elements"
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ export const VERSION_DIFF_BLOCK_TYPE = "version_diff" as const
12
+
13
+ /** A single diff hunk (line-level diff) */
14
+ export interface DiffLine {
15
+ /** Line content */
16
+ content: string
17
+ /** Diff type */
18
+ type: "added" | "removed" | "unchanged" | "modified"
19
+ /** Original line number (for removed/unchanged) */
20
+ oldLineNumber?: number
21
+ /** New line number (for added/unchanged) */
22
+ newLineNumber?: number
23
+ }
24
+
25
+ /** Represents a section of changes */
26
+ export interface DiffHunk {
27
+ /** Start line in old version */
28
+ oldStart: number
29
+ /** Start line in new version */
30
+ newStart: number
31
+ /** Lines in this hunk */
32
+ lines: DiffLine[]
33
+ }
34
+
35
+ /** Slate node for version diff blocks */
36
+ export interface VersionDiffElement {
37
+ type: typeof VERSION_DIFF_BLOCK_TYPE
38
+ /** Old (previous) version label */
39
+ oldVersionLabel: string
40
+ /** New (current) version label */
41
+ newVersionLabel: string
42
+ /** Old version full text */
43
+ oldText: string
44
+ /** New version full text */
45
+ newText: string
46
+ /** Pre-computed diff hunks */
47
+ hunks: DiffHunk[]
48
+ /** Summary stats */
49
+ stats: { additions: number; deletions: number; modifications: number }
50
+ /** Author of the change */
51
+ author?: string
52
+ /** Timestamp of the change */
53
+ timestamp?: string
54
+ children: Array<{ text: string }>
55
+ }
56
+
57
+ export interface WakaVersionDiffBlockProps extends PlateElementProps {
58
+ element?: VersionDiffElement & Record<string, unknown>
59
+ }
60
+
61
+ // ============================================================================
62
+ // Diff View Modes
63
+ // ============================================================================
64
+
65
+ type DiffViewMode = "unified" | "split" | "summary"
66
+
67
+ // ============================================================================
68
+ // Element Component
69
+ // ============================================================================
70
+
71
+ /**
72
+ * WakaVersionDiffBlock - A Plate.js block that displays a visual diff between
73
+ * two versions of text content. Shows additions, deletions, and modifications
74
+ * with unified or split view modes.
75
+ *
76
+ * Designed to integrate with `@platejs/diff` or work standalone with pre-computed
77
+ * diff data.
78
+ *
79
+ * Register in Plate editor:
80
+ * ```ts
81
+ * components: {
82
+ * [VERSION_DIFF_BLOCK_TYPE]: WakaVersionDiffBlock,
83
+ * }
84
+ * ```
85
+ */
86
+ export function WakaVersionDiffBlock({
87
+ attributes,
88
+ children,
89
+ element,
90
+ className,
91
+ }: WakaVersionDiffBlockProps) {
92
+ const el = element as VersionDiffElement | undefined
93
+ const [viewMode, setViewMode] = React.useState<DiffViewMode>("unified")
94
+ const [expanded, setExpanded] = React.useState(true)
95
+
96
+ const hunks = el?.hunks || []
97
+ const stats = el?.stats || { additions: 0, deletions: 0, modifications: 0 }
98
+ const oldLabel = el?.oldVersionLabel || "Previous"
99
+ const newLabel = el?.newVersionLabel || "Current"
100
+
101
+ return (
102
+ <div
103
+ {...attributes}
104
+ contentEditable={false}
105
+ className={cn(
106
+ "my-4 rounded-lg overflow-hidden border border-border shadow-sm",
107
+ className
108
+ )}
109
+ >
110
+ {/* Header */}
111
+ <div className="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b border-border">
112
+ <div className="flex items-center gap-3">
113
+ {/* Git-like icon */}
114
+ <svg
115
+ className="h-4 w-4 text-orange-600 dark:text-orange-400"
116
+ viewBox="0 0 24 24"
117
+ fill="none"
118
+ stroke="currentColor"
119
+ strokeWidth={2}
120
+ aria-hidden="true"
121
+ >
122
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 3v6m0 0l-3 3m3-3l3 3M6 21h12a2 2 0 002-2V7l-5-5H6a2 2 0 00-2 2v15a2 2 0 002 2z" />
123
+ </svg>
124
+ <span className="text-xs font-bold uppercase tracking-wider text-foreground">
125
+ Version Diff
126
+ </span>
127
+
128
+ {/* Version labels */}
129
+ <div className="flex items-center gap-1 text-[10px]">
130
+ <span className="px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400 font-mono font-medium">
131
+ {oldLabel}
132
+ </span>
133
+ <svg className="h-3 w-3 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
134
+ <path strokeLinecap="round" strokeLinejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />
135
+ </svg>
136
+ <span className="px-1.5 py-0.5 rounded bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 font-mono font-medium">
137
+ {newLabel}
138
+ </span>
139
+ </div>
140
+ </div>
141
+
142
+ <div className="flex items-center gap-3">
143
+ {/* Stats */}
144
+ <div className="flex items-center gap-2 text-[10px] font-mono">
145
+ {stats.additions > 0 && (
146
+ <span className="text-emerald-600 dark:text-emerald-400 font-semibold">+{stats.additions}</span>
147
+ )}
148
+ {stats.deletions > 0 && (
149
+ <span className="text-red-600 dark:text-red-400 font-semibold">-{stats.deletions}</span>
150
+ )}
151
+ {stats.modifications > 0 && (
152
+ <span className="text-amber-600 dark:text-amber-400 font-semibold">~{stats.modifications}</span>
153
+ )}
154
+ </div>
155
+
156
+ {/* View mode toggle */}
157
+ <div className="flex items-center border border-border rounded overflow-hidden">
158
+ {(["unified", "split", "summary"] as DiffViewMode[]).map((mode) => (
159
+ <button
160
+ key={mode}
161
+ type="button"
162
+ onClick={() => setViewMode(mode)}
163
+ className={cn(
164
+ "px-2 py-1 text-[10px] font-medium transition-colors",
165
+ viewMode === mode
166
+ ? "bg-foreground/10 text-foreground"
167
+ : "text-muted-foreground hover:text-foreground"
168
+ )}
169
+ >
170
+ {mode.charAt(0).toUpperCase() + mode.slice(1)}
171
+ </button>
172
+ ))}
173
+ </div>
174
+
175
+ {/* Collapse toggle */}
176
+ <button
177
+ type="button"
178
+ onClick={() => setExpanded(!expanded)}
179
+ className="text-muted-foreground hover:text-foreground transition-colors"
180
+ >
181
+ <svg
182
+ className={cn("h-4 w-4 transition-transform", !expanded && "-rotate-90")}
183
+ viewBox="0 0 24 24"
184
+ fill="none"
185
+ stroke="currentColor"
186
+ strokeWidth={2}
187
+ >
188
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
189
+ </svg>
190
+ </button>
191
+ </div>
192
+ </div>
193
+
194
+ {/* Author + timestamp */}
195
+ {(el?.author || el?.timestamp) && (
196
+ <div className="px-4 py-1.5 border-b border-border/50 bg-muted/10 flex items-center gap-3 text-[10px] text-muted-foreground">
197
+ {el.author && (
198
+ <div className="flex items-center gap-1">
199
+ <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
200
+ <path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
201
+ </svg>
202
+ <span className="font-medium">{el.author}</span>
203
+ </div>
204
+ )}
205
+ {el.timestamp && (
206
+ <span>{new Date(el.timestamp).toLocaleString("fr-FR")}</span>
207
+ )}
208
+ </div>
209
+ )}
210
+
211
+ {/* Diff content */}
212
+ {expanded && (
213
+ <div className="overflow-x-auto">
214
+ {viewMode === "summary" ? (
215
+ /* Summary view */
216
+ <div className="p-4 space-y-3">
217
+ <div className="grid grid-cols-3 gap-3">
218
+ <div className="rounded-lg bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 p-3 text-center">
219
+ <div className="text-2xl font-bold text-emerald-600 dark:text-emerald-400">{stats.additions}</div>
220
+ <div className="text-[10px] font-medium text-emerald-700/70 dark:text-emerald-400/70 uppercase tracking-wider">Additions</div>
221
+ </div>
222
+ <div className="rounded-lg bg-red-50 dark:bg-red-500/5 border border-red-200 dark:border-red-500/20 p-3 text-center">
223
+ <div className="text-2xl font-bold text-red-600 dark:text-red-400">{stats.deletions}</div>
224
+ <div className="text-[10px] font-medium text-red-700/70 dark:text-red-400/70 uppercase tracking-wider">Deletions</div>
225
+ </div>
226
+ <div className="rounded-lg bg-amber-50 dark:bg-amber-500/5 border border-amber-200 dark:border-amber-500/20 p-3 text-center">
227
+ <div className="text-2xl font-bold text-amber-600 dark:text-amber-400">{stats.modifications}</div>
228
+ <div className="text-[10px] font-medium text-amber-700/70 dark:text-amber-400/70 uppercase tracking-wider">Modified</div>
229
+ </div>
230
+ </div>
231
+ {/* Change bar */}
232
+ <div className="h-2 rounded-full bg-muted overflow-hidden flex">
233
+ {stats.additions > 0 && (
234
+ <div className="bg-emerald-500 h-full" style={{ width: `${(stats.additions / (stats.additions + stats.deletions + stats.modifications)) * 100}%` }} />
235
+ )}
236
+ {stats.modifications > 0 && (
237
+ <div className="bg-amber-500 h-full" style={{ width: `${(stats.modifications / (stats.additions + stats.deletions + stats.modifications)) * 100}%` }} />
238
+ )}
239
+ {stats.deletions > 0 && (
240
+ <div className="bg-red-500 h-full" style={{ width: `${(stats.deletions / (stats.additions + stats.deletions + stats.modifications)) * 100}%` }} />
241
+ )}
242
+ </div>
243
+ </div>
244
+ ) : viewMode === "split" ? (
245
+ /* Split view */
246
+ <div className="grid grid-cols-2 divide-x divide-border">
247
+ <div className="p-0">
248
+ <div className="px-3 py-1.5 border-b border-border/50 bg-red-50/50 dark:bg-red-950/10">
249
+ <span className="text-[10px] font-semibold text-red-700 dark:text-red-400 uppercase tracking-wider">{oldLabel}</span>
250
+ </div>
251
+ <div className="font-mono text-[12px] leading-relaxed">
252
+ {hunks.map((hunk, hi) => (
253
+ <React.Fragment key={hi}>
254
+ {hunk.lines.filter((l) => l.type !== "added").map((line, li) => (
255
+ <div
256
+ key={`${hi}-${li}`}
257
+ className={cn(
258
+ "px-3 py-0.5 flex",
259
+ line.type === "removed" && "bg-red-100/60 dark:bg-red-500/10",
260
+ line.type === "modified" && "bg-amber-100/60 dark:bg-amber-500/10"
261
+ )}
262
+ >
263
+ <span className="w-8 text-right pr-2 text-muted-foreground/50 select-none shrink-0">
264
+ {line.oldLineNumber}
265
+ </span>
266
+ <span className={cn(
267
+ line.type === "removed" && "text-red-800 dark:text-red-300",
268
+ line.type === "modified" && "text-amber-800 dark:text-amber-300"
269
+ )}>
270
+ {line.type === "removed" && <span className="text-red-500 mr-1">-</span>}
271
+ {line.content}
272
+ </span>
273
+ </div>
274
+ ))}
275
+ </React.Fragment>
276
+ ))}
277
+ </div>
278
+ </div>
279
+ <div className="p-0">
280
+ <div className="px-3 py-1.5 border-b border-border/50 bg-emerald-50/50 dark:bg-emerald-950/10">
281
+ <span className="text-[10px] font-semibold text-emerald-700 dark:text-emerald-400 uppercase tracking-wider">{newLabel}</span>
282
+ </div>
283
+ <div className="font-mono text-[12px] leading-relaxed">
284
+ {hunks.map((hunk, hi) => (
285
+ <React.Fragment key={hi}>
286
+ {hunk.lines.filter((l) => l.type !== "removed").map((line, li) => (
287
+ <div
288
+ key={`${hi}-${li}`}
289
+ className={cn(
290
+ "px-3 py-0.5 flex",
291
+ line.type === "added" && "bg-emerald-100/60 dark:bg-emerald-500/10",
292
+ line.type === "modified" && "bg-amber-100/60 dark:bg-amber-500/10"
293
+ )}
294
+ >
295
+ <span className="w-8 text-right pr-2 text-muted-foreground/50 select-none shrink-0">
296
+ {line.newLineNumber}
297
+ </span>
298
+ <span className={cn(
299
+ line.type === "added" && "text-emerald-800 dark:text-emerald-300",
300
+ line.type === "modified" && "text-amber-800 dark:text-amber-300"
301
+ )}>
302
+ {line.type === "added" && <span className="text-emerald-500 mr-1">+</span>}
303
+ {line.content}
304
+ </span>
305
+ </div>
306
+ ))}
307
+ </React.Fragment>
308
+ ))}
309
+ </div>
310
+ </div>
311
+ </div>
312
+ ) : (
313
+ /* Unified view */
314
+ <div className="font-mono text-[12px] leading-relaxed">
315
+ {hunks.map((hunk, hi) => (
316
+ <React.Fragment key={hi}>
317
+ {/* Hunk header */}
318
+ <div className="px-3 py-1 bg-blue-50/50 dark:bg-blue-950/10 text-blue-600 dark:text-blue-400 text-[10px]">
319
+ @@ -{hunk.oldStart} +{hunk.newStart} @@
320
+ </div>
321
+ {hunk.lines.map((line, li) => (
322
+ <div
323
+ key={`${hi}-${li}`}
324
+ className={cn(
325
+ "px-3 py-0.5 flex",
326
+ line.type === "added" && "bg-emerald-100/60 dark:bg-emerald-500/10",
327
+ line.type === "removed" && "bg-red-100/60 dark:bg-red-500/10",
328
+ line.type === "modified" && "bg-amber-100/60 dark:bg-amber-500/10"
329
+ )}
330
+ >
331
+ <span className="w-8 text-right pr-2 text-muted-foreground/50 select-none shrink-0">
332
+ {line.oldLineNumber || ""}
333
+ </span>
334
+ <span className="w-8 text-right pr-2 text-muted-foreground/50 select-none shrink-0">
335
+ {line.newLineNumber || ""}
336
+ </span>
337
+ <span className="w-4 shrink-0 text-center select-none">
338
+ {line.type === "added" ? (
339
+ <span className="text-emerald-500">+</span>
340
+ ) : line.type === "removed" ? (
341
+ <span className="text-red-500">-</span>
342
+ ) : line.type === "modified" ? (
343
+ <span className="text-amber-500">~</span>
344
+ ) : (
345
+ <span className="text-muted-foreground/30"> </span>
346
+ )}
347
+ </span>
348
+ <span className={cn(
349
+ line.type === "added" && "text-emerald-800 dark:text-emerald-300",
350
+ line.type === "removed" && "text-red-800 dark:text-red-300 line-through",
351
+ line.type === "modified" && "text-amber-800 dark:text-amber-300"
352
+ )}>
353
+ {line.content}
354
+ </span>
355
+ </div>
356
+ ))}
357
+ </React.Fragment>
358
+ ))}
359
+ {hunks.length === 0 && (
360
+ <div className="py-8 text-center text-sm text-muted-foreground italic">
361
+ No differences found.
362
+ </div>
363
+ )}
364
+ </div>
365
+ )}
366
+ </div>
367
+ )}
368
+
369
+ {/* Hidden Slate children */}
370
+ <div className="hidden">{children}</div>
371
+ </div>
372
+ )
373
+ }
374
+
375
+ WakaVersionDiffBlock.displayName = "WakaVersionDiffBlock"
376
+
377
+ // ============================================================================
378
+ // Node Factory
379
+ // ============================================================================
380
+
381
+ export function createVersionDiffNodes(options?: Partial<Omit<VersionDiffElement, "type" | "children">>): VersionDiffElement[] {
382
+ return [
383
+ {
384
+ type: VERSION_DIFF_BLOCK_TYPE,
385
+ oldVersionLabel: options?.oldVersionLabel || "v1.0",
386
+ newVersionLabel: options?.newVersionLabel || "v1.1",
387
+ oldText: options?.oldText || "",
388
+ newText: options?.newText || "",
389
+ hunks: options?.hunks || [
390
+ {
391
+ oldStart: 1,
392
+ newStart: 1,
393
+ lines: [
394
+ { content: "The system SHALL authenticate users via OAuth2.", type: "unchanged", oldLineNumber: 1, newLineNumber: 1 },
395
+ { content: "Rate limiting: 50 req/min per user.", type: "removed", oldLineNumber: 2 },
396
+ { content: "Rate limiting: 100 req/min per user.", type: "added", newLineNumber: 2 },
397
+ { content: "All API responses use JSON format.", type: "unchanged", oldLineNumber: 3, newLineNumber: 3 },
398
+ { content: "Added: Support for WebSocket real-time events.", type: "added", newLineNumber: 4 },
399
+ ],
400
+ },
401
+ ],
402
+ stats: options?.stats || { additions: 2, deletions: 1, modifications: 0 },
403
+ author: options?.author,
404
+ timestamp: options?.timestamp,
405
+ children: [{ text: "" }],
406
+ },
407
+ ]
408
+ }
409
+
410
+ export async function createVersionDiffPlugin() {
411
+ try {
412
+ const { createPlatePlugin } = await import("platejs/react")
413
+ return createPlatePlugin({
414
+ key: VERSION_DIFF_BLOCK_TYPE,
415
+ node: {
416
+ isElement: true,
417
+ isVoid: true,
418
+ type: VERSION_DIFF_BLOCK_TYPE,
419
+ component: WakaVersionDiffBlock,
420
+ },
421
+ })
422
+ } catch {
423
+ console.warn("[WakaVersionDiffBlock] platejs not installed")
424
+ return null
425
+ }
426
+ }