diff --git a/apps/fronted/package.json b/apps/fronted/package.json index 83a2e90..4385288 100644 --- a/apps/fronted/package.json +++ b/apps/fronted/package.json @@ -9,10 +9,14 @@ "lint": "next lint" }, "dependencies": { - "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.1.2", "@tiptap/extension-color": "^2.4.0", @@ -28,6 +32,8 @@ "clsx": "^2.1.1", "date-fns": "^3.6.0", "dayjs": "^1.11.12", + "framer-motion": "^11.11.10", + "html2pdf.js": "^0.10.2", "lucide-react": "^0.379.0", "next": "14.2.3", "react": "^18", diff --git a/apps/fronted/src/app/globals.css b/apps/fronted/src/app/globals.css index a6f08cd..92507c7 100644 --- a/apps/fronted/src/app/globals.css +++ b/apps/fronted/src/app/globals.css @@ -92,8 +92,6 @@ ol { margin: 0; li p { - margin-top: 0.25em; - margin-bottom: 0.25em; } } /* Heading styles */ @@ -107,8 +105,6 @@ h6 { h1, h2 { - margin-top: 3.5rem; - margin-bottom: 1.5rem; } h1 { diff --git a/apps/fronted/src/app/page.tsx b/apps/fronted/src/app/page.tsx index f5b83d7..010b287 100644 --- a/apps/fronted/src/app/page.tsx +++ b/apps/fronted/src/app/page.tsx @@ -19,7 +19,7 @@ import { } from "lucide-react"; import ScrollToTop from "../components/ScrollToTop"; import Logo from "@/assets/images/logo@2x.svg"; - +import EditButton from "@/components/EditButton"; const features = [ { icon: , @@ -145,10 +145,10 @@ export default function LandingPage() { )} - + diff --git a/apps/fronted/src/app/workbench/page.tsx b/apps/fronted/src/app/workbench/page.tsx index 86dbe96..59c33fe 100644 --- a/apps/fronted/src/app/workbench/page.tsx +++ b/apps/fronted/src/app/workbench/page.tsx @@ -1,657 +1,116 @@ "use client"; -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 { - 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 }) => ( -
- - {label} -
-); +import { useRef, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useResumeStore } from "@/store/useResumeStore"; +import { EditorHeader } from "@/components/editor/EditorHeader"; +import { SidePanel } from "@/components/editor/SidePanel"; +import { EditPanel } from "@/components/editor/EditPanel"; +import { PreviewPanel } from "@/components/editor/PreviewPanel"; +import { getThemeConfig } from "@/theme/themeConfig"; +import { Eye, Edit2, Menu } from "lucide-react"; -const FormField = ({ label, children }) => ( -
- - {children} -
-); +export default function Home() { + const theme = useResumeStore((state) => state.theme); + const previewRef = useRef(null); + const themeConfig = getThemeConfig(theme === "dark"); -const Section = ({ title, children, className = "" }) => ( -
-

{title}

- {children} -
-); + // 移动端状态管理 + const [showSidebar, setShowSidebar] = useState(false); + const [isPreviewMode, setIsPreviewMode] = useState(false); -// 简历数据结构 -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 ( -
- {/* 固定的下载按钮 */} -
-
- -
-
- - {/* 简历预览内容 */} -
( +
+ setShowSidebar(true)} > - {/* 个人信息头部 */} -
-

- {data.basics.name} -

- {data.basics.label && ( -
- {data.basics.label} -
- )} -
- {data.basics.email && ( -
- - {data.basics.email} -
- )} - {data.basics.phone && ( -
- - {data.basics.phone} -
- )} - {data.basics.location && ( -
- - {data.basics.location} -
- )} - {data.basics.website && ( - - )} -
-
- - {/* 主要内容区域 */} -
- {/* 个人简介 */} - {data.basics.summary && ( -
-

- - 个人简介 -

-
- {data.basics.summary} -
-
- )} - - {/* 工作经历 */} - {data.work.length > 0 && ( -
-

- - 工作经历 -

-
- {data.work.map((work, index) => ( -
-
-

- {work.company} -

- - {work.startDate} - {work.endDate} - -
-
- {work.position} -
-
-
- ))} -
-
- )} - - {/* 教育经历 */} - {data.education.length > 0 && ( -
-

- - 教育经历 -

-
- {data.education.map((edu, index) => ( -
-
-

- {edu.institution} -

- - {edu.startDate} - {edu.endDate} - -
-
- {edu.studyType} · {edu.area} -
-
-
- ))} -
-
- )} - - {/* 技能专长 */} - {data.skills.length > 0 && ( -
-

- - 技能专长 -

-
- {data.skills.map((skill, index) => ( -
-
- {skill.name} -
-
- {skill.keywords.join(" · ")} -
-
- ))} -
-
- )} - - {/* 项目经历 */} - {data.projects.length > 0 && ( -
-

- - 项目经历 -

-
- {data.projects.map((project, index) => ( -
-
-

- {project.name} -

- {project.url && ( - - 项目链接 - - )} -
-
- {project.highlights.length > 0 && ( -
    - {project.highlights.map((highlight, i) => ( -
  • - {highlight} -
  • - ))} -
- )} -
- ))} -
-
- )} -
-
+ + + setIsPreviewMode(!isPreviewMode)} + > + {isPreviewMode ? : } +
); -}; -// 主编辑器组件 -const ResumeEditor = () => { - const [resumeData, setResumeData] = useState({ - 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: "" - } - ] - })); - }; + // 移动端侧边栏 + const MobileSidebar = () => ( + + {showSidebar && ( + <> + setShowSidebar(false)} + /> + + setShowSidebar(false)} /> + + + )} + + ); return ( -
-
- -
- - - - - - - - - - - - - - - - - -
+
+ -
- - -
-
- - updateBasics("name", e.target.value)} - placeholder="你的名字" - className="bg-white" - /> - - - updateBasics("label", e.target.value)} - placeholder="期望职位" - className="bg-white" - /> - -
- -
- -
- - - updateBasics("email", e.target.value) - } - placeholder="邮箱地址" - className="pl-10 bg-white" - /> -
-
- -
- - - updateBasics("phone", e.target.value) - } - placeholder="联系电话" - className="pl-10 bg-white" - /> -
-
-
- -
- -
- - - updateBasics("website", e.target.value) - } - placeholder="个人网站或博客" - className="pl-10 bg-white" - /> -
-
- -
- - - updateBasics("location", e.target.value) - } - placeholder="所在城市" - className="pl-10 bg-white" - /> -
-
-
- - - updateBasics("summary", value)} - placeholder="简单介绍自己" - /> - -
-
-
- - - -
-

