@hexclave/dashboard-ui-components 1.0.3 → 1.0.5

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 (52) hide show
  1. package/dist/components/button.d.ts +2 -2
  2. package/dist/components/pill-toggle.js +2 -2
  3. package/dist/components/pill-toggle.js.map +1 -1
  4. package/dist/dashboard-ui-components.global.js +7447 -9242
  5. package/dist/dashboard-ui-components.global.js.map +4 -4
  6. package/dist/esm/components/button.d.ts +2 -2
  7. package/dist/esm/components/pill-toggle.js +3 -3
  8. package/dist/esm/components/pill-toggle.js.map +1 -1
  9. package/package.json +4 -3
  10. package/src/components/alert.tsx +120 -0
  11. package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
  12. package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
  13. package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
  14. package/src/components/analytics-chart/format.ts +101 -0
  15. package/src/components/analytics-chart/index.ts +75 -0
  16. package/src/components/analytics-chart/palette.ts +68 -0
  17. package/src/components/analytics-chart/render-data-series.tsx +169 -0
  18. package/src/components/analytics-chart/state.ts +165 -0
  19. package/src/components/analytics-chart/strings.ts +72 -0
  20. package/src/components/analytics-chart/types.ts +220 -0
  21. package/src/components/badge.tsx +108 -0
  22. package/src/components/button.tsx +104 -0
  23. package/src/components/card.tsx +263 -0
  24. package/src/components/chart-card.tsx +101 -0
  25. package/src/components/chart-container.tsx +155 -0
  26. package/src/components/chart-legend.tsx +65 -0
  27. package/src/components/chart-theme.tsx +52 -0
  28. package/src/components/chart-tooltip.tsx +165 -0
  29. package/src/components/cursor-blast-effect.tsx +334 -0
  30. package/src/components/data-grid/data-grid-sizing.ts +51 -0
  31. package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
  32. package/src/components/data-grid/data-grid.test.tsx +366 -0
  33. package/src/components/data-grid/data-grid.tsx +1220 -0
  34. package/src/components/data-grid/index.ts +64 -0
  35. package/src/components/data-grid/state.ts +235 -0
  36. package/src/components/data-grid/strings.ts +45 -0
  37. package/src/components/data-grid/types.ts +401 -0
  38. package/src/components/data-grid/use-data-source.ts +413 -0
  39. package/src/components/data-grid/use-url-state.test.tsx +88 -0
  40. package/src/components/data-grid/use-url-state.ts +298 -0
  41. package/src/components/dialog.tsx +207 -0
  42. package/src/components/edit-mode.tsx +17 -0
  43. package/src/components/empty-state.tsx +64 -0
  44. package/src/components/input.tsx +85 -0
  45. package/src/components/metric-card.tsx +142 -0
  46. package/src/components/pill-toggle.tsx +159 -0
  47. package/src/components/progress-bar.tsx +82 -0
  48. package/src/components/separator.tsx +36 -0
  49. package/src/components/skeleton.tsx +30 -0
  50. package/src/components/table.tsx +113 -0
  51. package/src/components/tabs.tsx +214 -0
  52. package/src/index.ts +69 -0
@@ -5,7 +5,7 @@ import * as class_variance_authority_types0 from "class-variance-authority/types
5
5
  //#region src/components/button.d.ts
6
6
  declare const designButtonVariants: (props?: ({
7
7
  variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "plain" | null | undefined;
8
- size?: "sm" | "lg" | "icon" | "default" | null | undefined;
8
+ size?: "icon" | "default" | "sm" | "lg" | null | undefined;
9
9
  } & class_variance_authority_types0.ClassProp) | undefined) => string;
10
10
  type DesignOriginalButtonProps = {
11
11
  asChild?: boolean;
@@ -37,7 +37,7 @@ declare const DesignButton: React.FC<{
37
37
  asChild?: boolean;
38
38
  } & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<(props?: ({
39
39
  variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "plain" | null | undefined;
40
- size?: "sm" | "lg" | "icon" | "default" | null | undefined;
40
+ size?: "icon" | "default" | "sm" | "lg" | null | undefined;
41
41
  } & class_variance_authority_types0.ClassProp) | undefined) => string> & {
42
42
  ref?: React.Ref<HTMLButtonElement> | undefined;
43
43
  }>;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Spinner, Tooltip, TooltipContent, TooltipTrigger, cn } from "@hexclave/ui";
3
+ import { Spinner, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, cn } from "@hexclave/ui";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { useState } from "react";
6
6
  import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
@@ -47,7 +47,7 @@ function DesignPillToggle({ options, selected, onSelect, size = "md", glassmorph
47
47
  runAsynchronouslyWithAlert(Promise.resolve(result).finally(() => setLoadingOptionId(null)));
48
48
  }
49
49
  };
50
- return /* @__PURE__ */ jsx("div", {
50
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx("div", {
51
51
  className: cn("inline-flex items-center gap-1 p-1 rounded-xl", glassmorphic ? "bg-foreground/[0.04] backdrop-blur-sm" : "bg-black/[0.08] dark:bg-white/[0.04]", className),
52
52
  children: options.map((option) => {
53
53
  const isActive = selected === option.id;
@@ -77,7 +77,7 @@ function DesignPillToggle({ options, selected, onSelect, size = "md", glassmorph
77
77
  }, option.id);
78
78
  return pill;
79
79
  })
80
- });
80
+ }) });
81
81
  }
82
82
 
