Enterprise Workspace Switcher
blocklayoutMulti-organization workspace switcher dropdown and creation modal. Features active organization display, 3 tier levels (Enterprise Scale, Pro Team, Free Sandbox), team role badges, member counts, and an organization creation dialog with dynamic slug preview, region selector, and tier plan radios.
Installation
$pnpm dlx uipkge-ng@latest add enterprise-workspace-switcher$npx uipkge-ng@latest add enterprise-workspace-switcher$yarn dlx uipkge-ng@latest add enterprise-workspace-switcher$bunx uipkge-ng@latest add enterprise-workspace-switcherManifest:
https://uipkge.dev/r/angular/enterprise-workspace-switcher.jsonInstalls to:src/app/components/blocks/enterprise-workspace-switcher/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
WorkspaceIteminterface WorkspaceItem {
id: string
name: string
slug?: string
plan: string
planTier: string
members: number
region: string
regionCode: string
role: 'Owner' | 'Admin' | 'Member' | 'Personal'
color: string
initials: string
}WorkspaceUserInfointerface WorkspaceUserInfo {
name: string
email: string
avatar?: string
}npm dependencies
Files installed (2)
src/app/components/blocks/enterprise-workspace-switcher/enterprise-workspace-switcher.component.ts22.2 kB
// Multi-organization workspace switcher dropdown and creation modal: active organization display, // tier levels, team role badges, member counts, and an organization creation dialog with dynamic // slug preview, region selector, and tier plan radios. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core' import { Building2, Check, ChevronsUpDown, Globe, LogOut, LucideAngularModule, Plus, Settings, UserPlus, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiRadioGroupComponent, UiRadioGroupItemComponent } from '@/ui/radio-group/radio-group.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' export interface WorkspaceItem { id: string name: string slug?: string plan: string planTier: string members: number region: string regionCode: string role: 'Owner' | 'Admin' | 'Member' | 'Personal' color: string initials: string } export interface WorkspaceUserInfo { name: string email: string avatar?: string } const DEFAULT_WORKSPACES: WorkspaceItem[] = [ { id: 'ws-1', name: 'UIPKGE Enterprise Inc.', slug: 'uipkge-enterprise', plan: 'Enterprise Scale', planTier: 'Enterprise Plan', members: 64, region: 'US East (N. Virginia)', regionCode: 'US-East', role: 'Owner', color: 'bg-primary/10 text-primary border border-primary/20', initials: 'UE', }, { id: 'ws-2', name: 'Acme Design Systems Lab', slug: 'acme-design', plan: 'Pro Team', planTier: 'Pro Team', members: 18, region: 'EU Central (Frankfurt)', regionCode: 'EU-Central', role: 'Admin', color: 'bg-info/10 text-info border border-info/20', initials: 'AD', }, { id: 'ws-3', name: 'Personal Sandbox', slug: 'personal-sandbox', plan: 'Developer Tier', planTier: 'Free Developer Tier', members: 1, region: 'Global Edge', regionCode: 'Global', role: 'Personal', color: 'bg-success/10 text-success border border-success/20', initials: 'PS', }, ] const DEFAULT_USER: WorkspaceUserInfo = { name: 'Elena Rostova', email: '[email protected]', avatar: '', } const FALLBACK_WORKSPACE: WorkspaceItem = { id: 'ws-fallback', name: 'Workspace', plan: 'Standard', planTier: 'Standard', members: 1, region: 'US-East', regionCode: 'US-East', role: 'Owner', color: 'bg-muted text-foreground border border-border', initials: 'WS', } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-enterprise-workspace-switcher, [ui-enterprise-workspace-switcher]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent, UiBadgeComponent, UiButtonComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiRadioGroupComponent, UiRadioGroupItemComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, ], template: ` <div data-slot="enterprise-workspace-switcher" [class]="rootClass"> <ui-dropdown-menu> <button type="button" ui-dropdown-menu-trigger class="group border-border/80 bg-card hover:bg-accent/40 focus-visible:ring-ring flex w-full items-center gap-3 rounded-xl border p-2.5 text-left shadow-xs transition-[background-color,box-shadow] duration-150 focus-visible:ring-2 focus-visible:outline-none" aria-label="Select organization workspace" > <!-- Org Avatar / Monogram --> <div class="relative size-10 shrink-0"> <div [class]=" cn( 'flex size-10 items-center justify-center rounded-lg text-sm font-semibold shadow-2xs', activeWorkspace().color ) " > {{ activeWorkspace().initials }} </div> <span class="border-card bg-success absolute -top-0.5 -right-0.5 size-2.5 rounded-full border-2" title="Active Organization" aria-label="Active workspace status: Online" ></span> </div> <!-- Active Org Info & Role Badge --> <div class="min-w-0 flex-1"> <div class="flex items-center gap-1.5"> <span class="text-foreground truncate text-sm font-semibold tracking-tight"> {{ activeWorkspace().name }} </span> <span ui-badge variant="secondary" class="shrink-0 px-1.5 py-0 text-xs font-medium"> {{ activeWorkspace().role }} </span> </div> <div class="text-muted-foreground mt-0.5 flex items-center gap-1.5 truncate text-xs"> <span class="truncate font-medium">Tier: {{ activeWorkspace().plan }}</span> <span class="shrink-0">{{ activeWorkspace().regionCode }}</span> </div> </div> <lucide-icon [img]="ChevronsUpDown" class="text-muted-foreground/70 group-hover:text-foreground size-4 shrink-0 transition-colors" /> </button> <ui-dropdown-menu-content class="border-border bg-popover w-80 rounded-xl p-2 shadow-xl sm:w-96" align="start" [sideOffset]="6" > <!-- User Header Profile Info --> <ui-dropdown-menu-label class="p-0 font-normal"> <div class="bg-muted/40 flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-left"> <ui-avatar class="size-8 rounded-full border"> @if (userState().avatar) { <ui-avatar-image [src]="userState().avatar" [alt]="userState().name" /> } <ui-avatar-fallback class="bg-primary/10 text-primary text-xs font-bold">{{ userInitials() }}</ui-avatar-fallback> </ui-avatar> <div class="grid min-w-0 flex-1 text-left leading-tight"> <span class="text-muted-foreground text-xs font-medium">Signed in as</span> <span class="text-foreground truncate text-xs font-semibold">{{ userState().email }}</span> </div> <div class="flex items-center gap-1"> <span class="bg-success size-2 rounded-full"></span> <span class="text-muted-foreground text-xs font-medium">Ready</span> </div> </div> </ui-dropdown-menu-label> <ui-dropdown-menu-separator class="my-1.5" /> <!-- Workspaces Section --> <div class="flex items-center justify-between px-2 py-1"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Workspaces</span> <span class="text-muted-foreground/70 text-xs">{{ workspaces().length }} total</span> </div> <div class="my-1 space-y-1"> @for (ws of workspaces(); track ws.id) { <ui-dropdown-menu-item class="hover:bg-accent focus:bg-accent flex cursor-pointer items-center gap-3 rounded-lg p-2 transition-colors" (select)="handleSelectWorkspace(ws)" > <div [class]=" cn( 'flex size-8 shrink-0 items-center justify-center rounded-lg text-xs font-semibold shadow-2xs', ws.color ) " > {{ ws.initials }} </div> <div class="min-w-0 flex-1"> <div class="flex items-center gap-1.5"> <span [class]=" cn( 'truncate text-sm font-medium', activeWorkspaceId() === ws.id ? 'text-foreground font-semibold' : 'text-foreground/90' ) " > {{ ws.name }} </span> @if (ws.role === 'Owner') { <span ui-badge variant="secondary" class="shrink-0 px-1 py-0 text-xs font-medium">Owner</span> } @if (ws.role === 'Admin') { <span ui-badge variant="outline" class="shrink-0 px-1 py-0 text-xs font-medium">Admin</span> } </div> <p class="text-muted-foreground truncate text-xs"> {{ ws.planTier }} · {{ ws.members }} Member{{ ws.members === 1 ? '' : 's' }} · {{ ws.regionCode }} </p> </div> @if (activeWorkspaceId() === ws.id) { <lucide-icon [img]="Check" class="text-success size-4 shrink-0" aria-hidden="true" /> } </ui-dropdown-menu-item> } </div> <ui-dropdown-menu-separator class="my-1.5" /> <!-- Actions --> <div> <ui-dropdown-menu-item class="text-foreground hover:bg-accent focus:bg-accent flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm transition-colors" (select)="isCreateDialogOpen.set(true)" > <div class="border-primary/30 bg-primary/10 text-primary flex size-6 items-center justify-center rounded-md border" > <lucide-icon [img]="Plus" class="size-3.5" /> </div> <span class="font-medium">Create New Workspace</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item class="text-muted-foreground hover:text-foreground hover:bg-accent focus:bg-accent flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm transition-colors" (select)="workspaceAction.emit('settings')" > <div class="border-border bg-muted flex size-6 items-center justify-center rounded-md border"> <lucide-icon [img]="Settings" class="size-3.5" /> </div> <span>Organization Settings & Billing</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item class="text-muted-foreground hover:text-foreground hover:bg-accent focus:bg-accent flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm transition-colors" (select)="workspaceAction.emit('invite')" > <div class="border-border bg-muted flex size-6 items-center justify-center rounded-md border"> <lucide-icon [img]="UserPlus" class="size-3.5" /> </div> <span>Invite Team Members</span> </ui-dropdown-menu-item> </div> <ui-dropdown-menu-separator class="my-1.5" /> <!-- Logout --> <ui-dropdown-menu-item class="text-destructive hover:bg-destructive/10 focus:bg-destructive/10 focus:text-destructive flex cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm font-medium transition-colors" (select)="workspaceAction.emit('logout')" > <div class="border-destructive/20 bg-destructive/10 text-destructive flex size-6 items-center justify-center rounded-md border" > <lucide-icon [img]="LogOut" class="size-3.5" /> </div> <span>Log out</span> </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> <!-- Create New Workspace Modal Dialog --> <ui-dialog [open]="isCreateDialogOpen()" (openChange)="isCreateDialogOpen.set($event)"> <ui-dialog-content class="border-border bg-background rounded-2xl p-6 shadow-sm sm:max-w-lg"> <div ui-dialog-header class="space-y-2 text-left"> <div class="border-primary/20 bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl border shadow-xs" > <lucide-icon [img]="Building2" class="size-5" /> </div> <h2 ui-dialog-title class="text-foreground text-lg font-bold tracking-tight">Create New Workspace</h2> <p ui-dialog-description class="text-muted-foreground text-sm leading-relaxed"> Spin up a dedicated organization workspace with multi-region routing and isolated billing. </p> </div> <form class="space-y-4 py-2" (submit)="handleCreateWorkspace($event)"> <!-- Workspace Name --> <div class="space-y-1.5"> <label for="new-workspace-name-angular" class="text-foreground text-sm font-medium">Workspace Name</label> <ui-input id="new-workspace-name-angular" [value]="newOrgName()" (valueChange)="newOrgName.set($event)" placeholder="e.g. Stripe Developer Platform" autoComplete="off" class="h-10 text-sm" /> </div> <!-- URL Slug Preview --> <div class="space-y-1.5"> <label class="text-muted-foreground text-xs font-medium">Workspace URL Slug</label> <div class="border-border bg-muted/40 flex items-center gap-2 rounded-lg border px-3 py-2 text-xs"> <lucide-icon [img]="Globe" class="text-muted-foreground size-4 shrink-0" aria-hidden="true" /> <span class="text-muted-foreground">uipkge.dev/</span> <span class="text-foreground truncate font-semibold">{{ slugPreview() }}</span> </div> </div> <!-- Primary Region Selection --> <div class="space-y-1.5"> <label class="text-foreground text-sm font-medium">Primary Region</label> <ui-select [value]="newOrgRegion()" (valueChange)="newOrgRegion.set($event)"> <button ui-select-trigger class="h-10 w-full"> <ui-select-value placeholder="Select primary region" /> </button> <ui-select-content> <ui-select-item value="us-east">US East (N. Virginia)</ui-select-item> <ui-select-item value="eu-central">EU Central (Frankfurt)</ui-select-item> <ui-select-item value="ap-south">AP South (Mumbai)</ui-select-item> </ui-select-content> </ui-select> <p class="text-muted-foreground text-xs"> Primary cluster location for low-latency queries and database replication. </p> </div> <!-- Plan Selection Radios --> <div class="space-y-2"> <label class="text-foreground text-sm font-medium">Choose Workspace Plan</label> <ui-radio-group [value]="newOrgPlan()" (valueChange)="newOrgPlan.set($event)" class="grid grid-cols-1 gap-3 sm:grid-cols-2" > <label for="plan-pro-angular" [class]="planCardClass('pro')"> <div class="flex items-start justify-between gap-2"> <div class="space-y-0.5"> <span class="text-foreground text-sm font-semibold">Pro Team</span> <p class="text-primary text-xs font-medium">$29 / month</p> </div> <ui-radio-group-item id="plan-pro-angular" value="pro" class="mt-0.5" /> </div> <p class="text-muted-foreground mt-2 text-xs leading-relaxed"> Up to 25 members, standard 99.9% SLA, and daily automated backups. </p> </label> <label for="plan-enterprise-angular" [class]="planCardClass('enterprise')"> <div class="flex items-start justify-between gap-2"> <div class="space-y-0.5"> <span class="text-foreground text-sm font-semibold">Enterprise Scale</span> <p class="text-primary text-xs font-medium">Custom billing</p> </div> <ui-radio-group-item id="plan-enterprise-angular" value="enterprise" class="mt-0.5" /> </div> <p class="text-muted-foreground mt-2 text-xs leading-relaxed"> Unlimited members, dedicated VPC peering, 99.99% uptime SLA & SSO. </p> </label> </ui-radio-group> </div> <div ui-dialog-footer class="border-border/60 flex gap-2 border-t pt-4 sm:justify-end"> <button ui-button type="button" variant="outline" (click)="isCreateDialogOpen.set(false)">Cancel</button> <button ui-button type="submit" [disabled]="!newOrgName().trim()">Create Workspace</button> </div> </form> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiEnterpriseWorkspaceSwitcherComponent { protected readonly Building2 = Building2 protected readonly Check = Check protected readonly ChevronsUpDown = ChevronsUpDown protected readonly Globe = Globe protected readonly LogOut = LogOut protected readonly Plus = Plus protected readonly Settings = Settings protected readonly UserPlus = UserPlus protected readonly cn = cn @Input() set initialWorkspaces(v: WorkspaceItem[] | undefined) { if (v) this.workspaces.set(v.map((w) => ({ ...w }))) } @Input() set defaultActiveId(v: string | undefined) { if (v) this.activeWorkspaceId.set(v) } @Input() set user(v: WorkspaceUserInfo | undefined) { if (v) this.userState.set({ ...v }) } @Input('class') className?: string @Output() workspaceChange = new EventEmitter<WorkspaceItem>() @Output() workspaceCreate = new EventEmitter<WorkspaceItem>() @Output() workspaceAction = new EventEmitter<'settings' | 'billing' | 'invite' | 'logout'>() readonly workspaces = signal<WorkspaceItem[]>(DEFAULT_WORKSPACES.map((w) => ({ ...w }))) readonly activeWorkspaceId = signal('ws-1') readonly userState = signal<WorkspaceUserInfo>({ ...DEFAULT_USER }) readonly isCreateDialogOpen = signal(false) // Dialog form state readonly newOrgName = signal('Stripe Developer Platform') readonly newOrgRegion = signal('us-east') readonly newOrgPlan = signal('pro') readonly activeWorkspace = computed<WorkspaceItem>(() => { const list = this.workspaces() return list.find((w) => w.id === this.activeWorkspaceId()) ?? list[0] ?? { ...FALLBACK_WORKSPACE } }) readonly slugPreview = computed(() => { const cleaned = this.newOrgName() .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') return cleaned || 'stripe-dev' }) readonly userInitials = computed(() => { return ( this.userState() .name.split(' ') .map((p) => p[0]) .join('') .toUpperCase() || 'U' ) }) get rootClass(): string { return cn('w-full', this.className) } planCardClass(plan: string): string { return cn( 'border-border/80 hover:border-primary/50 relative flex cursor-pointer flex-col justify-between rounded-xl border p-3.5 shadow-xs transition-colors', this.newOrgPlan() === plan && 'border-primary bg-primary/5 ring-primary/40 shadow-sm ring-1', ) } handleSelectWorkspace(workspace: WorkspaceItem): void { this.activeWorkspaceId.set(workspace.id) this.workspaceChange.emit(workspace) } handleCreateWorkspace(event: Event): void { event.preventDefault() if (!this.newOrgName().trim()) return const regionMap: Record<string, { full: string; code: string }> = { 'us-east': { full: 'US East (N. Virginia)', code: 'US-East' }, 'eu-central': { full: 'EU Central (Frankfurt)', code: 'EU-Central' }, 'ap-south': { full: 'AP South (Mumbai)', code: 'AP-South' }, } const regionInfo = regionMap[this.newOrgRegion()] ?? { full: 'US East (N. Virginia)', code: 'US-East' } const parts = this.newOrgName().trim().split(/\s+/) const initials = parts.length > 1 ? `${parts[0]?.[0] ?? ''}${parts[1]?.[0] ?? ''}`.toUpperCase() : (parts[0]?.slice(0, 2) ?? 'WS').toUpperCase() const newWs: WorkspaceItem = { id: `ws-${Date.now()}`, name: this.newOrgName().trim(), slug: this.slugPreview(), plan: this.newOrgPlan() === 'enterprise' ? 'Enterprise Scale' : 'Pro Team', planTier: this.newOrgPlan() === 'enterprise' ? 'Enterprise Custom' : 'Pro Team', members: 1, region: regionInfo.full, regionCode: regionInfo.code, role: 'Owner', color: this.newOrgPlan() === 'enterprise' ? 'bg-primary/10 text-primary border border-primary/20' : 'bg-info/10 text-info border border-info/20', initials, } this.workspaces.update((prev) => [newWs, ...prev]) this.activeWorkspaceId.set(newWs.id) this.workspaceCreate.emit(newWs) this.workspaceChange.emit(newWs) this.newOrgName.set('') this.isCreateDialogOpen.set(false) } }src/app/components/blocks/enterprise-workspace-switcher/index.ts0.1 kB
export { UiEnterpriseWorkspaceSwitcherComponent, type WorkspaceItem, type WorkspaceUserInfo, } from './enterprise-workspace-switcher.component'
Raw manifest:https://uipkge.dev/r/angular/enterprise-workspace-switcher.json