Files
CC-Switch/src/components/common/FullScreenPanel.tsx
T
Jason a3b3a06f5e feat(providers): tighten Add Provider header spacing and add footer hint
- Reduce title->tabs and tabs->card vertical gaps from 24px to 12px
- Add an always-visible footer hint guiding users to fill the fields
  below after choosing a preset (pinned footer, never scrolls off)
- FullScreenPanel: add optional contentClassName prop to scope content
  padding overrides without affecting the other panels that share it
- i18n: add provider.addFooterHint in zh/en/ja/zh-TW
2026-06-22 10:28:10 +08:00

168 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
isWindows,
isLinux,
DRAG_REGION_ATTR,
DRAG_REGION_STYLE,
} from "@/lib/platform";
import { isTextEditableTarget } from "@/utils/domUtils";
import { cn } from "@/lib/utils";
interface FullScreenPanelProps {
isOpen: boolean;
title: string;
onClose: () => void;
children: React.ReactNode;
footer?: React.ReactNode;
/**
* 覆盖内容区滚动容器的内边距/间距类。默认 `px-6 py-6 space-y-6`。
* 通过 `cn`(twMerge) 合并,传入如 `pt-3` 只覆盖顶部内边距,其余保持默认。
*/
contentClassName?: string;
}
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px - match App.tsx
const HEADER_HEIGHT = 64; // px - match App.tsx
/**
* Reusable full-screen panel component
* Handles portal rendering, header with back button, and footer
* Uses solid theme colors without transparency
*/
export const FullScreenPanel: React.FC<FullScreenPanelProps> = ({
isOpen,
title,
onClose,
children,
footer,
contentClassName,
}) => {
React.useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
// ESC 键关闭面板
const onCloseRef = React.useRef(onClose);
React.useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
React.useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
// 子组件(例如 Radix 的 Select/Dialog/Dropdown)如果已经消费了 ESC,就不要再关闭整个面板
if (event.defaultPrevented) {
return;
}
if (isTextEditableTarget(event.target)) {
return; // 让输入框自己处理 ESC(比如清空、失焦等)
}
event.stopPropagation(); // 阻止事件继续冒泡到 window,避免触发 App.tsx 的全局监听
onCloseRef.current();
}
};
// 使用冒泡阶段监听,让子组件(如 Radix UI)优先处理 ESC
window.addEventListener("keydown", handleKeyDown, false);
return () => {
window.removeEventListener("keydown", handleKeyDown, false);
};
}, [isOpen]);
return createPortal(
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-[60] flex flex-col"
style={{ backgroundColor: "hsl(var(--background))" }}
>
{/* Drag region - match App.tsx. Linux 上 DRAG_BAR_HEIGHT=0
直接跳过整个元素;macOS 保留 28px 拖拽占位。 */}
{DRAG_BAR_HEIGHT > 0 && (
<div
data-tauri-drag-region
style={
{
WebkitAppRegion: "drag",
height: DRAG_BAR_HEIGHT,
} as React.CSSProperties
}
/>
)}
{/* Header - match App.tsx */}
<div
className="flex-shrink-0 flex items-center"
{...DRAG_REGION_ATTR}
style={
{
...DRAG_REGION_STYLE,
backgroundColor: "hsl(var(--background))",
height: HEADER_HEIGHT,
} as React.CSSProperties
}
>
<div
className="px-6 w-full flex items-center gap-4"
{...DRAG_REGION_ATTR}
style={{ ...DRAG_REGION_STYLE } as React.CSSProperties}
>
<Button
type="button"
variant="outline"
size="icon"
onClick={onClose}
className="rounded-lg select-none"
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<h2 className="text-lg font-semibold text-foreground select-none">
{title}
</h2>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto scroll-overlay">
<div className={cn("px-6 py-6 space-y-6 w-full", contentClassName)}>
{children}
</div>
</div>
{/* Footer */}
{footer && (
<div
className="flex-shrink-0 py-4 border-t border-border-default"
style={{ backgroundColor: "hsl(var(--background))" }}
>
<div className="px-6 flex items-center justify-end gap-3">
{footer}
</div>
</div>
)}
</motion.div>
)}
</AnimatePresence>,
document.body,
);
};