教育经历

- -
- -
- {resumeData.education.map((edu, index) => ( - -
- - { - const newEducation = [...resumeData.education]; - newEducation[index].institution = e.target.value; - setResumeData({ - ...resumeData, - education: newEducation - }); - }} - placeholder="学校名称" - /> - - -
- -
- - { - const newEducation = [ - ...resumeData.education - ]; - newEducation[index].startDate = - e.target.value; - setResumeData({ - ...resumeData, - education: newEducation - }); - }} - className="pl-10" - /> -
-
- -
- - { - const newEducation = [ - ...resumeData.education - ]; - newEducation[index].endDate = e.target.value; - setResumeData({ - ...resumeData, - education: newEducation - }); - }} - className="pl-10" - /> -
-
-
- -
- - { - const newEducation = [...resumeData.education]; - newEducation[index].studyType = e.target.value; - setResumeData({ - ...resumeData, - education: newEducation - }); - }} - placeholder="学历" - /> - - - { - const newEducation = [...resumeData.education]; - newEducation[index].area = e.target.value; - setResumeData({ - ...resumeData, - education: newEducation - }); - }} - placeholder="专业" - /> - -
- - - { - const newEducation = [...resumeData.education]; - newEducation[index].description = value; - setResumeData({ - ...resumeData, - education: newEducation - }); - }} - placeholder="在校经历描述" - /> - - -
- -
-
-
- ))} -
-
-
-
- + {/* 桌面端布局 */} +
+ + +
-
- + + {/* 移动端布局 */} +
+ + {isPreviewMode ? ( + + + + ) : ( + + + + )} +
-
+ + + +
); -}; - -export default ResumeEditor; +} diff --git a/apps/fronted/src/components/editor/EditPanel.tsx b/apps/fronted/src/components/editor/EditPanel.tsx new file mode 100644 index 0000000..875d288 --- /dev/null +++ b/apps/fronted/src/components/editor/EditPanel.tsx @@ -0,0 +1,211 @@ +"use client"; + +import React from "react"; +import { + motion, + AnimatePresence, + Reorder, + useDragControls +} from "framer-motion"; +import { + CalendarIcon, + PlusCircle, + ChevronDown, + Trash2, + X, + GripVertical +} from "lucide-react"; +import { useResumeStore } from "@/store/useResumeStore"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import Field from "./Field"; +import ProjectItem from "./project/ProjectItem"; + +// ProjectEditor 组件用于编辑单个项目 +interface Project { + id: string; + name: string; + role: string; + date: string; + description: string; + technologies: string; + responsibilities: string; + achievements: string; +} + +// 主面板组件 +export function EditPanel() { + const { + theme, + activeSection, + menuSections, + basic, + projects = [], + updateBasicInfo, + updateProjects, + deleteProject + } = useResumeStore(); + + const [editingId, setEditingId] = React.useState(null); + const handleCreateProject = () => { + const newProject: Project = { + id: crypto.randomUUID(), + name: "Project Name", + role: "Project Role", + date: "", + description: "", + technologies: "", + responsibilities: "", + achievements: "" + }; + setEditingId(newProject.id); + updateProjects(newProject); + }; + + const renderFields = () => { + switch (activeSection) { + case "basic": + return ( +
+ updateBasicInfo({ name: value })} + placeholder="请输入你的姓名" + required + /> + updateBasicInfo({ title: value })} + placeholder="期望职位" + required + /> + updateBasicInfo({ birthDate: value })} + type="date" + /> +
+ updateBasicInfo({ email: value })} + placeholder="your@email.com" + required + /> + updateBasicInfo({ phone: value })} + placeholder="手机号码" + required + /> +
+ updateBasicInfo({ location: value })} + placeholder="城市" + required + /> + updateBasicInfo({ summary: value })} + type="textarea" + placeholder="简单介绍一下自己..." + /> +
+ ); + + case "projects": + return ( +
+ { + useResumeStore.setState({ projects: newOrder }); + }} + className="space-y-3" + > + {projects.map((project) => ( + + ))} + + {/* 添加项目按钮 */} + + +
+ ); + default: + return null; + } + }; + + return ( + +
+ +
+ + {menuSections?.find((s) => s.id === activeSection)?.icon} + +

+ {menuSections?.find((s) => s.id === activeSection)?.title} +

+
+
+ + + {renderFields()} + +
+
+ ); +} diff --git a/apps/fronted/src/components/editor/EditorHeader.tsx b/apps/fronted/src/components/editor/EditorHeader.tsx new file mode 100644 index 0000000..abc9120 --- /dev/null +++ b/apps/fronted/src/components/editor/EditorHeader.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Sun, Moon, Eye } from "lucide-react"; +import { useResumeStore } from "@/store/useResumeStore"; +import { getThemeConfig } from "@/theme/themeConfig"; +import { PdfExport } from "./PdfExport"; + +interface EditorHeaderProps { + previewRef: React.RefObject; + isMobile?: boolean; +} + +export function EditorHeader({ previewRef, isMobile }: EditorHeaderProps) { + const { theme, menuSections, activeSection, setActiveSection, toggleTheme } = + useResumeStore(); + + const themeConfig = getThemeConfig(theme === "dark"); + + const visibleSections = menuSections + .filter((section) => section.enabled) + .sort((a, b) => a.order - b.order); + + return ( + +
+
+ + Resume +
+ + + {/* 在移动端隐藏导航按钮 */} +
+ {visibleSections.map((section) => ( + setActiveSection(section.id)} + > + {section.icon} + {section.title} + + ))} +
+
+ + {/* 在移动端只显示主题切换按钮 */} +
+ + {theme === "dark" ? ( + + ) : ( + + )} + + + {/* 在移动端隐藏这些按钮 */} +
+ + + 预览 + + + +
+
+
+ + ); +} diff --git a/apps/fronted/src/components/editor/Field.tsx b/apps/fronted/src/components/editor/Field.tsx new file mode 100644 index 0000000..ba36050 --- /dev/null +++ b/apps/fronted/src/components/editor/Field.tsx @@ -0,0 +1,173 @@ +import { cn } from "@/lib/utils"; +import { motion } from "framer-motion"; +import { format } from "date-fns"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { Calendar } from "../ui/calendar"; +import { CalendarIcon } from "lucide-react"; +import { useResumeStore } from "@/store/useResumeStore"; + +// 扩展 FieldProps 类型定义 +type FieldProps = { + label: string; + value: string; + onChange: (value: string) => void; + type?: "text" | "textarea" | "date"; + placeholder?: string; + required?: boolean; +}; + +// Field 组件 +const Field: React.FC = ({ + label, + value, + onChange, + type = "text", + placeholder, + required +}) => { + const theme = useResumeStore((state) => state.theme); + + const inputStyles = cn( + "mt-1.5 block w-full transition-all duration-200", + "rounded-lg px-4 py-2.5", + "border", + theme === "dark" + ? [ + "bg-neutral-900/30 border-neutral-800", + "text-neutral-100", + "focus:bg-neutral-900/50", + "placeholder:text-neutral-500" + ] + : [ + "bg-white border-gray-100", + "text-gray-900", + "hover:border-gray-200", + "placeholder:text-gray-400" + ], + "focus:ring-1 focus:ring-offset-1", + theme === "dark" + ? "focus:ring-indigo-500/20 focus:ring-offset-neutral-900" + : "focus:ring-indigo-500/20 focus:ring-offset-white", + "focus:border-indigo-500" + ); + + if (type === "date") { + const date = value ? new Date(value) : undefined; + + return ( + + ); + } + + if (type === "textarea") { + return ( + + ); + } + + return ( + + ); +}; + +export default Field; diff --git a/apps/fronted/src/components/editor/PdfExport.tsx b/apps/fronted/src/components/editor/PdfExport.tsx new file mode 100644 index 0000000..6f69f61 --- /dev/null +++ b/apps/fronted/src/components/editor/PdfExport.tsx @@ -0,0 +1,59 @@ +import React, { useRef, useState } from "react"; +import { motion } from "framer-motion"; +import { Download, Loader2 } from "lucide-react"; +import html2pdf from "html2pdf.js"; +import { useResumeStore } from "@/store/useResumeStore"; +import { getThemeConfig } from "@/theme/themeConfig"; + +export function PdfExport() { + const { basic, theme } = useResumeStore(); + const [isExporting, setIsExporting] = useState(false); + const themeConfig = getThemeConfig(theme === "dark"); + + const handleExport = async () => { + try { + setIsExporting(true); + + const element = document.querySelector("#resume-preview"); + const opt = { + margin: [10, 10, 10, 10], + filename: `${basic.name}_简历.pdf`, + image: { type: "jpeg", quality: 0.98 }, + html2canvas: { + scale: 2, + useCORS: true, + letterRendering: true + }, + jsPDF: { + unit: "mm", + format: "a4", + orientation: "portrait" + } + }; + + await html2pdf().set(opt).from(element).save(); + } catch (error) { + console.error("PDF export failed:", error); + // 可以添加错误提示 + } finally { + setIsExporting(false); + } + }; + + return ( + + {isExporting ? ( + + ) : ( + + )} + {isExporting ? "导出中..." : "导出 PDF"} + + ); +} diff --git a/apps/fronted/src/components/editor/PreviewPanel.tsx b/apps/fronted/src/components/editor/PreviewPanel.tsx new file mode 100644 index 0000000..129b134 --- /dev/null +++ b/apps/fronted/src/components/editor/PreviewPanel.tsx @@ -0,0 +1,448 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { useResumeStore } from "@/store/useResumeStore"; +import { getThemeConfig } from "@/theme/themeConfig"; +import { cn } from "@/lib/utils"; + +const getFontFamilyClass = (fontFamily: string) => { + switch (fontFamily) { + case "serif": + return "font-serif"; + case "mono": + return "font-mono"; + default: + return "font-sans"; + } +}; + +export function PreviewPanel() { + const { + theme, + basic, + education, + experience, + menuSections, + globalSettings, + projects + } = useResumeStore(); + + console.log(projects, 111); + const fontFamilyClass = getFontFamilyClass( + globalSettings?.fontFamily || "sans" + ); + + const renderSection = (sectionId: string) => { + switch (sectionId) { + case "basic": + return ( + +

