106 lines
4.2 KiB
TypeScript
106 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { motion } from "framer-motion";
|
|
|
|
interface WidgetContainerProps {
|
|
title: string;
|
|
description?: string;
|
|
onRemove: () => void;
|
|
children: React.ReactNode;
|
|
isDragging?: boolean;
|
|
showDetails?: boolean;
|
|
onToggleDetails?: () => void;
|
|
}
|
|
|
|
export function WidgetContainer({
|
|
title,
|
|
description,
|
|
onRemove,
|
|
children,
|
|
isDragging,
|
|
showDetails = false,
|
|
onToggleDetails,
|
|
}: WidgetContainerProps) {
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.35, ease: "easeOut" }}
|
|
className={`flex flex-col h-full rounded-xl border overflow-hidden transition-all duration-200 ${
|
|
isDragging
|
|
? "shadow-xl border-brand-400 dark:border-brand-500 scale-[1.01] ring-2 ring-brand-400/30"
|
|
: "border-gray-200/80 bg-[linear-gradient(180deg,rgba(248,250,252,0.95),rgba(255,255,255,0.98))] shadow-sm hover:shadow-md hover:border-gray-300 dark:border-gray-700/60 dark:bg-[linear-gradient(180deg,rgba(17,24,39,0.96),rgba(17,24,39,0.92))] dark:hover:border-gray-600"
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between gap-3 px-4 pt-3.5 pb-3 shrink-0 widget-drag-handle group">
|
|
<div className="min-w-0 flex-1 cursor-grab active:cursor-grabbing">
|
|
<div className="flex items-center gap-2">
|
|
<svg
|
|
className="w-3.5 h-5 text-gray-300 dark:text-gray-600 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
viewBox="0 0 14 20"
|
|
fill="currentColor"
|
|
>
|
|
<circle cx="4" cy="4" r="1.5" />
|
|
<circle cx="10" cy="4" r="1.5" />
|
|
<circle cx="4" cy="10" r="1.5" />
|
|
<circle cx="10" cy="10" r="1.5" />
|
|
<circle cx="4" cy="16" r="1.5" />
|
|
<circle cx="10" cy="16" r="1.5" />
|
|
</svg>
|
|
<span className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
{title}
|
|
</span>
|
|
{showDetails ? (
|
|
<span className="rounded-full bg-brand-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-brand-700 dark:bg-brand-500/10 dark:text-brand-300">
|
|
Details
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
{showDetails && description && (
|
|
<p className="ml-[22px] mt-1 line-clamp-2 text-[11px] leading-4 text-gray-500 dark:text-gray-400">
|
|
{description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
{onToggleDetails ? (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleDetails();
|
|
}}
|
|
className={`rounded-xl border px-3 py-1.5 text-[11px] font-semibold transition ${
|
|
showDetails
|
|
? "border-brand-200 bg-brand-50 text-brand-700 hover:bg-brand-100 dark:border-brand-500/30 dark:bg-brand-500/10 dark:text-brand-300"
|
|
: "border-gray-200 bg-white/80 text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-900/70 dark:text-gray-400 dark:hover:text-gray-200"
|
|
}`}
|
|
title={showDetails ? "Hide details" : "Show details"}
|
|
>
|
|
{showDetails ? "Details on" : "Details off"}
|
|
</button>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onRemove();
|
|
}}
|
|
className="rounded-lg p-1.5 text-gray-300 transition-colors hover:bg-red-50 hover:text-red-500 dark:text-gray-600 dark:hover:bg-red-950/30 dark:hover:text-red-400"
|
|
title="Remove widget"
|
|
>
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mx-4 border-t border-gray-200/80 dark:border-gray-800" />
|
|
|
|
<div className="flex-1 overflow-auto p-4 pt-3">{children}</div>
|
|
</motion.div>
|
|
);
|
|
}
|