2025-06-28 19:30:35 -03:00
|
|
|
"use client";
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
|
|
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/react';
|
|
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
|
|
import TextStyle from '@tiptap/extension-text-style';
|
|
|
|
import Color from '@tiptap/extension-color';
|
|
|
|
import Highlight from '@tiptap/extension-highlight';
|
|
|
|
import TextAlign from '@tiptap/extension-text-align';
|
|
|
|
import Footer from '../footer'
|
|
|
|
|
|
|
|
import {
|
|
|
|
Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight,
|
|
|
|
Link, Highlighter, Type, Palette, Sparkles
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
|
|
const SimplePaperNote = () => {
|
|
|
|
const [title, setTitle] = useState('Untitled');
|
|
|
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
|
|
|
const titleInputRef = useRef(null);
|
|
|
|
|
|
|
|
const editor = useEditor({
|
|
|
|
extensions: [
|
|
|
|
StarterKit,
|
|
|
|
TextStyle,
|
|
|
|
Color,
|
|
|
|
Highlight.configure({ multicolor: true }),
|
|
|
|
TextAlign.configure({
|
|
|
|
types: ['heading', 'paragraph'],
|
|
|
|
}),
|
|
|
|
|
|
|
|
],
|
|
|
|
content: `
|
|
|
|
<p>Start writing your thoughts here...</p>
|
|
|
|
`,
|
|
|
|
editorProps: {
|
|
|
|
attributes: {
|
|
|
|
class: 'prose prose-invert max-w-none focus:outline-none min-h-[calc(100vh-8rem)] p-8 text-foreground',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const addLink = () => {
|
|
|
|
const previousUrl = editor?.getAttributes('link').href;
|
|
|
|
const url = window.prompt('Enter URL:', previousUrl);
|
|
|
|
if (url === null) return;
|
|
|
|
if (url === '') {
|
|
|
|
editor?.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (isEditingTitle && titleInputRef.current) {
|
|
|
|
titleInputRef.current.focus();
|
|
|
|
}
|
|
|
|
}, [isEditingTitle]);
|
|
|
|
|
|
|
|
if (!editor) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// OneNote-like keyboard shortcuts (note editing & navigation)
|
|
|
|
// Two rows, unique qkeys, avoid browser/reserved keys
|
|
|
|
// File/Note operations row
|
|
|
|
const shortcuts = [
|
|
|
|
[
|
|
|
|
{ key: 'Q', label: 'Resume', action: () => {/* Implement resume logic here */ } },
|
|
|
|
{ key: 'W', label: 'Write', action: () => {/* Implement write logic here */ } },
|
|
|
|
{ key: 'E', label: 'Expand', action: () => {/* Implement expand logic here */ } },
|
|
|
|
{ key: 'R', label: 'One Word', action: () => {/* Implement one word logic here */ } },
|
|
|
|
{ key: 'T', label: 'As List', action: () => editor?.chain().focus().toggleBulletList().run() },
|
|
|
|
{ key: 'Y', label: 'As Mail', action: () => {/* Implement as mail logic here */ } },
|
|
|
|
{ key: 'U', label: 'Copy', action: () => document.execCommand('copy') },
|
|
|
|
{ key: 'I', label: 'Paste', action: () => document.execCommand('paste') },
|
|
|
|
{ key: 'O', label: 'Undo', action: () => editor?.chain().focus().undo().run() },
|
|
|
|
{ key: 'P', label: 'Redo', action: () => editor?.chain().focus().redo().run() },
|
|
|
|
],
|
|
|
|
[
|
|
|
|
{ key: 'A', label: 'Select', action: () => {/* Implement select logic here */ } },
|
|
|
|
{ key: 'S', label: 'Select All', action: () => editor?.chain().focus().selectAll().run() },
|
|
|
|
{ key: 'D', label: 'Deselect', action: () => {/* Implement deselect logic here */ } },
|
|
|
|
{ key: 'G', label: 'Random', action: () => {/* Implement insert image logic here */ } },
|
|
|
|
{ key: 'H', label: 'Idea', action: () => {/* Implement insert table logic here */ } },
|
|
|
|
{ key: 'J', label: 'Insert Link', action: addLink },
|
|
|
|
{ key: 'K', label: 'Highlight', action: () => editor?.chain().focus().toggleHighlight({ color: '#ffff00' }).run() },
|
|
|
|
{ key: 'L', label: 'To-Do', action: () => editor?.chain().focus().toggleTaskList?.().run?.() },
|
|
|
|
{ key: 'Z', label: 'Zoom In', action: () => {/* Implement zoom in logic here */ } },
|
|
|
|
{ key: 'X', label: 'Zoom Out', action: () => {/* Implement zoom out logic here */ } },
|
|
|
|
]
|
|
|
|
];
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="min-h-screen bg-background text-foreground">
|
|
|
|
<div>
|
|
|
|
<div className="max-w-4xl mx-auto">
|
|
|
|
{/* Paper Shadow Effect */}
|
|
|
|
<div className="mx-4 my-8 bg-card rounded-lg shadow-2xl shadow-black/20 border border-border">
|
|
|
|
<EditorContent
|
|
|
|
editor={editor}
|
|
|
|
className="min-h-[calc(100vh-12rem)]"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* Floating Selection Toolbar */}
|
|
|
|
{editor && (
|
|
|
|
<BubbleMenu
|
|
|
|
editor={editor}
|
|
|
|
tippyOptions={{
|
|
|
|
duration: 100,
|
|
|
|
placement: 'top',
|
|
|
|
animation: 'shift-away'
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<div className="flex items-center bg-card border border-border rounded-lg shadow-lg p-1">
|
|
|
|
{/* Text Formatting */}
|
|
|
|
<button
|
|
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive('bold') ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Bold"
|
|
|
|
>
|
|
|
|
<Bold className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button
|
|
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive('italic') ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Italic"
|
|
|
|
>
|
|
|
|
<Italic className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button
|
|
|
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive('underline') ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Underline"
|
|
|
|
>
|
|
|
|
<Underline className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<div className="w-px h-6 bg-border mx-1"></div>
|
|
|
|
|
|
|
|
{/* Text Alignment */}
|
|
|
|
<button
|
|
|
|
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive({ textAlign: 'left' }) ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Align Left"
|
|
|
|
>
|
|
|
|
<AlignLeft className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button
|
|
|
|
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive({ textAlign: 'center' }) ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Align Center"
|
|
|
|
>
|
|
|
|
<AlignCenter className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button
|
|
|
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive({ textAlign: 'right' }) ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Align Right"
|
|
|
|
>
|
|
|
|
<AlignRight className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<div className="w-px h-6 bg-border mx-1"></div>
|
|
|
|
|
|
|
|
{/* Highlight */}
|
|
|
|
<button
|
|
|
|
onClick={() => editor.chain().focus().toggleHighlight({ color: '#ffff00' }).run()}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive('highlight') ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Highlight"
|
|
|
|
>
|
|
|
|
<Highlighter className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
{/* Link */}
|
|
|
|
<button
|
|
|
|
onClick={addLink}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive('link') ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Add Link"
|
|
|
|
>
|
|
|
|
<Link className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<div className="w-px h-6 bg-border mx-1"></div>
|
|
|
|
|
|
|
|
{/* Heading */}
|
|
|
|
<button
|
|
|
|
onClick={() => {
|
|
|
|
if (editor.isActive('heading')) {
|
|
|
|
editor.chain().focus().setParagraph().run();
|
|
|
|
} else {
|
|
|
|
editor.chain().focus().toggleHeading({ level: 2 }).run();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
className={`p-2 rounded hover:bg-accent transition-colors ${editor.isActive('heading') ? 'bg-primary text-primary-foreground' : 'text-foreground'
|
|
|
|
}`}
|
|
|
|
title="Heading"
|
|
|
|
>
|
|
|
|
<Type className="h-4 w-4" />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</BubbleMenu>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* Custom Styles */}
|
|
|
|
<style jsx global>{`
|
|
|
|
.ProseMirror {
|
|
|
|
outline: none;
|
|
|
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
|
|
font-size: 16px;
|
|
|
|
line-height: 1.7;
|
|
|
|
color: hsl(var(--foreground));
|
|
|
|
padding: 3rem;
|
|
|
|
|
|
|
|
min-height: calc(100vh - 12rem);
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror h1 {
|
|
|
|
font-size: 2.5rem;
|
|
|
|
font-weight: 700;
|
|
|
|
margin: 2rem 0 1rem 0;
|
|
|
|
color: hsl(var(--primary));
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror h2 {
|
|
|
|
font-size: 2rem;
|
|
|
|
font-weight: 600;
|
|
|
|
margin: 1.5rem 0 0.75rem 0;
|
|
|
|
color: hsl(var(--primary));
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror h3 {
|
|
|
|
font-size: 1.5rem;
|
|
|
|
font-weight: 600;
|
|
|
|
margin: 1.25rem 0 0.5rem 0;
|
|
|
|
color: hsl(var(--primary));
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror p {
|
|
|
|
margin: 0.75rem 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror a {
|
|
|
|
color: hsl(var(--accent));
|
|
|
|
text-decoration: underline;
|
|
|
|
text-underline-offset: 2px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror a:hover {
|
|
|
|
color: hsl(var(--primary));
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror mark {
|
|
|
|
background-color: #ffff0040;
|
|
|
|
border-radius: 2px;
|
|
|
|
padding: 0 2px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror ul, .ProseMirror ol {
|
|
|
|
margin: 1rem 0;
|
|
|
|
padding-left: 1.5rem;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror li {
|
|
|
|
margin: 0.25rem 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror blockquote {
|
|
|
|
border-left: 4px solid hsl(var(--primary));
|
|
|
|
padding-left: 1rem;
|
|
|
|
margin: 1rem 0;
|
|
|
|
font-style: italic;
|
|
|
|
color: hsl(var(--muted-foreground));
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror code {
|
|
|
|
background-color: hsl(var(--muted));
|
|
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
border-radius: 4px;
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
font-size: 0.9em;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror pre {
|
|
|
|
background-color: hsl(var(--muted));
|
|
|
|
padding: 1rem;
|
|
|
|
border-radius: 8px;
|
|
|
|
overflow-x: auto;
|
|
|
|
margin: 1rem 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.ProseMirror pre code {
|
|
|
|
background: none;
|
|
|
|
padding: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Selection highlighting */
|
|
|
|
.ProseMirror ::selection {
|
|
|
|
background-color: hsl(var(--primary) / 0.2);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Placeholder styling */
|
|
|
|
.ProseMirror p.is-editor-empty:first-child::before {
|
|
|
|
content: attr(data-placeholder);
|
|
|
|
float: left;
|
|
|
|
color: hsl(var(--muted-foreground));
|
|
|
|
pointer-events: none;
|
|
|
|
height: 0;
|
|
|
|
}
|
|
|
|
`}</style>
|
|
|
|
|
|
|
|
<Footer shortcuts={shortcuts} />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default SimplePaperNote;
|