+ 个人简介 +

+

+ {basic.summary} +

+
+ ); + + case "education": + return ( + +

+ 教育经历 +

+ {education.map((edu) => ( +
+
+
+

+ {edu.school} +

+

+ {edu.degree} +

+
+ + {edu.date} + +
+

+ {edu.details} +

+
+ ))} +
+ ); + + case "experience": + return ( + +

+ 工作经验 +

+ {experience.map((exp) => ( +
+
+
+

+ {exp.company} +

+

+ {exp.position} +

+
+ + {exp.date} + +
+

+ {exp.details} +

+
+ ))} +
+ ); + case "projects": + return ( + +

+ 项目经历 +

+ + {projects.map((project) => ( + + + + + {project.name} + + + {project.role} + + + + {project.date} + + + + {/* 项目描述 */} + {project.description && ( + + {project.description} + + )} + + {/* 技术栈 */} + {project.technologies && ( + + + 技术栈: + + + {project.technologies} + + + )} + + {/* 主要职责 */} + {project.responsibilities && ( + + + 主要职责: + + + {project.responsibilities} + + + )} + + {/* 项目成就 */} + {project.achievements && ( + + + 项目成就: + + + {project.achievements} + + + )} + + ))} + +
+ ); + default: + return null; + } + }; + + return ( + +
+ {/* A4 纸张容器 */} + + {/* 内容区域 */} +
+ + {/* Header */} +
+ + {basic.name} + + + {basic.title} + + + {basic.email} + + {basic.phone} + + {basic.location} + + + {basic.birthDate + ? new Date(basic.birthDate).toLocaleDateString() + : ""} + + +
+ + {/* Sections */} + {menuSections + .filter((section) => section.enabled) + .sort((a, b) => a.order - b.order) + .map((section) => ( + + {renderSection(section.id)} + + ))} +
+
+
+
+
+ ); +} diff --git a/apps/fronted/src/components/editor/SidePanel.tsx b/apps/fronted/src/components/editor/SidePanel.tsx new file mode 100644 index 0000000..dbb5191 --- /dev/null +++ b/apps/fronted/src/components/editor/SidePanel.tsx @@ -0,0 +1,339 @@ +"use client"; + +import { motion, Reorder } from "framer-motion"; +import { + GripVertical, + Eye, + EyeOff, + Layout, + Type, + SpaceIcon +} from "lucide-react"; +import { useResumeStore } from "@/store/useResumeStore"; +import { getThemeConfig } from "@/theme/themeConfig"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { Slider } from "@/components/ui/slider"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +const fontOptions = [ + { value: "sans", label: "无衬线体" }, + { value: "serif", label: "衬线体" }, + { value: "mono", label: "等宽体" } +]; + +function SettingCard({ + icon: Icon, + title, + children +}: { + icon: any; + title: string; + children: React.ReactNode; +}) { + const { theme } = useResumeStore(); + + return ( + + + + + + {title} + + + + {children} + + ); +} + +export function SidePanel() { + const { + theme, + menuSections, + reorderSections, + toggleSectionVisibility, + setActiveSection, + globalSettings, + updateGlobalSettings + } = useResumeStore(); + + return ( + +
+ {/* 布局部分 */} + + + {menuSections.map((item) => ( + +
+ + + {item.icon} + + setActiveSection(item.id)} + > + {item.title} + + toggleSectionVisibility(item.id)} + className={cn( + "p-1.5 rounded-md", + theme === "dark" + ? "hover:bg-neutral-700 text-neutral-300" + : "hover:bg-gray-100 text-gray-600" + )} + > + {item.enabled ? ( + + ) : ( + + )} + +
+
+ ))} +
+
+ + {/* 排版设置 */} + +
+
+ + +
+ +
+ +
+ + updateGlobalSettings?.({ baseFontSize: value }) + } + className={cn( + theme === "dark" + ? "[&_[role=slider]]:bg-neutral-200" + : "[&_[role=slider]]:bg-gray-900" + )} + /> + + {globalSettings?.baseFontSize}px + +
+
+
+
+ + {/* 间距设置 */} + +
+
+ +
+ + updateGlobalSettings?.({ pagePadding: value }) + } + className={cn( + theme === "dark" + ? "[&_[role=slider]]:bg-neutral-200" + : "[&_[role=slider]]:bg-gray-900" + )} + /> + + {globalSettings?.pagePadding}px + +
+
+ +
+ +
+ + updateGlobalSettings?.({ paragraphSpacing: value }) + } + className={cn( + theme === "dark" + ? "[&_[role=slider]]:bg-neutral-200" + : "[&_[role=slider]]:bg-gray-900" + )} + /> + + {globalSettings?.paragraphSpacing}em + +
+
+
+
+
+
+ ); +} diff --git a/apps/fronted/src/components/editor/project/ProjectItem.tsx b/apps/fronted/src/components/editor/project/ProjectItem.tsx new file mode 100644 index 0000000..d447b13 --- /dev/null +++ b/apps/fronted/src/components/editor/project/ProjectItem.tsx @@ -0,0 +1,272 @@ +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useResumeStore } from "@/store/useResumeStore"; +import { + AnimatePresence, + motion, + Reorder, + useDragControls +} from "framer-motion"; +import { ChevronDown, GripVertical, Trash2 } from "lucide-react"; +import { useState } from "react"; +import Field from "../Field"; + +interface ProjectEditorProps { + project: Project; + onSave: (project: Project) => void; + onDelete: () => void; + onCancel: () => void; +} + +const ProjectEditor: React.FC = ({ + project, + onSave, + onDelete, + onCancel +}) => { + const theme = useResumeStore((state) => state.theme); + const [data, setData] = useState(project); + + const handleSave = (e: React.FormEvent) => { + e.preventDefault(); + if (!data.name || !data.role || !data.date) return; + onSave(data); + }; + + return ( +
+
+
+ setData((prev) => ({ ...prev, name: value }))} + placeholder="项目名称" + required + /> + setData((prev) => ({ ...prev, role: value }))} + placeholder="如:前端负责人" + required + /> +
+ setData((prev) => ({ ...prev, date: value }))} + placeholder="如:2023.01 - 2023.06" + required + /> + + setData((prev) => ({ ...prev, description: value })) + } + type="textarea" + placeholder="简要描述项目的背景和目标..." + /> + + setData((prev) => ({ ...prev, technologies: value })) + } + type="textarea" + placeholder="使用的主要技术、框架和工具..." + /> + + setData((prev) => ({ ...prev, responsibilities: value })) + } + type="textarea" + placeholder="描述你在项目中的具体职责和工作内容..." + /> + + setData((prev) => ({ ...prev, achievements: value })) + } + type="textarea" + placeholder="项目取得的成果、解决的问题或改进的指标..." + /> +
+
+ +
+
+ ); +}; + +// ProjectEditor 组件用于编辑单个项目 +interface Project { + id: string; + name: string; + role: string; + date: string; + description: string; + technologies: string; + responsibilities: string; + achievements: string; +} + +const ProjectItem = ({ project }: { project: Project }) => { + const dragControls = useDragControls(); + const [expandedId, setExpandedId] = useState(null); + + const { theme, updateProjects, deleteProject } = useResumeStore(); + return ( + + {/* 拖拽手柄区域 */} +
{ + if (expandedId === project.id) return; + dragControls.start(event); + }} + className={cn( + "w-12 flex items-center justify-center border-r shrink-0 touch-none", + theme === "dark" ? "border-neutral-800" : "border-gray-100", + expandedId === project.id + ? "cursor-not-allowed" + : "cursor-grab hover:bg-gray-50 dark:hover:bg-neutral-800/50" + )} + > + +
+ + {/* 内容区域 */} +
+
{ + if (expandedId === project.id) { + setExpandedId(null); + } else { + setExpandedId(project.id); + } + }} + > +
+