83
83
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"pill-toggle.js","names":[],"sources":["../../../src/components/pill-toggle.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { cn, Spinner, Tooltip, TooltipContent, TooltipTrigger } from \"@hexclave/ui\";\nimport { TooltipPortal } from \"@radix-ui/react-tooltip\";\nimport { runAsynchronouslyWithAlert } from \"@hexclave/shared/dist/utils/promises\";\nimport { useGlassmorphicDefault } from \"./card\";\n\ntype DesignPillToggleSize = \"sm\" | \"md\" | \"lg\";\ntype DesignPillToggleGradient = \"blue\" | \"cyan\" | \"purple\" | \"green\" | \"orange\" | \"default\";\n\nexport type DesignPillToggleOption = {\n id: string,\n label: string,\n icon?: React.ElementType,\n};\n\nexport type DesignPillToggleProps = {\n options: DesignPillToggleOption[],\n selected: string,\n onSelect: (id: string) => void | Promise<void>,\n size?: DesignPillToggleSize,\n glassmorphic?: boolean,\n gradient?: DesignPillToggleGradient,\n /** When false, hides labels and shows a tooltip on hover instead. Defaults to true. */\n showLabels?: boolean,\n className?: string,\n};\n\ntype SizeClass = {\n button: string,\n icon: string,\n};\n\nconst sizeClasses = new Map<DesignPillToggleSize, SizeClass>([\n [\"sm\", { button: \"px-3 py-1.5 text-xs\", icon: \"h-3.5 w-3.5\" }],\n [\"md\", { button: \"px-4 py-2 text-sm\", icon: \"h-4 w-4\" }],\n [\"lg\", { button: \"px-5 py-2.5 text-sm\", icon: \"h-4 w-4\" }],\n]);\n\nconst gradientClasses = new Map<DesignPillToggleGradient, string>([\n [\"blue\", \"ring-blue-500/20 dark:ring-blue-400/20\"],\n [\"cyan\", \"ring-cyan-500/20 dark:ring-cyan-400/20\"],\n [\"purple\", \"ring-purple-500/20 dark:ring-purple-400/20\"],\n [\"green\", \"ring-emerald-500/20 dark:ring-emerald-400/20\"],\n [\"orange\", \"ring-amber-500/20 dark:ring-amber-400/20\"],\n [\"default\", \"ring-black/[0.12] dark:ring-white/[0.06]\"],\n]);\n\nfunction getMapValueOrThrow<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, mapName: string) {\n const value = map.get(key);\n if (!value) {\n throw new Error(`Missing ${mapName} entry for key \"${String(key)}\"`);\n }\n return value;\n}\n\nexport function DesignPillToggle({\n options,\n selected,\n onSelect,\n size = \"md\",\n glassmorphic: glassmorphicProp,\n gradient = \"default\",\n showLabels = true,\n className,\n}: DesignPillToggleProps) {\n const glassmorphic = useGlassmorphicDefault(glassmorphicProp);\n const sizeClass = getMapValueOrThrow(sizeClasses, size, \"sizeClasses\");\n const activeRingClass = getMapValueOrThrow(gradientClasses, gradient, \"gradientClasses\");\n\n const [loadingOptionId, setLoadingOptionId] = useState<string | null>(null);\n\n const handleClick = (optionId: string) => {\n const result = onSelect(optionId);\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setLoadingOptionId(optionId);\n runAsynchronouslyWithAlert(\n Promise.resolve(result).finally(() => setLoadingOptionId(null))\n );\n }\n };\n\n return (\n <div\n className={cn(\n \"inline-flex items-center gap-1 p-1 rounded-xl\",\n glassmorphic\n ? \"bg-foreground/[0.04] backdrop-blur-sm\"\n : \"bg-black/[0.08] dark:bg-white/[0.04]\",\n className\n )}\n >\n {options.map((option) => {\n const isActive = selected === option.id;\n const Icon = option.icon;\n\n const pill = (\n <button\n type=\"button\"\n key={option.id}\n onClick={() => handleClick(option.id)}\n disabled={loadingOptionId !== null}\n className={cn(\n \"relative flex items-center gap-2 font-medium rounded-lg transition-all duration-150 hover:transition-none\",\n sizeClass.button,\n isActive\n ? cn(\n \"bg-background text-foreground shadow-sm ring-1\",\n glassmorphic\n ? \"ring-foreground/[0.06] dark:bg-[hsl(240,71%,70%)]/10 dark:text-[hsl(240,71%,90%)] dark:ring-[hsl(240,71%,70%)]/20\"\n : activeRingClass\n )\n : cn(\n \"text-muted-foreground hover:text-foreground\",\n glassmorphic\n ? \"hover:bg-background/50\"\n : \"hover:bg-black/[0.06] dark:hover:bg-white/[0.04]\"\n )\n )}\n >\n {loadingOptionId === option.id && (\n <Spinner\n size={12}\n className=\"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none\"\n />\n )}\n <span className={cn(\n \"flex items-center gap-2\",\n loadingOptionId === option.id && \"invisible\"\n )}>\n {Icon && <Icon className={sizeClass.icon} />}\n {showLabels && option.label}\n </span>\n </button>\n );\n\n if (!showLabels) {\n return (\n <Tooltip key={option.id} delayDuration={0}>\n <TooltipTrigger asChild>\n {pill}\n </TooltipTrigger>\n <TooltipPortal>\n <TooltipContent side=\"top\">\n {option.label}\n </TooltipContent>\n </TooltipPortal>\n </Tooltip>\n );\n }\n\n return pill;\n })}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAkCA,MAAM,cAAc,IAAI,IAAqC;CAC3D,CAAC,MAAM;EAAE,QAAQ;EAAuB,MAAM;EAAe,CAAC;CAC9D,CAAC,MAAM;EAAE,QAAQ;EAAqB,MAAM;EAAW,CAAC;CACxD,CAAC,MAAM;EAAE,QAAQ;EAAuB,MAAM;EAAW,CAAC;CAC3D,CAAC;AAEF,MAAM,kBAAkB,IAAI,IAAsC;CAChE,CAAC,QAAQ,yCAAyC;CAClD,CAAC,QAAQ,yCAAyC;CAClD,CAAC,UAAU,6CAA6C;CACxD,CAAC,SAAS,+CAA+C;CACzD,CAAC,UAAU,2CAA2C;CACtD,CAAC,WAAW,2CAA2C;CACxD,CAAC;AAEF,SAAS,mBAAiC,KAAwB,KAAW,SAAiB;CAC5F,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,WAAW,QAAQ,kBAAkB,OAAO,IAAI,CAAC,GAAG;AAEtE,QAAO;;AAGT,SAAgB,iBAAiB,EAC/B,SACA,UACA,UACA,OAAO,MACP,cAAc,kBACd,WAAW,WACX,aAAa,MACb,aACwB;CACxB,MAAM,eAAe,uBAAuB,iBAAiB;CAC7D,MAAM,YAAY,mBAAmB,aAAa,MAAM,cAAc;CACtE,MAAM,kBAAkB,mBAAmB,iBAAiB,UAAU,kBAAkB;CAExF,MAAM,CAAC,iBAAiB,sBAAsB,SAAwB,KAAK;CAE3E,MAAM,eAAe,aAAqB;EACxC,MAAM,SAAS,SAAS,SAAS;AACjC,MAAI,UAAU,OAAQ,OAAyB,SAAS,YAAY;AAClE,sBAAmB,SAAS;AAC5B,8BACE,QAAQ,QAAQ,OAAO,CAAC,cAAc,mBAAmB,KAAK,CAAC,CAChE;;;AAIL,QACE,oBAAC;EACC,WAAW,GACT,iDACA,eACI,0CACA,wCACJ,UACD;YAEA,QAAQ,KAAK,WAAW;GACvB,MAAM,WAAW,aAAa,OAAO;GACrC,MAAM,OAAO,OAAO;GAEpB,MAAM,OACJ,qBAAC;IACC,MAAK;IAEL,eAAe,YAAY,OAAO,GAAG;IACrC,UAAU,oBAAoB;IAC9B,WAAW,GACT,6GACA,UAAU,QACV,WACI,GACA,kDACA,eACI,sHACA,gBACL,GACC,GACA,+CACA,eACI,2BACA,mDACL,CACJ;eAEA,oBAAoB,OAAO,MAC1B,oBAAC;KACC,MAAM;KACN,WAAU;MACV,EAEJ,qBAAC;KAAK,WAAW,GACf,2BACA,oBAAoB,OAAO,MAAM,YAClC;gBACE,QAAQ,oBAAC,QAAK,WAAW,UAAU,OAAQ,EAC3C,cAAc,OAAO;MACjB;MAjCF,OAAO,GAkCL;AAGX,OAAI,CAAC,WACH,QACE,qBAAC;IAAwB,eAAe;eACtC,oBAAC;KAAe;eACb;MACc,EACjB,oBAAC,2BACC,oBAAC;KAAe,MAAK;eAClB,OAAO;MACO,GACH;MARJ,OAAO,GASX;AAId,UAAO;IACP;GACE"}
1
+ {"version":3,"file":"pill-toggle.js","names":[],"sources":["../../../src/components/pill-toggle.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { cn, Spinner, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@hexclave/ui\";\nimport { TooltipPortal } from \"@radix-ui/react-tooltip\";\nimport { runAsynchronouslyWithAlert } from \"@hexclave/shared/dist/utils/promises\";\nimport { useGlassmorphicDefault } from \"./card\";\n\ntype DesignPillToggleSize = \"sm\" | \"md\" | \"lg\";\ntype DesignPillToggleGradient = \"blue\" | \"cyan\" | \"purple\" | \"green\" | \"orange\" | \"default\";\n\nexport type DesignPillToggleOption = {\n id: string,\n label: string,\n icon?: React.ElementType,\n};\n\nexport type DesignPillToggleProps = {\n options: DesignPillToggleOption[],\n selected: string,\n onSelect: (id: string) => void | Promise<void>,\n size?: DesignPillToggleSize,\n glassmorphic?: boolean,\n gradient?: DesignPillToggleGradient,\n /** When false, hides labels and shows a tooltip on hover instead. Defaults to true. */\n showLabels?: boolean,\n className?: string,\n};\n\ntype SizeClass = {\n button: string,\n icon: string,\n};\n\nconst sizeClasses = new Map<DesignPillToggleSize, SizeClass>([\n [\"sm\", { button: \"px-3 py-1.5 text-xs\", icon: \"h-3.5 w-3.5\" }],\n [\"md\", { button: \"px-4 py-2 text-sm\", icon: \"h-4 w-4\" }],\n [\"lg\", { button: \"px-5 py-2.5 text-sm\", icon: \"h-4 w-4\" }],\n]);\n\nconst gradientClasses = new Map<DesignPillToggleGradient, string>([\n [\"blue\", \"ring-blue-500/20 dark:ring-blue-400/20\"],\n [\"cyan\", \"ring-cyan-500/20 dark:ring-cyan-400/20\"],\n [\"purple\", \"ring-purple-500/20 dark:ring-purple-400/20\"],\n [\"green\", \"ring-emerald-500/20 dark:ring-emerald-400/20\"],\n [\"orange\", \"ring-amber-500/20 dark:ring-amber-400/20\"],\n [\"default\", \"ring-black/[0.12] dark:ring-white/[0.06]\"],\n]);\n\nfunction getMapValueOrThrow<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, mapName: string) {\n const value = map.get(key);\n if (!value) {\n throw new Error(`Missing ${mapName} entry for key \"${String(key)}\"`);\n }\n return value;\n}\n\nexport function DesignPillToggle({\n options,\n selected,\n onSelect,\n size = \"md\",\n glassmorphic: glassmorphicProp,\n gradient = \"default\",\n showLabels = true,\n className,\n}: DesignPillToggleProps) {\n const glassmorphic = useGlassmorphicDefault(glassmorphicProp);\n const sizeClass = getMapValueOrThrow(sizeClasses, size, \"sizeClasses\");\n const activeRingClass = getMapValueOrThrow(gradientClasses, gradient, \"gradientClasses\");\n\n const [loadingOptionId, setLoadingOptionId] = useState<string | null>(null);\n\n const handleClick = (optionId: string) => {\n const result = onSelect(optionId);\n if (result && typeof (result as Promise<void>).then === \"function\") {\n setLoadingOptionId(optionId);\n runAsynchronouslyWithAlert(\n Promise.resolve(result).finally(() => setLoadingOptionId(null))\n );\n }\n };\n\n return (\n <TooltipProvider>\n <div\n className={cn(\n \"inline-flex items-center gap-1 p-1 rounded-xl\",\n glassmorphic\n ? \"bg-foreground/[0.04] backdrop-blur-sm\"\n : \"bg-black/[0.08] dark:bg-white/[0.04]\",\n className\n )}\n >\n {options.map((option) => {\n const isActive = selected === option.id;\n const Icon = option.icon;\n\n const pill = (\n <button\n type=\"button\"\n key={option.id}\n onClick={() => handleClick(option.id)}\n disabled={loadingOptionId !== null}\n className={cn(\n \"relative flex items-center gap-2 font-medium rounded-lg transition-all duration-150 hover:transition-none\",\n sizeClass.button,\n isActive\n ? cn(\n \"bg-background text-foreground shadow-sm ring-1\",\n glassmorphic\n ? \"ring-foreground/[0.06] dark:bg-[hsl(240,71%,70%)]/10 dark:text-[hsl(240,71%,90%)] dark:ring-[hsl(240,71%,70%)]/20\"\n : activeRingClass\n )\n : cn(\n \"text-muted-foreground hover:text-foreground\",\n glassmorphic\n ? \"hover:bg-background/50\"\n : \"hover:bg-black/[0.06] dark:hover:bg-white/[0.04]\"\n )\n )}\n >\n {loadingOptionId === option.id && (\n <Spinner\n size={12}\n className=\"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none\"\n />\n )}\n <span className={cn(\n \"flex items-center gap-2\",\n loadingOptionId === option.id && \"invisible\"\n )}>\n {Icon && <Icon className={sizeClass.icon} />}\n {showLabels && option.label}\n </span>\n </button>\n );\n\n if (!showLabels) {\n return (\n <Tooltip key={option.id} delayDuration={0}>\n <TooltipTrigger asChild>\n {pill}\n </TooltipTrigger>\n <TooltipPortal>\n <TooltipContent side=\"top\">\n {option.label}\n </TooltipContent>\n </TooltipPortal>\n </Tooltip>\n );\n }\n\n return pill;\n })}\n </div>\n </TooltipProvider>\n );\n}\n"],"mappings":";;;;;;;;;;AAkCA,MAAM,cAAc,IAAI,IAAqC;CAC3D,CAAC,MAAM;EAAE,QAAQ;EAAuB,MAAM;EAAe,CAAC;CAC9D,CAAC,MAAM;EAAE,QAAQ;EAAqB,MAAM;EAAW,CAAC;CACxD,CAAC,MAAM;EAAE,QAAQ;EAAuB,MAAM;EAAW,CAAC;CAC3D,CAAC;AAEF,MAAM,kBAAkB,IAAI,IAAsC;CAChE,CAAC,QAAQ,yCAAyC;CAClD,CAAC,QAAQ,yCAAyC;CAClD,CAAC,UAAU,6CAA6C;CACxD,CAAC,SAAS,+CAA+C;CACzD,CAAC,UAAU,2CAA2C;CACtD,CAAC,WAAW,2CAA2C;CACxD,CAAC;AAEF,SAAS,mBAAiC,KAAwB,KAAW,SAAiB;CAC5F,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,WAAW,QAAQ,kBAAkB,OAAO,IAAI,CAAC,GAAG;AAEtE,QAAO;;AAGT,SAAgB,iBAAiB,EAC/B,SACA,UACA,UACA,OAAO,MACP,cAAc,kBACd,WAAW,WACX,aAAa,MACb,aACwB;CACxB,MAAM,eAAe,uBAAuB,iBAAiB;CAC7D,MAAM,YAAY,mBAAmB,aAAa,MAAM,cAAc;CACtE,MAAM,kBAAkB,mBAAmB,iBAAiB,UAAU,kBAAkB;CAExF,MAAM,CAAC,iBAAiB,sBAAsB,SAAwB,KAAK;CAE3E,MAAM,eAAe,aAAqB;EACxC,MAAM,SAAS,SAAS,SAAS;AACjC,MAAI,UAAU,OAAQ,OAAyB,SAAS,YAAY;AAClE,sBAAmB,SAAS;AAC5B,8BACE,QAAQ,QAAQ,OAAO,CAAC,cAAc,mBAAmB,KAAK,CAAC,CAChE;;;AAIL,QACE,oBAAC,6BACC,oBAAC;EACC,WAAW,GACT,iDACA,eACI,0CACA,wCACJ,UACD;YAEA,QAAQ,KAAK,WAAW;GACvB,MAAM,WAAW,aAAa,OAAO;GACrC,MAAM,OAAO,OAAO;GAEpB,MAAM,OACJ,qBAAC;IACC,MAAK;IAEL,eAAe,YAAY,OAAO,GAAG;IACrC,UAAU,oBAAoB;IAC9B,WAAW,GACT,6GACA,UAAU,QACV,WACI,GACA,kDACA,eACI,sHACA,gBACL,GACC,GACA,+CACA,eACI,2BACA,mDACL,CACJ;eAEA,oBAAoB,OAAO,MAC1B,oBAAC;KACC,MAAM;KACN,WAAU;MACV,EAEJ,qBAAC;KAAK,WAAW,GACf,2BACA,oBAAoB,OAAO,MAAM,YAClC;gBACE,QAAQ,oBAAC,QAAK,WAAW,UAAU,OAAQ,EAC3C,cAAc,OAAO;MACjB;MAjCF,OAAO,GAkCL;AAGX,OAAI,CAAC,WACH,QACE,qBAAC;IAAwB,eAAe;eACtC,oBAAC;KAAe;eACb;MACc,EACjB,oBAAC,2BACC,oBAAC;KAAe,MAAK;eAClB,OAAO;MACO,GACH;MARJ,OAAO,GASX;AAId,UAAO;IACP;GACE,GACU"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexclave/dashboard-ui-components",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "repository": "https://github.com/hexclave/hexclave",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "README.md",
10
10
  "dist",
11
+ "src",
11
12
  "CHANGELOG.md",
12
13
  "LICENSE"
13
14
  ],
