592 lines
20 KiB
TypeScript
592 lines
20 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
|
import { toast } from "@/lib/toast/sonner";
|
|
import {
|
|
IconLayout,
|
|
IconCheck,
|
|
IconDeviceFloppy,
|
|
IconColumns3,
|
|
IconFileText,
|
|
IconDeviceDesktop,
|
|
IconBrandVscode,
|
|
IconRestore,
|
|
IconTrash,
|
|
} from "@tabler/icons-react";
|
|
import { Button } from "@kandev/ui/button";
|
|
import { Input } from "@kandev/ui/input";
|
|
import { Label } from "@kandev/ui/label";
|
|
import { Checkbox } from "@kandev/ui/checkbox";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@kandev/ui/dialog";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@kandev/ui/dropdown-menu";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
|
import { Badge } from "@kandev/ui/badge";
|
|
import { useDockviewStore } from "@/lib/state/dockview-store";
|
|
import {
|
|
BUILT_IN_LAYOUT_PROFILES,
|
|
createLayoutProfileId,
|
|
getBuiltInLayoutOverride,
|
|
getLayoutProfileCompatibility,
|
|
isBuiltInLayoutOverride,
|
|
type BuiltInLayoutProfileId,
|
|
type CreateLayoutProfileInput,
|
|
} from "@/lib/layout/layout-profiles";
|
|
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
|
import type { SavedLayout } from "@/lib/types/http";
|
|
import { useTaskSessions } from "@/hooks/use-task-sessions";
|
|
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
|
import { useConfirmationBoundary } from "@/components/confirmation/mobile-action-confirmation";
|
|
import { InlineConfirmActions } from "@/components/confirmation/inline-confirm-actions";
|
|
import { resolveLayoutApplySessionIds } from "./layout-preset-selector-session-ids";
|
|
import { SavedLayoutDeleteConfirmation } from "./saved-layout-delete-confirmation";
|
|
import { useSavedLayoutMutations } from "./use-saved-layout-mutations";
|
|
import { useTranslation } from "react-i18next";
|
|
import { t } from "@/lib/i18n";
|
|
|
|
type PresetOption = {
|
|
id: BuiltInLayoutProfileId;
|
|
label: string;
|
|
description: string;
|
|
icon: React.ElementType;
|
|
};
|
|
|
|
const PRESET_ICONS: Record<BuiltInLayoutProfileId, React.ElementType> = {
|
|
default: IconColumns3,
|
|
plan: IconFileText,
|
|
preview: IconDeviceDesktop,
|
|
vscode: IconBrandVscode,
|
|
};
|
|
|
|
const BUILT_IN_PRESETS: PresetOption[] = BUILT_IN_LAYOUT_PROFILES.map((profile) => ({
|
|
id: profile.id,
|
|
label: profile.name,
|
|
description: profile.description,
|
|
icon: PRESET_ICONS[profile.id],
|
|
}));
|
|
|
|
function mutationError(error: unknown): string {
|
|
return error instanceof Error ? error.message : t("task:pleaseTryAgain");
|
|
}
|
|
|
|
function isSavedLayoutConfirmationTarget(target: EventTarget | null): boolean {
|
|
return target instanceof Element && target.closest("[data-confirmation-boundary]") !== null;
|
|
}
|
|
|
|
function SaveLayoutDialog({
|
|
open,
|
|
onOpenChange,
|
|
onSave,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSave: (input: CreateLayoutProfileInput) => Promise<void>;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [name, setName] = useState("");
|
|
const [isDefault, setIsDefault] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const captureCurrentLayout = useDockviewStore((s) => s.captureCurrentLayout);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return;
|
|
setSaving(true);
|
|
try {
|
|
const layout = captureCurrentLayout();
|
|
await onSave({
|
|
id: createLayoutProfileId(),
|
|
name: trimmed,
|
|
isDefault,
|
|
layout,
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
setName("");
|
|
setIsDefault(false);
|
|
onOpenChange(false);
|
|
} catch (error) {
|
|
toast.error(t("task:failedToSaveLayout"), { description: mutationError(error) });
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [name, isDefault, captureCurrentLayout, onOpenChange, onSave, t]);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-sm">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("task:saveCurrentLayout")}</DialogTitle>
|
|
<DialogDescription>{t("task:saveYourCurrentPanelArrangementAs")}</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="layout-name">{t("task:name")}</Label>
|
|
<Input
|
|
id="layout-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder={t("task:myLayout")}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="layout-default"
|
|
checked={isDefault}
|
|
onCheckedChange={(checked) => setIsDefault(checked === true)}
|
|
/>
|
|
<Label htmlFor="layout-default" className="text-sm font-normal cursor-pointer">
|
|
{t("task:setAsDefaultLayout")}
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="cursor-pointer"
|
|
onClick={() => onOpenChange(false)}
|
|
>
|
|
{t("common:cancel")}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
className="cursor-pointer"
|
|
onClick={handleSave}
|
|
disabled={!name.trim() || saving}
|
|
>
|
|
{saving ? t("task:saving") : t("common:save")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function SavedLayoutItems({
|
|
layouts,
|
|
onApply,
|
|
isFinePointer,
|
|
confirmingDeleteId,
|
|
onDelete,
|
|
onCancelDelete,
|
|
onCloseDelete,
|
|
onConfirmDelete,
|
|
}: {
|
|
layouts: SavedLayout[];
|
|
onApply: (layout: SavedLayout) => void | Promise<void>;
|
|
isFinePointer: boolean;
|
|
confirmingDeleteId: string | null;
|
|
onDelete: (layout: SavedLayout, anchor: HTMLElement) => void;
|
|
onCancelDelete: () => void;
|
|
onCloseDelete: () => void;
|
|
onConfirmDelete: (layoutId: string) => void | Promise<void>;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
if (layouts.length === 0) {
|
|
return (
|
|
<div className="px-2 py-1.5 text-xs text-muted-foreground">{t("task:noSavedLayouts")}</div>
|
|
);
|
|
}
|
|
return layouts.map((layout) => {
|
|
const isConfirming = !isFinePointer && confirmingDeleteId === layout.id;
|
|
if (isConfirming) {
|
|
const title = t("task:deleteLayoutConfirm", { name: layout.name });
|
|
const description = layout.is_default
|
|
? t("task:theBuiltInDefaultLayoutWill")
|
|
: t("task:thisSavedLayoutWillBePermanently");
|
|
return (
|
|
<div key={layout.id} className="min-w-0" role="presentation">
|
|
<InlineConfirmActions
|
|
density="touch"
|
|
testId="layout-saved-delete-inline-confirmation"
|
|
ariaLabel={title}
|
|
description={description}
|
|
cancelLabel={t("common:cancel")}
|
|
confirmLabel={t("task:delete")}
|
|
confirmAriaLabel={t("task:delete2", { name: layout.name })}
|
|
confirmTestId="layout-saved-delete-confirm"
|
|
onCancel={onCancelDelete}
|
|
onClose={onCloseDelete}
|
|
onConfirm={() => onConfirmDelete(layout.id)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div key={layout.id} className="flex min-w-0 items-stretch" role="presentation">
|
|
<DropdownMenuItem
|
|
className="min-w-0 flex-1 cursor-pointer"
|
|
onSelect={() => void onApply(layout)}
|
|
>
|
|
<SavedLayoutLabel layout={layout} />
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
className="min-h-11 min-w-11 shrink-0 cursor-pointer justify-center px-2 text-destructive/60 focus:text-destructive md:min-h-7 md:min-w-7"
|
|
aria-label={t("task:delete2", { name: layout.name })}
|
|
data-testid="layout-saved-delete"
|
|
data-layout-id={layout.id}
|
|
onSelect={(event) => {
|
|
event.preventDefault();
|
|
onDelete(layout, event.currentTarget as HTMLElement);
|
|
}}
|
|
>
|
|
<IconTrash className="h-3.5 w-3.5" />
|
|
</DropdownMenuItem>
|
|
</div>
|
|
);
|
|
});
|
|
}
|
|
|
|
function SavedLayoutLabel({ layout }: { layout: SavedLayout }) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<>
|
|
<IconCheck className="mr-2 h-3.5 w-3.5 shrink-0 opacity-0" />
|
|
<span className="flex-1 truncate text-xs">{layout.name}</span>
|
|
{layout.is_default && (
|
|
<Badge variant="secondary" className="ml-1 px-1 py-0 text-[9px]">
|
|
{t("common:default")}
|
|
</Badge>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/** Built-in preset menu items. Code-defined presets reset widths; customized
|
|
* built-ins restore the dimensions saved in their override. */
|
|
function BuiltInPresetItems({ onApply }: { onApply: (presetId: BuiltInLayoutProfileId) => void }) {
|
|
return BUILT_IN_PRESETS.map((preset) => {
|
|
const Icon = preset.icon;
|
|
return (
|
|
<DropdownMenuItem
|
|
key={preset.id}
|
|
data-testid="layout-preset-item"
|
|
data-preset-id={preset.id}
|
|
onClick={() => onApply(preset.id)}
|
|
className="cursor-pointer"
|
|
>
|
|
<Icon className="h-4 w-4 mr-2 shrink-0" />
|
|
<div className="flex flex-col min-w-0">
|
|
<span className="text-xs font-medium">{preset.label}</span>
|
|
<span className="text-[10px] text-muted-foreground truncate">{preset.description}</span>
|
|
</div>
|
|
</DropdownMenuItem>
|
|
);
|
|
});
|
|
}
|
|
|
|
function useApplySavedLayout() {
|
|
const applyCustomLayout = useDockviewStore((s) => s.applyCustomLayout);
|
|
const activeTaskId = useAppStore((s) => s.tasks.activeTaskId);
|
|
const activeSessionId = useAppStore((s) => s.tasks.activeSessionId);
|
|
const appStore = useAppStoreApi();
|
|
const { isLoaded: taskSessionsLoaded, loadSessions } = useTaskSessions(activeTaskId);
|
|
|
|
return useCallback(
|
|
async (layout: SavedLayout) => {
|
|
const sessionIds = await resolveLayoutApplySessionIds({
|
|
activeTaskId,
|
|
activeSessionId,
|
|
sessionsLoaded: taskSessionsLoaded,
|
|
loadSessions,
|
|
getSessionsForTask: (taskId) =>
|
|
appStore.getState().taskSessionsByTask.itemsByTaskId[taskId] ?? [],
|
|
});
|
|
applyCustomLayout(
|
|
{
|
|
id: layout.id,
|
|
name: layout.name,
|
|
isDefault: layout.is_default,
|
|
layout: layout.layout,
|
|
createdAt: layout.created_at,
|
|
},
|
|
{ activeSessionId, sessionIds },
|
|
);
|
|
},
|
|
[activeSessionId, activeTaskId, appStore, applyCustomLayout, loadSessions, taskSessionsLoaded],
|
|
);
|
|
}
|
|
|
|
function useApplyBuiltInLayout(
|
|
savedLayouts: SavedLayout[],
|
|
applySavedLayout: (layout: SavedLayout) => Promise<void>,
|
|
) {
|
|
const applyBuiltInPreset = useDockviewStore((s) => s.applyBuiltInPreset);
|
|
return useCallback(
|
|
(presetId: BuiltInLayoutProfileId) => {
|
|
const override = getBuiltInLayoutOverride(savedLayouts, presetId);
|
|
if (override && getLayoutProfileCompatibility(override).status === "editable") {
|
|
void applySavedLayout(override);
|
|
return;
|
|
}
|
|
applyBuiltInPreset(presetId, true);
|
|
},
|
|
[applyBuiltInPreset, applySavedLayout, savedLayouts],
|
|
);
|
|
}
|
|
|
|
type PresetDropdownProps = {
|
|
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
onCloseAutoFocus: (event: Event) => void;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
deleteMenuRef: RefObject<HTMLDivElement | null>;
|
|
tooltipOpen: boolean;
|
|
onTooltipOpenChange: (open: boolean) => void;
|
|
resetLayout: () => void;
|
|
savedLayouts: SavedLayout[];
|
|
isFinePointer: boolean;
|
|
onApplyCustom: (layout: SavedLayout) => void | Promise<void>;
|
|
onApplyBuiltIn: (presetId: BuiltInLayoutProfileId) => void;
|
|
confirmingDeleteId: string | null;
|
|
onDelete: (layout: SavedLayout, anchor: HTMLElement) => void;
|
|
onCancelDelete: () => void;
|
|
onCloseDelete: () => void;
|
|
onConfirmDelete: (layoutId: string) => void | Promise<void>;
|
|
onSaveLayout: () => void;
|
|
};
|
|
|
|
function PresetDropdown({
|
|
triggerRef,
|
|
onCloseAutoFocus,
|
|
open,
|
|
onOpenChange,
|
|
deleteMenuRef,
|
|
tooltipOpen,
|
|
onTooltipOpenChange,
|
|
resetLayout,
|
|
savedLayouts,
|
|
isFinePointer,
|
|
onApplyCustom,
|
|
onApplyBuiltIn,
|
|
confirmingDeleteId,
|
|
onDelete,
|
|
onCancelDelete,
|
|
onCloseDelete,
|
|
onConfirmDelete,
|
|
onSaveLayout,
|
|
}: PresetDropdownProps) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
|
<Tooltip open={tooltipOpen} onOpenChange={onTooltipOpenChange}>
|
|
<TooltipTrigger asChild>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
ref={triggerRef}
|
|
size="sm"
|
|
variant="outline"
|
|
className="cursor-pointer px-2"
|
|
data-testid="layout-preset-trigger"
|
|
aria-label={t("task:layoutPresets")}
|
|
>
|
|
<IconLayout className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">{t("task:layoutPresets")}</TooltipContent>
|
|
</Tooltip>
|
|
<DropdownMenuContent
|
|
onCloseAutoFocus={onCloseAutoFocus}
|
|
ref={deleteMenuRef}
|
|
align="end"
|
|
className="w-60"
|
|
onFocusOutside={(event) => {
|
|
if (confirmingDeleteId && isSavedLayoutConfirmationTarget(event.target)) {
|
|
event.preventDefault();
|
|
}
|
|
}}
|
|
onInteractOutside={(event) => {
|
|
if (confirmingDeleteId && isSavedLayoutConfirmationTarget(event.target)) {
|
|
event.preventDefault();
|
|
}
|
|
}}
|
|
>
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel className="text-xs">{t("task:presets")}</DropdownMenuLabel>
|
|
<BuiltInPresetItems onApply={onApplyBuiltIn} />
|
|
</DropdownMenuGroup>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
data-testid="layout-reset-item"
|
|
onClick={resetLayout}
|
|
className="cursor-pointer"
|
|
>
|
|
<IconRestore className="h-4 w-4 mr-2 shrink-0" />
|
|
<span className="text-xs">{t("task:resetLayout")}</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel className="text-xs">{t("task:savedLayouts")}</DropdownMenuLabel>
|
|
<SavedLayoutItems
|
|
layouts={savedLayouts.filter((layout) => !isBuiltInLayoutOverride(layout))}
|
|
onApply={onApplyCustom}
|
|
isFinePointer={isFinePointer}
|
|
confirmingDeleteId={confirmingDeleteId}
|
|
onDelete={onDelete}
|
|
onCancelDelete={onCancelDelete}
|
|
onCloseDelete={onCloseDelete}
|
|
onConfirmDelete={onConfirmDelete}
|
|
/>
|
|
</DropdownMenuGroup>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={onSaveLayout} className="cursor-pointer">
|
|
<IconDeviceFloppy className="h-4 w-4 mr-2 shrink-0" />
|
|
<span className="text-xs">{t("task:saveCurrentLayout2")}</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
function usePresetDropdownVisibility(onClose: () => void) {
|
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
const [tooltipOpen, setTooltipOpen] = useState(false);
|
|
const recentlyClosedRef = useRef(false);
|
|
const recentlyClosedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const handleDropdownOpenChange = useCallback(
|
|
(open: boolean) => {
|
|
setDropdownOpen(open);
|
|
if (!open) {
|
|
onClose();
|
|
recentlyClosedRef.current = true;
|
|
setTooltipOpen(false);
|
|
if (recentlyClosedTimerRef.current !== null) clearTimeout(recentlyClosedTimerRef.current);
|
|
recentlyClosedTimerRef.current = setTimeout(() => {
|
|
recentlyClosedRef.current = false;
|
|
recentlyClosedTimerRef.current = null;
|
|
}, 200);
|
|
}
|
|
},
|
|
[onClose],
|
|
);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
if (recentlyClosedTimerRef.current !== null) clearTimeout(recentlyClosedTimerRef.current);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const handleTooltipOpenChange = useCallback(
|
|
(open: boolean) => {
|
|
if (open && (dropdownOpen || recentlyClosedRef.current)) return;
|
|
setTooltipOpen(open);
|
|
},
|
|
[dropdownOpen],
|
|
);
|
|
|
|
return { dropdownOpen, tooltipOpen, handleDropdownOpenChange, handleTooltipOpenChange };
|
|
}
|
|
|
|
export function LayoutPresetSelector() {
|
|
const { t } = useTranslation();
|
|
const { isFinePointer, isMobile } = useResponsiveBreakpoint();
|
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
|
const [deleteCandidate, setDeleteCandidate] = useState<SavedLayout | null>(null);
|
|
const cancelDelete = useCallback(() => setDeleteCandidate(null), []);
|
|
useConfirmationBoundary(deleteCandidate !== null, deleteCandidate?.id ?? "", cancelDelete);
|
|
const deleteAnchorRef = useRef<HTMLElement>(null);
|
|
const deleteMenuRef = useRef<HTMLDivElement>(null);
|
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
const pendingTarget = useRef<SavedLayout | null>(null);
|
|
const resetLayout = useDockviewStore((s) => s.resetLayout);
|
|
const savedLayouts = useAppStore((s) => s.userSettings.savedLayouts);
|
|
const handleApplyCustom = useApplySavedLayout();
|
|
const handleApplyBuiltIn = useApplyBuiltInLayout(savedLayouts, handleApplyCustom);
|
|
const { saveLayout, deleteLayout } = useSavedLayoutMutations();
|
|
const { dropdownOpen, tooltipOpen, handleDropdownOpenChange, handleTooltipOpenChange } =
|
|
usePresetDropdownVisibility(cancelDelete);
|
|
const beginDelete = useCallback(
|
|
(layout: SavedLayout, anchor: HTMLElement) => {
|
|
if (isMobile) {
|
|
pendingTarget.current = layout;
|
|
handleDropdownOpenChange(false);
|
|
return;
|
|
}
|
|
deleteAnchorRef.current = anchor;
|
|
setDeleteCandidate(layout);
|
|
},
|
|
[isMobile, handleDropdownOpenChange],
|
|
);
|
|
|
|
const closeDelete = useCallback(() => {
|
|
setDeleteCandidate(null);
|
|
handleDropdownOpenChange(false);
|
|
}, [handleDropdownOpenChange]);
|
|
|
|
const confirmDeleteLayout = useCallback(
|
|
async (layoutId: string) => {
|
|
handleDropdownOpenChange(false);
|
|
try {
|
|
await deleteLayout(layoutId);
|
|
} catch (error) {
|
|
toast.error(t("task:failedToDeleteLayout"), { description: mutationError(error) });
|
|
}
|
|
},
|
|
[deleteLayout, handleDropdownOpenChange, t],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<PresetDropdown
|
|
triggerRef={triggerRef}
|
|
onCloseAutoFocus={(event) => {
|
|
const target = pendingTarget.current;
|
|
pendingTarget.current = null;
|
|
if (!target) return;
|
|
event.preventDefault();
|
|
if (savedLayouts.some((layout) => layout.id === target.id)) setDeleteCandidate(target);
|
|
}}
|
|
open={dropdownOpen}
|
|
onOpenChange={handleDropdownOpenChange}
|
|
deleteMenuRef={deleteMenuRef}
|
|
tooltipOpen={!isMobile && tooltipOpen}
|
|
onTooltipOpenChange={handleTooltipOpenChange}
|
|
resetLayout={resetLayout}
|
|
savedLayouts={savedLayouts}
|
|
isFinePointer={isMobile || isFinePointer}
|
|
onApplyCustom={handleApplyCustom}
|
|
onApplyBuiltIn={handleApplyBuiltIn}
|
|
confirmingDeleteId={deleteCandidate?.id ?? null}
|
|
onDelete={beginDelete}
|
|
onCancelDelete={cancelDelete}
|
|
onCloseDelete={closeDelete}
|
|
onConfirmDelete={confirmDeleteLayout}
|
|
onSaveLayout={() => setSaveDialogOpen(true)}
|
|
/>
|
|
<SaveLayoutDialog
|
|
open={saveDialogOpen}
|
|
onOpenChange={setSaveDialogOpen}
|
|
onSave={saveLayout}
|
|
/>
|
|
<SavedLayoutDeleteConfirmation
|
|
layout={deleteCandidate}
|
|
open={(isMobile || isFinePointer) && deleteCandidate !== null}
|
|
anchorRef={isMobile ? triggerRef : deleteAnchorRef}
|
|
focusBoundaryRef={deleteMenuRef}
|
|
onOpenChange={(open) => {
|
|
if (!open) cancelDelete();
|
|
}}
|
|
onConfirm={confirmDeleteLayout}
|
|
/>
|
|
</>
|
|
);
|
|
}
|