mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
refactor: right dock
This commit is contained in:
@@ -24,10 +24,8 @@ import {
|
||||
const LAYOUT_CONFIG = {
|
||||
DEFAULT: [20, 32, 48],
|
||||
SIDE_COLLAPSED: [50, 50],
|
||||
EDIT_FOCUSED_WITH_SIDE: [18, 82, 0],
|
||||
PREVIEW_FOCUSED_WITH_SIDE: [18, 0, 82],
|
||||
EDIT_FOCUSED_NO_SIDE: [100, 0],
|
||||
PREVIEW_FOCUSED_NO_SIDE: [0, 100],
|
||||
EDIT_FOCUSED: [20, 80],
|
||||
PREVIEW_FOCUSED: [20, 80],
|
||||
};
|
||||
|
||||
const DragHandle = ({ show = true }) => {
|
||||
@@ -59,14 +57,18 @@ const DragHandle = ({ show = true }) => {
|
||||
const LayoutControls = memo(
|
||||
({
|
||||
sidePanelCollapsed,
|
||||
focusedPanel,
|
||||
editPanelCollapsed,
|
||||
previewPanelCollapsed,
|
||||
toggleSidePanel,
|
||||
togglePanelFocus,
|
||||
toggleEditPanel,
|
||||
togglePreviewPanel,
|
||||
}: {
|
||||
sidePanelCollapsed: boolean;
|
||||
focusedPanel: null | "edit" | "preview";
|
||||
editPanelCollapsed: boolean;
|
||||
previewPanelCollapsed: boolean;
|
||||
toggleSidePanel: () => void;
|
||||
togglePanelFocus: (panel: "edit" | "preview") => void;
|
||||
toggleEditPanel: () => void;
|
||||
togglePreviewPanel: () => void;
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -102,21 +104,21 @@ const LayoutControls = memo(
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={focusedPanel === "edit" ? "secondary" : "ghost"}
|
||||
variant={editPanelCollapsed ? "secondary" : "ghost"}
|
||||
size="icon"
|
||||
className="h-9 w-9 rounded-full"
|
||||
onClick={() => togglePanelFocus("edit")}
|
||||
onClick={toggleEditPanel}
|
||||
>
|
||||
{focusedPanel === "edit" ? (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
) : (
|
||||
{editPanelCollapsed ? (
|
||||
<Edit2 className="h-4 w-4" />
|
||||
) : (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">
|
||||
{focusedPanel === "edit" ? "退出编辑模式" : "专注编辑"}
|
||||
{editPanelCollapsed ? "展开编辑面板" : "收起编辑面板"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -126,21 +128,21 @@ const LayoutControls = memo(
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={focusedPanel === "preview" ? "secondary" : "ghost"}
|
||||
variant={previewPanelCollapsed ? "secondary" : "ghost"}
|
||||
size="icon"
|
||||
className="h-9 w-9 rounded-full"
|
||||
onClick={() => togglePanelFocus("preview")}
|
||||
onClick={togglePreviewPanel}
|
||||
>
|
||||
{focusedPanel === "preview" ? (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
) : (
|
||||
{previewPanelCollapsed ? (
|
||||
<Eye className="h-4 w-4" />
|
||||
) : (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">
|
||||
{focusedPanel === "preview" ? "退出预览模式" : "专注预览"}
|
||||
{previewPanelCollapsed ? "展开预览面板" : "收起预览面板"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -153,117 +155,76 @@ LayoutControls.displayName = "LayoutControls";
|
||||
|
||||
export default function Home() {
|
||||
const [sidePanelCollapsed, setSidePanelCollapsed] = useState(false);
|
||||
const [focusedPanel, setFocusedPanel] = useState<null | "edit" | "preview">(
|
||||
null
|
||||
);
|
||||
|
||||
const [showMobileSidebar, setShowMobileSidebar] = useState(false);
|
||||
const [isMobilePreview, setIsMobilePreview] = useState(false);
|
||||
const [panelSizes, setPanelSizes] = useState(LAYOUT_CONFIG.DEFAULT);
|
||||
const [layoutKey, setLayoutKey] = useState(0);
|
||||
|
||||
const updateLayout = (newSizes: number[]) => {
|
||||
setPanelSizes(newSizes);
|
||||
setLayoutKey((prev) => prev + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let newSizes;
|
||||
if (sidePanelCollapsed) {
|
||||
if (focusedPanel === "edit") {
|
||||
newSizes = [100, 0];
|
||||
} else if (focusedPanel === "preview") {
|
||||
newSizes = [0, 100];
|
||||
} else {
|
||||
newSizes = [50, 50];
|
||||
}
|
||||
} else {
|
||||
if (focusedPanel === "edit") {
|
||||
newSizes = [18, 82, 0];
|
||||
} else if (focusedPanel === "preview") {
|
||||
newSizes = [18, 0, 82];
|
||||
} else {
|
||||
newSizes = [20, 32, 48];
|
||||
}
|
||||
}
|
||||
updateLayout([...newSizes]);
|
||||
}, [sidePanelCollapsed, focusedPanel]);
|
||||
const [editPanelCollapsed, setEditPanelCollapsed] = useState(false);
|
||||
const [previewPanelCollapsed, setPreviewPanelCollapsed] = useState(false);
|
||||
const [panelSizes, setPanelSizes] = useState<number[]>(LAYOUT_CONFIG.DEFAULT);
|
||||
|
||||
const toggleSidePanel = () => {
|
||||
setSidePanelCollapsed(!sidePanelCollapsed);
|
||||
};
|
||||
|
||||
const togglePanelFocus = (panel: "edit" | "preview") => {
|
||||
setFocusedPanel(focusedPanel === panel ? null : panel);
|
||||
const toggleEditPanel = () => {
|
||||
setEditPanelCollapsed(!editPanelCollapsed);
|
||||
};
|
||||
|
||||
// 移动端控制栏
|
||||
const MobileControls = () => (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed bottom-6 right-6 flex gap-2 md:hidden z-50",
|
||||
"p-2 rounded-full",
|
||||
"dark:bg-neutral-900/80 dark:border dark:border-neutral-800 bg-white/80 border border-gray-200",
|
||||
"backdrop-blur-sm shadow-lg"
|
||||
)}
|
||||
>
|
||||
<motion.button
|
||||
className={cn(
|
||||
"p-3 rounded-full",
|
||||
"dark:hover:bg-neutral-800 hover:bg-gray-100"
|
||||
)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setShowMobileSidebar(true)}
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</motion.button>
|
||||
<motion.button
|
||||
className={cn(
|
||||
"p-3 rounded-full",
|
||||
"dark:hover:bg-neutral-800 hover:bg-gray-100"
|
||||
)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setIsMobilePreview(!isMobilePreview)}
|
||||
>
|
||||
{isMobilePreview ? (
|
||||
<Edit2 className="h-5 w-5" />
|
||||
) : (
|
||||
<Eye className="h-5 w-5" />
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
);
|
||||
const togglePreviewPanel = () => {
|
||||
setPreviewPanelCollapsed(!previewPanelCollapsed);
|
||||
};
|
||||
|
||||
// 移动端侧边栏
|
||||
const MobileSidebar = () => (
|
||||
<AnimatePresence>
|
||||
{showMobileSidebar && (
|
||||
<>
|
||||
<motion.div
|
||||
className="fixed inset-0 bg-black/50 z-50 md:hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setShowMobileSidebar(false)}
|
||||
/>
|
||||
<motion.div
|
||||
className={cn(
|
||||
"fixed top-0 left-0 h-full w-80 z-50 md:hidden",
|
||||
"dark:bg-neutral-900 bg-white"
|
||||
)}
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "spring", damping: 20 }}
|
||||
>
|
||||
<SidePanel />
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
const updateLayout = (sizes: number[]) => {
|
||||
setPanelSizes(sizes);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let newSizes = [];
|
||||
|
||||
// 侧边栏尺寸
|
||||
newSizes.push(sidePanelCollapsed ? 0 : 20);
|
||||
|
||||
// 编辑区尺寸
|
||||
if (editPanelCollapsed) {
|
||||
newSizes.push(0);
|
||||
} else {
|
||||
if (sidePanelCollapsed) {
|
||||
newSizes.push(50);
|
||||
} else {
|
||||
if (previewPanelCollapsed) {
|
||||
newSizes.push(80);
|
||||
} else {
|
||||
newSizes.push(32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预览区尺寸
|
||||
if (previewPanelCollapsed) {
|
||||
newSizes.push(0);
|
||||
} else {
|
||||
if (editPanelCollapsed && sidePanelCollapsed) {
|
||||
newSizes.push(100);
|
||||
} else {
|
||||
if (editPanelCollapsed) {
|
||||
newSizes.push(80);
|
||||
} else {
|
||||
newSizes.push(48);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 确保总和为 100
|
||||
const total = newSizes.reduce((a, b) => a + b, 0);
|
||||
if (total < 100) {
|
||||
const lastNonZeroIndex = newSizes
|
||||
.map((size, index) => ({ size, index }))
|
||||
.filter(({ size }) => size > 0)
|
||||
.pop()?.index;
|
||||
|
||||
if (lastNonZeroIndex !== undefined) {
|
||||
newSizes[lastNonZeroIndex] += 100 - total;
|
||||
}
|
||||
}
|
||||
updateLayout([...newSizes]);
|
||||
}, [sidePanelCollapsed, editPanelCollapsed, previewPanelCollapsed]);
|
||||
|
||||
return (
|
||||
<main
|
||||
@@ -277,7 +238,7 @@ export default function Home() {
|
||||
{/* 桌面端布局 */}
|
||||
<div className="hidden md:block h-[calc(100vh-64px)]">
|
||||
<ResizablePanelGroup
|
||||
key={layoutKey}
|
||||
key={panelSizes?.join("-")}
|
||||
direction="horizontal"
|
||||
className={cn(
|
||||
"h-full rounded-lg",
|
||||
@@ -291,8 +252,8 @@ export default function Home() {
|
||||
<ResizablePanel
|
||||
id="side-panel"
|
||||
order={1}
|
||||
defaultSize={panelSizes[0]}
|
||||
minSize={18}
|
||||
defaultSize={panelSizes?.[0]}
|
||||
minSize={20}
|
||||
className={cn(
|
||||
"dark:bg-neutral-900 dark:border-r dark:border-neutral-800"
|
||||
)}
|
||||
@@ -306,57 +267,50 @@ export default function Home() {
|
||||
)}
|
||||
|
||||
{/* 编辑面板 */}
|
||||
<ResizablePanel
|
||||
id="edit-panel"
|
||||
order={2}
|
||||
defaultSize={panelSizes[1]}
|
||||
className={cn(
|
||||
"dark:bg-neutral-900 dark:border-r dark:border-neutral-800"
|
||||
)}
|
||||
>
|
||||
<div className="h-full">
|
||||
<EditPanel />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<DragHandle />
|
||||
{!editPanelCollapsed && (
|
||||
<>
|
||||
<ResizablePanel
|
||||
id="edit-panel"
|
||||
order={2}
|
||||
defaultSize={panelSizes?.[1]}
|
||||
className={cn(
|
||||
"dark:bg-neutral-900 dark:border-r dark:border-neutral-800"
|
||||
)}
|
||||
>
|
||||
<div className="h-full">
|
||||
<EditPanel />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<DragHandle />
|
||||
</>
|
||||
)}
|
||||
{/* 预览面板 */}
|
||||
<ResizablePanel
|
||||
id="preview-panel"
|
||||
order={3}
|
||||
minSize={48}
|
||||
collapsible={false}
|
||||
defaultSize={panelSizes ? panelSizes[2] : LAYOUT_CONFIG.DEFAULT[2]}
|
||||
className="bg-gray-100"
|
||||
>
|
||||
<div className="h-full overflow-y-auto">
|
||||
<PreviewPanel />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
{!previewPanelCollapsed && (
|
||||
<ResizablePanel
|
||||
id="preview-panel"
|
||||
order={3}
|
||||
collapsible={false}
|
||||
defaultSize={panelSizes?.[2]}
|
||||
className="bg-gray-100"
|
||||
>
|
||||
<div className="h-full overflow-y-auto">
|
||||
<PreviewPanel
|
||||
sidePanelCollapsed={sidePanelCollapsed}
|
||||
editPanelCollapsed={editPanelCollapsed}
|
||||
previewPanelCollapsed={previewPanelCollapsed}
|
||||
toggleSidePanel={toggleSidePanel}
|
||||
toggleEditPanel={toggleEditPanel}
|
||||
/>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
|
||||
<LayoutControls
|
||||
sidePanelCollapsed={sidePanelCollapsed}
|
||||
focusedPanel={focusedPanel}
|
||||
toggleSidePanel={toggleSidePanel}
|
||||
togglePanelFocus={togglePanelFocus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 移动端布局 */}
|
||||
<div className="md:hidden h-[calc(100vh-64px)]">
|
||||
<AnimatePresence mode="wait">
|
||||
{isMobilePreview ? (
|
||||
<motion.div
|
||||
key="preview"
|
||||
initial={{ opacity: 0, x: 300 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 300 }}
|
||||
transition={{ type: "spring", damping: 20 }}
|
||||
className="h-full"
|
||||
>
|
||||
<PreviewPanel />
|
||||
</motion.div>
|
||||
) : (
|
||||
{previewPanelCollapsed ? (
|
||||
<motion.div
|
||||
key="edit"
|
||||
initial={{ opacity: 0, x: -300 }}
|
||||
@@ -367,12 +321,26 @@ export default function Home() {
|
||||
>
|
||||
<EditPanel />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="preview"
|
||||
initial={{ opacity: 0, x: 300 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 300 }}
|
||||
transition={{ type: "spring", damping: 20 }}
|
||||
className="h-full"
|
||||
>
|
||||
<PreviewPanel
|
||||
sidePanelCollapsed={sidePanelCollapsed}
|
||||
editPanelCollapsed={editPanelCollapsed}
|
||||
previewPanelCollapsed={previewPanelCollapsed}
|
||||
toggleSidePanel={toggleSidePanel}
|
||||
toggleEditPanel={toggleEditPanel}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<MobileControls />
|
||||
<MobileSidebar />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ import { cn } from "@/lib/utils";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface DockProps extends PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {
|
||||
interface DockProps
|
||||
extends PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Dock({ children, className, ...props }: DockProps) {
|
||||
// Convert children to array to handle them
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
|
||||
|
||||
// Find the index of TemplateSheet for splitting
|
||||
const templateSheetIndex = childrenArray.findIndex((child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
@@ -22,7 +23,9 @@ export function Dock({ children, className, ...props }: DockProps) {
|
||||
const content = trigger.props.children;
|
||||
if (React.isValidElement(content)) {
|
||||
const icon = content.props.children;
|
||||
return React.isValidElement(icon) && icon.type?.name === "TemplateSheet";
|
||||
return (
|
||||
React.isValidElement(icon) && icon.type?.name === "TemplateSheet"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +39,7 @@ export function Dock({ children, className, ...props }: DockProps) {
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-4 rounded-xl bg-white/[0.7] p-4 shadow-lg backdrop-blur-md dark:bg-slate-800/[0.7] dark:shadow-slate-900/20",
|
||||
"flex flex-col items-center gap-4 rounded-xl bg-white/[0.7] p-2 shadow-lg backdrop-blur-md dark:bg-slate-800/[0.7] dark:shadow-slate-900/20",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -95,7 +98,7 @@ export function DockIcon({ children, className, onClick }: DockIconProps) {
|
||||
whileHover={{ scale: 1.2 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={cn(
|
||||
"flex size-10 items-center justify-center rounded-lg bg-white text-neutral-700 shadow-sm transition-colors hover:bg-gray-100 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:shadow-slate-900/20",
|
||||
"flex size-8 items-center justify-center rounded-sm bg-white text-neutral-700 shadow-sm transition-colors hover:bg-gray-100 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:shadow-slate-900/20",
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { Edit2, PanelRightClose, PanelRightOpen } from "lucide-react";
|
||||
import { Dock, DockIcon } from "@/components/magicui/dock";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import TemplateSheet from "@/components/shared/TemplateSheet";
|
||||
import { GITHUB_REPO_URL } from "@/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type IconProps = React.HTMLAttributes<SVGElement>;
|
||||
|
||||
interface PreviewDockProps {
|
||||
sidePanelCollapsed: boolean;
|
||||
editPanelCollapsed: boolean;
|
||||
toggleSidePanel: () => void;
|
||||
toggleEditPanel: () => void;
|
||||
}
|
||||
|
||||
const Icons = {
|
||||
gitHub: (props: IconProps) => (
|
||||
<svg viewBox="0 0 438.549 438.549" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
export const PreviewDock = ({
|
||||
sidePanelCollapsed,
|
||||
editPanelCollapsed,
|
||||
toggleSidePanel,
|
||||
toggleEditPanel,
|
||||
}: PreviewDockProps) => {
|
||||
const handleGoGitHub = () => {
|
||||
window.open(GITHUB_REPO_URL, "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed top-1/2 right-3 transform -translate-y-1/2">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Dock>
|
||||
<DockIcon>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"flex cursor-pointer h-7 w-7 items-center justify-center rounded-lg",
|
||||
"hover:bg-gray-100/50 dark:hover:bg-neutral-800/50"
|
||||
)}
|
||||
>
|
||||
<TemplateSheet />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={10}>
|
||||
<p>切换模版</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DockIcon>
|
||||
<div className="w-full h-[1px] bg-gray-200" />
|
||||
<DockIcon>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={toggleSidePanel}
|
||||
className={cn(
|
||||
"flex h-[30px] w-[30px] items-center justify-center rounded-sm transition-all",
|
||||
"hover:bg-gray-100/50 dark:hover:bg-neutral-800/50",
|
||||
"active:scale-95",
|
||||
!sidePanelCollapsed && [
|
||||
"bg-primary text-primary-foreground",
|
||||
"hover:bg-primary/90 dark:hover:bg-primary/90",
|
||||
"shadow-sm",
|
||||
]
|
||||
)}
|
||||
>
|
||||
{sidePanelCollapsed && <PanelRightClose size={20} />}
|
||||
{!sidePanelCollapsed && <PanelRightOpen size={20} />}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={10}>
|
||||
<p>{sidePanelCollapsed ? "展开侧边栏" : "收起侧边栏"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DockIcon>
|
||||
<DockIcon>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={toggleEditPanel}
|
||||
className={cn(
|
||||
"flex h-[30px] w-[30px] items-center justify-center rounded-sm transition-all",
|
||||
"hover:bg-gray-100/50 dark:hover:bg-neutral-800/50",
|
||||
"active:scale-95",
|
||||
!editPanelCollapsed && [
|
||||
"bg-primary text-primary-foreground",
|
||||
"hover:bg-primary/90 dark:hover:bg-primary/90",
|
||||
"shadow-sm",
|
||||
]
|
||||
)}
|
||||
>
|
||||
<Edit2 size={20} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={10}>
|
||||
{editPanelCollapsed ? "展开编辑面板" : "收起编辑面板"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DockIcon>
|
||||
<div className="w-full h-[1px] bg-gray-200" />
|
||||
<DockIcon>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={handleGoGitHub}
|
||||
className={cn(
|
||||
"flex h-[20px] w-[20px] items-center justify-center rounded-lg transition-all",
|
||||
"hover:bg-gray-100/50 dark:hover:bg-neutral-800/50",
|
||||
"active:scale-95"
|
||||
)}
|
||||
>
|
||||
<Icons.gitHub />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={10}>
|
||||
<p>GitHub</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DockIcon>
|
||||
</Dock>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useState, useRef } from "react";
|
||||
import { throttle } from "lodash";
|
||||
import TemplateSheet from "@/components/shared/TemplateSheet";
|
||||
import { Dock, DockIcon } from "@/components/magicui/dock";
|
||||
import ResumeTemplateComponent from "../templates";
|
||||
import { DEFAULT_TEMPLATES, GITHUB_REPO_URL } from "@/config";
|
||||
import { DEFAULT_TEMPLATES } from "@/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import ResumeTemplateComponent from "../templates";
|
||||
import { PreviewDock } from "./PreviewDock";
|
||||
|
||||
export type IconProps = React.HTMLAttributes<SVGElement>;
|
||||
const getFontFamilyClass = (fontFamily: string) => {
|
||||
switch (fontFamily) {
|
||||
case "serif":
|
||||
return "font-serif";
|
||||
case "mono":
|
||||
return "font-mono";
|
||||
default:
|
||||
return "font-sans";
|
||||
}
|
||||
};
|
||||
|
||||
interface PageBreakLineProps {
|
||||
pageNumber: number;
|
||||
interface PreviewPanelProps {
|
||||
sidePanelCollapsed: boolean;
|
||||
editPanelCollapsed: boolean;
|
||||
previewPanelCollapsed: boolean;
|
||||
toggleSidePanel: () => void;
|
||||
toggleEditPanel: () => void;
|
||||
}
|
||||
|
||||
const PageBreakLine = React.memo(({ pageNumber }: PageBreakLineProps) => {
|
||||
const PageBreakLine = React.memo(({ pageNumber }: { pageNumber: number }) => {
|
||||
const { activeResume } = useResumeStore();
|
||||
const { globalSettings } = activeResume || {};
|
||||
if (!globalSettings?.pagePadding) return;
|
||||
@@ -62,18 +48,12 @@ const PageBreakLine = React.memo(({ pageNumber }: PageBreakLineProps) => {
|
||||
|
||||
PageBreakLine.displayName = "PageBreakLine";
|
||||
|
||||
const Icons = {
|
||||
gitHub: (props: IconProps) => (
|
||||
<svg viewBox="0 0 438.549 438.549" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
const PreviewPanel = () => {
|
||||
const PreviewPanel = ({
|
||||
sidePanelCollapsed,
|
||||
editPanelCollapsed,
|
||||
toggleSidePanel,
|
||||
toggleEditPanel,
|
||||
}: PreviewPanelProps) => {
|
||||
const { activeResume } = useResumeStore();
|
||||
const template = useMemo(() => {
|
||||
return (
|
||||
@@ -83,7 +63,6 @@ const PreviewPanel = () => {
|
||||
}, [activeResume?.templateId]);
|
||||
|
||||
const startRef = useRef<HTMLDivElement>(null);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const previewRef = React.useRef<HTMLDivElement>(null);
|
||||
const resumeContentRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -119,16 +98,12 @@ const PreviewPanel = () => {
|
||||
|
||||
if (!activeResume) return null;
|
||||
|
||||
const handleGoGitHub = () => {
|
||||
window.open(GITHUB_REPO_URL, "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="relative w-full h-full overflow-auto bg-gray-100"
|
||||
>
|
||||
<div className="py-4 px-4 min-h-screen flex justify-center scale-90 origin-top-left">
|
||||
<div className="py-4 ml-4 px-4 min-h-screen flex justify-center scale-90 origin-top-left">
|
||||
<div
|
||||
ref={startRef}
|
||||
className={cn(
|
||||
@@ -153,36 +128,12 @@ const PreviewPanel = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fixed top-1/2 right-6 transform -translate-y-1/2">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Dock>
|
||||
<DockIcon>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="cursor-pointer" onClick={handleGoGitHub}>
|
||||
<Icons.gitHub className="size-6" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={10}>
|
||||
<p>Go GitHub</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DockIcon>
|
||||
<DockIcon>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="cursor-pointer">
|
||||
<TemplateSheet></TemplateSheet>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={10}>
|
||||
<p>切换模版</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DockIcon>
|
||||
</Dock>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<PreviewDock
|
||||
sidePanelCollapsed={sidePanelCollapsed}
|
||||
editPanelCollapsed={editPanelCollapsed}
|
||||
toggleSidePanel={toggleSidePanel}
|
||||
toggleEditPanel={toggleEditPanel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ const TemplateSheet = () => {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<PanelsLeftBottom size={24} />
|
||||
<PanelsLeftBottom size={20} />
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-1/2 sm:max-w-1/2">
|
||||
<SheetHeader>
|
||||
|
||||
Reference in New Issue
Block a user