@qwanyx/ai-editor 1.0.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 (46) hide show
  1. package/dist/components/AIEditor.d.ts +15 -0
  2. package/dist/components/AIEditor.d.ts.map +1 -0
  3. package/dist/components/AIEditor.js +154 -0
  4. package/dist/components/AIToolbar.d.ts +22 -0
  5. package/dist/components/AIToolbar.d.ts.map +1 -0
  6. package/dist/components/AIToolbar.js +10 -0
  7. package/dist/components/EditorToolbar.d.ts +8 -0
  8. package/dist/components/EditorToolbar.d.ts.map +1 -0
  9. package/dist/components/EditorToolbar.js +241 -0
  10. package/dist/components/MarkdownPreview.d.ts +7 -0
  11. package/dist/components/MarkdownPreview.d.ts.map +1 -0
  12. package/dist/components/MarkdownPreview.js +40 -0
  13. package/dist/components/PromptModal.d.ts +9 -0
  14. package/dist/components/PromptModal.d.ts.map +1 -0
  15. package/dist/components/PromptModal.js +38 -0
  16. package/dist/components/RichTextEditor.d.ts +15 -0
  17. package/dist/components/RichTextEditor.d.ts.map +1 -0
  18. package/dist/components/RichTextEditor.js +253 -0
  19. package/dist/hooks/useAIEditor.d.ts +15 -0
  20. package/dist/hooks/useAIEditor.d.ts.map +1 -0
  21. package/dist/hooks/useAIEditor.js +46 -0
  22. package/dist/hooks/useSelection.d.ts +12 -0
  23. package/dist/hooks/useSelection.d.ts.map +1 -0
  24. package/dist/hooks/useSelection.js +45 -0
  25. package/dist/index.d.ts +15 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +14 -0
  28. package/dist/nodes/ImageLinkNode.d.ts +48 -0
  29. package/dist/nodes/ImageLinkNode.d.ts.map +1 -0
  30. package/dist/nodes/ImageLinkNode.js +157 -0
  31. package/dist/nodes/ImageNode.d.ts +62 -0
  32. package/dist/nodes/ImageNode.d.ts.map +1 -0
  33. package/dist/nodes/ImageNode.js +487 -0
  34. package/dist/nodes/LinkNode.d.ts +33 -0
  35. package/dist/nodes/LinkNode.d.ts.map +1 -0
  36. package/dist/nodes/LinkNode.js +108 -0
  37. package/dist/plugins/ImageLinkPlugin.d.ts +2 -0
  38. package/dist/plugins/ImageLinkPlugin.d.ts.map +1 -0
  39. package/dist/plugins/ImageLinkPlugin.js +112 -0
  40. package/dist/plugins/InsertObjectPlugin.d.ts +4 -0
  41. package/dist/plugins/InsertObjectPlugin.d.ts.map +1 -0
  42. package/dist/plugins/InsertObjectPlugin.js +464 -0
  43. package/dist/plugins/LinkPlugin.d.ts +2 -0
  44. package/dist/plugins/LinkPlugin.d.ts.map +1 -0
  45. package/dist/plugins/LinkPlugin.js +45 -0
  46. package/package.json +45 -0
