feat: Replace note content textarea with Tiptap rich text editor supporting markdown and slash commands.
This commit is contained in:
11029
k-notes-frontend/package-lock.json
generated
Normal file
11029
k-notes-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,12 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
|
"@tiptap/extension-placeholder": "^3.14.0",
|
||||||
|
"@tiptap/extension-task-item": "^3.14.0",
|
||||||
|
"@tiptap/extension-task-list": "^3.14.0",
|
||||||
|
"@tiptap/react": "^3.14.0",
|
||||||
|
"@tiptap/starter-kit": "^3.14.0",
|
||||||
|
"@tiptap/suggestion": "^3.14.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -59,11 +65,14 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tippy.js": "^6.3.7",
|
||||||
|
"tiptap-markdown": "^0.9.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.2.1"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
@@ -74,6 +83,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.46.4",
|
"typescript-eslint": "^8.46.4",
|
||||||
|
|||||||
91
k-notes-frontend/src/components/editor/command-list.tsx
Normal file
91
k-notes-frontend/src/components/editor/command-list.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Heading1, Heading2, Heading3, List, ListOrdered, CheckSquare, Type, Quote, Code } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface CommandItemProps {
|
||||||
|
title: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
command: (editor: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandListProps {
|
||||||
|
items: CommandItemProps[];
|
||||||
|
command: (item: CommandItemProps) => void;
|
||||||
|
editor: any; // TipTap editor instance
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandList = forwardRef((props: CommandListProps, ref) => {
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
const selectItem = (index: number) => {
|
||||||
|
const item = props.items[index];
|
||||||
|
if (item) {
|
||||||
|
props.command(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upHandler = () => {
|
||||||
|
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downHandler = () => {
|
||||||
|
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enterHandler = () => {
|
||||||
|
selectItem(selectedIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIndex(0);
|
||||||
|
}, [props.items]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
onKeyDown: ({ event }: { event: KeyboardEvent }) => {
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
upHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
downHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
enterHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2">
|
||||||
|
<div className="flex flex-col gap-1 p-1">
|
||||||
|
{props.items.map((item, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors",
|
||||||
|
index === selectedIndex ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
||||||
|
)}
|
||||||
|
onClick={() => selectItem(index)}
|
||||||
|
>
|
||||||
|
<div className="mr-2 flex h-4 w-4 items-center justify-center">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{props.items.length === 0 && (
|
||||||
|
<div className="p-2 text-sm text-muted-foreground">No results</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CommandList.displayName = 'CommandList';
|
||||||
114
k-notes-frontend/src/components/editor/editor.tsx
Normal file
114
k-notes-frontend/src/components/editor/editor.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
|
||||||
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
|
import { Markdown } from 'tiptap-markdown';
|
||||||
|
import { SlashCommand } from './extensions';
|
||||||
|
import { getSuggestionItems, renderItems } from './suggestions';
|
||||||
|
import TaskList from '@tiptap/extension-task-list';
|
||||||
|
import TaskItem from '@tiptap/extension-task-item';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface EditorProps {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Editor({ value, onChange, placeholder, className }: EditorProps) {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
bulletList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
orderedList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder: placeholder || 'Type / for commands...',
|
||||||
|
}),
|
||||||
|
Markdown,
|
||||||
|
TaskList,
|
||||||
|
TaskItem.configure({
|
||||||
|
nested: true,
|
||||||
|
}),
|
||||||
|
SlashCommand.configure({
|
||||||
|
suggestion: {
|
||||||
|
items: getSuggestionItems,
|
||||||
|
render: renderItems,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
content: value,
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: cn(
|
||||||
|
"min-h-[100px] max-h-[400px] overflow-y-auto w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 prose dark:prose-invert max-w-none",
|
||||||
|
className
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
const markdown = (editor.storage as any).markdown.getMarkdown();
|
||||||
|
onChange?.(markdown);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync content if value changes externally (and editor is not focused? care to avoid loops)
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor && value !== undefined && value !== (editor.storage as any).markdown.getMarkdown()) {
|
||||||
|
// Only set content if it's different to avoid cursor jumping
|
||||||
|
// A simple check might not be enough but good for now for initial loading
|
||||||
|
if (editor.getText() === "" && value !== "") {
|
||||||
|
editor.commands.setContent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value, editor]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full">
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
{/*
|
||||||
|
TipTap styles for placeholder
|
||||||
|
*/}
|
||||||
|
<style>{`
|
||||||
|
.tiptap p.is-editor-empty:first-child::before {
|
||||||
|
color: #adb5bd;
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
float: left;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Basic Task list styles */
|
||||||
|
ul[data-type="taskList"] {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-type="taskList"] li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start; /* Align checkbox with top of text */
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-type="taskList"] li > label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
user-select: none;
|
||||||
|
margin-top: 0.15rem; /* Optical alignment */
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-type="taskList"] li > div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
k-notes-frontend/src/components/editor/extensions.ts
Normal file
27
k-notes-frontend/src/components/editor/extensions.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
import { Extension } from '@tiptap/core';
|
||||||
|
import Suggestion from '@tiptap/suggestion';
|
||||||
|
|
||||||
|
export const SlashCommand = Extension.create({
|
||||||
|
name: 'slashCommand',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
suggestion: {
|
||||||
|
char: '/',
|
||||||
|
command: ({ editor, range, props }: any) => {
|
||||||
|
props.command({ editor, range });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
Suggestion({
|
||||||
|
editor: this.editor,
|
||||||
|
...this.options.suggestion,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
128
k-notes-frontend/src/components/editor/suggestions.tsx
Normal file
128
k-notes-frontend/src/components/editor/suggestions.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
|
||||||
|
import { ReactRenderer } from '@tiptap/react';
|
||||||
|
import tippy from 'tippy.js';
|
||||||
|
import { CommandList } from './command-list';
|
||||||
|
import { Heading1, Heading2, Heading3, List, ListOrdered, CheckSquare, Type, Quote, Code } from 'lucide-react';
|
||||||
|
|
||||||
|
export const getSuggestionItems = ({ query }: { query: string }) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'Text',
|
||||||
|
icon: <Type size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setParagraph().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Heading 1',
|
||||||
|
icon: <Heading1 size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Heading 2',
|
||||||
|
icon: <Heading2 size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Heading 3',
|
||||||
|
icon: <Heading3 size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Ordered List',
|
||||||
|
icon: <ListOrdered size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Bullet List',
|
||||||
|
icon: <List size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Task List',
|
||||||
|
icon: <CheckSquare size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleTaskList().run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Blockquote',
|
||||||
|
icon: <Quote size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Code Block',
|
||||||
|
icon: <Code size={18} />,
|
||||||
|
command: ({ editor, range }: any) => {
|
||||||
|
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].filter((item) => item.title.toLowerCase().startsWith(query.toLowerCase()));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderItems = () => {
|
||||||
|
let component: any;
|
||||||
|
let popup: any;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onStart: (props: any) => {
|
||||||
|
component = new ReactRenderer(CommandList, {
|
||||||
|
props,
|
||||||
|
editor: props.editor,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!props.clientRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
popup = tippy('body', {
|
||||||
|
getReferenceClientRect: props.clientRect,
|
||||||
|
appendTo: () => document.body,
|
||||||
|
content: component.element,
|
||||||
|
showOnCreate: true,
|
||||||
|
interactive: true,
|
||||||
|
trigger: 'manual',
|
||||||
|
placement: 'bottom-start',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdate(props: any) {
|
||||||
|
component.updateProps(props);
|
||||||
|
|
||||||
|
if (!props.clientRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
popup[0].setProps({
|
||||||
|
getReferenceClientRect: props.clientRect,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown(props: any) {
|
||||||
|
if (props.event.key === 'Escape') {
|
||||||
|
popup[0].hide();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return component.ref?.onKeyDown(props);
|
||||||
|
},
|
||||||
|
|
||||||
|
onExit() {
|
||||||
|
popup[0].destroy();
|
||||||
|
component.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ import { NoteForm } from "./note-form";
|
|||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { getNoteColor } from "@/lib/constants";
|
import { getNoteColor } from "@/lib/constants";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
import { VersionHistoryDialog } from "./version-history-dialog";
|
import { VersionHistoryDialog } from "./version-history-dialog";
|
||||||
import { NoteViewDialog } from "./note-view-dialog";
|
import { NoteViewDialog } from "./note-view-dialog";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
@@ -116,7 +117,7 @@ export function NoteCard({ note }: NoteCardProps) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pb-2">
|
<CardContent className="pb-2">
|
||||||
<div className="text-sm prose dark:prose-invert prose-sm max-w-none line-clamp-4">
|
<div className="text-sm prose dark:prose-invert prose-sm max-w-none line-clamp-4">
|
||||||
<ReactMarkdown>{note.content}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{note.content}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col items-start gap-2 pt-2">
|
<CardFooter className="flex flex-col items-start gap-2 pt-2">
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Editor } from "@/components/editor/editor";
|
||||||
|
|
||||||
const noteSchema = z.object({
|
const noteSchema = z.object({
|
||||||
title: z.string().min(1, "Title is required").max(200, "Title too long"),
|
title: z.string().min(1, "Title is required").max(200, "Title too long"),
|
||||||
@@ -61,7 +62,11 @@ export function NoteForm({ defaultValues, onSubmit, isLoading, submitLabel = "Sa
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea placeholder="Note content..." className="min-h-[100px] font-mono" {...field} />
|
<Editor
|
||||||
|
placeholder="Note content... Type / for commands"
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -93,10 +98,8 @@ export function NoteForm({ defaultValues, onSubmit, isLoading, submitLabel = "Sa
|
|||||||
<div
|
<div
|
||||||
key={color.name}
|
key={color.name}
|
||||||
onClick={() => field.onChange(color.name)}
|
onClick={() => field.onChange(color.name)}
|
||||||
className={`w-8 h-8 rounded-full cursor-pointer border-2 transition-all ${
|
className={`w-8 h-8 rounded-full cursor-pointer border-2 transition-all ${color.value.split(" ")[0] // Take background class
|
||||||
color.value.split(" ")[0] // Take background class
|
} ${field.value === color.name
|
||||||
} ${
|
|
||||||
field.value === color.name
|
|
||||||
? "border-primary scale-110"
|
? "border-primary scale-110"
|
||||||
: "border-transparent hover:scale-105"
|
: "border-transparent hover:scale-105"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { type Note } from "@/hooks/use-notes";
|
|||||||
import { Edit, Calendar, Pin } from "lucide-react";
|
import { Edit, Calendar, Pin } from "lucide-react";
|
||||||
import { getNoteColor } from "@/lib/constants";
|
import { getNoteColor } from "@/lib/constants";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
|
|
||||||
interface NoteViewDialogProps {
|
interface NoteViewDialogProps {
|
||||||
@@ -39,7 +40,7 @@ export function NoteViewDialog({ note, open, onOpenChange, onEdit }: NoteViewDia
|
|||||||
|
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto -mx-6 px-6">
|
<div className="flex-1 min-h-0 overflow-y-auto -mx-6 px-6">
|
||||||
<div className="prose dark:prose-invert max-w-none text-base leading-relaxed break-words pb-6">
|
<div className="prose dark:prose-invert max-w-none text-base leading-relaxed break-words pb-6">
|
||||||
<ReactMarkdown>{note.content}</ReactMarkdown>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{note.content}</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user