@handled-ai/design-system 0.16.1 → 0.17.0

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 (30) hide show
  1. package/dist/components/contextual-quick-action-launcher.d.ts +32 -0
  2. package/dist/components/contextual-quick-action-launcher.js +202 -0
  3. package/dist/components/contextual-quick-action-launcher.js.map +1 -0
  4. package/dist/components/data-table-condition-filter.js +26 -9
  5. package/dist/components/data-table-condition-filter.js.map +1 -1
  6. package/dist/components/data-table-filter.js +3 -14
  7. package/dist/components/data-table-filter.js.map +1 -1
  8. package/dist/components/score-why-chips.d.ts +46 -0
  9. package/dist/components/score-why-chips.js +281 -0
  10. package/dist/components/score-why-chips.js.map +1 -0
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.js +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/prototype/index.d.ts +1 -1
  15. package/dist/prototype/prototype-config.d.ts +37 -1
  16. package/dist/prototype/prototype-inbox-view.d.ts +9 -3
  17. package/dist/prototype/prototype-inbox-view.js +28 -96
  18. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  19. package/package.json +1 -1
  20. package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +193 -0
  21. package/src/components/__tests__/data-table-condition-filter.test.tsx +26 -0
  22. package/src/components/__tests__/data-table-filter.test.tsx +21 -0
  23. package/src/components/contextual-quick-action-launcher.tsx +231 -0
  24. package/src/components/data-table-condition-filter.tsx +39 -11
  25. package/src/components/data-table-filter.tsx +3 -19
  26. package/src/components/score-why-chips.tsx +358 -0
  27. package/src/index.ts +2 -0
  28. package/src/prototype/__tests__/detail-view-score-why.test.tsx +326 -0
  29. package/src/prototype/prototype-config.ts +35 -0
  30. package/src/prototype/prototype-inbox-view.tsx +31 -104
