@handled-ai/design-system 0.18.11 → 0.18.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/components/account-contacts-popover.d.ts +5 -1
  2. package/dist/components/account-contacts-popover.js +25 -4
  3. package/dist/components/account-contacts-popover.js.map +1 -1
  4. package/dist/components/data-table-condition-filter.d.ts +2 -1
  5. package/dist/components/data-table-condition-filter.js +23 -41
  6. package/dist/components/data-table-condition-filter.js.map +1 -1
  7. package/dist/components/data-table-filter.js +8 -9
  8. package/dist/components/data-table-filter.js.map +1 -1
  9. package/dist/components/entity-panel.d.ts +2 -1
  10. package/dist/components/entity-panel.js +52 -45
  11. package/dist/components/entity-panel.js.map +1 -1
  12. package/dist/components/score-why-chips.d.ts +1 -1
  13. package/dist/components/signal-priority-popover.d.ts +1 -1
  14. package/dist/components/signal-priority-popover.js +4 -4
  15. package/dist/components/signal-priority-popover.js.map +1 -1
  16. package/dist/index.d.ts +2 -2
  17. package/dist/prototype/index.d.ts +1 -1
  18. package/dist/prototype/prototype-accounts-view.d.ts +1 -1
  19. package/dist/prototype/prototype-admin-view.d.ts +1 -1
  20. package/dist/prototype/prototype-config.d.ts +1 -1
  21. package/dist/prototype/prototype-inbox-view.d.ts +5 -3
  22. package/dist/prototype/prototype-inbox-view.js +11 -5
  23. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  24. package/dist/prototype/prototype-insights-view.d.ts +1 -1
  25. package/dist/prototype/prototype-shell.d.ts +1 -1
  26. package/dist/{signal-priority-popover-BT6CPYNs.d.ts → signal-priority-popover-BEDoPsNE.d.ts} +6 -0
  27. package/package.json +1 -2
  28. package/src/components/__tests__/account-contacts-popover.test.tsx +79 -0
  29. package/src/components/__tests__/data-table-condition-filter.test.tsx +96 -0
  30. package/src/components/__tests__/data-table-filter.test.tsx +45 -0
  31. package/src/components/__tests__/entity-panel-header.test.tsx +44 -0
  32. package/src/components/__tests__/signal-priority-popover.test.tsx +30 -0
  33. package/src/components/account-contacts-popover.tsx +29 -1
  34. package/src/components/data-table-condition-filter.tsx +32 -47
  35. package/src/components/data-table-filter.tsx +7 -10
  36. package/src/components/entity-panel.tsx +56 -40
  37. package/src/components/signal-priority-popover.tsx +15 -4
  38. package/src/prototype/__tests__/detail-view-title-slots.test.tsx +15 -0
  39. package/src/prototype/prototype-config.ts +2 -0
  40. package/src/prototype/prototype-inbox-view.tsx +17 -5
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { j as PrototypeConfig } from '../signal-priority-popover-BT6CPYNs.js';
2
+ import { j as PrototypeConfig } from '../signal-priority-popover-BEDoPsNE.js';
3
3
  import '../components/feedback-primitives.js';
4
4
  import '../components/quick-action-sidebar-nav.js';
5
5
  import '../components/quick-action-modal.js';
