@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,431 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Label } from "../label"
6
+ import { Textarea } from "../textarea"
7
+ import { WakaEditorToolbar, DEFAULT_TOOLBAR_GROUPS, type ToolbarGroup } from "./waka-editor-toolbar"
8
+ import type { WakaEditorPreset } from "./waka-editor-plugins"
9
+
10
+ // ─── Types ───────────────────────────────────────────────────────────────────
11
+
12
+ /**
13
+ * Slate/Plate node type. Using a generic type to avoid tight coupling
14
+ * with Plate internals while remaining compatible.
15
+ */
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ export type SlateNode = any
18
+
19
+ export interface WakaEditorProps {
20
+ /** Initial content as Slate value */
21
+ value?: SlateNode[]
22
+ /** Change callback with updated Slate value */
23
+ onChange?: (value: SlateNode[]) => void
24
+ /** Read-only mode */
25
+ readOnly?: boolean
26
+ /** Placeholder text */
27
+ placeholder?: string
28
+ /** Extra CSS class names on the root container */
29
+ className?: string
30
+ /** Extra CSS class names on the editable area */
31
+ editorClassName?: string
32
+ /** Minimum height of the editable area in px */
33
+ minHeight?: number
34
+ /** Label displayed above the editor */
35
+ label?: string
36
+ /** Description displayed below the label */
37
+ description?: string
38
+ /** Error message */
39
+ error?: string
40
+
41
+ // ── Plugin feature flags ──────────────────────────────────────────────────
42
+
43
+ /** Named preset: "minimal" | "standard" | "full" (default: "standard") */
44
+ preset?: WakaEditorPreset
45
+ /** Show AI assistant button in toolbar */
46
+ enableAI?: boolean
47
+ /** Enable Yjs collaboration (not yet implemented) */
48
+ enableCollaboration?: boolean
49
+ /** Enable inline comments (not yet implemented) */
50
+ enableComments?: boolean
51
+ /** Enable markdown import/export */
52
+ enableMarkdown?: boolean
53
+ /** Enable image and video embeds */
54
+ enableMedia?: boolean
55
+ /** Enable @mentions */
56
+ enableMentions?: boolean
57
+ /** Enable drag-and-drop reordering */
58
+ enableDnD?: boolean
59
+ /** Enable table of contents generation */
60
+ enableTOC?: boolean
61
+
62
+ // ── Toolbar ───────────────────────────────────────────────────────────────
63
+
64
+ /** Custom toolbar groups (overrides preset defaults) */
65
+ toolbarGroups?: ToolbarGroup[]
66
+ /** Hide the toolbar entirely */
67
+ hideToolbar?: boolean
68
+ /** Callback when AI button is clicked */
69
+ onAIClick?: () => void
70
+
71
+ // ── Advanced ──────────────────────────────────────────────────────────────
72
+
73
+ /** Extra Plate plugins to add on top of the preset */
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ extraPlugins?: any[]
76
+ /** Ref to the internal Plate editor instance */
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ editorRef?: React.MutableRefObject<any>
79
+ }
80
+
81
+ // ─── Plate component types (loaded dynamically) ─────────────────────────────
82
+
83
+ interface PlateComponents {
84
+ Plate: React.ComponentType<{
85
+ editor: unknown
86
+ children?: React.ReactNode
87
+ onChange?: (ctx: { value: SlateNode[] }) => void
88
+ readOnly?: boolean
89
+ }>
90
+ PlateContent: React.ComponentType<{
91
+ placeholder?: string
92
+ className?: string
93
+ readOnly?: boolean
94
+ }>
95
+ usePlateEditor: (config: unknown) => unknown
96
+ }
97
+
98
+ // ─── Main component ─────────────────────────────────────────────────────────
99
+
100
+ export const WakaEditor = React.forwardRef<HTMLDivElement, WakaEditorProps>(
101
+ (
102
+ {
103
+ value,
104
+ onChange,
105
+ readOnly = false,
106
+ placeholder = "Commencez a ecrire...",
107
+ className,
108
+ editorClassName,
109
+ minHeight = 200,
110
+ label,
111
+ description,
112
+ error,
113
+ preset = "standard",
114
+ enableAI = false,
115
+ enableMarkdown = false,
116
+ enableMedia = false,
117
+ enableMentions = false,
118
+ enableDnD = false,
119
+ enableTOC = false,
120
+ toolbarGroups = DEFAULT_TOOLBAR_GROUPS,
121
+ hideToolbar = false,
122
+ onAIClick,
123
+ extraPlugins,
124
+ editorRef,
125
+ },
126
+ ref
127
+ ) => {
128
+ const [plateState, setPlateState] = React.useState<{
129
+ components: PlateComponents
130
+ plugins: unknown[]
131
+ } | null>(null)
132
+ const [loadError, setLoadError] = React.useState(false)
133
+
134
+ // ── Load Plate and plugins asynchronously ─────────────────────────────
135
+ React.useEffect(() => {
136
+ let cancelled = false
137
+
138
+ const load = async () => {
139
+ try {
140
+ const [plateReact, pluginModule] = await Promise.all([
141
+ import("platejs/react"),
142
+ import("./waka-editor-plugins"),
143
+ ])
144
+
145
+ // Load core plugins
146
+ const corePlugins = await pluginModule.loadCorePlugins()
147
+
148
+ // Load preset-based optional plugins
149
+ const presetLoaders = pluginModule.getPresetLoaders(preset)
150
+ const optionalPluginSets = await Promise.all(
151
+ presetLoaders.map((loader) => loader())
152
+ )
153
+
154
+ // Load feature-flag plugins
155
+ const featureLoaders: Promise<unknown[]>[] = []
156
+ if (enableMarkdown) featureLoaders.push(pluginModule.loadMarkdownPlugin())
157
+ if (enableMedia) featureLoaders.push(pluginModule.loadMediaPlugins())
158
+ if (enableMentions) featureLoaders.push(pluginModule.loadMentionPlugins())
159
+ if (enableDnD) featureLoaders.push(pluginModule.loadDndPlugin())
160
+ if (enableTOC) featureLoaders.push(pluginModule.loadTocPlugin())
161
+
162
+ const featurePluginSets = await Promise.all(featureLoaders)
163
+
164
+ const allPlugins = [
165
+ ...corePlugins,
166
+ ...optionalPluginSets.flat(),
167
+ ...featurePluginSets.flat(),
168
+ ...(extraPlugins ?? []),
169
+ ]
170
+
171
+ if (!cancelled) {
172
+ setPlateState({
173
+ components: {
174
+ Plate: plateReact.Plate as PlateComponents["Plate"],
175
+ PlateContent: plateReact.PlateContent as PlateComponents["PlateContent"],
176
+ usePlateEditor: plateReact.usePlateEditor as PlateComponents["usePlateEditor"],
177
+ },
178
+ plugins: allPlugins,
179
+ })
180
+ }
181
+ } catch {
182
+ if (!cancelled) {
183
+ setLoadError(true)
184
+ }
185
+ }
186
+ }
187
+
188
+ load()
189
+ return () => {
190
+ cancelled = true
191
+ }
192
+ }, [preset, enableMarkdown, enableMedia, enableMentions, enableDnD, enableTOC, extraPlugins])
193
+
194
+ // ── Render with Plate ─────────────────────────────────────────────────
195
+ if (plateState) {
196
+ return (
197
+ <PlateEditorInner
198
+ ref={ref}
199
+ components={plateState.components}
200
+ plugins={plateState.plugins}
201
+ value={value}
202
+ onChange={onChange}
203
+ readOnly={readOnly}
204
+ placeholder={placeholder}
205
+ className={className}
206
+ editorClassName={editorClassName}
207
+ minHeight={minHeight}
208
+ label={label}
209
+ description={description}
210
+ error={error}
211
+ enableAI={enableAI}
212
+ toolbarGroups={toolbarGroups}
213
+ hideToolbar={hideToolbar}
214
+ onAIClick={onAIClick}
215
+ editorRef={editorRef}
216
+ />
217
+ )
218
+ }
219
+
220
+ // ── Fallback: loading or error ────────────────────────────────────────
221
+ return (
222
+ <FallbackEditor
223
+ ref={ref}
224
+ value={value}
225
+ onChange={onChange}
226
+ placeholder={placeholder}
227
+ className={className}
228
+ minHeight={minHeight}
229
+ label={label}
230
+ description={description}
231
+ error={error}
232
+ loading={!loadError}
233
+ readOnly={readOnly}
234
+ />
235
+ )
236
+ }
237
+ )
238
+
239
+ WakaEditor.displayName = "WakaEditor"
240
+
241
+ // ─── Plate inner component ──────────────────────────────────────────────────
242
+
243
+ interface PlateEditorInnerProps {
244
+ components: PlateComponents
245
+ plugins: unknown[]
246
+ value?: SlateNode[]
247
+ onChange?: (value: SlateNode[]) => void
248
+ readOnly?: boolean
249
+ placeholder?: string
250
+ className?: string
251
+ editorClassName?: string
252
+ minHeight?: number
253
+ label?: string
254
+ description?: string
255
+ error?: string
256
+ enableAI?: boolean
257
+ toolbarGroups?: ToolbarGroup[]
258
+ hideToolbar?: boolean
259
+ onAIClick?: () => void
260
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
261
+ editorRef?: React.MutableRefObject<any>
262
+ }
263
+
264
+ const PlateEditorInner = React.forwardRef<HTMLDivElement, PlateEditorInnerProps>(
265
+ (
266
+ {
267
+ components: { Plate, PlateContent, usePlateEditor },
268
+ plugins,
269
+ value,
270
+ onChange,
271
+ readOnly = false,
272
+ placeholder,
273
+ className,
274
+ editorClassName,
275
+ minHeight,
276
+ label,
277
+ description,
278
+ error,
279
+ enableAI,
280
+ toolbarGroups,
281
+ hideToolbar,
282
+ onAIClick,
283
+ editorRef,
284
+ },
285
+ ref
286
+ ) => {
287
+ const editor = usePlateEditor({
288
+ plugins,
289
+ value: value ?? [{ type: "p", children: [{ text: "" }] }],
290
+ })
291
+
292
+ // Expose editor ref
293
+ React.useEffect(() => {
294
+ if (editorRef) {
295
+ editorRef.current = editor
296
+ }
297
+ }, [editor, editorRef])
298
+
299
+ return (
300
+ <div ref={ref} className={cn("space-y-1.5", className)}>
301
+ {label && <Label>{label}</Label>}
302
+ {description && (
303
+ <p className="text-sm text-muted-foreground">{description}</p>
304
+ )}
305
+
306
+ <Plate
307
+ editor={editor as never}
308
+ onChange={onChange ? ({ value: v }: { value: SlateNode[] }) => onChange(v) : undefined}
309
+ readOnly={readOnly}
310
+ >
311
+ <div
312
+ className={cn(
313
+ "border rounded-lg overflow-hidden",
314
+ error && "border-destructive",
315
+ readOnly && "opacity-80"
316
+ )}
317
+ >
318
+ {/* Toolbar */}
319
+ {!hideToolbar && !readOnly && (
320
+ <WakaEditorToolbar
321
+ editor={editor}
322
+ groups={toolbarGroups}
323
+ enableAI={enableAI}
324
+ onAIClick={onAIClick}
325
+ />
326
+ )}
327
+
328
+ {/* Editable area */}
329
+ <div style={{ minHeight }}>
330
+ <PlateContent
331
+ placeholder={placeholder}
332
+ readOnly={readOnly}
333
+ className={cn(
334
+ "prose prose-sm max-w-none p-3",
335
+ "focus-within:outline-none",
336
+ "[&_[data-slate-editor]]:outline-none [&_[data-slate-editor]]:min-h-[inherit]",
337
+ editorClassName
338
+ )}
339
+ />
340
+ </div>
341
+ </div>
342
+ </Plate>
343
+
344
+ {error && <p className="text-sm text-destructive">{error}</p>}
345
+ </div>
346
+ )
347
+ }
348
+ )
349
+
350
+ PlateEditorInner.displayName = "PlateEditorInner"
351
+
352
+ // ─── Fallback editor (textarea) ─────────────────────────────────────────────
353
+
354
+ interface FallbackEditorProps {
355
+ value?: SlateNode[]
356
+ onChange?: (value: SlateNode[]) => void
357
+ placeholder?: string
358
+ className?: string
359
+ minHeight?: number
360
+ label?: string
361
+ description?: string
362
+ error?: string
363
+ loading?: boolean
364
+ readOnly?: boolean
365
+ }
366
+
367
+ const FallbackEditor = React.forwardRef<HTMLDivElement, FallbackEditorProps>(
368
+ (
369
+ {
370
+ value,
371
+ onChange,
372
+ placeholder,
373
+ className,
374
+ minHeight,
375
+ label,
376
+ description,
377
+ error,
378
+ loading,
379
+ readOnly,
380
+ },
381
+ ref
382
+ ) => {
383
+ // Extract raw text from Slate value for fallback display
384
+ const rawText = React.useMemo(() => {
385
+ if (!value) return ""
386
+ const extractText = (nodes: SlateNode[]): string => {
387
+ return nodes
388
+ .map((node: SlateNode) => {
389
+ if (typeof node.text === "string") return node.text
390
+ if (node.children) return extractText(node.children as SlateNode[])
391
+ return ""
392
+ })
393
+ .join("\n")
394
+ }
395
+ return extractText(value)
396
+ }, [value])
397
+
398
+ return (
399
+ <div ref={ref} className={cn("space-y-1.5", className)}>
400
+ {label && <Label>{label}</Label>}
401
+ {description && (
402
+ <p className="text-sm text-muted-foreground">{description}</p>
403
+ )}
404
+
405
+ <div className="relative">
406
+ <Textarea
407
+ value={rawText}
408
+ onChange={(e) => {
409
+ onChange?.([{ type: "p", children: [{ text: e.target.value }] }])
410
+ }}
411
+ placeholder={placeholder}
412
+ disabled={loading || readOnly}
413
+ style={{ minHeight }}
414
+ className={cn(error && "border-destructive")}
415
+ />
416
+ {loading && (
417
+ <div className="absolute inset-0 flex items-center justify-center bg-background/50 rounded-lg">
418
+ <span className="text-sm text-muted-foreground animate-pulse">
419
+ Chargement de l&apos;editeur...
420
+ </span>
421
+ </div>
422
+ )}
423
+ </div>
424
+
425
+ {error && <p className="text-sm text-destructive">{error}</p>}
426
+ </div>
427
+ )
428
+ }
429
+ )
430
+
431
+ FallbackEditor.displayName = "FallbackEditor"
@@ -0,0 +1,268 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ Bold,
6
+ Italic,
7
+ Underline,
8
+ Strikethrough,
9
+ Code,
10
+ Highlighter,
11
+ Link,
12
+ Sparkles,
13
+ type LucideIcon,
14
+ } from "lucide-react"
15
+ import { cn } from "../../utils/cn"
16
+
17
+ // ─── Types ───────────────────────────────────────────────────────────────────
18
+
19
+ /** A button definition for the floating toolbar */
20
+ export interface FloatingToolbarButton {
21
+ key: string
22
+ icon: LucideIcon
23
+ label: string
24
+ shortcut?: string
25
+ /** Execute the action on the editor */
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ action: (editor: any) => void
28
+ /** Check if this mark/block is active */
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ isActive?: (editor: any) => boolean
31
+ }
32
+
33
+ export interface WakaFloatingToolbarProps {
34
+ /** The Plate editor instance */
35
+ editor: unknown
36
+ /** Additional buttons to display */
37
+ buttons?: FloatingToolbarButton[]
38
+ /** Whether to show the AI "Ask AI" button */
39
+ enableAI?: boolean
40
+ /** Callback when AI button is clicked */
41
+ onAIClick?: () => void
42
+ /** CSS class */
43
+ className?: string
44
+ /** Whether to show in read-only mode */
45
+ showWhenReadOnly?: boolean
46
+ }
47
+
48
+ // ─── Default buttons ─────────────────────────────────────────────────────────
49
+
50
+ function safeAction(editor: unknown, fn: (e: Record<string, unknown>) => void) {
51
+ try { fn(editor as Record<string, unknown>) } catch { /* plugin not loaded */ }
52
+ }
53
+
54
+ function safeIsActive(editor: unknown, type: string): boolean {
55
+ try { return (editor as { isActive: (t: string) => boolean }).isActive(type) } catch { return false }
56
+ }
57
+
58
+ /** Default floating toolbar buttons */
59
+ export const DEFAULT_FLOATING_BUTTONS: FloatingToolbarButton[] = [
60
+ {
61
+ key: "bold",
62
+ icon: Bold,
63
+ label: "Gras",
64
+ shortcut: "Ctrl+B",
65
+ action: (e) => safeAction(e, (ed) => (ed as { chain: () => { focus: () => { toggleBold: () => { run: () => void } } } }).chain().focus().toggleBold().run()),
66
+ isActive: (e) => safeIsActive(e, "bold"),
67
+ },
68
+ {
69
+ key: "italic",
70
+ icon: Italic,
71
+ label: "Italique",
72
+ shortcut: "Ctrl+I",
73
+ action: (e) => safeAction(e, (ed) => (ed as { chain: () => { focus: () => { toggleItalic: () => { run: () => void } } } }).chain().focus().toggleItalic().run()),
74
+ isActive: (e) => safeIsActive(e, "italic"),
75
+ },
76
+ {
77
+ key: "underline",
78
+ icon: Underline,
79
+ label: "Souligne",
80
+ shortcut: "Ctrl+U",
81
+ action: (e) => safeAction(e, (ed) => (ed as { chain: () => { focus: () => { toggleUnderline: () => { run: () => void } } } }).chain().focus().toggleUnderline().run()),
82
+ isActive: (e) => safeIsActive(e, "underline"),
83
+ },
84
+ {
85
+ key: "strikethrough",
86
+ icon: Strikethrough,
87
+ label: "Barre",
88
+ action: (e) => safeAction(e, (ed) => (ed as { chain: () => { focus: () => { toggleStrike: () => { run: () => void } } } }).chain().focus().toggleStrike().run()),
89
+ isActive: (e) => safeIsActive(e, "strikethrough"),
90
+ },
91
+ {
92
+ key: "code",
93
+ icon: Code,
94
+ label: "Code",
95
+ shortcut: "Ctrl+E",
96
+ action: (e) => safeAction(e, (ed) => (ed as { chain: () => { focus: () => { toggleCode: () => { run: () => void } } } }).chain().focus().toggleCode().run()),
97
+ isActive: (e) => safeIsActive(e, "code"),
98
+ },
99
+ {
100
+ key: "highlight",
101
+ icon: Highlighter,
102
+ label: "Surligner",
103
+ action: (e) => safeAction(e, (ed) => (ed as { chain: () => { focus: () => { toggleHighlight: () => { run: () => void } } } }).chain().focus().toggleHighlight().run()),
104
+ isActive: (e) => safeIsActive(e, "highlight"),
105
+ },
106
+ {
107
+ key: "link",
108
+ icon: Link,
109
+ label: "Lien",
110
+ shortcut: "Ctrl+K",
111
+ action: (e) => { try { (e as { tf: { link: { toggle: () => void } } }).tf.link.toggle() } catch { /* */ } },
112
+ isActive: (e) => safeIsActive(e, "a"),
113
+ },
114
+ ]
115
+
116
+ // ─── Component ───────────────────────────────────────────────────────────────
117
+
118
+ /**
119
+ * WakaFloatingToolbar -- Floating contextual toolbar that appears on text selection.
120
+ *
121
+ * This component should be placed **inside** a `<Plate>` context. It uses
122
+ * `@platejs/floating`'s `useFloatingToolbar` hook internally when available,
123
+ * or falls back to a static inline toolbar.
124
+ *
125
+ * For the floating behavior to work, the app must install `@platejs/floating`.
126
+ *
127
+ * @example
128
+ * ```tsx
129
+ * <Plate editor={editor}>
130
+ * <WakaFloatingToolbar editor={editor} enableAI onAIClick={handleAI} />
131
+ * <PlateContent />
132
+ * </Plate>
133
+ * ```
134
+ */
135
+ export const WakaFloatingToolbar = React.forwardRef<HTMLDivElement, WakaFloatingToolbarProps>(
136
+ (
137
+ {
138
+ editor,
139
+ buttons = DEFAULT_FLOATING_BUTTONS,
140
+ enableAI = false,
141
+ onAIClick,
142
+ className,
143
+ },
144
+ ref
145
+ ) => {
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ const [floatingHook, setFloatingHook] = React.useState<any>(null)
148
+
149
+ React.useEffect(() => {
150
+ import("@platejs/floating")
151
+ .then((mod) => {
152
+ if (mod.useFloatingToolbar) {
153
+ setFloatingHook(() => mod.useFloatingToolbar)
154
+ }
155
+ })
156
+ .catch(() => {
157
+ // @platejs/floating not installed, use static fallback
158
+ })
159
+ }, [])
160
+
161
+ const allButtons = React.useMemo(() => {
162
+ if (!enableAI) return buttons
163
+ return [
164
+ ...buttons,
165
+ {
166
+ key: "ai",
167
+ icon: Sparkles,
168
+ label: "Demander a l'IA",
169
+ action: () => onAIClick?.(),
170
+ isActive: () => false,
171
+ } as FloatingToolbarButton,
172
+ ]
173
+ }, [buttons, enableAI, onAIClick])
174
+
175
+ // If floating hook is available, use it for positioning
176
+ // Otherwise render a static toolbar bar
177
+ return (
178
+ <FloatingToolbarContent
179
+ ref={ref}
180
+ editor={editor}
181
+ buttons={allButtons}
182
+ className={className}
183
+ floatingHook={floatingHook}
184
+ />
185
+ )
186
+ }
187
+ )
188
+
189
+ WakaFloatingToolbar.displayName = "WakaFloatingToolbar"
190
+
191
+ // ─── Inner rendering ────────────────────────────────────────────────────────
192
+
193
+ interface FloatingToolbarContentProps {
194
+ editor: unknown
195
+ buttons: FloatingToolbarButton[]
196
+ className?: string
197
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
+ floatingHook: any
199
+ }
200
+
201
+ const FloatingToolbarContent = React.forwardRef<HTMLDivElement, FloatingToolbarContentProps>(
202
+ ({ editor, buttons, className, floatingHook }, ref) => {
203
+ // Use floating positioning if available
204
+ const floating = floatingHook
205
+ ? floatingHook({ editor })
206
+ : { ref: null, hidden: false, props: {} }
207
+
208
+ const mergedRef = React.useCallback(
209
+ (node: HTMLDivElement | null) => {
210
+ if (typeof ref === "function") ref(node)
211
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
212
+ if (floating.ref) {
213
+ if (typeof floating.ref === "function") floating.ref(node)
214
+ else floating.ref.current = node
215
+ }
216
+ },
217
+ [ref, floating.ref]
218
+ )
219
+
220
+ if (floating.hidden) return null
221
+
222
+ return (
223
+ <div
224
+ ref={mergedRef}
225
+ {...floating.props}
226
+ className={cn(
227
+ "flex items-center gap-0.5 p-1",
228
+ "rounded-lg border bg-popover shadow-lg",
229
+ "animate-in fade-in-0 zoom-in-95",
230
+ className
231
+ )}
232
+ role="toolbar"
233
+ aria-label="Barre d'outils flottante"
234
+ >
235
+ {buttons.map((btn, i) => {
236
+ const active = btn.isActive?.(editor) ?? false
237
+ const Icon = btn.icon
238
+ const title = btn.shortcut ? `${btn.label} (${btn.shortcut})` : btn.label
239
+
240
+ return (
241
+ <button
242
+ key={btn.key}
243
+ type="button"
244
+ title={title}
245
+ aria-label={btn.label}
246
+ aria-pressed={active}
247
+ className={cn(
248
+ "inline-flex h-7 w-7 items-center justify-center rounded-md",
249
+ "text-sm transition-colors",
250
+ active
251
+ ? "bg-accent text-accent-foreground"
252
+ : "hover:bg-accent/50 text-foreground/70 hover:text-foreground"
253
+ )}
254
+ onMouseDown={(e) => {
255
+ e.preventDefault() // Keep selection
256
+ btn.action(editor)
257
+ }}
258
+ >
259
+ <Icon className="h-3.5 w-3.5" />
260
+ </button>
261
+ )
262
+ })}
263
+ </div>
264
+ )
265
+ }
266
+ )
267
+
268
+ FloatingToolbarContent.displayName = "FloatingToolbarContent"