Workspace invitations
A source-copy invitation list with consumer-owned recipients, roles, status, expiry, permissions, and actions.
Present invitations without owning invitation deliveryLink to section
WorkspaceInvitationsPanel renders a named invitation section with recipient,
role, status, sent/expiry timestamps and optional actions. Your application
supplies every invitation and owns what each action is allowed to do.
Presentation only
This Block does not create invitations, send email, generate or validate tokens, accept memberships, revoke access, authorize actions or persist state. Those behaviors remain behind your application's trusted boundaries.
Canonical identityLink to section
- Category: Account & workspace, slug
account. - Block:
workspace-invitations, exportWorkspaceInvitationsPanel. - Canonical source:
apps/marketing/content/blocks/account/workspace-invitations/. - Complete entry point:
index.tsx.
There is no Blocks npm package, Registry installer or CLI in this flow. The canonical file is presentation source that you copy into your application.
Copy sourceLink to section
Configure the public UI package and tokens, then create
src/components/blocks/workspace-invitations/index.tsx in your application.
Open the source below and use its copy button, or select the code manually. This
is the complete canonical file, not a duplicated implementation.
View and copy the complete source
import * as React from "react";
export interface WorkspaceInvitation {
/** Stable identity, unique within this panel; used as the React key. */
id: string;
recipient: string;
secondaryText?: string;
/** Display text only. The application owns role validation and authorization. */
role: string;
/** Consumer-defined invitation state such as Pending, Accepted, or Expired. */
status: string;
sentAt: string;
sentAtLabel: string;
expiresAt?: string;
expiresAtLabel?: string;
actions?: React.ReactNode;
}
export interface WorkspaceInvitationsPanelProps {
/** Stable HTML id, unique across the consuming page. */
id: string;
heading: string;
description?: string;
invitations: readonly WorkspaceInvitation[];
actions?: React.ReactNode;
emptyTitle?: string;
emptyDescription?: string;
className?: string;
}
function InvitationActions({ children }: { children?: React.ReactNode }) {
if (!React.Children.toArray(children).some((child) => child !== "")) {
return null;
}
return (
<div
data-slot="workspace-invitations-actions"
className="flex min-w-0 flex-wrap items-center gap-2 [&>*]:max-w-full [&>*]:whitespace-normal [&>*]:break-words"
>
{children}
</div>
);
}
export function WorkspaceInvitationsPanel({
id,
heading,
description,
invitations,
actions,
emptyTitle = "No invitations to display",
emptyDescription,
className,
}: WorkspaceInvitationsPanelProps) {
const headingId = `${id}-heading`;
const descriptionId = description ? `${id}-description` : undefined;
return (
<section
id={id}
aria-labelledby={headingId}
aria-describedby={descriptionId}
data-slot="workspace-invitations-panel"
className={[
"min-w-0 rounded-xl border border-border bg-card text-card-foreground",
className,
]
.filter(Boolean)
.join(" ")}
>
<div className="flex min-w-0 flex-col gap-4 border-b border-border p-4 sm:flex-row sm:items-start sm:justify-between sm:p-6">
<div className="min-w-0 space-y-1">
<h2 id={headingId} className="break-words text-base font-semibold">
{heading}
</h2>
{description ? (
<p
id={descriptionId}
className="break-words text-sm text-muted-foreground"
>
{description}
</p>
) : null}
</div>
<InvitationActions>{actions}</InvitationActions>
</div>
{invitations.length > 0 ? (
<ul
role="list"
aria-labelledby={headingId}
className="divide-y divide-border"
>
{invitations.map((invitation) => (
<li
key={invitation.id}
data-slot="workspace-invitation"
className="flex min-w-0 flex-col gap-4 p-4 sm:p-6 lg:flex-row lg:items-center lg:justify-between"
>
<div className="min-w-0 flex-1 space-y-3">
<div className="min-w-0 space-y-1">
<h3 className="break-words text-sm font-medium">
{invitation.recipient}
</h3>
{invitation.secondaryText ? (
<p className="break-words text-sm text-muted-foreground">
{invitation.secondaryText}
</p>
) : null}
</div>
<dl className="flex min-w-0 flex-wrap gap-x-6 gap-y-3">
<div className="min-w-0 space-y-1">
<dt className="text-xs text-muted-foreground">Role</dt>
<dd className="min-w-0 break-words text-sm">
{invitation.role}
</dd>
</div>
<div className="min-w-0 space-y-1">
<dt className="text-xs text-muted-foreground">Status</dt>
<dd className="min-w-0">
<span
data-slot="workspace-invitation-status"
className="inline-flex h-auto max-w-full items-center rounded-md border border-border px-2 py-0.5 text-xs font-medium whitespace-normal break-words"
>
{invitation.status}
</span>
</dd>
</div>
<div className="min-w-0 space-y-1">
<dt className="text-xs text-muted-foreground">Sent</dt>
<dd className="min-w-0 break-words text-sm">
<time dateTime={invitation.sentAt}>
{invitation.sentAtLabel}
</time>
</dd>
</div>
{invitation.expiresAt && invitation.expiresAtLabel ? (
<div className="min-w-0 space-y-1">
<dt className="text-xs text-muted-foreground">Expires</dt>
<dd className="min-w-0 break-words text-sm">
<time dateTime={invitation.expiresAt}>
{invitation.expiresAtLabel}
</time>
</dd>
</div>
) : null}
</dl>
</div>
<InvitationActions>{invitation.actions}</InvitationActions>
</li>
))}
</ul>
) : (
<div
data-slot="workspace-invitations-empty"
className="min-w-0 space-y-1 p-6 text-center sm:p-8"
>
<p className="break-words text-sm font-medium">{emptyTitle}</p>
{emptyDescription ? (
<p className="break-words text-sm text-muted-foreground">
{emptyDescription}
</p>
) : null}
</div>
)}
</section>
);
}
The documentation includes this file from canonical source at build time. Your application copy does not receive automatic updates. Connect your own invitation service and authorization, then validate it in your application. Return to the Blocks catalog to choose another pattern.
Install by copying sourceLink to section
- Copy the complete
workspace-invitationsdirectory into an application-owned path. - Configure React,
@pycolors/ui, tokens and semantic styles through the UI installation guide. - Supply validated invitation data and consumer-owned actions.
- Run lint, type-check, tests and build in the consuming application.
"use client";
import { Button } from "@pycolors/ui";
import { WorkspaceInvitationsPanel } from "./blocks/workspace-invitations";
export function WorkspaceInvitationsList() {
return (
<WorkspaceInvitationsPanel
id="workspace-invitations"
heading="Pending invitations"
description="Invitations supplied by the application."
invitations={[
{
id: "invite-1",
recipient: "sam@example.com",
secondaryText: "Example recipient",
role: "Editor",
status: "Pending",
sentAt: "2026-09-15T09:00:00Z",
sentAtLabel: "15 September 2026",
expiresAt: "2026-09-22T09:00:00Z",
expiresAtLabel: "22 September 2026",
actions: (
<Button type="button" variant="outline">
Revoke invitation
</Button>
),
},
]}
/>
);
}The example button performs no mutation by itself. Your application owns its handler, confirmation flow, permission check, network request and resulting state refresh.
Invitation truth remains yoursLink to section
id, heading and invitations are required. Give each panel a stable unique
HTML id and each invitation a stable id unique within that list.
recipient, role, status, sentAt and sentAtLabel are display-ready
consumer values. secondaryText, expiry values and actions are optional. The
Block does not validate an address, map a role to permissions, derive invitation
status, calculate expiry or decide whether an action should be shown.
Supply expiresAt and expiresAtLabel together when expiry should be visible.
Use machine-readable timestamps for the dateTime values and your own localized
human labels for presentation.
Top-level actions can expose an application-owned invitation entry point. Row
actions can expose resend, revoke or view flows only when the current user is
actually authorized. Omit unavailable actions rather than relying on a disabled
button as a security boundary.
Empty, responsive and accessible statesLink to section
With invitations={[]}, the Block renders emptyTitle and optional
emptyDescription. Loading, request failure and permission denial are distinct
states that the consuming application should model explicitly.
The section is labelled by its heading. Invitations use a semantic list, role
and status are visible text, and sent/expiry values use semantic <time>
elements. Long addresses, roles, statuses and controls wrap rather than forcing
page-level overflow.
The Block never moves focus, intercepts keys or announces status changes. Native controls and consumer components keep their own keyboard/focus semantics. Give repeated row controls distinct accessible names when their context would otherwise be ambiguous.
Ownership and security boundariesLink to section
Your backend owns invitation creation, token generation, email delivery, expiration, acceptance, revocation, membership mutation, replay protection and permission checks. The UI is not proof that an invitation is valid or that the current viewer may mutate it.
After copying, review future canonical changes deliberately and preserve local customizations. Validate long recipient text, empty results, expired/pending states, action permissions, narrow widths, both themes and keyboard behavior before production use.
No email delivery, token handling, authentication, RBAC implementation, database/schema, backend/network behavior, Registry or CLI capability is introduced by this Block or guide.