@@ -0,0 +1,193 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import React from "react";
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+
5
+ import {
6
+ ContextualQuickActionLauncher,
7
+ type ContextualQuickActionItem,
8
+ } from "../contextual-quick-action-launcher";
9
+
10
+ vi.mock("../dropdown-menu", async () => {
11
+ const ReactMod = await import("react");
12
+
13
+ /* eslint-disable @typescript-eslint/no-explicit-any */
14
+ const DropdownMenu = ({ children, open, defaultOpen, onOpenChange }: any) => {
15
+ const [internalOpen, setInternalOpen] = ReactMod.useState(defaultOpen ?? false);
16
+ const isControlled = open !== undefined;
17
+ const currentOpen = isControlled ? open : internalOpen;
18
+ const setOpen = (nextOpen: boolean) => {
19
+ if (!isControlled) setInternalOpen(nextOpen);
20
+ onOpenChange?.(nextOpen);
21
+ };
22
+
23
+ return ReactMod.createElement(
24
+ "div",
25
+ {
26
+ "data-slot": "dropdown-menu",
27
+ "data-open": currentOpen ? "true" : "false",
28
+ },
29
+ ReactMod.Children.map(children, (child: any) => {
30
+ if (!ReactMod.isValidElement(child)) return child;
31
+ return ReactMod.cloneElement(child, { __open: currentOpen, __setOpen: setOpen } as any);
32
+ })
33
+ );
34
+ };
35
+
36
+ const DropdownMenuTrigger = ({ children, asChild, __open, __setOpen, ...props }: any) => {
37
+ const childProps = {
38
+ "data-slot": "dropdown-menu-trigger",
39
+ "aria-expanded": __open ? "true" : "false",
40
+ onClick: () => __setOpen?.(!__open),
41
+ ...props,
42
+ };
43
+ if (asChild && ReactMod.isValidElement(children)) {
44
+ return ReactMod.cloneElement(children, childProps);
45
+ }
46
+ return ReactMod.createElement("button", childProps, children);
47
+ };
48
+
49
+ const DropdownMenuContent = ({
50
+ children,
51
+ __open,
52
+ __setOpen: _setOpen,
53
+ className,
54
+ align: _align,
55
+ side: _side,
56
+ sideOffset: _sideOffset,
57
+ ...props
58
+ }: any) => {
59
+ if (!__open) return null;
60
+ return ReactMod.createElement(
61
+ "div",
62
+ { "data-slot": "dropdown-menu-content", role: "menu", className, ...props },
63
+ children
64
+ );
65
+ };
66
+
67
+ const DropdownMenuItem = ({ children, onSelect, disabled, className, ...props }: any) =>
68
+ ReactMod.createElement(
69
+ "button",
70
+ {
71
+ "data-slot": "dropdown-menu-item",
72
+ role: "menuitem",
73
+ type: "button",
74
+ disabled,
75
+ className,
76
+ onClick: (event: any) => onSelect?.(event),
77
+ ...props,
78
+ },
79
+ children
80
+ );
81
+
82
+ const DropdownMenuSeparator = ({ className, ...props }: any) =>
83
+ ReactMod.createElement("div", { "data-slot": "dropdown-menu-separator", className, ...props });
84
+ /* eslint-enable @typescript-eslint/no-explicit-any */
85
+
86
+ return {
87
+ DropdownMenu,
88
+ DropdownMenuTrigger,
89
+ DropdownMenuContent,
90
+ DropdownMenuItem,
91
+ DropdownMenuSeparator,
92
+ };
93
+ });
94
+
95
+ const items: ContextualQuickActionItem[] = [
96
+ {
97
+ id: "create-opportunity",
98
+ label: "Create an opportunity",
99
+ description: "New sales or expansion deal",
100
+ icon: <span data-testid="salesforce-icon">SF</span>,
101
+ },
102
+ {
103
+ id: "update-opportunity",
104
+ label: "Update an opportunity",
105
+ description: "Edit stage, close date, or details",
106
+ disabled: true,
107
+ disabledReason: "No opportunity",
108
+ },
109
+ ];
110
+
111
+ function renderLauncher(
112
+ props: Partial<React.ComponentProps<typeof ContextualQuickActionLauncher>> = {}
113
+ ) {
114
+ return render(
115
+ <ContextualQuickActionLauncher
116
+ contextLabel="Supabase"
117
+ contextSecondary="Account"
118
+ items={items}
119
+ onSelect={() => {}}
120
+ {...props}
121
+ />
122
+ );
123
+ }
124
+
125
+ describe("ContextualQuickActionLauncher", () => {
126
+ it("renders the accessible trigger name", () => {
127
+ renderLauncher();
128
+
129
+ expect(screen.getByRole("button", { name: /quick action/i })).not.toBeNull();
130
+ });
131
+
132
+ it("opens and closes in uncontrolled mode", () => {
133
+ renderLauncher();
134
+
135
+ expect(screen.queryByText("Acting on")).toBeNull();
136
+
137
+ fireEvent.click(screen.getByRole("button", { name: /quick action/i }));
138
+ expect(screen.getByText("Acting on")).not.toBeNull();
139
+ expect(screen.getByText("Supabase")).not.toBeNull();
140
+
141
+ fireEvent.click(screen.getByRole("button", { name: /quick action/i }));
142
+ expect(screen.queryByText("Acting on")).toBeNull();
143
+ });
144
+
145
+ it("selects enabled items and closes the menu", () => {
146
+ const onSelect = vi.fn();
147
+ renderLauncher({ onSelect });
148
+
149
+ fireEvent.click(screen.getByRole("button", { name: /quick action/i }));
150
+ fireEvent.click(screen.getByRole("menuitem", { name: /create an opportunity/i }));
151
+
152
+ expect(onSelect).toHaveBeenCalledTimes(1);
153
+ expect(onSelect).toHaveBeenCalledWith(items[0]);
154
+ expect(screen.queryByText("Acting on")).toBeNull();
155
+ });
156
+
157
+ it("does not select disabled items", () => {
158
+ const onSelect = vi.fn();
159
+ renderLauncher({ onSelect });
160
+
161
+ fireEvent.click(screen.getByRole("button", { name: /quick action/i }));
162
+ fireEvent.click(screen.getByRole("menuitem", { name: /update an opportunity/i }));
163
+
164
+ expect(onSelect).not.toHaveBeenCalled();
165
+ expect(screen.getByText("No opportunity")).not.toBeNull();
166
+ });
167
+
168
+ it("calls browse all and closes the menu", () => {
169
+ const onBrowseAll = vi.fn();
170
+ renderLauncher({ onBrowseAll });
171
+
172
+ fireEvent.click(screen.getByRole("button", { name: /quick action/i }));
173
+ fireEvent.click(screen.getByRole("button", { name: /browse all actions/i }));
174
+
175
+ expect(onBrowseAll).toHaveBeenCalledTimes(1);
176
+ expect(screen.queryByText("Acting on")).toBeNull();
177
+ });
178
+
179
+ it("renders optional hint text", () => {
180
+ renderLauncher({ showHint: true });
181
+
182
+ expect(screen.getByText(/or press/i)).not.toBeNull();
183
+ expect(screen.getByText("⌘K")).not.toBeNull();
184
+ expect(screen.getByText(/for all actions/i)).not.toBeNull();
185
+ });
186
+
187
+ it("uses pointer-events-auto on portalled content", () => {
188
+ renderLauncher({ defaultOpen: true });
189
+
190
+ const content = document.querySelector('[data-slot="dropdown-menu-content"]');
191
+ expect(content?.className).toContain("pointer-events-auto");
192
+ });
193
+ });
@@ -151,6 +151,32 @@ describe("DataTableConditionFilter", () => {
151
151
  expect(onConditionsChange).toHaveBeenCalledWith([]);
152
152
  });
