Members, guests, invites and domains, with roles in place.
Every shadcn style · one file
v2.0 · Updated Oct 13pnpm dlx shadcn@latest add @notchset-pro/workspace-adminpnpm dlx shadcn@latest add @notchset-pro/workspace-adminNeeds your registry key. Every components.json style gets the same files.
Works with React 18.3 or 19 on Tailwind CSS v4, in any shadcn project. What's tested
The CLI adds the styles to the CSS file named in components.json (tailwind.css). Check that it points at your global stylesheet, that the project uses Tailwind CSS v4, and that the file is imported in your root layout. Installed by hand: paste styles.css into that file.
Import it and pass your content. The props that matter most are listed next.
"use client";
import * as React from "react";
import {
type AdminDomain,
type AdminInvite,
type AdminMember,
type WorkspaceAdminAdapter,
WorkspaceAdmin,
} from "@/components/ui/notchset/blocks/workspace-admin";
import type { InviteLink } from "@/components/ui/notchset/blocks/invite-dialog";
const ROLES = [
{ value: "owner", label: "Owner", invitable: false },
{
value: "admin",
label: "Admin",
hint: "Admins manage members, roles and billing.",
},
{
value: "member",
label: "Member",
hint: "Members see every company, deal and report. They can’t change billing or roles.",
},
{
value: "guest",
label: "Guest",
hint: "Guests see only the records they’re added to.",
},
];
/* The demo's own adapter: it keeps the lists in state, as a host keeps them in its store and hands each call on. */
export default function WorkspaceAdminDemo() {
const [members, setMembers] = React.useState<AdminMember[]>([
{
id: "ml",
name: "Maya Lindqvist",
email: "maya@notchset.com",
role: "owner",
seen: "Now",
you: true,
hue: 3,
},
{
id: "jb",
name: "Jonas Berg",
email: "jonas@notchset.com",
role: "admin",
seen: "2 hours ago",
hue: 1,
},
{
id: "pr",
name: "Priya Raman",
email: "priya@notchset.com",
role: "member",
seen: "Today",
hue: 4,
},
{
id: "to",
name: "Theo Okafor",
email: "theo@notchset.com",
role: "member",
seen: "Yesterday",
hue: 2,
},
{
id: "id",
name: "Ines Duarte",
email: "ines@notchset.com",
role: "member",
seen: "3 days ago",
hue: 5,
},
{
id: "km",
name: "Kofi Mensah",
email: "kofi@notchset.com",
role: "member",
seen: "Last week",
hue: 1,
},
{
id: "sw",
name: "Sam Whitfield",
email: "sam@haldenstudio.co",
role: "guest",
seen: "Today",
sees: "2 companies",
hue: 2,
},
]);
const [invites, setInvites] = React.useState<AdminInvite[]>([
{
id: "i1",
email: "lena@notchset.com",
role: "member",
sent: "Jonas · 2 days ago",
expires: "in 5 days",
},
{
id: "i2",
email: "rui@northwind.io",
role: "guest",
sent: "Maya · 9 days ago",
expires: "2 days ago",
expired: true,
},
]);
const [domains, setDomains] = React.useState<AdminDomain[]>([
{
domain: "notchset.com",
role: "member",
note: "Verified 12 Mar · 6 people joined this way",
},
]);
const [link, setLink] = React.useState<InviteLink | null>({
url: "notchset.app/join/7Hq2-xK9p",
role: "member",
expires: "7d",
uses: 0,
});
const adapter: WorkspaceAdminAdapter = {
setRole: (ids, role) =>
setMembers((ms) =>
ms.map((m) => (ids.includes(m.id) ? { ...m, role } : m)),
),
invite: ({ emails, role }) =>
setInvites((is) => [
...is,
...emails.map((email, n) => ({
id: `n${is.length + n}`,
email,
role,
sent: "you · just now",
expires: "in 7 days",
})),
]),
resendInvite: (id) =>
setInvites((is) =>
is.map((i) =>
i.id === id
? {
...i,
expired: false,
expires: "in 7 days",
sent: "you · just now",
}
: i,
),
),
revokeInvite: (id) => setInvites((is) => is.filter((i) => i.id !== id)),
deactivate: (id) =>
setMembers((ms) =>
ms.map((m) => (m.id === id ? { ...m, deactivated: true } : m)),
),
reactivate: (id) =>
setMembers((ms) =>
ms.map((m) => (m.id === id ? { ...m, deactivated: false } : m)),
),
setLink: ({ role, expires }) =>
setLink((l) => ({
url: l?.url ?? "notchset.app/join/Rm4t-8bQz",
role,
expires,
uses: l?.uses ?? 0,
})),
revokeLink: () => setLink(null),
addDomain: ({ domain, role }) =>
setDomains((ds) => [...ds, { domain, role, note: "Added just now" }]),
removeDomain: (domain) =>
setDomains((ds) => ds.filter((d) => d.domain !== domain)),
setDomainRole: (domain, role) =>
setDomains((ds) =>
ds.map((d) => (d.domain === domain ? { ...d, role } : d)),
),
};
const used =
members.filter((m) => !m.deactivated).length +
invites.filter((i) => !i.expired).length;
return (
<div className="w-full max-w-[880px]">
<WorkspaceAdmin
workspace="Notchset"
roles={ROLES}
members={members}
invites={invites}
domains={domains}
link={link}
adapter={adapter}
seats={{ used, total: 10, plan: "Team" }}
guestsSee="Guests see only the companies and deals they’re added to, and never Reports or Settings."
/>
</div>
);
}components/ui/notchset/blocks/workspace-admin.tsx
This component is included with Pro.
Try the live preview above. Get Pro to install the source.
workspace–
stringthe workspace's name, for the invite dialog
roles–
AdminRole[]every role, highest first (Owner, Admin, Member, Guest)
members–
AdminMember[]Required.
invites–
AdminInvite[]Required.
domains[]
AdminDomain[]link–
InviteLink | nulladapter–
WorkspaceAdminAdapterRequired.
guestRole"guest"
stringthe role that makes someone a guest: they're listed under Guests
guestsSee–
React.ReactNodewhat a guest sees, in one sentence
canManagetrue
booleanfalse: the lists read, nothing changes (the selects and actions go), with `readOnly` said above
readOnly–
React.ReactNodeseats–
{ used: number; total: number; plan?: string | undefined }tab–
AdminTabthe open tab, when the host keeps it (an address); otherwise the block does
tabRowtrue
booleanfalse: the host's own tab row names the kit's tabs (a Settings section's sub-pages, `adminTabs` for their counts), so navigation never nests in the same shape; the host then opens the invite dialog itself
onTabChange–
(tab: AdminTab) => voidinviteOpen–
booleanthe invite dialog, when the host opens it (a page header's Invite people; a dialog of its own, kept closed here); otherwise a button in the tab row. `role` is the one asked for (Invite a guest).
onInviteOpenChange–
(open: boolean, role?: string) => voidinviteFields–
React.ReactNodethe host's own fields in the invite dialog, under the role (a team to join)
toolbar–
React.ReactNodemore filters beside Find (a team), and the test they apply
filter–
(m: AdminMember) => booleanavatar–
(m: AdminMember) => React.ReactNodethe host's own face for a person (one that opens a card on hover); otherwise their Avatar
Give your coding assistant the Markdown guide instead of screenshots.
https://notchset.dev/api/mcp (setup)v2.0.0 · 2026-10-13
Released with the Notchset system.
The same file, at work in a finished app. Open it live and find it on its pages.
Workspace admin. Who is in a workspace and what each person can change: members, guests, invites and the domains that join on their own.
5 free · 2 ProSee the Workspace admin kit
Pro is in early access.