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 ? : null}
{problem && draft.name ? {problem} : null}
);
}