import React, { useEffect, useRef, useState } from "react"; import type { Bucket } from "@/lib/client/buckets"; type BucketCardProps = { bucket: Bucket; icon?: string | null; openEdit: (bucketId: number) => void; limit: number; usageLabel: string; }; const TAG_GAP_PX = 8; const TAG_CLASS = "shrink-0 rounded-full border border-accent-weak bg-accent-soft px-2 py-0.5 text-[11px]"; const TAG_MORE_CLASS = "shrink-0 rounded-full border border-accent-weak bg-panel px-2 py-0.5 text-[11px] text-soft"; function BucketTagsRow({ tags }: { tags: string[] }) { const containerRef = useRef(null); const moreRef = useRef(null); const tagRefs = useRef<(HTMLSpanElement | null)[]>([]); const [visibleCount, setVisibleCount] = useState(tags.length); useEffect(() => { tagRefs.current = tagRefs.current.slice(0, tags.length); setVisibleCount(tags.length); }, [tags]); useEffect(() => { if (!tags.length) return; function recomputeVisibleCount() { const containerWidth = containerRef.current?.clientWidth ?? 0; const widths = tags.map((_, index) => tagRefs.current[index]?.offsetWidth ?? 0); const moreProbe = moreRef.current; if (!containerWidth || !moreProbe || widths.some(width => !width)) { setVisibleCount(tags.length); return; } const totalTagsWidth = widths.reduce((sum, width) => sum + width, 0) + TAG_GAP_PX * Math.max(widths.length - 1, 0); if (totalTagsWidth <= containerWidth) { setVisibleCount(tags.length); return; } let nextVisibleCount = 0; let usedWidth = 0; for (let index = 0; index < widths.length; index += 1) { usedWidth += index === 0 ? widths[index] : TAG_GAP_PX + widths[index]; const remaining = widths.length - (index + 1); if (remaining <= 0) { nextVisibleCount = widths.length; break; } moreProbe.textContent = `${remaining} more...`; const moreWidth = moreProbe.offsetWidth; const totalWithMore = usedWidth + TAG_GAP_PX + moreWidth; if (totalWithMore <= containerWidth) nextVisibleCount = index + 1; else break; } setVisibleCount(nextVisibleCount); } recomputeVisibleCount(); if (typeof ResizeObserver !== "undefined") { const observer = new ResizeObserver(recomputeVisibleCount); if (containerRef.current) observer.observe(containerRef.current); return () => observer.disconnect(); } window.addEventListener("resize", recomputeVisibleCount); return () => window.removeEventListener("resize", recomputeVisibleCount); }, [tags]); if (!tags.length) { return No tags; } const visibleTags = tags.slice(0, visibleCount); const hasOverflow = visibleCount < tags.length; const remainingCount = tags.length - visibleCount; return (
{visibleTags.map((tag, index) => ( #{tag} ))} {hasOverflow ? {remainingCount} more... : null}
); } export function BucketCard({ bucket, icon, openEdit, limit, usageLabel, }: BucketCardProps) { const spent = bucket.totalUsage || 0; const rawPercent = limit > 0 ? (spent / limit) * 100 : 0; const progressPercent = Math.max(0, Math.min(100, rawPercent)); const progressColor = rawPercent > 100 ? "#ef4444" : rawPercent >= 80 ? "#facc15" : "#4ade80"; const ringTrackColor = "rgba(148, 163, 184, 0.25)"; return (
openEdit(bucket.id)} >
{limit > 0 ? (
{icon || "?"}
) : (
{icon || "?"}
)}
{bucket.name}
{bucket.description ? (
{bucket.description}
) : null}
{limit > 0 ?
{usageLabel}
: null}
); } export default React.memo(BucketCard, (prev, next) => ( prev.bucket === next.bucket && prev.icon === next.icon && prev.limit === next.limit && prev.usageLabel === next.usageLabel ));