This commit is contained in:
rgrgogu
2026-05-23 14:03:48 +08:00
parent 768092901a
commit 56d984a26a
15 changed files with 1395 additions and 251 deletions
+124
View File
@@ -0,0 +1,124 @@
/***********************************************************************************************************************************************************************
* File Name: OverflowBadges.jsx
* Type of Program: Generic Component
* Description: Renders up to `max` badges inline. Remaining items collapse into a
* "+N" overflow button that opens a dialog listing all items.
*
* HOW TO USE:
* <OverflowBadges
* items={groups} // array of objects (or primitives)
* labelKey="group_code" // key used for the badge label
* dialogTitleKey="name" // key used for the left side in the dialog row (optional)
* keyKey="group_id" // key used as React key
* dialogTitle="All groups" // dialog heading (optional)
* max={2} // how many badges to show before collapsing (default: 2)
* badgeVariant="outline" // shadcn Badge variant (default: "outline")
* badgeClassName="font-mono text-xs" // extra classes on each badge (optional)
* />
*
* // Primitive arrays (no keys needed):
* <OverflowBadges items={["SALES-A1", "HR-B2", "IT-C3"]} max={2} />
***********************************************************************************************************************************************************************/
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
/**
* @param {object} props
* @param {Array} props.items Array of objects or primitives to render.
* @param {string} [props.labelKey] Key on each item to use as the badge label. Omit for primitive arrays.
* @param {string} [props.dialogTitleKey] Key on each item to show as the row title in the dialog. Falls back to labelKey.
* @param {string} [props.keyKey] Key on each item to use as the React key. Falls back to index.
* @param {string} [props.dialogTitle] Heading shown in the overflow dialog. Default: "All items".
* @param {number} [props.max] Max badges shown inline before collapsing. Default: 2.
* @param {string} [props.badgeVariant] shadcn Badge variant. Default: "outline".
* @param {string} [props.badgeClassName] Extra className on each Badge.
* @param {string} [props.emptyText] Text shown when items is empty. Default: "—".
*/
export function OverflowBadges({
items = [],
labelKey,
dialogTitleKey,
keyKey,
dialogTitle = "All items",
max = 1,
badgeVariant = "outline",
badgeClassName = "text-xs",
emptyText = "—",
}) {
const [open, setOpen] = useState(false);
if (!items.length)
return <span className="text-muted-foreground text-xs">{emptyText}</span>;
const getLabel = (item) =>
labelKey ? item[labelKey] : String(item);
const getTitle = (item) =>
dialogTitleKey ? item[dialogTitleKey] : labelKey ? item[labelKey] : String(item);
const getKey = (item, i) =>
keyKey ? item[keyKey] : i;
const visible = items.slice(0, max);
const overflow = items.slice(max);
return (
<>
<div className="flex flex-wrap items-center gap-1">
{visible.map((item, i) => (
<Badge
key={getKey(item, i)}
variant={badgeVariant}
className={badgeClassName}
>
{getLabel(item)}
</Badge>
))}
{overflow.length > 0 && (
<button
onClick={(e) => { e.stopPropagation(); setOpen(true); }}
className="inline-flex items-center justify-center h-5 px-1.5 rounded-md border border-dashed text-xs text-muted-foreground hover:text-foreground hover:border-foreground transition-colors"
>
+{overflow.length}
</button>
)}
</div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[360px]">
<DialogHeader>
<DialogTitle>{dialogTitle}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-2 pt-1">
{items.map((item, i) => {
const label = getLabel(item);
const title = getTitle(item);
const isSame = label === title;
return (
<div
key={getKey(item, i)}
className="flex items-center justify-between rounded-lg border px-3 py-2"
>
<span className="text-sm font-medium">{title}</span>
{!isSame && (
<Badge variant={badgeVariant} className={badgeClassName}>
{label}
</Badge>
)}
</div>
);
})}
</div>
</DialogContent>
</Dialog>
</>
);
}