@@ -0,0 +1,15 @@
1
+ export interface RichTextEditorProps {
2
+ initialContent?: string;
3
+ onChange?: (html: string, json: string) => void;
4
+ onAIRequest?: (params: {
5
+ fullText: string;
6
+ selectedText?: string;
7
+ action: 'rewrite' | 'proofread' | 'custom';
8
+ customPrompt?: string;
9
+ }) => Promise<string>;
10
+ placeholder?: string;
11
+ className?: string;
12
+ minHeight?: string;
13
+ }
14
+ export declare function RichTextEditor({ initialContent, onChange, onAIRequest, placeholder, className, minHeight }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=RichTextEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../src/components/RichTextEditor.tsx"],"names":[],"mappings":"AAiCA,MAAM,WAAW,mBAAmB;IAClC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QACrB,QAAQ,EAAE,MAAM,CAAA;QAChB,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAA;QAC1C,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAqKD,wBAAgB,cAAc,CAAC,EAC7B,cAAmB,EACnB,QAAQ,EACR,WAAW,EACX,WAAqC,EACrC,SAAc,EACd,SAAmB,EACpB,EAAE,mBAAmB,2CAyIrB"}
@@ -0,0 +1,253 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useEffect } from 'react';
4
+ import { LexicalComposer } from '@lexical/react/LexicalComposer';
5
+ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
6
+ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
7
+ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
8
+ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
9
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
10
+ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
11
+ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
12
+ import { ListNode, ListItemNode } from '@lexical/list';
13
+ import { LinkNode } from '@lexical/link';
14
+ import { ImageNode } from '../nodes/ImageNode';
15
+ import { ImageLinkNode } from '../nodes/ImageLinkNode';
16
+ import { SimpleLinkNode } from '../nodes/LinkNode';
17
+ import { InsertObjectPlugin } from '../plugins/InsertObjectPlugin';
18
+ import { ImageLinkPlugin } from '../plugins/ImageLinkPlugin';
19
+ import { SimpleLinkPlugin } from '../plugins/LinkPlugin';
20
+ import { ListPlugin } from '@lexical/react/LexicalListPlugin';
21
+ import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
22
+ import { $getRoot, $getSelection, $isRangeSelection, $createParagraphNode, $createTextNode } from 'lexical';
23
+ import { EditorToolbar } from './EditorToolbar';
24
+ // Theme for Lexical editor
25
+ const editorTheme = {
26
+ paragraph: 'editor-paragraph',
27
+ heading: {
28
+ h1: 'editor-heading-h1',
29
+ h2: 'editor-heading-h2',
30
+ h3: 'editor-heading-h3',
31
+ },
32
+ text: {
33
+ bold: 'editor-text-bold',
34
+ italic: 'editor-text-italic',
35
+ underline: 'editor-text-underline',
36
+ strikethrough: 'editor-text-strikethrough',
37
+ },
38
+ list: {
39
+ ul: 'editor-list-ul',
40
+ ol: 'editor-list-ol',
41
+ listitem: 'editor-listitem',
42
+ nested: {
43
+ listitem: 'editor-nested-listitem',
44
+ },
45
+ },
46
+ quote: 'editor-quote',
47
+ link: 'editor-link',
48
+ };
49
+ // CSS styles for the editor (injected as style tag)
50
+ const editorStyles = `
51
+ .editor-paragraph {
52
+ margin-bottom: 8px;
53
+ }
54
+ .editor-heading-h1 {
55
+ font-size: 1.875rem;
56
+ font-weight: bold;
57
+ margin-bottom: 16px;
58
+ margin-top: 24px;
59
+ }
60
+ .editor-heading-h2 {
61
+ font-size: 1.5rem;
62
+ font-weight: bold;
63
+ margin-bottom: 12px;
64
+ margin-top: 20px;
65
+ }
66
+ .editor-heading-h3 {
67
+ font-size: 1.25rem;
68
+ font-weight: 600;
69
+ margin-bottom: 8px;
70
+ margin-top: 16px;
71
+ }
72
+ .editor-text-bold {
73
+ font-weight: bold;
74
+ }
75
+ .editor-text-italic {
76
+ font-style: italic;
77
+ }
78
+ .editor-text-underline {
79
+ text-decoration: underline;
80
+ }
81
+ .editor-text-strikethrough {
82
+ text-decoration: line-through;
83
+ }
84
+ .editor-list-ul {
85
+ list-style-type: disc;
86
+ margin-left: 24px;
87
+ margin-bottom: 8px;
88
+ padding-left: 0;
89
+ }
90
+ .editor-list-ol {
91
+ list-style-type: decimal;
92
+ margin-left: 24px;
93
+ margin-bottom: 8px;
94
+ padding-left: 0;
95
+ }
96
+ .editor-listitem {
97
+ margin-bottom: 4px;
98
+ }
99
+ .editor-nested-listitem {
100
+ list-style-type: none;
101
+ }
102
+ .editor-quote {
103
+ border-left: 4px solid #d1d5db;
104
+ padding-left: 16px;
105
+ font-style: italic;
106
+ color: #6b7280;
107
+ margin: 16px 0;
108
+ }
109
+ .editor-link {
110
+ color: #2563eb;
111
+ text-decoration: underline;
112
+ }
113
+ .editor-link:hover {
114
+ color: #1e40af;
115
+ }
116
+ `;
117
+ // Initial editor config
118
+ function getEditorConfig(initialContent) {
119
+ return {
120
+ namespace: 'RichTextEditor',
121
+ theme: editorTheme,
122
+ onError: (error) => {
123
+ console.error('Lexical error:', error);
124
+ },
125
+ nodes: [
126
+ HeadingNode,
127
+ QuoteNode,
128
+ ListNode,
129
+ ListItemNode,
130
+ LinkNode,
131
+ ImageNode,
132
+ ImageLinkNode,
133
+ SimpleLinkNode,
134
+ ],
135
+ };
136
+ }
137
+ // Plugin to set initial content (supports both JSON and plain text)
138
+ function InitialContentPlugin({ content }) {
139
+ const [editor] = useLexicalComposerContext();
140
+ const isFirstRender = React.useRef(true);
141
+ useEffect(() => {
142
+ if (isFirstRender.current && content) {
143
+ isFirstRender.current = false;
144
+ // Try to parse as JSON (Lexical state)
145
+ try {
146
+ const parsed = JSON.parse(content);
147
+ if (parsed.root) {
148
+ // It's a Lexical state JSON
149
+ const editorState = editor.parseEditorState(parsed);
150
+ editor.setEditorState(editorState);
151
+ return;
152
+ }
153
+ }
154
+ catch {
155
+ // Not JSON, treat as plain text
156
+ }
157
+ // Fallback: plain text
158
+ editor.update(() => {
159
+ const root = $getRoot();
160
+ root.clear();
161
+ const paragraph = $createParagraphNode();
162
+ paragraph.append($createTextNode(content));
163
+ root.append(paragraph);
164
+ });
165
+ }
166
+ }, [editor, content]);
167
+ return null;
168
+ }
169
+ // Plugin to get editor reference
170
+ function EditorRefPlugin({ editorRef }) {
171
+ const [editor] = useLexicalComposerContext();
172
+ useEffect(() => {
173
+ editorRef.current = editor;
174
+ }, [editor, editorRef]);
175
+ return null;
176
+ }
177
+ export function RichTextEditor({ initialContent = '', onChange, onAIRequest, placeholder = 'Commencez à écrire...', className = '', minHeight = '300px' }) {
178
+ const editorRef = React.useRef(null);
179
+ const [isLoading, setIsLoading] = React.useState(false);
180
+ const [isFullscreen, setIsFullscreen] = React.useState(false);
181
+ const toggleFullscreen = useCallback(() => {
182
+ setIsFullscreen(prev => !prev);
183
+ }, []);
184
+ // Handle editor changes
185
+ const handleChange = useCallback((editorState, editor) => {
186
+ editorState.read(() => {
187
+ // Get HTML
188
+ const root = $getRoot();
189
+ const textContent = root.getTextContent();
190
+ // Get JSON
191
+ const json = JSON.stringify(editorState.toJSON());
192
+ // For now, pass text content as "html" - we can enhance this later
193
+ onChange?.(textContent, json);
194
+ });
195
+ }, [onChange]);
196
+ // Get selected text for AI operations
197
+ const getSelectedText = useCallback(() => {
198
+ if (!editorRef.current)
199
+ return undefined;
200
+ let selectedText;
201
+ editorRef.current.getEditorState().read(() => {
202
+ const selection = $getSelection();
203
+ if ($isRangeSelection(selection)) {
204
+ selectedText = selection.getTextContent();
205
+ }
206
+ });
207
+ return selectedText || undefined;
208
+ }, []);
209
+ // Get full text for AI operations
210
+ const getFullText = useCallback(() => {
211
+ if (!editorRef.current)
212
+ return '';
213
+ let fullText = '';
214
+ editorRef.current.getEditorState().read(() => {
215
+ fullText = $getRoot().getTextContent();
216
+ });
217
+ return fullText;
218
+ }, []);
219
+ // Handle AI actions
220
+ const handleAIAction = useCallback(async (action, customPrompt) => {
221
+ if (!onAIRequest || !editorRef.current)
222
+ return;
223
+ setIsLoading(true);
224
+ try {
225
+ const selectedText = getSelectedText();
226
+ const fullText = getFullText();
227
+ const result = await onAIRequest({
228
+ fullText,
229
+ selectedText,
230
+ action,
231
+ customPrompt
232
+ });
233
+ // Replace content with AI result
234
+ editorRef.current.update(() => {
235
+ const root = $getRoot();
236
+ root.clear();
237
+ const paragraph = $createParagraphNode();
238
+ paragraph.append($createTextNode(result));
239
+ root.append(paragraph);
240
+ });
241
+ }
242
+ catch (error) {
243
+ console.error('AI request failed:', error);
244
+ }
245
+ finally {
246
+ setIsLoading(false);
247
+ }
248
+ }, [onAIRequest, getSelectedText, getFullText]);
249
+ const fullscreenClasses = isFullscreen
250
+ ? 'fixed inset-0 z-50'
251
+ : '';
252
+ return (_jsxs("div", { className: `border border-gray-200 rounded-lg bg-white flex flex-col ${fullscreenClasses} ${className}`, children: [_jsx("style", { dangerouslySetInnerHTML: { __html: editorStyles } }), _jsxs(LexicalComposer, { initialConfig: getEditorConfig(initialContent), children: [_jsx(EditorToolbar, { onAIAction: handleAIAction, isLoading: isLoading, isFullscreen: isFullscreen, onToggleFullscreen: toggleFullscreen }), _jsxs("div", { className: "relative flex-1 overflow-auto", style: minHeight && !isFullscreen ? { minHeight } : {}, children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "outline-none p-4 text-gray-900 h-full min-h-full" }), placeholder: _jsx("div", { className: "absolute top-4 left-4 text-gray-400 pointer-events-none", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, {}), _jsx(ListPlugin, {}), _jsx(LinkPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange }), _jsx(InitialContentPlugin, { content: initialContent }), _jsx(EditorRefPlugin, { editorRef: editorRef }), _jsx(InsertObjectPlugin, {}), _jsx(ImageLinkPlugin, {}), _jsx(SimpleLinkPlugin, {}), isLoading && (_jsx("div", { className: "absolute inset-0 bg-white/80 flex items-center justify-center z-10", children: _jsxs("div", { className: "flex items-center gap-2 text-blue-600", children: [_jsx("span", { className: "material-icons animate-spin", children: "sync" }), _jsx("span", { children: "L'IA travaille..." })] }) }))] })] })] }));
253
+ }
@@ -0,0 +1,15 @@
1
+ export interface AIEditorState {
2
+ content: string;
3
+ isLoading: boolean;
4
+ error: string | null;
5
+ }
6
+ export interface AIEditorActions {
7
+ setContent: (content: string) => void;
8
+ undo: () => void;
9
+ redo: () => void;
10
+ canUndo: boolean;
11
+ canRedo: boolean;
12
+ applyAIEdit: (newContent: string) => void;
13
+ }
14
+ export declare function useAIEditor(initialContent?: string): [AIEditorState, AIEditorActions];
15
+ //# sourceMappingURL=useAIEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAIEditor.d.ts","sourceRoot":"","sources":["../../src/hooks/useAIEditor.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;CAC1C;AAED,wBAAgB,WAAW,CAAC,cAAc,GAAE,MAAW,GAAG,CAAC,aAAa,EAAE,eAAe,CAAC,CAqDzF"}
@@ -0,0 +1,46 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+ export function useAIEditor(initialContent = '') {
3
+ const [content, setContentState] = useState(initialContent);
4
+ const [isLoading, setIsLoading] = useState(false);
5
+ const [error, setError] = useState(null);
6
+ // History for undo/redo
7
+ const historyRef = useRef([{ content: initialContent, timestamp: Date.now() }]);
8
+ const historyIndexRef = useRef(0);
9
+ const [canUndo, setCanUndo] = useState(false);
10
+ const [canRedo, setCanRedo] = useState(false);
11
+ const updateUndoRedoState = useCallback(() => {
12
+ setCanUndo(historyIndexRef.current > 0);
13
+ setCanRedo(historyIndexRef.current < historyRef.current.length - 1);
14
+ }, []);
15
+ const setContent = useCallback((newContent) => {
16
+ // Add to history, removing any future states
17
+ historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1);
18
+ historyRef.current.push({ content: newContent, timestamp: Date.now() });
19
+ historyIndexRef.current = historyRef.current.length - 1;
20
+ setContentState(newContent);
21
+ updateUndoRedoState();
22
+ }, [updateUndoRedoState]);
23
+ const undo = useCallback(() => {
24
+ if (historyIndexRef.current > 0) {
25
+ historyIndexRef.current--;
26
+ setContentState(historyRef.current[historyIndexRef.current].content);
27
+ updateUndoRedoState();
28
+ }
29
+ }, [updateUndoRedoState]);
30
+ const redo = useCallback(() => {
31
+ if (historyIndexRef.current < historyRef.current.length - 1) {
32
+ historyIndexRef.current++;
33
+ setContentState(historyRef.current[historyIndexRef.current].content);
34
+ updateUndoRedoState();
35
+ }
36
+ }, [updateUndoRedoState]);
37
+ const applyAIEdit = useCallback((newContent) => {
38
+ setContent(newContent);
39
+ setIsLoading(false);
40
+ setError(null);
41
+ }, [setContent]);
42
+ return [
43
+ { content, isLoading, error },
44
+ { setContent, undo, redo, canUndo, canRedo, applyAIEdit }
45
+ ];
46
+ }
@@ -0,0 +1,12 @@
1
+ export interface SelectionInfo {
2
+ text: string;
3
+ startOffset: number;
4
+ endOffset: number;
5
+ originalStart: number;
6
+ originalEnd: number;
7
+ }
8
+ export declare function useSelection(containerRef: React.RefObject<HTMLElement>): {
9
+ selection: SelectionInfo | null;
10
+ clearSelection: () => void;
11
+ };
12
+ //# sourceMappingURL=useSelection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useSelection.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IAEjB,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC;;;EAmDtE"}
@@ -0,0 +1,45 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ export function useSelection(containerRef) {
3
+ const [selection, setSelection] = useState(null);
4
+ const handleSelectionChange = useCallback(() => {
5
+ const sel = window.getSelection();
6
+ if (!sel || sel.isCollapsed || !containerRef.current) {
7
+ setSelection(null);
8
+ return;
9
+ }
10
+ // Check if selection is within our container
11
+ const range = sel.getRangeAt(0);
12
+ if (!containerRef.current.contains(range.commonAncestorContainer)) {
13
+ setSelection(null);
14
+ return;
15
+ }
16
+ const text = sel.toString();
17
+ if (!text.trim()) {
18
+ setSelection(null);
19
+ return;
20
+ }
21
+ // Get the text content of the container up to the selection
22
+ const preSelectionRange = document.createRange();
23
+ preSelectionRange.selectNodeContents(containerRef.current);
24
+ preSelectionRange.setEnd(range.startContainer, range.startOffset);
25
+ const startOffset = preSelectionRange.toString().length;
26
+ setSelection({
27
+ text,
28
+ startOffset,
29
+ endOffset: startOffset + text.length,
30
+ originalStart: startOffset,
31
+ originalEnd: startOffset + text.length
32
+ });
33
+ }, [containerRef]);
34
+ useEffect(() => {
35
+ document.addEventListener('selectionchange', handleSelectionChange);
36
+ return () => {
37
+ document.removeEventListener('selectionchange', handleSelectionChange);
38
+ };
39
+ }, [handleSelectionChange]);
40
+ const clearSelection = useCallback(() => {
41
+ setSelection(null);
42
+ window.getSelection()?.removeAllRanges();
43
+ }, []);
44
+ return { selection, clearSelection };
45
+ }
@@ -0,0 +1,15 @@
1
+ export { RichTextEditor } from './components/RichTextEditor';
2
+ export type { RichTextEditorProps } from './components/RichTextEditor';
3
+ export { EditorToolbar } from './components/EditorToolbar';
4
+ export type { EditorToolbarProps } from './components/EditorToolbar';
5
+ export { ImageNode, $createImageNode, $isImageNode } from './nodes/ImageNode';
6
+ export type { ImagePayload, ImageFloatType } from './nodes/ImageNode';
7
+ export { ImageLinkNode, $createImageLinkNode, $isImageLinkNode, $wrapSelectionInImageLink } from './nodes/ImageLinkNode';
8
+ export type { ImageLinkPayload } from './nodes/ImageLinkNode';
9
+ export { SimpleLinkNode, $createSimpleLinkNode, $isSimpleLinkNode, $wrapSelectionInSimpleLink } from './nodes/LinkNode';
10
+ export type { SimpleLinkPayload } from './nodes/LinkNode';
11
+ export { InsertObjectPlugin, INSERT_OBJECT_COMMAND } from './plugins/InsertObjectPlugin';
12
+ export { ImageLinkPlugin } from './plugins/ImageLinkPlugin';
13
+ export { SimpleLinkPlugin } from './plugins/LinkPlugin';
14
+ export { RichTextEditor as AIEditor } from './components/RichTextEditor';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,YAAY,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAGtE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAGpE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAC7E,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AACxH,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE7D,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAA;AACvH,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAGzD,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACxF,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAGvD,OAAO,EAAE,cAAc,IAAI,QAAQ,EAAE,MAAM,6BAA6B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // Main WYSIWYG editor component
2
+ export { RichTextEditor } from './components/RichTextEditor';
3
+ // Toolbar component (for custom implementations)
4
+ export { EditorToolbar } from './components/EditorToolbar';
5
+ // Custom nodes
6
+ export { ImageNode, $createImageNode, $isImageNode } from './nodes/ImageNode';
7
+ export { ImageLinkNode, $createImageLinkNode, $isImageLinkNode, $wrapSelectionInImageLink } from './nodes/ImageLinkNode';
8
+ export { SimpleLinkNode, $createSimpleLinkNode, $isSimpleLinkNode, $wrapSelectionInSimpleLink } from './nodes/LinkNode';
9
+ // Plugins
10
+ export { InsertObjectPlugin, INSERT_OBJECT_COMMAND } from './plugins/InsertObjectPlugin';
11
+ export { ImageLinkPlugin } from './plugins/ImageLinkPlugin';
12
+ export { SimpleLinkPlugin } from './plugins/LinkPlugin';
13
+ // Alias for backwards compatibility
14
+ export { RichTextEditor as AIEditor } from './components/RichTextEditor';
@@ -0,0 +1,48 @@
1
+ import { ElementNode, LexicalNode, NodeKey, RangeSelection, SerializedElementNode, Spread } from 'lexical';
2
+ export interface ImageLinkPayload {
3
+ url: string;
4
+ altText?: string;
5
+ copyright?: string;
6
+ photographer?: string;
7
+ comment?: string;
8
+ }
9
+ export type SerializedImageLinkNode = Spread<{
10
+ url: string;
11
+ altText?: string;
12
+ copyright?: string;
13
+ photographer?: string;
14
+ comment?: string;
15
+ }, SerializedElementNode>;
16
+ export declare class ImageLinkNode extends ElementNode {
17
+ __url: string;
18
+ __altText?: string;
19
+ __copyright?: string;
20
+ __photographer?: string;
21
+ __comment?: string;
22
+ static getType(): string;
23
+ static clone(node: ImageLinkNode): ImageLinkNode;
24
+ constructor(url: string, altText?: string, copyright?: string, photographer?: string, comment?: string, key?: NodeKey);
25
+ createDOM(): HTMLElement;
26
+ updateDOM(): boolean;
27
+ getUrl(): string;
28
+ setUrl(url: string): void;
29
+ getAltText(): string | undefined;
30
+ setAltText(altText: string | undefined): void;
31
+ getCopyright(): string | undefined;
32
+ setCopyright(copyright: string | undefined): void;
33
+ getPhotographer(): string | undefined;
34
+ setPhotographer(photographer: string | undefined): void;
35
+ getComment(): string | undefined;
36
+ setComment(comment: string | undefined): void;
37
+ static importJSON(serializedNode: SerializedImageLinkNode): ImageLinkNode;
38
+ exportJSON(): SerializedImageLinkNode;
39
+ canInsertTextBefore(): boolean;
40
+ canInsertTextAfter(): boolean;
41
+ canBeEmpty(): boolean;
42
+ isInline(): boolean;
43
+ extractWithChild(): boolean;
44
+ }
45
+ export declare function $createImageLinkNode(payload: ImageLinkPayload): ImageLinkNode;
46
+ export declare function $isImageLinkNode(node: LexicalNode | null | undefined): node is ImageLinkNode;
47
+ export declare function $wrapSelectionInImageLink(selection: RangeSelection, url: string, altText?: string, copyright?: string, photographer?: string, comment?: string): void;
48
+ //# sourceMappingURL=ImageLinkNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageLinkNode.d.ts","sourceRoot":"","sources":["../../src/nodes/ImageLinkNode.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,WAAW,EACX,WAAW,EACX,OAAO,EACP,cAAc,EACd,qBAAqB,EACrB,MAAM,EACP,MAAM,SAAS,CAAA;AAGhB,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAC1C;IACE,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,EACD,qBAAqB,CACtB,CAAA;AAoED,qBAAa,aAAc,SAAQ,WAAW;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa;gBAY9C,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,OAAO;IAUf,SAAS,IAAI,WAAW;IAUxB,SAAS,IAAI,OAAO;IAIpB,MAAM,IAAI,MAAM;IAIhB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKzB,UAAU,IAAI,MAAM,GAAG,SAAS;IAIhC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAK7C,YAAY,IAAI,MAAM,GAAG,SAAS;IAIlC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKjD,eAAe,IAAI,MAAM,GAAG,SAAS;IAIrC,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKvD,UAAU,IAAI,MAAM,GAAG,SAAS;IAIhC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAK7C,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,uBAAuB,GAAG,aAAa;IAczE,UAAU,IAAI,uBAAuB;IAarC,mBAAmB,IAAI,OAAO;IAI9B,kBAAkB,IAAI,OAAO;IAI7B,UAAU,IAAI,OAAO;IAIrB,QAAQ,IAAI,OAAO;IAInB,gBAAgB,IAAI,OAAO;CAG5B;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAU7E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,aAAa,CAE5F;AAGD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,cAAc,EACzB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,MAAM,GACf,IAAI,CAYN"}
@@ -0,0 +1,157 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { $applyNodeReplacement, ElementNode, } from 'lexical';
4
+ // Fullscreen modal component for the image link
5
+ function ImageLinkFullscreen({ url, altText, onClose, }) {
6
+ return (_jsxs("div", { style: {
7
+ position: 'fixed',
8
+ inset: 0,
9
+ backgroundColor: 'rgba(0, 0, 0, 0.9)',
10
+ display: 'flex',
11
+ alignItems: 'center',
12
+ justifyContent: 'center',
13
+ zIndex: 100000,
14
+ cursor: 'pointer',
15
+ }, onClick: onClose, children: [_jsx("button", { onClick: onClose, style: {
16
+ position: 'absolute',
17
+ top: '20px',
18
+ right: '20px',
19
+ width: '40px',
20
+ height: '40px',
21
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
22
+ border: 'none',
23
+ borderRadius: '50%',
24
+ cursor: 'pointer',
25
+ display: 'flex',
26
+ alignItems: 'center',
27
+ justifyContent: 'center',
28
+ transition: 'background-color 0.2s',
29
+ }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.2)', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.1)', title: "Fermer", children: _jsx("span", { className: "material-icons", style: { fontSize: '24px', color: 'white' }, children: "close" }) }), _jsx("img", { src: url, alt: altText || '', style: {
30
+ maxWidth: '95vw',
31
+ maxHeight: '95vh',
32
+ objectFit: 'contain',
33
+ borderRadius: '4px',
34
+ boxShadow: '0 25px 50px rgba(0,0,0,0.5)',
35
+ }, onClick: (e) => e.stopPropagation() })] }));
36
+ }
37
+ export class ImageLinkNode extends ElementNode {
38
+ static getType() {
39
+ return 'image-link';
40
+ }
41
+ static clone(node) {
42
+ return new ImageLinkNode(node.__url, node.__altText, node.__copyright, node.__photographer, node.__comment, node.__key);
43
+ }
44
+ constructor(url, altText, copyright, photographer, comment, key) {
45
+ super(key);
46
+ this.__url = url;
47
+ this.__altText = altText;
48
+ this.__copyright = copyright;
49
+ this.__photographer = photographer;
50
+ this.__comment = comment;
51
+ }
52
+ createDOM() {
53
+ const span = document.createElement('span');
54
+ span.style.color = '#8b5cf6';
55
+ span.style.textDecoration = 'underline';
56
+ span.style.textDecorationStyle = 'dotted';
57
+ span.style.cursor = 'pointer';
58
+ span.title = 'Lien image - Ctrl+Clic pour voir';
59
+ return span;
60
+ }
61
+ updateDOM() {
62
+ return false;
63
+ }
64
+ getUrl() {
65
+ return this.__url;
66
+ }
67
+ setUrl(url) {
68
+ const writable = this.getWritable();
69
+ writable.__url = url;
70
+ }
71
+ getAltText() {
72
+ return this.__altText;
73
+ }
74
+ setAltText(altText) {
75
+ const writable = this.getWritable();
76
+ writable.__altText = altText;
77
+ }
78
+ getCopyright() {
79
+ return this.__copyright;
80
+ }
81
+ setCopyright(copyright) {
82
+ const writable = this.getWritable();
83
+ writable.__copyright = copyright;
84
+ }
85
+ getPhotographer() {
86
+ return this.__photographer;
87
+ }
88
+ setPhotographer(photographer) {
89
+ const writable = this.getWritable();
90
+ writable.__photographer = photographer;
91
+ }
92
+ getComment() {
93
+ return this.__comment;
94
+ }
95
+ setComment(comment) {
96
+ const writable = this.getWritable();
97
+ writable.__comment = comment;
98
+ }
99
+ static importJSON(serializedNode) {
100
+ const node = $createImageLinkNode({
101
+ url: serializedNode.url,
102
+ altText: serializedNode.altText,
103
+ copyright: serializedNode.copyright,
104
+ photographer: serializedNode.photographer,
105
+ comment: serializedNode.comment,
106
+ });
107
+ node.setFormat(serializedNode.format);
108
+ node.setIndent(serializedNode.indent);
109
+ node.setDirection(serializedNode.direction);
110
+ return node;
111
+ }
112
+ exportJSON() {
113
+ return {
114
+ ...super.exportJSON(),
115
+ type: 'image-link',
116
+ url: this.__url,
117
+ altText: this.__altText,
118
+ copyright: this.__copyright,
119
+ photographer: this.__photographer,
120
+ comment: this.__comment,
121
+ version: 1,
122
+ };
123
+ }
124
+ canInsertTextBefore() {
125
+ return false;
126
+ }
127
+ canInsertTextAfter() {
128
+ return false;
129
+ }
130
+ canBeEmpty() {
131
+ return false;
132
+ }
133
+ isInline() {
134
+ return true;
135
+ }
136
+ extractWithChild() {
137
+ return true;
138
+ }
139
+ }
140
+ export function $createImageLinkNode(payload) {
141
+ return $applyNodeReplacement(new ImageLinkNode(payload.url, payload.altText, payload.copyright, payload.photographer, payload.comment));
142
+ }
143
+ export function $isImageLinkNode(node) {
144
+ return node instanceof ImageLinkNode;
145
+ }
146
+ // Helper to wrap selection in an image link
147
+ export function $wrapSelectionInImageLink(selection, url, altText, copyright, photographer, comment) {
148
+ const nodes = selection.extract();
149
+ const imageLinkNode = $createImageLinkNode({ url, altText, copyright, photographer, comment });
150
+ if (nodes.length > 0) {
151
+ const firstNode = nodes[0];
152
+ firstNode.insertBefore(imageLinkNode);
153
+ for (const node of nodes) {
154
+ imageLinkNode.append(node);
155
+ }
156
+ }
157
+ }