Este commit remove seções vazias de mudanças de código do arquivo de mudanças. Isso ajuda a manter o histórico de mudanças mais limpo e organizado, facilitando a leitura e a compreensão das alterações realizadas no projeto.
135 lines
3.9 KiB
TypeScript
135 lines
3.9 KiB
TypeScript
"use client";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { title_font } from "@/public/fonts/font_index";
|
|
import { RiExpandDiagonalLine } from "@remixicon/react";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { Button } from "./ui/button";
|
|
|
|
const OVERFLOW_THRESHOLD_PX = 16;
|
|
const OVERFLOW_CHECK_DEBOUNCE_MS = 100;
|
|
|
|
type WidgetProps = {
|
|
title: string;
|
|
subtitle: string;
|
|
children: React.ReactNode;
|
|
icon: React.ReactElement;
|
|
action?: React.ReactNode;
|
|
};
|
|
|
|
export default function WidgetCard({
|
|
title,
|
|
subtitle,
|
|
icon,
|
|
children,
|
|
action,
|
|
}: WidgetProps) {
|
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
const [hasOverflow, setHasOverflow] = useState(false);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const checkOverflow = useCallback(() => {
|
|
const el = contentRef.current;
|
|
if (!el) return;
|
|
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current);
|
|
}
|
|
|
|
debounceTimerRef.current = setTimeout(() => {
|
|
const hasOverflowNow =
|
|
el.scrollHeight - el.clientHeight > OVERFLOW_THRESHOLD_PX;
|
|
setHasOverflow(hasOverflowNow);
|
|
}, OVERFLOW_CHECK_DEBOUNCE_MS);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const el = contentRef.current;
|
|
if (!el) return;
|
|
|
|
// Checagem inicial
|
|
checkOverflow();
|
|
|
|
// Observa apenas resize do container (suficiente para detectar overflow)
|
|
const ro = new ResizeObserver(checkOverflow);
|
|
ro.observe(el);
|
|
|
|
return () => {
|
|
ro.disconnect();
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current);
|
|
}
|
|
};
|
|
}, [checkOverflow]);
|
|
|
|
return (
|
|
<Card className="md:h-custom-height-1 relative h-auto md:overflow-hidden">
|
|
<CardHeader className="border-b [.border-b]:pb-2">
|
|
<div className="flex w-full items-start justify-between">
|
|
<div>
|
|
<CardTitle
|
|
className={`${title_font.className} flex items-center gap-1`}
|
|
>
|
|
<span className="text-primary">{icon}</span>
|
|
{title}
|
|
</CardTitle>
|
|
<CardDescription className="text-muted-foreground text-sm capitalize mt-1">
|
|
{subtitle}
|
|
</CardDescription>
|
|
</div>
|
|
{action && <div className="shrink-0">{action}</div>}
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent
|
|
ref={contentRef}
|
|
className="max-h-[calc(var(--spacing-custom-height-1)-5rem)] overflow-hidden md:max-h-[calc(100%-5rem)]"
|
|
>
|
|
{children}
|
|
</CardContent>
|
|
|
|
{hasOverflow && (
|
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex justify-center bg-linear-to-t from-card to-transparent pt-12 pb-6">
|
|
<Button
|
|
variant="outline"
|
|
className="pointer-events-auto rounded-full text-xs dark:text-white"
|
|
onClick={() => setIsOpen(true)}
|
|
aria-label="Expandir para ver todo o conteúdo"
|
|
>
|
|
Ver tudo <RiExpandDiagonalLine size={10} aria-hidden="true" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogContent className="max-h-[85vh] w-full max-w-3xl overflow-hidden p-0">
|
|
<DialogHeader className="px-6 pt-4">
|
|
<DialogTitle className="flex items-center gap-2">
|
|
{icon}
|
|
<span>{title}</span>
|
|
</DialogTitle>
|
|
{subtitle ? (
|
|
<p className="text-muted-foreground text-sm">{subtitle}</p>
|
|
) : null}
|
|
</DialogHeader>
|
|
<div className="scrollbar-hide max-h-[calc(85vh-6rem)] overflow-y-auto px-6 pb-6">
|
|
{children}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</Card>
|
|
);
|
|
}
|