@@ -230,6 +230,8 @@ interface InboxViewConfig {
230
230
  attentionCount?: number;
231
231
  /** Render extra content inline with the detail title. */
232
232
  renderTitleExtra?: (item: QueueItem) => React.ReactNode;
233
+ /** Render a full-width action row below the detail title row. */
234
+ renderTitleActionRow?: (item: QueueItem) => React.ReactNode;
233
235
  /** Render supporting content below the detail title. */
234
236
  renderTitleSubtext?: (item: QueueItem) => React.ReactNode;
235
237
  /** Sort options for the inbox. When provided, a sort dropdown is rendered in the split view toolbar. */
@@ -397,8 +399,12 @@ interface PriorityFactor {
397
399
  tone: "alert" | "warn" | "info";
398
400
  /** Explicit semantic label - NOT inferred from score+weight. */
399
401
  direction: "raises" | "lowers" | "neutral";
402
+ /** Optional display label for the direction text. Keeps semantic direction icon/color unchanged. */
403
+ directionLabel?: string;
400
404
  /** 0-100 */
401
405
  score: number;
406
+ /** Optional display label rendered instead of the numeric score cell. */
407
+ displayValueLabel?: string;
402
408
  /** Evidence text (e.g. "$3.4M moved in 8h - current treasury balance $0.00"). */
403
409
  rationale: string;
404
410
  }
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
-
3
2
  "name": "@handled-ai/design-system",
4
- "version": "0.18.11",
3
+ "version": "0.18.12",
5
4
  "description": "Handled UI component library (shadcn-style, New York)",
6
5
  "type": "module",
7
6
  "packageManager": "pnpm@9.12.0",
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, vi } from "vitest"
2
+ import React from "react"
3
+ import { render, screen, fireEvent } from "@testing-library/react"
4
+
5
+ import { AccountContactsPopover } from "../account-contacts-popover"
6
+ import type { SuggestedContact } from "../suggested-actions"
7
+
8
+ const contacts: SuggestedContact[] = [
9
+ {
10
+ name: "Alex Admin",
11
+ role: "Controller",
12
+ email: "alex@example.com",
13
+ confirmed: true,
14
+ },
15
+ ]
16
+
17
+ describe("AccountContactsPopover", () => {
18
+ it("keeps default additive row selection behavior", () => {
19
+ const onSelect = vi.fn()
20
+ const onSelectTo = vi.fn()
21
+
22
+ render(
23
+ <AccountContactsPopover
24
+ contacts={contacts}
25
+ onSelect={onSelect}
26
+ onSelectTo={onSelectTo}
27
+ trigger={<button type="button">Contacts</button>}
28
+ />,
29
+ )
30
+
31
+ fireEvent.click(screen.getByRole("button", { name: "Contacts" }))
32
+ fireEvent.click(screen.getByText("Alex Admin"))
33
+
34
+ expect(onSelectTo).toHaveBeenCalledWith(contacts[0])
35
+ expect(onSelect).not.toHaveBeenCalled()
36
+ expect(screen.queryByRole("button", { name: "Add" })).toBeNull()
37
+ })
38
+
39
+ it("uses switch selection for default row clicks when onSelectSwitch is provided", () => {
40
+ const onSelect = vi.fn()
41
+ const onSelectTo = vi.fn()
42
+ const onSelectSwitch = vi.fn()
43
+
44
+ render(
45
+ <AccountContactsPopover
46
+ contacts={contacts}
47
+ onSelect={onSelect}
48
+ onSelectTo={onSelectTo}
49
+ onSelectSwitch={onSelectSwitch}
50
+ trigger={<button type="button">Contacts</button>}
51
+ />,
52
+ )
53
+
54
+ fireEvent.click(screen.getByRole("button", { name: "Contacts" }))
55
+ fireEvent.click(screen.getByRole("button", { name: /switch alex admin/i }))
56
+
57
+ expect(onSelectSwitch).toHaveBeenCalledWith(contacts[0])
58
+ expect(onSelectTo).not.toHaveBeenCalled()
59
+ expect(onSelect).not.toHaveBeenCalled()
60
+ })
61
+
62
+ it("allows custom default select label for switch action copy", () => {
63
+ const onSelect = vi.fn()
64
+ const onSelectSwitch = vi.fn()
65
+
66
+ render(
67
+ <AccountContactsPopover
68
+ contacts={contacts}
69
+ onSelect={onSelect}
70
+ onSelectSwitch={onSelectSwitch}
71
+ defaultSelectLabel="Replace"
72
+ trigger={<button type="button">Contacts</button>}
73
+ />,
74
+ )
75
+
76
+ fireEvent.click(screen.getByRole("button", { name: "Contacts" }))
77
+ expect(screen.getByRole("button", { name: /replace alex admin/i })).toBeTruthy()
78
+ })
79
+ })
@@ -161,6 +161,14 @@ describe("DataTableConditionFilter", () => {
161
161
  fireEvent.click(fieldOption!);
162
162
  };
163
163
 
164
+ const expectFieldTriggerHasNoFieldTypeIcon = (container: HTMLElement) => {
165
+ const fieldTrigger = container.querySelector('[data-slot="condition-row"] [data-slot="select-trigger"]');
166
+ expect(fieldTrigger).not.toBeNull();
167
+ expect(fieldTrigger?.querySelector("svg.lucide-ellipsis")).toBeNull();
168
+ expect(fieldTrigger?.querySelector("svg.lucide-list-filter")).toBeNull();
169
+ expect(fieldTrigger?.querySelector("svg.lucide-list-checks")).toBeNull();
170
+ };
171
+
164
172
  it("renders a polished empty panel with Add filter, disabled Add filter group, and quiet Clear filters", () => {
165
173
  const { container } = render(
166
174
  <DataTableConditionFilter
@@ -244,6 +252,8 @@ describe("DataTableConditionFilter", () => {
244
252
  const { container } = renderOptionFilter();
245
253
  chooseField(container, "Stage");
246
254
 
255
+ expectFieldTriggerHasNoFieldTypeIcon(container);
256
+
247
257
  expect(getOperators(selectField)).toEqual(["eq", "neq", "is_null", "is_not_null"]);
248
258
  fireEvent.click(screen.getByText("Qualified"));
249
259
  fireEvent.click(screen.getByText("Apply"));
@@ -295,10 +305,96 @@ describe("DataTableConditionFilter", () => {
295
305
  });
296
306
  });
297
307
 
308
+ it("uses DataTableFilter threshold semantics for option search", () => {
309
+ const thresholdField: ConditionFieldDef = {
310
+ id: "threshold",
311
+ label: "Threshold",
312
+ type: "select",
313
+ searchable: { threshold: 2 },
314
+ options: [
315
+ { label: "One", value: "one" },
316
+ { label: "Two", value: "two" },
317
+ ],
318
+ };
319
+ const aboveThresholdField: ConditionFieldDef = {
320
+ ...thresholdField,
321
+ id: "above_threshold",
322
+ options: [
323
+ { label: "One", value: "one" },
324
+ { label: "Two", value: "two" },
325
+ { label: "Three", value: "three" },
326
+ ],
327
+ };
328
+
329
+ const { rerender } = render(
330
+ <DataTableConditionFilter
331
+ fields={[thresholdField]}
332
+ conditions={[]}
333
+ onConditionsChange={onConditionsChange}
334
+ />,
335
+ );
336
+
337
+ fireEvent.click(screen.getByText("Add filter"));
338
+ expect(screen.queryByPlaceholderText("Search options...")).toBeNull();
339
+
340
+ rerender(
341
+ <DataTableConditionFilter
342
+ fields={[aboveThresholdField]}
343
+ conditions={[]}
344
+ onConditionsChange={onConditionsChange}
345
+ />,
346
+ );
347
+ expect(screen.getByPlaceholderText("Search options...")).toBeDefined();
348
+ });
349
+
350
+ it("does not apply a stale search query after switching to a non-searchable select field", () => {
351
+ const searchableOwnerField: ConditionFieldDef = {
352
+ id: "owner",
353
+ label: "Owner",
354
+ type: "select",
355
+ searchable: true,
356
+ options: [
357
+ { label: "Alice Adams", value: "alice@example.com" },
358
+ { label: "Bob Brown", value: "bob@example.com" },
359
+ ],
360
+ };
361
+ const statusField: ConditionFieldDef = {
362
+ id: "status",
363
+ label: "Status",
364
+ type: "select",
365
+ searchable: false,
366
+ options: [
367
+ { label: "Active", value: "active" },
368
+ { label: "Inactive", value: "inactive" },
369
+ ],
370
+ };
371
+
372
+ const { container } = render(
373
+ <DataTableConditionFilter
374
+ fields={[searchableOwnerField, statusField]}
375
+ conditions={[]}
376
+ onConditionsChange={onConditionsChange}
377
+ />,
378
+ );
379
+
380
+ fireEvent.click(screen.getByText("Add filter"));
381
+ fireEvent.change(screen.getByPlaceholderText("Search options..."), {
382
+ target: { value: "bob" },
383
+ });
384
+ chooseField(container, "Status");
385
+
386
+ expect(screen.queryByPlaceholderText("Search options...")).toBeNull();
387
+ expect(screen.getByText("Active")).toBeDefined();
388
+ expect(screen.getByText("Inactive")).toBeDefined();
389
+ expect(screen.queryByText("No options")).toBeNull();
390
+ });
391
+
298
392
  it("renders multi-select checkboxes, toggles multiple options, and commits an in condition", () => {
299
393
  const { container } = renderOptionFilter();
300
394
  chooseField(container, "Industry");
301
395
 
396
+ expectFieldTriggerHasNoFieldTypeIcon(container);
397
+
302
398
  expect(getOperators(multiSelectField)).toEqual(["in", "is_null", "is_not_null"]);
303
399
 
304
400
  const financeCheckbox = screen.getByRole("checkbox", { name: "Finance" });
@@ -367,4 +367,49 @@ describe("DataTableFilter", () => {
367
367
 
368
368
  expect(screen.getByText("Advanced filters")).toBeDefined();
369
369
  });
370
+
371
+ it("renders badge with bg-muted and text-foreground when activeCount > 0", () => {
372
+ render(
373
+ <DataTableFilter
374
+ categories={[
375
+ {
376
+ id: "status",
377
+ label: "Status",
378
+ icon: ListFilter,
379
+ options: ["Open", "Closed"],
380
+ },
381
+ ]}
382
+ selectedFilters={{ status: ["Open"] }}
383
+ onToggleFilter={() => {}}
384
+ />
385
+ );
386
+
387
+ const badge = screen.getByText("1");
388
+ expect(badge.tagName).toBe("SPAN");
389
+ expect(badge.className).toContain("bg-muted");
390
+ expect(badge.className).toContain("text-foreground");
391
+ });
392
+
393
+ it("does not render badge when activeCount is 0", () => {
394
+ const { container } = render(
395
+ <DataTableFilter
396
+ categories={[
397
+ {
398
+ id: "status",
399
+ label: "Status",
400
+ icon: ListFilter,
401
+ options: ["Open", "Closed"],
402
+ },
403
+ ]}
404
+ selectedFilters={{}}
405
+ onToggleFilter={() => {}}
406
+ />
407
+ );
408
+
409
+ const button = container.querySelector("button");
410
+ expect(button).not.toBeNull();
411
+ // The button should contain "Filter" text but no badge span with a count
412
+ const spans = button!.querySelectorAll("span.bg-muted");
413
+ expect(spans.length).toBe(0);
414
+ });
370
415
  });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect, vi } from "vitest"
2
+ import React from "react"
3
+ import { render, screen } from "@testing-library/react"
4
+
5
+ import { EntityPanel, EntityPanelHeader } from "../entity-panel"
6
+
7
+ function renderHeader(node: React.ReactNode) {
8
+ return render(
9
+ <EntityPanel isOpen onClose={vi.fn()}>
10
+ {node}
11
+ </EntityPanel>,
12
+ )
13
+ }
14
+
15
+ describe("EntityPanelHeader", () => {
16
+ it("preserves headerAction and panel controls", () => {
17
+ renderHeader(
18
+ <EntityPanelHeader
19
+ title="Acme Corp"
20
+ headerAction={<button type="button">Mercury Admin</button>}
21
+ />,
22
+ )
23
+
24
+ expect(screen.getByText("Acme Corp")).toBeTruthy()
25
+ expect(screen.getByRole("button", { name: "Mercury Admin" })).toBeTruthy()
26
+ expect(screen.getByTitle("Copy Link")).toBeTruthy()
27
+ expect(screen.getByTitle("Wide")).toBeTruthy()
28
+ expect(screen.getByTitle("Close")).toBeTruthy()
29
+ })
30
+
31
+ it("renders subtitle and secondary action on the secondary row", () => {
32
+ renderHeader(
33
+ <EntityPanelHeader
34
+ title="Acme Corp"
35
+ subtitle="Preferred account · Treasury"
36
+ headerSecondaryAction={<button type="button">Quick action</button>}
37
+ />,
38
+ )
39
+
40
+ expect(screen.getByText("Preferred account · Treasury")).toBeTruthy()
41
+ expect(screen.getByRole("button", { name: "Quick action" })).toBeTruthy()
42
+ expect(screen.getByTitle("Copy Link")).toBeTruthy()
43
+ })
44
+ })
@@ -157,6 +157,36 @@ describe("SignalPriorityPopover", () => {
157
157
  expect(neutralRow.textContent).toContain("50")
158
158
  })
159
159
 
160
+
161
+ it("keeps default direction and score labels unchanged", () => {
162
+ render(<SignalPriorityPopover {...defaultProps} />)
163
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
164
+
165
+ const row = screen.getByTestId("factor-row-test_severity")
166
+ expect(row.textContent).toContain("Raises")
167
+ expect(row.textContent).toContain("85")
168
+ expect(row.textContent).toContain("/100")
169
+ })
170
+
171
+ it("renders custom direction and display value labels", () => {
172
+ const customFactors: PriorityFactor[] = [
173
+ {
174
+ ...mockFactors[0],
175
+ directionLabel: "Raises urgency",
176
+ displayValueLabel: "Open window",
177
+ score: 0,
178
+ },
179
+ ]
180
+
181
+ render(<SignalPriorityPopover {...defaultProps} factors={customFactors} />)
182
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
183
+
184
+ const row = screen.getByTestId("factor-row-test_severity")
185
+ expect(row.textContent).toContain("Raises urgency")
186
+ expect(row.textContent).toContain("Open window")
187
+ expect(row.textContent).not.toContain("Raises0/100")
188
+ })
189
+
160
190
  it("renders Contributing factors section label", () => {
161
191
  render(<SignalPriorityPopover {...defaultProps} />)
162
192
  fireEvent.click(screen.getByTestId("priority-popover-trigger"))
@@ -32,6 +32,10 @@ export interface AccountContactsPopoverProps {
32
32
  onSelectTo?: (contact: SuggestedContact) => void
33
33
  onSelectCc?: (contact: SuggestedContact) => void
34
34
  onSelectBcc?: (contact: SuggestedContact) => void
35
+ /** Label for the default contact row action. Defaults to "Add" or "Switch" when onSelectSwitch is provided. */
36
+ defaultSelectLabel?: string
37
+ /** Optional replacement-selection callback. When provided, row clicks call this instead of additive onSelect/onSelectTo. */
38
+ onSelectSwitch?: (contact: SuggestedContact) => void
35
39
  onViewAll?: () => void
36
40
  onOpenRecentActivity?: () => void
37
41
  trigger: React.ReactNode
@@ -44,6 +48,8 @@ export function AccountContactsPopover({
44
48
  onSelectTo,
45
49
  onSelectCc,
46
50
  onSelectBcc,
51
+ defaultSelectLabel,
52
+ onSelectSwitch,
47
53
  onViewAll,
48
54
  onOpenRecentActivity,
49
55
  trigger,
@@ -52,6 +58,15 @@ export function AccountContactsPopover({
52
58
  const [open, setOpen] = React.useState(false)
53
59
  const triggerRef = React.useRef<HTMLDivElement>(null)
54
60
  const [popoverStyle, setPopoverStyle] = React.useState<React.CSSProperties>({})
61
+ const resolvedDefaultSelectLabel = defaultSelectLabel ?? (onSelectSwitch ? "Switch" : undefined)
62
+ const handleDefaultSelect = React.useCallback((contact: SuggestedContact) => {
63
+ if (onSelectSwitch) {
64
+ onSelectSwitch(contact)
65
+ } else {
66
+ (onSelectTo ?? onSelect)(contact)
67
+ }
68
+ setOpen(false)
69
+ }, [onSelect, onSelectSwitch, onSelectTo])
55
70
 
56
71
  React.useEffect(() => {
57
72
  if (open && triggerRef.current) {
@@ -84,7 +99,8 @@ export function AccountContactsPopover({
84
99
  <div
85
100
  key={i}
86
101
  role="button"
87
- onClick={() => { (onSelectTo ?? onSelect)(c); setOpen(false) }}
102
+ onClick={() => handleDefaultSelect(c)}
103
+ aria-label={resolvedDefaultSelectLabel ? `${resolvedDefaultSelectLabel} ${c.name}` : undefined}
88
104
  className="flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-muted/50 transition-colors cursor-pointer"
89
105
  >
90
106
  <div className="w-7 h-7 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground shrink-0">
@@ -115,6 +131,18 @@ export function AccountContactsPopover({
115
131
  )}
116
132
  </div>
117
133
  <div className="ml-2 flex items-center gap-1.5 shrink-0">
134
+ {resolvedDefaultSelectLabel && (
135
+ <button
136
+ type="button"
137
+ onClick={(e) => {
138
+ e.stopPropagation()
139
+ handleDefaultSelect(c)
140
+ }}
141
+ className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
142
+ >
143
+ {resolvedDefaultSelectLabel}
144
+ </button>
145
+ )}
118
146
  {onSelectTo && (
119
147
  <button
120
148
  type="button"
@@ -2,16 +2,11 @@
2
2
 
3
3
  import * as React from "react"
4
4
  import {
5
- CalendarDays,
6
5
  Check,
7
- DollarSign,
8
6
  Eye,
9
- Hash,
10
7
  MoreHorizontal,
11
8
  Plus,
12
9
  Trash2,
13
- Type,
14
- type LucideIcon,
15
10
  } from "lucide-react"
16
11
 
17
12
  import { cn } from "../lib/utils"
@@ -103,6 +98,22 @@ const NUMERIC_OPERATORS: ConditionOperator[] = [
103
98
  "is_not_null",
104
99
  ]
105
100
 
101
+ const NAV_KEYS = new Set(["ArrowDown", "ArrowUp", "Enter", "Escape", "Tab"])
102
+
103
+ export function shouldShowOptionSearch(
104
+ searchable: ConditionFieldDef["searchable"],
105
+ optionCount: number,
106
+ defaultThreshold = 8,
107
+ ): boolean {
108
+ if (searchable === true) return true
109
+ if (searchable === false) return false
110
+ const threshold =
111
+ typeof searchable === "object"
112
+ ? (searchable.threshold ?? defaultThreshold)
113
+ : defaultThreshold
114
+ return optionCount > threshold
115
+ }
116
+
106
117
  const DEFAULT_OPERATORS: Record<ConditionFieldDef["type"], ConditionOperator[]> = {
107
118
  text: ["eq", "neq", "is_null", "is_not_null"],
108
119
  number: NUMERIC_OPERATORS,
@@ -241,19 +252,6 @@ function getCommittedConditions(
241
252
  .filter((condition) => isCompleteCondition(condition, fields))
242
253
  }
243
254
 
244
- const FIELD_ICON_BY_TYPE: Record<ConditionFieldDef["type"], LucideIcon> = {
245
- text: Type,
246
- number: Hash,
247
- currency: DollarSign,
248
- date: CalendarDays,
249
- select: MoreHorizontal,
250
- multi_select: MoreHorizontal,
251
- }
252
-
253
- function getFieldIcon(type: ConditionFieldDef["type"]): LucideIcon {
254
- return FIELD_ICON_BY_TYPE[type]
255
- }
256
-
257
255
  // ── Condition Row ──────────────────────────────────────────────
258
256
 
259
257
  function getInputType(fieldType: ConditionFieldDef["type"]): "text" | "number" | "date" {
@@ -278,18 +276,6 @@ interface ConditionValueInputProps {
278
276
  onCommit: () => void
279
277
  }
280
278
 
281
- function shouldShowOptionSearch(
282
- fieldDef: ConditionFieldDef,
283
- optionCount: number,
284
- ): boolean {
285
- if (fieldDef.searchable === true) return true
286
- if (fieldDef.searchable === false) return false
287
- if (typeof fieldDef.searchable === "object") {
288
- return optionCount >= (fieldDef.searchable.threshold ?? 8)
289
- }
290
- return false
291
- }
292
-
293
279
  function SelectConditionValueInput({
294
280
  condition,
295
281
  fieldDef,
@@ -297,11 +283,15 @@ function SelectConditionValueInput({
297
283
  }: Pick<ConditionValueInputProps, "condition" | "fieldDef" | "onSelectValueChange">) {
298
284
  const [query, setQuery] = React.useState("")
299
285
  const options = normalizeFieldOptions(fieldDef)
300
- const normalizedQuery = query.trim().toLowerCase()
286
+ const showSearch = shouldShowOptionSearch(fieldDef.searchable, options.length)
287
+ const normalizedQuery = showSearch ? query.trim().toLowerCase() : ""
301
288
  const filteredOptions = normalizedQuery
302
289
  ? options.filter((option) => option.label.toLowerCase().includes(normalizedQuery))
303
290
  : options
304
- const showSearch = shouldShowOptionSearch(fieldDef, options.length)
291
+
292
+ React.useEffect(() => {
293
+ setQuery("")
294
+ }, [fieldDef.id])
305
295
 
306
296
  return (
307
297
  <Select
@@ -318,7 +308,11 @@ function SelectConditionValueInput({
318
308
  value={query}
319
309
  onChange={(event) => setQuery(event.target.value)}
320
310
  onClick={(event) => event.stopPropagation()}
321
- onKeyDown={(event) => event.stopPropagation()}
311
+ onKeyDown={(event) => {
312
+ if (!NAV_KEYS.has(event.key)) {
313
+ event.stopPropagation()
314
+ }
315
+ }}
322
316
  placeholder="Search options..."
323
317
  className="h-7 text-xs"
324
318
  />
@@ -453,8 +447,6 @@ function ConditionRow({
453
447
  const fieldDef = fields.find((f) => f.id === condition.field) ?? fields[0]
454
448
  const operators = getOperators(fieldDef)
455
449
  const isUnary = isUnaryOperator(condition.operator)
456
- const FieldIcon = getFieldIcon(fieldDef.type)
457
-
458
450
  const handleFieldChange = (newFieldId: string) => {
459
451
  const newFieldDef = fields.find((f) => f.id === newFieldId) ?? fields[0]
460
452
  if (!newFieldDef) return
@@ -512,21 +504,14 @@ function ConditionRow({
512
504
 
513
505
  <Select value={condition.field} onValueChange={handleFieldChange}>
514
506
  <SelectTrigger className="h-8 w-full justify-start gap-2" size="sm">
515
- <FieldIcon className="h-3.5 w-3.5 text-muted-foreground" />
516
507
  <SelectValue placeholder={fieldDef.label} />
517
508
  </SelectTrigger>
518
509
  <SelectContent>
519
- {fields.map((field) => {
520
- const Icon = getFieldIcon(field.type)
521
- return (
522
- <SelectItem key={field.id} value={field.id}>
523
- <span className="inline-flex items-center gap-2">
524
- <Icon className="h-3.5 w-3.5 text-muted-foreground" />
525
- {field.label}
526
- </span>
527
- </SelectItem>
528
- )
529
- })}
510
+ {fields.map((field) => (
511
+ <SelectItem key={field.id} value={field.id}>
512
+ {field.label}
513
+ </SelectItem>
514
+ ))}
530
515
  </SelectContent>
531
516
  </Select>
532
517
 
@@ -9,6 +9,7 @@ import { cn } from "../lib/utils"
9
9
  import { Button } from "./button"
10
10
  import {
11
11
  DataTableConditionFilter,
12
+ shouldShowOptionSearch,
12
13
  type ConditionFieldDef,
13
14
  type ConditionFilterValue,
14
15
  } from "./data-table-condition-filter"
@@ -160,7 +161,7 @@ export function DataTableFilter({
160
161
  <ListFilter className="h-3.5 w-3.5" />
161
162
  Filter
162
163
  {activeCount > 0 ? (
163
- <span className="rounded bg-muted px-1.5 py-0 text-[10px] font-semibold">
164
+ <span className="rounded bg-muted px-1.5 py-0 text-[10px] font-semibold text-foreground">
164
165
  {activeCount}
165
166
  </span>
166
167
  ) : null}
@@ -214,15 +215,11 @@ export function DataTableFilter({
214
215
  getOptionLabel(opt).toLowerCase().includes(subQuery)
215
216
  )
216
217
  : category.options
217
- const shouldShowSubmenuSearch = (() => {
218
- if (category.searchable === true) return true
219
- if (category.searchable === false) return false
220
- const threshold =
221
- typeof category.searchable === "object"
222
- ? (category.searchable.threshold ?? optionSearchThreshold)
223
- : optionSearchThreshold
224
- return category.options.length > threshold
225
- })()
218
+ const shouldShowSubmenuSearch = shouldShowOptionSearch(
219
+ category.searchable,
220
+ category.options.length,
221
+ optionSearchThreshold,
222
+ )
226
223
 
227
224
  return (
228
225
  <DropdownMenuSub