+ {project.name || "未命名项目"} +

+
+
+ + + + +
+
+ + {expandedId === project.id && ( + +
e.stopPropagation()} + > +
+ { + updateProjects(updatedProject); + setExpandedId(null); + }} + onDelete={() => { + deleteProject(project.id); + setExpandedId(null); + }} + onCancel={() => setExpandedId(null)} + /> +
+ + )} + +
+ + ); +}; + +export default ProjectItem; diff --git a/apps/fronted/src/components/ui/accordion.tsx b/apps/fronted/src/components/ui/accordion.tsx new file mode 100644 index 0000000..24c788c --- /dev/null +++ b/apps/fronted/src/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/apps/fronted/src/components/ui/select.tsx b/apps/fronted/src/components/ui/select.tsx new file mode 100644 index 0000000..cbe5a36 --- /dev/null +++ b/apps/fronted/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/apps/fronted/src/components/ui/slider.tsx b/apps/fronted/src/components/ui/slider.tsx new file mode 100644 index 0000000..c31c2b3 --- /dev/null +++ b/apps/fronted/src/components/ui/slider.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SliderPrimitive from "@radix-ui/react-slider" + +import { cn } from "@/lib/utils" + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)) +Slider.displayName = SliderPrimitive.Root.displayName + +export { Slider } diff --git a/apps/fronted/src/components/ui/switch.tsx b/apps/fronted/src/components/ui/switch.tsx new file mode 100644 index 0000000..bc69cf2 --- /dev/null +++ b/apps/fronted/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/apps/fronted/src/store/useResumeStore.ts b/apps/fronted/src/store/useResumeStore.ts new file mode 100644 index 0000000..a7c7905 --- /dev/null +++ b/apps/fronted/src/store/useResumeStore.ts @@ -0,0 +1,197 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { + ResumeData, + ResumeSection, + BasicInfo, + Education, + Experience, + GlobalSettings, + Project +} from "../types/resume"; + +interface ResumeStore { + // 基础数据 + basic: BasicInfo; + education: Education[]; + experience: Experience[]; + + // 菜单配置 + menuSections: { + id: string; + title: string; + icon: string; + enabled: boolean; + order: number; + }[]; + + // 主题设置 + theme: "light" | "dark"; + activeSection: string; + + // Actions + updateBasicInfo: (data: Partial) => void; + updateEducation: (id: string, data: Partial) => void; + updateExperience: (id: string, data: Partial) => void; + + // 菜单操作 + reorderSections: (newOrder: typeof initialState.menuSections) => void; + toggleSectionVisibility: (sectionId: string) => void; + setActiveSection: (sectionId: string) => void; + toggleTheme: () => void; + // 全局设置 + globalSettings?: GlobalSettings; + updateGlobalSettings?: (settings: Partial) => void; + // 项目经历 + projects: Project[]; + updateProjects: (project: Project) => void; + deleteProject: (id: string) => void; +} + +const initialState = { + basic: { + name: "张三", + title: "高级前端工程师", + email: "example@email.com", + phone: "13800138000", + location: "北京市", + summary: "5年前端开发经验...", + birthDate: "1995-01-01" + }, + education: [ + { + id: "1", + school: "北京大学", + degree: "计算机科学与技术", + date: "2016-2020", + details: "主修课程:..." + } + ], + experience: [ + { + id: "1", + company: "某科技有限公司", + position: "高级前端工程师", + date: "2020-至今", + details: "负责公司核心产品..." + } + ], + menuSections: [ + { id: "basic", title: "基本信息", icon: "👤", enabled: true, order: 0 }, + { id: "education", title: "教育经历", icon: "🎓", enabled: true, order: 1 }, + { + id: "experience", + title: "工作经验", + icon: "💼", + enabled: true, + order: 2 + }, + { id: "skills", title: "技能特长", icon: "⚡", enabled: true, order: 3 }, + { id: "projects", title: "项目经历", icon: "🚀", enabled: true, order: 4 } + ], + theme: "light" as const, + activeSection: "basic", + projects: [ + { + id: "p1", + name: "企业中台系统", + role: "前端负责人", + date: "2023.06 - 2023.12", + description: + "基于 React 的企业级中台项目,包含工作流、报表、系统管理等多个子系统", + technologies: "React, TypeScript, TailwindCSS, shadcn/ui", + responsibilities: + "负责整体技术方案设计和团队管理,把控项目进度和代码质量", + achievements: "系统整体性能提升 50%,代码重用率提高到 80%" + }, + { + id: "p2", + name: "xxx", + role: "前端负责人", + date: "2023.06 - 2023.12", + description: + "基于 React 的企业级中台项目,包含工作流、报表、系统管理等多个子系统", + technologies: "React, TypeScript, TailwindCSS, shadcn/ui", + responsibilities: + "负责整体技术方案设计和团队管理,把控项目进度和代码质量", + achievements: "系统整体性能提升 50%,代码重用率提高到 80%" + } + ] +}; + +export const useResumeStore = create()( + persist( + (set) => ({ + ...initialState, + + updateBasicInfo: (data) => + set((state) => ({ basic: { ...state.basic, ...data } })), + + updateEducation: (id, data) => + set((state) => ({ + education: state.education.map((edu) => + edu.id === id ? { ...edu, ...data } : edu + ) + })), + + updateExperience: (id, data) => + set((state) => ({ + experience: state.experience.map((exp) => + exp.id === id ? { ...exp, ...data } : exp + ) + })), + + reorderSections: (newOrder) => { + // 根据新顺序重新计算每个部分的 order + const updatedSections = newOrder.map((section, index) => ({ + ...section, + order: index // 根据数组索引设置新的顺序 + })); + + set({ menuSections: updatedSections }); + }, + + toggleSectionVisibility: (sectionId) => + set((state) => ({ + menuSections: state.menuSections.map((section) => + section.id === sectionId + ? { ...section, enabled: !section.enabled } + : section + ) + })), + + setActiveSection: (sectionId) => set({ activeSection: sectionId }), + + updateProjects: (project) => + set((state) => ({ + projects: state.projects.some((p) => p.id === project.id) + ? state.projects.map((p) => (p.id === project.id ? project : p)) + : [...state.projects, project] + })), + + deleteProject: (id) => + set((state) => ({ + projects: state.projects.filter((p) => p.id !== id) + })), + + toggleTheme: () => + set((state) => ({ + theme: state.theme === "light" ? "dark" : "light" + })), + updateGlobalSettings: (settings) => + set((state) => { + const newSettings = { + ...state.globalSettings, + ...settings + }; + return { + globalSettings: newSettings + }; + }) + }), + + { + name: "resume-storage" + } + ) +); diff --git a/apps/fronted/src/theme/themeConfig.ts b/apps/fronted/src/theme/themeConfig.ts new file mode 100644 index 0000000..af1bd7b --- /dev/null +++ b/apps/fronted/src/theme/themeConfig.ts @@ -0,0 +1,15 @@ +export const getThemeConfig = (isDark: boolean) => ({ + bg: isDark ? "bg-black" : "bg-gray-50", + sidebar: isDark ? "bg-zinc-900/50" : "bg-white", + text: isDark ? "text-white" : "text-gray-900", + textSecondary: isDark ? "text-zinc-400" : "text-gray-500", + border: isDark ? "border-zinc-800" : "border-gray-200", + card: isDark ? "bg-zinc-800/50" : "bg-white", + hover: isDark ? "hover:bg-zinc-800" : "hover:bg-gray-100", + input: isDark ? "bg-zinc-800/50 border-zinc-700" : "bg-white border-gray-200", + button: isDark ? "bg-zinc-800" : "bg-white", + buttonPrimary: isDark ? "bg-indigo-500" : "bg-indigo-600", + preview: isDark ? "bg-zinc-900" : "bg-white" +}); + +export type ThemeConfig = ReturnType; diff --git a/apps/fronted/src/types/resume.ts b/apps/fronted/src/types/resume.ts new file mode 100644 index 0000000..41f99d6 --- /dev/null +++ b/apps/fronted/src/types/resume.ts @@ -0,0 +1,69 @@ +export interface BasicInfo { + birthDate: string; + name: string; + title: string; + email: string; + phone: string; + location: string; + summary: string; +} + +export interface Education { + id: string; + school: string; + degree: string; + date: string; + details: string; +} + +export interface Experience { + id: string; + company: string; + position: string; + date: string; + details: string; +} + +export interface Skill { + id: string; + name: string; + level: number; +} + +export interface Project { + id: string; + name: string; + role: string; + date: string; + description: string; + technologies: string; + responsibilities: string; + achievements: string; +} + +export interface ResumeSection { + id: string; + title: string; + icon: string; + type: "basic" | "education" | "experience" | "skills" | "projects"; +} + +export interface ResumeData { + basic: BasicInfo; + education: Education[]; + experience: Experience[]; + skills: Skill[]; + projects: Project[]; + sections: ResumeSection[]; + activeSection: string; + theme: "light" | "dark"; +} + +export type GlobalSettings = { + theme: "light" | "dark"; + themeColor: string; + fontFamily: string; + baseFontSize: number; + pagePadding: number; + paragraphSpacing: number; +}; diff --git a/apps/fronted/tailwind.config.ts b/apps/fronted/tailwind.config.ts index 84287e8..f613e42 100644 --- a/apps/fronted/tailwind.config.ts +++ b/apps/fronted/tailwind.config.ts @@ -10,69 +10,95 @@ const config = { ], prefix: "", theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, + container: { + center: 'true', + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + }, + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + } + } }, plugins: [require("tailwindcss-animate")], } satisfies Config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14708b6..92a356d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,18 +92,30 @@ importers: apps/fronted: dependencies: + '@radix-ui/react-accordion': + specifier: ^1.2.1 + version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-label': - specifier: ^2.0.2 + specifier: ^2.1.0 version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-popover': specifier: ^1.1.1 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-separator': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slider': + specifier: ^1.2.1 + version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) @@ -149,6 +161,12 @@ importers: dayjs: specifier: ^1.11.12 version: 1.11.13 + framer-motion: + specifier: ^11.11.10 + version: 11.11.10(react-dom@18.3.1)(react@18.3.1) + html2pdf.js: + specifier: ^0.10.2 + version: 0.10.2 lucide-react: specifier: ^0.379.0 version: 0.379.0(react@18.3.1) @@ -1416,10 +1434,42 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@radix-ui/number@1.1.0: + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + dev: false + /@radix-ui/primitive@1.1.0: resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} dev: false + /@radix-ui/react-accordion@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collapsible': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} peerDependencies: @@ -1440,6 +1490,33 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-collapsible@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1775,6 +1852,46 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-select@2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.2)(react@18.3.1) + dev: false + /@radix-ui/react-separator@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==} peerDependencies: @@ -1795,6 +1912,36 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-slider@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.3.2)(react@18.3.1): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -1824,6 +1971,32 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-switch@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.2)(react@18.3.1) + '@types/react': 18.3.2 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-tabs@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==} peerDependencies: @@ -1936,6 +2109,19 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-use-previous@1.1.0(@types/react@18.3.2)(react@18.3.1): + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.2 + react: 18.3.1 + dev: false + /@radix-ui/react-use-rect@1.1.0(@types/react@18.3.2)(react@18.3.1): resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: @@ -2492,6 +2678,12 @@ packages: resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} dev: true + /@types/raf@3.4.3: + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + requiresBuild: true + dev: false + optional: true + /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true @@ -3085,6 +3277,12 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true + /atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + dev: false + /available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -3178,6 +3376,11 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true @@ -3266,6 +3469,12 @@ packages: node-int64: 0.4.0 dev: true + /btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3322,6 +3531,22 @@ packages: resolution: {integrity: sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==} dev: true + /canvg@3.0.10: + resolution: {integrity: sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==} + engines: {node: '>=10.0.0'} + requiresBuild: true + dependencies: + '@babel/runtime': 7.24.5 + '@types/raf': 3.4.3 + core-js: 3.38.1 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + dev: false + optional: true + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3550,6 +3775,12 @@ packages: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} dev: true + /core-js@3.38.1: + resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==} + requiresBuild: true + dev: false + optional: true + /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3611,6 +3842,12 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + dependencies: + utrie: 1.0.2 + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3808,6 +4045,12 @@ packages: esutils: 2.0.3 dev: true + /dompurify@2.5.7: + resolution: {integrity: sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==} + requiresBuild: true + dev: false + optional: true + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -3973,6 +4216,10 @@ packages: is-symbol: 1.0.4 dev: true + /es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + dev: false + /escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -4457,6 +4704,10 @@ packages: bser: 2.1.1 dev: true + /fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + dev: false + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -4586,6 +4837,25 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + /framer-motion@11.11.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-061Bt1jL/vIm+diYIiA4dP/Yld7vD47ROextS7ESBW5hr4wQFhxB5D5T5zAc3c/5me3cOa+iO5LqhA38WDln/A==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.6.2 + dev: false + /fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -4838,6 +5108,22 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + dev: false + + /html2pdf.js@0.10.2: + resolution: {integrity: sha512-WyHVeMb18Bp7vYTmBv1GVsThH//K7SRfHdSdhHPkl4JvyQarNQXnailkYn0QUbRRmnN5rdbbmSIGEsPZtzPy2Q==} + dependencies: + es6-promise: 4.2.8 + html2canvas: 1.4.1 + jspdf: 2.5.2 + dev: false + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -5758,6 +6044,20 @@ packages: graceful-fs: 4.2.11 dev: true + /jspdf@2.5.2: + resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==} + dependencies: + '@babel/runtime': 7.24.5 + atob: 2.1.2 + btoa: 1.2.1 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.10 + core-js: 3.38.1 + dompurify: 2.5.7 + html2canvas: 1.4.1 + dev: false + /jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -6387,6 +6687,12 @@ packages: engines: {node: '>=8'} dev: true + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + requiresBuild: true + dev: false + optional: true + /picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} @@ -6751,6 +7057,14 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + requiresBuild: true + dependencies: + performance-now: 2.1.0 + dev: false + optional: true + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -6925,6 +7239,12 @@ packages: which-builtin-type: 1.1.3 dev: true + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + requiresBuild: true + dev: false + optional: true + /regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -7008,6 +7328,13 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + /rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + requiresBuild: true + dev: false + optional: true + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -7261,6 +7588,13 @@ packages: escape-string-regexp: 2.0.0 dev: true + /stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + requiresBuild: true + dev: false + optional: true + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -7464,6 +7798,13 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dev: false + optional: true + /symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -7594,6 +7935,12 @@ packages: minimatch: 3.1.2 dev: true + /text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + dependencies: + utrie: 1.0.2 + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -7970,6 +8317,12 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + /utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + dependencies: + base64-arraybuffer: 1.0.2 + dev: false + /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} dev: true