xertica-ui 2.1.2 → 2.1.4
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/CHANGELOG.md +46 -0
- package/README.md +1 -1
- package/bin/cli.ts +1 -1
- package/bin/generate-tokens.ts +13 -7
- package/components/assistant/xertica-assistant/index.ts +2 -0
- package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +97 -0
- package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -0
- package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -0
- package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +86 -0
- package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +77 -0
- package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +573 -0
- package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +65 -0
- package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -0
- package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +98 -0
- package/components/assistant/xertica-assistant/parts/index.ts +16 -0
- package/components/assistant/xertica-assistant/types.ts +139 -0
- package/components/assistant/xertica-assistant/use-assistant.ts +559 -0
- package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +200 -0
- package/components/assistant/xertica-assistant/xertica-assistant.tsx +198 -1460
- package/components/brand/theme-toggle/ThemeToggle.tsx +8 -27
- package/components/hooks/index.ts +3 -0
- package/components/hooks/use-layout-shortcuts.ts +46 -0
- package/components/layout/sidebar/index.ts +2 -0
- package/components/layout/sidebar/sidebar.stories.tsx +160 -8
- package/components/layout/sidebar/sidebar.tsx +606 -497
- package/components/layout/sidebar/use-sidebar.ts +104 -0
- package/components/media/audio-player/AudioPlayer.tsx +131 -206
- package/components/media/audio-player/use-audio-player.ts +298 -0
- package/components/pages/home-page/HomePage.tsx +1 -1
- package/components/pages/template-content/TemplateContent.tsx +5 -5
- package/components/pages/template-page/TemplatePage.tsx +5 -5
- package/components/shared/CustomTooltipContent.tsx +52 -0
- package/components/shared/layout-constants.ts +1 -1
- package/components/ui/chart/chart.stories.tsx +966 -7
- package/components/ui/chart/chart.tsx +918 -45
- package/components/ui/file-upload/file-upload.stories.tsx +100 -0
- package/components/ui/file-upload/file-upload.tsx +14 -74
- package/components/ui/file-upload/index.ts +1 -0
- package/components/ui/file-upload/use-file-upload.ts +181 -0
- package/components/ui/pagination/index.ts +2 -0
- package/components/ui/pagination/pagination.stories.tsx +94 -0
- package/components/ui/pagination/use-pagination.ts +194 -0
- package/components/ui/rich-text-editor/index.ts +2 -0
- package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +129 -1
- package/components/ui/rich-text-editor/rich-text-editor.tsx +86 -305
- package/components/ui/rich-text-editor/use-rich-text-editor.ts +439 -0
- package/components/ui/stepper/index.ts +3 -1
- package/components/ui/stepper/stepper.stories.tsx +116 -0
- package/components/ui/stepper/stepper.tsx +4 -4
- package/components/ui/stepper/use-stepper.ts +137 -0
- package/components/ui/tree-view/index.ts +4 -1
- package/components/ui/tree-view/tree-view.stories.tsx +110 -4
- package/components/ui/tree-view/tree-view.tsx +17 -125
- package/components/ui/tree-view/use-tree-view.ts +229 -0
- package/contexts/AssistenteContext.tsx +17 -54
- package/contexts/BrandColorsContext.tsx +6 -17
- package/contexts/LayoutContext.tsx +5 -31
- package/dist/AssistantChart-BAudAfne.cjs +3591 -0
- package/dist/AssistantChart-BP8upjMk.js +3565 -0
- package/dist/AudioPlayer-1ypwE2Wh.cjs +936 -0
- package/dist/AudioPlayer-DuKXrCfy.js +937 -0
- package/dist/CustomTooltipContent-DHjkY0ww.js +40 -0
- package/dist/CustomTooltipContent-c_K-DWRr.cjs +56 -0
- package/dist/LanguageContext-BwhwC3G2.js +657 -0
- package/dist/LanguageContext-DvUt5jBg.cjs +656 -0
- package/dist/LayoutContext-BDmcZfMH.cjs +84 -0
- package/dist/LayoutContext-dbQvdC4O.js +85 -0
- package/dist/ThemeContext-RTy1m2Uq.js +82 -0
- package/dist/ThemeContext-bSzuOit2.cjs +81 -0
- package/dist/VerifyEmailPage-C_ihbcth.js +2828 -0
- package/dist/VerifyEmailPage-Dt7zgA4w.cjs +2827 -0
- package/dist/XerticaProvider-CW9hpCdF.cjs +39 -0
- package/dist/XerticaProvider-siSt9uG2.js +40 -0
- package/dist/XerticaXLogo-D8jf0SNv.cjs +214 -0
- package/dist/XerticaXLogo-fAJMy3H4.js +215 -0
- package/dist/assistant.cjs.js +2 -1
- package/dist/assistant.es.js +3 -2
- package/dist/brand.cjs.js +2 -2
- package/dist/brand.es.js +2 -2
- package/dist/cli.js +14 -8
- package/dist/components/assistant/xertica-assistant/index.d.ts +2 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +13 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +16 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +17 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +19 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +11 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +29 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +13 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +4 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +17 -0
- package/dist/components/assistant/xertica-assistant/parts/index.d.ts +16 -0
- package/dist/components/assistant/xertica-assistant/types.d.ts +106 -0
- package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +125 -0
- package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +8 -97
- package/dist/components/hooks/index.d.ts +3 -0
- package/dist/components/hooks/use-layout-shortcuts.d.ts +22 -0
- package/dist/components/layout/sidebar/index.d.ts +2 -0
- package/dist/components/layout/sidebar/sidebar.d.ts +80 -0
- package/dist/components/layout/sidebar/use-sidebar.d.ts +22 -0
- package/dist/components/media/audio-player/AudioPlayer.d.ts +4 -1
- package/dist/components/media/audio-player/use-audio-player.d.ts +72 -0
- package/dist/components/shared/CustomTooltipContent.d.ts +20 -0
- package/dist/components/shared/layout-constants.d.ts +1 -1
- package/dist/components/ui/alert/alert.d.ts +1 -1
- package/dist/components/ui/badge/badge.d.ts +1 -1
- package/dist/components/ui/button/button.d.ts +2 -2
- package/dist/components/ui/chart/chart.d.ts +162 -5
- package/dist/components/ui/file-upload/file-upload.d.ts +2 -0
- package/dist/components/ui/file-upload/index.d.ts +1 -0
- package/dist/components/ui/file-upload/use-file-upload.d.ts +49 -0
- package/dist/components/ui/pagination/index.d.ts +2 -0
- package/dist/components/ui/pagination/use-pagination.d.ts +78 -0
- package/dist/components/ui/rich-text-editor/index.d.ts +2 -0
- package/dist/components/ui/rich-text-editor/use-rich-text-editor.d.ts +107 -0
- package/dist/components/ui/stepper/index.d.ts +3 -1
- package/dist/components/ui/stepper/stepper.d.ts +2 -2
- package/dist/components/ui/stepper/use-stepper.d.ts +60 -0
- package/dist/components/ui/tree-view/index.d.ts +4 -1
- package/dist/components/ui/tree-view/tree-view.d.ts +4 -6
- package/dist/components/ui/tree-view/use-tree-view.d.ts +60 -0
- package/dist/contexts/AssistenteContext.d.ts +10 -49
- package/dist/hooks.cjs.js +30 -10
- package/dist/hooks.es.js +25 -4
- package/dist/index.cjs.js +20 -9
- package/dist/index.es.js +38 -27
- package/dist/layout.cjs.js +82 -1
- package/dist/layout.es.js +83 -2
- package/dist/media.cjs.js +1 -1
- package/dist/media.es.js +1 -1
- package/dist/pages.cjs.js +1 -1
- package/dist/pages.es.js +1 -1
- package/dist/rich-text-editor-BmsjY03B.js +2949 -0
- package/dist/rich-text-editor-GS2kpTAK.cjs +2966 -0
- package/dist/sidebar-CVUGHOS_.cjs +756 -0
- package/dist/sidebar-CmvwjnVb.js +757 -0
- package/dist/ui.cjs.js +12 -2
- package/dist/ui.es.js +24 -14
- package/dist/use-audio-player-Bkh23vQ3.js +177 -0
- package/dist/use-audio-player-Dn1NR9xN.cjs +176 -0
- package/dist/utils/color-utils.d.ts +51 -0
- package/dist/xertica-assistant-BMqdyRVi.js +2082 -0
- package/dist/xertica-assistant-Bj3vBCq_.cjs +2081 -0
- package/dist/xertica-ui.css +1 -1
- package/docs/ai-usage.md +28 -10
- package/docs/architecture-improvements.md +463 -0
- package/docs/architecture.md +77 -1
- package/docs/components/assistant-chart.md +1 -1
- package/docs/components/assistant.md +159 -0
- package/docs/components/audio-player.md +46 -0
- package/docs/components/branding.md +251 -0
- package/docs/components/chart.md +354 -39
- package/docs/components/code-block.md +108 -0
- package/docs/components/file-upload.md +119 -2
- package/docs/components/formatted-document.md +113 -0
- package/docs/components/hooks.md +430 -0
- package/docs/components/image-with-fallback.md +106 -0
- package/docs/components/map-layers.md +140 -0
- package/docs/components/modern-chat-input.md +163 -0
- package/docs/components/pages.md +351 -0
- package/docs/components/pagination.md +187 -0
- package/docs/components/rich-text-editor.md +164 -0
- package/docs/components/sidebar.md +153 -4
- package/docs/components/stepper.md +157 -12
- package/docs/components/tree-view.md +164 -6
- package/docs/doc-audit.md +223 -0
- package/docs/getting-started.md +155 -1
- package/docs/guidelines.md +14 -8
- package/docs/layout.md +2 -2
- package/docs/llms.md +29 -9
- package/docs/patterns/detail-page.md +276 -0
- package/docs/patterns/settings.md +346 -0
- package/docs/patterns/wizard.md +217 -0
- package/guidelines/Guidelines.md +5 -3
- package/llms.txt +1 -1
- package/package.json +10 -10
- package/styles/xertica/tokens.css +41 -12
- package/templates/CLAUDE.md +16 -6
- package/templates/guidelines/Guidelines.md +16 -4
- package/templates/package.json +3 -3
- package/templates/src/styles/xertica/tokens.css +39 -10
- package/utils/color-utils.ts +72 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useRichTextEditor } from './use-rich-text-editor';
|
|
2
3
|
import { cn } from '../../shared/utils';
|
|
3
4
|
import { Button } from '../button';
|
|
4
5
|
import {
|
|
@@ -46,6 +47,9 @@ export interface RichTextEditorProps {
|
|
|
46
47
|
maxHeight?: string;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Component
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
53
|
export function RichTextEditor({
|
|
50
54
|
value,
|
|
51
55
|
onChange,
|
|
@@ -70,51 +74,30 @@ export function RichTextEditor({
|
|
|
70
74
|
}: RichTextEditorProps) {
|
|
71
75
|
const isEditable = !disabled && !readOnly;
|
|
72
76
|
const showToolbar = isEditable;
|
|
73
|
-
const editorRef = useRef<HTMLDivElement>(null);
|
|
74
|
-
const [activeFormats, setActiveFormats] = useState<Record<string, boolean>>({});
|
|
75
|
-
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
76
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
77
|
-
const [linkUrl, setLinkUrl] = useState('https://');
|
|
78
|
-
const [isLinkOpen, setIsLinkOpen] = useState(false);
|
|
79
|
-
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
80
|
-
const linkInputRef = useRef<HTMLInputElement>(null);
|
|
81
|
-
const savedSelection = useRef<Range | null>(null);
|
|
82
77
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
justifyLeft: document.queryCommandState('justifyLeft'),
|
|
107
|
-
justifyCenter: document.queryCommandState('justifyCenter'),
|
|
108
|
-
justifyRight: document.queryCommandState('justifyRight'),
|
|
109
|
-
insertUnorderedList: document.queryCommandState('insertUnorderedList'),
|
|
110
|
-
insertOrderedList: document.queryCommandState('insertOrderedList'),
|
|
111
|
-
link: !!(findParentTag(anchorNode || null, 'A') || findParentTag(focusNode || null, 'A')),
|
|
112
|
-
h1: formatBlock === 'h1' || formatBlock === 'H1',
|
|
113
|
-
h2: formatBlock === 'h2' || formatBlock === 'H2',
|
|
114
|
-
h3: formatBlock === 'h3' || formatBlock === 'H3',
|
|
115
|
-
p: formatBlock === 'p' || formatBlock === 'P' || formatBlock === 'div' || formatBlock === 'DIV' || formatBlock === '',
|
|
116
|
-
});
|
|
117
|
-
};
|
|
78
|
+
const {
|
|
79
|
+
editorRef,
|
|
80
|
+
searchInputRef,
|
|
81
|
+
linkInputRef,
|
|
82
|
+
activeFormats,
|
|
83
|
+
isSearchOpen,
|
|
84
|
+
setIsSearchOpen,
|
|
85
|
+
searchQuery,
|
|
86
|
+
setSearchQuery,
|
|
87
|
+
linkUrl,
|
|
88
|
+
setLinkUrl,
|
|
89
|
+
isLinkOpen,
|
|
90
|
+
hasSavedSelection,
|
|
91
|
+
wordCount,
|
|
92
|
+
characterCount,
|
|
93
|
+
updateActiveFormats,
|
|
94
|
+
execCommand,
|
|
95
|
+
handleInput,
|
|
96
|
+
performSearch,
|
|
97
|
+
handleCreateLink,
|
|
98
|
+
handleUnlink,
|
|
99
|
+
onLinkPopoverOpenChange,
|
|
100
|
+
} = useRichTextEditor({ value, onChange });
|
|
118
101
|
|
|
119
102
|
const getCurrentBlockLabel = () => {
|
|
120
103
|
if (activeFormats.h1) return <><Heading1 className="w-4 h-4 mr-1 shrink-0" /><span className="truncate">Título 1</span></>;
|
|
@@ -123,214 +106,6 @@ export function RichTextEditor({
|
|
|
123
106
|
return <><Type className="w-4 h-4 mr-1 shrink-0" /><span className="truncate">Parágrafo</span></>;
|
|
124
107
|
};
|
|
125
108
|
|
|
126
|
-
// Only initialize value once unless externally forced to completely rewrite.
|
|
127
|
-
// Using simple innerHTML is sufficient for a lightweight editor.
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
if (editorRef.current && editorRef.current.innerHTML !== value) {
|
|
130
|
-
editorRef.current.innerHTML = value;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const handleSelectionChange = () => {
|
|
134
|
-
if (document.activeElement === editorRef.current) {
|
|
135
|
-
updateActiveFormats();
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
document.addEventListener('selectionchange', handleSelectionChange);
|
|
140
|
-
return () => document.removeEventListener('selectionchange', handleSelectionChange);
|
|
141
|
-
}, []);
|
|
142
|
-
|
|
143
|
-
const execCommand = (command: string, value: string = '') => {
|
|
144
|
-
document.execCommand(command, false, value);
|
|
145
|
-
updateActiveFormats();
|
|
146
|
-
editorRef.current?.focus();
|
|
147
|
-
handleInput();
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const handleInput = () => {
|
|
151
|
-
if (editorRef.current) {
|
|
152
|
-
onChange?.(editorRef.current.innerHTML);
|
|
153
|
-
updateActiveFormats();
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const getWordCount = () => {
|
|
158
|
-
const text = editorRef.current?.innerText || '';
|
|
159
|
-
return text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const getCharacterCount = () => {
|
|
163
|
-
const text = editorRef.current?.innerText || '';
|
|
164
|
-
return text.trim() ? text.length : 0;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const performSearch = (text: string, backward = false) => {
|
|
168
|
-
if (!text || !editorRef.current) return;
|
|
169
|
-
|
|
170
|
-
const editor = editorRef.current;
|
|
171
|
-
const selection = window.getSelection();
|
|
172
|
-
if (!selection) return;
|
|
173
|
-
|
|
174
|
-
// Collect all text nodes inside the editor using TreeWalker
|
|
175
|
-
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
|
176
|
-
const segments: Array<{ node: Text; start: number }> = [];
|
|
177
|
-
let offset = 0;
|
|
178
|
-
let node: Node | null;
|
|
179
|
-
while ((node = walker.nextNode())) {
|
|
180
|
-
segments.push({ node: node as Text, start: offset });
|
|
181
|
-
offset += (node as Text).length;
|
|
182
|
-
}
|
|
183
|
-
const fullText = segments.reduce((acc, s) => acc + (s.node.textContent ?? ''), '');
|
|
184
|
-
|
|
185
|
-
// Find current cursor/selection position in the concatenated text
|
|
186
|
-
let searchFrom = 0;
|
|
187
|
-
if (selection.rangeCount > 0) {
|
|
188
|
-
const range = selection.getRangeAt(0);
|
|
189
|
-
if (editor.contains(range.startContainer)) {
|
|
190
|
-
const seg = segments.find(s => s.node === range.startContainer);
|
|
191
|
-
if (seg) {
|
|
192
|
-
searchFrom = backward
|
|
193
|
-
? seg.start + range.startOffset - 1
|
|
194
|
-
: seg.start + range.endOffset;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const lowerFull = fullText.toLowerCase();
|
|
200
|
-
const lowerQuery = text.toLowerCase();
|
|
201
|
-
let matchStart = -1;
|
|
202
|
-
|
|
203
|
-
if (backward) {
|
|
204
|
-
matchStart = lowerFull.lastIndexOf(lowerQuery, Math.max(0, searchFrom));
|
|
205
|
-
if (matchStart === -1) matchStart = lowerFull.lastIndexOf(lowerQuery);
|
|
206
|
-
} else {
|
|
207
|
-
matchStart = lowerFull.indexOf(lowerQuery, searchFrom);
|
|
208
|
-
if (matchStart === -1) matchStart = lowerFull.indexOf(lowerQuery);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (matchStart === -1) return;
|
|
212
|
-
|
|
213
|
-
const matchEnd = matchStart + text.length;
|
|
214
|
-
|
|
215
|
-
// Build a DOM Range spanning the match across text nodes
|
|
216
|
-
const range = document.createRange();
|
|
217
|
-
let startSet = false;
|
|
218
|
-
let endSet = false;
|
|
219
|
-
|
|
220
|
-
for (let i = 0; i < segments.length && !endSet; i++) {
|
|
221
|
-
const seg = segments[i];
|
|
222
|
-
const segEnd = seg.start + seg.node.length;
|
|
223
|
-
|
|
224
|
-
if (!startSet && matchStart < segEnd && matchStart >= seg.start) {
|
|
225
|
-
range.setStart(seg.node, matchStart - seg.start);
|
|
226
|
-
startSet = true;
|
|
227
|
-
}
|
|
228
|
-
if (startSet && matchEnd <= segEnd) {
|
|
229
|
-
range.setEnd(seg.node, matchEnd - seg.start);
|
|
230
|
-
endSet = true;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (startSet && endSet) {
|
|
235
|
-
selection.removeAllRanges();
|
|
236
|
-
selection.addRange(range);
|
|
237
|
-
(range.startContainer as Element).parentElement?.scrollIntoView?.({ block: 'nearest' });
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const handleCreateLink = () => {
|
|
243
|
-
if (savedSelection.current) {
|
|
244
|
-
const selection = window.getSelection();
|
|
245
|
-
selection?.removeAllRanges();
|
|
246
|
-
selection?.addRange(savedSelection.current);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const selection = window.getSelection();
|
|
250
|
-
const anchorNode = selection?.anchorNode;
|
|
251
|
-
const existingLink = findParentTag(anchorNode || null, 'A');
|
|
252
|
-
|
|
253
|
-
if (existingLink) {
|
|
254
|
-
if (linkUrl) {
|
|
255
|
-
existingLink.setAttribute('href', linkUrl);
|
|
256
|
-
existingLink.setAttribute('target', '_blank');
|
|
257
|
-
existingLink.setAttribute('rel', 'noopener noreferrer');
|
|
258
|
-
existingLink.style.color = 'hsl(var(--primary))';
|
|
259
|
-
existingLink.style.textDecoration = 'underline';
|
|
260
|
-
existingLink.style.cursor = 'pointer';
|
|
261
|
-
}
|
|
262
|
-
handleInput(); // Trigger onChange
|
|
263
|
-
setIsLinkOpen(false);
|
|
264
|
-
savedSelection.current = null;
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (linkUrl) {
|
|
273
|
-
execCommand('createLink', linkUrl);
|
|
274
|
-
|
|
275
|
-
// The execCommand might have finished, let's find the newly created anchor
|
|
276
|
-
// We wait a bit for the DOM to update if needed, but usually it's immediate
|
|
277
|
-
setTimeout(() => {
|
|
278
|
-
const anchor = findParentTag(window.getSelection()?.anchorNode || null, 'A');
|
|
279
|
-
if (anchor) {
|
|
280
|
-
anchor.setAttribute('target', '_blank');
|
|
281
|
-
anchor.setAttribute('rel', 'noopener noreferrer');
|
|
282
|
-
anchor.style.color = 'hsl(var(--primary))';
|
|
283
|
-
anchor.style.textDecoration = 'underline';
|
|
284
|
-
anchor.style.cursor = 'pointer';
|
|
285
|
-
}
|
|
286
|
-
handleInput(); // Trigger onChange
|
|
287
|
-
}, 10);
|
|
288
|
-
|
|
289
|
-
setIsLinkOpen(false);
|
|
290
|
-
savedSelection.current = null;
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const handleUnlink = () => {
|
|
295
|
-
const selection = window.getSelection();
|
|
296
|
-
const anchorNode = selection?.anchorNode;
|
|
297
|
-
const existingLink = findParentTag(anchorNode || null, 'A');
|
|
298
|
-
|
|
299
|
-
if (existingLink) {
|
|
300
|
-
const parent = existingLink.parentNode;
|
|
301
|
-
while (existingLink.firstChild) {
|
|
302
|
-
parent?.insertBefore(existingLink.firstChild, existingLink);
|
|
303
|
-
}
|
|
304
|
-
parent?.removeChild(existingLink);
|
|
305
|
-
handleInput();
|
|
306
|
-
} else {
|
|
307
|
-
execCommand('unlink');
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const onLinkPopoverOpenChange = (open: boolean) => {
|
|
312
|
-
if (open) {
|
|
313
|
-
const selection = window.getSelection();
|
|
314
|
-
const anchorNode = selection?.anchorNode;
|
|
315
|
-
const focusNode = selection?.focusNode;
|
|
316
|
-
const existingLink = findParentTag(anchorNode || null, 'A') || findParentTag(focusNode || null, 'A');
|
|
317
|
-
|
|
318
|
-
if (existingLink) {
|
|
319
|
-
setLinkUrl(existingLink.getAttribute('href') || 'https://');
|
|
320
|
-
} else {
|
|
321
|
-
setLinkUrl('https://');
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (selection && selection.rangeCount > 0 && (!selection.isCollapsed || existingLink)) {
|
|
325
|
-
savedSelection.current = selection.getRangeAt(0).cloneRange();
|
|
326
|
-
} else {
|
|
327
|
-
savedSelection.current = null;
|
|
328
|
-
}
|
|
329
|
-
setTimeout(() => linkInputRef.current?.focus(), 100);
|
|
330
|
-
}
|
|
331
|
-
setIsLinkOpen(open);
|
|
332
|
-
};
|
|
333
|
-
|
|
334
109
|
return (
|
|
335
110
|
<div className={cn("flex flex-col h-full w-full bg-background sm:border border-border sm:shadow-sm sm:rounded-lg overflow-hidden", disabled && "opacity-50 pointer-events-none", className)}>
|
|
336
111
|
<style>{`
|
|
@@ -399,25 +174,25 @@ export function RichTextEditor({
|
|
|
399
174
|
outline: none !important;
|
|
400
175
|
}
|
|
401
176
|
`}</style>
|
|
402
|
-
|
|
177
|
+
|
|
403
178
|
{/* Toolbar — hidden in disabled/readOnly mode */}
|
|
404
179
|
{showToolbar && <div className="px-3 py-2 border-b border-border bg-muted/30 flex items-center gap-1 flex-wrap shrink-0">
|
|
405
180
|
{allowUndoRedo && (
|
|
406
181
|
<>
|
|
407
|
-
<Button
|
|
408
|
-
variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"
|
|
182
|
+
<Button
|
|
183
|
+
variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"
|
|
409
184
|
onClick={() => execCommand('undo')} title="Desfazer" aria-label="Desfazer">
|
|
410
185
|
<Undo className="w-4 h-4" />
|
|
411
186
|
</Button>
|
|
412
|
-
<Button
|
|
413
|
-
variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"
|
|
187
|
+
<Button
|
|
188
|
+
variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"
|
|
414
189
|
onClick={() => execCommand('redo')} title="Refazer" aria-label="Refazer">
|
|
415
190
|
<Redo className="w-4 h-4" />
|
|
416
191
|
</Button>
|
|
417
192
|
<div className="w-px h-6 bg-border mx-1 hidden sm:block"></div>
|
|
418
193
|
</>
|
|
419
194
|
)}
|
|
420
|
-
|
|
195
|
+
|
|
421
196
|
{allowHeadings && (
|
|
422
197
|
<>
|
|
423
198
|
<DropdownMenu modal={false}>
|
|
@@ -445,88 +220,94 @@ export function RichTextEditor({
|
|
|
445
220
|
<div className="w-px h-6 bg-border mx-1 hidden sm:block"></div>
|
|
446
221
|
</>
|
|
447
222
|
)}
|
|
448
|
-
|
|
223
|
+
|
|
449
224
|
{allowFormatting && (
|
|
450
225
|
<>
|
|
451
|
-
<Button
|
|
452
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.bold && "bg-muted text-foreground")}
|
|
226
|
+
<Button
|
|
227
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.bold && "bg-muted text-foreground")}
|
|
453
228
|
onClick={() => execCommand('bold')} title="Negrito" aria-label="Negrito">
|
|
454
229
|
<Bold className="w-4 h-4" />
|
|
455
230
|
</Button>
|
|
456
|
-
<Button
|
|
457
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.italic && "bg-muted text-foreground")}
|
|
231
|
+
<Button
|
|
232
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.italic && "bg-muted text-foreground")}
|
|
458
233
|
onClick={() => execCommand('italic')} title="Itálico" aria-label="Itálico">
|
|
459
234
|
<Italic className="w-4 h-4" />
|
|
460
235
|
</Button>
|
|
461
|
-
<Button
|
|
462
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.underline && "bg-muted text-foreground")}
|
|
236
|
+
<Button
|
|
237
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.underline && "bg-muted text-foreground")}
|
|
463
238
|
onClick={() => execCommand('underline')} title="Sublinhado" aria-label="Sublinhado">
|
|
464
239
|
<Underline className="w-4 h-4" />
|
|
465
240
|
</Button>
|
|
466
241
|
<div className="w-px h-6 bg-border mx-1 hidden sm:block"></div>
|
|
467
242
|
</>
|
|
468
243
|
)}
|
|
469
|
-
|
|
244
|
+
|
|
470
245
|
{allowAlignment && (
|
|
471
246
|
<>
|
|
472
|
-
<Button
|
|
473
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.justifyLeft && "bg-muted text-foreground")}
|
|
247
|
+
<Button
|
|
248
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.justifyLeft && "bg-muted text-foreground")}
|
|
474
249
|
onClick={() => execCommand('justifyLeft')} title="Alinhar à esquerda" aria-label="Alinhar à esquerda">
|
|
475
250
|
<AlignLeft className="w-4 h-4" />
|
|
476
251
|
</Button>
|
|
477
|
-
<Button
|
|
478
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.justifyCenter && "bg-muted text-foreground")}
|
|
252
|
+
<Button
|
|
253
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.justifyCenter && "bg-muted text-foreground")}
|
|
479
254
|
onClick={() => execCommand('justifyCenter')} title="Centralizar" aria-label="Centralizar">
|
|
480
255
|
<AlignCenter className="w-4 h-4" />
|
|
481
256
|
</Button>
|
|
482
|
-
<Button
|
|
483
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.justifyRight && "bg-muted text-foreground")}
|
|
257
|
+
<Button
|
|
258
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.justifyRight && "bg-muted text-foreground")}
|
|
484
259
|
onClick={() => execCommand('justifyRight')} title="Alinhar à direita" aria-label="Alinhar à direita">
|
|
485
260
|
<AlignRight className="w-4 h-4" />
|
|
486
261
|
</Button>
|
|
487
262
|
<div className="w-px h-6 bg-border mx-1 hidden sm:block"></div>
|
|
488
263
|
</>
|
|
489
264
|
)}
|
|
490
|
-
|
|
265
|
+
|
|
491
266
|
{allowLists && (
|
|
492
267
|
<>
|
|
493
|
-
<Button
|
|
494
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.insertUnorderedList && "bg-muted text-foreground")}
|
|
268
|
+
<Button
|
|
269
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.insertUnorderedList && "bg-muted text-foreground")}
|
|
495
270
|
onClick={() => execCommand('insertUnorderedList')} title="Lista com marcadores" aria-label="Lista com marcadores">
|
|
496
271
|
<List className="w-4 h-4" />
|
|
497
272
|
</Button>
|
|
498
|
-
<Button
|
|
499
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.insertOrderedList && "bg-muted text-foreground")}
|
|
273
|
+
<Button
|
|
274
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", activeFormats.insertOrderedList && "bg-muted text-foreground")}
|
|
500
275
|
onClick={() => execCommand('insertOrderedList')} title="Lista numerada" aria-label="Lista numerada">
|
|
501
276
|
<ListOrdered className="w-4 h-4" />
|
|
502
277
|
</Button>
|
|
503
278
|
<div className="w-px h-6 bg-border mx-1 hidden sm:block"></div>
|
|
504
279
|
</>
|
|
505
280
|
)}
|
|
506
|
-
|
|
281
|
+
|
|
507
282
|
{allowLinks && (
|
|
508
283
|
<>
|
|
509
284
|
<Popover open={isLinkOpen} onOpenChange={onLinkPopoverOpenChange} modal={false}>
|
|
510
285
|
<PopoverTrigger asChild>
|
|
511
|
-
<Button
|
|
512
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", (activeFormats.link || isLinkOpen) && "bg-muted text-foreground")}
|
|
286
|
+
<Button
|
|
287
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", (activeFormats.link || isLinkOpen) && "bg-muted text-foreground")}
|
|
513
288
|
title="Inserir link" aria-label="Inserir link">
|
|
514
289
|
<LinkIcon className="w-4 h-4" />
|
|
515
290
|
</Button>
|
|
516
291
|
</PopoverTrigger>
|
|
517
292
|
<PopoverContent className="w-80 p-3" align="start" style={{ zIndex: 9999 }}>
|
|
518
|
-
{!
|
|
293
|
+
{!activeFormats.link && !hasSavedSelection ? (
|
|
519
294
|
<p className="text-xs text-muted-foreground py-1">Selecione um texto para criar um link.</p>
|
|
520
295
|
) : (
|
|
521
296
|
<div className="flex flex-col gap-2">
|
|
522
297
|
<div className="text-xs font-medium">{activeFormats.link ? "Editar Link" : "Inserir Link"}</div>
|
|
523
298
|
<div className="flex gap-2">
|
|
524
|
-
<Input
|
|
299
|
+
<Input
|
|
525
300
|
ref={linkInputRef}
|
|
526
|
-
placeholder="https://..."
|
|
527
|
-
value={linkUrl}
|
|
301
|
+
placeholder="https://..."
|
|
302
|
+
value={linkUrl}
|
|
528
303
|
onChange={(e) => setLinkUrl(e.target.value)}
|
|
529
|
-
onKeyDown={(e) =>
|
|
304
|
+
onKeyDown={(e) => {
|
|
305
|
+
if (e.key === 'Enter') {
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
e.stopPropagation();
|
|
308
|
+
handleCreateLink();
|
|
309
|
+
}
|
|
310
|
+
}}
|
|
530
311
|
className="h-8 text-xs"
|
|
531
312
|
/>
|
|
532
313
|
<Button size="sm" className="h-8" onClick={handleCreateLink}>Aplicar</Button>
|
|
@@ -536,36 +317,36 @@ export function RichTextEditor({
|
|
|
536
317
|
</PopoverContent>
|
|
537
318
|
</Popover>
|
|
538
319
|
|
|
539
|
-
<Button
|
|
540
|
-
variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"
|
|
320
|
+
<Button
|
|
321
|
+
variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"
|
|
541
322
|
onClick={handleUnlink} title="Remover link" aria-label="Remover link">
|
|
542
323
|
<X className="w-4 h-4" />
|
|
543
324
|
</Button>
|
|
544
|
-
|
|
325
|
+
|
|
545
326
|
<div className="w-px h-6 bg-border mx-1 hidden sm:block"></div>
|
|
546
327
|
</>
|
|
547
328
|
)}
|
|
548
|
-
|
|
329
|
+
|
|
549
330
|
{(allowFormatting || allowHeadings || allowAlignment || allowLists || allowLinks) && (
|
|
550
|
-
<Button
|
|
551
|
-
variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"
|
|
331
|
+
<Button
|
|
332
|
+
variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"
|
|
552
333
|
onClick={() => execCommand('removeFormat')} title="Limpar formatação" aria-label="Limpar formatação">
|
|
553
334
|
<Type className="w-4 h-4" />
|
|
554
335
|
</Button>
|
|
555
336
|
)}
|
|
556
|
-
|
|
337
|
+
|
|
557
338
|
{allowSearch && (
|
|
558
|
-
<Button
|
|
559
|
-
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", isSearchOpen && "bg-muted text-foreground")}
|
|
339
|
+
<Button
|
|
340
|
+
variant="ghost" size="icon" className={cn("h-8 w-8 text-muted-foreground", isSearchOpen && "bg-muted text-foreground")}
|
|
560
341
|
onClick={() => {
|
|
561
342
|
setIsSearchOpen(!isSearchOpen);
|
|
562
343
|
if (!isSearchOpen) setTimeout(() => searchInputRef.current?.focus(), 100);
|
|
563
|
-
}}
|
|
344
|
+
}}
|
|
564
345
|
title="Buscar" aria-label="Buscar">
|
|
565
346
|
<Search className="w-4 h-4" />
|
|
566
347
|
</Button>
|
|
567
348
|
)}
|
|
568
|
-
|
|
349
|
+
|
|
569
350
|
{actionButton && (
|
|
570
351
|
<>
|
|
571
352
|
<div className="flex-1"></div>
|
|
@@ -597,21 +378,21 @@ export function RichTextEditor({
|
|
|
597
378
|
/>
|
|
598
379
|
</div>
|
|
599
380
|
<div className="flex items-center gap-1">
|
|
600
|
-
<Button
|
|
601
|
-
variant="ghost" size="icon" className="h-7 w-7"
|
|
381
|
+
<Button
|
|
382
|
+
variant="ghost" size="icon" className="h-7 w-7"
|
|
602
383
|
onMouseDown={(e) => e.preventDefault()}
|
|
603
384
|
onClick={() => performSearch(searchQuery, true)} title="Anterior">
|
|
604
385
|
<ChevronUp className="w-4 h-4" />
|
|
605
386
|
</Button>
|
|
606
|
-
<Button
|
|
607
|
-
variant="ghost" size="icon" className="h-7 w-7"
|
|
387
|
+
<Button
|
|
388
|
+
variant="ghost" size="icon" className="h-7 w-7"
|
|
608
389
|
onMouseDown={(e) => e.preventDefault()}
|
|
609
390
|
onClick={() => performSearch(searchQuery, false)} title="Próximo">
|
|
610
391
|
<ChevronDown className="w-4 h-4" />
|
|
611
392
|
</Button>
|
|
612
393
|
<div className="w-px h-4 bg-border mx-1"></div>
|
|
613
|
-
<Button
|
|
614
|
-
variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
394
|
+
<Button
|
|
395
|
+
variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
615
396
|
onClick={() => setIsSearchOpen(false)} title="Fechar busca">
|
|
616
397
|
<X className="w-4 h-4" />
|
|
617
398
|
</Button>
|
|
@@ -644,13 +425,13 @@ export function RichTextEditor({
|
|
|
644
425
|
<div className="px-4 py-2 border-t border-border bg-muted/20 flex items-center justify-between">
|
|
645
426
|
<div className="text-[10px] sm:text-xs text-muted-foreground font-medium uppercase tracking-wider flex items-center gap-2">
|
|
646
427
|
{showWordCount && (
|
|
647
|
-
<span>{
|
|
428
|
+
<span>{wordCount} {wordCount === 1 ? 'palavra' : 'palavras'}</span>
|
|
648
429
|
)}
|
|
649
430
|
{showWordCount && showCharacterCount && (
|
|
650
431
|
<span className="text-muted-foreground">•</span>
|
|
651
432
|
)}
|
|
652
433
|
{showCharacterCount && (
|
|
653
|
-
<span>{
|
|
434
|
+
<span>{characterCount} {characterCount === 1 ? 'caractere' : 'caracteres'}</span>
|
|
654
435
|
)}
|
|
655
436
|
</div>
|
|
656
437
|
<div className="text-[10px] sm:text-xs text-muted-foreground italic">
|