mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
124 lines
4.8 KiB
React
124 lines
4.8 KiB
React
/***********************************************************************************************************************************************************************
|
|
* 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>
|
|
</>
|
|
);
|
|
} |