@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,448 @@
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 COMMENT_THREAD_BLOCK_TYPE = "comment_thread" as const
12
+
13
+ /** A single comment in a thread */
14
+ export interface ThreadComment {
15
+ /** Unique comment ID */
16
+ id: string
17
+ /** Author information */
18
+ author: {
19
+ id: string
20
+ name: string
21
+ avatar?: string
22
+ }
23
+ /** Comment text content */
24
+ content: string
25
+ /** ISO timestamp */
26
+ createdAt: string
27
+ /** Whether this comment has been edited */
28
+ edited?: boolean
29
+ /** Reactions on this comment */
30
+ reactions?: Array<{ emoji: string; count: number; reacted: boolean }>
31
+ }
32
+
33
+ /** Thread status */
34
+ export type ThreadStatus = "open" | "resolved" | "archived"
35
+
36
+ /** Slate node for comment thread blocks */
37
+ export interface CommentThreadElement {
38
+ type: typeof COMMENT_THREAD_BLOCK_TYPE
39
+ /** Thread ID */
40
+ threadId: string
41
+ /** The original highlighted text that was commented on */
42
+ quotedText: string
43
+ /** Thread status */
44
+ status: ThreadStatus
45
+ /** Comments in the thread */
46
+ comments: ThreadComment[]
47
+ /** Total number of comments (may differ from comments.length if paginated) */
48
+ totalComments?: number
49
+ children: Array<{ text: string }>
50
+ }
51
+
52
+ export interface WakaCommentThreadBlockProps extends PlateElementProps {
53
+ element?: CommentThreadElement & Record<string, unknown>
54
+ /** Current user ID (for highlighting own comments) */
55
+ currentUserId?: string
56
+ /** Callback when a reply is submitted */
57
+ onReply?: (threadId: string, content: string) => void
58
+ /** Callback when thread is resolved */
59
+ onResolve?: (threadId: string) => void
60
+ /** Callback when thread is reopened */
61
+ onReopen?: (threadId: string) => void
62
+ }
63
+
64
+ // ============================================================================
65
+ // Relative Time
66
+ // ============================================================================
67
+
68
+ function relativeTime(isoDate: string): string {
69
+ const now = Date.now()
70
+ const then = new Date(isoDate).getTime()
71
+ const diff = now - then
72
+ const seconds = Math.floor(diff / 1000)
73
+
74
+ if (seconds < 60) return "just now"
75
+ const minutes = Math.floor(seconds / 60)
76
+ if (minutes < 60) return `${minutes}m ago`
77
+ const hours = Math.floor(minutes / 60)
78
+ if (hours < 24) return `${hours}h ago`
79
+ const days = Math.floor(hours / 24)
80
+ if (days < 30) return `${days}d ago`
81
+ const months = Math.floor(days / 30)
82
+ if (months < 12) return `${months}mo ago`
83
+ return `${Math.floor(months / 12)}y ago`
84
+ }
85
+
86
+ // ============================================================================
87
+ // Avatar Component
88
+ // ============================================================================
89
+
90
+ function CommentAvatar({ author, size = "sm" }: { author: ThreadComment["author"]; size?: "sm" | "md" }) {
91
+ const sizeClass = size === "sm" ? "h-6 w-6 text-[9px]" : "h-8 w-8 text-[10px]"
92
+
93
+ if (author.avatar) {
94
+ return (
95
+ <img
96
+ src={author.avatar}
97
+ alt={author.name}
98
+ className={cn("rounded-full object-cover flex-shrink-0", sizeClass)}
99
+ />
100
+ )
101
+ }
102
+
103
+ // Generate a deterministic color from the name
104
+ const colorIndex = author.name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0) % 6
105
+ const colors = [
106
+ "bg-blue-500", "bg-emerald-500", "bg-violet-500",
107
+ "bg-amber-500", "bg-rose-500", "bg-cyan-500",
108
+ ]
109
+
110
+ return (
111
+ <div className={cn(
112
+ "rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0",
113
+ sizeClass,
114
+ colors[colorIndex]
115
+ )}>
116
+ {author.name.charAt(0).toUpperCase()}
117
+ </div>
118
+ )
119
+ }
120
+
121
+ // ============================================================================
122
+ // Single Comment Component
123
+ // ============================================================================
124
+
125
+ interface CommentItemProps {
126
+ comment: ThreadComment
127
+ isCurrentUser: boolean
128
+ isFirst: boolean
129
+ }
130
+
131
+ function CommentItem({ comment, isCurrentUser, isFirst }: CommentItemProps) {
132
+ return (
133
+ <div className={cn(
134
+ "flex gap-2.5 px-3 py-2",
135
+ !isFirst && "border-t border-border/30"
136
+ )}>
137
+ <CommentAvatar author={comment.author} />
138
+ <div className="flex-1 min-w-0">
139
+ <div className="flex items-center gap-2">
140
+ <span className={cn(
141
+ "text-xs font-semibold",
142
+ isCurrentUser ? "text-primary" : "text-foreground"
143
+ )}>
144
+ {comment.author.name}
145
+ </span>
146
+ <span className="text-[10px] text-muted-foreground">
147
+ {relativeTime(comment.createdAt)}
148
+ </span>
149
+ {comment.edited && (
150
+ <span className="text-[9px] text-muted-foreground/60 italic">(edited)</span>
151
+ )}
152
+ </div>
153
+ <p className="text-xs text-foreground/80 mt-0.5 leading-relaxed whitespace-pre-wrap">
154
+ {comment.content}
155
+ </p>
156
+
157
+ {/* Reactions */}
158
+ {comment.reactions && comment.reactions.length > 0 && (
159
+ <div className="flex flex-wrap gap-1 mt-1.5">
160
+ {comment.reactions.map((reaction, i) => (
161
+ <button
162
+ key={i}
163
+ type="button"
164
+ className={cn(
165
+ "inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-[10px] border transition-colors",
166
+ reaction.reacted
167
+ ? "bg-primary/10 border-primary/30 text-primary"
168
+ : "bg-muted/50 border-border/50 text-muted-foreground hover:bg-muted"
169
+ )}
170
+ >
171
+ <span>{reaction.emoji}</span>
172
+ <span className="font-medium">{reaction.count}</span>
173
+ </button>
174
+ ))}
175
+ </div>
176
+ )}
177
+ </div>
178
+ </div>
179
+ )
180
+ }
181
+
182
+ // ============================================================================
183
+ // Element Component
184
+ // ============================================================================
185
+
186
+ /**
187
+ * WakaCommentThreadBlock - A Plate.js block for inline threaded comments,
188
+ * similar to Google Docs commenting. Shows the quoted text that was commented on,
189
+ * a thread of replies, and allows adding new replies.
190
+ *
191
+ * Designed to integrate with `@platejs/comments` or work standalone.
192
+ *
193
+ * Register in Plate editor:
194
+ * ```ts
195
+ * components: {
196
+ * [COMMENT_THREAD_BLOCK_TYPE]: (props) => (
197
+ * <WakaCommentThreadBlock {...props} currentUserId="user-123" onReply={handleReply} />
198
+ * ),
199
+ * }
200
+ * ```
201
+ */
202
+ export function WakaCommentThreadBlock({
203
+ attributes,
204
+ children,
205
+ element,
206
+ className,
207
+ currentUserId,
208
+ onReply,
209
+ onResolve,
210
+ onReopen,
211
+ }: WakaCommentThreadBlockProps) {
212
+ const el = element as CommentThreadElement | undefined
213
+ const threadId = el?.threadId || ""
214
+ const quotedText = el?.quotedText || ""
215
+ const status = el?.status || "open"
216
+ const comments = el?.comments || []
217
+ const totalComments = el?.totalComments ?? comments.length
218
+
219
+ const [replyText, setReplyText] = React.useState("")
220
+ const [isCollapsed, setIsCollapsed] = React.useState(status === "resolved")
221
+
222
+ const statusConfig = {
223
+ open: { label: "Open", color: "text-blue-600 dark:text-blue-400", bg: "bg-blue-50 dark:bg-blue-500/10" },
224
+ resolved: { label: "Resolved", color: "text-emerald-600 dark:text-emerald-400", bg: "bg-emerald-50 dark:bg-emerald-500/10" },
225
+ archived: { label: "Archived", color: "text-muted-foreground", bg: "bg-muted" },
226
+ }
227
+
228
+ const handleSubmitReply = () => {
229
+ if (!replyText.trim() || !onReply) return
230
+ onReply(threadId, replyText.trim())
231
+ setReplyText("")
232
+ }
233
+
234
+ return (
235
+ <div
236
+ {...attributes}
237
+ contentEditable={false}
238
+ className={cn(
239
+ "my-3 rounded-lg overflow-hidden border shadow-sm transition-all duration-200",
240
+ status === "resolved"
241
+ ? "border-emerald-200 dark:border-emerald-800/30"
242
+ : "border-primary/20",
243
+ className
244
+ )}
245
+ >
246
+ {/* Header */}
247
+ <div
248
+ className={cn(
249
+ "flex items-center justify-between px-3 py-2 cursor-pointer",
250
+ status === "resolved"
251
+ ? "bg-emerald-50/50 dark:bg-emerald-950/10"
252
+ : "bg-primary/5"
253
+ )}
254
+ onClick={() => setIsCollapsed(!isCollapsed)}
255
+ role="button"
256
+ tabIndex={0}
257
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") setIsCollapsed(!isCollapsed) }}
258
+ aria-expanded={!isCollapsed}
259
+ >
260
+ <div className="flex items-center gap-2">
261
+ {/* Comment icon */}
262
+ <svg
263
+ className={cn("h-4 w-4", status === "resolved" ? "text-emerald-500" : "text-primary")}
264
+ viewBox="0 0 24 24"
265
+ fill="none"
266
+ stroke="currentColor"
267
+ strokeWidth={2}
268
+ aria-hidden="true"
269
+ >
270
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
271
+ </svg>
272
+
273
+ {/* Status badge */}
274
+ <span className={cn(
275
+ "text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded",
276
+ statusConfig[status].bg,
277
+ statusConfig[status].color
278
+ )}>
279
+ {statusConfig[status].label}
280
+ </span>
281
+
282
+ {/* Comment count */}
283
+ <span className="text-[10px] text-muted-foreground">
284
+ {totalComments} comment{totalComments !== 1 ? "s" : ""}
285
+ </span>
286
+ </div>
287
+
288
+ <div className="flex items-center gap-2">
289
+ {/* Resolve/reopen button */}
290
+ {status === "open" && onResolve && (
291
+ <button
292
+ type="button"
293
+ onClick={(e) => { e.stopPropagation(); onResolve(threadId) }}
294
+ className="text-[10px] font-medium text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 transition-colors"
295
+ >
296
+ Resolve
297
+ </button>
298
+ )}
299
+ {status === "resolved" && onReopen && (
300
+ <button
301
+ type="button"
302
+ onClick={(e) => { e.stopPropagation(); onReopen(threadId) }}
303
+ className="text-[10px] font-medium text-primary hover:text-primary/80 transition-colors"
304
+ >
305
+ Reopen
306
+ </button>
307
+ )}
308
+
309
+ {/* Collapse arrow */}
310
+ <svg
311
+ className={cn("h-3.5 w-3.5 text-muted-foreground transition-transform", isCollapsed && "-rotate-90")}
312
+ viewBox="0 0 24 24"
313
+ fill="none"
314
+ stroke="currentColor"
315
+ strokeWidth={2}
316
+ >
317
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
318
+ </svg>
319
+ </div>
320
+ </div>
321
+
322
+ {!isCollapsed && (
323
+ <>
324
+ {/* Quoted text */}
325
+ {quotedText && (
326
+ <div className="mx-3 mt-3 mb-1 px-3 py-2 rounded bg-muted/50 border-l-2 border-primary/30">
327
+ <p className="text-xs text-muted-foreground italic leading-relaxed line-clamp-3">
328
+ "{quotedText}"
329
+ </p>
330
+ </div>
331
+ )}
332
+
333
+ {/* Comments */}
334
+ <div className="py-1">
335
+ {comments.map((comment, i) => (
336
+ <CommentItem
337
+ key={comment.id}
338
+ comment={comment}
339
+ isCurrentUser={comment.author.id === currentUserId}
340
+ isFirst={i === 0}
341
+ />
342
+ ))}
343
+ </div>
344
+
345
+ {/* Reply input */}
346
+ {status === "open" && onReply && (
347
+ <div className="px-3 pb-3 pt-1 border-t border-border/30">
348
+ <div className="flex gap-2">
349
+ <input
350
+ type="text"
351
+ value={replyText}
352
+ onChange={(e) => setReplyText(e.target.value)}
353
+ onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmitReply() } }}
354
+ placeholder="Reply..."
355
+ className={cn(
356
+ "flex-1 px-3 py-1.5 rounded-md text-xs bg-muted/50 border border-border/50",
357
+ "focus:outline-none focus:ring-1 focus:ring-ring",
358
+ "placeholder:text-muted-foreground/50"
359
+ )}
360
+ />
361
+ <button
362
+ type="button"
363
+ onClick={handleSubmitReply}
364
+ disabled={!replyText.trim()}
365
+ className={cn(
366
+ "px-3 py-1.5 rounded-md text-xs font-medium transition-colors",
367
+ "bg-primary text-primary-foreground hover:bg-primary/90",
368
+ "disabled:opacity-40 disabled:cursor-not-allowed"
369
+ )}
370
+ >
371
+ Reply
372
+ </button>
373
+ </div>
374
+ </div>
375
+ )}
376
+ </>
377
+ )}
378
+
379
+ {/* Hidden Slate children */}
380
+ <div className="hidden">{children}</div>
381
+ </div>
382
+ )
383
+ }
384
+
385
+ WakaCommentThreadBlock.displayName = "WakaCommentThreadBlock"
386
+
387
+ // ============================================================================
388
+ // Node Factory
389
+ // ============================================================================
390
+
391
+ export function createCommentThreadNodes(options?: {
392
+ threadId?: string
393
+ quotedText?: string
394
+ initialComment?: { authorId: string; authorName: string; content: string }
395
+ }): CommentThreadElement[] {
396
+ const now = new Date().toISOString()
397
+ return [
398
+ {
399
+ type: COMMENT_THREAD_BLOCK_TYPE,
400
+ threadId: options?.threadId || `thread-${Date.now()}`,
401
+ quotedText: options?.quotedText || "",
402
+ status: "open",
403
+ comments: options?.initialComment
404
+ ? [
405
+ {
406
+ id: `comment-${Date.now()}`,
407
+ author: {
408
+ id: options.initialComment.authorId,
409
+ name: options.initialComment.authorName,
410
+ },
411
+ content: options.initialComment.content,
412
+ createdAt: now,
413
+ },
414
+ ]
415
+ : [],
416
+ children: [{ text: "" }],
417
+ },
418
+ ]
419
+ }
420
+
421
+ export async function createCommentThreadPlugin(
422
+ currentUserId?: string,
423
+ onReply?: (threadId: string, content: string) => void,
424
+ onResolve?: (threadId: string) => void,
425
+ ) {
426
+ try {
427
+ const { createPlatePlugin } = await import("platejs/react")
428
+ return createPlatePlugin({
429
+ key: COMMENT_THREAD_BLOCK_TYPE,
430
+ node: {
431
+ isElement: true,
432
+ isVoid: true,
433
+ type: COMMENT_THREAD_BLOCK_TYPE,
434
+ component: (props: WakaCommentThreadBlockProps) => (
435
+ <WakaCommentThreadBlock
436
+ {...props}
437
+ currentUserId={currentUserId}
438
+ onReply={onReply}
439
+ onResolve={onResolve}
440
+ />
441
+ ),
442
+ },
443
+ })
444
+ } catch {
445
+ console.warn("[WakaCommentThreadBlock] platejs not installed")
446
+ return null
447
+ }
448
+ }