1
0
Fork 0

feat(ui): searchable combobox across data-driven dropdowns

- new ui/combobox primitive (popover plus cmdk): search input, scrolling
  list, trigger-width popover, per-option disabled and hint lines
- converts every data-driven single-value select site-wide (blueprints,
  factions, accounts, assignments, NPCs, structures, locker items, seats,
  missions, ranks, resources, mod lists, event targets, sprints)
- long multi-select checkbox menus (event log types, kanban labels) gain
  search inputs following the resource picker pattern
- static short enum selects stay as plain selects
This commit is contained in:
Jason Fraley 2026-09-17 16:28:17 -04:00
parent 2bc61fe211
commit 947a5c1bbf
21 changed files with 651 additions and 597 deletions

View file

@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
import { ArrowLeftRightIcon, Loader2Icon } from "lucide-react"; import { ArrowLeftRightIcon, Loader2Icon } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Combobox } from "@/components/ui/combobox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -16,7 +17,14 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { requestTransfer } from "@/app/(frontend)/transfers/actions"; import { requestTransfer } from "@/app/(frontend)/transfers/actions";
@ -199,22 +207,20 @@ export function RequestTransferDialog({
<form className="flex flex-col gap-4" onSubmit={handleSubmit}> <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label htmlFor="transfer-source">Current assignment</Label> <Label htmlFor="transfer-source">Current assignment</Label>
<Select value={sourceId} onValueChange={setSourceId} disabled={pending}> <Combobox
<SelectTrigger id="transfer-source"> id="transfer-source"
<SelectValue placeholder="Select current assignment" /> value={sourceId}
</SelectTrigger> onValueChange={setSourceId}
<SelectContent> disabled={pending}
<SelectGroup> placeholder="Select current assignment"
{requestableAssignments.map((assignment) => ( searchPlaceholder="Search assignments..."
<SelectItem key={assignment.id} value={String(assignment.id)}> options={requestableAssignments.map((assignment) => ({
{assignment.callsign value: String(assignment.id),
? `${assignment.name} · ${assignment.callsign}` label: assignment.callsign
: assignment.name} ? `${assignment.name} · ${assignment.callsign}`
</SelectItem> : assignment.name,
))} }))}
</SelectGroup> />
</SelectContent>
</Select>
</div> </div>
<div className="flex flex-col gap-1.5" aria-busy={loadingDestinations}> <div className="flex flex-col gap-1.5" aria-busy={loadingDestinations}>
@ -222,26 +228,20 @@ export function RequestTransferDialog({
{loadingDestinations ? ( {loadingDestinations ? (
<Skeleton className="h-9 w-full" /> <Skeleton className="h-9 w-full" />
) : destinations.length > 0 ? ( ) : destinations.length > 0 ? (
<Select <Combobox
id="transfer-destination"
value={destinationId} value={destinationId}
onValueChange={setDestinationId} onValueChange={setDestinationId}
disabled={pending} disabled={pending}
> placeholder="Select destination assignment"
<SelectTrigger id="transfer-destination"> searchPlaceholder="Search assignments..."
<SelectValue placeholder="Select destination assignment" /> options={destinations.map((assignment) => ({
</SelectTrigger> value: String(assignment.id),
<SelectContent> label: assignment.callsign
<SelectGroup> ? `${assignment.name} · ${assignment.callsign}`
{destinations.map((assignment) => ( : assignment.name,
<SelectItem key={assignment.id} value={String(assignment.id)}> }))}
{assignment.callsign />
? `${assignment.name} · ${assignment.callsign}`
: assignment.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
) : ( ) : (
<Alert> <Alert>
<AlertDescription> <AlertDescription>
@ -271,10 +271,18 @@ export function RequestTransferDialog({
)} )}
<DialogFooter> <DialogFooter>
<Button type="button" variant="outline" onClick={() => setInternalOpen(false)} disabled={pending}> <Button
type="button"
variant="outline"
onClick={() => setInternalOpen(false)}
disabled={pending}
>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={pending || loadingDestinations || destinations.length === 0}> <Button
type="submit"
disabled={pending || loadingDestinations || destinations.length === 0}
>
{pending && <Loader2Icon data-icon="inline-start" className="animate-spin" />} {pending && <Loader2Icon data-icon="inline-start" className="animate-spin" />}
{pending ? "Submitting..." : "Submit request"} {pending ? "Submitting..." : "Submit request"}
</Button> </Button>

View file

@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { ArrowDownLeftIcon, ArrowRightLeftIcon, ArrowUpRightIcon } from "lucide-react"; import { ArrowDownLeftIcon, ArrowRightLeftIcon, ArrowUpRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Combobox } from "@/components/ui/combobox";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
@ -164,18 +165,17 @@ export function BankTransactionDialog({
{mode === "transfer" && ( {mode === "transfer" && (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Destination account</Label> <Label>Destination account</Label>
<Select value={toId} onValueChange={setToId}> <Combobox
<SelectTrigger> value={toId}
<SelectValue placeholder="Select account" /> onValueChange={setToId}
</SelectTrigger> placeholder="Select account"
<SelectContent> searchPlaceholder="Search accounts..."
{otherAccounts.map((a) => ( emptyMessage="No other accounts."
<SelectItem key={a.id} value={String(a.id)}> options={otherAccounts.map((a) => ({
{a.name} value: String(a.id),
</SelectItem> label: a.name,
))} }))}
</SelectContent> />
</Select>
</div> </div>
)} )}
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">

View file

@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Combobox } from "@/components/ui/combobox";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
Select, Select,
@ -117,18 +118,16 @@ export function CreateAccountDialog({
{accountType === "faction" && ( {accountType === "faction" && (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Owner faction</Label> <Label>Owner faction</Label>
<Select value={ownerFactionId} onValueChange={setOwnerFactionId}> <Combobox
<SelectTrigger> value={ownerFactionId}
<SelectValue placeholder="Select faction" /> onValueChange={setOwnerFactionId}
</SelectTrigger> placeholder="Select faction"
<SelectContent> searchPlaceholder="Search factions..."
{factions.map((f) => ( options={factions.map((f) => ({
<SelectItem key={f.id} value={String(f.id)}> value: String(f.id),
{f.name} label: f.name,
</SelectItem> }))}
))} />
</SelectContent>
</Select>
</div> </div>
)} )}
{accountType === "personal" && !isManager && ( {accountType === "personal" && !isManager && (

View file

@ -15,15 +15,8 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Combobox } from "@/components/ui/combobox";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { LaborClassificationOption, StructureStaffingOption } from "@/lib/base"; import type { LaborClassificationOption, StructureStaffingOption } from "@/lib/base";
import { currencyLabel, type CurrencyConfig } from "@/lib/banking/format"; import { currencyLabel, type CurrencyConfig } from "@/lib/banking/format";
@ -353,61 +346,51 @@ export function HireStaffDialog({
{fixedNpc?.name ?? `NPC #${fixedNpcId}`} {fixedNpc?.name ?? `NPC #${fixedNpcId}`}
</p> </p>
) : ( ) : (
<Select value={npcId} onValueChange={setNpcId}> <Combobox
<SelectTrigger> value={npcId}
<SelectValue placeholder="Select an NPC" /> onValueChange={setNpcId}
</SelectTrigger> placeholder="Select an NPC"
<SelectContent> searchPlaceholder="Search NPCs..."
<SelectGroup> emptyMessage="No candidates available."
{npcs.map((npc) => ( options={npcs.map((npc) => ({
<SelectItem key={npc.id} value={String(npc.id)}> value: String(npc.id),
{npc.name} label: npc.name,
</SelectItem> }))}
))} />
</SelectGroup>
</SelectContent>
</Select>
)} )}
</div> </div>
)} )}
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Structure</Label> <Label>Structure</Label>
<Select value={structureId} onValueChange={handleStructureChange}> <Combobox
<SelectTrigger> value={structureId}
<SelectValue placeholder="Select a structure" /> onValueChange={handleStructureChange}
</SelectTrigger> placeholder="Select a structure"
<SelectContent> searchPlaceholder="Search structures..."
<SelectGroup> options={structures.map((structure) => ({
{structures.map((structure) => ( value: String(structure.id),
<SelectItem key={structure.id} value={String(structure.id)}> label: structure.name,
{structure.name} }))}
</SelectItem> />
))}
</SelectGroup>
</SelectContent>
</Select>
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Position type</Label> <Label>Position type</Label>
<Select value={positionType} onValueChange={handlePositionTypeChange}> <Combobox
<SelectTrigger> value={positionType}
<SelectValue /> onValueChange={handlePositionTypeChange}
</SelectTrigger> searchPlaceholder="Search positions..."
<SelectContent> options={[
<SelectGroup> ...(fixedNpcId == null && approvedClassifications.length > 0
{fixedNpcId == null && approvedClassifications.length > 0 && ( ? [{ value: "labor", label: "Labor" }]
<SelectItem value="labor">Labor</SelectItem> : []),
)} ...approvedPositionTypes.map((position) => ({
{approvedPositionTypes.map((position) => ( value: position.value,
<SelectItem key={position.value} value={position.value}> label: position.label,
{position.label} })),
</SelectItem> ]}
))} />
</SelectGroup>
</SelectContent>
</Select>
{selectedStructure && {selectedStructure &&
approvedClassifications.length === 0 && approvedClassifications.length === 0 &&
approvedPositionTypes.length === 0 && ( approvedPositionTypes.length === 0 && (
@ -421,20 +404,15 @@ export function HireStaffDialog({
<> <>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Classification</Label> <Label>Classification</Label>
<Select value={laborClassification} onValueChange={handleClassificationChange}> <Combobox
<SelectTrigger> value={laborClassification}
<SelectValue /> onValueChange={handleClassificationChange}
</SelectTrigger> searchPlaceholder="Search classifications..."
<SelectContent> options={approvedClassifications.map((classification) => ({
<SelectGroup> value: String(classification.id),
{approvedClassifications.map((classification) => ( label: classification.name,
<SelectItem key={classification.id} value={String(classification.id)}> }))}
{classification.name} />
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
{selectedStructure && approvedClassifications.length === 0 && ( {selectedStructure && approvedClassifications.length === 0 && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
This structure has no approved labor positions. This structure has no approved labor positions.

View file

@ -4,6 +4,7 @@ import * as React from "react";
import { ChevronLeft, ChevronRight, RotateCcw, Search } from "lucide-react"; import { ChevronLeft, ChevronRight, RotateCcw, Search } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Combobox } from "@/components/ui/combobox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
@ -305,19 +306,20 @@ export default function EventLogTable() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Select value={targetCollection} onValueChange={setTargetCollection}> <Combobox
<SelectTrigger className="h-8 w-44"> className="h-8 w-44"
<SelectValue placeholder="Target" /> value={targetCollection}
</SelectTrigger> onValueChange={setTargetCollection}
<SelectContent className="max-h-72"> placeholder="Target"
<SelectItem value="all">All targets</SelectItem> searchPlaceholder="Search targets..."
{TARGET_COLLECTION_OPTIONS.map(([value, label]) => ( options={[
<SelectItem key={value} value={value}> { value: "all", label: "All targets" },
{label} ...TARGET_COLLECTION_OPTIONS.map(([value, label]) => ({
</SelectItem> value,
))} label,
</SelectContent> })),
</Select> ]}
/>
<Select value={source} onValueChange={(v) => setSource(v as SourceFilter)}> <Select value={source} onValueChange={(v) => setSource(v as SourceFilter)}>
<SelectTrigger className="h-8 w-36"> <SelectTrigger className="h-8 w-36">
@ -442,10 +444,7 @@ export default function EventLogTable() {
Showing {from.toLocaleString()}-{to.toLocaleString()} of {totalDocs.toLocaleString()} Showing {from.toLocaleString()}-{to.toLocaleString()} of {totalDocs.toLocaleString()}
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Select <Select value={String(pageSize)} onValueChange={(v) => setPageSize(Number(v))}>
value={String(pageSize)}
onValueChange={(v) => setPageSize(Number(v))}
>
<SelectTrigger className="h-8 w-28"> <SelectTrigger className="h-8 w-28">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>

View file

@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
import type { Ticket, User } from "@/payload-types"; import type { Ticket, User } from "@/payload-types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item"; import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@ -264,23 +265,21 @@ export function TicketDetail({
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label htmlFor="ticket-assignee">Assignee</Label> <Label htmlFor="ticket-assignee">Assignee</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Select <Combobox
id="ticket-assignee"
className="min-w-0 flex-1"
value={assignee} value={assignee}
onValueChange={setAssignee} onValueChange={setAssignee}
disabled={controlBusy !== null} disabled={controlBusy !== null}
> searchPlaceholder="Search members..."
<SelectTrigger id="ticket-assignee" className="min-w-0 flex-1"> options={[
<SelectValue /> { value: "unassigned", label: "Unassigned" },
</SelectTrigger> ...assigneeOptions.map((option) => ({
<SelectContent> value: String(option.id),
<SelectItem value="unassigned">Unassigned</SelectItem> label: option.label,
{assigneeOptions.map((option) => ( })),
<SelectItem key={option.id} value={String(option.id)}> ]}
{option.label} />
</SelectItem>
))}
</SelectContent>
</Select>
<Button <Button
size="sm" size="sm"
onClick={() => void applyControl("assignee")} onClick={() => void applyControl("assignee")}

View file

@ -10,6 +10,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Combobox } from "@/components/ui/combobox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@ -21,11 +22,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { import {
createFridaySideOp, createFridaySideOp,
updateFridaySideOp, updateFridaySideOp,
@ -238,7 +235,9 @@ export function CreateSideOpDialog({
} }
function modsInput() { function modsInput() {
return requiredModsId === "none" ? { requiredModsId: null } : { requiredModsId: Number(requiredModsId) }; return requiredModsId === "none"
? { requiredModsId: null }
: { requiredModsId: Number(requiredModsId) };
} }
async function handleModlistUpload(file: File) { async function handleModlistUpload(file: File) {
@ -344,7 +343,8 @@ export function CreateSideOpDialog({
estimatedDuration: Number(estimatedDuration) || 240, estimatedDuration: Number(estimatedDuration) || 240,
maxPlayers: Number(maxPlayers) || 32, maxPlayers: Number(maxPlayers) || 32,
...modsInput(), ...modsInput(),
campaignId: campaignChoice === "new" || campaignChoice === "none" ? null : Number(campaignChoice), campaignId:
campaignChoice === "new" || campaignChoice === "none" ? null : Number(campaignChoice),
newCampaignName: campaignChoice === "new" ? newCampaignName || null : null, newCampaignName: campaignChoice === "new" ? newCampaignName || null : null,
noCampaign: campaignChoice === "none", noCampaign: campaignChoice === "none",
...narrativeInput(), ...narrativeInput(),
@ -430,25 +430,20 @@ export function CreateSideOpDialog({
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="grid gap-1.5"> <div className="grid gap-1.5">
<Label>Map</Label> <Label>Map</Label>
<Select <Combobox
className="w-full"
value={mapChoice} value={mapChoice}
onValueChange={(v) => { onValueChange={(v) => {
setMapChoice(v); setMapChoice(v);
if (v !== "custom" && v !== "") setMapId(v); if (v !== "custom" && v !== "") setMapId(v);
}} }}
> placeholder="Choose a map"
<SelectTrigger className="w-full"> searchPlaceholder="Search maps..."
<SelectValue placeholder="Choose a map" /> options={[
</SelectTrigger> ...maps.map((m) => ({ value: String(m.id), label: m.name })),
<SelectContent> { value: "custom", label: "+ Custom map…" },
{maps.map((m) => ( ]}
<SelectItem key={m.id} value={String(m.id)}> />
{m.name}
</SelectItem>
))}
<SelectItem value="custom">+ Custom map…</SelectItem>
</SelectContent>
</Select>
</div> </div>
<div className="grid gap-1.5"> <div className="grid gap-1.5">
<Label>Mission type</Label> <Label>Mission type</Label>
@ -490,20 +485,22 @@ export function CreateSideOpDialog({
</div> </div>
<div className="grid gap-1.5"> <div className="grid gap-1.5">
<Label>Campaign</Label> <Label>Campaign</Label>
<Select value={campaignChoice} onValueChange={setCampaignChoice}> <Combobox
<SelectTrigger className="w-full"> className="w-full"
<SelectValue placeholder="Choose a campaign" /> value={campaignChoice}
</SelectTrigger> onValueChange={setCampaignChoice}
<SelectContent> placeholder="Choose a campaign"
<SelectItem value="none">No campaign (one-off op)</SelectItem> searchPlaceholder="Search campaigns..."
{campaigns.map((c) => ( options={[
<SelectItem key={c.id} value={String(c.id)} disabled={c.disabled}> { value: "none", label: "No campaign (one-off op)" },
{c.name} ...campaigns.map((c) => ({
</SelectItem> value: String(c.id),
))} label: c.name,
<SelectItem value="new">+ New campaign…</SelectItem> disabled: c.disabled,
</SelectContent> })),
</Select> { value: "new", label: "+ New campaign…" },
]}
/>
</div> </div>
</div> </div>
{mapChoice === "custom" && ( {mapChoice === "custom" && (
@ -531,19 +528,17 @@ export function CreateSideOpDialog({
<div className="grid gap-1.5"> <div className="grid gap-1.5">
<Label>Mod list</Label> <Label>Mod list</Label>
<Select value={requiredModsId} onValueChange={setRequiredModsId}> <Combobox
<SelectTrigger className="w-full"> className="w-full"
<SelectValue placeholder="No mod list (vanilla)" /> value={requiredModsId}
</SelectTrigger> onValueChange={setRequiredModsId}
<SelectContent> placeholder="No mod list (vanilla)"
<SelectItem value="none">No mod list (vanilla)</SelectItem> searchPlaceholder="Search mod lists..."
{modLists.map((l) => ( options={[
<SelectItem key={l.id} value={String(l.id)}> { value: "none", label: "No mod list (vanilla)" },
{l.name} ...modLists.map((l) => ({ value: String(l.id), label: l.name })),
</SelectItem> ]}
))} />
</SelectContent>
</Select>
<Input <Input
type="file" type="file"
accept=".html,.htm,.txt,text/html,text/plain" accept=".html,.htm,.txt,text/html,text/plain"
@ -555,8 +550,8 @@ export function CreateSideOpDialog({
}} }}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Upload the mod list file exported from the Arma 3 launcher (.html) or a .txt mod Upload the mod list file exported from the Arma 3 launcher (.html) or a .txt mod list,
list, or pick an existing one. or pick an existing one.
</p> </p>
</div> </div>

View file

@ -5,15 +5,9 @@ import { useRouter } from "next/navigation";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import type { Asset } from "@/payload-types"; import type { Asset } from "@/payload-types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -86,18 +80,17 @@ export function AddLockerItemDialog({ assets }: AddLockerItemDialogProps) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Asset</Label> <Label>Asset</Label>
<Select value={assetId} onValueChange={setAssetId}> <Combobox
<SelectTrigger> value={assetId}
<SelectValue placeholder="Select an asset" /> onValueChange={setAssetId}
</SelectTrigger> placeholder="Select an asset"
<SelectContent> searchPlaceholder="Search assets..."
{assets.map((a) => ( emptyMessage="No assets in your locker."
<SelectItem key={a.id} value={String(a.id)}> options={assets.map((a) => ({
{a.name} · {getAssetTypeLabel(a.assetType)} value: String(a.id),
</SelectItem> label: `${a.name} · ${getAssetTypeLabel(a.assetType)}`,
))} }))}
</SelectContent> />
</Select>
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Quantity</Label> <Label>Quantity</Label>

View file

@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { Asset } from "@/payload-types"; import type { Asset } from "@/payload-types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@ -14,13 +15,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { import {
getAssetTypeLabel, getAssetTypeLabel,
getAttachmentOptionsForItem, getAttachmentOptionsForItem,
@ -202,30 +196,24 @@ export function EquipmentEditorDialog({
className="grid grid-cols-[100px_1fr] items-center gap-2" className="grid grid-cols-[100px_1fr] items-center gap-2"
> >
<span className="text-xs text-muted-foreground">{category.label}</span> <span className="text-xs text-muted-foreground">{category.label}</span>
<Select <Combobox
value={installed ? String(installed.id) : "none"} value={installed ? String(installed.id) : "none"}
onValueChange={(v) => handleCategoryPick(category, v)} onValueChange={(v) => handleCategoryPick(category, v)}
disabled={busy} disabled={busy}
> placeholder={
<SelectTrigger> categoryOptions.length === 0
<SelectValue ? "No compatible items"
placeholder={ : "Choose an attachment"
categoryOptions.length === 0 }
? "No compatible items" searchPlaceholder="Search attachments..."
: "Choose an attachment" options={[
} { value: "none", label: "None" },
/> ...categoryOptions.map((o) => ({
</SelectTrigger> value: String(o.assetId),
<SelectContent> label: `${o.asset.name} · ${o.quantity > 0 ? `${o.quantity.toLocaleString()}×` : "installed"}`,
<SelectItem value="none">None</SelectItem> })),
{categoryOptions.map((o) => ( ]}
<SelectItem key={o.assetId} value={String(o.assetId)}> />
{o.asset.name} ·{" "}
{o.quantity > 0 ? `${o.quantity.toLocaleString()}×` : "installed"}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
); );
})} })}
@ -240,28 +228,22 @@ export function EquipmentEditorDialog({
<Label className="text-sm font-semibold">Skin</Label> <Label className="text-sm font-semibold">Skin</Label>
<div className="grid grid-cols-[100px_1fr] items-center gap-2"> <div className="grid grid-cols-[100px_1fr] items-center gap-2">
<span className="text-xs text-muted-foreground">Applied</span> <span className="text-xs text-muted-foreground">Applied</span>
<Select <Combobox
value={installedSkin ? String(installedSkin.id) : "none"} value={installedSkin ? String(installedSkin.id) : "none"}
onValueChange={handleSkinPick} onValueChange={handleSkinPick}
disabled={busy} disabled={busy}
> placeholder={
<SelectTrigger> mergedSkinOptions.length === 0 ? "No compatible skins" : "Choose a skin"
<SelectValue }
placeholder={ searchPlaceholder="Search skins..."
mergedSkinOptions.length === 0 ? "No compatible skins" : "Choose a skin" options={[
} { value: "none", label: "None" },
/> ...mergedSkinOptions.map((o) => ({
</SelectTrigger> value: String(o.assetId),
<SelectContent> label: `${o.asset.name} · ${o.quantity > 0 ? `${o.quantity.toLocaleString()}×` : "applied"}`,
<SelectItem value="none">None</SelectItem> })),
{mergedSkinOptions.map((o) => ( ]}
<SelectItem key={o.assetId} value={String(o.assetId)}> />
{o.asset.name} ·{" "}
{o.quantity > 0 ? `${o.quantity.toLocaleString()}×` : "applied"}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
{installedSkin && ( {installedSkin && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">

View file

@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { Loadout } from "@/payload-types"; import type { Loadout } from "@/payload-types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -160,24 +161,21 @@ export function LoadoutEditor({
return ( return (
<div key={slot.name} className="flex flex-col gap-1.5"> <div key={slot.name} className="flex flex-col gap-1.5">
<Label>{slot.label}</Label> <Label>{slot.label}</Label>
<Select <Combobox
value={slots[slot.name] ?? ""} value={slots[slot.name] ?? ""}
onValueChange={(v) => onValueChange={(v) =>
setSlots((prev) => ({ ...prev, [slot.name]: v === "none" ? "" : v })) setSlots((prev) => ({ ...prev, [slot.name]: v === "none" ? "" : v }))
} }
> placeholder={slot.description}
<SelectTrigger> searchPlaceholder="Search items..."
<SelectValue placeholder={slot.description} /> options={[
</SelectTrigger> { value: "none", label: "Empty" },
<SelectContent> ...options.map((item) => ({
<SelectItem value="none">Empty</SelectItem> value: String(item.assetId),
{options.map((item) => ( label: item.asset.name,
<SelectItem key={item.assetId} value={String(item.assetId)}> })),
{item.asset.name} ]}
</SelectItem> />
))}
</SelectContent>
</Select>
{options.length === 0 && ( {options.length === 0 && (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
No matching assets in your locker. No matching assets in your locker.

View file

@ -4,13 +4,7 @@ import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { Loadout } from "@/payload-types"; import type { Loadout } from "@/payload-types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import { Combobox } from "@/components/ui/combobox";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { setLoadoutSlot, equipLoadout } from "@/app/(frontend)/locker/actions"; import { setLoadoutSlot, equipLoadout } from "@/app/(frontend)/locker/actions";
import { import {
getLoadoutSlot, getLoadoutSlot,
@ -77,22 +71,18 @@ export function LockerCharacterPanel({ loadouts, items }: LockerCharacterPanelPr
</div> </div>
<div className="border-b border-border/50 p-2"> <div className="border-b border-border/50 p-2">
<Select <Combobox
className="h-8 rounded-sm border-border/60 bg-background/40 text-xs uppercase tracking-wide"
value={equippedLoadout ? String(equippedLoadout.id) : ""} value={equippedLoadout ? String(equippedLoadout.id) : ""}
onValueChange={changeLoadout} onValueChange={changeLoadout}
disabled={busy || loadouts.length === 0} disabled={busy || loadouts.length === 0}
> placeholder="SELECT LOADOUT"
<SelectTrigger className="h-8 rounded-sm border-border/60 bg-background/40 text-xs uppercase tracking-wide"> searchPlaceholder="Search loadouts..."
<SelectValue placeholder="SELECT LOADOUT" /> options={loadouts.map((loadout) => ({
</SelectTrigger> value: String(loadout.id),
<SelectContent> label: loadout.name,
{loadouts.map((loadout) => ( }))}
<SelectItem key={loadout.id} value={String(loadout.id)}> />
{loadout.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<div className="p-2"> <div className="p-2">

View file

@ -4,14 +4,8 @@ import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { Asset, Loadout } from "@/payload-types"; import type { Asset, Loadout } from "@/payload-types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item"; import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Wardrobe } from "./Wardrobe"; import { Wardrobe } from "./Wardrobe";
import { WardrobeSlotPicker } from "./WardrobeSlotPicker"; import { WardrobeSlotPicker } from "./WardrobeSlotPicker";
import { import {
@ -115,26 +109,20 @@ export function WardrobePanel({ userName, loadouts, equippedLoadout, items }: Wa
) : ( ) : (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Select <Combobox
className="flex-1"
value={equippedLoadout ? String(equippedLoadout.id) : ""} value={equippedLoadout ? String(equippedLoadout.id) : ""}
onValueChange={(v) => handleEquip(Number(v))} onValueChange={(v) => handleEquip(Number(v))}
disabled={busy} disabled={busy}
> placeholder={
<SelectTrigger className="flex-1"> equippedLoadout ? equippedLoadout.name : "Select a loadout to equip"
<SelectValue }
placeholder={ searchPlaceholder="Search loadouts..."
equippedLoadout ? equippedLoadout.name : "Select a loadout to equip" options={loadouts.map((l) => ({
} value: String(l.id),
/> label: l.name,
</SelectTrigger> }))}
<SelectContent> />
{loadouts.map((l) => (
<SelectItem key={l.id} value={String(l.id)}>
{l.name}
</SelectItem>
))}
</SelectContent>
</Select>
{equippedLoadout && ( {equippedLoadout && (
<Button <Button
variant="outline" variant="outline"

View file

@ -3,6 +3,7 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -13,15 +14,13 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { placeStructure } from "@/app/(frontend)/logistics/map/actions"; import { placeStructure } from "@/app/(frontend)/logistics/map/actions";
import { validatePlacement, type NodeInput, type StructureInput, type ZoneInput } from "@/lib/map/placement"; import {
validatePlacement,
type NodeInput,
type StructureInput,
type ZoneInput,
} from "@/lib/map/placement";
import type { MapPoint } from "@/lib/map/points"; import type { MapPoint } from "@/lib/map/points";
import type { MapConfig, NodeFeature, StructureFeature, ZoneFeature } from "@/lib/map/types"; import type { MapConfig, NodeFeature, StructureFeature, ZoneFeature } from "@/lib/map/types";
@ -86,9 +85,18 @@ export function PlacementDialog({
terrainType?: string | null; terrainType?: string | null;
constructionDurationMinutes?: number | null; constructionDurationMinutes?: number | null;
requiredTech?: { name?: string }[] | number[]; requiredTech?: { name?: string }[] | number[];
materials?: { resource?: { id: number; name?: string } | number; amount?: number | null }[]; materials?: {
resourcesInRange?: { resource?: { id: number } | number | null; range?: number | null }[]; resource?: { id: number; name?: string } | number;
structuresInRange?: { structure?: { id: number } | number | null; range?: number | null }[]; amount?: number | null;
}[];
resourcesInRange?: {
resource?: { id: number } | number | null;
range?: number | null;
}[];
structuresInRange?: {
structure?: { id: number } | number | null;
range?: number | null;
}[];
}[]; }[];
}; };
setBlueprints( setBlueprints(
@ -98,7 +106,9 @@ export function PlacementDialog({
terrainType: (doc.terrainType as "land" | "water" | null | undefined) ?? "land", terrainType: (doc.terrainType as "land" | "water" | null | undefined) ?? "land",
constructionDurationMinutes: doc.constructionDurationMinutes ?? null, constructionDurationMinutes: doc.constructionDurationMinutes ?? null,
requiredTech: (doc.requiredTech ?? []) requiredTech: (doc.requiredTech ?? [])
.filter((tech): tech is { name?: string } => typeof tech === "object" && tech !== null) .filter(
(tech): tech is { name?: string } => typeof tech === "object" && tech !== null,
)
.map((tech) => tech.name ?? "Unknown"), .map((tech) => tech.name ?? "Unknown"),
materials: (doc.materials ?? []).map((material) => ({ materials: (doc.materials ?? []).map((material) => ({
resource: resource:
@ -235,18 +245,18 @@ export function PlacementDialog({
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label htmlFor="place-blueprint">Blueprint</Label> <Label htmlFor="place-blueprint">Blueprint</Label>
<Select value={blueprintId} onValueChange={setBlueprintId}> <Combobox
<SelectTrigger id="place-blueprint" className="w-full"> id="place-blueprint"
<SelectValue placeholder="Select blueprint" /> value={blueprintId}
</SelectTrigger> onValueChange={setBlueprintId}
<SelectContent> placeholder="Select blueprint"
{blueprints.map((option) => ( searchPlaceholder="Search blueprints..."
<SelectItem key={option.id} value={String(option.id)}> emptyMessage="No blueprints available."
{option.name} options={blueprints.map((option) => ({
</SelectItem> value: String(option.id),
))} label: option.name,
</SelectContent> }))}
</Select> />
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
@ -261,19 +271,20 @@ export function PlacementDialog({
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label htmlFor="place-faction">Faction (optional)</Label> <Label htmlFor="place-faction">Faction (optional)</Label>
<Select value={factionId} onValueChange={setFactionId}> <Combobox
<SelectTrigger id="place-faction" className="w-full"> id="place-faction"
<SelectValue /> value={factionId}
</SelectTrigger> onValueChange={setFactionId}
<SelectContent> searchPlaceholder="Search factions..."
<SelectItem value="none">No faction</SelectItem> emptyMessage="No factions available."
{factions.map((faction) => ( options={[
<SelectItem key={faction.id} value={String(faction.id)}> { value: "none", label: "No faction" },
{faction.name} ...factions.map((faction) => ({
</SelectItem> value: String(faction.id),
))} label: faction.name,
</SelectContent> })),
</Select> ]}
/>
</div> </div>
{blueprint && ( {blueprint && (

View file

@ -5,17 +5,11 @@ import { useRouter } from "next/navigation";
import type { Asset, LockerStorage } from "@/payload-types"; import type { Asset, LockerStorage } from "@/payload-types";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Combobox } from "@/components/ui/combobox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -196,28 +190,21 @@ export function CreateListingDialog({
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>Item</Label> <Label>Item</Label>
<Select <Combobox
value={assetId} value={assetId}
onValueChange={(v) => { onValueChange={(v) => {
setAssetId(v); setAssetId(v);
setQuantity("1"); setQuantity("1");
}} }}
> placeholder="Select an item from your locker"
<SelectTrigger> searchPlaceholder="Search items..."
<SelectValue placeholder="Select an item from your locker" /> emptyMessage="Nothing is approved for trading yet."
</SelectTrigger> options={sellable.map((s) => ({
<SelectContent> value: String(s.assetId),
{sellable.map((s) => ( label: `${s.name} (${s.quantity.toLocaleString()}×)`,
<SelectItem disabled: !s.tradeable || !s.isLive,
key={s.assetId} }))}
value={String(s.assetId)} />
disabled={!s.tradeable || !s.isLive}
>
{s.name} ({s.quantity.toLocaleString()}×)
</SelectItem>
))}
</SelectContent>
</Select>
{sellable.length === 0 && ( {sellable.length === 0 && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{hasLockerItems {hasLockerItems

View file

@ -10,6 +10,7 @@ import {
setPokerHoleQueue, setPokerHoleQueue,
} from "@/app/(frontend)/minigames/poker/actions"; } from "@/app/(frontend)/minigames/poker/actions";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -65,11 +66,7 @@ function cardLabel(card: Card): string {
* what lets a single pick land; the previous null-propagating version made * what lets a single pick land; the previous null-propagating version made
* first picks no-ops and hole-card queuing impossible. * first picks no-ops and hole-card queuing impossible.
*/ */
export function applyCardPick( export function applyCardPick(value: Card | null, part: "rank" | "suit", next: string): Card {
value: Card | null,
part: "rank" | "suit",
next: string,
): Card {
if (part === "rank") { if (part === "rank") {
return { rank: Number(next) as Rank, suit: value?.suit ?? "s" }; return { rank: Number(next) as Rank, suit: value?.suit ?? "s" };
} }
@ -91,18 +88,17 @@ function CardPicker({
return ( return (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="w-10 text-xs text-muted-foreground">{label}</span> <span className="w-10 text-xs text-muted-foreground">{label}</span>
<Select value={value ? String(value.rank) : ""} onValueChange={setRank}> <Combobox
<SelectTrigger className="h-8 w-20 text-xs"> className="h-8 w-20 text-xs"
<SelectValue placeholder="Rank" /> value={value ? String(value.rank) : ""}
</SelectTrigger> onValueChange={setRank}
<SelectContent> placeholder="Rank"
{RANK_OPTIONS.map((option) => ( searchPlaceholder="Rank..."
<SelectItem key={option.value} value={String(option.value)}> options={RANK_OPTIONS.map((option) => ({
{option.label} value: String(option.value),
</SelectItem> label: option.label,
))} }))}
</SelectContent> />
</Select>
<Select value={value?.suit ?? ""} onValueChange={setSuit}> <Select value={value?.suit ?? ""} onValueChange={setSuit}>
<SelectTrigger className="h-8 w-24 text-xs"> <SelectTrigger className="h-8 w-24 text-xs">
<SelectValue placeholder="Suit" /> <SelectValue placeholder="Suit" />
@ -258,20 +254,19 @@ export function PokerDevPanel({
<section className="flex flex-col gap-2 border border-border p-3"> <section className="flex flex-col gap-2 border border-border p-3">
<h3 className="text-xs font-semibold uppercase tracking-wider">Next hand hole cards</h3> <h3 className="text-xs font-semibold uppercase tracking-wider">Next hand hole cards</h3>
<Select value={seatId} onValueChange={setSeatId}> <Combobox
<SelectTrigger className="h-8 text-xs"> className="h-8 text-xs"
<SelectValue placeholder="Player" /> value={seatId}
</SelectTrigger> onValueChange={setSeatId}
<SelectContent> placeholder="Player"
{view.players searchPlaceholder="Search players..."
.filter((player) => !player.eliminated) options={view.players
.map((player) => ( .filter((player) => !player.eliminated)
<SelectItem key={player.seatId} value={String(player.seatId)}> .map((player) => ({
{player.name} value: String(player.seatId),
</SelectItem> label: player.name,
))} }))}
</SelectContent> />
</Select>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<CardPicker label="Card 1" value={holeA} onChange={setHoleA} /> <CardPicker label="Card 1" value={holeA} onChange={setHoleA} />
<CardPicker label="Card 2" value={holeB} onChange={setHoleB} /> <CardPicker label="Card 2" value={holeB} onChange={setHoleB} />
@ -289,20 +284,19 @@ export function PokerDevPanel({
<section className="flex flex-col gap-2 border border-border p-3"> <section className="flex flex-col gap-2 border border-border p-3">
<h3 className="text-xs font-semibold uppercase tracking-wider">Stack adjustment</h3> <h3 className="text-xs font-semibold uppercase tracking-wider">Stack adjustment</h3>
<Select value={adjustSeat} onValueChange={setAdjustSeat}> <Combobox
<SelectTrigger className="h-8 text-xs"> className="h-8 text-xs"
<SelectValue placeholder="Player" /> value={adjustSeat}
</SelectTrigger> onValueChange={setAdjustSeat}
<SelectContent> placeholder="Player"
{view.players searchPlaceholder="Search players..."
.filter((player) => !player.eliminated) options={view.players
.map((player) => ( .filter((player) => !player.eliminated)
<SelectItem key={player.seatId} value={String(player.seatId)}> .map((player) => ({
{player.name} ({player.stack.toLocaleString()}) value: String(player.seatId),
</SelectItem> label: `${player.name} (${player.stack.toLocaleString()})`,
))} }))}
</SelectContent> />
</Select>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
type="number" type="number"

View file

@ -12,6 +12,7 @@ import {
} from "@/lib/evaluations/levels"; } from "@/lib/evaluations/levels";
import { submitEvaluation } from "@/app/(frontend)/profile/[username]/actions"; import { submitEvaluation } from "@/app/(frontend)/profile/[username]/actions";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -134,20 +135,20 @@ export function LeadershipEvaluationDialog({
<form className="flex flex-col gap-4" onSubmit={handleSubmit} aria-busy={pending}> <form className="flex flex-col gap-4" onSubmit={handleSubmit} aria-busy={pending}>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label htmlFor="evaluation-mission">Mission</Label> <Label htmlFor="evaluation-mission">Mission</Label>
<Select value={missionId} onValueChange={setMissionId} disabled={pending}> <Combobox
<SelectTrigger id="evaluation-mission" aria-invalid={error !== null}> id="evaluation-mission"
<SelectValue placeholder="Select a completed mission" /> aria-invalid={error !== null}
</SelectTrigger> value={missionId}
<SelectContent> onValueChange={setMissionId}
<SelectGroup> disabled={pending}
{missions.map((mission) => ( placeholder="Select a completed mission"
<SelectItem key={mission.id} value={String(mission.id)}> searchPlaceholder="Search missions..."
{mission.name} · {formatMissionDate(mission.startDateTime)} emptyMessage="No completed missions to evaluate."
</SelectItem> options={missions.map((mission) => ({
))} value: String(mission.id),
</SelectGroup> label: `${mission.name} · ${formatMissionDate(mission.startDateTime)}`,
</SelectContent> }))}
</Select> />
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">

View file

@ -3,6 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -11,13 +12,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { promoteMember } from "@/app/(frontend)/profile/[username]/actions"; import { promoteMember } from "@/app/(frontend)/profile/[username]/actions";
import { ArrowUpIcon, Loader2Icon, MedalIcon } from "lucide-react"; import { ArrowUpIcon, Loader2Icon, MedalIcon } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@ -97,21 +91,16 @@ export function PromoteButton({ username, currentRankName, targetRanks }: Promot
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Select <Combobox
value={selectedRankId != null ? String(selectedRankId) : undefined} value={selectedRankId != null ? String(selectedRankId) : undefined}
onValueChange={(v) => setRankId(Number(v))} onValueChange={(v) => setRankId(Number(v))}
> placeholder="Select a rank"
<SelectTrigger> searchPlaceholder="Search ranks..."
<SelectValue placeholder="Select a rank" /> options={targetRanks.map((r) => ({
</SelectTrigger> value: String(r.id),
<SelectContent> label: r.name,
{targetRanks.map((r) => ( }))}
<SelectItem key={r.id} value={String(r.id)}> />
{r.name}
</SelectItem>
))}
</SelectContent>
</Select>
{error && <p className="text-sm text-red-500">{error}</p>} {error && <p className="text-sm text-red-500">{error}</p>}
</div> </div>
<DialogFooter> <DialogFooter>

View file

@ -1,8 +1,11 @@
"use client"; "use client";
import { useState } from "react";
import { TagsIcon } from "lucide-react"; import { TagsIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Input } from "@/components/ui/input";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@ -11,15 +14,6 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { KanbanFiltersProps, SprintFilter } from "./KanbanBoard"; import type { KanbanFiltersProps, SprintFilter } from "./KanbanBoard";
function sprintFilterValue(value: string): SprintFilter { function sprintFilterValue(value: string): SprintFilter {
@ -29,7 +23,9 @@ function sprintFilterValue(value: string): SprintFilter {
} }
function activeSprints(sprints: KanbanFiltersProps["sprints"]) { function activeSprints(sprints: KanbanFiltersProps["sprints"]) {
const active = sprints.filter((sprint) => ["planned", "active", "in_progress"].includes(sprint.status)); const active = sprints.filter((sprint) =>
["planned", "active", "in_progress"].includes(sprint.status),
);
return active.length > 0 ? active : sprints; return active.length > 0 ? active : sprints;
} }
@ -43,6 +39,12 @@ export function KanbanFilters({
}: KanbanFiltersProps) { }: KanbanFiltersProps) {
const selectedLabelCount = selectedLabelIds.length; const selectedLabelCount = selectedLabelIds.length;
const sprintOptions = activeSprints(sprints); const sprintOptions = activeSprints(sprints);
const [labelQuery, setLabelQuery] = useState("");
const [labelsOpen, setLabelsOpen] = useState(false);
const labelNeedle = labelQuery.trim().toLowerCase();
const visibleLabels = labelNeedle
? labels.filter((label) => label.name.toLowerCase().includes(labelNeedle))
: labels;
const sprintLabel = const sprintLabel =
sprintFilter === "all" sprintFilter === "all"
? "All sprints" ? "All sprints"
@ -50,27 +52,23 @@ export function KanbanFilters({
return ( return (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Select <Combobox
value={String(sprintFilter)} value={String(sprintFilter)}
onValueChange={(value) => onSprintFilterChange(sprintFilterValue(value))} onValueChange={(value) => onSprintFilterChange(sprintFilterValue(value))}
> className="w-[190px]"
<SelectTrigger className="w-[190px]" aria-label="Filter by sprint"> aria-label="Filter by sprint"
<SelectValue placeholder={sprintLabel ?? "Select sprint"} /> placeholder={sprintLabel ?? "Select sprint"}
</SelectTrigger> searchPlaceholder="Search sprints..."
<SelectContent> options={[
<SelectGroup> { value: "all", label: "All sprints" },
<SelectLabel>Sprint</SelectLabel> ...sprintOptions.map((sprint) => ({
<SelectItem value="all">All sprints</SelectItem> value: String(sprint.id),
{sprintOptions.map((sprint) => ( label: sprint.name,
<SelectItem key={sprint.id} value={String(sprint.id)}> })),
{sprint.name} ]}
</SelectItem> />
))}
</SelectGroup>
</SelectContent>
</Select>
<DropdownMenu> <DropdownMenu open={labelsOpen} onOpenChange={setLabelsOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" aria-label="Filter by label"> <Button variant="outline" size="sm" aria-label="Filter by label">
<TagsIcon data-icon="inline-start" /> <TagsIcon data-icon="inline-start" />
@ -81,10 +79,22 @@ export function KanbanFilters({
<DropdownMenuContent align="start" className="w-56"> <DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel>Filter by label</DropdownMenuLabel> <DropdownMenuLabel>Filter by label</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="px-2 pb-1.5">
<Input
value={labelQuery}
onChange={(event) => setLabelQuery(event.target.value)}
placeholder="Search labels..."
className="h-7 text-xs"
/>
</div>
{labels.length === 0 ? ( {labels.length === 0 ? (
<span className="block px-2 py-1.5 text-sm text-muted-foreground">No labels yet</span> <span className="block px-2 py-1.5 text-sm text-muted-foreground">No labels yet</span>
) : visibleLabels.length === 0 ? (
<span className="block px-2 py-1.5 text-sm text-muted-foreground">
No matching labels
</span>
) : ( ) : (
labels.map((label) => ( visibleLabels.map((label) => (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
key={label.id} key={label.id}
checked={selectedLabelIds.includes(label.id)} checked={selectedLabelIds.includes(label.id)}

View file

@ -11,6 +11,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
interface LogEntry { interface LogEntry {
id: number; id: number;
@ -37,6 +38,8 @@ export function EventLedger({
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedTypes, setSelectedTypes] = useState<Set<string>>(new Set()); const [selectedTypes, setSelectedTypes] = useState<Set<string>>(new Set());
const [currentUserId, setCurrentUserId] = useState<number | null>(null); const [currentUserId, setCurrentUserId] = useState<number | null>(null);
const [typeQuery, setTypeQuery] = useState("");
const [filterMenuOpen, setFilterMenuOpen] = useState(false);
const initialDone = useRef(false); const initialDone = useRef(false);
const eventTypes = useMemo(() => { const eventTypes = useMemo(() => {
@ -44,6 +47,11 @@ export function EventLedger({
return Array.from(set).sort(); return Array.from(set).sort();
}, [entries]); }, [entries]);
const typeNeedle = typeQuery.trim().toLowerCase();
const visibleTypes = typeNeedle
? eventTypes.filter((t) => formatType(t).toLowerCase().includes(typeNeedle))
: eventTypes;
const filtered = useMemo( const filtered = useMemo(
() => (selectedTypes.size === 0 ? entries : entries.filter((e) => selectedTypes.has(e.type))), () => (selectedTypes.size === 0 ? entries : entries.filter((e) => selectedTypes.has(e.type))),
[entries, selectedTypes], [entries, selectedTypes],
@ -138,7 +146,13 @@ export function EventLedger({
</ItemHeader> </ItemHeader>
<ItemContent> <ItemContent>
{eventTypes.length > 1 && ( {eventTypes.length > 1 && (
<DropdownMenu> <DropdownMenu
open={filterMenuOpen}
onOpenChange={(next) => {
setFilterMenuOpen(next);
if (!next) setTypeQuery("");
}}
>
<span className="text-xs text-muted-foreground">Filter event logs...</span> <span className="text-xs text-muted-foreground">Filter event logs...</span>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <button
@ -148,22 +162,39 @@ export function EventLedger({
{dropdownLabel} {dropdownLabel}
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start"> <DropdownMenuContent align="start" className="w-60">
<div className="p-1.5">
<Input
value={typeQuery}
onChange={(event) => setTypeQuery(event.target.value)}
placeholder="Search event types..."
className="h-7 rounded-none text-xs"
/>
</div>
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
checked={selectedTypes.size === 0} checked={selectedTypes.size === 0}
onCheckedChange={() => setSelectedTypes(new Set())} onCheckedChange={() => setSelectedTypes(new Set())}
onSelect={(event) => event.preventDefault()}
> >
All All
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
{eventTypes.map((t) => ( <div className="max-h-64 overflow-y-auto">
<DropdownMenuCheckboxItem {visibleTypes.map((t) => (
key={t} <DropdownMenuCheckboxItem
checked={selectedTypes.has(t)} key={t}
onCheckedChange={() => toggleType(t)} checked={selectedTypes.has(t)}
> onCheckedChange={() => toggleType(t)}
{formatType(t)} onSelect={(event) => event.preventDefault()}
</DropdownMenuCheckboxItem> >
))} {formatType(t)}
</DropdownMenuCheckboxItem>
))}
{visibleTypes.length === 0 && (
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
No matches
</div>
)}
</div>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}

View file

@ -9,6 +9,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Combobox } from "@/components/ui/combobox";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -508,21 +509,20 @@ export function ManageStorageDialog({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Transport Vehicle</Label> <Label>Transport Vehicle</Label>
{availableVehicles.length > 0 ? ( {availableVehicles.length > 0 ? (
<Select value={shipmentVehicleId} onValueChange={setShipmentVehicleId}> <Combobox
<SelectTrigger> value={shipmentVehicleId}
<SelectValue placeholder="Select a vehicle..." /> onValueChange={setShipmentVehicleId}
</SelectTrigger> placeholder="Select a vehicle..."
<SelectContent> searchPlaceholder="Search vehicles..."
{availableVehicles.map((v) => { emptyMessage="No vehicles with range."
const vType = typeof v.type === "object" ? v.type : null; options={availableVehicles.map((v) => {
return ( const vType = typeof v.type === "object" ? v.type : null;
<SelectItem key={v.id} value={String(v.id)}> return {
{v.name} ({vType?.name ?? "Unknown"}), {v.currentFuel?.toFixed(0)} fuel value: String(v.id),
</SelectItem> label: `${v.name} (${vType?.name ?? "Unknown"}), ${v.currentFuel?.toFixed(0)} fuel`,
); };
})} })}
</SelectContent> />
</Select>
) : ( ) : (
<div className="flex items-center gap-2 p-3 rounded-md border border-amber-500/30 bg-amber-500/5 text-sm text-amber-400"> <div className="flex items-center gap-2 p-3 rounded-md border border-amber-500/30 bg-amber-500/5 text-sm text-amber-400">
<TruckIcon className="size-4 shrink-0" /> <TruckIcon className="size-4 shrink-0" />
@ -684,7 +684,7 @@ export function ManageStorageDialog({
<TabsContent value="deposit" className="flex flex-col gap-4 pt-4"> <TabsContent value="deposit" className="flex flex-col gap-4 pt-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Resource</Label> <Label>Resource</Label>
<Select <Combobox
value={depositResourceId} value={depositResourceId}
onValueChange={(val) => { onValueChange={(val) => {
setDepositResourceId(val); setDepositResourceId(val);
@ -692,29 +692,20 @@ export function ManageStorageDialog({
clampAmount(depositAmount, getDepositLimitInfoFor(val)?.max), clampAmount(depositAmount, getDepositLimitInfoFor(val)?.max),
); );
}} }}
> placeholder="Select a resource..."
<SelectTrigger> searchPlaceholder="Search resources..."
<SelectValue placeholder="Select a resource..." /> emptyMessage="Nothing may be deposited here."
</SelectTrigger> options={allResources
<SelectContent> .filter((r) => isResourceAllowed(r.id))
{allResources .map((resource) => {
.filter((r) => isResourceAllowed(r.id)) const cap = getAllowedDepositCap(resource.id);
.map((resource) => { return {
const cap = getAllowedDepositCap(resource.id); value: String(resource.id),
return ( label: resource.name,
<SelectItem key={resource.id} value={String(resource.id)}> hint: cap !== undefined ? `${cap.toLocaleString()} max` : undefined,
{resource.name} };
{cap !== undefined && ( })}
<span className="text-muted-foreground"> />
{" "}
({cap.toLocaleString()} max)
</span>
)}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Amount</Label> <Label>Amount</Label>
@ -754,30 +745,27 @@ export function ManageStorageDialog({
<TabsContent value="withdraw" className="flex flex-col gap-4 pt-4"> <TabsContent value="withdraw" className="flex flex-col gap-4 pt-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Resource</Label> <Label>Resource</Label>
<Select <Combobox
value={withdrawResourceId} value={withdrawResourceId}
onValueChange={(val) => { onValueChange={(val) => {
setWithdrawResourceId(val); setWithdrawResourceId(val);
const stored = findStoredResource(val); const stored = findStoredResource(val);
setWithdrawAmount(clampAmount(withdrawAmount, stored?.amount)); setWithdrawAmount(clampAmount(withdrawAmount, stored?.amount));
}} }}
> placeholder="Select a resource..."
<SelectTrigger> searchPlaceholder="Search resources..."
<SelectValue placeholder="Select a resource..." /> emptyMessage="Nothing stored yet."
</SelectTrigger> options={storedResources
<SelectContent> .map((entry) => {
{storedResources.map((entry) => {
const resource = entry.resource as Resource; const resource = entry.resource as Resource;
if (!resource || typeof resource === "number") return null; if (!resource || typeof resource === "number") return null;
return ( return {
<SelectItem key={entry.id ?? resource.id} value={String(resource.id)}> value: String(resource.id),
{resource.name} ({entry.amount.toLocaleString()}{" "} label: `${resource.name} (${entry.amount.toLocaleString()} ${pluralize(entry.amount, resource.unitOfMeasure)})`,
{pluralize(entry.amount, resource.unitOfMeasure)}) };
</SelectItem> })
); .filter((option) => option !== null)}
})} />
</SelectContent>
</Select>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Amount</Label> <Label>Amount</Label>
@ -817,51 +805,43 @@ export function ManageStorageDialog({
<TabsContent value="transfer" className="flex flex-col gap-4 pt-4"> <TabsContent value="transfer" className="flex flex-col gap-4 pt-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Destination</Label> <Label>Destination</Label>
<Select <Combobox
value={transferToId} value={transferToId}
onValueChange={(val) => { onValueChange={(val) => {
setTransferToId(val); setTransferToId(val);
setTransferAmount(clampAmount(transferAmount, getTransferMaxFor(val))); setTransferAmount(clampAmount(transferAmount, getTransferMaxFor(val)));
}} }}
> placeholder="Select a structure..."
<SelectTrigger> searchPlaceholder="Search structures..."
<SelectValue placeholder="Select a structure..." /> options={allStructures.map((s) => ({
</SelectTrigger> value: String(s.id),
<SelectContent> label: s.name,
{allStructures.map((s) => ( }))}
<SelectItem key={s.id} value={String(s.id)}> />
{s.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Resource</Label> <Label>Resource</Label>
<Select <Combobox
value={transferResourceId} value={transferResourceId}
onValueChange={(val) => { onValueChange={(val) => {
setTransferResourceId(val); setTransferResourceId(val);
const stored = findStoredResource(val); const stored = findStoredResource(val);
setTransferAmount(clampAmount(transferAmount, stored?.amount)); setTransferAmount(clampAmount(transferAmount, stored?.amount));
}} }}
> placeholder="Select a resource..."
<SelectTrigger> searchPlaceholder="Search resources..."
<SelectValue placeholder="Select a resource..." /> emptyMessage="Nothing stored yet."
</SelectTrigger> options={storedResources
<SelectContent> .map((entry) => {
{storedResources.map((entry) => {
const resource = entry.resource as Resource; const resource = entry.resource as Resource;
if (!resource || typeof resource === "number") return null; if (!resource || typeof resource === "number") return null;
return ( return {
<SelectItem key={entry.id ?? resource.id} value={String(resource.id)}> value: String(resource.id),
{resource.name} ({entry.amount.toLocaleString()}{" "} label: `${resource.name} (${entry.amount.toLocaleString()} ${pluralize(entry.amount, resource.unitOfMeasure)})`,
{pluralize(entry.amount, resource.unitOfMeasure)}) };
</SelectItem> })
); .filter((option) => option !== null)}
})} />
</SelectContent>
</Select>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Amount</Label> <Label>Amount</Label>

View file

@ -0,0 +1,122 @@
"use client";
import * as React from "react";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export interface ComboboxOption {
value: string;
label: string;
/** Optional secondary line rendered under the label. */
hint?: string;
/** Disabled options render muted and cannot be selected. */
disabled?: boolean;
}
interface ComboboxProps extends React.ComponentPropsWithoutRef<"button"> {
value?: string;
onValueChange: (value: string) => void;
options: ComboboxOption[];
placeholder?: string;
searchPlaceholder?: string;
emptyMessage?: string;
/** Trigger sizing classes (the dropdown keeps the trigger's width). */
className?: string;
/** Popover content classes. */
contentClassName?: string;
}
/**
* Searchable single-value dropdown (Popover + cmdk), a drop-in replacement for
* ui/select wherever the item list is data-driven and can grow long. The
* popover stays as wide as the trigger and the list scrolls at 16rem.
*/
function Combobox({
value,
onValueChange,
options,
placeholder = "Select...",
searchPlaceholder = "Search...",
emptyMessage = "No matches.",
disabled = false,
className,
contentClassName,
...rest
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const selected = options.find((option) => option.value === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
disabled={disabled}
role="combobox"
aria-expanded={open}
className={cn(
"flex h-9 w-full items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs outline-none",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
"disabled:cursor-not-allowed disabled:opacity-50",
!selected && "text-muted-foreground",
className,
)}
{...rest}
>
<span className="truncate">{selected ? selected.label : placeholder}</span>
<ChevronDown className="size-4 shrink-0 opacity-50" aria-hidden />
</PopoverTrigger>
<PopoverContent
className={cn("w-[var(--radix-popover-trigger-width)] p-0", contentClassName)}
align="start"
>
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList className="max-h-64">
<CommandEmpty>{emptyMessage}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={`${option.label} ${option.value}`}
disabled={option.disabled}
onSelect={() => {
onValueChange(option.value);
setOpen(false);
}}
>
<Check
className={cn(
"mr-1 size-4",
option.value === value ? "opacity-100" : "opacity-0",
)}
aria-hidden
/>
<span className="min-w-0">
<span className="block truncate">{option.label}</span>
{option.hint && (
<span className="block truncate text-xs text-muted-foreground">
{option.hint}
</span>
)}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
export { Combobox };