# Workspace admin (Notchset): prompt.md (v2.0.0)

- id: `workspace-admin` · version 2.0.0 · block · pro (Pro)
- category: Application UI
- build: one set of files for every shadcn style
- install: `npx shadcn@latest add @notchset-pro/workspace-admin`
- npm dependencies: lucide-react@^1
- registry dependencies: utils, https://notchset.dev/r/notchset-foundation.json, @notchset/avatar, @notchset/button, @notchset/checkbox, @notchset/empty-state, @notchset/icon-button, @notchset-pro/invite-dialog, @notchset/menu, @notchset/search-field, @notchset/select, @notchset/status-pill, @notchset/tabs, @notchset/text-field
- docs: https://notchset.dev/blocks/workspace-admin
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Who is in a workspace and what each can change, as tabs with counts: Members with a role select per row, a bar that changes every ticked role at once and a tally by role; Guests with what a guest sees; Invites with expiry, Send again and Revoke; allowed Domains. Each change is one adapter call..

## Build it
- Stack: React 18.3 or 19, TypeScript, Tailwind CSS v4 utility classes on the foundation's roles, a components.json (imports use its aliases).
- Packages: `lucide-react@^1`.
- Files: `components/ui/notchset/blocks/workspace-admin.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`, `avatar`, `button`, `checkbox`, `empty-state`, `icon-button`, `invite-dialog`, `menu`, `search-field`, `select`, `status-pill`, `tabs`, `text-field`.
- Builds: one set of files, the same for every shadcn style.
- Exports to keep: `WorkspaceAdmin`, `adminTabs`, and every exported type.
- CSS: the install adds the notchset foundation (its roles, keyframes and motion switch, reading your shadcn tokens for colour) to your global stylesheet once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- This item reads `--ns-bad-ink`, `--ns-check`, `--ns-font-mono`, `--ns-hover`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-line-soft`, `--ns-radius`, `--ns-selected`, `--ns-signal`, `--ns-text-small`, `--ns-weight-medium`, `--ns-weight-regular`.
- Retune it (on any ancestor) with the foundation's roles and hooks: `--ns-bad`, `--ns-bad-ink`, `--ns-band`, `--ns-band-edge`, `--ns-band-grade`, `--ns-band-optimal`, `--ns-beat`, `--ns-bezel`, `--ns-canvas`, `--ns-cat-1`, `--ns-cat-2`, `--ns-cat-3`, `--ns-cat-4`, `--ns-cat-5`, `--ns-cat-6`, `--ns-check`, `--ns-chip`, `--ns-control`, `--ns-control-pad`, `--ns-control-sm`, `--ns-deep`, `--ns-deep-ink`, `--ns-deep-ink-2`, `--ns-deep-line`, `--ns-dialog`, `--ns-dialog-lg`, `--ns-dialog-sm`, `--ns-ease`, `--ns-ease-in`, `--ns-ease-panel`, `--ns-ease-pop`, `--ns-field`, `--ns-flood-arc`, `--ns-flood-aurora`, `--ns-flood-behind-1`, `--ns-flood-behind-2`, `--ns-flood-bloom`, `--ns-flood-deep`, `--ns-flood-dial-bloom`, `--ns-flood-glass`, `--ns-flood-grain`, `--ns-flood-image`, `--ns-flood-ink`, `--ns-flood-ink-low`, `--ns-flood-light`, `--ns-flood-mid`, `--ns-flood-pale`, `--ns-flood-pane`, `--ns-flood-pane-edge`, `--ns-flood-pill-ink`, `--ns-flood-ribbon`, `--ns-flood-ribbon-core`, `--ns-flood-shadow`, `--ns-flood-sheen`, `--ns-flood-wash`, `--ns-font-body`, `--ns-font-display`, `--ns-font-mono`, `--ns-font-script`, `--ns-glass`, `--ns-glass-blur`, `--ns-glass-shadow`, `--ns-glow`, `--ns-grain`, `--ns-gutter`, `--ns-header`, `--ns-heat`, `--ns-hover`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-ink-4`, `--ns-leading`, `--ns-lift`, `--ns-light-1`, `--ns-light-2`, `--ns-light-3`, `--ns-light-spread`, `--ns-light-strength`, `--ns-line`, `--ns-line-soft`, `--ns-lit-edge`, `--ns-menu-dy`, `--ns-menu-row`, `--ns-mesh`, `--ns-mesh-frost`, `--ns-ok`, `--ns-ok-ink`, `--ns-orb-body`, `--ns-orb-cool`, `--ns-orb-core`, `--ns-orb-dawn`, `--ns-orb-deep`, `--ns-orb-edge`, `--ns-orb-grain`, `--ns-orb-ground`, `--ns-orb-halo`, `--ns-orb-inner`, `--ns-orb-key`, `--ns-orb-lift`, `--ns-orb-limb`, `--ns-orb-limb-from`, `--ns-orb-rim`, `--ns-orb-skin-hi`, `--ns-orb-skin-lo`, `--ns-orb-skin-mid`, `--ns-orb-warm`, `--ns-panel`, `--ns-panel-w`, `--ns-prism-edge`, `--ns-prism-glint`, `--ns-prism-sheen`, `--ns-prism-tint`, `--ns-pulse`, `--ns-radius`, `--ns-radius-card`, `--ns-radius-full`, `--ns-radius-lg`, `--ns-radius-sm`, `--ns-radius-xl`, `--ns-raised`, `--ns-range`, `--ns-row`, `--ns-row-compact`, `--ns-segment`, `--ns-selected`, `--ns-series`, `--ns-series-core`, `--ns-series-lilac`, `--ns-series-tint`, `--ns-shadow-dialog`, `--ns-shadow-float`, `--ns-shadow-panel`, `--ns-sheet`, `--ns-sheet-bottom`, `--ns-side`, `--ns-signal`, `--ns-sleep-awake`, `--ns-sleep-deep`, `--ns-sleep-light`, `--ns-sleep-rem`, `--ns-space-1`, `--ns-space-2`, `--ns-space-3`, `--ns-space-4`, `--ns-space-5`, `--ns-space-6`, `--ns-space-8`, `--ns-space-section`, `--ns-stack-on`, `--ns-standing-critical`, `--ns-standing-in`, `--ns-standing-out`, `--ns-state-doing`, `--ns-state-done`, `--ns-state-review`, `--ns-state-todo`, `--ns-switch-h`, `--ns-switch-w`, `--ns-text-body`, `--ns-text-control`, `--ns-text-display`, `--ns-text-figure`, `--ns-text-headline`, `--ns-text-hero`, `--ns-text-lede`, `--ns-text-meta`, `--ns-text-page`, `--ns-text-record`, `--ns-text-small`, `--ns-text-strong`, `--ns-text-sub`, `--ns-text-title`, `--ns-tile-shadow`, `--ns-time-born`, `--ns-time-color`, `--ns-time-dialog`, `--ns-time-menu`, `--ns-time-page`, `--ns-time-panel`, `--ns-time-toast`, `--ns-tint`, `--ns-tint-ink`, `--ns-toast-bottom`, `--ns-toast-left`, `--ns-touch`, `--ns-tracking-display`, `--ns-tracking-headline`, `--ns-warn`, `--ns-warn-ink`, `--ns-wash-clay`, `--ns-wash-dusk`, `--ns-wash-graphite`, `--ns-wash-ink`, `--ns-wash-sage`, `--ns-wash-sand`, `--ns-weight-light`, `--ns-weight-medium`, `--ns-weight-regular`, `--ns-weight-strong`, `--ns-well`.
- Never add Tailwind colour classes inside the component.

