@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 AIEditorProps {
2
+ initialContent?: string;
3
+ onChange?: (content: 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 AIEditor({ initialContent, onChange, onAIRequest, placeholder, className, minHeight }: AIEditorProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=AIEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIEditor.d.ts","sourceRoot":"","sources":["../../src/components/AIEditor.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,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;AAED,wBAAgB,QAAQ,CAAC,EACvB,cAAmB,EACnB,QAAQ,EACR,WAAW,EACX,WAAqC,EACrC,SAAc,EACd,SAAmB,EACpB,EAAE,aAAa,2CAuOf"}
@@ -0,0 +1,154 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState, useCallback, useEffect } from 'react';
3
+ import { useAIEditor } from '../hooks/useAIEditor';
4
+ import { useSelection } from '../hooks/useSelection';
5
+ import { AIToolbar } from './AIToolbar';
6
+ import { PromptModal } from './PromptModal';
7
+ import { MarkdownPreview } from './MarkdownPreview';
8
+ export function AIEditor({ initialContent = '', onChange, onAIRequest, placeholder = 'Commencez à écrire...', className = '', minHeight = '300px' }) {
9
+ const previewRef = useRef(null);
10
+ const textareaRef = useRef(null);
11
+ const [state, actions] = useAIEditor(initialContent);
12
+ const { selection, clearSelection } = useSelection(previewRef);
13
+ const [isEditMode, setIsEditMode] = useState(true); // Start in edit mode
14
+ const [isPromptModalOpen, setIsPromptModalOpen] = useState(false);
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ // Helper to wrap selected text with markdown syntax
17
+ const wrapSelection = useCallback((before, after) => {
18
+ const textarea = textareaRef.current;
19
+ if (!textarea)
20
+ return;
21
+ const start = textarea.selectionStart;
22
+ const end = textarea.selectionEnd;
23
+ const text = state.content;
24
+ const selectedText = text.substring(start, end);
25
+ const newText = text.substring(0, start) + before + selectedText + after + text.substring(end);
26
+ actions.setContent(newText);
27
+ // Restore cursor position after the inserted text
28
+ setTimeout(() => {
29
+ textarea.focus();
30
+ const newCursorPos = start + before.length + selectedText.length + after.length;
31
+ textarea.setSelectionRange(newCursorPos, newCursorPos);
32
+ }, 0);
33
+ }, [state.content, actions]);
34
+ // Helper to insert text at line start
35
+ const insertAtLineStart = useCallback((prefix) => {
36
+ const textarea = textareaRef.current;
37
+ if (!textarea)
38
+ return;
39
+ const start = textarea.selectionStart;
40
+ const text = state.content;
41
+ // Find the start of the current line
42
+ let lineStart = start;
43
+ while (lineStart > 0 && text[lineStart - 1] !== '\n') {
44
+ lineStart--;
45
+ }
46
+ const newText = text.substring(0, lineStart) + prefix + text.substring(lineStart);
47
+ actions.setContent(newText);
48
+ setTimeout(() => {
49
+ textarea.focus();
50
+ textarea.setSelectionRange(start + prefix.length, start + prefix.length);
51
+ }, 0);
52
+ }, [state.content, actions]);
53
+ // Formatting handlers
54
+ const handleBold = useCallback(() => wrapSelection('**', '**'), [wrapSelection]);
55
+ const handleItalic = useCallback(() => wrapSelection('*', '*'), [wrapSelection]);
56
+ const handleHeading = useCallback((level) => {
57
+ const prefix = '#'.repeat(level) + ' ';
58
+ insertAtLineStart(prefix);
59
+ }, [insertAtLineStart]);
60
+ const handleBulletList = useCallback(() => insertAtLineStart('- '), [insertAtLineStart]);
61
+ const handleNumberedList = useCallback(() => insertAtLineStart('1. '), [insertAtLineStart]);
62
+ const handleQuote = useCallback(() => insertAtLineStart('> '), [insertAtLineStart]);
63
+ const handleLink = useCallback(() => {
64
+ const textarea = textareaRef.current;
65
+ if (!textarea)
66
+ return;
67
+ const start = textarea.selectionStart;
68
+ const end = textarea.selectionEnd;
69
+ const text = state.content;
70
+ const selectedText = text.substring(start, end) || 'texte';
71
+ const newText = text.substring(0, start) + `[${selectedText}](url)` + text.substring(end);
72
+ actions.setContent(newText);
73
+ setTimeout(() => {
74
+ textarea.focus();
75
+ // Select "url" for easy replacement
76
+ const urlStart = start + selectedText.length + 3;
77
+ textarea.setSelectionRange(urlStart, urlStart + 3);
78
+ }, 0);
79
+ }, [state.content, actions]);
80
+ // Notify parent of changes
81
+ useEffect(() => {
82
+ onChange?.(state.content);
83
+ }, [state.content, onChange]);
84
+ // Keyboard shortcuts
85
+ useEffect(() => {
86
+ const handleKeyDown = (e) => {
87
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
88
+ if (e.shiftKey) {
89
+ actions.redo();
90
+ }
91
+ else {
92
+ actions.undo();
93
+ }
94
+ e.preventDefault();
95
+ }
96
+ if ((e.ctrlKey || e.metaKey) && e.key === 'y') {
97
+ actions.redo();
98
+ e.preventDefault();
99
+ }
100
+ };
101
+ window.addEventListener('keydown', handleKeyDown);
102
+ return () => window.removeEventListener('keydown', handleKeyDown);
103
+ }, [actions]);
104
+ const handleAIAction = useCallback(async (action, customPrompt) => {
105
+ if (!onAIRequest) {
106
+ console.warn('AIEditor: onAIRequest handler not provided');
107
+ return;
108
+ }
109
+ setIsLoading(true);
110
+ try {
111
+ const result = await onAIRequest({
112
+ fullText: state.content,
113
+ selectedText: selection?.text,
114
+ action,
115
+ customPrompt
116
+ });
117
+ if (selection) {
118
+ // Replace only the selected portion
119
+ const before = state.content.substring(0, selection.originalStart);
120
+ const after = state.content.substring(selection.originalEnd);
121
+ actions.setContent(before + result + after);
122
+ clearSelection();
123
+ }
124
+ else {
125
+ // Replace entire content
126
+ actions.setContent(result);
127
+ }
128
+ }
129
+ catch (error) {
130
+ console.error('AI request failed:', error);
131
+ }
132
+ finally {
133
+ setIsLoading(false);
134
+ setIsPromptModalOpen(false);
135
+ }
136
+ }, [state.content, selection, onAIRequest, actions, clearSelection]);
137
+ const handleRewrite = useCallback(() => {
138
+ handleAIAction('rewrite');
139
+ }, [handleAIAction]);
140
+ const handleProofread = useCallback(() => {
141
+ handleAIAction('proofread');
142
+ }, [handleAIAction]);
143
+ const handleCustomPrompt = useCallback(() => {
144
+ setIsPromptModalOpen(true);
145
+ }, []);
146
+ const handlePromptSubmit = useCallback((prompt) => {
147
+ handleAIAction('custom', prompt);
148
+ }, [handleAIAction]);
149
+ return (_jsxs("div", { className: `border border-gray-200 rounded-lg overflow-hidden ${className}`, children: [_jsx(AIToolbar, { onRewrite: handleRewrite, onProofread: handleProofread, onCustomPrompt: handleCustomPrompt, onUndo: actions.undo, onRedo: actions.redo, canUndo: actions.canUndo, canRedo: actions.canRedo, hasSelection: !!selection, isLoading: isLoading, isEditMode: isEditMode, onToggleMode: () => setIsEditMode(!isEditMode), onBold: handleBold, onItalic: handleItalic, onHeading: handleHeading, onBulletList: handleBulletList, onNumberedList: handleNumberedList, onLink: handleLink, onQuote: handleQuote }), _jsxs("div", { className: "relative", style: { minHeight }, children: [isEditMode ? (
150
+ // Edit mode - raw markdown
151
+ _jsx("textarea", { ref: textareaRef, value: state.content, onChange: (e) => actions.setContent(e.target.value), className: "w-full h-full p-4 font-mono text-sm resize-none border-0 focus:ring-0 focus:outline-none text-gray-900", style: { minHeight }, placeholder: placeholder })) : (
152
+ // Preview mode - rendered markdown with selection
153
+ _jsx("div", { className: "p-4 overflow-auto", style: { minHeight }, children: state.content ? (_jsx(MarkdownPreview, { ref: previewRef, content: state.content, className: "cursor-text" })) : (_jsx("p", { className: "text-gray-400 italic", children: placeholder })) })), isLoading && (_jsx("div", { className: "absolute inset-0 bg-white/80 flex items-center justify-center", 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..." })] }) }))] }), _jsx(PromptModal, { isOpen: isPromptModalOpen, onClose: () => setIsPromptModalOpen(false), onSubmit: handlePromptSubmit, selectedText: selection?.text, isLoading: isLoading })] }));
154
+ }
@@ -0,0 +1,22 @@
1
+ export interface AIToolbarProps {
2
+ onRewrite: () => void;
3
+ onProofread: () => void;
4
+ onCustomPrompt: () => void;
5
+ onUndo: () => void;
6
+ onRedo: () => void;
7
+ canUndo: boolean;
8
+ canRedo: boolean;
9
+ hasSelection: boolean;
10
+ isLoading: boolean;
11
+ isEditMode: boolean;
12
+ onToggleMode: () => void;
13
+ onBold?: () => void;
14
+ onItalic?: () => void;
15
+ onHeading?: (level: 1 | 2 | 3) => void;
16
+ onBulletList?: () => void;
17
+ onNumberedList?: () => void;
18
+ onLink?: () => void;
19
+ onQuote?: () => void;
20
+ }
21
+ export declare function AIToolbar({ onRewrite, onProofread, onCustomPrompt, onUndo, onRedo, canUndo, canRedo, hasSelection, isLoading, isEditMode, onToggleMode, onBold, onItalic, onHeading, onBulletList, onNumberedList, onLink, onQuote }: AIToolbarProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=AIToolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIToolbar.d.ts","sourceRoot":"","sources":["../../src/components/AIToolbar.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,YAAY,EAAE,MAAM,IAAI,CAAA;IAExB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,CAAA;IACtC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,WAAW,EACX,cAAc,EACd,MAAM,EACN,MAAM,EACN,OAAO,EACP,OAAO,EACP,YAAY,EACZ,SAAS,EACT,UAAU,EACV,YAAY,EACZ,MAAM,EACN,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,cAAc,EACd,MAAM,EACN,OAAO,EACR,EAAE,cAAc,2CA8LhB"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ export function AIToolbar({ onRewrite, onProofread, onCustomPrompt, onUndo, onRedo, canUndo, canRedo, hasSelection, isLoading, isEditMode, onToggleMode, onBold, onItalic, onHeading, onBulletList, onNumberedList, onLink, onQuote }) {
4
+ const [showHeadingMenu, setShowHeadingMenu] = React.useState(false);
5
+ return (_jsxs("div", { className: "flex flex-wrap items-center gap-1 p-2 bg-gray-50 border-b border-gray-200 rounded-t-lg", children: [_jsxs("div", { className: "flex items-center bg-gray-200 rounded-lg p-0.5", children: [_jsx("button", { onClick: onToggleMode, className: `px-2 py-1 text-sm rounded-md transition-colors ${!isEditMode
6
+ ? 'bg-white text-gray-900 shadow-sm'
7
+ : 'text-gray-600 hover:text-gray-900'}`, title: "Mode aper\u00E7u", children: _jsx("span", { className: "material-icons text-base", children: "visibility" }) }), _jsx("button", { onClick: onToggleMode, className: `px-2 py-1 text-sm rounded-md transition-colors ${isEditMode
8
+ ? 'bg-white text-gray-900 shadow-sm'
9
+ : 'text-gray-600 hover:text-gray-900'}`, title: "Mode \u00E9dition", children: _jsx("span", { className: "material-icons text-base", children: "edit" }) })] }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), isEditMode && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: onBold, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Gras (Ctrl+B)", children: _jsx("span", { className: "material-icons text-lg", children: "format_bold" }) }), _jsx("button", { onClick: onItalic, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Italique (Ctrl+I)", children: _jsx("span", { className: "material-icons text-lg", children: "format_italic" }) }), _jsxs("div", { className: "relative", children: [_jsxs("button", { onClick: () => setShowHeadingMenu(!showHeadingMenu), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors flex items-center", title: "Titres", children: [_jsx("span", { className: "material-icons text-lg", children: "title" }), _jsx("span", { className: "material-icons text-sm", children: "arrow_drop_down" })] }), showHeadingMenu && (_jsxs("div", { className: "absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-10 py-1", children: [_jsx("button", { onClick: () => { onHeading?.(1); setShowHeadingMenu(false); }, className: "block w-full px-4 py-1.5 text-left text-lg font-bold hover:bg-gray-100", children: "Titre 1" }), _jsx("button", { onClick: () => { onHeading?.(2); setShowHeadingMenu(false); }, className: "block w-full px-4 py-1.5 text-left text-base font-bold hover:bg-gray-100", children: "Titre 2" }), _jsx("button", { onClick: () => { onHeading?.(3); setShowHeadingMenu(false); }, className: "block w-full px-4 py-1.5 text-left text-sm font-bold hover:bg-gray-100", children: "Titre 3" })] }))] }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: onBulletList, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Liste \u00E0 puces", children: _jsx("span", { className: "material-icons text-lg", children: "format_list_bulleted" }) }), _jsx("button", { onClick: onNumberedList, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Liste num\u00E9rot\u00E9e", children: _jsx("span", { className: "material-icons text-lg", children: "format_list_numbered" }) }), _jsx("button", { onClick: onQuote, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Citation", children: _jsx("span", { className: "material-icons text-lg", children: "format_quote" }) }), _jsx("button", { onClick: onLink, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Lien", children: _jsx("span", { className: "material-icons text-lg", children: "link" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" })] })), _jsxs("button", { onClick: onRewrite, disabled: isLoading, className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", title: hasSelection ? "Réécrire la sélection" : "Réécrire tout le texte", children: [_jsx("span", { className: "material-icons text-base", children: "auto_fix_high" }), "R\u00E9\u00E9crire"] }), _jsxs("button", { onClick: onProofread, disabled: isLoading, className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", title: hasSelection ? "Corriger la sélection" : "Corriger tout le texte", children: [_jsx("span", { className: "material-icons text-base", children: "spellcheck" }), "Proof reading"] }), _jsxs("button", { onClick: onCustomPrompt, disabled: isLoading, className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", title: "Instruction personnalis\u00E9e", children: [_jsx("span", { className: "material-icons text-base", children: "smart_toy" }), "Custom..."] }), _jsx("div", { className: "flex-1" }), hasSelection && (_jsx("span", { className: "text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded", children: "Texte s\u00E9lectionn\u00E9" })), isLoading && (_jsxs("span", { className: "text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded flex items-center gap-1", children: [_jsx("span", { className: "material-icons text-sm animate-spin", children: "sync" }), "IA en cours..."] })), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: onUndo, disabled: !canUndo || isLoading, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded disabled:opacity-30 disabled:cursor-not-allowed transition-colors", title: "Annuler (Ctrl+Z)", children: _jsx("span", { className: "material-icons text-xl", children: "undo" }) }), _jsx("button", { onClick: onRedo, disabled: !canRedo || isLoading, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded disabled:opacity-30 disabled:cursor-not-allowed transition-colors", title: "Refaire (Ctrl+Y)", children: _jsx("span", { className: "material-icons text-xl", children: "redo" }) })] }));
10
+ }
@@ -0,0 +1,8 @@
1
+ export interface EditorToolbarProps {
2
+ onAIAction?: (action: 'rewrite' | 'proofread' | 'custom', customPrompt?: string) => void;
3
+ isLoading?: boolean;
4
+ isFullscreen?: boolean;
5
+ onToggleFullscreen?: () => void;
6
+ }
7
+ export declare function EditorToolbar({ onAIAction, isLoading, isFullscreen, onToggleFullscreen }: EditorToolbarProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=EditorToolbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditorToolbar.d.ts","sourceRoot":"","sources":["../../src/components/EditorToolbar.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxF,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAA;CAChC;AA0DD,wBAAgB,aAAa,CAAC,EAAE,UAAU,EAAE,SAAiB,EAAE,YAAoB,EAAE,kBAAkB,EAAE,EAAE,kBAAkB,2CA+iB5H"}
@@ -0,0 +1,241 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useState, useRef } from 'react';
4
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
5
+ import { FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, UNDO_COMMAND, REDO_COMMAND, $getSelection, $isRangeSelection, $selectAll, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, COMMAND_PRIORITY_CRITICAL, } from 'lexical';
6
+ import { INSERT_OBJECT_COMMAND } from '../plugins/InsertObjectPlugin';
7
+ import { INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, } from '@lexical/list';
8
+ import { $createHeadingNode, $createQuoteNode, } from '@lexical/rich-text';
9
+ import { $setBlocksType, $patchStyleText } from '@lexical/selection';
10
+ import { $createParagraphNode } from 'lexical';
11
+ const FONT_FAMILIES = [
12
+ { label: 'Par défaut', value: '' },
13
+ { label: 'Arial', value: 'Arial, sans-serif' },
14
+ { label: 'Times New Roman', value: 'Times New Roman, serif' },
15
+ { label: 'Georgia', value: 'Georgia, serif' },
16
+ { label: 'Courier New', value: 'Courier New, monospace' },
17
+ { label: 'Verdana', value: 'Verdana, sans-serif' },
18
+ { label: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
19
+ { label: 'Comic Sans MS', value: 'Comic Sans MS, cursive' },
20
+ ];
21
+ // Dropdown component using fixed positioning to escape overflow containers
22
+ function FixedDropdown({ show, buttonRef, children }) {
23
+ const [position, setPosition] = useState({ top: 0, left: 0 });
24
+ useEffect(() => {
25
+ if (show && buttonRef.current) {
26
+ const rect = buttonRef.current.getBoundingClientRect();
27
+ setPosition({
28
+ top: rect.bottom + 4,
29
+ left: rect.left
30
+ });
31
+ }
32
+ }, [show, buttonRef]);
33
+ if (!show)
34
+ return null;
35
+ return (_jsx("div", { style: {
36
+ position: 'fixed',
37
+ top: position.top,
38
+ left: position.left,
39
+ zIndex: 99999,
40
+ backgroundColor: 'white',
41
+ border: '1px solid #e5e7eb',
42
+ borderRadius: '8px',
43
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
44
+ padding: '4px 0',
45
+ minWidth: '180px'
46
+ }, onClick: (e) => e.stopPropagation(), children: children }));
47
+ }
48
+ export function EditorToolbar({ onAIAction, isLoading = false, isFullscreen = false, onToggleFullscreen }) {
49
+ const [editor] = useLexicalComposerContext();
50
+ const [canUndo, setCanUndo] = useState(false);
51
+ const [canRedo, setCanRedo] = useState(false);
52
+ const [isBold, setIsBold] = useState(false);
53
+ const [isItalic, setIsItalic] = useState(false);
54
+ const [isUnderline, setIsUnderline] = useState(false);
55
+ const [isStrikethrough, setIsStrikethrough] = useState(false);
56
+ const [showHeadingMenu, setShowHeadingMenu] = useState(false);
57
+ const [showAIMenu, setShowAIMenu] = useState(false);
58
+ const [showFontMenu, setShowFontMenu] = useState(false);
59
+ const [currentFont, setCurrentFont] = useState('');
60
+ const [fontSize, setFontSize] = useState(16);
61
+ const fontButtonRef = useRef(null);
62
+ const headingButtonRef = useRef(null);
63
+ const aiButtonRef = useRef(null);
64
+ const toolbarRef = useRef(null);
65
+ // Update toolbar state based on selection
66
+ const updateToolbar = useCallback(() => {
67
+ const selection = $getSelection();
68
+ if ($isRangeSelection(selection)) {
69
+ setIsBold(selection.hasFormat('bold'));
70
+ setIsItalic(selection.hasFormat('italic'));
71
+ setIsUnderline(selection.hasFormat('underline'));
72
+ setIsStrikethrough(selection.hasFormat('strikethrough'));
73
+ }
74
+ }, []);
75
+ // Register listeners
76
+ useEffect(() => {
77
+ return editor.registerUpdateListener(({ editorState }) => {
78
+ editorState.read(() => {
79
+ updateToolbar();
80
+ });
81
+ });
82
+ }, [editor, updateToolbar]);
83
+ // Register undo/redo commands
84
+ useEffect(() => {
85
+ return editor.registerCommand(CAN_UNDO_COMMAND, (payload) => {
86
+ setCanUndo(payload);
87
+ return false;
88
+ }, COMMAND_PRIORITY_CRITICAL);
89
+ }, [editor]);
90
+ useEffect(() => {
91
+ return editor.registerCommand(CAN_REDO_COMMAND, (payload) => {
92
+ setCanRedo(payload);
93
+ return false;
94
+ }, COMMAND_PRIORITY_CRITICAL);
95
+ }, [editor]);
96
+ // Format handlers
97
+ const formatBold = () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
98
+ const formatItalic = () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
99
+ const formatUnderline = () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
100
+ const formatStrikethrough = () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
101
+ const formatHeading = (headingTag) => {
102
+ editor.update(() => {
103
+ const selection = $getSelection();
104
+ if ($isRangeSelection(selection)) {
105
+ $setBlocksType(selection, () => $createHeadingNode(headingTag));
106
+ }
107
+ });
108
+ setShowHeadingMenu(false);
109
+ };
110
+ const formatParagraph = () => {
111
+ editor.update(() => {
112
+ const selection = $getSelection();
113
+ if ($isRangeSelection(selection)) {
114
+ $setBlocksType(selection, () => $createParagraphNode());
115
+ }
116
+ });
117
+ setShowHeadingMenu(false);
118
+ };
119
+ const formatQuote = () => {
120
+ editor.update(() => {
121
+ const selection = $getSelection();
122
+ if ($isRangeSelection(selection)) {
123
+ $setBlocksType(selection, () => $createQuoteNode());
124
+ }
125
+ });
126
+ };
127
+ const openInsertDialog = () => {
128
+ editor.dispatchCommand(INSERT_OBJECT_COMMAND, undefined);
129
+ };
130
+ const formatAlign = (alignment) => {
131
+ editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment);
132
+ };
133
+ const formatBulletList = () => {
134
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
135
+ };
136
+ const formatNumberedList = () => {
137
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
138
+ };
139
+ const undo = () => editor.dispatchCommand(UNDO_COMMAND, undefined);
140
+ const redo = () => editor.dispatchCommand(REDO_COMMAND, undefined);
141
+ // Close menus when clicking outside the toolbar
142
+ useEffect(() => {
143
+ const handleClickOutside = (e) => {
144
+ if (toolbarRef.current && !toolbarRef.current.contains(e.target)) {
145
+ setShowHeadingMenu(false);
146
+ setShowAIMenu(false);
147
+ setShowFontMenu(false);
148
+ }
149
+ };
150
+ document.addEventListener('mousedown', handleClickOutside);
151
+ return () => document.removeEventListener('mousedown', handleClickOutside);
152
+ }, []);
153
+ // Apply font family to selection
154
+ const applyFontFamily = (fontFamily) => {
155
+ editor.update(() => {
156
+ let selection = $getSelection();
157
+ if (!$isRangeSelection(selection) || selection.isCollapsed()) {
158
+ $selectAll();
159
+ selection = $getSelection();
160
+ }
161
+ if ($isRangeSelection(selection)) {
162
+ $patchStyleText(selection, { 'font-family': fontFamily || null });
163
+ }
164
+ });
165
+ setCurrentFont(fontFamily);
166
+ setShowFontMenu(false);
167
+ };
168
+ // Apply font size to selection
169
+ const applyFontSize = (size) => {
170
+ editor.update(() => {
171
+ let selection = $getSelection();
172
+ if (!$isRangeSelection(selection) || selection.isCollapsed()) {
173
+ $selectAll();
174
+ selection = $getSelection();
175
+ }
176
+ if ($isRangeSelection(selection)) {
177
+ $patchStyleText(selection, { 'font-size': `${size}px` });
178
+ }
179
+ });
180
+ setFontSize(size);
181
+ };
182
+ const increaseFontSize = () => {
183
+ const newSize = Math.min(fontSize + 2, 72);
184
+ applyFontSize(newSize);
185
+ };
186
+ const decreaseFontSize = () => {
187
+ const newSize = Math.max(fontSize - 2, 8);
188
+ applyFontSize(newSize);
189
+ };
190
+ const handleFontSizeChange = (e) => {
191
+ const value = parseInt(e.target.value, 10);
192
+ if (!isNaN(value) && value >= 8 && value <= 72) {
193
+ applyFontSize(value);
194
+ }
195
+ else {
196
+ setFontSize(value || 16);
197
+ }
198
+ };
199
+ const handleFontSizeBlur = () => {
200
+ if (fontSize < 8)
201
+ setFontSize(8);
202
+ if (fontSize > 72)
203
+ setFontSize(72);
204
+ };
205
+ return (_jsxs("div", { ref: toolbarRef, className: "flex flex-wrap items-center gap-1 p-2 bg-gray-50 border-b border-gray-200 flex-shrink-0", children: [_jsx("button", { onClick: undo, disabled: !canUndo, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded disabled:opacity-30 disabled:cursor-not-allowed transition-colors", title: "Annuler (Ctrl+Z)", children: _jsx("span", { className: "material-icons text-lg", children: "undo" }) }), _jsx("button", { onClick: redo, disabled: !canRedo, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded disabled:opacity-30 disabled:cursor-not-allowed transition-colors", title: "Refaire (Ctrl+Y)", children: _jsx("span", { className: "material-icons text-lg", children: "redo" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: formatBold, className: `p-1.5 rounded transition-colors ${isBold
206
+ ? 'bg-gray-200 text-gray-900'
207
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-200'}`, title: "Gras (Ctrl+B)", children: _jsx("span", { className: "material-icons text-lg", children: "format_bold" }) }), _jsx("button", { onClick: formatItalic, className: `p-1.5 rounded transition-colors ${isItalic
208
+ ? 'bg-gray-200 text-gray-900'
209
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-200'}`, title: "Italique (Ctrl+I)", children: _jsx("span", { className: "material-icons text-lg", children: "format_italic" }) }), _jsx("button", { onClick: formatUnderline, className: `p-1.5 rounded transition-colors ${isUnderline
210
+ ? 'bg-gray-200 text-gray-900'
211
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-200'}`, title: "Soulign\u00E9 (Ctrl+U)", children: _jsx("span", { className: "material-icons text-lg", children: "format_underlined" }) }), _jsx("button", { onClick: formatStrikethrough, className: `p-1.5 rounded transition-colors ${isStrikethrough
212
+ ? 'bg-gray-200 text-gray-900'
213
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-200'}`, title: "Barr\u00E9", children: _jsx("span", { className: "material-icons text-lg", children: "strikethrough_s" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsxs("button", { ref: fontButtonRef, onClick: (e) => {
214
+ e.stopPropagation();
215
+ setShowFontMenu(!showFontMenu);
216
+ setShowHeadingMenu(false);
217
+ setShowAIMenu(false);
218
+ }, className: "h-8 px-2 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors flex items-center gap-1 text-sm border border-gray-300 bg-white min-w-[100px]", title: "Police", children: [_jsx("span", { className: "truncate flex-1 text-left", style: { fontFamily: currentFont || 'inherit' }, children: FONT_FAMILIES.find(f => f.value === currentFont)?.label || 'Police' }), _jsx("span", { className: "material-icons text-sm", children: "arrow_drop_down" })] }), _jsxs(FixedDropdown, { show: showFontMenu, buttonRef: fontButtonRef, children: [_jsx("button", { onClick: () => applyFontFamily(''), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Par d\u00E9faut" }), _jsx("button", { onClick: () => applyFontFamily('Arial, sans-serif'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Arial, sans-serif' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Arial" }), _jsx("button", { onClick: () => applyFontFamily('Times New Roman, serif'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Times New Roman, serif' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Times New Roman" }), _jsx("button", { onClick: () => applyFontFamily('Georgia, serif'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Georgia, serif' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Georgia" }), _jsx("button", { onClick: () => applyFontFamily('Courier New, monospace'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Courier New, monospace' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Courier New" }), _jsx("button", { onClick: () => applyFontFamily('Verdana, sans-serif'), style: { display: 'block', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', fontFamily: 'Verdana, sans-serif' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Verdana" })] })] }), _jsxs("div", { className: "flex items-center border border-gray-300 rounded bg-white h-8", children: [_jsx("button", { onClick: decreaseFontSize, className: "px-1 h-full text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors", title: "R\u00E9duire la taille", children: _jsx("span", { className: "material-icons text-sm", children: "remove" }) }), _jsx("input", { type: "number", value: fontSize, onChange: handleFontSizeChange, onBlur: handleFontSizeBlur, className: "w-10 h-full text-center text-sm text-gray-900 border-x border-gray-300 focus:outline-none", min: 8, max: 72, title: "Taille de police (px)" }), _jsx("button", { onClick: increaseFontSize, className: "px-1 h-full text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors", title: "Augmenter la taille", children: _jsx("span", { className: "material-icons text-sm", children: "add" }) })] }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsxs("div", { style: { position: 'relative' }, children: [_jsxs("button", { ref: headingButtonRef, onClick: (e) => {
219
+ e.stopPropagation();
220
+ setShowHeadingMenu(!showHeadingMenu);
221
+ setShowAIMenu(false);
222
+ setShowFontMenu(false);
223
+ }, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors flex items-center", title: "Titres", children: [_jsx("span", { className: "material-icons text-lg", children: "title" }), _jsx("span", { className: "material-icons text-sm", children: "arrow_drop_down" })] }), _jsxs(FixedDropdown, { show: showHeadingMenu, buttonRef: headingButtonRef, children: [_jsx("button", { onClick: formatParagraph, style: { display: 'block', width: '100%', padding: '6px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Paragraphe" }), _jsx("button", { onClick: () => formatHeading('h1'), style: { display: 'block', width: '100%', padding: '6px 16px', textAlign: 'left', fontSize: '20px', fontWeight: 'bold', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Titre 1" }), _jsx("button", { onClick: () => formatHeading('h2'), style: { display: 'block', width: '100%', padding: '6px 16px', textAlign: 'left', fontSize: '18px', fontWeight: 'bold', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Titre 2" }), _jsx("button", { onClick: () => formatHeading('h3'), style: { display: 'block', width: '100%', padding: '6px 16px', textAlign: 'left', fontSize: '16px', fontWeight: '600', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: "Titre 3" })] })] }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: formatBulletList, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Liste \u00E0 puces", children: _jsx("span", { className: "material-icons text-lg", children: "format_list_bulleted" }) }), _jsx("button", { onClick: formatNumberedList, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Liste num\u00E9rot\u00E9e", children: _jsx("span", { className: "material-icons text-lg", children: "format_list_numbered" }) }), _jsx("button", { onClick: formatQuote, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Citation", children: _jsx("span", { className: "material-icons text-lg", children: "format_quote" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: () => formatAlign('left'), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Aligner \u00E0 gauche", children: _jsx("span", { className: "material-icons text-lg", children: "format_align_left" }) }), _jsx("button", { onClick: () => formatAlign('center'), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Centrer", children: _jsx("span", { className: "material-icons text-lg", children: "format_align_center" }) }), _jsx("button", { onClick: () => formatAlign('right'), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Aligner \u00E0 droite", children: _jsx("span", { className: "material-icons text-lg", children: "format_align_right" }) }), _jsx("button", { onClick: () => formatAlign('justify'), className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Justifier", children: _jsx("span", { className: "material-icons text-lg", children: "format_align_justify" }) }), _jsx("div", { className: "w-px h-6 bg-gray-300 mx-1" }), _jsx("button", { onClick: openInsertDialog, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: "Ins\u00E9rer un objet (Ctrl+K)", children: _jsx("span", { className: "material-icons text-lg", children: "add_photo_alternate" }) }), _jsx("div", { className: "flex-1" }), onAIAction && (_jsxs("div", { style: { position: 'relative' }, children: [_jsxs("button", { ref: aiButtonRef, onClick: (e) => {
224
+ e.stopPropagation();
225
+ setShowAIMenu(!showAIMenu);
226
+ setShowHeadingMenu(false);
227
+ setShowFontMenu(false);
228
+ }, disabled: isLoading, className: "flex items-center gap-1 px-3 py-1.5 text-sm bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", title: "Actions IA", children: [isLoading ? (_jsx("span", { className: "material-icons text-base animate-spin", children: "sync" })) : (_jsx("span", { className: "material-icons text-base", children: "auto_awesome" })), _jsx("span", { children: "IA" }), _jsx("span", { className: "material-icons text-sm", children: "arrow_drop_down" })] }), !isLoading && (_jsxs(FixedDropdown, { show: showAIMenu, buttonRef: aiButtonRef, children: [_jsxs("button", { onClick: () => {
229
+ onAIAction('rewrite');
230
+ setShowAIMenu(false);
231
+ }, style: { display: 'flex', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', alignItems: 'center', gap: '8px' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: [_jsx("span", { className: "material-icons text-base text-blue-500", children: "auto_fix_high" }), "R\u00E9\u00E9crire"] }), _jsxs("button", { onClick: () => {
232
+ onAIAction('proofread');
233
+ setShowAIMenu(false);
234
+ }, style: { display: 'flex', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', alignItems: 'center', gap: '8px' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: [_jsx("span", { className: "material-icons text-base text-green-500", children: "spellcheck" }), "Corriger"] }), _jsxs("button", { onClick: () => {
235
+ const prompt = window.prompt('Instruction pour l\'IA:');
236
+ if (prompt) {
237
+ onAIAction('custom', prompt);
238
+ }
239
+ setShowAIMenu(false);
240
+ }, style: { display: 'flex', width: '100%', padding: '8px 16px', textAlign: 'left', fontSize: '14px', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', color: '#374151', alignItems: 'center', gap: '8px' }, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#f3f4f6', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = 'transparent', children: [_jsx("span", { className: "material-icons text-base text-purple-500", children: "smart_toy" }), "Personnalis\u00E9..."] })] }))] })), onToggleFullscreen && (_jsx("button", { onClick: onToggleFullscreen, className: "p-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-200 rounded transition-colors", title: isFullscreen ? "Quitter le plein écran" : "Plein écran", children: _jsx("span", { className: "material-icons text-lg", children: isFullscreen ? 'fullscreen_exit' : 'fullscreen' }) }))] }));
241
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export interface MarkdownPreviewProps {
3
+ content: string;
4
+ className?: string;
5
+ }
6
+ export declare const MarkdownPreview: React.ForwardRefExoticComponent<MarkdownPreviewProps & React.RefAttributes<HTMLDivElement>>;
7
+ //# sourceMappingURL=MarkdownPreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownPreview.d.ts","sourceRoot":"","sources":["../../src/components/MarkdownPreview.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAA;AAEzC,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAGD,eAAO,MAAM,eAAe,6FA+C3B,CAAA"}
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ // Simple markdown renderer - can be enhanced with react-markdown
4
+ export const MarkdownPreview = forwardRef(({ content, className = '' }, ref) => {
5
+ // Basic markdown to HTML conversion
6
+ const renderMarkdown = (md) => {
7
+ let html = md
8
+ // Escape HTML
9
+ .replace(/&/g, '&amp;')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ // Headers
13
+ .replace(/^### (.*$)/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>')
14
+ .replace(/^## (.*$)/gm, '<h2 class="text-xl font-semibold mt-6 mb-3">$1</h2>')
15
+ .replace(/^# (.*$)/gm, '<h1 class="text-2xl font-bold mt-6 mb-4">$1</h1>')
16
+ // Bold and italic
17
+ .replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
18
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
19
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
20
+ // Code blocks
21
+ .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="bg-gray-100 p-3 rounded-lg my-3 overflow-x-auto"><code>$2</code></pre>')
22
+ // Inline code
23
+ .replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-1.5 py-0.5 rounded text-sm">$1</code>')
24
+ // Lists
25
+ .replace(/^\s*[-*]\s+(.*$)/gm, '<li class="ml-4">$1</li>')
26
+ // Numbered lists
27
+ .replace(/^\s*\d+\.\s+(.*$)/gm, '<li class="ml-4 list-decimal">$1</li>')
28
+ // Line breaks (double newline = paragraph)
29
+ .replace(/\n\n/g, '</p><p class="mb-3">')
30
+ // Single line breaks
31
+ .replace(/\n/g, '<br/>');
32
+ // Wrap in paragraph
33
+ html = `<p class="mb-3">${html}</p>`;
34
+ // Clean up empty paragraphs
35
+ html = html.replace(/<p class="mb-3"><\/p>/g, '');
36
+ return html;
37
+ };
38
+ return (_jsx("div", { ref: ref, className: `prose prose-sm max-w-none select-text ${className}`, dangerouslySetInnerHTML: { __html: renderMarkdown(content) } }));
39
+ });
40
+ MarkdownPreview.displayName = 'MarkdownPreview';
@@ -0,0 +1,9 @@
1
+ export interface PromptModalProps {
2
+ isOpen: boolean;
3
+ onClose: () => void;
4
+ onSubmit: (prompt: string) => void;
5
+ selectedText?: string;
6
+ isLoading: boolean;
7
+ }
8
+ export declare function PromptModal({ isOpen, onClose, onSubmit, selectedText, isLoading }: PromptModalProps): import("react/jsx-runtime").JSX.Element | null;
9
+ //# sourceMappingURL=PromptModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PromptModal.d.ts","sourceRoot":"","sources":["../../src/components/PromptModal.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,SAAS,EACV,EAAE,gBAAgB,kDAkIlB"}
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef } from 'react';
3
+ export function PromptModal({ isOpen, onClose, onSubmit, selectedText, isLoading }) {
4
+ const [prompt, setPrompt] = useState('');
5
+ const inputRef = useRef(null);
6
+ useEffect(() => {
7
+ if (isOpen && inputRef.current) {
8
+ inputRef.current.focus();
9
+ }
10
+ }, [isOpen]);
11
+ useEffect(() => {
12
+ const handleKeyDown = (e) => {
13
+ if (e.key === 'Escape' && isOpen) {
14
+ onClose();
15
+ }
16
+ };
17
+ window.addEventListener('keydown', handleKeyDown);
18
+ return () => window.removeEventListener('keydown', handleKeyDown);
19
+ }, [isOpen, onClose]);
20
+ const handleSubmit = (e) => {
21
+ e.preventDefault();
22
+ if (prompt.trim()) {
23
+ onSubmit(prompt.trim());
24
+ setPrompt('');
25
+ }
26
+ };
27
+ if (!isOpen)
28
+ return null;
29
+ return (_jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: _jsxs("div", { className: "bg-white rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden", children: [_jsxs("div", { className: "px-6 py-4 border-b border-gray-200 flex items-center justify-between", children: [_jsx("h3", { className: "text-lg font-semibold text-gray-900", children: "Instruction IA" }), _jsx("button", { onClick: onClose, className: "p-1 text-gray-400 hover:text-gray-600 transition-colors", children: _jsx("span", { className: "material-icons", children: "close" }) })] }), _jsxs("form", { onSubmit: handleSubmit, className: "p-6", children: [selectedText && (_jsxs("div", { className: "mb-4", children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "Texte s\u00E9lectionn\u00E9" }), _jsx("div", { className: "p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-gray-700 max-h-32 overflow-y-auto", children: selectedText.length > 200
30
+ ? `${selectedText.substring(0, 200)}...`
31
+ : selectedText })] })), _jsxs("div", { className: "mb-4", children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: selectedText ? 'Que voulez-vous faire avec ce texte ?' : 'Instruction pour l\'IA' }), _jsx("textarea", { ref: inputRef, value: prompt, onChange: (e) => setPrompt(e.target.value), placeholder: "Ex: Rends ce texte plus concis, ajoute des exemples, traduis en anglais...", className: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500 resize-none", rows: 3, disabled: isLoading })] }), _jsxs("div", { className: "mb-4", children: [_jsx("label", { className: "block text-sm font-medium text-gray-500 mb-2", children: "Actions rapides" }), _jsx("div", { className: "flex flex-wrap gap-2", children: [
32
+ 'Rends plus concis',
33
+ 'Ajoute des détails',
34
+ 'Simplifie le vocabulaire',
35
+ 'Traduis en anglais',
36
+ 'Reformule autrement'
37
+ ].map((action) => (_jsx("button", { type: "button", onClick: () => setPrompt(action), className: "px-3 py-1 text-xs bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 transition-colors", children: action }, action))) })] }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx("button", { type: "button", onClick: onClose, className: "px-4 py-2 text-gray-600 hover:text-gray-900 transition-colors", disabled: isLoading, children: "Annuler" }), _jsx("button", { type: "submit", disabled: !prompt.trim() || isLoading, className: "px-6 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2", children: isLoading ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "material-icons text-sm animate-spin", children: "sync" }), "Traitement..."] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "material-icons text-sm", children: "send" }), "Appliquer"] })) })] })] })] }) }));
38
+ }