refactor: share floating sidebar shell
This commit is contained in:
parent
3c45bdfef9
commit
3f4d850815
@ -28,6 +28,12 @@ import {
|
||||
parseMindMapFileToDrawnixMind,
|
||||
} from "@/lib/flow-board/mind-map-import";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import {
|
||||
FloatingLogoPill,
|
||||
FloatingSidebarAction,
|
||||
FloatingSidebarRail,
|
||||
FloatingSidebarShell,
|
||||
} from "@/components/home/floating-sidebar-shell";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
@ -46,12 +52,6 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type ClipboardEvent,
|
||||
@ -121,10 +121,6 @@ const FLOW_BOARD_IMAGE_UPLOAD_TYPES = new Set([
|
||||
"image/png",
|
||||
"image/webp",
|
||||
]);
|
||||
const FLOW_BOARD_SIDEBAR_BUTTON_CLASS =
|
||||
"flex h-10 w-10 items-center justify-center rounded-full text-[#4f566b] transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-45";
|
||||
const FLOW_BOARD_SIDEBAR_BUTTON_ACTIVE_CLASS =
|
||||
"bg-[#111827] text-white shadow-sm hover:bg-[#1f2937] hover:text-white";
|
||||
const FLOW_BOARD_MARKER_GROUPS: FlowBoardMarkerGroup[] = [
|
||||
{
|
||||
id: "tag",
|
||||
@ -375,16 +371,6 @@ function normalizeSnapshot(value: unknown): FlowBoardSnapshot {
|
||||
};
|
||||
}
|
||||
|
||||
function formatSavedAt(value: Date | null) {
|
||||
if (!value) {
|
||||
return "数据库自动保存";
|
||||
}
|
||||
return `已保存 ${value.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}`;
|
||||
}
|
||||
|
||||
function formatFileUpdatedAt(value?: string | null) {
|
||||
if (!value) return "刚刚";
|
||||
const date = new Date(value);
|
||||
@ -564,7 +550,7 @@ export function DrawnixWorkspace({
|
||||
const [isFilePanelOpen, setIsFilePanelOpen] = useState(false);
|
||||
const [isFileListLoading, setIsFileListLoading] = useState(false);
|
||||
const [isSavingNow, setIsSavingNow] = useState(false);
|
||||
const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);
|
||||
const [, setLastSavedAt] = useState<Date | null>(null);
|
||||
const snapshotRef = useRef<FlowBoardSnapshot>({ children: [] });
|
||||
const currentFileIdRef = useRef<string | null>(null);
|
||||
const currentTitleRef = useRef("流程白板");
|
||||
@ -1328,15 +1314,6 @@ export function DrawnixWorkspace({
|
||||
[cloudFiles, hydrateCloudFile, updateBrowserFileParam]
|
||||
);
|
||||
|
||||
const handleTitleCommit = useCallback(async () => {
|
||||
const normalizedTitle = currentTitle.trim() || "未命名流程白板";
|
||||
if (normalizedTitle !== currentTitle) {
|
||||
setCurrentTitle(normalizedTitle);
|
||||
currentTitleRef.current = normalizedTitle;
|
||||
}
|
||||
await flushPendingSave(false);
|
||||
}, [currentTitle, flushPendingSave]);
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<div className="flex h-svh w-full items-center justify-center bg-[#f7f8fb] text-[#697386]">
|
||||
@ -1383,154 +1360,56 @@ export function DrawnixWorkspace({
|
||||
ref={mindMapFileInputRef}
|
||||
type="file"
|
||||
/>
|
||||
<aside
|
||||
className="pointer-events-none absolute top-4 left-4 z-20 flex flex-col items-center gap-2 lg:top-6 lg:left-6"
|
||||
data-flow-board-shell-control="true"
|
||||
<FloatingSidebarShell
|
||||
className="absolute top-4 left-4 z-20 lg:top-6 lg:left-6"
|
||||
flowBoardControl
|
||||
>
|
||||
<TooltipProvider delayDuration={120}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="pointer-events-auto flex h-[52px] w-[52px] items-center justify-center gap-3 rounded-full border border-black/10 bg-white px-2.5 text-left shadow transition hover:bg-black/[0.03] hover:shadow-md lg:w-[164px] lg:justify-start lg:px-4"
|
||||
onClick={() => {
|
||||
window.location.href = "/";
|
||||
}}
|
||||
title="返回首页"
|
||||
type="button"
|
||||
>
|
||||
{displayLogo ? (
|
||||
<img
|
||||
alt={siteName}
|
||||
className="h-8 w-8 rounded-full object-cover"
|
||||
src={displayLogo}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-black font-bold text-white text-sm">
|
||||
{siteName.slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="hidden min-w-0 truncate font-semibold text-[#111827] text-[15px] leading-none tracking-tight lg:block">
|
||||
{siteName}
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">返回首页</TooltipContent>
|
||||
</Tooltip>
|
||||
<FloatingLogoPill
|
||||
onClick={() => {
|
||||
window.location.href = "/";
|
||||
}}
|
||||
siteLogo={displayLogo}
|
||||
siteName={siteName}
|
||||
tooltip="返回首页"
|
||||
/>
|
||||
|
||||
<div className="pointer-events-auto hidden w-[164px] rounded-[22px] border border-black/10 bg-white px-3 py-2 shadow lg:block">
|
||||
<div className="mb-1 px-1 font-medium text-[#7a8297] text-[10px] uppercase tracking-[0.18em]">
|
||||
流程白板
|
||||
</div>
|
||||
<input
|
||||
aria-label="流程白板文件名"
|
||||
className="h-8 w-full rounded-xl border border-transparent bg-transparent px-1 font-semibold text-[#111827] text-sm tracking-wide outline-none transition focus:border-black/10 focus:bg-[#f7f8fb]"
|
||||
onBlur={handleTitleCommit}
|
||||
onChange={(event) => setCurrentTitle(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
value={currentTitle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-auto flex max-h-[calc(100vh-148px)] w-[56px] flex-col items-center gap-1 overflow-y-auto rounded-[28px] border border-black/10 bg-white px-1.5 py-2.5 shadow lg:max-h-[calc(100vh-236px)]">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={FLOW_BOARD_SIDEBAR_BUTTON_CLASS}
|
||||
disabled={isSavingNow}
|
||||
onClick={() => flushPendingSave(true)}
|
||||
title="保存云端"
|
||||
type="button"
|
||||
>
|
||||
{isSavingNow ? (
|
||||
<Loader2 className="h-[18px] w-[18px] animate-spin" />
|
||||
) : (
|
||||
<Save className="h-[18px] w-[18px]" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">保存云端</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={FLOW_BOARD_SIDEBAR_BUTTON_CLASS}
|
||||
onClick={handleOpenFilePanel}
|
||||
title="云文件"
|
||||
type="button"
|
||||
>
|
||||
<FolderOpen className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">云文件</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={FLOW_BOARD_SIDEBAR_BUTTON_CLASS}
|
||||
disabled={isSavingNow || isImportingMindMap}
|
||||
onClick={() => mindMapFileInputRef.current?.click()}
|
||||
title="导入"
|
||||
type="button"
|
||||
>
|
||||
{isImportingMindMap ? (
|
||||
<Loader2 className="h-[18px] w-[18px] animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-[18px] w-[18px]" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">导入</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={`${FLOW_BOARD_SIDEBAR_BUTTON_CLASS} ${
|
||||
isMarkerPanelOpen ? FLOW_BOARD_SIDEBAR_BUTTON_ACTIVE_CLASS : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
setIsFilePanelOpen(false);
|
||||
setIsMarkerPanelOpen((open) => !open);
|
||||
refreshMarkerSelectionState();
|
||||
}}
|
||||
title="标记"
|
||||
type="button"
|
||||
>
|
||||
<Tag className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">标记</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={FLOW_BOARD_SIDEBAR_BUTTON_CLASS}
|
||||
onClick={handleCreateCloudFile}
|
||||
title="新建"
|
||||
type="button"
|
||||
>
|
||||
<Plus className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">新建</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-auto hidden max-w-[164px] items-center gap-1.5 rounded-full border border-black/10 bg-white px-3 py-1.5 text-[#7a8297] shadow lg:flex">
|
||||
<Save className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate font-medium text-[11px]">
|
||||
{formatSavedAt(lastSavedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</aside>
|
||||
<FloatingSidebarRail className="max-h-[calc(100vh-88px)]">
|
||||
<FloatingSidebarAction
|
||||
disabled={isSavingNow}
|
||||
icon={Save}
|
||||
label="保存云端"
|
||||
loading={isSavingNow}
|
||||
onClick={() => flushPendingSave(true)}
|
||||
/>
|
||||
<FloatingSidebarAction
|
||||
icon={FolderOpen}
|
||||
label="云文件"
|
||||
onClick={handleOpenFilePanel}
|
||||
/>
|
||||
<FloatingSidebarAction
|
||||
disabled={isSavingNow || isImportingMindMap}
|
||||
icon={Upload}
|
||||
label="导入"
|
||||
loading={isImportingMindMap}
|
||||
onClick={() => mindMapFileInputRef.current?.click()}
|
||||
/>
|
||||
<FloatingSidebarAction
|
||||
active={isMarkerPanelOpen}
|
||||
icon={Tag}
|
||||
label="标记"
|
||||
onClick={() => {
|
||||
setIsFilePanelOpen(false);
|
||||
setIsMarkerPanelOpen((open) => !open);
|
||||
refreshMarkerSelectionState();
|
||||
}}
|
||||
/>
|
||||
<FloatingSidebarAction
|
||||
icon={Plus}
|
||||
label="新建"
|
||||
onClick={handleCreateCloudFile}
|
||||
/>
|
||||
</FloatingSidebarRail>
|
||||
</FloatingSidebarShell>
|
||||
|
||||
{isFilePanelOpen ? (
|
||||
<aside
|
||||
|
||||
142
components/home/floating-sidebar-shell.tsx
Normal file
142
components/home/floating-sidebar-shell.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Loader2, type LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type FloatingSidebarShellProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
flowBoardControl?: boolean;
|
||||
};
|
||||
|
||||
export function FloatingSidebarShell({
|
||||
children,
|
||||
className,
|
||||
flowBoardControl = false,
|
||||
}: FloatingSidebarShellProps) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"pointer-events-none flex flex-col items-center gap-2",
|
||||
className
|
||||
)}
|
||||
data-flow-board-shell-control={flowBoardControl ? "true" : undefined}
|
||||
>
|
||||
<TooltipProvider delayDuration={120}>{children}</TooltipProvider>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
type FloatingLogoPillProps = {
|
||||
siteName: string;
|
||||
siteLogo?: string | null;
|
||||
tooltip?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export function FloatingLogoPill({
|
||||
siteName,
|
||||
siteLogo,
|
||||
tooltip = siteName,
|
||||
onClick,
|
||||
}: FloatingLogoPillProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="pointer-events-auto flex h-[52px] w-[52px] items-center justify-center gap-3 rounded-full border border-black/10 bg-white px-2.5 text-left shadow transition hover:bg-black/[0.03] hover:shadow-md lg:w-[164px] lg:justify-start lg:px-4"
|
||||
onClick={onClick}
|
||||
title={tooltip}
|
||||
type="button"
|
||||
>
|
||||
{siteLogo ? (
|
||||
<img
|
||||
alt={siteName}
|
||||
className="h-8 w-8 rounded-full object-cover"
|
||||
src={siteLogo}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-black font-bold text-white text-sm">
|
||||
{siteName.slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="hidden min-w-0 truncate font-semibold text-[#111827] text-[15px] leading-none tracking-tight lg:block">
|
||||
{siteName}
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
type FloatingSidebarRailProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function FloatingSidebarRail({
|
||||
children,
|
||||
className,
|
||||
}: FloatingSidebarRailProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto flex w-[56px] flex-col items-center gap-1 overflow-y-auto rounded-[28px] border border-black/10 bg-white px-1.5 py-2.5 shadow",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type FloatingSidebarActionProps = {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export function FloatingSidebarAction({
|
||||
label,
|
||||
icon: Icon,
|
||||
active = false,
|
||||
disabled = false,
|
||||
loading = false,
|
||||
onClick,
|
||||
}: FloatingSidebarActionProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"flex h-10 w-10 items-center justify-center rounded-full text-[#4f566b] transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-45",
|
||||
active &&
|
||||
"bg-[#111827] text-white shadow-sm hover:bg-[#1f2937] hover:text-white"
|
||||
)}
|
||||
disabled={disabled || loading}
|
||||
onClick={onClick}
|
||||
title={label}
|
||||
type="button"
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="h-[18px] w-[18px] animate-spin" />
|
||||
) : (
|
||||
<Icon className="h-[18px] w-[18px]" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@ -56,6 +56,7 @@ import {
|
||||
import type { MembershipConfig } from "@/lib/membership/config";
|
||||
import { ImageAnnotator } from "@/components/image-annotator";
|
||||
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
|
||||
import { FloatingSidebarRail } from "@/components/home/floating-sidebar-shell";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
|
||||
type HomeUser = {
|
||||
@ -1432,7 +1433,7 @@ export function LovartHome({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">新建项目</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex max-h-[calc(100vh-172px)] w-[56px] flex-col items-center gap-1 overflow-y-auto rounded-[28px] border border-black/10 bg-white px-1.5 py-2.5 shadow">
|
||||
<FloatingSidebarRail className="max-h-[calc(100vh-172px)]">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
@ -1711,7 +1712,7 @@ export function LovartHome({
|
||||
<TooltipContent side="right">系统设置</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</FloatingSidebarRail>
|
||||
</TooltipProvider>
|
||||
</aside>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user