xertica-ui 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.
- package/App.tsx +182 -0
- package/README.md +330 -0
- package/assets/xertica-logo.svg +38 -0
- package/assets/xertica-x-logo.svg +21 -0
- package/bin/cli.ts +193 -0
- package/components/AssistenteXertica.tsx +2003 -0
- package/components/AudioPlayer.tsx +203 -0
- package/components/CodeBlock.tsx +242 -0
- package/components/DocumentEditor.tsx +504 -0
- package/components/ForgotPasswordPage.tsx +170 -0
- package/components/FormattedDocument.tsx +87 -0
- package/components/HomeContent.tsx +123 -0
- package/components/HomePage.tsx +70 -0
- package/components/LanguageSelector.tsx +54 -0
- package/components/LoginPage.tsx +199 -0
- package/components/MarkdownMessage.tsx +62 -0
- package/components/ModernChatInput.tsx +502 -0
- package/components/PodcastPlayer.tsx +409 -0
- package/components/ResetPasswordPage.tsx +234 -0
- package/components/Sidebar.tsx +489 -0
- package/components/TemplateContent.tsx +629 -0
- package/components/TemplatePage.tsx +70 -0
- package/components/ThemeToggle.tsx +65 -0
- package/components/VerifyEmailPage.tsx +187 -0
- package/components/XerticaLogo.tsx +69 -0
- package/components/XerticaOrbe.tsx +1339 -0
- package/components/XerticaXLogo.tsx +53 -0
- package/components/examples/DrawingMapExample.tsx +530 -0
- package/components/examples/FilterableMapExample.tsx +380 -0
- package/components/examples/LocationPickerExample.tsx +330 -0
- package/components/examples/MapExamples.tsx +280 -0
- package/components/examples/MapShowcase.tsx +446 -0
- package/components/examples/RouteMapExamples.tsx +329 -0
- package/components/examples/SimpleFilterableMap.tsx +192 -0
- package/components/examples/index.ts +52 -0
- package/components/figma/ImageWithFallback.tsx +27 -0
- package/components/index.ts +44 -0
- package/components/media/AudioPlayer.tsx +278 -0
- package/components/media/FloatingMediaWrapper.tsx +166 -0
- package/components/media/VideoPlayer.tsx +285 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +159 -0
- package/components/ui/alert.tsx +91 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +65 -0
- package/components/ui/badge.tsx +55 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button.tsx +78 -0
- package/components/ui/calendar.tsx +235 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +177 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +138 -0
- package/components/ui/drawer.tsx +134 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +90 -0
- package/components/ui/file-upload.tsx +152 -0
- package/components/ui/form.tsx +195 -0
- package/components/ui/google-maps-loader.tsx +379 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/index.ts +242 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +38 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/map-config.ts +12 -0
- package/components/ui/map-layers.tsx +129 -0
- package/components/ui/map.exports.ts +31 -0
- package/components/ui/map.tsx +412 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +162 -0
- package/components/ui/notification-badge.tsx +61 -0
- package/components/ui/page-header.tsx +229 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +56 -0
- package/components/ui/rating.tsx +102 -0
- package/components/ui/resizable.tsx +405 -0
- package/components/ui/route-map.tsx +246 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/search.tsx +70 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/simple-map.tsx +92 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +58 -0
- package/components/ui/sonner.tsx +77 -0
- package/components/ui/stats-card.tsx +84 -0
- package/components/ui/stepper.tsx +126 -0
- package/components/ui/switch.tsx +34 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +26 -0
- package/components/ui/timeline.tsx +140 -0
- package/components/ui/toggle-group.tsx +71 -0
- package/components/ui/toggle.tsx +46 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/tree-view.tsx +123 -0
- package/components/ui/use-mobile.ts +24 -0
- package/components/ui/utils.ts +6 -0
- package/components/ui/xertica-assistant.tsx +1420 -0
- package/contexts/ApiKeyContext.tsx +123 -0
- package/contexts/AssistenteContext.tsx +118 -0
- package/contexts/BrandColorsContext.tsx +551 -0
- package/contexts/LanguageContext.tsx +36 -0
- package/contexts/ThemeContext.tsx +85 -0
- package/dist/cli.js +20922 -0
- package/eslint.config.js +41 -0
- package/guidelines/Guidelines.md +61 -0
- package/hooks/useTheme.ts +4 -0
- package/imports/Podcast.tsx +389 -0
- package/imports/XerticaAi.tsx +46 -0
- package/imports/XerticaX.tsx +20 -0
- package/imports/svg-aueiaqngck.ts +11 -0
- package/imports/svg-v9krss1ozd.ts +16 -0
- package/imports/svg-vhrdofe3qe.ts +5 -0
- package/index.css +4448 -0
- package/index.html +14 -0
- package/main.tsx +10 -0
- package/package.json +119 -0
- package/postcss.config.js +6 -0
- package/routes.tsx +33 -0
- package/styles/globals.css +15 -0
- package/styles/xertica/app-overrides/chat.css +61 -0
- package/styles/xertica/app-overrides/scrollbar.css +33 -0
- package/styles/xertica/base.css +70 -0
- package/styles/xertica/integrations/google-maps.css +76 -0
- package/styles/xertica/integrations/sonner.css +73 -0
- package/styles/xertica/theme-map.css +88 -0
- package/styles/xertica/tokens.css +190 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +10 -0
- package/utils/gemini.ts +140 -0
- package/vite-env.d.ts +12 -0
- package/vite.config.ts +36 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { X, Save, Download, FileText, Undo, Redo, Bold, Italic, List, ListOrdered, Type } from 'lucide-react';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
import { Separator } from './ui/separator';
|
|
5
|
+
|
|
6
|
+
interface DocumentEditorProps {
|
|
7
|
+
initialContent: string;
|
|
8
|
+
initialTitle?: string;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DocumentEditor({ initialContent, initialTitle = 'Novo Documento', onClose }: DocumentEditorProps) {
|
|
13
|
+
// Converter Markdown para HTML formatado
|
|
14
|
+
const convertMarkdownToHtml = (markdown: string): string => {
|
|
15
|
+
let html = markdown;
|
|
16
|
+
|
|
17
|
+
// Headers
|
|
18
|
+
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
|
|
19
|
+
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
|
|
20
|
+
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
|
|
21
|
+
|
|
22
|
+
// Bold
|
|
23
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
24
|
+
|
|
25
|
+
// Italic
|
|
26
|
+
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
27
|
+
|
|
28
|
+
// Lists (remover marcadores markdown, manter como texto)
|
|
29
|
+
html = html.replace(/^\- (.*$)/gim, '• $1');
|
|
30
|
+
html = html.replace(/^\d+\. (.*$)/gim, '$1');
|
|
31
|
+
|
|
32
|
+
// Checkboxes (converter para texto)
|
|
33
|
+
html = html.replace(/- \[ \] (.*$)/gim, '☐ $1');
|
|
34
|
+
html = html.replace(/- \[x\] (.*$)/gim, '☑ $1');
|
|
35
|
+
|
|
36
|
+
// Horizontal rule
|
|
37
|
+
html = html.replace(/^---$/gim, '─────────────────────');
|
|
38
|
+
|
|
39
|
+
// Line breaks
|
|
40
|
+
html = html.replace(/\n/g, '<br/>');
|
|
41
|
+
|
|
42
|
+
return html;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const formattedInitialContent = convertMarkdownToHtml(initialContent);
|
|
46
|
+
|
|
47
|
+
const [title, setTitle] = useState(initialTitle);
|
|
48
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
49
|
+
const [isUserEditing, setIsUserEditing] = useState(false);
|
|
50
|
+
const [lastSavedContent, setLastSavedContent] = useState(initialContent);
|
|
51
|
+
const [isActivelyTyping, setIsActivelyTyping] = useState(false);
|
|
52
|
+
const [history, setHistory] = useState<string[]>([initialContent]);
|
|
53
|
+
const [historyIndex, setHistoryIndex] = useState(0);
|
|
54
|
+
|
|
55
|
+
const editorRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
const typingTimer = useRef<NodeJS.Timeout | null>(null);
|
|
57
|
+
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
|
|
58
|
+
const highlightTimer = useRef<NodeJS.Timeout | null>(null);
|
|
59
|
+
const lastContentRef = useRef(initialContent);
|
|
60
|
+
const isComposingRef = useRef(false);
|
|
61
|
+
const isProcessingRef = useRef(false);
|
|
62
|
+
|
|
63
|
+
// Handler para input
|
|
64
|
+
const handleInput = () => {
|
|
65
|
+
if (!editorRef.current || isComposingRef.current || isProcessingRef.current) return;
|
|
66
|
+
|
|
67
|
+
const newContent = editorRef.current.innerText;
|
|
68
|
+
const oldContent = lastContentRef.current;
|
|
69
|
+
|
|
70
|
+
if (newContent !== oldContent) {
|
|
71
|
+
lastContentRef.current = newContent;
|
|
72
|
+
setIsUserEditing(true);
|
|
73
|
+
setIsActivelyTyping(true);
|
|
74
|
+
|
|
75
|
+
// Adicionar ao histórico com debounce
|
|
76
|
+
if (debounceTimer.current) {
|
|
77
|
+
clearTimeout(debounceTimer.current);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
debounceTimer.current = setTimeout(() => {
|
|
81
|
+
setHistory(prev => {
|
|
82
|
+
const newHistory = prev.slice(0, historyIndex + 1);
|
|
83
|
+
return [...newHistory, newContent];
|
|
84
|
+
});
|
|
85
|
+
setHistoryIndex(prev => prev + 1);
|
|
86
|
+
}, 500);
|
|
87
|
+
|
|
88
|
+
// Aplicar destaque temporário
|
|
89
|
+
applyTemporaryHighlight();
|
|
90
|
+
|
|
91
|
+
// Timer para parar indicador de digitação
|
|
92
|
+
if (typingTimer.current) {
|
|
93
|
+
clearTimeout(typingTimer.current);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
typingTimer.current = setTimeout(() => {
|
|
97
|
+
setIsActivelyTyping(false);
|
|
98
|
+
}, 1500);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Aplicar destaque temporário ao fundo
|
|
103
|
+
const applyTemporaryHighlight = () => {
|
|
104
|
+
if (!editorRef.current) return;
|
|
105
|
+
|
|
106
|
+
isProcessingRef.current = true;
|
|
107
|
+
|
|
108
|
+
// Adicionar cor de destaque usando variável do sistema
|
|
109
|
+
editorRef.current.style.backgroundColor = 'var(--accent)';
|
|
110
|
+
|
|
111
|
+
// Remover destaque após 2 segundos
|
|
112
|
+
if (highlightTimer.current) {
|
|
113
|
+
clearTimeout(highlightTimer.current);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
highlightTimer.current = setTimeout(() => {
|
|
117
|
+
if (editorRef.current) {
|
|
118
|
+
editorRef.current.style.backgroundColor = '';
|
|
119
|
+
}
|
|
120
|
+
isProcessingRef.current = false;
|
|
121
|
+
}, 2000);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Undo
|
|
125
|
+
const handleUndo = () => {
|
|
126
|
+
if (historyIndex > 0) {
|
|
127
|
+
const newIndex = historyIndex - 1;
|
|
128
|
+
setHistoryIndex(newIndex);
|
|
129
|
+
const content = history[newIndex];
|
|
130
|
+
if (editorRef.current) {
|
|
131
|
+
isProcessingRef.current = true;
|
|
132
|
+
editorRef.current.innerText = content;
|
|
133
|
+
lastContentRef.current = content;
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
isProcessingRef.current = false;
|
|
136
|
+
}, 100);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Redo
|
|
142
|
+
const handleRedo = () => {
|
|
143
|
+
if (historyIndex < history.length - 1) {
|
|
144
|
+
const newIndex = historyIndex + 1;
|
|
145
|
+
setHistoryIndex(newIndex);
|
|
146
|
+
const content = history[newIndex];
|
|
147
|
+
if (editorRef.current) {
|
|
148
|
+
isProcessingRef.current = true;
|
|
149
|
+
editorRef.current.innerText = content;
|
|
150
|
+
lastContentRef.current = content;
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
isProcessingRef.current = false;
|
|
153
|
+
}, 100);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Aplicar formatação
|
|
159
|
+
const applyFormat = (command: string, value?: string) => {
|
|
160
|
+
document.execCommand(command, false, value);
|
|
161
|
+
editorRef.current?.focus();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Handlers para composição (IME)
|
|
165
|
+
const handleCompositionStart = () => {
|
|
166
|
+
isComposingRef.current = true;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleCompositionEnd = () => {
|
|
170
|
+
isComposingRef.current = false;
|
|
171
|
+
handleInput();
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Atalhos de teclado
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
177
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
handleUndo();
|
|
180
|
+
} else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
handleRedo();
|
|
183
|
+
} else if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
applyFormat('bold');
|
|
186
|
+
} else if ((e.ctrlKey || e.metaKey) && e.key === 'i') {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
applyFormat('italic');
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
193
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
194
|
+
}, [historyIndex, history]);
|
|
195
|
+
|
|
196
|
+
// Carregar conteúdo formatado inicialmente
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (editorRef.current && !editorRef.current.innerHTML) {
|
|
199
|
+
editorRef.current.innerHTML = formattedInitialContent;
|
|
200
|
+
lastContentRef.current = initialContent;
|
|
201
|
+
}
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
// Limpar timers
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
return () => {
|
|
207
|
+
if (typingTimer.current) clearTimeout(typingTimer.current);
|
|
208
|
+
if (debounceTimer.current) clearTimeout(debounceTimer.current);
|
|
209
|
+
if (highlightTimer.current) clearTimeout(highlightTimer.current);
|
|
210
|
+
};
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const handleSave = () => {
|
|
214
|
+
setIsSaving(true);
|
|
215
|
+
setIsActivelyTyping(false);
|
|
216
|
+
|
|
217
|
+
if (typingTimer.current) {
|
|
218
|
+
clearTimeout(typingTimer.current);
|
|
219
|
+
}
|
|
220
|
+
if (highlightTimer.current) {
|
|
221
|
+
clearTimeout(highlightTimer.current);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
setIsSaving(false);
|
|
226
|
+
setLastSavedContent(editorRef.current?.innerText || '');
|
|
227
|
+
setIsUserEditing(false);
|
|
228
|
+
|
|
229
|
+
// Remover destaque
|
|
230
|
+
if (editorRef.current) {
|
|
231
|
+
editorRef.current.style.backgroundColor = '';
|
|
232
|
+
}
|
|
233
|
+
}, 1000);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleDownload = () => {
|
|
237
|
+
const content = editorRef.current?.innerText || '';
|
|
238
|
+
const blob = new Blob([content], { type: 'text/plain' });
|
|
239
|
+
const url = URL.createObjectURL(blob);
|
|
240
|
+
const a = document.createElement('a');
|
|
241
|
+
a.href = url;
|
|
242
|
+
a.download = `${title}.txt`;
|
|
243
|
+
document.body.appendChild(a);
|
|
244
|
+
a.click();
|
|
245
|
+
document.body.removeChild(a);
|
|
246
|
+
URL.revokeObjectURL(url);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const getWordCount = () => {
|
|
250
|
+
const content = editorRef.current?.innerText || '';
|
|
251
|
+
return content.split(/\s+/).filter(word => word.length > 0).length;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const hasUnsavedChanges = () => {
|
|
255
|
+
return (editorRef.current?.innerText || '') !== lastSavedContent;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const canUndo = historyIndex > 0;
|
|
259
|
+
const canRedo = historyIndex < history.length - 1;
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div className="flex flex-col h-full bg-background">
|
|
263
|
+
{/* Header */}
|
|
264
|
+
<div className="flex-shrink-0 border-b border-border">
|
|
265
|
+
<div className="flex items-center justify-between px-[10px] py-[16px]">
|
|
266
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
267
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[var(--chart-4)] to-[var(--chart-4)]/80 flex items-center justify-center flex-shrink-0">
|
|
268
|
+
<FileText className="w-4 h-4 text-white" />
|
|
269
|
+
</div>
|
|
270
|
+
<input
|
|
271
|
+
type="text"
|
|
272
|
+
value={title}
|
|
273
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
274
|
+
className="flex-1 bg-transparent border-none outline-none text-sm font-medium text-foreground min-w-0"
|
|
275
|
+
placeholder="Título do documento"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="flex items-center gap-1 flex-shrink-0 ml-2">
|
|
279
|
+
<Button
|
|
280
|
+
variant="ghost"
|
|
281
|
+
size="sm"
|
|
282
|
+
onClick={handleUndo}
|
|
283
|
+
disabled={!canUndo}
|
|
284
|
+
className="h-8 w-8 p-0"
|
|
285
|
+
title="Desfazer (Ctrl+Z)"
|
|
286
|
+
>
|
|
287
|
+
<Undo className="w-4 h-4" />
|
|
288
|
+
</Button>
|
|
289
|
+
<Button
|
|
290
|
+
variant="ghost"
|
|
291
|
+
size="sm"
|
|
292
|
+
onClick={handleRedo}
|
|
293
|
+
disabled={!canRedo}
|
|
294
|
+
className="h-8 w-8 p-0"
|
|
295
|
+
title="Refazer (Ctrl+Y)"
|
|
296
|
+
>
|
|
297
|
+
<Redo className="w-4 h-4" />
|
|
298
|
+
</Button>
|
|
299
|
+
<Separator orientation="vertical" className="h-5 mx-1" />
|
|
300
|
+
<Button
|
|
301
|
+
variant="ghost"
|
|
302
|
+
size="sm"
|
|
303
|
+
onClick={onClose}
|
|
304
|
+
className="h-8 w-8 p-0"
|
|
305
|
+
>
|
|
306
|
+
<X className="w-4 h-4" />
|
|
307
|
+
</Button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{/* Toolbar */}
|
|
312
|
+
<div className="px-3 pb-2 border-t border-border bg-muted">
|
|
313
|
+
<div className="flex items-center gap-1 py-2">
|
|
314
|
+
<Button
|
|
315
|
+
variant="ghost"
|
|
316
|
+
size="sm"
|
|
317
|
+
onClick={() => applyFormat('bold')}
|
|
318
|
+
className="h-8 w-8 p-0"
|
|
319
|
+
title="Negrito (Ctrl+B)"
|
|
320
|
+
>
|
|
321
|
+
<Bold className="w-4 h-4" />
|
|
322
|
+
</Button>
|
|
323
|
+
<Button
|
|
324
|
+
variant="ghost"
|
|
325
|
+
size="sm"
|
|
326
|
+
onClick={() => applyFormat('italic')}
|
|
327
|
+
className="h-8 w-8 p-0"
|
|
328
|
+
title="Itálico (Ctrl+I)"
|
|
329
|
+
>
|
|
330
|
+
<Italic className="w-4 h-4" />
|
|
331
|
+
</Button>
|
|
332
|
+
<Separator orientation="vertical" className="h-5 mx-1" />
|
|
333
|
+
<Button
|
|
334
|
+
variant="ghost"
|
|
335
|
+
size="sm"
|
|
336
|
+
onClick={() => applyFormat('formatBlock', 'h1')}
|
|
337
|
+
className="h-8 px-2"
|
|
338
|
+
title="Título 1"
|
|
339
|
+
>
|
|
340
|
+
<span className="text-xs font-medium">H1</span>
|
|
341
|
+
</Button>
|
|
342
|
+
<Button
|
|
343
|
+
variant="ghost"
|
|
344
|
+
size="sm"
|
|
345
|
+
onClick={() => applyFormat('formatBlock', 'h2')}
|
|
346
|
+
className="h-8 px-2"
|
|
347
|
+
title="Título 2"
|
|
348
|
+
>
|
|
349
|
+
<span className="text-xs font-medium">H2</span>
|
|
350
|
+
</Button>
|
|
351
|
+
<Button
|
|
352
|
+
variant="ghost"
|
|
353
|
+
size="sm"
|
|
354
|
+
onClick={() => applyFormat('formatBlock', 'p')}
|
|
355
|
+
className="h-8 px-2"
|
|
356
|
+
title="Parágrafo"
|
|
357
|
+
>
|
|
358
|
+
<Type className="w-4 h-4" />
|
|
359
|
+
</Button>
|
|
360
|
+
<Separator orientation="vertical" className="h-5 mx-1" />
|
|
361
|
+
<Button
|
|
362
|
+
variant="ghost"
|
|
363
|
+
size="sm"
|
|
364
|
+
onClick={() => applyFormat('insertUnorderedList')}
|
|
365
|
+
className="h-8 w-8 p-0"
|
|
366
|
+
title="Lista com marcadores"
|
|
367
|
+
>
|
|
368
|
+
<List className="w-4 h-4" />
|
|
369
|
+
</Button>
|
|
370
|
+
<Button
|
|
371
|
+
variant="ghost"
|
|
372
|
+
size="sm"
|
|
373
|
+
onClick={() => applyFormat('insertOrderedList')}
|
|
374
|
+
className="h-8 w-8 p-0"
|
|
375
|
+
title="Lista numerada"
|
|
376
|
+
>
|
|
377
|
+
<ListOrdered className="w-4 h-4" />
|
|
378
|
+
</Button>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{/* Content Editor */}
|
|
384
|
+
<div className="flex-1 overflow-auto">
|
|
385
|
+
<div
|
|
386
|
+
ref={editorRef}
|
|
387
|
+
contentEditable
|
|
388
|
+
onInput={handleInput}
|
|
389
|
+
onCompositionStart={handleCompositionStart}
|
|
390
|
+
onCompositionEnd={handleCompositionEnd}
|
|
391
|
+
className="p-4 min-h-full outline-none text-foreground leading-relaxed text-sm transition-colors duration-300 editor-content font-sans whitespace-pre-wrap break-word"
|
|
392
|
+
suppressContentEditableWarning
|
|
393
|
+
data-placeholder="Comece a escrever seu documento..."
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{/* Footer Actions */}
|
|
398
|
+
<div className="flex-shrink-0 border-t border-border p-3">
|
|
399
|
+
<div className="flex items-center justify-between gap-2">
|
|
400
|
+
<div className="flex items-center gap-2">
|
|
401
|
+
<div className="text-xs text-muted-foreground">
|
|
402
|
+
{getWordCount()} palavras
|
|
403
|
+
</div>
|
|
404
|
+
{isActivelyTyping ? (
|
|
405
|
+
<div className="flex items-center gap-1 text-xs text-[var(--toast-success-icon)]">
|
|
406
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--toast-success-icon)] animate-pulse" />
|
|
407
|
+
<span>Digitando...</span>
|
|
408
|
+
</div>
|
|
409
|
+
) : isUserEditing && hasUnsavedChanges() ? (
|
|
410
|
+
<div className="flex items-center gap-1 text-xs text-[var(--toast-warning-icon)]">
|
|
411
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--toast-warning-icon)] animate-pulse" />
|
|
412
|
+
<span>Alterações não salvas</span>
|
|
413
|
+
</div>
|
|
414
|
+
) : null}
|
|
415
|
+
</div>
|
|
416
|
+
<div className="flex items-center gap-2">
|
|
417
|
+
<Button
|
|
418
|
+
variant="outline"
|
|
419
|
+
size="sm"
|
|
420
|
+
onClick={handleDownload}
|
|
421
|
+
className="h-8"
|
|
422
|
+
>
|
|
423
|
+
<Download className="w-3.5 h-3.5 mr-1.5" />
|
|
424
|
+
Baixar
|
|
425
|
+
</Button>
|
|
426
|
+
<Button
|
|
427
|
+
size="sm"
|
|
428
|
+
onClick={handleSave}
|
|
429
|
+
disabled={isSaving}
|
|
430
|
+
className="bg-primary hover:bg-primary/90 text-primary-foreground h-8"
|
|
431
|
+
>
|
|
432
|
+
<Save className="w-3.5 h-3.5 mr-1.5" />
|
|
433
|
+
{isSaving ? 'Salvando...' : 'Salvar'}
|
|
434
|
+
</Button>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<style>{`
|
|
440
|
+
[contenteditable]:empty:before {
|
|
441
|
+
content: attr(data-placeholder);
|
|
442
|
+
color: var(--muted-foreground);
|
|
443
|
+
pointer-events: none;
|
|
444
|
+
}
|
|
445
|
+
.dark [contenteditable]:empty:before {
|
|
446
|
+
color: var(--muted-foreground);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* Estilos do editor sem margens extras */
|
|
450
|
+
.editor-content h1,
|
|
451
|
+
.editor-content h2,
|
|
452
|
+
.editor-content h3,
|
|
453
|
+
.editor-content h4,
|
|
454
|
+
.editor-content h5,
|
|
455
|
+
.editor-content h6 {
|
|
456
|
+
margin: 0;
|
|
457
|
+
padding: 0;
|
|
458
|
+
font-weight: 600;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.editor-content h1 {
|
|
462
|
+
font-size: 2em;
|
|
463
|
+
margin-top: 0.5em;
|
|
464
|
+
margin-bottom: 0.5em;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.editor-content h2 {
|
|
468
|
+
font-size: 1.5em;
|
|
469
|
+
margin-top: 0.5em;
|
|
470
|
+
margin-bottom: 0.5em;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.editor-content h3 {
|
|
474
|
+
font-size: 1.25em;
|
|
475
|
+
margin-top: 0.5em;
|
|
476
|
+
margin-bottom: 0.5em;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.editor-content p {
|
|
480
|
+
margin: 0;
|
|
481
|
+
padding: 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.editor-content ul,
|
|
485
|
+
.editor-content ol {
|
|
486
|
+
margin: 0.5em 0;
|
|
487
|
+
padding-left: 1.5em;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.editor-content li {
|
|
491
|
+
margin: 0.25em 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.editor-content strong {
|
|
495
|
+
font-weight: 600;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.editor-content em {
|
|
499
|
+
font-style: italic;
|
|
500
|
+
}
|
|
501
|
+
`}</style>
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button } from './ui/button';
|
|
3
|
+
import { Input } from './ui/input';
|
|
4
|
+
import { Label } from './ui/label';
|
|
5
|
+
import { XerticaLogo } from './XerticaLogo';
|
|
6
|
+
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
7
|
+
import { LanguageSelector } from './LanguageSelector';
|
|
8
|
+
import { ArrowLeft, Lock } from 'lucide-react';
|
|
9
|
+
import { useNavigate } from 'react-router';
|
|
10
|
+
|
|
11
|
+
export function ForgotPasswordPage() {
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const [email, setEmail] = useState('');
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
setIsLoading(true);
|
|
19
|
+
|
|
20
|
+
// Simula envio de email
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
22
|
+
|
|
23
|
+
// Navega para a tela de verificação
|
|
24
|
+
navigate('/verify-email', { state: { email } });
|
|
25
|
+
|
|
26
|
+
setIsLoading(false);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleSocialLogin = (provider: string) => {
|
|
30
|
+
// Simula login social/SSO
|
|
31
|
+
console.log(`Login com ${provider}`);
|
|
32
|
+
// Aqui seria implementada a integração real com cada provedor
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="min-h-screen flex">
|
|
37
|
+
{/* Lado esquerdo - Imagem de fundo completa */}
|
|
38
|
+
<div className="hidden lg:flex lg:flex-1 relative overflow-hidden">
|
|
39
|
+
{/* Imagem de fundo preenchendo todo o espaço */}
|
|
40
|
+
<ImageWithFallback
|
|
41
|
+
src="https://images.unsplash.com/photo-1557804506-669a67965ba0?w=1200&h=800&fit=crop&auto=format"
|
|
42
|
+
alt="Segurança e tecnologia"
|
|
43
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
44
|
+
/>
|
|
45
|
+
|
|
46
|
+
{/* Overlay com gradiente */}
|
|
47
|
+
<div className="absolute inset-0 bg-[image:var(--gradient-diagonal)] opacity-80" />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Lado direito - Formulário */}
|
|
51
|
+
<div className="flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8 lg:flex-none lg:w-1/2 relative bg-muted">
|
|
52
|
+
{/* Seletor de idioma no canto superior direito */}
|
|
53
|
+
<div className="absolute top-4 right-4 z-20">
|
|
54
|
+
<LanguageSelector variant="minimal" showIcon={false} />
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{/* Gradiente de fundo para mobile */}
|
|
58
|
+
<div className="absolute inset-0 lg:hidden bg-[image:var(--gradient-diagonal)] opacity-10 dark:opacity-5" />
|
|
59
|
+
|
|
60
|
+
<div className="w-full max-w-sm space-y-6 relative z-10">
|
|
61
|
+
{/* Header do formulário */}
|
|
62
|
+
<div className="text-center">
|
|
63
|
+
<div className="flex items-center justify-center mb-4">
|
|
64
|
+
<XerticaLogo
|
|
65
|
+
className="h-12 w-auto text-primary dark:text-foreground"
|
|
66
|
+
variant="theme"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<h2 className="text-sm text-muted-foreground">Recuperar senha</h2>
|
|
70
|
+
<p className="mt-2 text-muted-foreground">
|
|
71
|
+
Digite seu email e enviaremos instruções para redefinir sua senha
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Formulário */}
|
|
76
|
+
<form className="space-y-6" onSubmit={handleSubmit}>
|
|
77
|
+
<div className="form-group space-y-2">
|
|
78
|
+
<Label htmlFor="email" className="form-label">
|
|
79
|
+
E-mail
|
|
80
|
+
</Label>
|
|
81
|
+
<Input
|
|
82
|
+
id="email"
|
|
83
|
+
name="email"
|
|
84
|
+
type="email"
|
|
85
|
+
required
|
|
86
|
+
className="w-full"
|
|
87
|
+
placeholder="seu@email.com"
|
|
88
|
+
value={email}
|
|
89
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="space-y-3">
|
|
94
|
+
<Button
|
|
95
|
+
type="submit"
|
|
96
|
+
className="w-full"
|
|
97
|
+
disabled={isLoading}
|
|
98
|
+
>
|
|
99
|
+
{isLoading ? 'Enviando...' : 'Enviar instruções'}
|
|
100
|
+
</Button>
|
|
101
|
+
|
|
102
|
+
<Button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => navigate('/login')}
|
|
105
|
+
variant="outline"
|
|
106
|
+
className="w-full text-muted-foreground hover:text-foreground"
|
|
107
|
+
>
|
|
108
|
+
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
109
|
+
Voltar para login
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
</form>
|
|
113
|
+
|
|
114
|
+
{/* Divider */}
|
|
115
|
+
<div className="relative">
|
|
116
|
+
<div className="absolute inset-0 flex items-center">
|
|
117
|
+
<div className="w-full border-t border-border"></div>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="relative flex justify-center text-sm">
|
|
120
|
+
<span className="bg-muted px-2 text-muted-foreground">
|
|
121
|
+
ou continuar com
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Opções de login social/SSO */}
|
|
127
|
+
<div className="space-y-3">
|
|
128
|
+
{/* Google */}
|
|
129
|
+
<Button
|
|
130
|
+
type="button"
|
|
131
|
+
variant="outline"
|
|
132
|
+
className="w-full justify-center"
|
|
133
|
+
onClick={() => handleSocialLogin('Google')}
|
|
134
|
+
>
|
|
135
|
+
<svg className="w-5 h-5 mr-2" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
|
136
|
+
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
|
|
137
|
+
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
|
|
138
|
+
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
|
|
139
|
+
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
|
|
140
|
+
<path fill="none" d="M0 0h48v48H0z"></path>
|
|
141
|
+
</svg>
|
|
142
|
+
<span>Entrar com Google</span>
|
|
143
|
+
</Button>
|
|
144
|
+
|
|
145
|
+
{/* MT Login */}
|
|
146
|
+
<Button
|
|
147
|
+
type="button"
|
|
148
|
+
variant="outline"
|
|
149
|
+
className="w-full justify-center"
|
|
150
|
+
onClick={() => handleSocialLogin('MT Login')}
|
|
151
|
+
>
|
|
152
|
+
<Lock className="w-5 h-5 mr-2 text-[var(--chart-4)]" />
|
|
153
|
+
<span>Entrar com MT Login</span>
|
|
154
|
+
</Button>
|
|
155
|
+
|
|
156
|
+
{/* gov.br */}
|
|
157
|
+
<Button
|
|
158
|
+
type="button"
|
|
159
|
+
variant="outline"
|
|
160
|
+
className="w-full justify-center font-normal"
|
|
161
|
+
onClick={() => handleSocialLogin('gov.br')}
|
|
162
|
+
>
|
|
163
|
+
<span>Entrar com <strong className="font-semibold">gov.br</strong></span>
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|