evOo-local/apps/web/src/pages/shell/project-row.tsx

121 lines
3.9 KiB
TypeScript

import { useLingui } from "@lingui/react/macro";
import type { GroupAvatarMember } from "@rakazo/ui-web";
import { BotAvatar, Button } from "@rakazo/ui-web";
import { MoreHorizontal } from "lucide-react";
import { ProjectIcon } from "../../components/project-icon";
import { rosterNameClass, rosterRowSurfaceClass } from "./roster-row-style";
import { FALLBACK_BOT_COLOR } from "./shell-types";
import type { SpaceMenuRequest } from "./sidebar-group-header";
/** One project in the sidebar: its icon, name, and group member avatars; it opens the project's group chat. */
export function ProjectRow({
spaceId,
name,
icon,
color,
members,
hasMenu,
selected,
unread,
needsYou,
running,
onOpen,
onMenu,
}: {
spaceId: string;
name: string;
icon?: string | null;
color?: string | null;
members?: GroupAvatarMember[];
hasMenu: boolean;
selected: boolean;
unread: boolean;
needsYou: number;
running: boolean;
onOpen: () => void;
onMenu: (menu: SpaceMenuRequest, anchor: HTMLElement) => void;
}) {
const { t } = useLingui();
return (
<div className={`group flex w-full items-center ${rosterRowSurfaceClass(selected)}`}>
<button
type="button"
data-testid={`project-row-${spaceId}`}
data-long-press={hasMenu ? "" : undefined}
aria-label={needsYou > 0 ? t`${name}, ${needsYou} need you` : name}
aria-current={selected ? "page" : undefined}
onClick={onOpen}
onContextMenu={(event) => {
if (!hasMenu) return;
event.preventDefault();
onMenu(
{ id: spaceId, position: { x: event.clientX, y: event.clientY } },
event.currentTarget,
);
}}
className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 min-h-[48px] rounded-2xl py-2 ps-3 pe-2 text-start outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
>
<ProjectIcon icon={icon} color={color} size={30} />
<span
dir="auto"
className={`min-w-0 flex-1 truncate text-ui-body tracking-tight ${rosterNameClass(unread)}`}
>
{name}
</span>
{members && members.length > 0 ? (
<div
data-testid={`project-row-avatars-${spaceId}`}
className="flex shrink-0 items-center -space-x-1.5"
>
{members.slice(0, 3).map((member) => (
<div
key={member.botId}
title={member.name}
className="rounded-full ring-2 ring-sidebar"
>
<BotAvatar
color={member.color ?? FALLBACK_BOT_COLOR}
identity={member.botId}
size={20}
status={member.status}
/>
</div>
))}
</div>
) : null}
{needsYou > 0 ? (
<span
aria-hidden="true"
className="grid min-w-[20px] shrink-0 place-items-center rounded-full bg-warning px-1.5 text-ui-label font-bold leading-5 text-background"
>
{needsYou > 9 ? "9+" : needsYou}
</span>
) : running ? (
<span
aria-hidden="true"
className="size-2 shrink-0 animate-pulse rounded-full bg-success"
/>
) : null}
</button>
{hasMenu ? (
// Pointer devices reveal the menu on hover; touch opens it with a long press.
<Button
variant="ghost"
size="icon-sm"
aria-label={t`Actions for ${name}`}
className="me-2 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
onClick={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
onMenu(
{ id: spaceId, position: { x: rect.left, y: rect.bottom } },
event.currentTarget,
);
}}
>
<MoreHorizontal aria-hidden="true" className="size-icon-md" />
</Button>
) : null}
</div>
);
}