153
153
 
154
+
155
+ it("preserves an in-progress draft when fields are recreated with the same definition", () => {
156
+ const { rerender } = render(
157
+ <DataTableConditionFilter
158
+ fields={[...allFields]}
159
+ conditions={[]}
160
+ onConditionsChange={onConditionsChange}
161
+ />,
162
+ );
163
+
164
+ fireEvent.click(screen.getByText("Add filter"));
165
+ fireEvent.change(screen.getByPlaceholderText("Enter value..."), {
166
+ target: { value: "Acme" },
167
+ });
168
+
169
+ rerender(
170
+ <DataTableConditionFilter
171
+ fields={[...allFields]}
172
+ conditions={[]}
173
+ onConditionsChange={onConditionsChange}
174
+ />,
175
+ );
176
+
177
+ expect((screen.getByPlaceholderText("Enter value...") as HTMLInputElement).value).toBe("Acme");
178
+ });
179
+
154
180
  it("commits a text draft with Apply after a value is entered", () => {
155
181
  render(
156
182
  <DataTableConditionFilter
@@ -294,6 +294,27 @@ describe("DataTableFilter", () => {
294
294
  expect(screen.getByText("Filter builder")).toBeDefined();
295
295
  });
296
296
 
297
+
298
+ it("opens the condition builder without closing the menu selection", () => {
299
+ render(
300
+ <DataTableFilter
301
+ {...defaultProps}
302
+ conditionFields={[{ id: "name", label: "Name", type: "text" }]}
303
+ />
304
+ );
305
+
306
+ const builderEntry = document.querySelector('[data-slot="popover-trigger"]');
307
+ expect(builderEntry).not.toBeNull();
308
+
309
+ const preventDefaultSpy = vi.fn();
310
+ const mockEvent = new MouseEvent("click", { bubbles: true });
311
+ Object.defineProperty(mockEvent, "preventDefault", { value: preventDefaultSpy });
312
+ builderEntry!.dispatchEvent(mockEvent);
313
+
314
+ expect(preventDefaultSpy).toHaveBeenCalled();
315
+ expect(screen.getByText("Filter builder")).toBeDefined();
316
+ });
317
+
297
318
  it("uses the custom condition builder label when provided", () => {
298
319
  render(
299
320
  <DataTableFilter
@@ -0,0 +1,231 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ArrowRight, ChevronDown, Zap } from "lucide-react"
5
+
6
+ import { cn } from "../lib/utils"
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from "./dropdown-menu"
14
+
15
+ export interface ContextualQuickActionItem {
16
+ id: string
17
+ label: string
18
+ description?: string
19
+ icon?: React.ReactNode
20
+ disabled?: boolean
21
+ disabledReason?: string
22
+ }
23
+
24
+ export interface ContextualQuickActionContextLabelProps {
25
+ contextLabel: string
26
+ contextSecondary?: string
27
+ className?: string
28
+ }
29
+
30
+ export interface ContextualQuickActionLauncherProps {
31
+ contextLabel: string
32
+ contextSecondary?: string
33
+ items: ContextualQuickActionItem[]
34
+ onSelect: (item: ContextualQuickActionItem) => void
35
+ onBrowseAll?: () => void
36
+ showHint?: boolean
37
+ align?: "start" | "end"
38
+ className?: string
39
+ open?: boolean
40
+ defaultOpen?: boolean
41
+ onOpenChange?: (open: boolean) => void
42
+ }
43
+
44
+ function ContextualQuickActionContextLabel({
45
+ contextLabel,
46
+ contextSecondary,
47
+ className,
48
+ }: ContextualQuickActionContextLabelProps) {
49
+ return (
50
+ <div
51
+ data-slot="contextual-quick-action-context-label"
52
+ className={cn(
53
+ "-mx-1 -mt-1 mb-1 flex items-center gap-1.5 border-b px-3 py-2 text-[11px] text-muted-foreground",
54
+ className
55
+ )}
56
+ >
57
+ <Zap className="h-3 w-3 shrink-0" strokeWidth={2.25} aria-hidden="true" />
58
+ <span>Acting on</span>
59
+ <strong className="max-w-[180px] truncate font-semibold text-foreground">
60
+ {contextLabel}
61
+ </strong>
62
+ {contextSecondary ? (
63
+ <span className="min-w-0 truncate text-muted-foreground">
64
+ · {contextSecondary}
65
+ </span>
66
+ ) : null}
67
+ </div>
68
+ )
69
+ }
70
+
71
+ function DefaultActionIcon() {
72
+ return <Zap className="h-3.5 w-3.5" aria-hidden="true" />
73
+ }
74
+
75
+ function ContextualQuickActionLauncher({
76
+ contextLabel,
77
+ contextSecondary,
78
+ items,
79
+ onSelect,
80
+ onBrowseAll,
81
+ showHint = false,
82
+ align = "start",
83
+ className,
84
+ open,
85
+ defaultOpen,
86
+ onOpenChange,
87
+ }: ContextualQuickActionLauncherProps) {
88
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false)
89
+ const isControlled = open !== undefined
90
+ const isOpen = isControlled ? open : uncontrolledOpen
91
+
92
+ const handleOpenChange = React.useCallback(
93
+ (nextOpen: boolean) => {
94
+ if (!isControlled) {
95
+ setUncontrolledOpen(nextOpen)
96
+ }
97
+ onOpenChange?.(nextOpen)
98
+ },
99
+ [isControlled, onOpenChange]
100
+ )
101
+
102
+ const closeMenu = React.useCallback(() => {
103
+ handleOpenChange(false)
104
+ }, [handleOpenChange])
105
+
106
+ const handleSelect = React.useCallback(
107
+ (item: ContextualQuickActionItem, event?: Event) => {
108
+ if (item.disabled) {
109
+ event?.preventDefault()
110
+ return
111
+ }
112
+
113
+ onSelect(item)
114
+ closeMenu()
115
+ },
116
+ [closeMenu, onSelect]
117
+ )
118
+
119
+ const handleBrowseAll = React.useCallback(() => {
120
+ onBrowseAll?.()
121
+ closeMenu()
122
+ }, [closeMenu, onBrowseAll])
123
+
124
+ return (
125
+ <div
126
+ data-slot="contextual-quick-action-launcher"
127
+ className={cn("inline-flex items-center gap-2", className)}
128
+ >
129
+ <DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
130
+ <DropdownMenuTrigger asChild>
131
+ <button
132
+ type="button"
133
+ data-slot="contextual-quick-action-trigger"
134
+ data-state={isOpen ? "open" : "closed"}
135
+ className={cn(
136
+ "inline-flex h-8 items-center gap-2 rounded-lg border border-border bg-background py-1.5 pr-2.5 pl-2 text-xs font-medium text-foreground shadow-sm transition-colors hover:border-muted-foreground/40 hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
137
+ "data-[state=open]:border-foreground data-[state=open]:bg-foreground data-[state=open]:text-background data-[state=open]:hover:bg-foreground"
138
+ )}
139
+ >
140
+ <span
141
+ className={cn(
142
+ "inline-flex h-[18px] w-[18px] items-center justify-center rounded bg-foreground/[0.04]",
143
+ isOpen && "bg-background/15"
144
+ )}
145
+ >
146
+ <Zap className="h-3 w-3" strokeWidth={2} aria-hidden="true" />
147
+ </span>
148
+ <span className="tracking-[-0.005em]">Quick action</span>
149
+ <ChevronDown className="h-3 w-3 opacity-60" strokeWidth={2} aria-hidden="true" />
150
+ </button>
151
+ </DropdownMenuTrigger>
152
+
153
+ <DropdownMenuContent
154
+ align={align}
155
+ side="bottom"
156
+ sideOffset={6}
157
+ className="pointer-events-auto w-[308px] rounded-[10px] p-1.5 text-[12.5px] shadow-[0_12px_28px_rgba(0,0,0,0.12),0_2px_6px_rgba(0,0,0,0.04)]"
158
+ >
159
+ <ContextualQuickActionContextLabel
160
+ contextLabel={contextLabel}
161
+ contextSecondary={contextSecondary}
162
+ />
163
+
164
+ {items.map((item) => (
165
+ <DropdownMenuItem
166
+ key={item.id}
167
+ disabled={item.disabled}
168
+ onSelect={(event) => handleSelect(item, event)}
169
+ className={cn(
170
+ "flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-left outline-none focus:bg-accent data-[disabled]:cursor-not-allowed data-[disabled]:opacity-100",
171
+ item.disabled && "text-muted-foreground"
172
+ )}
173
+ >
174
+ <span
175
+ data-slot="contextual-quick-action-item-icon"
176
+ className={cn(
177
+ "flex h-[26px] w-[26px] shrink-0 items-center justify-center rounded-md bg-secondary text-foreground",
178
+ item.disabled && "opacity-60"
179
+ )}
180
+ >
181
+ {item.icon ?? <DefaultActionIcon />}
182
+ </span>
183
+ <span className="min-w-0 flex-1">
184
+ <span className="block truncate text-[12.5px] font-medium leading-tight text-current">
185
+ {item.label}
186
+ </span>
187
+ {item.description ? (
188
+ <span className="mt-0.5 block truncate text-[11px] leading-tight text-muted-foreground">
189
+ {item.description}
190
+ </span>
191
+ ) : null}
192
+ </span>
193
+ {item.disabled && item.disabledReason ? (
194
+ <span className="ml-auto shrink-0 text-[10.5px] italic text-muted-foreground/80">
195
+ {item.disabledReason}
196
+ </span>
197
+ ) : null}
198
+ </DropdownMenuItem>
199
+ ))}
200
+
201
+ <DropdownMenuSeparator className="-mx-1.5 my-1" />
202
+ <div className="flex items-center justify-between px-2 py-1.5 text-[11px] text-muted-foreground">
203
+ <button
204
+ type="button"
205
+ className="inline-flex items-center gap-1 font-medium text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
206
+ onClick={handleBrowseAll}
207
+ >
208
+ Browse all actions
209
+ <ArrowRight className="h-3 w-3" strokeWidth={2} aria-hidden="true" />
210
+ </button>
211
+ <span className="inline-flex items-center rounded border border-border px-1 py-0.5 text-[10px] font-medium leading-none text-muted-foreground">
212
+ ⌘K
213
+ </span>
214
+ </div>
215
+ </DropdownMenuContent>
216
+ </DropdownMenu>
217
+
218
+ {showHint ? (
219
+ <span className="text-[11px] text-muted-foreground">
220
+ Or press{" "}
221
+ <span className="inline-flex items-center rounded border border-border bg-muted/40 px-1 py-0.5 text-[10px] font-medium leading-none">
222
+ ⌘K
223
+ </span>{" "}
224
+ for all actions
225
+ </span>
226
+ ) : null}
227
+ </div>
228
+ )
229
+ }
230
+
231
+ export { ContextualQuickActionContextLabel, ContextualQuickActionLauncher }
@@ -171,6 +171,27 @@ function isCompleteCondition(
171
171
  return condition.value !== null && condition.value !== ""
172
172
  }
173
173
 
174
+ function getConditionsSignature(conditions: ConditionFilterValue[]): string {
175
+ return conditions
176
+ .map((condition) => `${condition.id}:${condition.field}:${condition.operator}:${String(condition.value)}`)
177
+ .join(";")
178
+ }
179
+
180
+ function getFieldsSignature(fields: ConditionFieldDef[]): string {
181
+ return fields
182
+ .map((field) => `${field.id}:${field.type}:${field.label}:${(field.operators ?? []).join("|")}`)
183
+ .join(";")
184
+ }
185
+
186
+ function getCommittedConditions(
187
+ drafts: ConditionFilterValue[],
188
+ fields: ConditionFieldDef[],
189
+ ): ConditionFilterValue[] {
190
+ return drafts
191
+ .map((condition) => normalizeCondition(condition, fields))
192
+ .filter((condition) => isCompleteCondition(condition, fields))
193
+ }
194
+
174
195
  function getFieldIcon(type: ConditionFieldDef["type"]) {
175
196
  if (type === "currency") return DollarSign
176
197
  if (type === "number") return Hash
@@ -363,17 +384,26 @@ function DataTableConditionFilter({
363
384
  conditions.map((condition) => normalizeCondition(condition, fields)),
364
385
  )
365
386
 
387
+ const fieldsSignature = React.useMemo(() => getFieldsSignature(fields), [fields])
388
+ const conditionsSignature = React.useMemo(() => getConditionsSignature(conditions), [conditions])
389
+ const fieldsRef = React.useRef(fields)
390
+
391
+ React.useEffect(() => {
392
+ setDrafts(conditions.map((condition) => normalizeCondition(condition, fieldsRef.current)))
393
+ }, [conditionsSignature])
394
+
366
395
  React.useEffect(() => {
367
- setDrafts(conditions.map((condition) => normalizeCondition(condition, fields)))
368
- }, [conditions, fields])
396
+ if (fieldsRef.current !== fields) {
397
+ fieldsRef.current = fields
398
+ setDrafts((current) => current.map((condition) => normalizeCondition(condition, fields)))
399
+ }
400
+ // Depend on a structural signature so inline-but-equivalent field arrays do
401
+ // not wipe in-progress drafts before Apply.
402
+ }, [fieldsSignature, fields])
369
403
 
370
404
  const commitDrafts = React.useCallback(
371
405
  (nextDrafts: ConditionFilterValue[] = drafts) => {
372
- onConditionsChange(
373
- nextDrafts
374
- .map((condition) => normalizeCondition(condition, fields))
375
- .filter((condition) => isCompleteCondition(condition, fields)),
376
- )
406
+ onConditionsChange(getCommittedConditions(nextDrafts, fields))
377
407
  },
378
408
  [drafts, fields, onConditionsChange],
379
409
  )
@@ -381,12 +411,10 @@ function DataTableConditionFilter({
381
411
  const handleAdd = () => {
382
412
  const firstField = fields[0]
383
413
  if (!firstField) return
384
- const committedDrafts = drafts.filter((condition) =>
385
- isCompleteCondition(condition, fields),
386
- )
414
+ const committedDrafts = getCommittedConditions(drafts, fields)
387
415
  const nextDrafts = [...committedDrafts, createDraftCondition(firstField)]
388
416
  setDrafts(nextDrafts)
389
- commitDrafts(committedDrafts)
417
+ onConditionsChange(committedDrafts)
390
418
  }
391
419
 
392
420
  const handleUpdate = (index: number, updated: ConditionFilterValue) => {
@@ -111,29 +111,13 @@ export function DataTableFilter({
111
111
  )
112
112
 
113
113
  const activeCount = React.useMemo(() => {
114
- // Count user-selected filters
115
114
  const userCount = Object.values(selectedFilters).reduce(
116
115
  (count, selected) => count + selected.length,
117
116
  0
118
117
  )
119
118
 
120
- // Count active preset filters (those that are in presetFilters AND currently active in selectedFilters)
121
- const presetCount = 0
122
- if (presetFilters) {
123
- for (const [categoryId, presetValues] of Object.entries(presetFilters)) {
124
- for (const value of presetValues) {
125
- // Only count if the preset is active (in selectedFilters) but NOT already counted as a user filter
126
- if (selectedFilters[categoryId]?.includes(value)) {
127
- // Already counted in userCount, skip
128
- } else {
129
- // Not in selectedFilters — it's an inactive preset, don't count
130
- }
131
- }
132
- }
133
- }
134
-
135
- return userCount + presetCount + conditionFilters.length
136
- }, [selectedFilters, presetFilters, conditionFilters.length])
119
+ return userCount + conditionFilters.length
120
+ }, [selectedFilters, conditionFilters.length])
137
121
 
138
122
  /** Collect all preset chips to render */
139
123
  const presetChips = React.useMemo(() => {
@@ -345,7 +329,7 @@ export function DataTableFilter({
345
329
  align="start"
346
330
  side="right"
347
331
  sideOffset={8}
348
- className="z-60 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
332
+ className="z-50 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
349
333
  onEscapeKeyDown={() => setConditionBuilderOpen(false)}
350
334
  onInteractOutside={(event) => {
351
335
  const target = event.target as HTMLElement | null