Kobana UI
GitHub

ScopeSelector

Seletor de escopo para header com busca assíncrona, badge compacto e persistência em localStorage.

Carregando...

Instalação

npx @kobana/ui add scope-selector

Dependências instaladas automaticamente: button, command, popover (shadcn/ui), lucide-react

Importação

import {
  ScopeSelector,
  ScopeProvider,
  useScope,
} from "@/components/kobana/scope-selector"

Props

ScopeSelector

PropTipoDefaultDescrição
valueT | nullItem selecionado
onChange(value: T | null) => voidCallback de seleção/limpeza
onSearch(query: string) => Promise<T[]>Busca assíncrona de itens
getItemValue(item: T) => stringExtrai ID único do item
getItemLabel(item: T) => stringExtrai label de exibição
renderItem(item: T) => ReactNodeRender customizado de cada item na lista
iconReactNode<User />Ícone do trigger
placeholderstring"Filtrar..."Título do botão sem seleção
searchPlaceholderstring"Buscar..."Placeholder do campo de busca
emptyMessagestring"Nenhum resultado encontrado."Mensagem quando a busca não retorna resultados
loadingMessagestring"Carregando..."Mensagem de loading
clearLabelstring"Limpar filtro"Label acessível do botão de limpar
groupHeadingstringHeading do grupo de itens
align"start" | "center" | "end""end"Alinhamento do popover
popoverWidthstring"w-[350px]"Largura do popover
classNamestringClasses adicionais

ScopeProvider

PropTipoDescrição
storageKeystringChave usada no localStorage
childrenReactNodeConteúdo do provider

useScope<T>

Hook que retorna:

CampoTipoDescrição
valueT | nullValor atual do escopo
setValue(value: T | null) => voidDefine o valor
clear() => voidLimpa o escopo
isHydratedbooleantrue após hidratar do localStorage

Uso

Básico (controlado)

const [customer, setCustomer] = useState<Customer | null>(null)

<ScopeSelector
  value={customer}
  onChange={setCustomer}
  onSearch={async (query) => {
    const res = await fetch(`/api/customers?search=${query}`)
    const json = await res.json()
    return json.data
  }}
  getItemValue={(c) => c.id}
  getItemLabel={(c) => c.name}
  renderItem={(c) => (
    <div className="flex flex-col">
      <span className="font-medium">{c.name}</span>
      <span className="text-xs text-muted-foreground">{c.email}</span>
    </div>
  )}
  placeholder="Filtrar por cliente"
  searchPlaceholder="Buscar cliente..."
/>

Com ScopeProvider (persistência)

// layout.tsx
<ScopeProvider storageKey="customer-scope">
  <Header />
  <main>{children}</main>
</ScopeProvider>

// header.tsx
function Header() {
  const { value, setValue } = useScope<Customer>()

  return (
    <header>
      <ScopeSelector
        value={value}
        onChange={setValue}
        onSearch={fetchCustomers}
        getItemValue={(c) => c.id}
        getItemLabel={(c) => c.name}
      />
    </header>
  )
}

// any-page.tsx — lê o escopo
function InvoicesPage() {
  const { value: customer } = useScope<Customer>()
  // filtra dados pelo customer.id
}

Com ícone customizado

import { Building } from "lucide-react"

<ScopeSelector
  value={selectedOrg}
  onChange={setSelectedOrg}
  onSearch={fetchOrganizations}
  getItemValue={(o) => o.id}
  getItemLabel={(o) => o.name}
  icon={<Building className="h-4 w-4" />}
  placeholder="Filtrar por organização"
/>

On this page