feat: editor update

This commit is contained in:
siyue
2024-10-26 15:24:22 +08:00
parent 7b146338f4
commit bb6f65e3d7
5 changed files with 936 additions and 24 deletions
+1
View File
@@ -19,6 +19,7 @@
"@tiptap/extension-list-item": "^2.4.0",
"@tiptap/extension-text-align": "^2.4.0",
"@tiptap/extension-text-style": "^2.4.0",
"@tiptap/extension-underline": "^2.9.1",
"@tiptap/pm": "^2.4.0",
"@tiptap/react": "^2.4.0",
"@tiptap/starter-kit": "^2.4.0",
@@ -0,0 +1,249 @@
import React from "react";
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import TextAlign from "@tiptap/extension-text-align";
import TextStyle from "@tiptap/extension-text-style";
import Underline from "@tiptap/extension-underline";
import Color from "@tiptap/extension-color";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Bold,
Italic,
Underline as UnderlineIcon,
Strikethrough,
AlignLeft,
AlignCenter,
AlignRight,
AlignJustify,
List,
ListOrdered,
Quote,
Undo,
Redo
} from "lucide-react";
interface RichTextEditorProps {
content: string;
onChange: (content: string) => void;
placeholder?: string;
}
const MenuButton = ({ onClick, isActive = false, children }) => (
<Button
variant="ghost"
size="sm"
className={`h-8 w-8 p-0 hover:bg-muted ${isActive ? "bg-muted" : ""}`}
onClick={onClick}
>
{children}
</Button>
);
const RichTextEditor = ({
content,
onChange,
placeholder
}: RichTextEditorProps) => {
const editor = useEditor({
extensions: [
StarterKit.configure({
heading: {
levels: [1, 2, 3]
}
}),
TextAlign.configure({
types: ["heading", "paragraph"],
alignments: ["left", "center", "right", "justify"]
}),
TextStyle,
Underline,
Color
],
content,
onUpdate: ({ editor }) => {
onChange(editor.getHTML());
},
editorProps: {
attributes: {
class:
"prose prose-sm sm:prose max-w-none focus:outline-none min-h-[150px] p-4",
placeholder: placeholder
}
}
});
if (!editor) {
return null;
}
return (
<div className="border rounded-lg bg-card">
{/* 工具栏 */}
<div className="border-b p-2 flex flex-wrap gap-1 bg-muted/50">
<div className="flex items-center gap-1">
<MenuButton
onClick={() => editor.chain().focus().toggleBold().run()}
isActive={editor.isActive("bold")}
>
<Bold className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleItalic().run()}
isActive={editor.isActive("italic")}
>
<Italic className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleUnderline().run()}
isActive={editor.isActive("underline")}
>
<UnderlineIcon className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleStrike().run()}
isActive={editor.isActive("strike")}
>
<Strikethrough className="h-4 w-4" />
</MenuButton>
</div>
<Separator orientation="vertical" className="h-8" />
<div className="flex items-center gap-1">
<select
className="h-8 rounded-md border bg-background px-2 text-sm"
onChange={(e) => {
const value = e.target.value;
if (value === "p") {
editor.chain().focus().setParagraph().run();
} else {
editor
.chain()
.focus()
.toggleHeading({ level: parseInt(value) })
.run();
}
}}
value={
editor.isActive("heading", { level: 1 })
? "1"
: editor.isActive("heading", { level: 2 })
? "2"
: editor.isActive("heading", { level: 3 })
? "3"
: "p"
}
>
<option value="p"></option>
<option value="1"> 1</option>
<option value="2"> 2</option>
<option value="3"> 3</option>
</select>
</div>
<Separator orientation="vertical" className="h-8" />
<div className="flex items-center gap-1">
<MenuButton
onClick={() => editor.chain().focus().setTextAlign("left").run()}
isActive={editor.isActive({ textAlign: "left" })}
>
<AlignLeft className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().setTextAlign("center").run()}
isActive={editor.isActive({ textAlign: "center" })}
>
<AlignCenter className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().setTextAlign("right").run()}
isActive={editor.isActive({ textAlign: "right" })}
>
<AlignRight className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().setTextAlign("justify").run()}
isActive={editor.isActive({ textAlign: "justify" })}
>
<AlignJustify className="h-4 w-4" />
</MenuButton>
</div>
<Separator orientation="vertical" className="h-8" />
<div className="flex items-center gap-1">
<MenuButton
onClick={() => editor.chain().focus().toggleBulletList().run()}
isActive={editor.isActive("bulletList")}
>
<List className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleOrderedList().run()}
isActive={editor.isActive("orderedList")}
>
<ListOrdered className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleBlockquote().run()}
isActive={editor.isActive("blockquote")}
>
<Quote className="h-4 w-4" />
</MenuButton>
</div>
<Separator orientation="vertical" className="h-8" />
<div className="flex items-center gap-1">
<MenuButton
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
>
<Undo className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
>
<Redo className="h-4 w-4" />
</MenuButton>
</div>
</div>
{/* 编辑区域 */}
<EditorContent editor={editor} />
{/* 气泡菜单 */}
{editor && (
<BubbleMenu
className="flex items-center gap-1 p-1 rounded-lg border bg-background shadow-lg"
tippyOptions={{ duration: 100 }}
editor={editor}
>
<MenuButton
onClick={() => editor.chain().focus().toggleBold().run()}
isActive={editor.isActive("bold")}
>
<Bold className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleItalic().run()}
isActive={editor.isActive("italic")}
>
<Italic className="h-4 w-4" />
</MenuButton>
<MenuButton
onClick={() => editor.chain().focus().toggleUnderline().run()}
isActive={editor.isActive("underline")}
>
<UnderlineIcon className="h-4 w-4" />
</MenuButton>
</BubbleMenu>
)}
</div>
);
};
export default RichTextEditor;
+651 -24
View File
@@ -1,32 +1,659 @@
"use client";
import Editor from "./compoents/Editor";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup
} from "@/components/ui/resizable";
import Preview from "./compoents/Preview";
import styles from "./index.module.scss";
const WorkBench = () => {
Type,
Layout,
Phone,
Mail,
Globe,
MapPin,
GraduationCap,
Briefcase,
Award,
Plus,
Trash,
Download,
User,
Cpu,
Calendar,
Trash2
} from "lucide-react";
import RichTextEditor from "./compoents/Editor/RichText";
const TabIcon = ({ icon: Icon, label }) => (
<div className="flex flex-col items-center gap-1">
<Icon className="h-5 w-5" />
<span className="text-xs">{label}</span>
</div>
);
const FormField = ({ label, children }) => (
<div className="space-y-2">
<Label className="text-sm font-medium">{label}</Label>
{children}
</div>
);
const Section = ({ title, children, className = "" }) => (
<div className={`space-y-4 ${className}`}>
<h3 className="font-semibold text-lg flex items-center gap-2">{title}</h3>
{children}
</div>
);
// 简历数据结构
interface ResumeData {
basics: {
name: string;
label: string;
email: string;
phone: string;
website: string;
location: string;
summary: string;
};
education: Array<{
institution: string;
area: string;
studyType: string;
startDate: string;
endDate: string;
description: string;
}>;
work: Array<{
company: string;
position: string;
startDate: string;
endDate: string;
description: string;
}>;
skills: Array<{
name: string;
level: string;
keywords: string[];
}>;
projects: Array<{
name: string;
description: string;
highlights: string[];
url: string;
}>;
}
// 简历预览组件
const ResumePreview = ({ data }: { data: ResumeData }) => {
const handleDownloadPDF = () => {
window.print();
};
return (
<div className={styles.container}>
<ResizablePanelGroup
direction="horizontal"
className="h-[100vh] rounded-lg border "
<div className="relative h-full bg-gray-100 overflow-y-auto">
{/* 固定的下载按钮 */}
<div className="sticky top-4 z-10 container max-w-[21cm] mx-auto px-4">
<div className="flex justify-end">
<Button
onClick={handleDownloadPDF}
className="bg-blue-600 hover:bg-blue-700 transition-colors shadow-lg"
>
<Download className="h-4 w-4 mr-2" />
PDF
</Button>
</div>
</div>
{/* 简历预览内容 */}
<div
id="resume-preview"
className="max-w-[21cm] mx-auto my-8 bg-white shadow-2xl rounded-lg overflow-hidden"
>
<ResizablePanel defaultSize={50} className="max-w-[60%] min-w-[20%]">
<Editor></Editor>
</ResizablePanel>
<ResizableHandle
withHandle
className="hover:w-[2px] hover:bg-[#80ed99]"
/>
<ResizablePanel>
<Preview></Preview>
</ResizablePanel>
</ResizablePanelGroup>
{/* 个人信息头部 */}
<header className="px-8 py-12 bg-gradient-to-br from-blue-50 to-indigo-50">
<h1 className="text-4xl font-bold text-gray-900 text-center mb-4">
{data.basics.name}
</h1>
{data.basics.label && (
<div className="text-xl text-gray-600 text-center mb-6">
{data.basics.label}
</div>
)}
<div className="flex justify-center flex-wrap gap-6 text-gray-600">
{data.basics.email && (
<div className="flex items-center gap-2 hover:text-blue-600 transition-colors">
<Mail className="h-4 w-4" />
<span>{data.basics.email}</span>
</div>
)}
{data.basics.phone && (
<div className="flex items-center gap-2 hover:text-blue-600 transition-colors">
<Phone className="h-4 w-4" />
<span>{data.basics.phone}</span>
</div>
)}
{data.basics.location && (
<div className="flex items-center gap-2 hover:text-blue-600 transition-colors">
<MapPin className="h-4 w-4" />
<span>{data.basics.location}</span>
</div>
)}
{data.basics.website && (
<div className="flex items-center gap-2 hover:text-blue-600 transition-colors">
<Globe className="h-4 w-4" />
<a
href={data.basics.website}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{data.basics.website}
</a>
</div>
)}
</div>
</header>
{/* 主要内容区域 */}
<div className="p-8 space-y-8">
{/* 个人简介 */}
{data.basics.summary && (
<section>
<h2 className="text-2xl font-bold text-gray-900 mb-4 pb-2 border-b border-gray-200 flex items-center gap-2">
<User className="h-5 w-5 text-blue-600" />
</h2>
<div className="text-gray-600 leading-relaxed">
{data.basics.summary}
</div>
</section>
)}
{/* 工作经历 */}
{data.work.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-gray-900 mb-4 pb-2 border-b border-gray-200 flex items-center gap-2">
<Briefcase className="h-5 w-5 text-blue-600" />
</h2>
<div className="space-y-6">
{data.work.map((work, index) => (
<div
key={index}
className="relative pl-6 before:absolute before:left-0 before:top-2 before:w-2 before:h-2 before:bg-blue-600 before:rounded-full"
>
<div className="flex justify-between items-center mb-2">
<h3 className="text-xl font-semibold text-gray-900">
{work.company}
</h3>
<span className="text-sm text-gray-500">
{work.startDate} - {work.endDate}
</span>
</div>
<div className="text-lg text-blue-600 mb-2">
{work.position}
</div>
<div
className="text-gray-600 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: work.description }}
/>
</div>
))}
</div>
</section>
)}
{/* 教育经历 */}
{data.education.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-gray-900 mb-4 pb-2 border-b border-gray-200 flex items-center gap-2">
<GraduationCap className="h-5 w-5 text-blue-600" />
</h2>
<div className="space-y-6">
{data.education.map((edu, index) => (
<div
key={index}
className="relative pl-6 before:absolute before:left-0 before:top-2 before:w-2 before:h-2 before:bg-blue-600 before:rounded-full"
>
<div className="flex justify-between items-center mb-2">
<h3 className="text-xl font-semibold text-gray-900">
{edu.institution}
</h3>
<span className="text-sm text-gray-500">
{edu.startDate} - {edu.endDate}
</span>
</div>
<div className="text-lg text-blue-600 mb-2">
{edu.studyType} · {edu.area}
</div>
<div
className="text-gray-600 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: edu.description }}
/>
</div>
))}
</div>
</section>
)}
{/* 技能专长 */}
{data.skills.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-gray-900 mb-4 pb-2 border-b border-gray-200 flex items-center gap-2">
<Cpu className="h-5 w-5 text-blue-600" />
</h2>
<div className="grid grid-cols-2 gap-6">
{data.skills.map((skill, index) => (
<div
key={index}
className="relative pl-6 before:absolute before:left-0 before:top-2 before:w-2 before:h-2 before:bg-blue-600 before:rounded-full"
>
<div className="font-semibold text-gray-900 mb-1">
{skill.name}
</div>
<div className="text-sm text-gray-600">
{skill.keywords.join(" · ")}
</div>
</div>
))}
</div>
</section>
)}
{/* 项目经历 */}
{data.projects.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-gray-900 mb-4 pb-2 border-b border-gray-200 flex items-center gap-2">
<Layout className="h-5 w-5 text-blue-600" />
</h2>
<div className="space-y-6">
{data.projects.map((project, index) => (
<div
key={index}
className="relative pl-6 before:absolute before:left-0 before:top-2 before:w-2 before:h-2 before:bg-blue-600 before:rounded-full"
>
<div className="flex items-center gap-2 mb-2">
<h3 className="text-xl font-semibold text-gray-900">
{project.name}
</h3>
{project.url && (
<a
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline text-sm"
>
</a>
)}
</div>
<div
className="text-gray-600 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: project.description }}
/>
{project.highlights.length > 0 && (
<ul className="mt-2 space-y-1 text-gray-600">
{project.highlights.map((highlight, i) => (
<li
key={i}
className="relative pl-4 before:absolute before:left-0 before:top-[0.6em] before:w-1 before:h-1 before:bg-gray-400 before:rounded-full"
>
{highlight}
</li>
))}
</ul>
)}
</div>
))}
</div>
</section>
)}
</div>
</div>
</div>
);
};
export default WorkBench;
// 主编辑器组件
const ResumeEditor = () => {
const [resumeData, setResumeData] = useState<ResumeData>({
basics: {
name: "",
label: "",
email: "",
phone: "",
website: "",
location: "",
summary: ""
},
education: [],
work: [],
skills: [],
projects: []
});
// 更新基本信息
const updateBasics = (field: keyof ResumeData["basics"], value: string) => {
setResumeData((prev) => ({
...prev,
basics: {
...prev.basics,
[field]: value
}
}));
};
// 添加教育经历
const addEducation = () => {
setResumeData((prev) => ({
...prev,
education: [
...prev.education,
{
institution: "",
area: "",
studyType: "",
startDate: "",
endDate: "",
description: ""
}
]
}));
};
return (
<div className="h-screen flex">
<div className="w-[800px] border-r h-full flex flex-col bg-gray-50/50">
<Tabs defaultValue="basics" className="flex-1">
<div className="border-b bg-white p-4">
<TabsList className="grid grid-cols-5 h-auto gap-2 bg-muted/50 p-1">
<TabsTrigger
value="basics"
className="data-[state=active]:bg-white flex flex-col gap-1 py-2"
>
<TabIcon icon={User} label="基本信息" />
</TabsTrigger>
<TabsTrigger
value="education"
className="data-[state=active]:bg-white flex flex-col gap-1 py-2"
>
<TabIcon icon={GraduationCap} label="教育经历" />
</TabsTrigger>
<TabsTrigger
value="work"
className="data-[state=active]:bg-white flex flex-col gap-1 py-2"
>
<TabIcon icon={Briefcase} label="工作经历" />
</TabsTrigger>
<TabsTrigger
value="skills"
className="data-[state=active]:bg-white flex flex-col gap-1 py-2"
>
<TabIcon icon={Layout} label="技能特长" />
</TabsTrigger>
<TabsTrigger
value="projects"
className="data-[state=active]:bg-white flex flex-col gap-1 py-2"
>
<TabIcon icon={Award} label="项目经历" />
</TabsTrigger>
</TabsList>
</div>
<div className="flex-1 overflow-y-auto p-6">
<TabsContent value="basics" className="mt-0 space-y-6">
<Card className="border-none shadow-none bg-transparent">
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<FormField label="姓名">
<Input
value={resumeData.basics.name}
onChange={(e) => updateBasics("name", e.target.value)}
placeholder="你的名字"
className="bg-white"
/>
</FormField>
<FormField label="职位">
<Input
value={resumeData.basics.label}
onChange={(e) => updateBasics("label", e.target.value)}
placeholder="期望职位"
className="bg-white"
/>
</FormField>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField label="邮箱">
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
<Input
value={resumeData.basics.email}
onChange={(e) =>
updateBasics("email", e.target.value)
}
placeholder="邮箱地址"
className="pl-10 bg-white"
/>
</div>
</FormField>
<FormField label="电话">
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
<Input
value={resumeData.basics.phone}
onChange={(e) =>
updateBasics("phone", e.target.value)
}
placeholder="联系电话"
className="pl-10 bg-white"
/>
</div>
</FormField>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField label="个人网站">
<div className="relative">
<Globe className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
<Input
value={resumeData.basics.website}
onChange={(e) =>
updateBasics("website", e.target.value)
}
placeholder="个人网站或博客"
className="pl-10 bg-white"
/>
</div>
</FormField>
<FormField label="地址">
<div className="relative">
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
<Input
value={resumeData.basics.location}
onChange={(e) =>
updateBasics("location", e.target.value)
}
placeholder="所在城市"
className="pl-10 bg-white"
/>
</div>
</FormField>
</div>
<FormField label="个人简介">
<RichTextEditor
content={resumeData.basics.summary}
onChange={(value) => updateBasics("summary", value)}
placeholder="简单介绍自己"
/>
</FormField>
</div>
</Card>
</TabsContent>
<TabsContent value="education" className="mt-0 space-y-6">
<Card className="border-none shadow-none bg-transparent">
<div className="flex justify-between items-center mb-6">
<h3 className="font-semibold text-lg"></h3>
<Button
onClick={addEducation}
className="bg-blue-500 hover:bg-blue-600"
>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
<div className="space-y-6">
{resumeData.education.map((edu, index) => (
<Card key={index} className="p-6 bg-white">
<div className="space-y-4">
<FormField label="学校名称">
<Input
value={edu.institution}
onChange={(e) => {
const newEducation = [...resumeData.education];
newEducation[index].institution = e.target.value;
setResumeData({
...resumeData,
education: newEducation
});
}}
placeholder="学校名称"
/>
</FormField>
<div className="grid grid-cols-2 gap-4">
<FormField label="开始时间">
<div className="relative">
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
<Input
type="date"
value={edu.startDate}
onChange={(e) => {
const newEducation = [
...resumeData.education
];
newEducation[index].startDate =
e.target.value;
setResumeData({
...resumeData,
education: newEducation
});
}}
className="pl-10"
/>
</div>
</FormField>
<FormField label="结束时间">
<div className="relative">
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
<Input
type="date"
value={edu.endDate}
onChange={(e) => {
const newEducation = [
...resumeData.education
];
newEducation[index].endDate = e.target.value;
setResumeData({
...resumeData,
education: newEducation
});
}}
className="pl-10"
/>
</div>
</FormField>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField label="学历">
<Input
value={edu.studyType}
onChange={(e) => {
const newEducation = [...resumeData.education];
newEducation[index].studyType = e.target.value;
setResumeData({
...resumeData,
education: newEducation
});
}}
placeholder="学历"
/>
</FormField>
<FormField label="专业">
<Input
value={edu.area}
onChange={(e) => {
const newEducation = [...resumeData.education];
newEducation[index].area = e.target.value;
setResumeData({
...resumeData,
education: newEducation
});
}}
placeholder="专业"
/>
</FormField>
</div>
<FormField label="在校经历">
<RichTextEditor
content={edu.description}
onChange={(value) => {
const newEducation = [...resumeData.education];
newEducation[index].description = value;
setResumeData({
...resumeData,
education: newEducation
});
}}
placeholder="在校经历描述"
/>
</FormField>
<div className="flex justify-end">
<Button
variant="destructive"
size="sm"
onClick={() => {
const newEducation = resumeData.education.filter(
(_, i) => i !== index
);
setResumeData({
...resumeData,
education: newEducation
});
}}
>
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</Card>
))}
</div>
</Card>
</TabsContent>
{/* 其他 TabsContent 内容类似 */}
</div>
</Tabs>
</div>
<div className="flex-1 h-full">
<ResumePreview data={resumeData} />
</div>
</div>
);
};
export default ResumeEditor;
@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }
+11
View File
@@ -122,6 +122,9 @@ importers:
'@tiptap/extension-text-style':
specifier: ^2.4.0
version: 2.9.1(@tiptap/core@2.9.1)
'@tiptap/extension-underline':
specifier: ^2.9.1
version: 2.9.1(@tiptap/core@2.9.1)
'@tiptap/pm':
specifier: ^2.4.0
version: 2.9.1
@@ -2227,6 +2230,14 @@ packages:
'@tiptap/core': 2.9.1(@tiptap/pm@2.9.1)
dev: false
/@tiptap/extension-underline@2.9.1(@tiptap/core@2.9.1):
resolution: {integrity: sha512-IrUsIqKPgD7GcAjr4D+RC0WvLHUDBTMkD8uPNEoeD1uH9t9zFyDfMRPnx/z3/6Gf6fTh3HzLcHGibiW2HiMi2A==}
peerDependencies:
'@tiptap/core': ^2.7.0
dependencies:
'@tiptap/core': 2.9.1(@tiptap/pm@2.9.1)
dev: false
/@tiptap/pm@2.9.1:
resolution: {integrity: sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==}
dependencies: