Merge redesign/ui-v2 into main: full v2 rewrite (Vite SPA + Hono API + Bun worker)
Resolved conflicts in web-legacy pages and report schema by taking v2 side. v2 is the deployed, current architecture; v1 paths preserved under apps/web-legacy.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
'use client';
|
||||
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKitRaw from "@tiptap/starter-kit";
|
||||
const StarterKit = StarterKitRaw as any;
|
||||
import Link from '@tiptap/extension-link';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
Code,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Undo,
|
||||
Redo,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface NoteEditorProps {
|
||||
content: string;
|
||||
onChange: (content: string) => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export function NoteEditor({ content, onChange, onBlur }: NoteEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: 'Start writing... Use [[Note Title]] to link to other notes',
|
||||
}),
|
||||
],
|
||||
content,
|
||||
onUpdate: ({ editor: e }) => {
|
||||
onChange(e.getHTML());
|
||||
},
|
||||
onBlur: () => {
|
||||
onBlur?.();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && content !== editor.getHTML()) {
|
||||
editor.commands.setContent(content);
|
||||
}
|
||||
}, [content, editor]);
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Toolbar */}
|
||||
<div className="mb-4 flex flex-wrap gap-1 border-b pb-2">
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).toggleBold().run()}
|
||||
active={editor.isActive('bold')}
|
||||
label="Bold"
|
||||
>
|
||||
<Bold className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).toggleItalic().run()}
|
||||
active={editor.isActive('italic')}
|
||||
label="Italic"
|
||||
>
|
||||
<Italic className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).toggleStrike().run()}
|
||||
active={editor.isActive('strike')}
|
||||
label="Strikethrough"
|
||||
>
|
||||
<Strikethrough className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).toggleCode().run()}
|
||||
active={editor.isActive('code')}
|
||||
label="Code"
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<div className="mx-1 w-px bg-border" />
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).toggleBulletList().run()}
|
||||
active={editor.isActive('bulletList')}
|
||||
label="Bullet list"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).toggleOrderedList().run()}
|
||||
active={editor.isActive('orderedList')}
|
||||
label="Ordered list"
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).toggleBlockquote().run()}
|
||||
active={editor.isActive('blockquote')}
|
||||
label="Blockquote"
|
||||
>
|
||||
<Quote className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<div className="mx-1 w-px bg-border" />
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).undo().run()}
|
||||
disabled={!editor.can().undo()}
|
||||
label="Undo"
|
||||
>
|
||||
<Undo className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
onClick={() => (editor.chain().focus() as any).redo().run()}
|
||||
disabled={!editor.can().redo()}
|
||||
label="Redo"
|
||||
>
|
||||
<Redo className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
|
||||
{/* Editor content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<EditorContent editor={editor} className="tiptap-editor min-h-[400px]" tabIndex={0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarButton({
|
||||
onClick,
|
||||
active,
|
||||
disabled,
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn('h-8 w-8', active && 'bg-accent text-accent-foreground')}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={label}
|
||||
aria-pressed={active}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user