import type { ChatChannel, TeamInviteLink, TeamMember, TeamSentInvitation, TeamSummary, } from "../../../../api-client"; import { Button } from "../../../../components"; import { colors } from "../../../../theme/colors"; import { Box, Text, TextAttributes } from "../../../../ui"; import { canInviteToTeam, canManageTeam, describeExpiry, normalizeTeamChannelName, roleLabel, teamAccentHex, teamChannelId, teamPrefix, userHandle, } from "./model"; import { draftProblem, setDraftName, setDraftShortName, shortenInviteUrl, type TeamDraft, } from "./pane-model"; import { AccentPicker, AccentRow, ConfirmAction, Muted, PaneButton, PaneCheckbox, PaneField, SectionTitle, } from "./pane-ui"; const LABEL_WIDTH = 12; function truncate(value: string, width: number): string { if (width <= 0) return ""; if (value.length <= width) return value; return width <= 1 ? value.slice(0, width) : `${value.slice(0, width - 1)}…`; } export function MembersSection({ team, members, selfUserId, width, busyId, onChangeRole, onRemove, }: { team: TeamSummary; members: readonly TeamMember[]; selfUserId: string | null; width: number; busyId: string | null; onChangeRole: (member: TeamMember, role: "admin" | "member") => void; onRemove: (member: TeamMember) => void; }) { const manage = canManageTeam(team.role); const accent = teamAccentHex(team.accentColor); const compact = width < 64; const handleWidth = compact ? 16 : 18; const nameWidth = compact ? 0 : Math.max(0, Math.min(22, width - handleWidth - 40)); return ( Members {members.map((member) => { const self = member.user.id === selfUserId; const editable = manage && member.role !== "owner" && !self; const busy = busyId === member.id; return ( {truncate(userHandle(member.user), handleWidth)} {nameWidth > 0 ? ( {truncate(member.user.username ? member.user.displayName : "", nameWidth)} ) : null} {roleLabel(member.role)} {self ? you : null} {editable ? ( onChangeRole(member, member.role === "admin" ? "member" : "admin")} /> onRemove(member)} /> ) : null} ); })} ); } export function InvitesSection({ team, invitations, links, username, width, busy, onUsernameChange, onInvite, onCancelInvitation, onNewLink, onCopyLink, onRevokeLink, }: { team: TeamSummary; invitations: readonly TeamSentInvitation[]; links: readonly TeamInviteLink[]; username: string; width: number; busy: string | null; onUsernameChange: (value: string) => void; onInvite: () => void; onCancelInvitation: (invitation: TeamSentInvitation) => void; onNewLink: () => void; onCopyLink: (link: TeamInviteLink) => void; onRevokeLink: (link: TeamInviteLink) => void; }) { const manage = canManageTeam(team.role); const canLink = canInviteToTeam(team); return ( {manage ? ( Invite by username ) : null} {manage ? ( Pending {invitations.map((invitation) => ( {truncate(invitation.invitee ? userHandle(invitation.invitee) : "someone by email", 18)} {`from ${userHandle(invitation.inviter)} · expires ${describeExpiry(invitation.expiresAt)}`} onCancelInvitation(invitation)} /> ))} ) : null} Invite links {canLink ? ( <> {links.map((link) => { const uses = link.maxUses === null ? `${link.uses} uses` : `${link.uses}/${link.maxUses} uses`; return ( {truncate(shortenInviteUrl(link.url), Math.max(12, width - 44))} {`· ${uses} · ${describeExpiry(link.expiresAt)}`} onCopyLink(link)} /> onRevokeLink(link)} /> ); })} ) : ( {`Only owners and admins of ${team.name} can share invite links. An admin can allow every member to in Settings.`} )} ); } export function ChannelsSection({ team, channels, unreadByChannel, channelName, width, busy, onChannelNameChange, onCreate, onOpen, onDelete, }: { team: TeamSummary; channels: readonly ChatChannel[]; unreadByChannel: ReadonlyMap; channelName: string; width: number; busy: string | null; onChannelNameChange: (value: string) => void; onCreate: () => void; onOpen: (channel: ChatChannel) => void; onDelete: (channel: ChatChannel) => void; }) { const manage = canManageTeam(team.role); const accent = teamAccentHex(team.accentColor); const normalized = normalizeTeamChannelName(channelName); const general = teamChannelId(team.id); return ( Channels {channels.map((channel) => { const unread = unreadByChannel.get(channel.id) ?? 0; return ( 0 ? colors.textBright : colors.text} attributes={unread > 0 ? TextAttributes.BOLD : 0} flexShrink={0}> {`#${channel.name}`} {unread > 0 ? {`[${unread}]`} : null} onOpen(channel)} /> {manage && channel.id !== general ? ( onDelete(channel)} /> ) : null} ); })} New channel {normalized && normalized !== channelName.trim() ? `Will be #${normalized}. Letters, digits, and dashes; every member can add channels.` : "Letters, digits, and dashes. Every member can add channels; owners and admins remove them."} ); } export function TeamDraftFields({ draft, width, onChange, onSubmit, editable, }: { draft: TeamDraft; width: number; onChange: (draft: TeamDraft) => void; onSubmit?: () => void; editable: boolean; }) { return ( onChange(setDraftName(draft, name))} onSubmit={onSubmit} /> onChange(setDraftShortName(draft, shortName))} onSubmit={onSubmit} hint={`Up to 4 letters or digits. Marks the team's content as ${teamPrefix({ shortName: draft.shortName || "MD" })} where color cannot.`} hintWidth={width} /> onChange({ ...draft, accentColor })} /> {editable ? ( onChange({ ...draft, allowMemberInvites })} /> Otherwise only owners and admins invite people. ) : null} ); } export function SettingsSection({ team, draft, width, busy, dirty, onChange, onSave, onLeave, onDelete, }: { team: TeamSummary; draft: TeamDraft; width: number; busy: string | null; dirty: boolean; onChange: (draft: TeamDraft) => void; onSave: () => void; onLeave: () => void; onDelete: () => void; }) { const manage = canManageTeam(team.role); const problem = draftProblem(draft); return ( {manage ? ( <> Team {problem && dirty ? {problem} : null} ) : ( <> Team {` Name ${team.name}`} {` Short name ${team.shortName}`} {` Accent `}{team.accentColor} {` Invite links ${team.allowMemberInvites ? "every member" : "owners and admins"}`} Owners and admins change these. )} {team.role === "owner" ? "Delete team" : "Leave team"} {team.role === "owner" ? ( Removes the team for everyone: its channels, shared layouts, notes, watchlists, and views. Members keep personal copies of linked tabs. ) : ( Your linked tabs become personal copies. Team notes and collections leave your terminal. )} ); } export function CreateTeamForm({ draft, width, busy, hasPro, onChange, onCreate, onCancel, onUpgrade, }: { draft: TeamDraft; width: number; busy: boolean; hasPro: boolean; onChange: (draft: TeamDraft) => void; onCreate: () => void; onCancel: (() => void) | null; onUpgrade: () => void; }) { const problem = draftProblem(draft); return ( {/* The "New team" tab above already names this form. */} A team shares layouts, watchlists, paper portfolios, notes, custom views, and chat channels. Creating needs Pro; joining is free. {hasPro ? ( ) : ( )} {onCancel ?