-

Workspace Selector

Previous

A searchable workspace selector for console headers, dialogs, and ownership fields.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/workspace-selector.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/workspace-selector.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/workspace-selector.json

Component

components/ui/workspace-selector.tsx
"use client"

import * as React from "react"
import {
  Building2Icon,
  CheckIcon,
  ChevronsUpDownIcon,
  SearchIcon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export interface WorkspaceSelectorOption {
  id: string
  name: string
  description?: string
  disabled?: boolean
}

export interface WorkspaceSelectorProps {
  options: WorkspaceSelectorOption[]
  value?: string
  onValueChange?: (value: string, option: WorkspaceSelectorOption) => void
  placeholder?: string
  searchPlaceholder?: string
  emptyText?: string
  disabled?: boolean
  className?: string
  contentClassName?: string
}

export function WorkspaceSelector({
  options,
  value,
  onValueChange,
  placeholder = "Select workspace",
  searchPlaceholder = "Search workspaces...",
  emptyText = "No workspaces found.",
  disabled = false,
  className,
  contentClassName,
}: WorkspaceSelectorProps) {
  const [open, setOpen] = React.useState(false)
  const selected = options.find((option) => option.id === value)

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          type="button"
          variant="outline"
          role="combobox"
          aria-expanded={open}
          disabled={disabled}
          className={cn(
            "h-10 w-full justify-between rounded-md px-3 font-normal",
            !selected && "text-muted-foreground",
            className
          )}
        >
          <span className="flex min-w-0 items-center gap-2">
            <Building2Icon className="size-4 shrink-0" />
            <span className="truncate">{selected?.name ?? placeholder}</span>
          </span>
          <ChevronsUpDownIcon className="text-muted-foreground size-4 shrink-0" />
        </Button>
      </PopoverTrigger>
      <PopoverContent
        align="start"
        className={cn("w-[min(360px,calc(100vw-2rem))] p-0", contentClassName)}
      >
        <Command>
          <div className="border-b px-3">
            <div className="flex items-center">
              <SearchIcon className="text-muted-foreground mr-2 size-4" />
              <CommandInput
                placeholder={searchPlaceholder}
                className="h-10 border-0 px-0 focus:ring-0"
              />
            </div>
          </div>
          <CommandList className="max-h-72">
            <CommandEmpty>{emptyText}</CommandEmpty>
            {options.map((option) => (
              <CommandItem
                key={option.id}
                value={`${option.name} ${option.description ?? ""}`}
                disabled={option.disabled}
                onSelect={() => {
                  onValueChange?.(option.id, option)
                  setOpen(false)
                }}
                className="items-start gap-3 py-3"
              >
                <CheckIcon
                  className={cn(
                    "mt-0.5 size-4 shrink-0",
                    value === option.id ? "opacity-100" : "opacity-0"
                  )}
                />
                <span className="min-w-0 flex-1">
                  <span className="block truncate text-sm font-medium">
                    {option.name}
                  </span>
                  {option.description ? (
                    <span className="text-muted-foreground mt-0.5 line-clamp-2 block text-xs leading-5">
                      {option.description}
                    </span>
                  ) : null}
                </span>
              </CommandItem>
            ))}
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  )
}

Usage

import { WorkspaceSelector } from "@/components/ui/workspace-selector"
 
export function Example() {
  return (
    <WorkspaceSelector
      value="support"
      options={[{ id: "support", name: "Support Ops" }]}
    />
  )
}

Props

PropTypeDefault
optionsWorkspaceSelectorOption[]required
valuestring-
onValueChange(value, option) => void-
placeholderstring"Select workspace"
searchPlaceholderstring"Search workspaces..."
emptyTextstring"No workspaces found."
disabledbooleanfalse