@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,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'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"
|