121 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|