@@ -49,8 +50,8 @@
49
50
  "@tanstack/react-table": "^8.21.3",
50
51
  "@tanstack/react-virtual": "^3.13.0",
51
52
  "class-variance-authority": "^0.7.0",
52
- "@hexclave/ui": "1.0.3",
53
- "@hexclave/shared": "1.0.3"
53
+ "@hexclave/shared": "1.0.5",
54
+ "@hexclave/ui": "1.0.5"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/react": "^19.0.0",
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { CheckCircle, Info, WarningCircle, XCircle } from "@phosphor-icons/react";
4
+ import { cn } from "@hexclave/ui";
5
+
6
+ type DesignAlertVariant = "default" | "success" | "error" | "warning" | "info";
7
+
8
+ const variantIconMap = new Map<DesignAlertVariant, React.ElementType>([
9
+ ["default", Info],
10
+ ["success", CheckCircle],
11
+ ["error", XCircle],
12
+ ["warning", WarningCircle],
13
+ ["info", Info],
14
+ ]);
15
+
16
+ type VariantStyles = {
17
+ container: string,
18
+ icon: string,
19
+ title: string,
20
+ };
21
+
22
+ const variantStyles = new Map<DesignAlertVariant, VariantStyles>([
23
+ [
24
+ "default",
25
+ {
26
+ container: "bg-background border-border",
27
+ icon: "text-foreground",
28
+ title: "text-foreground",
29
+ },
30
+ ],
31
+ [
32
+ "success",
33
+ {
34
+ container: "bg-green-500/[0.06] border-green-500/30",
35
+ icon: "text-green-500",
36
+ title: "text-green-600 dark:text-green-400",
37
+ },
38
+ ],
39
+ [
40
+ "error",
41
+ {
42
+ container: "bg-red-500/[0.06] border-red-500/30",
43
+ icon: "text-red-500",
44
+ title: "text-red-600 dark:text-red-400",
45
+ },
46
+ ],
47
+ [
48
+ "warning",
49
+ {
50
+ container: "bg-amber-500/[0.08] border-amber-500/40",
51
+ icon: "text-amber-600 dark:text-amber-400",
52
+ title: "text-amber-700 dark:text-amber-300",
53
+ },
54
+ ],
55
+ [
56
+ "info",
57
+ {
58
+ container: "bg-blue-500/[0.06] border-blue-500/30",
59
+ icon: "text-blue-500",
60
+ title: "text-blue-600 dark:text-blue-400",
61
+ },
62
+ ],
63
+ ]);
64
+
65
+ function getMapValueOrThrow<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, mapName: string) {
66
+ const value = map.get(key);
67
+ if (!value) {
68
+ throw new Error(`Missing ${mapName} entry for key "${String(key)}"`);
69
+ }
70
+ return value;
71
+ }
72
+
73
+ export type DesignAlertProps = React.HTMLAttributes<HTMLDivElement> & {
74
+ variant?: DesignAlertVariant,
75
+ title?: React.ReactNode,
76
+ description?: React.ReactNode,
77
+ glassmorphic?: boolean,
78
+ };
79
+
80
+ export function DesignAlert({
81
+ variant = "default",
82
+ title,
83
+ description,
84
+ glassmorphic = false,
85
+ className,
86
+ children,
87
+ ...props
88
+ }: DesignAlertProps) {
89
+ const styles = getMapValueOrThrow(variantStyles, variant, "variantStyles");
90
+ const Icon = getMapValueOrThrow(variantIconMap, variant, "variantIconMap");
91
+
92
+ return (
93
+ <div
94
+ role="alert"
95
+ className={cn(
96
+ "relative w-full rounded-2xl border p-4 text-sm",
97
+ "flex gap-3 items-start",
98
+ styles.container,
99
+ glassmorphic && "backdrop-blur-xl",
100
+ className
101
+ )}
102
+ {...props}
103
+ >
104
+ <Icon className={cn("h-4 w-4 mt-0.5 flex-shrink-0", styles.icon)} />
105
+ <div className="min-w-0">
106
+ {title && (
107
+ <h5 className={cn("mb-1 font-medium leading-none tracking-tight", styles.title)}>
108
+ {title}
109
+ </h5>
110
+ )}
111
+ {description && (
112
+ <div className="text-sm text-foreground/80 dark:text-muted-foreground [&_p]:leading-relaxed">
113
+ {description}
114
+ </div>
115
+ )}
116
+ {children}
117
+ </div>
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,369 @@
1
+ import { MagnifyingGlassMinusIcon } from "@phosphor-icons/react";
2
+ import { cn } from "@hexclave/ui";
3
+ import { type DesignChartConfig, DesignChartContainer } from "../chart-container";
4
+ import { type CSSProperties, type Ref, useEffect, useMemo, useState } from "react";
5
+ import { Cell, Pie, PieChart } from "recharts";
6
+ import { TrendPill } from "./default-analytics-chart-tooltip";
7
+ import { formatDelta } from "./format";
8
+ import type { AnalyticsChartStrings } from "./strings";
9
+ import {
10
+ cssIdent,
11
+ type AnalyticsChartPieProps,
12
+ type AnalyticsChartSeries,
13
+ type FormatKind,
14
+ type Point,
15
+ } from "./types";
16
+
17
+ type SegmentColors = {
18
+ primary: { light: string[], dark: string[] },
19
+ compare: { light: string[], dark: string[] },
20
+ };
21
+
22
+ type AnalyticsChartPieBodyProps = {
23
+ wrapperRef: Ref<HTMLDivElement>,
24
+ primarySegmentSeries: readonly AnalyticsChartSeries[],
25
+ compareSegmentSeries: readonly AnalyticsChartSeries[],
26
+ aggregatedPrimarySegments: number[],
27
+ aggregatedCompareSegments: number[],
28
+ aggregatedPrimaryTotal: number,
29
+ aggregatedCompareTotal: number,
30
+ segmentColors: SegmentColors,
31
+ showPrimary: boolean,
32
+ showCompare: boolean,
33
+ xFormatKind: FormatKind,
34
+ yFormatKind: FormatKind,
35
+ hoverKey: string | null,
36
+ setHoverKey: (k: string | null) => void,
37
+ zoomRange: [number, number] | null,
38
+ onResetZoom: () => void,
39
+ visibleStart: number,
40
+ visibleEnd: number,
41
+ fullData: Point[],
42
+ strings: AnalyticsChartStrings,
43
+ fmtValue: (value: number, kind: FormatKind) => string,
44
+ pie: AnalyticsChartPieProps | undefined,
45
+ };
46
+
47
+ const DEFAULT_PIE_CLASSNAME = "aspect-square h-[220px] w-[220px] sm:h-[240px] sm:w-[240px]";
48
+
49
+ export function AnalyticsChartPie({
50
+ wrapperRef,
51
+ primarySegmentSeries,
52
+ compareSegmentSeries,
53
+ aggregatedPrimarySegments,
54
+ aggregatedCompareSegments,
55
+ aggregatedPrimaryTotal,
56
+ aggregatedCompareTotal,
57
+ segmentColors,
58
+ showPrimary,
59
+ showCompare,
60
+ xFormatKind,
61
+ yFormatKind,
62
+ hoverKey,
63
+ setHoverKey,
64
+ zoomRange,
65
+ onResetZoom,
66
+ visibleStart,
67
+ visibleEnd,
68
+ fullData,
69
+ strings,
70
+ fmtValue,
71
+ pie,
72
+ }: AnalyticsChartPieBodyProps) {
73
+ const innerRadius = pie?.innerRadius ?? 60;
74
+ const outerRadius = pie?.outerRadius ?? 84;
75
+ const compareInnerRadius = pie?.compareInnerRadius ?? 36;
76
+ const compareOuterRadius = pie?.compareOuterRadius ?? 52;
77
+ const containerClassName = pie?.className ?? DEFAULT_PIE_CLASSNAME;
78
+ const showDateRange = pie?.showDateRange ?? true;
79
+ const centerTextMaxWidth = Math.max(64, innerRadius * 2 - 28);
80
+ const segmentTransitionStyle: CSSProperties = {
81
+ transition: "opacity 180ms ease-out",
82
+ };
83
+
84
+ const canonicalSeries = primarySegmentSeries.length > 0
85
+ ? primarySegmentSeries
86
+ : compareSegmentSeries;
87
+ const usePrimaryForCanonical = primarySegmentSeries.length > 0;
88
+
89
+ const compareValueByKey = useMemo(() => {
90
+ const m = new Map<string, number>();
91
+ compareSegmentSeries.forEach((s, sIdx) => {
92
+ m.set(s.key, aggregatedCompareSegments[sIdx] ?? 0);
93
+ });
94
+ return m;
95
+ }, [compareSegmentSeries, aggregatedCompareSegments]);
96
+ const primaryValueByKey = useMemo(() => {
97
+ const m = new Map<string, number>();
98
+ primarySegmentSeries.forEach((s, sIdx) => {
99
+ m.set(s.key, aggregatedPrimarySegments[sIdx] ?? 0);
100
+ });
101
+ return m;
102
+ }, [primarySegmentSeries, aggregatedPrimarySegments]);
103
+
104
+ const canonicalTotal = usePrimaryForCanonical
105
+ ? aggregatedPrimaryTotal
106
+ : aggregatedCompareTotal;
107
+
108
+ const legendRows = useMemo(
109
+ () =>
110
+ canonicalSeries
111
+ .map((s, sIdx) => {
112
+ const value = usePrimaryForCanonical
113
+ ? (aggregatedPrimarySegments[sIdx] ?? 0)
114
+ : (aggregatedCompareSegments[sIdx] ?? 0);
115
+ const prevValue = usePrimaryForCanonical
116
+ ? (compareValueByKey.get(s.key) ?? 0)
117
+ : (primaryValueByKey.get(s.key) ?? 0);
118
+ return {
119
+ key: s.key,
120
+ label: s.label,
121
+ sIdx,
122
+ value,
123
+ prevValue,
124
+ pct: canonicalTotal > 0 ? value / canonicalTotal : 0,
125
+ fill: segmentColors.primary.light[sIdx],
126
+ fillDark: segmentColors.primary.dark[sIdx],
127
+ fillCompare: segmentColors.compare.light[sIdx],
128
+ fillCompareDark: segmentColors.compare.dark[sIdx],
129
+ };
130
+ })
131
+ .sort((a, b) => b.value - a.value),
132
+ [
133
+ canonicalSeries,
134
+ usePrimaryForCanonical,
135
+ aggregatedPrimarySegments,
136
+ aggregatedCompareSegments,
137
+ compareValueByKey,
138
+ primaryValueByKey,
139
+ canonicalTotal,
140
+ segmentColors,
141
+ ],
142
+ );
143
+
144
+ const chartConfig = useMemo<DesignChartConfig>(() => {
145
+ const config: DesignChartConfig = {};
146
+ canonicalSeries.forEach((s, sIdx) => {
147
+ config[cssIdent(s.key)] = {
148
+ label: s.label,
149
+ theme: {
150
+ light: segmentColors.primary.light[sIdx],
151
+ dark: segmentColors.primary.dark[sIdx],
152
+ },
153
+ };
154
+ config[`compare-${cssIdent(s.key)}`] = {
155
+ label: s.label,
156
+ theme: {
157
+ light: segmentColors.compare.light[sIdx],
158
+ dark: segmentColors.compare.dark[sIdx],
159
+ },
160
+ };
161
+ });
162
+ return config;
163
+ }, [canonicalSeries, segmentColors]);
164
+
165
+ const activeRow = hoverKey != null
166
+ ? legendRows.find((r) => r.key === hoverKey) ?? null
167
+ : null;
168
+ const [centerDisplayKey, setCenterDisplayKey] = useState<string | null>(null);
169
+ const [centerDisplayVisible, setCenterDisplayVisible] = useState(true);
170
+ const centerDisplayRow = centerDisplayKey != null
171
+ ? legendRows.find((r) => r.key === centerDisplayKey) ?? null
172
+ : null;
173
+ useEffect(() => {
174
+ const nextKey = activeRow?.key ?? null;
175
+ if (nextKey === centerDisplayKey) {
176
+ return;
177
+ }
178
+
179
+ setCenterDisplayVisible(false);
180
+ const timeoutId = window.setTimeout(() => {
181
+ setCenterDisplayKey(nextKey);
182
+ setCenterDisplayVisible(true);
183
+ }, 120);
184
+
185
+ return () => {
186
+ window.clearTimeout(timeoutId);
187
+ };
188
+ }, [activeRow?.key, centerDisplayKey]);
189
+
190
+ const activeDelta = activeRow
191
+ ? formatDelta(activeRow.value, activeRow.prevValue)
192
+ : formatDelta(aggregatedPrimaryTotal, aggregatedCompareTotal);
193
+
194
+ const windowDays = visibleEnd - visibleStart + 1;
195
+ const startLabel = fmtValue(fullData[visibleStart]!.ts, xFormatKind);
196
+ const endLabel = fmtValue(fullData[visibleEnd]!.ts, xFormatKind);
197
+
198
+ const outerData = legendRows.map((r) => ({ name: cssIdent(r.key), hoverKey: r.key, value: r.value, fill: r.fill }));
199
+ const innerData = legendRows.map((r) => ({ name: cssIdent(r.key), hoverKey: r.key, value: r.prevValue, fill: r.fillCompare }));
200
+ const activeIdx = hoverKey != null ? legendRows.findIndex((r) => r.key === hoverKey) : -1;
201
+
202
+ return (
203
+ <div
204
+ ref={wrapperRef}
205
+ className="relative flex h-full min-h-0 w-full min-w-0 flex-1 flex-col select-none"
206
+ onClick={(e) => e.stopPropagation()}
207
+ >
208
+ {zoomRange && (
209
+ <div className="absolute right-2 top-2 z-20">
210
+ <button
211
+ type="button"
212
+ onClick={onResetZoom}
213
+ className="inline-flex items-center gap-1.5 rounded-full bg-blue-500/10 px-2.5 py-1 font-mono text-[10px] font-semibold uppercase tracking-wider text-blue-600 ring-1 ring-blue-500/30 transition-colors duration-150 hover:bg-blue-500/15 hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 dark:text-blue-300 dark:ring-blue-400/30"
214
+ >
215
+ <MagnifyingGlassMinusIcon weight="bold" className="size-3" aria-hidden="true" />
216
+ <span>{strings.resetZoom}</span>
217
+ </button>
218
+ </div>
219
+ )}
220
+
221
+ <div className="flex h-full min-h-0 w-full flex-1 flex-wrap content-center items-center justify-center gap-x-10 gap-y-6">
222
+ <div className="flex shrink-0 flex-col items-center gap-3">
223
+ <div className="relative">
224
+ <DesignChartContainer config={chartConfig} className={containerClassName}>
225
+ <PieChart
226
+ role="img"
227
+ aria-label={strings.pieAriaLabel({
228
+ segmentCount: canonicalSeries.length,
229
+ windowDays,
230
+ })}
231
+ >
232
+ {showPrimary && (
233
+ <Pie
234
+ data={outerData}
235
+ dataKey="value"
236
+ nameKey="name"
237
+ cx="50%"
238
+ cy="50%"
239
+ innerRadius={innerRadius}
240
+ outerRadius={outerRadius}
241
+ paddingAngle={1}
242
+ startAngle={90}
243
+ endAngle={-270}
244
+ isAnimationActive={false}
245
+ stroke="none"
246
+ >
247
+ {outerData.map((d, i) => {
248
+ const inactive = activeIdx >= 0 && activeIdx !== i;
249
+ return (
250
+ <Cell
251
+ key={`outer-${d.name}`}
252
+ fill={`var(--color-${d.name})`}
253
+ opacity={inactive ? 0.22 : 1}
254
+ style={segmentTransitionStyle}
255
+ onMouseEnter={() => setHoverKey(d.hoverKey)}
256
+ onMouseLeave={() => setHoverKey(null)}
257
+ />
258
+ );
259
+ })}
260
+ </Pie>
261
+ )}
262
+ {showCompare && (
263
+ <Pie
264
+ data={innerData}
265
+ dataKey="value"
266
+ nameKey="name"
267
+ cx="50%"
268
+ cy="50%"
269
+ innerRadius={compareInnerRadius}
270
+ outerRadius={compareOuterRadius}
271
+ paddingAngle={1}
272
+ startAngle={90}
273
+ endAngle={-270}
274
+ isAnimationActive={false}
275
+ stroke="none"
276
+ >
277
+ {innerData.map((d, i) => {
278
+ const inactive = activeIdx >= 0 && activeIdx !== i;
279
+ return (
280
+ <Cell
281
+ key={`inner-${d.name}`}
282
+ fill={`var(--color-compare-${d.name})`}
283
+ opacity={inactive ? 0.22 : 0.95}
284
+ style={segmentTransitionStyle}
285
+ onMouseEnter={() => setHoverKey(d.hoverKey)}
286
+ onMouseLeave={() => setHoverKey(null)}
287
+ />
288
+ );
289
+ })}
290
+ </Pie>
291
+ )}
292
+ </PieChart>
293
+ </DesignChartContainer>
294
+
295
+ <div
296
+ className={cn(
297
+ "pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center transition-opacity duration-200 ease-out",
298
+ centerDisplayVisible ? "opacity-100" : "opacity-35",
299
+ )}
300
+ >
301
+ <span className="block truncate font-mono text-[9px] uppercase tracking-wider text-muted-foreground" style={{ maxWidth: centerTextMaxWidth }}>
302
+ {centerDisplayRow ? centerDisplayRow.label : strings.pieTotalCenter}
303
+ </span>
304
+ <span className="mt-0.5 block truncate font-mono text-xl font-semibold leading-none tabular-nums text-foreground" style={{ maxWidth: centerTextMaxWidth }}>
305
+ {fmtValue(centerDisplayRow ? centerDisplayRow.value : canonicalTotal, yFormatKind)}
306
+ </span>
307
+ </div>
308
+ </div>
309
+
310
+ {(showDateRange || showCompare) && (
311
+ <div className="flex items-center gap-2 text-center">
312
+ {showDateRange && (
313
+ <span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground/80">
314
+ {startLabel} – {endLabel}
315
+ </span>
316
+ )}
317
+ {showCompare && (
318
+ <TrendPill delta={activeDelta} size="sm" label={strings.pieVsPrev} />
319
+ )}
320
+ </div>
321
+ )}
322
+ </div>
323
+
324
+ <ul className="flex min-w-[200px] max-w-[300px] flex-col gap-1">
325
+ {legendRows.map((r) => {
326
+ const isActive = hoverKey === r.key;
327
+ const dimmed = hoverKey != null && !isActive;
328
+ const rowDelta = formatDelta(r.value, r.prevValue);
329
+ return (
330
+ <li key={r.key}>
331
+ <button
332
+ type="button"
333
+ onMouseEnter={() => setHoverKey(r.key)}
334
+ onMouseLeave={() => setHoverKey(null)}
335
+ onFocus={() => setHoverKey(r.key)}
336
+ onBlur={() => setHoverKey(null)}
337
+ className={cn(
338
+ "flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left transition-[background-color,opacity] duration-200 ease-out hover:bg-foreground/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/20",
339
+ isActive && "bg-foreground/[0.05]",
340
+ dimmed && "opacity-50",
341
+ )}
342
+ >
343
+ <span
344
+ className="size-2.5 shrink-0 rounded-[3px] bg-[var(--c-l)] dark:bg-[var(--c-d)]"
345
+ style={{ "--c-l": r.fill, "--c-d": r.fillDark } as CSSProperties}
346
+ />
347
+ <span className="min-w-0 flex-1 truncate text-[12px] font-medium text-foreground">
348
+ {r.label}
349
+ </span>
350
+ <span className="shrink-0 font-mono text-[11px] tabular-nums text-muted-foreground">
351
+ {(r.pct * 100).toFixed(1)}%
352
+ </span>
353
+ <span className="shrink-0 min-w-[48px] text-right font-mono text-[11px] font-semibold tabular-nums text-foreground">
354
+ {fmtValue(r.value, yFormatKind)}
355
+ </span>
356
+ {showCompare && (
357
+ <span className="shrink-0">
358
+ <TrendPill delta={rowDelta} size="sm" />
359
+ </span>
360
+ )}
361
+ </button>
362
+ </li>
363
+ );
364
+ })}
365
+ </ul>
366
+ </div>
367
+ </div>
368
+ );
369
+ }