@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,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
|
+
}
|