Files
2026-06-08 06:05:31 -05:00

155 lines
5.6 KiB
TypeScript

"use client";
import { useMemo } from "react";
import { getDebugFeatureCategories } from "@/config/debug-features";
import { useDevDebug } from "@/app/context/DevDebugContext";
function ToggleSwitch({
checked,
onChange,
label,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
label: string;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={label}
onClick={() => onChange(!checked)}
className={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${
checked ? "bg-[#ff4d4f]" : "bg-gray-300 dark:bg-gray-600"
}`}
>
<span
className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform ${
checked ? "translate-x-5" : "translate-x-0"
}`}
/>
</button>
);
}
export default function DevDebugFab() {
const {
isDev,
panelOpen,
setPanelOpen,
features,
isFeatureVisible,
setFeatureVisible,
resetAll,
overrides,
} = useDevDebug();
const categories = useMemo(() => getDebugFeatureCategories(), []);
const overrideCount = Object.keys(overrides).length;
if (!isDev) return null;
return (
<>
{panelOpen && (
<button
type="button"
aria-label="关闭调试面板"
className="fixed inset-0 z-[9998] bg-black/20 backdrop-blur-[1px]"
onClick={() => setPanelOpen(false)}
/>
)}
<div className="fixed bottom-5 right-5 z-[9999] flex flex-col items-end gap-3">
{panelOpen && (
<div className="w-[min(92vw,360px)] rounded-2xl border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900">
<div className="border-b border-gray-100 px-4 py-3 dark:border-gray-800">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="text-sm font-bold text-gray-900 dark:text-gray-100"></h2>
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
·
</p>
</div>
<button
type="button"
onClick={() => setPanelOpen(false)}
className="rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-200"
aria-label="关闭"
>
</button>
</div>
</div>
<div className="max-h-[min(60vh,420px)] overflow-y-auto p-4 space-y-5">
{categories.map((category) => (
<section key={category}>
<h3 className="mb-2 text-[11px] font-bold uppercase tracking-wider text-gray-400 dark:text-gray-500">
{category}
</h3>
<ul className="space-y-2">
{features
.filter((feature) => feature.category === category)
.map((feature) => {
const visible = isFeatureVisible(feature.id);
return (
<li
key={feature.id}
className="flex items-center justify-between gap-3 rounded-xl border border-gray-100 bg-gray-50 px-3 py-2.5 dark:border-gray-800 dark:bg-gray-800/60"
>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{feature.label}
</p>
<p className="mt-0.5 text-[11px] text-gray-500 dark:text-gray-400">
{feature.description}
</p>
</div>
<ToggleSwitch
checked={visible}
onChange={(next) => setFeatureVisible(feature.id, next)}
label={`${feature.label} ${visible ? "显示" : "隐藏"}`}
/>
</li>
);
})}
</ul>
</section>
))}
</div>
<div className="flex items-center justify-between border-t border-gray-100 px-4 py-3 dark:border-gray-800">
<span className="text-[11px] text-gray-400 dark:text-gray-500">
{overrideCount > 0 ? `已覆盖 ${overrideCount}` : "使用默认配置"}
</span>
<button
type="button"
onClick={resetAll}
className="rounded-lg px-3 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
>
</button>
</div>
</div>
)}
<button
type="button"
onClick={() => setPanelOpen(!panelOpen)}
className={`flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-all ${
panelOpen
? "bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900"
: "bg-[#ff4d4f] text-white hover:bg-[#ff7a45]"
}`}
aria-label={panelOpen ? "关闭调试模式" : "打开调试模式"}
title="调试模式"
>
<span className="text-lg">{panelOpen ? "✕" : "🛠️"}</span>
</button>
</div>
</>
);
}