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.
Files changed (181) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/bin/cli.ts +1 -1
  4. package/bin/generate-tokens.ts +13 -7
  5. package/components/assistant/xertica-assistant/index.ts +2 -0
  6. package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +97 -0
  7. package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -0
  8. package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -0
  9. package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +86 -0
  10. package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +77 -0
  11. package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +573 -0
  12. package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +65 -0
  13. package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -0
  14. package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +98 -0
  15. package/components/assistant/xertica-assistant/parts/index.ts +16 -0
  16. package/components/assistant/xertica-assistant/types.ts +139 -0
  17. package/components/assistant/xertica-assistant/use-assistant.ts +559 -0
  18. package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +200 -0
  19. package/components/assistant/xertica-assistant/xertica-assistant.tsx +198 -1460
  20. package/components/brand/theme-toggle/ThemeToggle.tsx +8 -27
  21. package/components/hooks/index.ts +3 -0
  22. package/components/hooks/use-layout-shortcuts.ts +46 -0
  23. package/components/layout/sidebar/index.ts +2 -0
  24. package/components/layout/sidebar/sidebar.stories.tsx +160 -8
  25. package/components/layout/sidebar/sidebar.tsx +606 -497
  26. package/components/layout/sidebar/use-sidebar.ts +104 -0
  27. package/components/media/audio-player/AudioPlayer.tsx +131 -206
  28. package/components/media/audio-player/use-audio-player.ts +298 -0
  29. package/components/pages/home-page/HomePage.tsx +1 -1
  30. package/components/pages/template-content/TemplateContent.tsx +5 -5
  31. package/components/pages/template-page/TemplatePage.tsx +5 -5
  32. package/components/shared/CustomTooltipContent.tsx +52 -0
  33. package/components/shared/layout-constants.ts +1 -1
  34. package/components/ui/chart/chart.stories.tsx +966 -7
  35. package/components/ui/chart/chart.tsx +918 -45
  36. package/components/ui/file-upload/file-upload.stories.tsx +100 -0
  37. package/components/ui/file-upload/file-upload.tsx +14 -74
  38. package/components/ui/file-upload/index.ts +1 -0
  39. package/components/ui/file-upload/use-file-upload.ts +181 -0
  40. package/components/ui/pagination/index.ts +2 -0
  41. package/components/ui/pagination/pagination.stories.tsx +94 -0
  42. package/components/ui/pagination/use-pagination.ts +194 -0
  43. package/components/ui/rich-text-editor/index.ts +2 -0
  44. package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +129 -1
  45. package/components/ui/rich-text-editor/rich-text-editor.tsx +86 -305
  46. package/components/ui/rich-text-editor/use-rich-text-editor.ts +439 -0
  47. package/components/ui/stepper/index.ts +3 -1
  48. package/components/ui/stepper/stepper.stories.tsx +116 -0
  49. package/components/ui/stepper/stepper.tsx +4 -4
  50. package/components/ui/stepper/use-stepper.ts +137 -0
  51. package/components/ui/tree-view/index.ts +4 -1
  52. package/components/ui/tree-view/tree-view.stories.tsx +110 -4
  53. package/components/ui/tree-view/tree-view.tsx +17 -125
  54. package/components/ui/tree-view/use-tree-view.ts +229 -0
  55. package/contexts/AssistenteContext.tsx +17 -54
  56. package/contexts/BrandColorsContext.tsx +6 -17
  57. package/contexts/LayoutContext.tsx +5 -31
  58. package/dist/AssistantChart-BAudAfne.cjs +3591 -0
  59. package/dist/AssistantChart-BP8upjMk.js +3565 -0
  60. package/dist/AudioPlayer-1ypwE2Wh.cjs +936 -0
  61. package/dist/AudioPlayer-DuKXrCfy.js +937 -0
  62. package/dist/CustomTooltipContent-DHjkY0ww.js +40 -0
  63. package/dist/CustomTooltipContent-c_K-DWRr.cjs +56 -0
  64. package/dist/LanguageContext-BwhwC3G2.js +657 -0
  65. package/dist/LanguageContext-DvUt5jBg.cjs +656 -0
  66. package/dist/LayoutContext-BDmcZfMH.cjs +84 -0
  67. package/dist/LayoutContext-dbQvdC4O.js +85 -0
  68. package/dist/ThemeContext-RTy1m2Uq.js +82 -0
  69. package/dist/ThemeContext-bSzuOit2.cjs +81 -0
  70. package/dist/VerifyEmailPage-C_ihbcth.js +2828 -0
  71. package/dist/VerifyEmailPage-Dt7zgA4w.cjs +2827 -0
  72. package/dist/XerticaProvider-CW9hpCdF.cjs +39 -0
  73. package/dist/XerticaProvider-siSt9uG2.js +40 -0
  74. package/dist/XerticaXLogo-D8jf0SNv.cjs +214 -0
  75. package/dist/XerticaXLogo-fAJMy3H4.js +215 -0
  76. package/dist/assistant.cjs.js +2 -1
  77. package/dist/assistant.es.js +3 -2
  78. package/dist/brand.cjs.js +2 -2
  79. package/dist/brand.es.js +2 -2
  80. package/dist/cli.js +14 -8
  81. package/dist/components/assistant/xertica-assistant/index.d.ts +2 -0
  82. package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +13 -0
  83. package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +16 -0
  84. package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +17 -0
  85. package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +19 -0
  86. package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +11 -0
  87. package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +29 -0
  88. package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +13 -0
  89. package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +4 -0
  90. package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +17 -0
  91. package/dist/components/assistant/xertica-assistant/parts/index.d.ts +16 -0
  92. package/dist/components/assistant/xertica-assistant/types.d.ts +106 -0
  93. package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +125 -0
  94. package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +8 -97
  95. package/dist/components/hooks/index.d.ts +3 -0
  96. package/dist/components/hooks/use-layout-shortcuts.d.ts +22 -0
  97. package/dist/components/layout/sidebar/index.d.ts +2 -0
  98. package/dist/components/layout/sidebar/sidebar.d.ts +80 -0
  99. package/dist/components/layout/sidebar/use-sidebar.d.ts +22 -0
  100. package/dist/components/media/audio-player/AudioPlayer.d.ts +4 -1
  101. package/dist/components/media/audio-player/use-audio-player.d.ts +72 -0
  102. package/dist/components/shared/CustomTooltipContent.d.ts +20 -0
  103. package/dist/components/shared/layout-constants.d.ts +1 -1
  104. package/dist/components/ui/alert/alert.d.ts +1 -1
  105. package/dist/components/ui/badge/badge.d.ts +1 -1
  106. package/dist/components/ui/button/button.d.ts +2 -2
  107. package/dist/components/ui/chart/chart.d.ts +162 -5
  108. package/dist/components/ui/file-upload/file-upload.d.ts +2 -0
  109. package/dist/components/ui/file-upload/index.d.ts +1 -0
  110. package/dist/components/ui/file-upload/use-file-upload.d.ts +49 -0
  111. package/dist/components/ui/pagination/index.d.ts +2 -0
  112. package/dist/components/ui/pagination/use-pagination.d.ts +78 -0
  113. package/dist/components/ui/rich-text-editor/index.d.ts +2 -0
  114. package/dist/components/ui/rich-text-editor/use-rich-text-editor.d.ts +107 -0
  115. package/dist/components/ui/stepper/index.d.ts +3 -1
  116. package/dist/components/ui/stepper/stepper.d.ts +2 -2
  117. package/dist/components/ui/stepper/use-stepper.d.ts +60 -0
  118. package/dist/components/ui/tree-view/index.d.ts +4 -1
  119. package/dist/components/ui/tree-view/tree-view.d.ts +4 -6
  120. package/dist/components/ui/tree-view/use-tree-view.d.ts +60 -0
  121. package/dist/contexts/AssistenteContext.d.ts +10 -49
  122. package/dist/hooks.cjs.js +30 -10
  123. package/dist/hooks.es.js +25 -4
  124. package/dist/index.cjs.js +20 -9
  125. package/dist/index.es.js +38 -27
  126. package/dist/layout.cjs.js +82 -1
  127. package/dist/layout.es.js +83 -2
  128. package/dist/media.cjs.js +1 -1
  129. package/dist/media.es.js +1 -1
  130. package/dist/pages.cjs.js +1 -1
  131. package/dist/pages.es.js +1 -1
  132. package/dist/rich-text-editor-BmsjY03B.js +2949 -0
  133. package/dist/rich-text-editor-GS2kpTAK.cjs +2966 -0
  134. package/dist/sidebar-CVUGHOS_.cjs +756 -0
  135. package/dist/sidebar-CmvwjnVb.js +757 -0
  136. package/dist/ui.cjs.js +12 -2
  137. package/dist/ui.es.js +24 -14
  138. package/dist/use-audio-player-Bkh23vQ3.js +177 -0
  139. package/dist/use-audio-player-Dn1NR9xN.cjs +176 -0
  140. package/dist/utils/color-utils.d.ts +51 -0
  141. package/dist/xertica-assistant-BMqdyRVi.js +2082 -0
  142. package/dist/xertica-assistant-Bj3vBCq_.cjs +2081 -0
  143. package/dist/xertica-ui.css +1 -1
  144. package/docs/ai-usage.md +28 -10
  145. package/docs/architecture-improvements.md +463 -0
  146. package/docs/architecture.md +77 -1
  147. package/docs/components/assistant-chart.md +1 -1
  148. package/docs/components/assistant.md +159 -0
  149. package/docs/components/audio-player.md +46 -0
  150. package/docs/components/branding.md +251 -0
  151. package/docs/components/chart.md +354 -39
  152. package/docs/components/code-block.md +108 -0
  153. package/docs/components/file-upload.md +119 -2
  154. package/docs/components/formatted-document.md +113 -0
  155. package/docs/components/hooks.md +430 -0
  156. package/docs/components/image-with-fallback.md +106 -0
  157. package/docs/components/map-layers.md +140 -0
  158. package/docs/components/modern-chat-input.md +163 -0
  159. package/docs/components/pages.md +351 -0
  160. package/docs/components/pagination.md +187 -0
  161. package/docs/components/rich-text-editor.md +164 -0
  162. package/docs/components/sidebar.md +153 -4
  163. package/docs/components/stepper.md +157 -12
  164. package/docs/components/tree-view.md +164 -6
  165. package/docs/doc-audit.md +223 -0
  166. package/docs/getting-started.md +155 -1
  167. package/docs/guidelines.md +14 -8
  168. package/docs/layout.md +2 -2
  169. package/docs/llms.md +29 -9
  170. package/docs/patterns/detail-page.md +276 -0
  171. package/docs/patterns/settings.md +346 -0
  172. package/docs/patterns/wizard.md +217 -0
  173. package/guidelines/Guidelines.md +5 -3
  174. package/llms.txt +1 -1
  175. package/package.json +10 -10
  176. package/styles/xertica/tokens.css +41 -12
  177. package/templates/CLAUDE.md +16 -6
  178. package/templates/guidelines/Guidelines.md +16 -4
  179. package/templates/package.json +3 -3
  180. package/templates/src/styles/xertica/tokens.css +39 -10
  181. package/utils/color-utils.ts +72 -0
@@ -1,4 +1,5 @@
1
- import React, { useRef, useState, useEffect } from '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 findParentTag = (node: Node | null, tagName: string): HTMLElement | null => {
84
- let current = node;
85
- while (current && current !== editorRef.current) {
86
- if (current.nodeName === tagName) return current as HTMLElement;
87
- current = current.parentNode;
88
- }
89
- return null;
90
- };
91
-
92
- const updateActiveFormats = () => {
93
- let formatBlock = '';
94
- try {
95
- formatBlock = document.queryCommandValue('formatBlock');
96
- } catch (e) {}
97
-
98
- const selection = window.getSelection();
99
- const anchorNode = selection?.anchorNode;
100
- const focusNode = selection?.focusNode;
101
-
102
- setActiveFormats({
103
- bold: document.queryCommandState('bold'),
104
- italic: document.queryCommandState('italic'),
105
- underline: document.queryCommandState('underline'),
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
- {!savedSelection.current && !activeFormats.link ? (
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) => e.key === 'Enter' && handleCreateLink()}
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>{getWordCount()} {getWordCount() === 1 ? 'palavra' : 'palavras'}</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>{getCharacterCount()} {getCharacterCount() === 1 ? 'caractere' : 'caracteres'}</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">