fix(erp): enable erp pages and menu items

This commit is contained in:
Erik Silva
2025-12-29 17:23:59 -03:00
parent e124a64a5d
commit adbff9bb1e
13990 changed files with 1110936 additions and 59 deletions

View File

@@ -0,0 +1,199 @@
'use client';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Document, docApi } from '@/lib/api-docs';
import {
XMarkIcon,
CheckIcon,
ArrowLeftIcon,
Bars3BottomLeftIcon,
HashtagIcon,
CloudArrowUpIcon,
SparklesIcon
} from "@heroicons/react/24/outline";
import NotionEditor from './NotionEditor';
import DocumentSidebar from './DocumentSidebar';
import { toast } from 'react-hot-toast';
interface DocumentEditorProps {
initialDocument: Partial<Document> | null;
onSave: (doc: Partial<Document>) => void;
onCancel: () => void;
}
export default function DocumentEditor({ initialDocument, onSave, onCancel }: DocumentEditorProps) {
const [document, setDocument] = useState<Partial<Document> | null>(initialDocument);
const [title, setTitle] = useState(initialDocument?.title || '');
const [content, setContent] = useState(initialDocument?.content || '[]');
const [showSidebar, setShowSidebar] = useState(true);
const [saving, setSaving] = useState(false);
// Refs para controle fino de salvamento
const saveTimeout = useRef<NodeJS.Timeout | null>(null);
const lastSaved = useRef({ title: initialDocument?.title || '', content: initialDocument?.content || '[]' });
useEffect(() => {
if (initialDocument) {
setDocument(initialDocument);
setTitle(initialDocument.title || '');
setContent(initialDocument.content || '[]');
lastSaved.current = {
title: initialDocument.title || '',
content: initialDocument.content || '[]'
};
}
}, [initialDocument]);
// Função de Auto-Save Robusta
const autoSave = useCallback(async (newTitle: string, newContent: string) => {
if (!document?.id) return;
setSaving(true);
console.log('💾 Inactivity detected. Saving document...', document.id);
try {
await docApi.updateDocument(document.id, {
title: newTitle,
content: newContent,
status: 'published'
});
// Atualiza o ref do último salvo para evitar loop
lastSaved.current = { title: newTitle, content: newContent };
console.log('✅ Document saved successfully');
} catch (e) {
console.error('❌ Auto-save failed', e);
toast.error('Erro ao salvar automaticamente');
} finally {
// Delay visual para o feedback de "Salvo"
setTimeout(() => setSaving(false), 800);
}
}, [document?.id]);
// Trigger de auto-save com debounce de 1 segundo
useEffect(() => {
if (!document?.id) return;
// Verifica se houve mudança real em relação ao último salvo
if (title === lastSaved.current.title && content === lastSaved.current.content) {
return;
}
if (saveTimeout.current) clearTimeout(saveTimeout.current);
saveTimeout.current = setTimeout(() => {
autoSave(title, content);
}, 1000); // Salva após 1 segundo de inatividade
return () => {
if (saveTimeout.current) clearTimeout(saveTimeout.current);
};
}, [title, content, document?.id, autoSave]);
const navigateToDoc = async (doc: Document) => {
// Antes de navegar, salva o atual se necessário
if (title !== lastSaved.current.title || content !== lastSaved.current.content) {
await autoSave(title, content);
}
setDocument(doc);
setTitle(doc.title);
setContent(doc.content);
lastSaved.current = { title: doc.title, content: doc.content };
toast.success(`Abrindo: ${doc.title || 'Untitled'}`, { duration: 1000, position: 'bottom-center' });
};
return (
<div className="fixed inset-0 z-[60] bg-white dark:bg-zinc-950 flex flex-col">
{/* Header Clean */}
<header className="h-16 border-b border-zinc-200 dark:border-zinc-800 px-6 flex items-center justify-between bg-white dark:bg-zinc-950 z-20 shrink-0">
<div className="flex items-center gap-4 flex-1">
<button
onClick={async () => {
if (title !== lastSaved.current.title || content !== lastSaved.current.content) {
await autoSave(title, content);
}
onCancel();
}}
className="p-2 text-zinc-400 hover:text-zinc-900 dark:hover:text-white rounded-xl hover:bg-zinc-100 dark:hover:bg-zinc-900 transition-all"
title="Back to list"
>
<ArrowLeftIcon className="w-5 h-5" />
</button>
<button
onClick={() => setShowSidebar(!showSidebar)}
className={`p-2 rounded-xl transition-all ${showSidebar ? 'text-brand-500 bg-brand-50/50 dark:bg-brand-500/10' : 'text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-900'}`}
title={showSidebar ? "Hide Navigation" : "Show Navigation"}
>
<Bars3BottomLeftIcon className="w-5 h-5" />
</button>
<div className="h-4 w-px bg-zinc-200 dark:bg-zinc-800 mx-2" />
<div className="flex items-center gap-2 flex-1">
<HashtagIcon className="w-4 h-4 text-zinc-300" />
<input
type="text"
placeholder="Untitled Document"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="text-lg font-bold bg-transparent border-none outline-none text-zinc-900 dark:text-white placeholder:text-zinc-300 dark:placeholder:text-zinc-600 w-full max-w-xl"
/>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-50 dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800">
{saving ? (
<div className="flex items-center gap-2 text-brand-500">
<div className="w-1.5 h-1.5 bg-brand-500 rounded-full animate-pulse" />
<span className="text-[10px] font-black uppercase tracking-widest leading-none">Saving...</span>
</div>
) : (
<div className="flex items-center gap-2 text-emerald-500">
<CheckIcon className="w-3.5 h-3.5" />
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-500 dark:text-zinc-400 leading-none">Sync Saved</span>
</div>
)}
</div>
</div>
</header>
<div className="flex-1 flex overflow-hidden">
{/* Lateral Sidebar (Navegação) */}
{showSidebar && document?.id && (
<DocumentSidebar
key={document.id} // Re-render sidebar on doc change to ensure fresh data
documentId={document.id}
onNavigate={navigateToDoc}
/>
)}
{/* Área do Editor */}
<main className="flex-1 overflow-y-auto bg-white dark:bg-zinc-950 flex flex-col items-center custom-scrollbar">
<div className="w-full max-w-[850px] px-12 md:px-24 py-16 animate-in slide-in-from-bottom-2 duration-500">
{/* Title Hero Display */}
<div className="mb-14 group">
<h1 className="text-5xl font-black text-zinc-900 dark:text-white tracking-tighter leading-tight">
{title || 'Untitled'}
</h1>
<div className="mt-6 flex items-center gap-4 text-zinc-400">
<SparklesIcon className="w-4 h-4 text-brand-500" />
<div className="flex items-center gap-2">
<div className="w-5 h-5 rounded-full bg-brand-500 flex items-center justify-center text-[10px] font-bold text-white uppercase">Me</div>
<span className="text-[10px] font-black uppercase tracking-widest">Editing Mode Real-time Sync</span>
</div>
</div>
</div>
<NotionEditor
documentId={document?.id}
initialContent={content}
onChange={setContent}
/>
</div>
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,86 @@
'use client';
import React from 'react';
import { Document } from '@/lib/api-docs';
import {
DocumentTextIcon,
PencilSquareIcon,
TrashIcon,
CalendarIcon
} from "@heroicons/react/24/outline";
import { format, parseISO } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { Card } from "@/components/ui";
interface DocumentListProps {
documents: Document[];
onEdit: (doc: Document) => void;
onDelete: (id: string) => void;
}
export default function DocumentList({ documents, onEdit, onDelete }: DocumentListProps) {
if (documents.length === 0) {
return (
<div className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 p-12 text-center">
<DocumentTextIcon className="w-12 h-12 text-zinc-300 mx-auto mb-4" />
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Nenhum documento ainda</h3>
<p className="text-zinc-500 max-w-xs mx-auto mt-2">
Comece criando seu primeiro documento de texto para sua agência.
</p>
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{documents.map((doc) => (
<Card
key={doc.id}
className="group hover:shadow-xl transition-all border-2 border-transparent hover:border-brand-500/20"
>
<div className="flex flex-col h-full">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-brand-50 dark:bg-brand-500/10 rounded-xl">
<DocumentTextIcon className="w-6 h-6 text-brand-600 dark:text-brand-400" />
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-all">
<button
onClick={() => onEdit(doc)}
className="p-2 text-zinc-400 hover:text-brand-500 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
>
<PencilSquareIcon className="w-4 h-4" />
</button>
<button
onClick={() => onDelete(doc.id)}
className="p-2 text-zinc-400 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-lg transition-colors"
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
</div>
<h3 className="text-base font-bold text-zinc-900 dark:text-white mb-2 line-clamp-1">
{doc.title || 'Documento sem título'}
</h3>
<p className="text-sm text-zinc-500 line-clamp-3 mb-6 flex-1">
{doc.content ? doc.content.replace(/<[^>]*>/g, '').substring(0, 150) : 'Sem conteúdo...'}
</p>
<div className="flex items-center gap-2 pt-4 border-t border-zinc-100 dark:border-zinc-800">
<CalendarIcon className="w-3.5 h-3.5 text-zinc-400" />
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">
{format(parseISO(doc.updated_at), "dd 'de' MMMM", { locale: ptBR })}
</span>
{doc.status === 'draft' && (
<span className="ml-auto text-[10px] font-black uppercase tracking-widest text-amber-500 bg-amber-50 dark:bg-amber-500/10 px-2 py-0.5 rounded-full">
Rascunho
</span>
)}
</div>
</div>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,225 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Document, DocumentActivity, docApi } from '@/lib/api-docs';
import {
ClockIcon,
DocumentIcon,
PlusIcon,
MagnifyingGlassIcon,
ChevronRightIcon,
HomeIcon,
ArrowUpIcon,
FolderIcon,
FolderOpenIcon
} from "@heroicons/react/24/outline";
import { format, parseISO } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { toast } from 'react-hot-toast';
interface DocumentSidebarProps {
documentId: string;
onNavigate: (doc: Document) => void;
}
export default function DocumentSidebar({ documentId, onNavigate }: DocumentSidebarProps) {
const [currentDoc, setCurrentDoc] = useState<Document | null>(null);
const [children, setChildren] = useState<Document[]>([]);
const [parentDoc, setParentDoc] = useState<Document | null>(null);
const [rootPages, setRootPages] = useState<Document[]>([]);
const [activities, setActivities] = useState<DocumentActivity[]>([]);
const [activeTab, setActiveTab] = useState<'subpages' | 'activities'>('subpages');
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
if (documentId) {
fetchData();
}
}, [documentId]);
const fetchData = async () => {
setLoading(true);
console.log('🔍 Sidebar fetching data for:', documentId);
const fetchSafely = async (promise: Promise<any>) => {
try { return await promise; }
catch (e) { console.warn('Sidebar part failed:', e); return null; }
};
try {
const [doc, roots, logs, subpages] = await Promise.all([
docApi.getDocument(documentId),
fetchSafely(docApi.getDocuments()),
fetchSafely(docApi.getActivities(documentId)),
fetchSafely(docApi.getSubpages(documentId))
]);
if (doc) {
setCurrentDoc(doc);
setChildren(subpages || []);
if (doc.parent_id) {
const parent = await fetchSafely(docApi.getDocument(doc.parent_id));
setParentDoc(parent);
} else {
setParentDoc(null);
}
}
setRootPages(roots || []);
setActivities(logs || []);
} catch (e) {
console.error('❌ Sidebar critical error:', e);
toast.error('Erro ao carregar estrutura');
} finally {
setLoading(false);
}
};
const handleCreateSubpage = async () => {
try {
const newDoc = await docApi.createDocument({
title: 'Nova Subpágina',
parent_id: documentId as any,
content: '{"type":"doc","content":[{"type":"paragraph"}]}',
status: 'published'
});
toast.success('Página criada!');
// Refresh local para aparecer imediatamente
setChildren(prev => [...prev, newDoc]);
onNavigate(newDoc);
} catch (e) {
toast.error('Erro ao criar subpágina');
}
};
const filteredChildren = children.filter(p => p.title.toLowerCase().includes(searchTerm.toLowerCase()));
const rootWikis = rootPages.filter(p => !p.parent_id);
return (
<div className="w-72 border-r border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 flex flex-col h-full overflow-hidden shrink-0">
{/* Tabs de Navegação */}
<div className="flex border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950">
<button
onClick={() => setActiveTab('subpages')}
className={`flex-1 px-4 py-4 text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'subpages' ? 'text-brand-500 bg-brand-50/10 border-b-2 border-brand-500' : 'text-zinc-500'}`}
>
Estrutura
</button>
<button
onClick={() => setActiveTab('activities')}
className={`flex-1 px-4 py-4 text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'activities' ? 'text-brand-500 bg-brand-50/10 border-b-2 border-brand-500' : 'text-zinc-500'}`}
>
Histórico
</button>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-6">
{activeTab === 'subpages' ? (
<>
{/* Status do Nível Atual */}
<div className="space-y-3">
{parentDoc && (
<button
onClick={() => onNavigate(parentDoc)}
className="w-full flex items-center gap-2 p-2 px-3 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-900 text-zinc-400 hover:text-brand-500 transition-all group border border-dashed border-zinc-200 dark:border-zinc-800"
>
<ArrowUpIcon className="w-3 h-3 transition-transform group-hover:-translate-y-0.5" />
<span className="text-[9px] font-black uppercase tracking-widest truncate">Pai: {parentDoc.title}</span>
</button>
)}
<div className="p-3 rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 shadow-sm flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-brand-50 dark:bg-brand-500/10 flex items-center justify-center text-brand-500">
<FolderOpenIcon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-tighter">Você está em</p>
<p className="text-xs font-bold text-zinc-900 dark:text-white truncate">{currentDoc?.title || 'Sem título'}</p>
</div>
</div>
</div>
{/* Search */}
<div className="relative">
<MagnifyingGlassIcon className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 transition-colors ${loading ? 'text-brand-500 animate-pulse' : 'text-zinc-400'}`} />
<input
type="text"
placeholder="Filtrar subpáginas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl pl-10 pr-4 py-2 text-xs outline-none focus:ring-2 ring-brand-500/20"
/>
</div>
{/* Subpages (Children) */}
<div className="space-y-1">
<div className="flex items-center justify-between px-2 mb-2">
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Subpáginas ({children.length})</p>
<button
onClick={handleCreateSubpage}
className="p-1 text-zinc-400 hover:text-brand-500 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-md shadow-sm transition-all"
>
<PlusIcon className="w-3.5 h-3.5" />
</button>
</div>
{filteredChildren.length === 0 && (
<div className="text-center py-6 border border-dashed border-zinc-200 dark:border-zinc-800 rounded-xl">
<p className="text-[10px] text-zinc-400 uppercase font-black">Nenhuma subpágina</p>
</div>
)}
{filteredChildren.map(page => (
<button
key={page.id}
onClick={() => onNavigate(page)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all group border border-transparent hover:border-zinc-100 dark:hover:border-zinc-800 ${documentId === page.id ? 'bg-brand-500 text-white shadow-lg' : 'hover:bg-white dark:hover:bg-zinc-900 text-zinc-600 dark:text-zinc-400'}`}
>
<DocumentIcon className={`w-4 h-4 ${documentId === page.id ? 'text-white' : 'text-zinc-300 group-hover:text-brand-500'}`} />
<span className="text-xs font-bold truncate flex-1 text-left">{page.title || 'Subpágina'}</span>
<ChevronRightIcon className={`w-3 h-3 opacity-0 group-hover:opacity-100 ${documentId === page.id ? 'text-white' : ''}`} />
</button>
))}
</div>
{/* Root wikis */}
{!searchTerm && (
<div className="pt-4 border-t border-zinc-100 dark:border-zinc-800 space-y-1">
<p className="px-3 text-[10px] font-black text-zinc-400 uppercase tracking-widest mb-3">Todas as Wikis</p>
{rootWikis.map(page => (
<button
key={page.id}
onClick={() => onNavigate(page)}
className={`w-full flex items-center gap-3 px-3 py-2 rounded-xl transition-all group ${documentId === page.id ? 'text-brand-500 font-bold' : 'text-zinc-500 hover:bg-white dark:hover:bg-zinc-900'}`}
>
<HomeIcon className="w-3.5 h-3.5" />
<span className="text-[11px] font-semibold truncate flex-1 text-left">{page.title || 'Início'}</span>
</button>
))}
</div>
)}
</>
) : (
<div className="space-y-6">
{activities.map(log => (
<div key={log.id} className="flex gap-4">
<div className="w-8 h-8 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center text-[10px] font-black text-zinc-400 shrink-0">
{log.user_name?.[0]?.toUpperCase()}
</div>
<div className="min-w-0 pt-0.5">
<p className="text-[11px] text-zinc-900 dark:text-zinc-100 leading-tight">
<span className="font-bold">{log.user_name}</span> {log.description}
</p>
<span className="text-[9px] text-zinc-400 uppercase font-black block mt-1">
{format(parseISO(log.created_at), "dd MMM, HH:mm", { locale: ptBR })}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,233 @@
'use client';
import React, { useMemo, useEffect, useState } from 'react';
import { useEditor, EditorContent, BubbleMenu, FloatingMenu } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Placeholder from '@tiptap/extension-placeholder';
import TaskList from '@tiptap/extension-task-list';
import TaskItem from '@tiptap/extension-task-item';
import Heading from '@tiptap/extension-heading';
import {
BoldIcon,
ItalicIcon,
ListBulletIcon,
HashtagIcon,
CodeBracketIcon,
PlusIcon,
Bars2Icon
} from "@heroicons/react/24/outline";
interface NotionEditorProps {
initialContent: string;
onChange: (jsonContent: string) => void;
documentId?: string;
}
export default function NotionEditor({ initialContent, onChange, documentId }: NotionEditorProps) {
const editor = useEditor({
extensions: [
StarterKit.configure({
heading: false,
}),
Heading.configure({
levels: [1, 2, 3],
}),
Placeholder.configure({
placeholder: "Comece a digitar ou aperte '/' para comandos...",
emptyEditorClass: 'is-editor-empty',
}),
TaskList,
TaskItem.configure({
nested: true,
}),
],
content: '',
onUpdate: ({ editor }) => {
const json = editor.getJSON();
onChange(JSON.stringify(json));
},
editorProps: {
attributes: {
class: 'prose prose-zinc dark:prose-invert max-w-none focus:outline-none min-h-[600px] py-10 px-2 editor-content',
},
},
}, [documentId]);
// Sincronizar apenas na primeira carga ou troca de documento
useEffect(() => {
if (!editor || !initialContent) return;
try {
const parsed = JSON.parse(initialContent);
// Evita resetar se o editor já tem conteúdo (para não quebrar a digitação)
if (editor.isEmpty && initialContent !== '{"type":"doc","content":[{"type":"paragraph"}]}') {
editor.commands.setContent(parsed, false);
}
} catch (e) {
if (editor.isEmpty && initialContent !== '[]') {
editor.commands.setContent(initialContent, false);
}
}
}, [editor, documentId]); // Só roda quando o editor é criado ou o documento muda
if (!editor) return null;
return (
<div className="w-full relative min-h-[600px] group/editor selection:bg-brand-100 dark:selection:bg-brand-500/30">
{/* Bubble Menu Customizado */}
<BubbleMenu editor={editor} tippyOptions={{ duration: 150 }} className="flex bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-2xl p-1 gap-0.5 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<MenuButton
onClick={() => editor.chain().focus().toggleBold().run()}
active={editor.isActive('bold')}
icon={<BoldIcon className="w-4 h-4" />}
/>
<MenuButton
onClick={() => editor.chain().focus().toggleItalic().run()}
active={editor.isActive('italic')}
icon={<ItalicIcon className="w-4 h-4" />}
/>
<div className="w-px h-4 bg-zinc-200 dark:bg-zinc-800 mx-1 self-center" />
<MenuButton
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
active={editor.isActive('heading', { level: 1 })}
label="H1"
/>
<MenuButton
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
active={editor.isActive('heading', { level: 2 })}
label="H2"
/>
<MenuButton
onClick={() => editor.chain().focus().toggleBulletList().run()}
active={editor.isActive('bulletList')}
icon={<ListBulletIcon className="w-4 h-4" />}
/>
</BubbleMenu>
{/* Menu Flutuante Estilo Notion */}
<FloatingMenu editor={editor} tippyOptions={{ duration: 150 }} className="flex bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl shadow-2xl p-2 gap-1 overflow-hidden min-w-[240px] flex-col animate-in slide-in-from-left-4 duration-300">
<p className="px-3 py-1 text-[10px] font-black uppercase tracking-widest text-zinc-400">Blocos Básicos</p>
<FloatingItem
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
icon={<HashtagIcon className="w-4 h-4 text-brand-500" />}
title="Título 1"
desc="Título de seção grande"
/>
<FloatingItem
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
icon={<HashtagIcon className="w-4 h-4 text-brand-600" />}
title="Título 2"
desc="Título médio"
/>
<FloatingItem
onClick={() => editor.chain().focus().toggleBulletList().run()}
icon={<ListBulletIcon className="w-4 h-4 text-emerald-500" />}
title="Lista"
desc="Lista simples com marcadores"
/>
<FloatingItem
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
icon={<CodeBracketIcon className="w-4 h-4 text-zinc-600" />}
title="Código"
desc="Bloco para trechos de código"
/>
</FloatingMenu>
{/* Área do Editor */}
<div className="relative editor-shell">
<EditorContent editor={editor} />
</div>
<style dangerouslySetInnerHTML={{
__html: `
.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #adb5bd;
pointer-events: none;
height: 0;
}
.tiptap {
font-size: 1.15rem;
line-height: 1.8;
color: #374151;
padding-bottom: 200px;
}
.dark .tiptap { color: #d1d5db; }
.tiptap h1 { font-size: 3.5rem; font-weight: 950; letter-spacing: -0.06em; margin-top: 3rem; margin-bottom: 1.5rem; color: #111827; line-height: 1.1; }
.tiptap h2 { font-size: 2.2rem; font-weight: 800; letter-spacing: -0.04em; margin-top: 2.5rem; margin-bottom: 1rem; color: #1f2937; line-height: 1.2; }
.tiptap h3 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; margin-top: 2rem; margin-bottom: 0.75rem; color: #374151; }
.dark .tiptap h1 { color: #f9fafb; }
.dark .tiptap h2 { color: #f3f4f6; }
.dark .tiptap h3 { color: #e5e7eb; }
.tiptap p { margin: 0.5rem 0; }
.tiptap ul { list-style-type: disc; padding-left: 1.5rem; margin: 1rem 0; }
.tiptap ol { list-style-type: decimal; padding-left: 1.5rem; margin: 1rem 0; }
.tiptap li { margin: 0.25rem 0; }
.tiptap pre {
background: #f8fafc;
border-radius: 1.25rem;
padding: 1.5rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
border: 1px solid #e2e8f0;
margin: 2rem 0;
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05);
}
.dark .tiptap pre { background: #0a0a0a; border-color: #1a1a1a; box-shadow: none; }
/* Efeito de Bloco Notion no Hover */
.tiptap > * {
position: relative;
transition: all 0.2s;
border-radius: 0.375rem;
}
.tiptap > *:hover::before {
content: ':::';
position: absolute;
left: -2rem;
top: 50%;
transform: translateY(-50%);
color: #d1d5db;
font-family: monospace;
font-weight: bold;
font-size: 1.25rem;
opacity: 0.5;
}
.dark .tiptap > *:hover::before { color: #334155; }
`}} />
</div>
);
}
function MenuButton({ onClick, active, icon, label }: { onClick: () => void, active: boolean, icon?: React.ReactNode, label?: string }) {
return (
<button
onClick={onClick}
className={`p-2 rounded-lg transition-all flex items-center justify-center min-w-[32px] ${active ? 'bg-brand-500 text-white shadow-lg' : 'hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-500'}`}
>
{icon || <span className="text-xs font-black uppercase tracking-widest">{label}</span>}
</button>
);
}
function FloatingItem({ onClick, icon, title, desc }: { onClick: () => void, icon: React.ReactNode, title: string, desc: string }) {
return (
<button
onClick={onClick}
className="flex items-center gap-4 p-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-xl text-left transition-all group"
>
<div className="w-10 h-10 rounded-xl bg-white dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 flex items-center justify-center shadow-sm group-hover:scale-110 transition-transform">
{icon}
</div>
<div>
<p className="text-xs font-black text-zinc-900 dark:text-white uppercase tracking-widest">{title}</p>
<p className="text-[10px] text-zinc-400 font-medium">{desc}</p>
</div>
</button>
);
}