```tsx
import type { InviteLink } from "@/components/ui/notchset/blocks/invite-dialog";
```

## Parts
- A single export that takes props.

## Match the original
- Read `components/ui/notchset/blocks/workspace-admin.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when

### Not when

## Built from
- avatar (`avatar`), installed with it: reuse it, don't rebuild it
- button (`button`), installed with it: reuse it, don't rebuild it
- checkbox (`checkbox`), installed with it: reuse it, don't rebuild it
- empty-state (`empty-state`), installed with it: reuse it, don't rebuild it
- icon-button (`icon-button`), installed with it: reuse it, don't rebuild it
- invite-dialog (`invite-dialog`), installed with it: reuse it, don't rebuild it
- menu (`menu`), installed with it: reuse it, don't rebuild it
- search-field (`search-field`), installed with it: reuse it, don't rebuild it
- select (`select`), installed with it: reuse it, don't rebuild it
- status-pill (`status-pill`), installed with it: reuse it, don't rebuild it
- tabs (`tabs`), installed with it: reuse it, don't rebuild it
- text-field (`text-field`), installed with it: reuse it, don't rebuild it

## Mistakes
- Recreating the component instead of installing it

## Usage

```tsx
"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>
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `workspace` | `string` |  | the 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 \| null` |  |  |
| `adapter` | `WorkspaceAdminAdapter` |  | Required. |
| `guestRole` | `string` | `"guest"` | the role that makes someone a guest: they're listed under Guests |
| `guestsSee` | `React.ReactNode` |  | what a guest sees, in one sentence |
| `canManage` | `boolean` | `true` | false: the lists read, nothing changes (the selects and actions go), with `readOnly` said above |
| `readOnly` | `React.ReactNode` |  |  |
| `seats` | `{ used: number; total: number; plan?: string \| undefined }` |  |  |
| `tab` | `AdminTab` |  | the open tab, when the host keeps it (an address); otherwise the block does |
| `tabRow` | `boolean` | `true` | false: 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) => void` |  |  |
| `inviteOpen` | `boolean` |  | the 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) => void` |  |  |
| `inviteFields` | `React.ReactNode` |  | the host's own fields in the invite dialog, under the role (a team to join) |
| `toolbar` | `React.ReactNode` |  | more filters beside Find (a team), and the test they apply |
| `filter` | `(m: AdminMember) => boolean` |  |  |
| `avatar` | `(m: AdminMember) => React.ReactNode` |  | the host's own face for a person (one that opens a card on hover); otherwise their Avatar |

Full docs: https://notchset.dev/blocks/workspace-admin

## Customising
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- Dark mode follows the `.dark` class or `data-theme="dark"` on an ancestor (the shadcn and next-themes conventions).
- Update later: Commit or stash local changes first (or copy the files), so the update can be undone. Read https://notchset.dev/r/changelog.json for the new versions: a major version, an entry marked breaking or a merge says what to change. Then re-run the install with `--overwrite`, on a branch. It replaces the component's files, the shared helper files they import, and the item's CSS rules in the global stylesheet, so review the whole diff (components, shared files, CSS), re-apply your own edits, and run the app's typecheck, tests and build.

## Accessibility contract (preserve when editing)

## Install

```bash
npx shadcn@latest add @notchset-pro/workspace-admin
```

Pro item: needs the `@notchset-pro` registry in `components.json` and `NOTCHSET_TOKEN` in `.env.local` (https://notchset.dev/account). Setup: https://notchset.dev/docs/pro. Every components.json `style` gets the same files.

## Continue the workflow

- [Invite dialog](https://notchset.dev/blocks/invite-dialog)
- [Select](https://notchset.dev/components/select)
- [Avatar](https://notchset.dev/components/avatar)

The foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://notchset.dev/r/notchset-foundation.json.
