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("summary", value)}
- placeholder="简单介绍自己"
- />
-
-
-
-
-
-
-
-
-
-
- {resumeData.education.map((edu, index) => (
-
-
-
- ))}
-
-
-
-
-
+ {/* 桌面端布局 */}
+
-
-
+
+ {/* 移动端布局 */}
+
+
+ {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 (
+
+ );
+};
+
+// 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