@handled-ai/design-system 0.15.1 → 0.16.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.
- package/dist/components/collapsible-section.d.ts +20 -0
- package/dist/components/collapsible-section.js +48 -0
- package/dist/components/collapsible-section.js.map +1 -0
- package/dist/components/contact-list.d.ts +3 -1
- package/dist/components/contact-list.js +20 -3
- package/dist/components/contact-list.js.map +1 -1
- package/dist/components/data-table-filter.d.ts +8 -2
- package/dist/components/data-table-filter.js +73 -8
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/entity-panel.js +1 -1
- package/dist/components/entity-panel.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/collapsible-section.test.tsx +143 -0
- package/src/components/__tests__/contact-list.test.tsx +116 -0
- package/src/components/__tests__/data-table-filter-presets.test.tsx +209 -0
- package/src/components/__tests__/entity-metadata-grid.test.tsx +25 -0
- package/src/components/__tests__/virtualized-data-table.test.tsx +0 -1
- package/src/components/collapsible-section.tsx +62 -0
- package/src/components/contact-list.tsx +22 -3
- package/src/components/data-table-filter.tsx +102 -12
- package/src/components/entity-panel.tsx +1 -1
- package/src/index.ts +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface CollapsibleSectionProps {
|
|
4
|
+
/** Total number of items (used in the expansion bar label). */
|
|
5
|
+
count: number;
|
|
6
|
+
/** Items to show before collapsing. Default: 5. */
|
|
7
|
+
maxItems?: number;
|
|
8
|
+
/** Children to render — the component slices React.Children.toArray(children) at maxItems. */
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
/** Start expanded. Default: false. */
|
|
11
|
+
defaultExpanded?: boolean;
|
|
12
|
+
/** Custom label for the expansion bar. Default: "Show all {count}". */
|
|
13
|
+
expandLabel?: string;
|
|
14
|
+
/** Custom label when expanded. Default: "Show less". */
|
|
15
|
+
collapseLabel?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function CollapsibleSection({ count, maxItems, children, defaultExpanded, expandLabel, collapseLabel, className, }: CollapsibleSectionProps): React.JSX.Element;
|
|
19
|
+
|
|
20
|
+
export { CollapsibleSection, type CollapsibleSectionProps };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
"use client";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { ChevronDown } from "lucide-react";
|
|
7
|
+
import { cn } from "../lib/utils.js";
|
|
8
|
+
function CollapsibleSection({
|
|
9
|
+
count,
|
|
10
|
+
maxItems = 5,
|
|
11
|
+
children,
|
|
12
|
+
defaultExpanded = false,
|
|
13
|
+
expandLabel,
|
|
14
|
+
collapseLabel,
|
|
15
|
+
className
|
|
16
|
+
}) {
|
|
17
|
+
const [expanded, setExpanded] = React.useState(defaultExpanded);
|
|
18
|
+
const items = React.Children.toArray(children);
|
|
19
|
+
const visible = expanded ? items : items.slice(0, maxItems);
|
|
20
|
+
const showBar = items.length > maxItems;
|
|
21
|
+
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
22
|
+
visible,
|
|
23
|
+
showBar && /* @__PURE__ */ jsxs(
|
|
24
|
+
"button",
|
|
25
|
+
{
|
|
26
|
+
type: "button",
|
|
27
|
+
onClick: () => setExpanded(!expanded),
|
|
28
|
+
className: "flex items-center justify-between w-full px-3 py-1.5 mt-1 text-xs font-medium text-muted-foreground border border-border/50 rounded-md hover:bg-muted/30 transition-colors cursor-pointer",
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsx("span", { children: expanded ? collapseLabel != null ? collapseLabel : "Show less" : expandLabel != null ? expandLabel : `Show all ${count}` }),
|
|
31
|
+
/* @__PURE__ */ jsx(
|
|
32
|
+
ChevronDown,
|
|
33
|
+
{
|
|
34
|
+
className: cn(
|
|
35
|
+
"h-3.5 w-3.5 transition-transform duration-200",
|
|
36
|
+
expanded && "rotate-180"
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
] });
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
CollapsibleSection
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=collapsible-section.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/collapsible-section.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\n\nexport interface CollapsibleSectionProps {\n /** Total number of items (used in the expansion bar label). */\n count: number\n /** Items to show before collapsing. Default: 5. */\n maxItems?: number\n /** Children to render — the component slices React.Children.toArray(children) at maxItems. */\n children: React.ReactNode\n /** Start expanded. Default: false. */\n defaultExpanded?: boolean\n /** Custom label for the expansion bar. Default: \"Show all {count}\". */\n expandLabel?: string\n /** Custom label when expanded. Default: \"Show less\". */\n collapseLabel?: string\n className?: string\n}\n\nexport function CollapsibleSection({\n count,\n maxItems = 5,\n children,\n defaultExpanded = false,\n expandLabel,\n collapseLabel,\n className,\n}: CollapsibleSectionProps) {\n const [expanded, setExpanded] = React.useState(defaultExpanded)\n\n const items = React.Children.toArray(children)\n const visible = expanded ? items : items.slice(0, maxItems)\n const showBar = items.length > maxItems\n\n return (\n <div className={className}>\n {visible}\n {showBar && (\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"flex items-center justify-between w-full px-3 py-1.5 mt-1 text-xs font-medium text-muted-foreground border border-border/50 rounded-md hover:bg-muted/30 transition-colors cursor-pointer\"\n >\n <span>\n {expanded\n ? (collapseLabel ?? \"Show less\")\n : (expandLabel ?? `Show all ${count}`)}\n </span>\n <ChevronDown\n className={cn(\n \"h-3.5 w-3.5 transition-transform duration-200\",\n expanded && \"rotate-180\"\n )}\n />\n </button>\n )}\n </div>\n )\n}\n"],"mappings":";AAyCQ,SAKE,KALF;AAvCR,YAAY,WAAW;AACvB,SAAS,mBAAmB;AAC5B,SAAS,UAAU;AAkBZ,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AACF,GAA4B;AAC1B,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,eAAe;AAE9D,QAAM,QAAQ,MAAM,SAAS,QAAQ,QAAQ;AAC7C,QAAM,UAAU,WAAW,QAAQ,MAAM,MAAM,GAAG,QAAQ;AAC1D,QAAM,UAAU,MAAM,SAAS;AAE/B,SACE,qBAAC,SAAI,WACF;AAAA;AAAA,IACA,WACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM,YAAY,CAAC,QAAQ;AAAA,QACpC,WAAU;AAAA,QAEV;AAAA,8BAAC,UACE,qBACI,wCAAiB,cACjB,oCAAe,YAAY,KAAK,IACvC;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,YAAY;AAAA,cACd;AAAA;AAAA,UACF;AAAA;AAAA;AAAA,IACF;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -28,7 +28,9 @@ interface ContactListProps {
|
|
|
28
28
|
contacts: ContactItem[];
|
|
29
29
|
onAdd?: () => void;
|
|
30
30
|
addLabel?: string;
|
|
31
|
+
/** Maximum contacts to show before collapsing. Shows expansion bar when exceeded. Undefined = show all (backward compatible). */
|
|
32
|
+
maxItems?: number;
|
|
31
33
|
}
|
|
32
|
-
declare function ContactList({ title, count, contacts, onAdd, addLabel }: ContactListProps): React.JSX.Element;
|
|
34
|
+
declare function ContactList({ title, count, contacts, onAdd, addLabel, maxItems }: ContactListProps): React.JSX.Element;
|
|
33
35
|
|
|
34
36
|
export { type ContactChannel, type ContactItem, ContactList, type ContactListProps };
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
"use client";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { ChevronDown, Plus, X } from "lucide-react";
|
|
7
|
+
import { cn } from "../lib/utils.js";
|
|
6
8
|
import { Badge } from "./badge.js";
|
|
7
9
|
import { Button } from "./button.js";
|
|
8
10
|
const badgeColors = {
|
|
@@ -62,7 +64,10 @@ function ContactRow({ contact }) {
|
|
|
62
64
|
] })
|
|
63
65
|
] });
|
|
64
66
|
}
|
|
65
|
-
function ContactList({ title, count, contacts, onAdd, addLabel }) {
|
|
67
|
+
function ContactList({ title, count, contacts, onAdd, addLabel, maxItems }) {
|
|
68
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
69
|
+
const visibleContacts = maxItems != null && !expanded ? contacts.slice(0, maxItems) : contacts;
|
|
70
|
+
const showExpansionBar = maxItems != null && contacts.length > maxItems;
|
|
66
71
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2.5", children: [
|
|
67
72
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
68
73
|
/* @__PURE__ */ jsx("h3", { className: "text-[13px] font-semibold text-foreground", children: title }),
|
|
@@ -75,7 +80,19 @@ function ContactList({ title, count, contacts, onAdd, addLabel }) {
|
|
|
75
80
|
] })
|
|
76
81
|
] })
|
|
77
82
|
] }),
|
|
78
|
-
/* @__PURE__ */ jsx("div", { className: "space-y-0", children:
|
|
83
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-0", children: visibleContacts.map((contact) => /* @__PURE__ */ jsx(ContactRow, { contact }, contact.id)) }),
|
|
84
|
+
showExpansionBar && /* @__PURE__ */ jsxs(
|
|
85
|
+
"button",
|
|
86
|
+
{
|
|
87
|
+
type: "button",
|
|
88
|
+
onClick: () => setExpanded(!expanded),
|
|
89
|
+
className: "flex items-center justify-between w-full px-3 py-1.5 mt-1 text-xs font-medium text-muted-foreground border border-border/50 rounded-md hover:bg-muted/30 transition-colors",
|
|
90
|
+
children: [
|
|
91
|
+
/* @__PURE__ */ jsx("span", { children: expanded ? "Show less" : `Show all ${contacts.length} contacts` }),
|
|
92
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: cn("h-3.5 w-3.5 transition-transform duration-200", expanded && "rotate-180") })
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
)
|
|
79
96
|
] });
|
|
80
97
|
}
|
|
81
98
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/contact-list.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Plus, X } from \"lucide-react\"\nimport { Badge } from \"./badge\"\nimport { Button } from \"./button\"\n\nexport interface ContactChannel {\n type: \"linkedin\" | \"gmail\" | \"salesforce\" | \"phone\" | \"custom\"\n icon: React.ReactNode\n label?: string\n onClick?: () => void\n}\n\nexport interface ContactItem {\n id: string\n name: string\n role: string\n badge?: {\n label: string\n color?: \"indigo\" | \"green\" | \"amber\" | \"red\" | \"muted\"\n }\n channels?: ContactChannel[]\n action?: {\n label: string\n onClick?: () => void\n }\n description?: string\n onDismiss?: () => void\n}\n\nexport interface ContactListProps {\n title?: string\n count?: string\n contacts: ContactItem[]\n onAdd?: () => void\n addLabel?: string\n}\n\nconst badgeColors: Record<string, string> = {\n indigo: \"bg-indigo-50 text-indigo-700 border-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-300 dark:border-indigo-800\",\n green: \"bg-green-50 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800\",\n amber: \"bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-800\",\n red: \"bg-red-50 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800\",\n muted: \"bg-muted text-muted-foreground border-border\",\n}\n\nfunction ContactRow({ contact }: { contact: ContactItem }) {\n return (\n <div className=\"flex items-center justify-between gap-3 group py-2 border-b border-border/30 last:border-0 hover:bg-muted/20 -mx-2 px-2 rounded-sm transition-colors\">\n <div className=\"flex items-center gap-2.5 min-w-0\">\n {contact.badge && (\n <Badge\n variant=\"outline\"\n className={`shadow-none px-2 py-0 text-[11px] font-medium shrink-0 ${badgeColors[contact.badge.color ?? \"muted\"]}`}\n >\n {contact.badge.label}\n </Badge>\n )}\n <span className=\"font-medium text-[13px] text-foreground truncate\">{contact.name}</span>\n <span className=\"text-muted-foreground text-[13px] shrink-0\">·</span>\n <span className=\"text-muted-foreground text-[13px] truncate\">{contact.role}</span>\n </div>\n\n <div className=\"flex items-center gap-1 shrink-0\">\n {contact.channels?.map((ch, i) => (\n <button\n key={i}\n onClick={ch.onClick}\n className=\"h-7 w-7 flex items-center justify-center hover:bg-muted rounded-md transition-colors\"\n title={ch.label}\n >\n {ch.icon}\n </button>\n ))}\n {contact.action && (\n <Button\n size=\"sm\"\n className=\"bg-foreground text-background hover:bg-foreground/90 h-6 text-[11px] font-medium shadow-none ml-1\"\n onClick={contact.action.onClick}\n >\n <Plus className=\"w-3 h-3 mr-1\" />\n {contact.action.label}\n </Button>\n )}\n {contact.onDismiss && (\n <button\n onClick={contact.onDismiss}\n className=\"h-6 w-6 flex items-center justify-center text-muted-foreground/40 hover:text-foreground hover:bg-muted rounded-md transition-colors opacity-0 group-hover:opacity-100\"\n >\n <X className=\"w-3 h-3\" />\n </button>\n )}\n </div>\n </div>\n )\n}\n\nexport function ContactList({ title, count, contacts, onAdd, addLabel }: ContactListProps) {\n return (\n <div className=\"space-y-2.5\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-[13px] font-semibold text-foreground\">{title}</h3>\n <div className=\"flex items-center gap-3\">\n {count && <span className=\"text-xs text-muted-foreground\">{count}</span>}\n {onAdd && (\n <Button variant=\"ghost\" size=\"sm\" onClick={onAdd} className=\"h-7 text-xs font-medium hover:bg-muted/50\">\n <Plus className=\"w-3.5 h-3.5 mr-1\" /> {addLabel ?? \"Add\"}\n </Button>\n )}\n </div>\n </div>\n\n <div className=\"space-y-0\">\n {
|
|
1
|
+
{"version":3,"sources":["../../src/components/contact-list.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, Plus, X } from \"lucide-react\"\nimport { cn } from \"../lib/utils\"\nimport { Badge } from \"./badge\"\nimport { Button } from \"./button\"\n\nexport interface ContactChannel {\n type: \"linkedin\" | \"gmail\" | \"salesforce\" | \"phone\" | \"custom\"\n icon: React.ReactNode\n label?: string\n onClick?: () => void\n}\n\nexport interface ContactItem {\n id: string\n name: string\n role: string\n badge?: {\n label: string\n color?: \"indigo\" | \"green\" | \"amber\" | \"red\" | \"muted\"\n }\n channels?: ContactChannel[]\n action?: {\n label: string\n onClick?: () => void\n }\n description?: string\n onDismiss?: () => void\n}\n\nexport interface ContactListProps {\n title?: string\n count?: string\n contacts: ContactItem[]\n onAdd?: () => void\n addLabel?: string\n /** Maximum contacts to show before collapsing. Shows expansion bar when exceeded. Undefined = show all (backward compatible). */\n maxItems?: number\n}\n\nconst badgeColors: Record<string, string> = {\n indigo: \"bg-indigo-50 text-indigo-700 border-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-300 dark:border-indigo-800\",\n green: \"bg-green-50 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800\",\n amber: \"bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-800\",\n red: \"bg-red-50 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800\",\n muted: \"bg-muted text-muted-foreground border-border\",\n}\n\nfunction ContactRow({ contact }: { contact: ContactItem }) {\n return (\n <div className=\"flex items-center justify-between gap-3 group py-2 border-b border-border/30 last:border-0 hover:bg-muted/20 -mx-2 px-2 rounded-sm transition-colors\">\n <div className=\"flex items-center gap-2.5 min-w-0\">\n {contact.badge && (\n <Badge\n variant=\"outline\"\n className={`shadow-none px-2 py-0 text-[11px] font-medium shrink-0 ${badgeColors[contact.badge.color ?? \"muted\"]}`}\n >\n {contact.badge.label}\n </Badge>\n )}\n <span className=\"font-medium text-[13px] text-foreground truncate\">{contact.name}</span>\n <span className=\"text-muted-foreground text-[13px] shrink-0\">·</span>\n <span className=\"text-muted-foreground text-[13px] truncate\">{contact.role}</span>\n </div>\n\n <div className=\"flex items-center gap-1 shrink-0\">\n {contact.channels?.map((ch, i) => (\n <button\n key={i}\n onClick={ch.onClick}\n className=\"h-7 w-7 flex items-center justify-center hover:bg-muted rounded-md transition-colors\"\n title={ch.label}\n >\n {ch.icon}\n </button>\n ))}\n {contact.action && (\n <Button\n size=\"sm\"\n className=\"bg-foreground text-background hover:bg-foreground/90 h-6 text-[11px] font-medium shadow-none ml-1\"\n onClick={contact.action.onClick}\n >\n <Plus className=\"w-3 h-3 mr-1\" />\n {contact.action.label}\n </Button>\n )}\n {contact.onDismiss && (\n <button\n onClick={contact.onDismiss}\n className=\"h-6 w-6 flex items-center justify-center text-muted-foreground/40 hover:text-foreground hover:bg-muted rounded-md transition-colors opacity-0 group-hover:opacity-100\"\n >\n <X className=\"w-3 h-3\" />\n </button>\n )}\n </div>\n </div>\n )\n}\n\nexport function ContactList({ title, count, contacts, onAdd, addLabel, maxItems }: ContactListProps) {\n const [expanded, setExpanded] = React.useState(false)\n\n const visibleContacts = maxItems != null && !expanded ? contacts.slice(0, maxItems) : contacts\n const showExpansionBar = maxItems != null && contacts.length > maxItems\n\n return (\n <div className=\"space-y-2.5\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-[13px] font-semibold text-foreground\">{title}</h3>\n <div className=\"flex items-center gap-3\">\n {count && <span className=\"text-xs text-muted-foreground\">{count}</span>}\n {onAdd && (\n <Button variant=\"ghost\" size=\"sm\" onClick={onAdd} className=\"h-7 text-xs font-medium hover:bg-muted/50\">\n <Plus className=\"w-3.5 h-3.5 mr-1\" /> {addLabel ?? \"Add\"}\n </Button>\n )}\n </div>\n </div>\n\n <div className=\"space-y-0\">\n {visibleContacts.map((contact) => (\n <ContactRow key={contact.id} contact={contact} />\n ))}\n </div>\n\n {showExpansionBar && (\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"flex items-center justify-between w-full px-3 py-1.5 mt-1 text-xs font-medium text-muted-foreground border border-border/50 rounded-md hover:bg-muted/30 transition-colors\"\n >\n <span>{expanded ? \"Show less\" : `Show all ${contacts.length} contacts`}</span>\n <ChevronDown className={cn(\"h-3.5 w-3.5 transition-transform duration-200\", expanded && \"rotate-180\")} />\n </button>\n )}\n </div>\n )\n}\n"],"mappings":";AAqDM,SAEI,KAFJ;AAnDN,YAAY,WAAW;AACvB,SAAS,aAAa,MAAM,SAAS;AACrC,SAAS,UAAU;AACnB,SAAS,aAAa;AACtB,SAAS,cAAc;AAoCvB,MAAM,cAAsC;AAAA,EAC1C,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,OAAO;AAAA,EACP,KAAK;AAAA,EACL,OAAO;AACT;AAEA,SAAS,WAAW,EAAE,QAAQ,GAA6B;AAlD3D;AAmDE,SACE,qBAAC,SAAI,WAAU,wJACb;AAAA,yBAAC,SAAI,WAAU,qCACZ;AAAA,cAAQ,SACP;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,WAAW,0DAA0D,aAAY,aAAQ,MAAM,UAAd,YAAuB,OAAO,CAAC;AAAA,UAE/G,kBAAQ,MAAM;AAAA;AAAA,MACjB;AAAA,MAEF,oBAAC,UAAK,WAAU,oDAAoD,kBAAQ,MAAK;AAAA,MACjF,oBAAC,UAAK,WAAU,8CAA6C,kBAAQ;AAAA,MACrE,oBAAC,UAAK,WAAU,8CAA8C,kBAAQ,MAAK;AAAA,OAC7E;AAAA,IAEA,qBAAC,SAAI,WAAU,oCACZ;AAAA,oBAAQ,aAAR,mBAAkB,IAAI,CAAC,IAAI,MAC1B;AAAA,QAAC;AAAA;AAAA,UAEC,SAAS,GAAG;AAAA,UACZ,WAAU;AAAA,UACV,OAAO,GAAG;AAAA,UAET,aAAG;AAAA;AAAA,QALC;AAAA,MAMP;AAAA,MAED,QAAQ,UACP;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS,QAAQ,OAAO;AAAA,UAExB;AAAA,gCAAC,QAAK,WAAU,gBAAe;AAAA,YAC9B,QAAQ,OAAO;AAAA;AAAA;AAAA,MAClB;AAAA,MAED,QAAQ,aACP;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,QAAQ;AAAA,UACjB,WAAU;AAAA,UAEV,8BAAC,KAAE,WAAU,WAAU;AAAA;AAAA,MACzB;AAAA,OAEJ;AAAA,KACF;AAEJ;AAEO,SAAS,YAAY,EAAE,OAAO,OAAO,UAAU,OAAO,UAAU,SAAS,GAAqB;AACnG,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AAEpD,QAAM,kBAAkB,YAAY,QAAQ,CAAC,WAAW,SAAS,MAAM,GAAG,QAAQ,IAAI;AACtF,QAAM,mBAAmB,YAAY,QAAQ,SAAS,SAAS;AAE/D,SACE,qBAAC,SAAI,WAAU,eACb;AAAA,yBAAC,SAAI,WAAU,qCACb;AAAA,0BAAC,QAAG,WAAU,6CAA6C,iBAAM;AAAA,MACjE,qBAAC,SAAI,WAAU,2BACZ;AAAA,iBAAS,oBAAC,UAAK,WAAU,iCAAiC,iBAAM;AAAA,QAChE,SACC,qBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,SAAS,OAAO,WAAU,6CAC1D;AAAA,8BAAC,QAAK,WAAU,oBAAmB;AAAA,UAAE;AAAA,UAAE,8BAAY;AAAA,WACrD;AAAA,SAEJ;AAAA,OACF;AAAA,IAEA,oBAAC,SAAI,WAAU,aACZ,0BAAgB,IAAI,CAAC,YACpB,oBAAC,cAA4B,WAAZ,QAAQ,EAAsB,CAChD,GACH;AAAA,IAEC,oBACC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS,MAAM,YAAY,CAAC,QAAQ;AAAA,QACpC,WAAU;AAAA,QAEV;AAAA,8BAAC,UAAM,qBAAW,cAAc,YAAY,SAAS,MAAM,aAAY;AAAA,UACvE,oBAAC,eAAY,WAAW,GAAG,iDAAiD,YAAY,YAAY,GAAG;AAAA;AAAA;AAAA,IACzG;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -19,7 +19,13 @@ interface DataTableFilterProps {
|
|
|
19
19
|
className?: string;
|
|
20
20
|
/** Minimum number of options before showing the sub-menu search input. Defaults to 8. */
|
|
21
21
|
optionSearchThreshold?: number;
|
|
22
|
+
/** Filters applied by default. Shown as distinct chips that can be toggled off but not dismissed. */
|
|
23
|
+
presetFilters?: Record<string, string[]>;
|
|
24
|
+
/** Callback when a preset filter is toggled on/off. */
|
|
25
|
+
onTogglePreset?: (categoryId: string, option: string) => void;
|
|
26
|
+
/** Label shown on preset chips to distinguish from user-applied filters. Default: "Default". */
|
|
27
|
+
presetLabel?: string;
|
|
22
28
|
}
|
|
23
|
-
declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, className, optionSearchThreshold, }: DataTableFilterProps): React.JSX.Element;
|
|
29
|
+
declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, className, optionSearchThreshold, presetFilters, onTogglePreset, presetLabel, }: DataTableFilterProps): React.JSX.Element;
|
|
24
30
|
|
|
25
|
-
export { DataTableFilter, type DataTableFilterCategory, type FilterOption };
|
|
31
|
+
export { DataTableFilter, type DataTableFilterCategory, type DataTableFilterProps, type FilterOption };
|
|
@@ -45,7 +45,10 @@ function DataTableFilter({
|
|
|
45
45
|
selectedFilters,
|
|
46
46
|
onToggleFilter,
|
|
47
47
|
className,
|
|
48
|
-
optionSearchThreshold = 8
|
|
48
|
+
optionSearchThreshold = 8,
|
|
49
|
+
presetFilters,
|
|
50
|
+
onTogglePreset,
|
|
51
|
+
presetLabel = "Default"
|
|
49
52
|
}) {
|
|
50
53
|
const [query, setQuery] = React.useState("");
|
|
51
54
|
const [subQueries, setSubQueries] = React.useState({});
|
|
@@ -63,14 +66,49 @@ function DataTableFilter({
|
|
|
63
66
|
);
|
|
64
67
|
});
|
|
65
68
|
}, [categories, query]);
|
|
66
|
-
const
|
|
67
|
-
() =>
|
|
69
|
+
const isPresetOption = React.useCallback(
|
|
70
|
+
(categoryId, value) => {
|
|
71
|
+
var _a, _b;
|
|
72
|
+
return (_b = (_a = presetFilters == null ? void 0 : presetFilters[categoryId]) == null ? void 0 : _a.includes(value)) != null ? _b : false;
|
|
73
|
+
},
|
|
74
|
+
[presetFilters]
|
|
75
|
+
);
|
|
76
|
+
const activeCount = React.useMemo(() => {
|
|
77
|
+
var _a;
|
|
78
|
+
const userCount = Object.values(selectedFilters).reduce(
|
|
68
79
|
(count, selected) => count + selected.length,
|
|
69
80
|
0
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
);
|
|
82
|
+
let presetCount = 0;
|
|
83
|
+
if (presetFilters) {
|
|
84
|
+
for (const [categoryId, presetValues] of Object.entries(presetFilters)) {
|
|
85
|
+
for (const value of presetValues) {
|
|
86
|
+
if ((_a = selectedFilters[categoryId]) == null ? void 0 : _a.includes(value)) {
|
|
87
|
+
} else {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return userCount + presetCount;
|
|
93
|
+
}, [selectedFilters, presetFilters]);
|
|
94
|
+
const presetChips = React.useMemo(() => {
|
|
95
|
+
var _a, _b;
|
|
96
|
+
if (!presetFilters) return [];
|
|
97
|
+
const chips = [];
|
|
98
|
+
for (const [categoryId, values] of Object.entries(presetFilters)) {
|
|
99
|
+
const category = categories.find((c) => c.id === categoryId);
|
|
100
|
+
for (const value of values) {
|
|
101
|
+
const option = category == null ? void 0 : category.options.find(
|
|
102
|
+
(opt) => getOptionValue(opt) === value
|
|
103
|
+
);
|
|
104
|
+
const label = option ? getOptionLabel(option) : value;
|
|
105
|
+
const active = (_b = (_a = selectedFilters[categoryId]) == null ? void 0 : _a.includes(value)) != null ? _b : false;
|
|
106
|
+
chips.push({ categoryId, value, label, active });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return chips;
|
|
110
|
+
}, [presetFilters, categories, selectedFilters]);
|
|
111
|
+
const triggerButton = /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
74
112
|
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
75
113
|
Button,
|
|
76
114
|
{
|
|
@@ -151,6 +189,7 @@ function DataTableFilter({
|
|
|
151
189
|
const value = getOptionValue(option);
|
|
152
190
|
const label = getOptionLabel(option);
|
|
153
191
|
const selected = (_b2 = (_a2 = selectedFilters[category.id]) == null ? void 0 : _a2.includes(value)) != null ? _b2 : false;
|
|
192
|
+
const isPreset = isPresetOption(category.id, value);
|
|
154
193
|
return /* @__PURE__ */ jsxs(
|
|
155
194
|
DropdownMenuItem,
|
|
156
195
|
{
|
|
@@ -161,7 +200,7 @@ function DataTableFilter({
|
|
|
161
200
|
},
|
|
162
201
|
children: [
|
|
163
202
|
label,
|
|
164
|
-
selected ? /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold text-brand-purple", children: "Applied" }) : null
|
|
203
|
+
selected ? isPreset ? /* @__PURE__ */ jsx("span", { className: "text-brand-purple text-[10px] font-semibold", children: presetLabel }) : /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold text-brand-purple", children: "Applied" }) : null
|
|
165
204
|
]
|
|
166
205
|
},
|
|
167
206
|
value
|
|
@@ -178,6 +217,32 @@ function DataTableFilter({
|
|
|
178
217
|
] })
|
|
179
218
|
] })
|
|
180
219
|
] });
|
|
220
|
+
if (presetChips.length > 0) {
|
|
221
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
222
|
+
triggerButton,
|
|
223
|
+
presetChips.map((chip) => /* @__PURE__ */ jsxs(
|
|
224
|
+
"button",
|
|
225
|
+
{
|
|
226
|
+
type: "button",
|
|
227
|
+
onClick: () => onTogglePreset == null ? void 0 : onTogglePreset(chip.categoryId, chip.value),
|
|
228
|
+
className: cn(
|
|
229
|
+
"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium transition-colors",
|
|
230
|
+
chip.active ? "border-dashed border-brand-purple/30 bg-brand-purple/5 text-brand-purple/80" : "border-border/40 bg-transparent text-muted-foreground/60 line-through"
|
|
231
|
+
),
|
|
232
|
+
children: [
|
|
233
|
+
/* @__PURE__ */ jsxs("span", { className: "text-brand-purple/50 text-[10px]", children: [
|
|
234
|
+
presetLabel,
|
|
235
|
+
":",
|
|
236
|
+
" "
|
|
237
|
+
] }),
|
|
238
|
+
chip.label
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
`${chip.categoryId}-${chip.value}`
|
|
242
|
+
))
|
|
243
|
+
] });
|
|
244
|
+
}
|
|
245
|
+
return triggerButton;
|
|
181
246
|
}
|
|
182
247
|
export {
|
|
183
248
|
DataTableFilter
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ListFilter, Search } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: (string | FilterOption)[]\n}\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\ninterface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n const activeCount = React.useMemo(\n () =>\n Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n ),\n [selectedFilters]\n )\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — only for categories with many options */}\n {category.options.length > optionSearchThreshold && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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","names":["_a","_b"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ListFilter, Search } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface FilterOption {\n label: string\n value: string\n}\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: (string | FilterOption)[]\n}\n\nfunction getOptionValue(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.value\n}\nfunction getOptionLabel(option: string | FilterOption): string {\n return typeof option === \"string\" ? option : option.label\n}\n\nexport interface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n /** Minimum number of options before showing the sub-menu search input. Defaults to 8. */\n optionSearchThreshold?: number\n /** Filters applied by default. Shown as distinct chips that can be toggled off but not dismissed. */\n presetFilters?: Record<string, string[]>\n /** Callback when a preset filter is toggled on/off. */\n onTogglePreset?: (categoryId: string, option: string) => void\n /** Label shown on preset chips to distinguish from user-applied filters. Default: \"Default\". */\n presetLabel?: string\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n optionSearchThreshold = 8,\n presetFilters,\n onTogglePreset,\n presetLabel = \"Default\",\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n getOptionLabel(option).toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n /** Check if a specific option is a preset filter */\n const isPresetOption = React.useCallback(\n (categoryId: string, value: string): boolean => {\n return presetFilters?.[categoryId]?.includes(value) ?? false\n },\n [presetFilters]\n )\n\n const activeCount = React.useMemo(() => {\n // Count user-selected filters\n const userCount = Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n )\n\n // Count active preset filters (those that are in presetFilters AND currently active in selectedFilters)\n let presetCount = 0\n if (presetFilters) {\n for (const [categoryId, presetValues] of Object.entries(presetFilters)) {\n for (const value of presetValues) {\n // Only count if the preset is active (in selectedFilters) but NOT already counted as a user filter\n if (selectedFilters[categoryId]?.includes(value)) {\n // Already counted in userCount, skip\n } else {\n // Not in selectedFilters — it's an inactive preset, don't count\n }\n }\n }\n }\n\n return userCount + presetCount\n }, [selectedFilters, presetFilters])\n\n /** Collect all preset chips to render */\n const presetChips = React.useMemo(() => {\n if (!presetFilters) return []\n\n const chips: { categoryId: string; value: string; label: string; active: boolean }[] = []\n\n for (const [categoryId, values] of Object.entries(presetFilters)) {\n const category = categories.find((c) => c.id === categoryId)\n for (const value of values) {\n const option = category?.options.find(\n (opt) => getOptionValue(opt) === value\n )\n const label = option ? getOptionLabel(option) : value\n const active = selectedFilters[categoryId]?.includes(value) ?? false\n chips.push({ categoryId, value, label, active })\n }\n }\n\n return chips\n }, [presetFilters, categories, selectedFilters])\n\n const triggerButton = (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => {\n const subQuery = (subQueries[category.id] ?? \"\").trim().toLowerCase()\n const filteredOptions = subQuery\n ? category.options.filter((opt) =>\n getOptionLabel(opt).toLowerCase().includes(subQuery)\n )\n : category.options\n\n return (\n <DropdownMenuSub\n key={category.id}\n onOpenChange={(open) => {\n if (!open) {\n setSubQueries((prev) => {\n const next = { ...prev }\n delete next[category.id]\n return next\n })\n }\n }}\n >\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"max-h-[320px] w-52 overflow-y-auto p-1\">\n {/* Submenu search — only for categories with many options */}\n {category.options.length > optionSearchThreshold && (\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-1.5\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-7 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search...\"\n value={subQueries[category.id] ?? \"\"}\n onChange={(e) =>\n setSubQueries((prev) => ({ ...prev, [category.id]: e.target.value }))\n }\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n // Allow navigation keys to propagate to Radix menu handling\n // so keyboard users can move to and select filtered options.\n const navKeys = [\"ArrowDown\", \"ArrowUp\", \"Enter\", \"Escape\", \"Tab\"]\n if (!navKeys.includes(e.key)) {\n e.stopPropagation()\n }\n }}\n />\n </div>\n </div>\n )}\n {/* Filtered options */}\n {filteredOptions.map((option) => {\n const value = getOptionValue(option)\n const label = getOptionLabel(option)\n const selected = selectedFilters[category.id]?.includes(value) ?? false\n const isPreset = isPresetOption(category.id, value)\n return (\n <DropdownMenuItem\n key={value}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, value)\n }}\n >\n {label}\n {selected ? (\n isPreset ? (\n <span className=\"text-brand-purple text-[10px] font-semibold\">\n {presetLabel}\n </span>\n ) : (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n )\n ) : null}\n </DropdownMenuItem>\n )\n })}\n {filteredOptions.length === 0 && category.options.length > 0 && (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No matches\n </div>\n )}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n )\n })}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n\n // If there are preset chips, wrap trigger + chips together\n if (presetChips.length > 0) {\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {triggerButton}\n {presetChips.map((chip) => (\n <button\n key={`${chip.categoryId}-${chip.value}`}\n type=\"button\"\n onClick={() => onTogglePreset?.(chip.categoryId, chip.value)}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] font-medium transition-colors\",\n chip.active\n ? \"border-dashed border-brand-purple/30 bg-brand-purple/5 text-brand-purple/80\"\n : \"border-border/40 bg-transparent text-muted-foreground/60 line-through\"\n )}\n >\n <span className=\"text-brand-purple/50 text-[10px]\">\n {presetLabel}:{\" \"}\n </span>\n {chip.label}\n </button>\n ))}\n </div>\n )\n }\n\n return triggerButton\n}\n"],"mappings":"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","names":["_a","_b"]}
|
|
@@ -167,7 +167,7 @@ function EntityPanelTabs({
|
|
|
167
167
|
)) });
|
|
168
168
|
}
|
|
169
169
|
function EntityMetadataGrid({ fields }) {
|
|
170
|
-
return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-[140px_1fr] gap-y-3 gap-x-4 mb-7 text-[13px]", children: fields.map((field, idx) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
170
|
+
return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-[140px_1fr] gap-y-3 gap-x-4 mb-7 text-[13px] overflow-hidden", children: fields.map((field, idx) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
171
171
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground text-[13px] font-normal", children: [
|
|
172
172
|
/* @__PURE__ */ jsx(field.icon, { className: "w-3.5 h-3.5 shrink-0" }),
|
|
173
173
|
/* @__PURE__ */ jsx("span", { children: field.label })
|