@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,284 @@
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 AI_ASSIST_BLOCK_TYPE = "ai_assist" as const
12
+
13
+ /** Status of the AI request */
14
+ export type AiAssistStatus = "idle" | "loading" | "streaming" | "complete" | "error"
15
+
16
+ /** Slate node for AI assist blocks */
17
+ export interface AiAssistElement {
18
+ type: typeof AI_ASSIST_BLOCK_TYPE
19
+ /** The user's prompt/question */
20
+ prompt: string
21
+ /** The AI-generated response */
22
+ response: string
23
+ /** Current status */
24
+ status: AiAssistStatus
25
+ /** Model used for generation */
26
+ model?: string
27
+ /** Timestamp of generation */
28
+ generatedAt?: string
29
+ /** Error message if status is "error" */
30
+ errorMessage?: string
31
+ /** Token count */
32
+ tokenCount?: number
33
+ children: Array<{ text: string }>
34
+ }
35
+
36
+ export interface WakaAiAssistBlockProps extends PlateElementProps {
37
+ element?: AiAssistElement & Record<string, unknown>
38
+ /** Callback to trigger AI generation. Receives the prompt, returns a response or streams it. */
39
+ onGenerate?: (prompt: string) => Promise<string>
40
+ /** Callback when the user accepts the response and wants it inserted into the document */
41
+ onAccept?: (response: string) => void
42
+ /** Callback when the user discards the response */
43
+ onDiscard?: () => void
44
+ }
45
+
46
+ // ============================================================================
47
+ // Status Indicator
48
+ // ============================================================================
49
+
50
+ function StatusIndicator({ status }: { status: AiAssistStatus }) {
51
+ const config: Record<AiAssistStatus, { label: string; color: string; pulse: boolean }> = {
52
+ idle: { label: "Ready", color: "bg-muted-foreground/30", pulse: false },
53
+ loading: { label: "Thinking...", color: "bg-amber-500", pulse: true },
54
+ streaming: { label: "Writing...", color: "bg-blue-500", pulse: true },
55
+ complete: { label: "Complete", color: "bg-emerald-500", pulse: false },
56
+ error: { label: "Error", color: "bg-destructive", pulse: false },
57
+ }
58
+
59
+ const c = config[status]
60
+
61
+ return (
62
+ <div className="flex items-center gap-1.5">
63
+ <div className={cn("h-2 w-2 rounded-full", c.color, c.pulse && "animate-pulse")} />
64
+ <span className="text-[10px] font-medium text-muted-foreground">{c.label}</span>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ // ============================================================================
70
+ // Element Component
71
+ // ============================================================================
72
+
73
+ /**
74
+ * WakaAiAssistBlock - A Plate.js block where the user poses a question or prompt,
75
+ * and the AI response is inserted directly into the document. Supports streaming
76
+ * responses, accept/discard actions, and model attribution.
77
+ *
78
+ * Designed to work with `@platejs/ai` AIChatPlugin, or standalone with a custom
79
+ * `onGenerate` callback.
80
+ *
81
+ * Register in Plate editor:
82
+ * ```ts
83
+ * components: {
84
+ * [AI_ASSIST_BLOCK_TYPE]: (props) => <WakaAiAssistBlock {...props} onGenerate={myHandler} />,
85
+ * }
86
+ * ```
87
+ */
88
+ export function WakaAiAssistBlock({
89
+ attributes,
90
+ children,
91
+ element,
92
+ className,
93
+ onGenerate,
94
+ onAccept,
95
+ onDiscard,
96
+ }: WakaAiAssistBlockProps) {
97
+ const el = element as AiAssistElement | undefined
98
+ const status = el?.status || "idle"
99
+ const prompt = el?.prompt || ""
100
+ const response = el?.response || ""
101
+
102
+ return (
103
+ <div
104
+ {...attributes}
105
+ contentEditable={false}
106
+ className={cn(
107
+ "my-4 rounded-lg overflow-hidden",
108
+ "border border-border",
109
+ "bg-gradient-to-br from-violet-50/30 via-card to-blue-50/30",
110
+ "dark:from-violet-950/10 dark:via-card dark:to-blue-950/10",
111
+ "shadow-sm",
112
+ className
113
+ )}
114
+ >
115
+ {/* Header */}
116
+ <div className="flex items-center justify-between px-4 py-2.5 border-b border-border/50">
117
+ <div className="flex items-center gap-2">
118
+ {/* Sparkle icon */}
119
+ <div className="relative">
120
+ <svg
121
+ className="h-4 w-4 text-violet-600 dark:text-violet-400"
122
+ viewBox="0 0 24 24"
123
+ fill="currentColor"
124
+ aria-hidden="true"
125
+ >
126
+ <path d="M12 2L13.09 8.26L18 6L14.74 10.91L21 12L14.74 13.09L18 18L13.09 15.74L12 22L10.91 15.74L6 18L9.26 13.09L3 12L9.26 10.91L6 6L10.91 8.26L12 2Z" />
127
+ </svg>
128
+ {(status === "loading" || status === "streaming") && (
129
+ <div className="absolute inset-0 animate-ping">
130
+ <svg className="h-4 w-4 text-violet-400/50" viewBox="0 0 24 24" fill="currentColor">
131
+ <path d="M12 2L13.09 8.26L18 6L14.74 10.91L21 12L14.74 13.09L18 18L13.09 15.74L12 22L10.91 15.74L6 18L9.26 13.09L3 12L9.26 10.91L6 6L10.91 8.26L12 2Z" />
132
+ </svg>
133
+ </div>
134
+ )}
135
+ </div>
136
+ <span className="text-xs font-bold uppercase tracking-wider text-violet-700 dark:text-violet-300">
137
+ AI Assistant
138
+ </span>
139
+ {el?.model && (
140
+ <span className="text-[10px] font-mono text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
141
+ {el.model}
142
+ </span>
143
+ )}
144
+ </div>
145
+
146
+ <StatusIndicator status={status} />
147
+ </div>
148
+
149
+ {/* Prompt */}
150
+ <div className="px-4 py-3 border-b border-border/30">
151
+ <div className="flex items-start gap-2">
152
+ <div className="flex-shrink-0 mt-0.5 h-5 w-5 rounded-full bg-foreground/10 flex items-center justify-center">
153
+ <svg className="h-3 w-3 text-foreground/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
154
+ <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" />
155
+ </svg>
156
+ </div>
157
+ <div className="flex-1 min-w-0">
158
+ <p className="text-sm text-foreground leading-relaxed">
159
+ {prompt || <span className="text-muted-foreground italic">Ask the AI assistant a question...</span>}
160
+ </p>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ {/* Response */}
166
+ {(response || status === "loading" || status === "streaming") && (
167
+ <div className="px-4 py-3">
168
+ <div className="flex items-start gap-2">
169
+ <div className="flex-shrink-0 mt-0.5 h-5 w-5 rounded-full bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">
170
+ <svg className="h-3 w-3 text-violet-600 dark:text-violet-400" viewBox="0 0 24 24" fill="currentColor">
171
+ <path d="M12 2L13.09 8.26L18 6L14.74 10.91L21 12L14.74 13.09L18 18L13.09 15.74L12 22L10.91 15.74L6 18L9.26 13.09L3 12L9.26 10.91L6 6L10.91 8.26L12 2Z" />
172
+ </svg>
173
+ </div>
174
+ <div className="flex-1 min-w-0">
175
+ {status === "loading" && !response ? (
176
+ <div className="space-y-2">
177
+ <div className="h-3 w-3/4 bg-muted rounded animate-pulse" />
178
+ <div className="h-3 w-1/2 bg-muted rounded animate-pulse" />
179
+ <div className="h-3 w-5/6 bg-muted rounded animate-pulse" />
180
+ </div>
181
+ ) : (
182
+ <div className="text-sm text-foreground leading-relaxed whitespace-pre-wrap prose prose-sm dark:prose-invert max-w-none">
183
+ {response}
184
+ {status === "streaming" && (
185
+ <span className="inline-block w-1.5 h-4 bg-violet-500 ml-0.5 animate-pulse rounded-sm" />
186
+ )}
187
+ </div>
188
+ )}
189
+ </div>
190
+ </div>
191
+ </div>
192
+ )}
193
+
194
+ {/* Error */}
195
+ {status === "error" && el?.errorMessage && (
196
+ <div className="mx-4 mb-3 px-3 py-2 rounded-md bg-destructive/10 border border-destructive/20">
197
+ <p className="text-xs text-destructive">{el.errorMessage}</p>
198
+ </div>
199
+ )}
200
+
201
+ {/* Actions */}
202
+ {status === "complete" && response && (
203
+ <div className="flex items-center justify-between px-4 py-2 border-t border-border/50 bg-muted/20">
204
+ <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
205
+ {el?.tokenCount && <span>{el.tokenCount} tokens</span>}
206
+ {el?.generatedAt && (
207
+ <span>
208
+ {new Date(el.generatedAt).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}
209
+ </span>
210
+ )}
211
+ </div>
212
+ <div className="flex items-center gap-2">
213
+ {onDiscard && (
214
+ <button
215
+ type="button"
216
+ onClick={onDiscard}
217
+ className="px-3 py-1 rounded text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
218
+ >
219
+ Discard
220
+ </button>
221
+ )}
222
+ {onAccept && (
223
+ <button
224
+ type="button"
225
+ onClick={() => onAccept(response)}
226
+ className="px-3 py-1 rounded text-xs font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-sm"
227
+ >
228
+ Insert into document
229
+ </button>
230
+ )}
231
+ </div>
232
+ </div>
233
+ )}
234
+
235
+ {/* Hidden Slate children */}
236
+ <div className="hidden">{children}</div>
237
+ </div>
238
+ )
239
+ }
240
+
241
+ WakaAiAssistBlock.displayName = "WakaAiAssistBlock"
242
+
243
+ // ============================================================================
244
+ // Node Factory
245
+ // ============================================================================
246
+
247
+ export function createAiAssistNodes(options?: {
248
+ prompt?: string
249
+ model?: string
250
+ }): AiAssistElement[] {
251
+ return [
252
+ {
253
+ type: AI_ASSIST_BLOCK_TYPE,
254
+ prompt: options?.prompt || "",
255
+ response: "",
256
+ status: "idle",
257
+ model: options?.model || "claude-sonnet-4-20250514",
258
+ children: [{ text: "" }],
259
+ },
260
+ ]
261
+ }
262
+
263
+ export async function createAiAssistPlugin(
264
+ onGenerate?: (prompt: string) => Promise<string>,
265
+ onAccept?: (response: string) => void,
266
+ ) {
267
+ try {
268
+ const { createPlatePlugin } = await import("platejs/react")
269
+ return createPlatePlugin({
270
+ key: AI_ASSIST_BLOCK_TYPE,
271
+ node: {
272
+ isElement: true,
273
+ isVoid: true,
274
+ type: AI_ASSIST_BLOCK_TYPE,
275
+ component: (props: WakaAiAssistBlockProps) => (
276
+ <WakaAiAssistBlock {...props} onGenerate={onGenerate} onAccept={onAccept} />
277
+ ),
278
+ },
279
+ })
280
+ } catch {
281
+ console.warn("[WakaAiAssistBlock] platejs not installed")
282
+ return null
283
+ }
284
+ }
@@ -0,0 +1,382 @@
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 API_ENDPOINT_BLOCK_TYPE = "api_endpoint" as const
12
+
13
+ /** HTTP method type */
14
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"
15
+
16
+ /** A single parameter/header definition */
17
+ export interface ApiParam {
18
+ name: string
19
+ type: string
20
+ required: boolean
21
+ description: string
22
+ }
23
+
24
+ /** A response definition */
25
+ export interface ApiResponse {
26
+ status: number
27
+ description: string
28
+ body?: string
29
+ }
30
+
31
+ /** Slate node for API endpoint block */
32
+ export interface ApiEndpointElement {
33
+ type: typeof API_ENDPOINT_BLOCK_TYPE
34
+ /** HTTP method */
35
+ method: HttpMethod
36
+ /** Route path (e.g. "/api/v1/projects/:id") */
37
+ route: string
38
+ /** Endpoint description */
39
+ description: string
40
+ /** Authentication requirement */
41
+ auth: string
42
+ /** Required permissions/roles */
43
+ permissions: string[]
44
+ /** Rate limit description */
45
+ rateLimit?: string
46
+ /** Path/query parameters */
47
+ params?: ApiParam[]
48
+ /** Request body (JSON string) */
49
+ requestBody?: string
50
+ /** Response definitions */
51
+ responses: ApiResponse[]
52
+ children: Array<{ text: string }>
53
+ }
54
+
55
+ export interface WakaApiEndpointBlockProps extends PlateElementProps {
56
+ element?: ApiEndpointElement & Record<string, unknown>
57
+ }
58
+
59
+ // ============================================================================
60
+ // Method Badge
61
+ // ============================================================================
62
+
63
+ const METHOD_COLORS: Record<HttpMethod, { bg: string; text: string; border: string }> = {
64
+ GET: { bg: "bg-emerald-100 dark:bg-emerald-500/15", text: "text-emerald-700 dark:text-emerald-400", border: "border-emerald-300 dark:border-emerald-600/40" },
65
+ POST: { bg: "bg-blue-100 dark:bg-blue-500/15", text: "text-blue-700 dark:text-blue-400", border: "border-blue-300 dark:border-blue-600/40" },
66
+ PUT: { bg: "bg-amber-100 dark:bg-amber-500/15", text: "text-amber-700 dark:text-amber-400", border: "border-amber-300 dark:border-amber-600/40" },
67
+ PATCH: { bg: "bg-orange-100 dark:bg-orange-500/15", text: "text-orange-700 dark:text-orange-400", border: "border-orange-300 dark:border-orange-600/40" },
68
+ DELETE: { bg: "bg-red-100 dark:bg-red-500/15", text: "text-red-700 dark:text-red-400", border: "border-red-300 dark:border-red-600/40" },
69
+ HEAD: { bg: "bg-gray-100 dark:bg-gray-500/15", text: "text-gray-700 dark:text-gray-400", border: "border-gray-300 dark:border-gray-600/40" },
70
+ OPTIONS: { bg: "bg-purple-100 dark:bg-purple-500/15", text: "text-purple-700 dark:text-purple-400", border: "border-purple-300 dark:border-purple-600/40" },
71
+ }
72
+
73
+ function MethodBadge({ method }: { method: HttpMethod }) {
74
+ const colors = METHOD_COLORS[method] || METHOD_COLORS.GET
75
+ return (
76
+ <span className={cn(
77
+ "inline-flex items-center px-2 py-0.5 rounded text-[11px] font-bold font-mono tracking-wide border",
78
+ colors.bg, colors.text, colors.border
79
+ )}>
80
+ {method}
81
+ </span>
82
+ )
83
+ }
84
+
85
+ // ============================================================================
86
+ // Status Badge
87
+ // ============================================================================
88
+
89
+ function StatusBadge({ status }: { status: number }) {
90
+ const color = status < 300
91
+ ? "text-emerald-700 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10"
92
+ : status < 400
93
+ ? "text-blue-700 dark:text-blue-400 bg-blue-50 dark:bg-blue-500/10"
94
+ : status < 500
95
+ ? "text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10"
96
+ : "text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10"
97
+
98
+ return (
99
+ <span className={cn("text-[11px] font-mono font-bold px-1.5 py-0.5 rounded", color)}>
100
+ {status}
101
+ </span>
102
+ )
103
+ }
104
+
105
+ // ============================================================================
106
+ // JSON Code Block
107
+ // ============================================================================
108
+
109
+ function CodePreview({ code, label }: { code: string; label: string }) {
110
+ const [expanded, setExpanded] = React.useState(false)
111
+ const lines = code.split("\n")
112
+ const isLong = lines.length > 6
113
+
114
+ return (
115
+ <div className="mt-2">
116
+ <div className="flex items-center justify-between mb-1">
117
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
118
+ {label}
119
+ </span>
120
+ {isLong && (
121
+ <button
122
+ type="button"
123
+ onClick={() => setExpanded(!expanded)}
124
+ className="text-[10px] text-primary hover:underline"
125
+ >
126
+ {expanded ? "Collapse" : "Expand"}
127
+ </button>
128
+ )}
129
+ </div>
130
+ <pre className={cn(
131
+ "text-[12px] font-mono leading-relaxed p-3 rounded-md overflow-x-auto",
132
+ "bg-gray-950 dark:bg-gray-900 text-gray-100",
133
+ "border border-gray-800/50",
134
+ !expanded && isLong && "max-h-[150px] overflow-hidden relative"
135
+ )}>
136
+ {!expanded && isLong && (
137
+ <div className="absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-gray-950 dark:from-gray-900 to-transparent" />
138
+ )}
139
+ <code>{code}</code>
140
+ </pre>
141
+ </div>
142
+ )
143
+ }
144
+
145
+ // ============================================================================
146
+ // Element Component
147
+ // ============================================================================
148
+
149
+ /**
150
+ * WakaApiEndpointBlock - A Plate.js block for documenting REST API endpoints.
151
+ * Displays method, route, parameters, request body, and responses with
152
+ * syntax-colored JSON previews.
153
+ *
154
+ * Register in Plate editor:
155
+ * ```ts
156
+ * components: {
157
+ * [API_ENDPOINT_BLOCK_TYPE]: WakaApiEndpointBlock,
158
+ * }
159
+ * ```
160
+ */
161
+ export function WakaApiEndpointBlock({
162
+ attributes,
163
+ children,
164
+ element,
165
+ className,
166
+ }: WakaApiEndpointBlockProps) {
167
+ const el = element as ApiEndpointElement | undefined
168
+ const method = el?.method || "GET"
169
+ const route = el?.route || "/api/v1/resource"
170
+ const methodColors = METHOD_COLORS[method] || METHOD_COLORS.GET
171
+
172
+ const [activeTab, setActiveTab] = React.useState<"params" | "body" | "responses">(
173
+ el?.params && el.params.length > 0 ? "params" : el?.requestBody ? "body" : "responses"
174
+ )
175
+
176
+ return (
177
+ <div
178
+ {...attributes}
179
+ contentEditable={false}
180
+ className={cn(
181
+ "my-4 rounded-lg overflow-hidden",
182
+ "border",
183
+ methodColors.border,
184
+ "shadow-sm",
185
+ className
186
+ )}
187
+ >
188
+ {/* Method + Route header */}
189
+ <div className={cn(
190
+ "flex items-center gap-3 px-4 py-3",
191
+ methodColors.bg
192
+ )}>
193
+ <MethodBadge method={method} />
194
+ <code className={cn("text-sm font-mono font-semibold", methodColors.text)}>
195
+ {route}
196
+ </code>
197
+ </div>
198
+
199
+ {/* Description + meta */}
200
+ <div className="px-4 py-3 border-b border-border/50 bg-card/50">
201
+ {el?.description && (
202
+ <p className="text-sm text-foreground mb-2">{el.description}</p>
203
+ )}
204
+
205
+ <div className="flex flex-wrap gap-3 text-[11px]">
206
+ {el?.auth && (
207
+ <div className="flex items-center gap-1 text-muted-foreground">
208
+ <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
209
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
210
+ </svg>
211
+ <span className="font-medium">{el.auth}</span>
212
+ </div>
213
+ )}
214
+
215
+ {el?.permissions && el.permissions.length > 0 && (
216
+ <div className="flex items-center gap-1">
217
+ <svg className="h-3 w-3 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
218
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
219
+ </svg>
220
+ {el.permissions.map((perm, i) => (
221
+ <span key={i} className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground font-mono text-[10px]">
222
+ {perm}
223
+ </span>
224
+ ))}
225
+ </div>
226
+ )}
227
+
228
+ {el?.rateLimit && (
229
+ <div className="flex items-center gap-1 text-muted-foreground">
230
+ <svg className="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
231
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
232
+ </svg>
233
+ <span className="font-medium">{el.rateLimit}</span>
234
+ </div>
235
+ )}
236
+ </div>
237
+ </div>
238
+
239
+ {/* Tabs */}
240
+ <div className="flex border-b border-border/50">
241
+ {[
242
+ { id: "params" as const, label: "Parameters", count: el?.params?.length },
243
+ { id: "body" as const, label: "Request Body", show: !!el?.requestBody },
244
+ { id: "responses" as const, label: "Responses", count: el?.responses?.length },
245
+ ].filter((tab) => tab.show !== false && (tab.count === undefined || tab.count > 0)).map((tab) => (
246
+ <button
247
+ key={tab.id}
248
+ type="button"
249
+ onClick={() => setActiveTab(tab.id)}
250
+ className={cn(
251
+ "px-4 py-2 text-xs font-medium transition-colors relative",
252
+ activeTab === tab.id
253
+ ? "text-foreground"
254
+ : "text-muted-foreground hover:text-foreground"
255
+ )}
256
+ >
257
+ {tab.label}
258
+ {tab.count !== undefined && (
259
+ <span className="ml-1 text-[10px] text-muted-foreground">({tab.count})</span>
260
+ )}
261
+ {activeTab === tab.id && (
262
+ <div className={cn("absolute bottom-0 left-0 right-0 h-0.5", methodColors.bg.replace("bg-", "bg-").replace("/15", ""))} style={{ backgroundColor: method === "GET" ? "#10b981" : method === "POST" ? "#3b82f6" : method === "PUT" ? "#f59e0b" : method === "DELETE" ? "#ef4444" : "#6b7280" }} />
263
+ )}
264
+ </button>
265
+ ))}
266
+ </div>
267
+
268
+ {/* Tab content */}
269
+ <div className="px-4 py-3">
270
+ {/* Parameters tab */}
271
+ {activeTab === "params" && el?.params && el.params.length > 0 && (
272
+ <div className="overflow-x-auto">
273
+ <table className="w-full text-xs">
274
+ <thead>
275
+ <tr className="border-b border-border">
276
+ <th className="text-left font-semibold text-muted-foreground py-1.5 pr-4">Name</th>
277
+ <th className="text-left font-semibold text-muted-foreground py-1.5 pr-4">Type</th>
278
+ <th className="text-left font-semibold text-muted-foreground py-1.5 pr-4">Required</th>
279
+ <th className="text-left font-semibold text-muted-foreground py-1.5">Description</th>
280
+ </tr>
281
+ </thead>
282
+ <tbody>
283
+ {el.params.map((param, i) => (
284
+ <tr key={i} className="border-b border-border/30">
285
+ <td className="py-1.5 pr-4 font-mono font-medium text-foreground">{param.name}</td>
286
+ <td className="py-1.5 pr-4 font-mono text-muted-foreground">{param.type}</td>
287
+ <td className="py-1.5 pr-4">
288
+ {param.required ? (
289
+ <span className="text-red-600 dark:text-red-400 font-semibold">required</span>
290
+ ) : (
291
+ <span className="text-muted-foreground">optional</span>
292
+ )}
293
+ </td>
294
+ <td className="py-1.5 text-muted-foreground">{param.description}</td>
295
+ </tr>
296
+ ))}
297
+ </tbody>
298
+ </table>
299
+ </div>
300
+ )}
301
+
302
+ {/* Request body tab */}
303
+ {activeTab === "body" && el?.requestBody && (
304
+ <CodePreview code={el.requestBody} label="Request Body" />
305
+ )}
306
+
307
+ {/* Responses tab */}
308
+ {activeTab === "responses" && el?.responses && (
309
+ <div className="space-y-3">
310
+ {el.responses.map((resp, i) => (
311
+ <div key={i}>
312
+ <div className="flex items-center gap-2 mb-1">
313
+ <StatusBadge status={resp.status} />
314
+ <span className="text-xs text-muted-foreground">{resp.description}</span>
315
+ </div>
316
+ {resp.body && <CodePreview code={resp.body} label={`Response ${resp.status}`} />}
317
+ </div>
318
+ ))}
319
+ </div>
320
+ )}
321
+ </div>
322
+
323
+ {/* Hidden Slate children */}
324
+ <div className="hidden">{children}</div>
325
+ </div>
326
+ )
327
+ }
328
+
329
+ WakaApiEndpointBlock.displayName = "WakaApiEndpointBlock"
330
+
331
+ // ============================================================================
332
+ // Node Factory
333
+ // ============================================================================
334
+
335
+ export function createApiEndpointNodes(options?: Partial<Omit<ApiEndpointElement, "type" | "children">>): ApiEndpointElement[] {
336
+ return [
337
+ {
338
+ type: API_ENDPOINT_BLOCK_TYPE,
339
+ method: options?.method || "GET",
340
+ route: options?.route || "/api/v1/resource/:id",
341
+ description: options?.description || "Retrieves a resource by its unique identifier.",
342
+ auth: options?.auth || "Bearer JWT",
343
+ permissions: options?.permissions || ["ROLE_USER"],
344
+ rateLimit: options?.rateLimit || "100 req/min",
345
+ params: options?.params || [
346
+ { name: "id", type: "UUID", required: true, description: "Resource unique identifier" },
347
+ ],
348
+ requestBody: options?.requestBody,
349
+ responses: options?.responses || [
350
+ {
351
+ status: 200,
352
+ description: "Success",
353
+ body: '{\n "data": {\n "id": "uuid",\n "name": "Example",\n "createdAt": "2026-04-14T00:00:00Z"\n }\n}',
354
+ },
355
+ {
356
+ status: 404,
357
+ description: "Resource not found",
358
+ body: '{\n "error": "NOT_FOUND",\n "message": "Resource not found"\n}',
359
+ },
360
+ ],
361
+ children: [{ text: "" }],
362
+ },
363
+ ]
364
+ }
365
+
366
+ export async function createApiEndpointPlugin() {
367
+ try {
368
+ const { createPlatePlugin } = await import("platejs/react")
369
+ return createPlatePlugin({
370
+ key: API_ENDPOINT_BLOCK_TYPE,
371
+ node: {
372
+ isElement: true,
373
+ isVoid: true,
374
+ type: API_ENDPOINT_BLOCK_TYPE,
375
+ component: WakaApiEndpointBlock,
376
+ },
377
+ })
378
+ } catch {
379
+ console.warn("[WakaApiEndpointBlock] platejs not installed")
380
+ return null
381
+ }
382
+ }