metis-desk/packages/shadcn-ui/src/components/entity-list-controls.tsx

146 lines
3.7 KiB
TypeScript
Raw Normal View History

2026-10-07 15:27:23 +00:00
import { Menu } from "@base-ui/react/menu";
import { EllipsisIcon } from "lucide-react";
2026-10-07 15:27:23 +00:00
import type { ReactNode } from "react";
import { entityListLabels, entityListPages } from "../lib/entity-list";
import { Button } from "./button";
import { SelectWithAction } from "./select-with-action";
const ENTITY_PAGE_SIZES = [10, 25, 50, 100];
export function EntityRowActions({ children, label }: { children: ReactNode; label: string }) {
return (
<Menu.Root>
<Menu.Trigger
aria-label={label}
render={<Button size="icon-sm" variant="ghost" />}
title={label}
>
<EllipsisIcon aria-hidden="true" />
2026-10-07 15:27:23 +00:00
</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner align="end" className="z-50" sideOffset={4}>
<Menu.Popup className="min-w-44 rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none">
{children}
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
);
}
export function EntityAction({
children,
onClick,
disabled = false,
className,
}: {
children: ReactNode;
onClick: () => void;
disabled?: boolean;
className?: string;
}) {
return (
<Menu.Item
className={`cursor-pointer rounded-sm px-3 py-2 text-sm outline-none data-highlighted:bg-accent data-disabled:pointer-events-none data-disabled:opacity-50 ${className ?? ""}`}
disabled={disabled}
onClick={onClick}
>
{children}
</Menu.Item>
);
}
export function EntityStatus({
children,
active = true,
}: {
children: ReactNode;
active?: boolean;
}) {
return (
<span className="inline-flex items-center gap-2 text-sm">
<span
aria-hidden="true"
className={
active
? "size-2 shrink-0 rounded-full bg-primary"
: "size-2 shrink-0 rounded-full bg-muted-foreground"
}
/>
{children}
</span>
);
}
export function EntityListPagination({
locale,
page,
pages,
pageSize,
onPage,
onPageSize,
id,
}: {
locale: string;
page: number;
pages: number;
pageSize: number;
onPage: (page: number) => void;
onPageSize: (size: number) => void;
id: string;
}) {
const labels = entityListLabels(locale);
return (
<nav aria-label={labels.page} className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2">
<label className="text-xs text-muted-foreground" htmlFor={`${id}-size`}>
{labels.pageSize}
</label>
<SelectWithAction
id={`${id}-size`}
label={labels.pageSize}
onValueChange={(value) => {
const size = Number(value);
if (ENTITY_PAGE_SIZES.includes(size)) {
onPageSize(size);
}
}}
options={ENTITY_PAGE_SIZES.map((size) => ({ value: String(size), label: String(size) }))}
placeholder={labels.pageSize}
value={String(pageSize)}
/>
</div>
<Button
aria-label={labels.previous}
disabled={page === 0}
onClick={() => onPage(page - 1)}
size="sm"
variant="outline"
>
‹
</Button>
{entityListPages(page, pages).map((index) => (
<Button
aria-current={index === page ? "page" : undefined}
aria-label={`${labels.page} ${index + 1}`}
key={index}
onClick={() => onPage(index)}
size="sm"
variant={index === page ? "secondary" : "ghost"}
>
{index + 1}
</Button>
))}
<Button
aria-label={labels.next}
disabled={page + 1 >= pages}
onClick={() => onPage(page + 1)}
size="sm"
variant="outline"
>
›
</Button>
</nav>
);
}