@handled-ai/design-system 0.13.2 → 0.14.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.
@@ -12,7 +12,8 @@ interface DataTableFilterProps {
12
12
  categories: DataTableFilterCategory[];
13
13
  selectedFilters: Record<string, string[]>;
14
14
  onToggleFilter: (categoryId: string, option: string) => void;
15
+ className?: string;
15
16
  }
16
- declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, }: DataTableFilterProps): React.JSX.Element;
17
+ declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, className, }: DataTableFilterProps): React.JSX.Element;
17
18
 
18
19
  export { DataTableFilter, type DataTableFilterCategory };
@@ -4,6 +4,7 @@
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import * as React from "react";
6
6
  import { ListFilter, Search } from "lucide-react";
7
+ import { cn } from "../lib/utils.js";
7
8
  import { Button } from "./button.js";
8
9
  import {
9
10
  DropdownMenu,
@@ -17,7 +18,8 @@ import {
17
18
  function DataTableFilter({
18
19
  categories,
19
20
  selectedFilters,
20
- onToggleFilter
21
+ onToggleFilter,
22
+ className
21
23
  }) {
22
24
  const [query, setQuery] = React.useState("");
23
25
  const visibleCategories = React.useMemo(() => {
@@ -47,7 +49,10 @@ function DataTableFilter({
47
49
  {
48
50
  variant: "outline",
49
51
  size: "sm",
50
- className: "h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50",
52
+ className: cn(
53
+ "h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50",
54
+ className
55
+ ),
51
56
  children: [
52
57
  /* @__PURE__ */ jsx(ListFilter, { className: "h-3.5 w-3.5" }),
53
58
  "Filter",
@@ -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 { 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 DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: string[]\n}\n\ninterface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\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 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=\"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\"\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 <DropdownMenuSub key={category.id}>\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=\"w-52 p-1\">\n {category.options.map((option) => {\n const selected =\n selectedFilters[category.id]?.includes(option) ?? false\n\n return (\n <DropdownMenuItem\n key={option}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, option)\n }}\n >\n {option}\n {selected ? (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n ) : null}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\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":";AAiEQ,SAKE,KALF;AA/DR,YAAY,WAAW;AACvB,SAAS,YAAY,cAAc;AAEnC,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAeA,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAE3C,QAAM,oBAAoB,MAAM,QAAQ,MAAM;AAC5C,UAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AAEA,WAAO,WAAW,OAAO,CAAC,aAAa;AACrC,UAAI,SAAS,MAAM,YAAY,EAAE,SAAS,UAAU,GAAG;AACrD,eAAO;AAAA,MACT;AAEA,aAAO,SAAS,QAAQ;AAAA,QAAK,CAAC,WAC5B,OAAO,YAAY,EAAE,SAAS,UAAU;AAAA,MAC1C;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,YAAY,KAAK,CAAC;AAEtB,QAAM,cAAc,MAAM;AAAA,IACxB,MACE,OAAO,OAAO,eAAe,EAAE;AAAA,MAC7B,CAAC,OAAO,aAAa,QAAQ,SAAS;AAAA,MACtC;AAAA,IACF;AAAA,IACF,CAAC,eAAe;AAAA,EAClB;AAEA,SACE,qBAAC,gBACC;AAAA,wBAAC,uBAAoB,SAAO,MAC1B;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,WAAU;AAAA,QAEV;AAAA,8BAAC,cAAW,WAAU,eAAc;AAAA,UAAE;AAAA,UAErC,cAAc,IACb,oBAAC,UAAK,WAAU,0DACb,uBACH,IACE;AAAA;AAAA;AAAA,IACN,GACF;AAAA,IACA,qBAAC,uBAAoB,OAAM,SAAQ,WAAU,iBAC3C;AAAA,0BAAC,SAAI,WAAU,2DACb,+BAAC,SAAI,WAAU,YACb;AAAA,4BAAC,UAAO,WAAU,8EAA6E;AAAA,QAC/F;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,aAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,YAChD,SAAS,CAAC,UAAU,MAAM,gBAAgB;AAAA,YAC1C,WAAW,CAAC,UAAU,MAAM,gBAAgB;AAAA;AAAA,QAC9C;AAAA,SACF,GACF;AAAA,MAEA,qBAAC,SAAI,WAAU,qCACZ;AAAA,0BAAkB,IAAI,CAAC,aACtB,qBAAC,mBACC;AAAA,+BAAC,0BAAuB,WAAU,iCAChC;AAAA,gCAAC,SAAS,MAAT,EAAc,WAAU,0CAAyC;AAAA,YACjE,SAAS;AAAA,aACZ;AAAA,UACA,oBAAC,0BAAuB,WAAU,YAC/B,mBAAS,QAAQ,IAAI,CAAC,WAAW;AAtGlD;AAuGkB,kBAAM,YACJ,2BAAgB,SAAS,EAAE,MAA3B,mBAA8B,SAAS,YAAvC,YAAkD;AAEpD,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,WAAU;AAAA,gBACV,UAAU,CAAC,UAAU;AACnB,wBAAM,eAAe;AACrB,iCAAe,SAAS,IAAI,MAAM;AAAA,gBACpC;AAAA,gBAEC;AAAA;AAAA,kBACA,WACC,oBAAC,UAAK,WAAU,+CAA8C,qBAE9D,IACE;AAAA;AAAA;AAAA,cAZC;AAAA,YAaP;AAAA,UAEJ,CAAC,GACH;AAAA,aA5BoB,SAAS,EA6B/B,CACD;AAAA,QAEA,kBAAkB,WAAW,IAC5B,oBAAC,SAAI,WAAU,iDAAgD,8BAE/D,IACE;AAAA,SACN;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
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 DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: string[]\n}\n\ninterface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\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 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 <DropdownMenuSub key={category.id}>\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=\"w-52 p-1\">\n {category.options.map((option) => {\n const selected =\n selectedFilters[category.id]?.includes(option) ?? false\n\n return (\n <DropdownMenuItem\n key={option}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, option)\n }}\n >\n {option}\n {selected ? (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n ) : null}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\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":[]}
@@ -5,7 +5,9 @@ interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
5
5
  title?: string;
6
6
  description: string;
7
7
  action?: React.ReactNode;
8
+ secondaryAction?: React.ReactNode;
9
+ size?: 'sm' | 'lg';
8
10
  }
9
- declare function EmptyState({ icon, title, description, action, className, ...rest }: EmptyStateProps): React.JSX.Element;
11
+ declare function EmptyState({ icon, title, description, action, secondaryAction, size, className, ...rest }: EmptyStateProps): React.JSX.Element;
10
12
 
11
13
  export { EmptyState, type EmptyStateProps };
@@ -32,12 +32,21 @@ var __objRest = (source, exclude) => {
32
32
  import { jsx, jsxs } from "react/jsx-runtime";
33
33
  import { cn } from "../lib/utils.js";
34
34
  function EmptyState(_a) {
35
- var _b = _a, { icon, title, description, action, className } = _b, rest = __objRest(_b, ["icon", "title", "description", "action", "className"]);
36
- return /* @__PURE__ */ jsxs("div", __spreadProps(__spreadValues({ "data-slot": "empty-state", className: cn("flex flex-col items-center justify-center py-24 gap-3", className) }, rest), { children: [
37
- icon && /* @__PURE__ */ jsx("div", { "data-slot": "empty-state-icon", className: "text-muted-foreground/30 [&>svg]:w-12 [&>svg]:h-12", children: icon }),
38
- title && /* @__PURE__ */ jsx("p", { "data-slot": "empty-state-title", className: "text-sm font-medium text-muted-foreground", children: title }),
39
- /* @__PURE__ */ jsx("p", { "data-slot": "empty-state-description", className: "text-xs text-muted-foreground", children: description }),
40
- action && /* @__PURE__ */ jsx("div", { "data-slot": "empty-state-action", className: "mt-3", children: action })
35
+ var _b = _a, { icon, title, description, action, secondaryAction, size = "sm", className } = _b, rest = __objRest(_b, ["icon", "title", "description", "action", "secondaryAction", "size", "className"]);
36
+ const isLg = size === "lg";
37
+ return /* @__PURE__ */ jsxs("div", __spreadProps(__spreadValues({ "data-slot": "empty-state", className: cn("flex flex-col items-center justify-center py-24 gap-3 text-center", className) }, rest), { children: [
38
+ icon && /* @__PURE__ */ jsx("div", { "data-slot": "empty-state-icon", className: cn(
39
+ "text-muted-foreground/30",
40
+ isLg ? "[&>svg]:w-16 [&>svg]:h-16 [&>img]:h-[140px] [&>img]:w-auto [&>img]:object-contain" : "[&>svg]:w-12 [&>svg]:h-12"
41
+ ), children: icon }),
42
+ title && /* @__PURE__ */ jsx("p", { "data-slot": "empty-state-title", className: cn(
43
+ isLg ? "text-base font-semibold text-foreground" : "text-sm font-medium text-muted-foreground"
44
+ ), children: title }),
45
+ /* @__PURE__ */ jsx("p", { "data-slot": "empty-state-description", className: cn(
46
+ isLg ? "text-sm text-muted-foreground" : "text-xs text-muted-foreground"
47
+ ), children: description }),
48
+ action && /* @__PURE__ */ jsx("div", { "data-slot": "empty-state-action", className: "mt-3", children: action }),
49
+ secondaryAction && /* @__PURE__ */ jsx("div", { "data-slot": "empty-state-secondary-action", className: action ? "mt-1" : "mt-3", children: secondaryAction })
41
50
  ] }));
42
51
  }
43
52
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/empty-state.tsx"],"sourcesContent":["import * as React from \"react\"\n\nimport { cn } from \"../lib/utils\"\n\ninterface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {\n icon?: React.ReactNode\n title?: string\n description: string\n action?: React.ReactNode\n}\n\nfunction EmptyState({ icon, title, description, action, className, ...rest }: EmptyStateProps) {\n return (\n <div data-slot=\"empty-state\" className={cn(\"flex flex-col items-center justify-center py-24 gap-3\", className)} {...rest}>\n {icon && (\n <div data-slot=\"empty-state-icon\" className=\"text-muted-foreground/30 [&>svg]:w-12 [&>svg]:h-12\">\n {icon}\n </div>\n )}\n {title && (\n <p data-slot=\"empty-state-title\" className=\"text-sm font-medium text-muted-foreground\">\n {title}\n </p>\n )}\n <p data-slot=\"empty-state-description\" className=\"text-xs text-muted-foreground\">\n {description}\n </p>\n {action && (\n <div data-slot=\"empty-state-action\" className=\"mt-3\">\n {action}\n </div>\n )}\n </div>\n )\n}\n\nexport { EmptyState, type EmptyStateProps }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaI,SAEI,KAFJ;AAXJ,SAAS,UAAU;AASnB,SAAS,WAAW,IAA2E;AAA3E,eAAE,QAAM,OAAO,aAAa,QAAQ,UAXxD,IAWoB,IAAkD,iBAAlD,IAAkD,CAAhD,QAAM,SAAO,eAAa,UAAQ;AACtD,SACE,qBAAC,sCAAI,aAAU,eAAc,WAAW,GAAG,yDAAyD,SAAS,KAAO,OAAnH,EACE;AAAA,YACC,oBAAC,SAAI,aAAU,oBAAmB,WAAU,sDACzC,gBACH;AAAA,IAED,SACC,oBAAC,OAAE,aAAU,qBAAoB,WAAU,6CACxC,iBACH;AAAA,IAEF,oBAAC,OAAE,aAAU,2BAA0B,WAAU,iCAC9C,uBACH;AAAA,IACC,UACC,oBAAC,SAAI,aAAU,sBAAqB,WAAU,QAC3C,kBACH;AAAA,MAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/empty-state.tsx"],"sourcesContent":["import * as React from \"react\"\n\nimport { cn } from \"../lib/utils\"\n\ninterface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {\n icon?: React.ReactNode\n title?: string\n description: string\n action?: React.ReactNode\n secondaryAction?: React.ReactNode\n size?: 'sm' | 'lg'\n}\n\nfunction EmptyState({ icon, title, description, action, secondaryAction, size = 'sm', className, ...rest }: EmptyStateProps) {\n const isLg = size === 'lg'\n return (\n <div data-slot=\"empty-state\" className={cn(\"flex flex-col items-center justify-center py-24 gap-3 text-center\", className)} {...rest}>\n {icon && (\n <div data-slot=\"empty-state-icon\" className={cn(\n \"text-muted-foreground/30\",\n isLg\n ? \"[&>svg]:w-16 [&>svg]:h-16 [&>img]:h-[140px] [&>img]:w-auto [&>img]:object-contain\"\n : \"[&>svg]:w-12 [&>svg]:h-12\"\n )}>\n {icon}\n </div>\n )}\n {title && (\n <p data-slot=\"empty-state-title\" className={cn(\n isLg ? \"text-base font-semibold text-foreground\" : \"text-sm font-medium text-muted-foreground\"\n )}>\n {title}\n </p>\n )}\n <p data-slot=\"empty-state-description\" className={cn(\n isLg ? \"text-sm text-muted-foreground\" : \"text-xs text-muted-foreground\"\n )}>\n {description}\n </p>\n {action && (\n <div data-slot=\"empty-state-action\" className=\"mt-3\">\n {action}\n </div>\n )}\n {secondaryAction && (\n <div data-slot=\"empty-state-secondary-action\" className={action ? \"mt-1\" : \"mt-3\"}>\n {secondaryAction}\n </div>\n )}\n </div>\n )\n}\n\nexport { EmptyState, type EmptyStateProps }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBI,SAEI,KAFJ;AAdJ,SAAS,UAAU;AAWnB,SAAS,WAAW,IAAyG;AAAzG,eAAE,QAAM,OAAO,aAAa,QAAQ,iBAAiB,OAAO,MAAM,UAbtF,IAaoB,IAAgF,iBAAhF,IAAgF,CAA9E,QAAM,SAAO,eAAa,UAAQ,mBAAiB,QAAa;AACpF,QAAM,OAAO,SAAS;AACtB,SACE,qBAAC,sCAAI,aAAU,eAAc,WAAW,GAAG,qEAAqE,SAAS,KAAO,OAA/H,EACE;AAAA,YACC,oBAAC,SAAI,aAAU,oBAAmB,WAAW;AAAA,MAC3C;AAAA,MACA,OACI,sFACA;AAAA,IACN,GACG,gBACH;AAAA,IAED,SACC,oBAAC,OAAE,aAAU,qBAAoB,WAAW;AAAA,MAC1C,OAAO,4CAA4C;AAAA,IACrD,GACG,iBACH;AAAA,IAEF,oBAAC,OAAE,aAAU,2BAA0B,WAAW;AAAA,MAChD,OAAO,kCAAkC;AAAA,IAC3C,GACG,uBACH;AAAA,IACC,UACC,oBAAC,SAAI,aAAU,sBAAqB,WAAU,QAC3C,kBACH;AAAA,IAED,mBACC,oBAAC,SAAI,aAAU,gCAA+B,WAAW,SAAS,SAAS,QACxE,2BACH;AAAA,MAEJ;AAEJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handled-ai/design-system",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "Handled UI component library (shadcn-style, New York)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.0",
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import React from "react";
3
+ import { render } from "@testing-library/react";
4
+ import { DataTableFilter } from "../data-table-filter";
5
+ import { ListFilter } from "lucide-react";
6
+
7
+ const defaultProps = {
8
+ categories: [
9
+ {
10
+ id: "status",
11
+ label: "Status",
12
+ icon: ListFilter,
13
+ options: ["Open", "Closed"],
14
+ },
15
+ ],
16
+ selectedFilters: {} as Record<string, string[]>,
17
+ onToggleFilter: () => {},
18
+ };
19
+
20
+ describe("DataTableFilter", () => {
21
+ it("renders trigger button with default classes", () => {
22
+ const { container } = render(<DataTableFilter {...defaultProps} />);
23
+ const button = container.querySelector("button");
24
+ expect(button).not.toBeNull();
25
+ expect(button!.className).toContain("h-8");
26
+ });
27
+
28
+ it("merges custom className into trigger button", () => {
29
+ const { container } = render(
30
+ <DataTableFilter
31
+ {...defaultProps}
32
+ className="bg-primary text-primary-foreground"
33
+ />
34
+ );
35
+ const button = container.querySelector("button");
36
+ expect(button).not.toBeNull();
37
+ expect(button!.className).toContain("bg-primary");
38
+ expect(button!.className).toContain("text-primary-foreground");
39
+ expect(button!.className).toContain("h-8");
40
+ });
41
+ });
@@ -73,4 +73,78 @@ describe("EmptyState", () => {
73
73
  const el = container.querySelector('[data-slot="empty-state"]')!;
74
74
  expect(el.classList.contains("my-custom-class")).toBe(true);
75
75
  });
76
+
77
+ it("default size is 'sm' with small typography", () => {
78
+ const { container } = render(
79
+ <EmptyState description="No items" title="Empty" />
80
+ );
81
+ const titleEl = container.querySelector('[data-slot="empty-state-title"]')!;
82
+ const descEl = container.querySelector('[data-slot="empty-state-description"]')!;
83
+ expect(titleEl.className).toContain("text-sm");
84
+ expect(descEl.className).toContain("text-xs");
85
+ });
86
+
87
+ it("size 'lg' applies larger typography", () => {
88
+ const { container } = render(
89
+ <EmptyState description="No items" title="Empty" size="lg" />
90
+ );
91
+ const titleEl = container.querySelector('[data-slot="empty-state-title"]')!;
92
+ const descEl = container.querySelector('[data-slot="empty-state-description"]')!;
93
+ expect(titleEl.className).toContain("text-base");
94
+ expect(titleEl.className).toContain("font-semibold");
95
+ expect(descEl.className).toContain("text-sm");
96
+ });
97
+
98
+ it("size 'lg' uses larger icon sizing", () => {
99
+ const { container } = render(
100
+ <EmptyState
101
+ description="No items"
102
+ size="lg"
103
+ icon={<svg data-testid="test-svg"><rect /></svg>}
104
+ />
105
+ );
106
+ const iconWrapper = container.querySelector('[data-slot="empty-state-icon"]')!;
107
+ expect(iconWrapper.className).toContain("[&>svg]:w-16");
108
+ });
109
+
110
+ it("renders secondaryAction when provided", () => {
111
+ const { container } = render(
112
+ <EmptyState
113
+ description="No items"
114
+ secondaryAction={<a href="/help">Help link</a>}
115
+ />
116
+ );
117
+ const secondaryWrapper = container.querySelector('[data-slot="empty-state-secondary-action"]');
118
+ expect(secondaryWrapper).not.toBeNull();
119
+ expect(screen.getByText("Help link")).not.toBeNull();
120
+ });
121
+
122
+ it("omits secondaryAction wrapper when not provided", () => {
123
+ const { container } = render(<EmptyState description="No items" />);
124
+ const secondaryWrapper = container.querySelector('[data-slot="empty-state-secondary-action"]');
125
+ expect(secondaryWrapper).toBeNull();
126
+ });
127
+
128
+ it("secondaryAction uses mt-1 when action is also present", () => {
129
+ const { container } = render(
130
+ <EmptyState
131
+ description="No items"
132
+ action={<button type="button">Primary</button>}
133
+ secondaryAction={<a href="/help">Secondary</a>}
134
+ />
135
+ );
136
+ const secondaryWrapper = container.querySelector('[data-slot="empty-state-secondary-action"]')!;
137
+ expect(secondaryWrapper.className).toContain("mt-1");
138
+ });
139
+
140
+ it("secondaryAction uses mt-3 when action is not present", () => {
141
+ const { container } = render(
142
+ <EmptyState
143
+ description="No items"
144
+ secondaryAction={<a href="/help">Secondary</a>}
145
+ />
146
+ );
147
+ const secondaryWrapper = container.querySelector('[data-slot="empty-state-secondary-action"]')!;
148
+ expect(secondaryWrapper.className).toContain("mt-3");
149
+ });
76
150
  });
@@ -3,6 +3,7 @@
3
3
  import * as React from "react"
4
4
  import { ListFilter, Search } from "lucide-react"
5
5
 
6
+ import { cn } from "../lib/utils"
6
7
  import { Button } from "./button"
7
8
  import {
8
9
  DropdownMenu,
@@ -25,12 +26,14 @@ interface DataTableFilterProps {
25
26
  categories: DataTableFilterCategory[]
26
27
  selectedFilters: Record<string, string[]>
27
28
  onToggleFilter: (categoryId: string, option: string) => void
29
+ className?: string
28
30
  }
29
31
 
30
32
  export function DataTableFilter({
31
33
  categories,
32
34
  selectedFilters,
33
35
  onToggleFilter,
36
+ className,
34
37
  }: DataTableFilterProps) {
35
38
  const [query, setQuery] = React.useState("")
36
39
 
@@ -66,7 +69,10 @@ export function DataTableFilter({
66
69
  <Button
67
70
  variant="outline"
68
71
  size="sm"
69
- className="h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50"
72
+ className={cn(
73
+ "h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50",
74
+ className
75
+ )}
70
76
  >
71
77
  <ListFilter className="h-3.5 w-3.5" />
72
78
  Filter
@@ -7,22 +7,34 @@ interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
7
7
  title?: string
8
8
  description: string
9
9
  action?: React.ReactNode
10
+ secondaryAction?: React.ReactNode
11
+ size?: 'sm' | 'lg'
10
12
  }
11
13
 
12
- function EmptyState({ icon, title, description, action, className, ...rest }: EmptyStateProps) {
14
+ function EmptyState({ icon, title, description, action, secondaryAction, size = 'sm', className, ...rest }: EmptyStateProps) {
15
+ const isLg = size === 'lg'
13
16
  return (
14
- <div data-slot="empty-state" className={cn("flex flex-col items-center justify-center py-24 gap-3", className)} {...rest}>
17
+ <div data-slot="empty-state" className={cn("flex flex-col items-center justify-center py-24 gap-3 text-center", className)} {...rest}>
15
18
  {icon && (
16
- <div data-slot="empty-state-icon" className="text-muted-foreground/30 [&>svg]:w-12 [&>svg]:h-12">
19
+ <div data-slot="empty-state-icon" className={cn(
20
+ "text-muted-foreground/30",
21
+ isLg
22
+ ? "[&>svg]:w-16 [&>svg]:h-16 [&>img]:h-[140px] [&>img]:w-auto [&>img]:object-contain"
23
+ : "[&>svg]:w-12 [&>svg]:h-12"
24
+ )}>
17
25
  {icon}
18
26
  </div>
19
27
  )}
20
28
  {title && (
21
- <p data-slot="empty-state-title" className="text-sm font-medium text-muted-foreground">
29
+ <p data-slot="empty-state-title" className={cn(
30
+ isLg ? "text-base font-semibold text-foreground" : "text-sm font-medium text-muted-foreground"
31
+ )}>
22
32
  {title}
23
33
  </p>
24
34
  )}
25
- <p data-slot="empty-state-description" className="text-xs text-muted-foreground">
35
+ <p data-slot="empty-state-description" className={cn(
36
+ isLg ? "text-sm text-muted-foreground" : "text-xs text-muted-foreground"
37
+ )}>
26
38
  {description}
27
39
  </p>
28
40
  {action && (
@@ -30,6 +42,11 @@ function EmptyState({ icon, title, description, action, className, ...rest }: Em
30
42
  {action}
31
43
  </div>
32
44
  )}
45
+ {secondaryAction && (
46
+ <div data-slot="empty-state-secondary-action" className={action ? "mt-1" : "mt-3"}>
47
+ {secondaryAction}
48
+ </div>
49
+ )}
33
50
  </div>
34
51
  )
35
52
  }