diff --git a/src/components/timer/header-timer.tsx b/src/components/timer/header-timer.tsx
index 98031ba..efbe94c 100644
--- a/src/components/timer/header-timer.tsx
+++ b/src/components/timer/header-timer.tsx
@@ -12,13 +12,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
+import { SearchableSelect } from "@/components/ui/searchable-select";
import { Pause, Play, Square, Timer as TimerIcon, Loader2, X, PlusCircle } from "lucide-react";
import { toast } from "sonner";
import { NewFeeItemDialog } from "@/components/fees/new-fee-item-dialog";
@@ -209,44 +203,37 @@ export function HeaderTimer() {
Client
- setClient(v || null)}
- >
-
-
-
-
- {clients.map((c) => (
- {c.name}
- ))}
-
-
+ placeholder="Select client"
+ searchPlaceholder="Search clients…"
+ emptyText="No clients found."
+ options={clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name }))}
+ />
Case
-
setCase(v || null)}
disabled={!timer.clientId}
- >
-
-
-
-
- {filteredCases.length === 0 ? (
- No cases for this client.
- ) : (
- filteredCases.map((c) => (
-
- {c.case_number}
- {c.title}
-
- ))
- )}
-
-
+ placeholder={timer.clientId ? "Select case" : "Pick a client first"}
+ searchPlaceholder="Search cases…"
+ emptyText="No cases for this client."
+ options={filteredCases.map((c) => ({
+ value: c.id,
+ label: `${c.case_number} — ${c.title}`,
+ keywords: `${c.case_number} ${c.title}`,
+ render: (
+
+ {c.case_number}
+ {c.title}
+
+ ),
+ }))}
+ />
@@ -263,26 +250,27 @@ export function HeaderTimer() {
{feeItems.length > 0 ? (
-
handleSelectFee(v === "__none" ? "" : v)}
- >
-
-
-
-
- No fee item
- {feeItems.map((f) => (
-
+ handleSelectFee(v)}
+ placeholder="No fee item"
+ searchPlaceholder="Search fee items…"
+ emptyText="No fee items found."
+ options={feeItems.map((f) => ({
+ value: f.id,
+ label: `${f.name} — ${formatCurrency(f.amount)}${f.pricing_type === "flat" ? " flat" : "/hr"}`,
+ keywords: `${f.name} ${f.description ?? ""}`,
+ render: (
+
{f.name}
{formatCurrency(f.amount)}
{f.pricing_type === "flat" ? " flat" : "/hr"}
-
- ))}
-
-
+
+ ),
+ }))}
+ />
) : (
No saved items yet. Click "Add new item" to save one for reuse.
diff --git a/src/components/ui/searchable-select.tsx b/src/components/ui/searchable-select.tsx
new file mode 100644
index 0000000..de7a756
--- /dev/null
+++ b/src/components/ui/searchable-select.tsx
@@ -0,0 +1,127 @@
+import * as React from "react";
+import { Check, ChevronsUpDown } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+
+export interface SearchableSelectOption {
+ value: string;
+ /** Visible label in the selected trigger */
+ label: string;
+ /** Custom render for the option row (defaults to label) */
+ render?: React.ReactNode;
+ /** Extra text used for fuzzy matching (case number, email, etc.) */
+ keywords?: string;
+ disabled?: boolean;
+}
+
+interface SearchableSelectProps {
+ value?: string | null;
+ onValueChange: (value: string) => void;
+ options: SearchableSelectOption[];
+ placeholder?: string;
+ searchPlaceholder?: string;
+ emptyText?: string;
+ disabled?: boolean;
+ className?: string;
+ triggerClassName?: string;
+ /** Width of popover content. Default: trigger width via "w-[--radix-popover-trigger-width]" */
+ contentClassName?: string;
+ /** Optional element rendered above the list (e.g. "Add new") */
+ footer?: React.ReactNode;
+}
+
+export function SearchableSelect({
+ value,
+ onValueChange,
+ options,
+ placeholder = "Select…",
+ searchPlaceholder = "Search…",
+ emptyText = "No results.",
+ disabled,
+ className,
+ triggerClassName,
+ contentClassName,
+ footer,
+}: SearchableSelectProps) {
+ const [open, setOpen] = React.useState(false);
+ const selected = options.find((o) => o.value === value);
+
+ return (
+
+
+
+
+
+ {selected ? selected.label : placeholder}
+
+
+
+
+
+ {
+ const opt = options.find((o) => o.value === itemValue);
+ if (!opt) return 0;
+ const hay = `${opt.label} ${opt.keywords ?? ""}`.toLowerCase();
+ return hay.includes(search.toLowerCase()) ? 1 : 0;
+ }}
+ >
+
+
+ {emptyText}
+
+ {options.map((opt) => (
+ {
+ onValueChange(v);
+ setOpen(false);
+ }}
+ >
+
+ {opt.render ?? opt.label}
+
+ ))}
+
+ {footer}
+
+
+
+
+
+ );
+}
diff --git a/src/routes/cases.new.tsx b/src/routes/cases.new.tsx
index d685840..156db0e 100644
--- a/src/routes/cases.new.tsx
+++ b/src/routes/cases.new.tsx
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { SearchableSelect } from "@/components/ui/searchable-select";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { ArrowLeft, Loader2 } from "lucide-react";
@@ -111,13 +112,17 @@ function NewCase() {
Client (optional)
- setForm({ ...form, client_id: v === "__none" ? "" : v })}>
-
-
- No client (attach later)
- {clients.map((c) => {c.name} )}
-
-
+ setForm({ ...form, client_id: v === "__none" ? "" : v })}
+ placeholder="No client"
+ searchPlaceholder="Search clients…"
+ emptyText="No clients found."
+ options={[
+ { value: "__none", label: "No client (attach later)" },
+ ...clients.map((c) => ({ value: c.id, label: c.name, keywords: c.name })),
+ ]}
+ />
Case number
diff --git a/src/routes/documents.pleading.new.tsx b/src/routes/documents.pleading.new.tsx
index 897c953..24a59f1 100644
--- a/src/routes/documents.pleading.new.tsx
+++ b/src/routes/documents.pleading.new.tsx
@@ -10,6 +10,7 @@ import { Textarea } from "@/components/ui/textarea";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
+import { SearchableSelect } from "@/components/ui/searchable-select";
import { Switch } from "@/components/ui/switch";
import { FL_CIRCUITS } from "@/lib/florida";
import { Packer } from "docx";
@@ -331,7 +332,7 @@ function PleadingNewPage() {
Client
- {
const next = v === "__none__" ? "" : v;
@@ -341,29 +342,32 @@ function PleadingNewPage() {
if (k && k.client_id !== next) setCaseId("");
}
}}
- >
-
-
- — None —
- {clientOptions.map((c) => (
- {c.name}
- ))}
-
-
+ placeholder="None"
+ searchPlaceholder="Search clients…"
+ emptyText="No clients found."
+ options={[
+ { value: "__none__", label: "— None —" },
+ ...clientOptions.map((c) => ({ value: c.id, label: c.name, keywords: c.name })),
+ ]}
+ />
Case
- setCaseId(v === "__none__" ? "" : v)}>
-
-
- — None —
- {filteredCases.map((k) => (
-
- {k.case_number} — {k.title}
-
- ))}
-
-
+ setCaseId(v === "__none__" ? "" : v)}
+ placeholder="None"
+ searchPlaceholder="Search cases…"
+ emptyText="No cases found."
+ options={[
+ { value: "__none__", label: "— None —" },
+ ...filteredCases.map((k) => ({
+ value: k.id,
+ label: `${k.case_number} — ${k.title}`,
+ keywords: `${k.case_number} ${k.title}`,
+ })),
+ ]}
+ />
diff --git a/src/routes/status.index.tsx b/src/routes/status.index.tsx
index 1b40a95..37fd7aa 100644
--- a/src/routes/status.index.tsx
+++ b/src/routes/status.index.tsx
@@ -14,6 +14,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { SearchableSelect } from "@/components/ui/searchable-select";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { Loader2, Send, ExternalLink } from "lucide-react";
@@ -109,19 +110,18 @@ function StatusUpdatesPage() {