@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,502 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Label } from "../label"
6
+ import { ScrollArea } from "../scroll-area"
7
+ import { Textarea } from "../textarea"
8
+
9
+ // ─── Types ───────────────────────────────────────────────────────────────────
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ type SlateNode = any
13
+
14
+ /** TOC entry extracted from headings */
15
+ export interface TocEntry {
16
+ id: string
17
+ text: string
18
+ level: number
19
+ depth: number
20
+ }
21
+
22
+ export interface WakaDocumentEditorProps {
23
+ /** Initial Slate content */
24
+ value?: SlateNode[]
25
+ /** Change callback */
26
+ onChange?: (value: SlateNode[]) => void
27
+ /** Read-only mode */
28
+ readOnly?: boolean
29
+ /** Placeholder */
30
+ placeholder?: string
31
+ /** CSS class for the root layout */
32
+ className?: string
33
+ /** CSS class for the editor area */
34
+ editorClassName?: string
35
+ /** Minimum height in px */
36
+ minHeight?: number
37
+ /** Label */
38
+ label?: string
39
+ /** Description */
40
+ description?: string
41
+ /** Error message */
42
+ error?: string
43
+ /** Document title (displayed in the header) */
44
+ title?: string
45
+ /** Callback when title changes */
46
+ onTitleChange?: (title: string) => void
47
+ /** Show TOC sidebar */
48
+ showTOC?: boolean
49
+ /** Enable @mentions */
50
+ enableMentions?: boolean
51
+ /** Mention search function. Returns matching users for a query string. */
52
+ onMentionSearch?: (query: string) => Promise<Array<{ key: string; label: string; avatar?: string }>>
53
+ /** Enable slash commands */
54
+ enableSlashCommands?: boolean
55
+ /** Enable AI features */
56
+ enableAI?: boolean
57
+ /** AI endpoint for AI features */
58
+ aiEndpoint?: string
59
+ /** System prompt for AI */
60
+ aiSystemPrompt?: string
61
+ /** Enable DnD block reordering */
62
+ enableDnD?: boolean
63
+ /** Enable markdown paste support */
64
+ enableMarkdown?: boolean
65
+ /** Extra Plate plugins */
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ extraPlugins?: any[]
68
+ /** Ref to the editor instance */
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ editorRef?: React.MutableRefObject<any>
71
+ }
72
+
73
+ // ─── Plate bundle ────────────────────────────────────────────────────────────
74
+
75
+ interface PlateBundle {
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ Plate: React.ComponentType<any>
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ PlateContent: React.ComponentType<any>
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ usePlateEditor: (config: any) => any
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ plugins: any[]
84
+ }
85
+
86
+ // ─── Component ───────────────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * WakaDocumentEditor -- Google-Docs-style full document editor.
90
+ *
91
+ * Features:
92
+ * - Fixed toolbar with formatting controls
93
+ * - Optional TOC sidebar (auto-extracted from headings)
94
+ * - @mentions with async user search
95
+ * - Slash commands (/) for quick block insertion
96
+ * - Drag-and-drop block reordering
97
+ * - AI integration (optional)
98
+ * - Markdown paste support
99
+ *
100
+ * Perfect for writing specifications, proposals, documentation,
101
+ * or any long-form content within WakaStart apps.
102
+ *
103
+ * @example
104
+ * ```tsx
105
+ * <WakaDocumentEditor
106
+ * title="Architecture Specification"
107
+ * onTitleChange={setTitle}
108
+ * showTOC
109
+ * enableMentions
110
+ * enableSlashCommands
111
+ * enableDnD
112
+ * value={content}
113
+ * onChange={setContent}
114
+ * />
115
+ * ```
116
+ */
117
+ export const WakaDocumentEditor = React.forwardRef<HTMLDivElement, WakaDocumentEditorProps>(
118
+ (
119
+ {
120
+ value,
121
+ onChange,
122
+ readOnly = false,
123
+ placeholder = "Commencez a rediger votre document...",
124
+ className,
125
+ editorClassName,
126
+ minHeight = 500,
127
+ label,
128
+ description,
129
+ error,
130
+ title,
131
+ onTitleChange,
132
+ showTOC = true,
133
+ enableMentions = false,
134
+ enableSlashCommands = false,
135
+ enableAI = false,
136
+ aiEndpoint,
137
+ aiSystemPrompt,
138
+ enableDnD = false,
139
+ enableMarkdown = true,
140
+ extraPlugins,
141
+ editorRef,
142
+ },
143
+ ref
144
+ ) => {
145
+ const [bundle, setBundle] = React.useState<PlateBundle | null>(null)
146
+ const [loadError, setLoadError] = React.useState(false)
147
+ const [toc, setToc] = React.useState<TocEntry[]>([])
148
+
149
+ const configRef = React.useRef({
150
+ enableMentions, enableSlashCommands, enableAI, enableDnD, enableMarkdown,
151
+ aiEndpoint, aiSystemPrompt,
152
+ })
153
+ configRef.current = {
154
+ enableMentions, enableSlashCommands, enableAI, enableDnD, enableMarkdown,
155
+ aiEndpoint, aiSystemPrompt,
156
+ }
157
+
158
+ // ── Load Plate + plugins ──────────────────────────────────────────────
159
+ React.useEffect(() => {
160
+ let cancelled = false
161
+
162
+ const load = async () => {
163
+ try {
164
+ const cfg = configRef.current
165
+
166
+ const [plateReact, basicNodes, tableModule, layoutModule, calloutModule, linkModule, indentModule] =
167
+ await Promise.all([
168
+ import("platejs/react"),
169
+ import("@platejs/basic-nodes/react"),
170
+ import("@platejs/table/react"),
171
+ import("@platejs/layout/react"),
172
+ import("@platejs/callout/react"),
173
+ import("@platejs/link/react"),
174
+ import("@platejs/indent/react"),
175
+ ])
176
+
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ const plugins: any[] = [
179
+ basicNodes.BoldPlugin, basicNodes.ItalicPlugin, basicNodes.UnderlinePlugin,
180
+ basicNodes.StrikethroughPlugin, basicNodes.CodePlugin, basicNodes.HighlightPlugin,
181
+ basicNodes.SuperscriptPlugin, basicNodes.SubscriptPlugin,
182
+ basicNodes.H1Plugin, basicNodes.H2Plugin, basicNodes.H3Plugin,
183
+ basicNodes.H4Plugin, basicNodes.H5Plugin, basicNodes.H6Plugin,
184
+ basicNodes.BlockquotePlugin,
185
+ tableModule.TablePlugin, tableModule.TableRowPlugin,
186
+ tableModule.TableCellPlugin, tableModule.TableCellHeaderPlugin,
187
+ layoutModule.ColumnPlugin, layoutModule.ColumnItemPlugin,
188
+ calloutModule.CalloutPlugin,
189
+ linkModule.LinkPlugin.configure({
190
+ options: { allowedSchemes: ["http", "https", "mailto", "tel"] },
191
+ }),
192
+ indentModule.IndentPlugin,
193
+ ]
194
+
195
+ // Optional: TOC
196
+ try {
197
+ const { TocPlugin } = await import("@platejs/toc/react")
198
+ plugins.push(TocPlugin)
199
+ } catch { /* not installed */ }
200
+
201
+ // Optional: Selection
202
+ try {
203
+ const { BlockSelectionPlugin } = await import("@platejs/selection/react")
204
+ plugins.push(BlockSelectionPlugin)
205
+ } catch { /* not installed */ }
206
+
207
+ // Optional: Mentions
208
+ if (cfg.enableMentions) {
209
+ try {
210
+ const { MentionPlugin, MentionInputPlugin } = await import("@platejs/mention/react")
211
+ plugins.push(
212
+ MentionPlugin.configure({
213
+ options: { trigger: "@", triggerPreviousCharPattern: /^$|^[\s"']$/, insertSpaceAfterMention: true },
214
+ }),
215
+ MentionInputPlugin,
216
+ )
217
+ } catch { /* not installed */ }
218
+ }
219
+
220
+ // Optional: DnD
221
+ if (cfg.enableDnD) {
222
+ try {
223
+ const { DndPlugin } = await import("@platejs/dnd/react")
224
+ plugins.push(DndPlugin)
225
+ } catch { /* not installed */ }
226
+ }
227
+
228
+ // Optional: Markdown
229
+ if (cfg.enableMarkdown) {
230
+ try {
231
+ const { MarkdownPlugin } = await import("@platejs/markdown")
232
+ plugins.push(MarkdownPlugin)
233
+ } catch { /* not installed */ }
234
+ }
235
+
236
+ // Optional: AI
237
+ if (cfg.enableAI && cfg.aiEndpoint) {
238
+ try {
239
+ const { AIPlugin, AIChatPlugin } = await import("@platejs/ai/react")
240
+ plugins.push(
241
+ AIPlugin,
242
+ AIChatPlugin.configure({
243
+ options: {
244
+ chat: {
245
+ api: cfg.aiEndpoint,
246
+ body: { system: cfg.aiSystemPrompt ?? "You are a helpful writing assistant." },
247
+ },
248
+ },
249
+ }),
250
+ )
251
+ } catch { /* not installed */ }
252
+ }
253
+
254
+ if (!cancelled) {
255
+ setBundle({
256
+ Plate: plateReact.Plate,
257
+ PlateContent: plateReact.PlateContent,
258
+ usePlateEditor: plateReact.usePlateEditor,
259
+ plugins,
260
+ })
261
+ }
262
+ } catch (err) {
263
+ console.error("[WakaDocumentEditor] Failed to load:", err)
264
+ if (!cancelled) setLoadError(true)
265
+ }
266
+ }
267
+
268
+ load()
269
+ return () => { cancelled = true }
270
+ }, [])
271
+
272
+ // ── Extract TOC from value ────────────────────────────────────────────
273
+ React.useEffect(() => {
274
+ if (!showTOC || !value) { setToc([]); return }
275
+
276
+ const entries: TocEntry[] = []
277
+ let counter = 0
278
+ const headingTypes = new Set(["h1", "h2", "h3", "h4", "h5", "h6"])
279
+
280
+ for (const node of value) {
281
+ if (headingTypes.has(node?.type)) {
282
+ const text = extractText(node)
283
+ if (text.trim()) {
284
+ const level = parseInt(node.type.replace("h", ""), 10)
285
+ entries.push({
286
+ id: `toc-${counter++}`,
287
+ text: text.trim(),
288
+ level,
289
+ depth: level - 1,
290
+ })
291
+ }
292
+ }
293
+ }
294
+
295
+ setToc(entries)
296
+ }, [value, showTOC])
297
+
298
+ // ── Render ────────────────────────────────────────────────────────────
299
+ if (bundle) {
300
+ return (
301
+ <DocumentEditorInner
302
+ ref={ref}
303
+ bundle={bundle}
304
+ value={value}
305
+ onChange={onChange}
306
+ readOnly={readOnly}
307
+ placeholder={placeholder}
308
+ className={className}
309
+ editorClassName={editorClassName}
310
+ minHeight={minHeight}
311
+ label={label}
312
+ description={description}
313
+ error={error}
314
+ title={title}
315
+ onTitleChange={onTitleChange}
316
+ showTOC={showTOC}
317
+ toc={toc}
318
+ extraPlugins={extraPlugins}
319
+ editorRef={editorRef}
320
+ />
321
+ )
322
+ }
323
+
324
+ // Fallback
325
+ return (
326
+ <div ref={ref} className={cn("space-y-1.5", className)}>
327
+ {label && <Label>{label}</Label>}
328
+ {description && <p className="text-sm text-muted-foreground">{description}</p>}
329
+ <div className="relative">
330
+ <Textarea placeholder={placeholder} disabled style={{ minHeight }} />
331
+ <div className="absolute inset-0 flex items-center justify-center bg-background/50 rounded-lg">
332
+ <span className="text-sm text-muted-foreground animate-pulse">
333
+ {loadError ? "Erreur de chargement de l'editeur" : "Chargement de l'editeur..."}
334
+ </span>
335
+ </div>
336
+ </div>
337
+ {error && <p className="text-sm text-destructive">{error}</p>}
338
+ </div>
339
+ )
340
+ }
341
+ )
342
+
343
+ WakaDocumentEditor.displayName = "WakaDocumentEditor"
344
+
345
+ // ─── Inner Plate component ──────────────────────────────────────────────────
346
+
347
+ interface DocumentEditorInnerProps {
348
+ bundle: PlateBundle
349
+ value?: SlateNode[]
350
+ onChange?: (value: SlateNode[]) => void
351
+ readOnly?: boolean
352
+ placeholder?: string
353
+ className?: string
354
+ editorClassName?: string
355
+ minHeight?: number
356
+ label?: string
357
+ description?: string
358
+ error?: string
359
+ title?: string
360
+ onTitleChange?: (title: string) => void
361
+ showTOC?: boolean
362
+ toc: TocEntry[]
363
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
364
+ extraPlugins?: any[]
365
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
366
+ editorRef?: React.MutableRefObject<any>
367
+ }
368
+
369
+ const DocumentEditorInner = React.forwardRef<HTMLDivElement, DocumentEditorInnerProps>(
370
+ (
371
+ {
372
+ bundle: { Plate, PlateContent, usePlateEditor, plugins },
373
+ value,
374
+ onChange,
375
+ readOnly,
376
+ placeholder,
377
+ className,
378
+ editorClassName,
379
+ minHeight,
380
+ label,
381
+ description,
382
+ error,
383
+ title,
384
+ onTitleChange,
385
+ showTOC,
386
+ toc,
387
+ extraPlugins,
388
+ editorRef,
389
+ },
390
+ ref
391
+ ) => {
392
+ const allPlugins = React.useMemo(
393
+ () => [...plugins, ...(extraPlugins ?? [])],
394
+ [plugins, extraPlugins]
395
+ )
396
+
397
+ const editor = usePlateEditor({
398
+ plugins: allPlugins,
399
+ value: value ?? [{ type: "p", children: [{ text: "" }] }],
400
+ })
401
+
402
+ React.useEffect(() => {
403
+ if (editorRef) editorRef.current = editor
404
+ }, [editor, editorRef])
405
+
406
+ return (
407
+ <div ref={ref} className={cn("space-y-1.5", className)}>
408
+ {label && <Label>{label}</Label>}
409
+ {description && <p className="text-sm text-muted-foreground">{description}</p>}
410
+
411
+ <Plate
412
+ editor={editor}
413
+ onChange={onChange ? ({ value: v }: { value: SlateNode[] }) => onChange(v) : undefined}
414
+ readOnly={readOnly}
415
+ >
416
+ <div className={cn(
417
+ "flex border rounded-lg overflow-hidden",
418
+ error && "border-destructive",
419
+ )}>
420
+ {/* TOC Sidebar */}
421
+ {showTOC && toc.length > 0 && (
422
+ <aside className="w-56 shrink-0 border-r bg-muted/20 hidden lg:block">
423
+ <div className="p-3 border-b">
424
+ <h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
425
+ Table des matieres
426
+ </h4>
427
+ </div>
428
+ <ScrollArea className="h-full max-h-[600px]">
429
+ <nav className="p-2 space-y-0.5">
430
+ {toc.map((entry) => (
431
+ <button
432
+ key={entry.id}
433
+ className={cn(
434
+ "block w-full text-left px-2 py-1 rounded-md text-sm",
435
+ "hover:bg-accent/50 transition-colors truncate",
436
+ "text-muted-foreground hover:text-foreground",
437
+ )}
438
+ style={{ paddingLeft: `${entry.depth * 12 + 8}px` }}
439
+ title={entry.text}
440
+ >
441
+ {entry.text}
442
+ </button>
443
+ ))}
444
+ </nav>
445
+ </ScrollArea>
446
+ </aside>
447
+ )}
448
+
449
+ {/* Main editor area */}
450
+ <div className="flex-1 min-w-0">
451
+ {/* Title input */}
452
+ {(title !== undefined || onTitleChange) && (
453
+ <div className="border-b px-6 py-4">
454
+ <input
455
+ type="text"
456
+ value={title ?? ""}
457
+ onChange={(e) => onTitleChange?.(e.target.value)}
458
+ placeholder="Titre du document"
459
+ readOnly={readOnly}
460
+ className={cn(
461
+ "w-full bg-transparent text-2xl font-bold text-foreground",
462
+ "placeholder:text-muted-foreground/50",
463
+ "focus:outline-none",
464
+ )}
465
+ />
466
+ </div>
467
+ )}
468
+
469
+ {/* Editor content */}
470
+ <div style={{ minHeight }}>
471
+ <PlateContent
472
+ placeholder={placeholder}
473
+ readOnly={readOnly}
474
+ className={cn(
475
+ "prose prose-sm max-w-none px-6 py-4",
476
+ "focus-within:outline-none",
477
+ "[&_[data-slate-editor]]:outline-none [&_[data-slate-editor]]:min-h-[inherit]",
478
+ editorClassName,
479
+ )}
480
+ />
481
+ </div>
482
+ </div>
483
+ </div>
484
+ </Plate>
485
+
486
+ {error && <p className="text-sm text-destructive">{error}</p>}
487
+ </div>
488
+ )
489
+ }
490
+ )
491
+
492
+ DocumentEditorInner.displayName = "DocumentEditorInner"
493
+
494
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
495
+
496
+ function extractText(node: SlateNode): string {
497
+ if (typeof node?.text === "string") return node.text
498
+ if (Array.isArray(node?.children)) {
499
+ return node.children.map((c: SlateNode) => extractText(c)).join("")
500
+ }
501
+ return ""
502
+ }