uibee 3.1.1 → 3.1.3

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.
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { t as useClickOutside } from "../../useClickOutside-Cmp-RsP3.js";
2
+ import { t as useClickOutside$1 } from "../../useClickOutside-Cmp-RsP3.js";
3
3
  import { D as Facebook, Kt as Wikijs, N as Github, W as Linkedin, x as Discord } from "../../icons-lZYQ6Vlr.js";
4
- import React, { createContext, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
5
- import { AlertCircle, AlertTriangle, ArrowUpRight, Calendar, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, CircleAlert, CircleHelp, Clock, EllipsisVertical, ExternalLink, Eye, Globe, Info, LogIn, LogOut, Pencil, Search, TriangleAlert, User, X } from "lucide-react";
4
+ import React, { Suspense, createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
5
+ import { AlertCircle, AlertTriangle, ArrowUpRight, Calendar, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, ChevronsUpDown, CircleAlert, CircleHelp, Clock, EllipsisVertical, ExternalLink, Eye, Globe, Info, LogIn, LogOut, Pencil, Search, TableIcon, TriangleAlert, User, X } from "lucide-react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import Markdown from "react-markdown";
8
8
  import remarkGfm from "remark-gfm";
@@ -651,7 +651,7 @@ function Input(props) {
651
651
  const [isOpen, setIsOpen] = useState(false);
652
652
  const anchorName = `--input-${useId().replace(/:/g, "")}`;
653
653
  const containerRef = useRef(null);
654
- useClickOutside(containerRef, () => setIsOpen(false));
654
+ useClickOutside$1(containerRef, () => setIsOpen(false));
655
655
  const isDateType = [
656
656
  "date",
657
657
  "datetime-local",
@@ -988,7 +988,7 @@ function Select({ label, name, value, onChange, options, error, className, disab
988
988
  setSelectedOption(options.find((opt) => opt.value === value));
989
989
  }, [value, options]);
990
990
  const containerRef = useRef(null);
991
- useClickOutside(containerRef, () => setIsOpen(false));
991
+ useClickOutside$1(containerRef, () => setIsOpen(false));
992
992
  function handleSelect(option) {
993
993
  if (disabled) return;
994
994
  setSelectedOption(option);
@@ -1200,7 +1200,7 @@ function TagInput({ label, name, value = [], onChange, placeholder = "Add...", e
1200
1200
  function MultiSelect({ label, name, options, value = [], onChange, placeholder = "Select…", error, className, disabled, required, info, description, textSize = "sm" }) {
1201
1201
  const [open, setOpen] = useState(false);
1202
1202
  const containerRef = useRef(null);
1203
- useClickOutside(containerRef, () => setOpen(false));
1203
+ useClickOutside$1(containerRef, () => setOpen(false));
1204
1204
  function toggleOption(optionValue) {
1205
1205
  if (!onChange) return;
1206
1206
  if (value.includes(optionValue)) onChange(value.filter((v) => v !== optionValue));
@@ -2649,7 +2649,9 @@ const variants = {
2649
2649
  success: "bg-green-600 text-white hover:brightness-110",
2650
2650
  info: "bg-blue-600 text-white hover:brightness-110"
2651
2651
  };
2652
- const base = "cursor-pointer px-4 rounded-md h-9 flex items-center justify-center gap-2 select-none text-sm font-medium transition-all duration-150 w-fit";
2652
+ const base = "cursor-pointer rounded-md h-9 flex items-center justify-center gap-2 select-none text-sm font-medium transition-all duration-150";
2653
+ const withText = "px-4 w-fit";
2654
+ const iconOnly = "w-9";
2653
2655
  const disabledCls = "opacity-40 cursor-not-allowed pointer-events-none";
2654
2656
  const iconWrap = (icon) => icon ? /* @__PURE__ */ jsx("span", {
2655
2657
  className: "[&>svg]:w-4 [&>svg]:h-4 flex items-center",
@@ -2657,7 +2659,8 @@ const iconWrap = (icon) => icon ? /* @__PURE__ */ jsx("span", {
2657
2659
  }) : null;
2658
2660
  function Button({ text, className, icon, path, variant = "primary", type, onClick, disabled }) {
2659
2661
  const color = variants[variant];
2660
- const cls = `${base} ${color} ${disabled ? disabledCls : ""} ${className || ""}`;
2662
+ const sizing = icon && !text ? iconOnly : withText;
2663
+ const cls = `${base} ${sizing} ${color} ${disabled ? disabledCls : ""} ${className || ""}`;
2661
2664
  if (!path) return /* @__PURE__ */ jsxs("button", {
2662
2665
  type: type || "button",
2663
2666
  disabled,
@@ -2672,7 +2675,7 @@ function Button({ text, className, icon, path, variant = "primary", type, onClic
2672
2675
  });
2673
2676
  return /* @__PURE__ */ jsxs(Link, {
2674
2677
  href: path,
2675
- className: `${base} ${color} ${className || ""}`,
2678
+ className: `${base} ${sizing} ${color} ${className || ""}`,
2676
2679
  children: [iconWrap(icon), text]
2677
2680
  });
2678
2681
  }
@@ -2702,138 +2705,387 @@ function Alert({ children, variant = "warning", className = "" }) {
2702
2705
  });
2703
2706
  }
2704
2707
  //#endregion
2708
+ //#region src/components/table/constants.ts
2709
+ const HIGHLIGHT = {
2710
+ green: "bg-green-500/15 text-green-400 ring-1 ring-green-500/25",
2711
+ yellow: "bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/25",
2712
+ red: "bg-red-500/15 text-red-400 ring-1 ring-red-500/25",
2713
+ blue: "bg-blue-500/15 text-blue-400 ring-1 ring-blue-500/25",
2714
+ gray: "bg-gray-500/15 text-gray-400 ring-1 ring-gray-500/25",
2715
+ orange: "bg-orange-500/15 text-orange-400 ring-1 ring-orange-500/25",
2716
+ purple: "bg-purple-500/15 text-purple-400 ring-1 ring-purple-500/25"
2717
+ };
2718
+ const DENSITY_TH = {
2719
+ compact: "px-4 py-2",
2720
+ comfortable: "px-6 py-3",
2721
+ spacious: "px-8 py-4"
2722
+ };
2723
+ const DENSITY_TD = {
2724
+ compact: "px-4 py-1.5",
2725
+ comfortable: "px-6 py-3.5",
2726
+ spacious: "px-8 py-5"
2727
+ };
2728
+ const VARIANT_CONTAINER = {
2729
+ original: "bg-login-500/50 rounded-lg border border-login-600 shadow",
2730
+ modern: "bg-transparent"
2731
+ };
2732
+ const VARIANT_THEAD = {
2733
+ original: "bg-login-700",
2734
+ modern: "bg-transparent border-b border-login-500/40"
2735
+ };
2736
+ const VARIANT_THEAD_TH = {
2737
+ original: "text-login-200",
2738
+ modern: "text-login-300"
2739
+ };
2740
+ const VARIANT_TBODY = {
2741
+ original: "bg-login-500/50 divide-login-600",
2742
+ modern: "divide-login-600/15"
2743
+ };
2744
+ const VARIANT_ROW_HOVER = {
2745
+ original: "hover:bg-login-600/30",
2746
+ modern: "hover:bg-login-700/50"
2747
+ };
2748
+ const VARIANT_ROW_STRIPED = {
2749
+ original: "even:bg-login-600/40",
2750
+ modern: "even:bg-login-800/40"
2751
+ };
2752
+ //#endregion
2753
+ //#region src/components/table/header.tsx
2754
+ function formatLabel(key, label) {
2755
+ if (label) return label;
2756
+ if (key.length <= 2) return key.toUpperCase();
2757
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll("_", " ").replaceAll("-", " ").split(/\s+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
2758
+ }
2759
+ function Header({ columns, sort, onSort, hasMenu, hasSelect, hasExpand, allSelected, someSelected, onSelectAll, variant, density }) {
2760
+ function handleSort(key) {
2761
+ onSort({
2762
+ column: key,
2763
+ order: sort?.column === key && sort?.order === "asc" ? "desc" : "asc"
2764
+ });
2765
+ }
2766
+ return /* @__PURE__ */ jsx("thead", {
2767
+ className: `block w-full sticky top-0 z-10 ${VARIANT_THEAD[variant]}`,
2768
+ children: /* @__PURE__ */ jsxs("tr", {
2769
+ className: "flex w-full",
2770
+ children: [
2771
+ hasSelect && /* @__PURE__ */ jsx("th", {
2772
+ className: "flex items-center justify-center",
2773
+ style: {
2774
+ width: "3rem",
2775
+ minWidth: "3rem",
2776
+ flexShrink: 0
2777
+ },
2778
+ children: /* @__PURE__ */ jsxs("button", {
2779
+ type: "button",
2780
+ "aria-label": allSelected ? "Deselect all" : "Select all",
2781
+ onClick: onSelectAll,
2782
+ className: `
2783
+ h-4 w-4 rounded border flex items-center justify-center transition-colors cursor-pointer
2784
+ ${allSelected || someSelected ? "bg-login border-login text-white" : "border-login-400 bg-transparent hover:border-login-200"}
2785
+ `,
2786
+ children: [someSelected && !allSelected && /* @__PURE__ */ jsx("span", { className: "block h-0.5 w-2 bg-white rounded-full" }), allSelected && /* @__PURE__ */ jsx("svg", {
2787
+ className: "h-3 w-3",
2788
+ viewBox: "0 0 12 12",
2789
+ fill: "none",
2790
+ children: /* @__PURE__ */ jsx("path", {
2791
+ d: "M2 6l3 3 5-5",
2792
+ stroke: "currentColor",
2793
+ strokeWidth: "1.5",
2794
+ strokeLinecap: "round",
2795
+ strokeLinejoin: "round"
2796
+ })
2797
+ })]
2798
+ })
2799
+ }),
2800
+ columns.map((col) => {
2801
+ const sortable = col.sortable !== false;
2802
+ const isActive = sort?.column === col.key;
2803
+ const ariaSort = isActive ? sort.order === "asc" ? "ascending" : "descending" : "none";
2804
+ const alignClass = col.align === "right" ? "justify-end" : col.align === "center" ? "justify-center" : "";
2805
+ return /* @__PURE__ */ jsx("th", {
2806
+ "aria-sort": sortable ? ariaSort : void 0,
2807
+ style: col.width ? {
2808
+ width: col.width,
2809
+ flexShrink: 0
2810
+ } : void 0,
2811
+ className: `
2812
+ ${col.width ? "" : "flex-1"}
2813
+ text-xs font-medium uppercase tracking-wider
2814
+ ${DENSITY_TH[density]} ${VARIANT_THEAD_TH[variant]}
2815
+ `,
2816
+ children: sortable ? /* @__PURE__ */ jsxs("button", {
2817
+ type: "button",
2818
+ className: `group inline-flex items-center gap-1.5 w-full cursor-pointer ${alignClass}`,
2819
+ onClick: () => handleSort(col.key),
2820
+ children: [/* @__PURE__ */ jsx("span", {
2821
+ className: "whitespace-nowrap",
2822
+ children: formatLabel(col.key, col.label)
2823
+ }), /* @__PURE__ */ jsx("span", {
2824
+ className: "shrink-0 text-current",
2825
+ children: isActive ? sort.order === "asc" ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { className: "h-3.5 w-3.5 opacity-0 group-hover:opacity-35 transition-opacity" })
2826
+ })]
2827
+ }) : /* @__PURE__ */ jsx("span", {
2828
+ className: `flex w-full ${alignClass}`,
2829
+ children: formatLabel(col.key, col.label)
2830
+ })
2831
+ }, col.key);
2832
+ }),
2833
+ hasExpand && /* @__PURE__ */ jsx("th", {
2834
+ className: "flex items-center justify-center",
2835
+ style: {
2836
+ width: "2.5rem",
2837
+ minWidth: "2.5rem",
2838
+ flexShrink: 0
2839
+ },
2840
+ children: /* @__PURE__ */ jsx("span", {
2841
+ className: `text-xs font-medium tracking-wider uppercase opacity-50 ${VARIANT_THEAD_TH[variant]}`,
2842
+ children: "+"
2843
+ })
2844
+ }),
2845
+ hasMenu && /* @__PURE__ */ jsx("th", {
2846
+ "aria-hidden": "true",
2847
+ style: {
2848
+ width: "3.5rem",
2849
+ minWidth: "3.5rem",
2850
+ flexShrink: 0
2851
+ }
2852
+ })
2853
+ ]
2854
+ })
2855
+ });
2856
+ }
2857
+ //#endregion
2858
+ //#region src/components/table/format.ts
2859
+ const nullableTimeKeys = /* @__PURE__ */ new Set([
2860
+ "date",
2861
+ "last_sent",
2862
+ "sent_at",
2863
+ "time"
2864
+ ]);
2865
+ const RX_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
2866
+ const RX_DATE = /^\d{4}-\d{2}-\d{2}$/;
2867
+ const RX_TIME = /^\d{2}:\d{2}(:\d{2})?$/;
2868
+ function formatValue(key, value) {
2869
+ if (value === null || value === void 0) return nullableTimeKeys.has(key) ? "Never" : "-";
2870
+ if (typeof value === "boolean") return value ? "Yes" : "No";
2871
+ if (typeof value === "number") {
2872
+ if (key.includes("capacity") && value === 0) return "Unlimited";
2873
+ return value;
2874
+ }
2875
+ if (typeof value === "string") {
2876
+ if (RX_DATETIME.test(value)) return new Date(value).toLocaleString("nb-NO", {
2877
+ timeZone: "Europe/Oslo",
2878
+ year: "numeric",
2879
+ month: "2-digit",
2880
+ day: "2-digit",
2881
+ hour: "2-digit",
2882
+ minute: "2-digit"
2883
+ });
2884
+ if (RX_DATE.test(value)) return new Date(value).toLocaleDateString("nb-NO", { timeZone: "Europe/Oslo" });
2885
+ if (RX_TIME.test(value)) return (/* @__PURE__ */ new Date(`1970-01-01T${value}`)).toLocaleTimeString("nb-NO", {
2886
+ timeZone: "Europe/Oslo",
2887
+ hour: "2-digit",
2888
+ minute: "2-digit"
2889
+ });
2890
+ return value;
2891
+ }
2892
+ if (Array.isArray(value)) return value.join(", ");
2893
+ return String(value);
2894
+ }
2895
+ //#endregion
2896
+ //#region src/components/table/utils.ts
2897
+ function resolveId(row, idKey, columns) {
2898
+ if (idKey && row[idKey] !== void 0) return String(row[idKey]);
2899
+ if (row["id"] !== void 0) return String(row["id"]);
2900
+ const firstKey = columns[0]?.key ?? Object.keys(row)[0];
2901
+ return String(row[firstKey] ?? "");
2902
+ }
2903
+ //#endregion
2705
2904
  //#region src/components/table/menu.tsx
2706
- const MenuContext = createContext({});
2905
+ const MenuCtx = createContext({});
2707
2906
  function Menu({ ref, children, anchor, onClose }) {
2907
+ useEffect(() => {
2908
+ function handleKey(e) {
2909
+ if (e.key === "Escape") onClose?.();
2910
+ }
2911
+ window.addEventListener("keydown", handleKey);
2912
+ return () => window.removeEventListener("keydown", handleKey);
2913
+ }, [onClose]);
2708
2914
  return createPortal(/* @__PURE__ */ jsx("div", {
2709
2915
  ref,
2916
+ role: "menu",
2917
+ "aria-orientation": "vertical",
2710
2918
  style: {
2711
2919
  top: anchor.top,
2712
2920
  right: anchor.right
2713
2921
  },
2714
- className: "fixed bg-login-500 border border-login-600 rounded-lg shadow-lg z-9999 w-44",
2715
- children: /* @__PURE__ */ jsx(MenuContext.Provider, {
2922
+ className: "fixed z-[9999] w-44 overflow-hidden rounded-lg border border-login-500/60 bg-login-600 shadow-xl shadow-black/40",
2923
+ children: /* @__PURE__ */ jsx(MenuCtx.Provider, {
2716
2924
  value: { onClose },
2717
2925
  children
2718
2926
  })
2719
2927
  }), document.body);
2720
2928
  }
2721
- function MenuButton({ icon, text, hotKey, onClick, className }) {
2722
- const { onClose } = useContext(MenuContext);
2929
+ function MenuButton({ icon, text, hotKey, onClick, className = "" }) {
2930
+ const { onClose } = useContext(MenuCtx);
2723
2931
  useEffect(() => {
2724
2932
  if (!hotKey) return;
2725
- function handleKeyDown(e) {
2933
+ function handleKey(e) {
2934
+ const tag = e.target.tagName;
2935
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
2726
2936
  if (e.key.toLowerCase() === hotKey.toLowerCase()) {
2727
2937
  e.preventDefault();
2728
2938
  onClick();
2729
2939
  onClose?.();
2730
2940
  }
2731
2941
  }
2732
- window.addEventListener("keydown", handleKeyDown);
2733
- return () => window.removeEventListener("keydown", handleKeyDown);
2942
+ window.addEventListener("keydown", handleKey);
2943
+ return () => window.removeEventListener("keydown", handleKey);
2734
2944
  }, [
2735
2945
  hotKey,
2736
2946
  onClick,
2737
2947
  onClose
2738
2948
  ]);
2739
2949
  return /* @__PURE__ */ jsxs("button", {
2950
+ role: "menuitem",
2740
2951
  onClick: () => {
2741
2952
  onClick();
2742
2953
  onClose?.();
2743
2954
  },
2744
- className: `flex items-center justify-between w-full px-3 py-2 text-sm hover:bg-login-600 cursor-pointer
2745
- ${className || ""}
2955
+ className: `
2956
+ flex w-full items-center justify-between px-3 py-2 text-sm
2957
+ text-login-75 transition-colors duration-100
2958
+ hover:bg-login-500 focus:bg-login-500 focus:outline-none
2959
+ first:rounded-t-lg last:rounded-b-lg
2960
+ ${className}
2746
2961
  `,
2747
- children: [/* @__PURE__ */ jsxs("div", {
2748
- className: "flex items-center",
2749
- children: [React.cloneElement(icon, { className: "w-4 h-4 mr-2" }), text]
2750
- }), /* @__PURE__ */ jsx("span", {
2751
- className: "text-xs opacity-50 font-mono",
2962
+ children: [/* @__PURE__ */ jsxs("span", {
2963
+ className: "flex items-center gap-2",
2964
+ children: [/* @__PURE__ */ jsx("span", {
2965
+ className: "h-4 w-4 shrink-0 flex items-center justify-center",
2966
+ children: icon
2967
+ }), text]
2968
+ }), hotKey && /* @__PURE__ */ jsx("kbd", {
2969
+ className: "font-mono text-xs opacity-40",
2752
2970
  children: hotKey
2753
2971
  })]
2754
2972
  });
2755
2973
  }
2756
2974
  //#endregion
2757
- //#region src/components/table/format.ts
2758
- const nullableTimeKeys = [
2759
- "date",
2760
- "last_sent",
2761
- "time"
2762
- ];
2763
- const ISODateTimeReg = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
2764
- const ISODateReg = /^\d{4}-\d{2}-\d{2}$/;
2765
- const ISOTimeReg = /^\d{2}:\d{2}(:\d{2})?$/;
2766
- const OsloTZ = { timeZone: "Europe/Oslo" };
2767
- const OsloTime = {
2768
- ...OsloTZ,
2769
- hour: "2-digit",
2770
- minute: "2-digit"
2771
- };
2772
- const OsloDateTime = {
2773
- ...OsloTime,
2774
- year: "numeric",
2775
- month: "2-digit",
2776
- day: "2-digit"
2777
- };
2778
- function formatValue(key, value) {
2779
- if (nullableTimeKeys.includes(key) && !value) return "Never";
2780
- if (typeof value === "string") {
2781
- if (ISODateTimeReg.test(value)) return new Date(value).toLocaleString("nb-NO", OsloDateTime);
2782
- if (ISODateReg.test(value)) return new Date(value).toLocaleDateString("nb-NO", OsloTZ);
2783
- if (ISOTimeReg.test(value)) return (/* @__PURE__ */ new Date(`1970-01-01T${value}`)).toLocaleTimeString("nb-NO", OsloTime);
2975
+ //#region src/components/table/body.tsx
2976
+ function useClickOutside(ref, cb, enabled) {
2977
+ useEffect(() => {
2978
+ if (!enabled) return;
2979
+ function handler(e) {
2980
+ if (ref.current && !ref.current.contains(e.target)) cb();
2981
+ }
2982
+ document.addEventListener("mousedown", handler);
2983
+ return () => document.removeEventListener("mousedown", handler);
2984
+ }, [
2985
+ ref,
2986
+ cb,
2987
+ enabled
2988
+ ]);
2989
+ }
2990
+ function resolveRedirectUrl(redirectPath, row, id) {
2991
+ if (!redirectPath) return null;
2992
+ const cfg = typeof redirectPath === "string" ? {
2993
+ path: redirectPath,
2994
+ key: void 0
2995
+ } : redirectPath;
2996
+ if (!cfg.path) return null;
2997
+ const rid = cfg.key ? String(row[cfg.key] ?? id) : id;
2998
+ return cfg.path.includes("?") ? `${cfg.path}${rid}` : `${cfg.path}/${rid}`;
2999
+ }
3000
+ function Cell({ col, row, density }) {
3001
+ const value = row[col.key];
3002
+ const shouldTruncate = col.truncate === true;
3003
+ const align = col.align ?? "left";
3004
+ const wrapperAlign = align === "right" ? "text-right" : align === "center" ? "text-center" : "";
3005
+ let content;
3006
+ if (col.render) content = col.render(value, row);
3007
+ else if (col.highlight) {
3008
+ const colorName = col.highlight[String(value)] ?? col.highlight["default"];
3009
+ const formatted = String(formatValue(col.key, value));
3010
+ if (colorName) content = /* @__PURE__ */ jsx("span", {
3011
+ className: `inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${HIGHLIGHT[colorName]}`,
3012
+ children: formatted
3013
+ });
3014
+ else content = /* @__PURE__ */ jsx("span", {
3015
+ className: shouldTruncate ? "block truncate" : "whitespace-nowrap",
3016
+ children: formatted
3017
+ });
3018
+ } else {
3019
+ const formatted = formatValue(col.key, value);
3020
+ content = /* @__PURE__ */ jsx("span", {
3021
+ className: shouldTruncate ? "block truncate" : "whitespace-nowrap",
3022
+ children: formatted === null || formatted === void 0 ? "-" : String(formatted)
3023
+ });
2784
3024
  }
2785
- if (key.includes("capacity")) return value === 0 ? "Unlimited" : value;
2786
- return value;
3025
+ return /* @__PURE__ */ jsx("td", {
3026
+ style: col.width ? {
3027
+ width: col.width,
3028
+ flexShrink: 0
3029
+ } : void 0,
3030
+ className: `
3031
+ ${col.width ? "" : "flex-1"} min-w-0 flex items-center text-sm text-login-75
3032
+ ${DENSITY_TD[density]}
3033
+ `,
3034
+ children: /* @__PURE__ */ jsx("div", {
3035
+ className: `min-w-0 w-full ${wrapperAlign}`,
3036
+ children: content
3037
+ })
3038
+ });
2787
3039
  }
2788
- //#endregion
2789
- //#region src/components/table/body.tsx
2790
- function Body({ list, columns, menuItems, redirectPath, variant = "default", idKey }) {
3040
+ function Body({ data, columns, idKey, variant, density, striped, redirectPath, onRowClick, renderExpandedRow, selectable, selectedIds, onSelectionChange, menuItems }) {
3041
+ const router = useRouter();
2791
3042
  const [openMenuId, setOpenMenuId] = useState(null);
2792
3043
  const [anchor, setAnchor] = useState(null);
2793
- const router = useRouter();
3044
+ const [expandedId, setExpandedId] = useState(null);
2794
3045
  const menuRef = useRef(null);
2795
3046
  const tbodyRef = useRef(null);
2796
3047
  const menuWasOpenOnMouseDown = useRef(false);
2797
- useClickOutside(menuRef, () => setOpenMenuId(null));
3048
+ const closeMenu = useCallback(() => setOpenMenuId(null), []);
3049
+ useClickOutside(menuRef, closeMenu, openMenuId !== null);
2798
3050
  useEffect(() => {
2799
3051
  const el = tbodyRef.current;
2800
3052
  if (!el) return;
2801
- const close = () => setOpenMenuId(null);
2802
- el.addEventListener("scroll", close);
2803
- return () => el.removeEventListener("scroll", close);
2804
- }, []);
3053
+ el.addEventListener("scroll", closeMenu);
3054
+ return () => el.removeEventListener("scroll", closeMenu);
3055
+ }, [closeMenu]);
3056
+ function openMenu(id, coords) {
3057
+ setAnchor(coords);
3058
+ setOpenMenuId(id);
3059
+ }
3060
+ const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
3061
+ function toggleSelect(id) {
3062
+ onSelectionChange(selectedSet.has(id) ? selectedIds.filter((s) => s !== id) : [...selectedIds, id]);
3063
+ }
3064
+ const hasMenu = Boolean(menuItems);
3065
+ const hasExpand = Boolean(renderExpandedRow);
2805
3066
  return /* @__PURE__ */ jsx("tbody", {
2806
3067
  ref: tbodyRef,
2807
- className: `
2808
- block overflow-y-auto flex-1 min-h-0 divide-y
2809
- ${variant === "default" ? "bg-login-800 divide-login-600/25" : "bg-transparent divide-login-600/20"}
2810
- `,
2811
- children: list.map((item, index) => {
2812
- const itemRecord = item;
2813
- let id = "";
2814
- if (idKey && itemRecord[idKey] !== void 0) id = String(itemRecord[idKey]);
2815
- else if (itemRecord["id"] !== void 0) id = String(itemRecord["id"]);
2816
- else {
2817
- const firstKey = columns[0]?.key || Object.keys(itemRecord)[0];
2818
- id = String(itemRecord[firstKey]);
2819
- }
2820
- const redirectConfig = typeof redirectPath === "object" && redirectPath ? redirectPath : { path: redirectPath };
2821
- const redirectId = redirectConfig.key ? String(itemRecord[redirectConfig.key]) : id;
2822
- const menuButtonColors = variant === "minimal" ? {
2823
- active: "bg-login-600 text-login-100",
2824
- inactive: "hover:bg-login-600 text-login-200 hover:text-login-100"
2825
- } : {
2826
- active: "bg-login-400 text-login-100",
2827
- inactive: "hover:bg-login-400 text-login-200 hover:text-login-100"
2828
- };
2829
- const buttonClass = openMenuId === id ? menuButtonColors.active : menuButtonColors.inactive;
2830
- return /* @__PURE__ */ jsxs("tr", {
2831
- className: `
2832
- flex w-full group/row transition-colors
2833
- ${redirectConfig.path ? "cursor-pointer" : ""}
2834
- ${variant === "default" && redirectConfig.path ? "hover:bg-login-600/80" : ""}
2835
- ${variant === "minimal" ? "hover:bg-login-600/70 border-b border-login-600/20 last:border-0" : ""}
2836
- `,
3068
+ className: `block overflow-y-auto flex-1 min-h-0 divide-y ${VARIANT_TBODY[variant]}`,
3069
+ children: data.map((row, rowIdx) => {
3070
+ const id = resolveId(row, idKey, columns);
3071
+ const url = resolveRedirectUrl(redirectPath, row, id);
3072
+ const isClickable = Boolean(url || onRowClick || hasExpand);
3073
+ const isMenuOpen = openMenuId === id;
3074
+ const isExpanded = expandedId === id;
3075
+ const isSelected = selectable && selectedSet.has(id);
3076
+ const expandedContent = renderExpandedRow?.(row);
3077
+ const rowClass = [
3078
+ "flex w-full transition-colors duration-100",
3079
+ isClickable ? "cursor-pointer" : "",
3080
+ VARIANT_ROW_HOVER[variant],
3081
+ striped ? VARIANT_ROW_STRIPED[variant] : "",
3082
+ isSelected ? "bg-login/5" : ""
3083
+ ].filter(Boolean).join(" ");
3084
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsxs("tr", {
3085
+ className: rowClass,
3086
+ onMouseEnter: () => {
3087
+ if (url) router.prefetch(url);
3088
+ },
2837
3089
  onMouseDown: () => {
2838
3090
  menuWasOpenOnMouseDown.current = openMenuId !== null;
2839
3091
  },
@@ -2842,297 +3094,531 @@ function Body({ list, columns, menuItems, redirectPath, variant = "default", idK
2842
3094
  menuWasOpenOnMouseDown.current = false;
2843
3095
  return;
2844
3096
  }
2845
- if (redirectConfig.path) if (redirectConfig.path.includes("?")) router.push(`${redirectConfig.path}${redirectId}`);
2846
- else router.push(`${redirectConfig.path}/${redirectId}`);
3097
+ if (hasExpand) {
3098
+ setExpandedId(isExpanded ? null : id);
3099
+ return;
3100
+ }
3101
+ if (onRowClick) {
3102
+ onRowClick(row, id);
3103
+ return;
3104
+ }
3105
+ if (url) router.push(url);
2847
3106
  },
2848
3107
  onContextMenu: (e) => {
3108
+ if (!hasMenu) return;
2849
3109
  e.preventDefault();
2850
- setAnchor({
3110
+ openMenu(id, {
2851
3111
  top: e.clientY,
2852
3112
  right: window.innerWidth - e.clientX
2853
3113
  });
2854
- setOpenMenuId(id);
2855
3114
  },
2856
- children: [columns.map((col) => {
2857
- const value = itemRecord[col.key];
2858
- let badgeClass = "";
2859
- if (col.highlight) {
2860
- const highlightKey = String(value);
2861
- const colorName = col.highlight[highlightKey] || col.highlight.default;
2862
- if (colorName) {
2863
- switch (colorName) {
2864
- case "green":
2865
- badgeClass = "bg-green-500/20 text-green-400";
2866
- break;
2867
- case "yellow":
2868
- badgeClass = "bg-yellow-500/20 text-yellow-400";
2869
- break;
2870
- case "red":
2871
- badgeClass = "bg-red-500/20 text-red-400";
2872
- break;
2873
- case "blue":
2874
- badgeClass = "bg-blue-500/20 text-blue-400";
2875
- break;
2876
- case "gray":
2877
- badgeClass = "bg-gray-500/20 text-gray-400";
2878
- break;
2879
- }
2880
- badgeClass += " px-2 py-1 rounded";
2881
- }
2882
- }
2883
- return /* @__PURE__ */ jsx("td", {
2884
- className: `
2885
- flex-1 min-w-0 px-6 py-4 whitespace-nowrap text-sm flex items-center text-login-100
2886
- ${variant === "minimal" ? "px-4! py-2!" : ""}
2887
- `,
2888
- children: /* @__PURE__ */ jsx("div", {
2889
- className: "relative w-full min-w-0",
2890
- children: /* @__PURE__ */ jsx("span", {
2891
- className: `block max-w-full truncate ${badgeClass}`,
2892
- children: formatValue(col.key, value)
2893
- })
2894
- })
2895
- }, col.key);
2896
- }), menuItems && /* @__PURE__ */ jsx("td", {
2897
- className: "shrink-0 w-16 flex flex-row items-center justify-end p-2 px-4\n whitespace-nowrap text-right text-sm font-medium",
2898
- children: /* @__PURE__ */ jsxs("div", {
2899
- className: "relative",
2900
- children: [/* @__PURE__ */ jsx("button", {
3115
+ children: [
3116
+ selectable && /* @__PURE__ */ jsx("td", {
3117
+ className: "flex items-center justify-center",
3118
+ style: {
3119
+ width: "3rem",
3120
+ minWidth: "3rem",
3121
+ flexShrink: 0
3122
+ },
3123
+ children: /* @__PURE__ */ jsx("button", {
2901
3124
  type: "button",
2902
- className: `p-1.5 rounded flex items-center justify-center transition-colors ${buttonClass}`,
2903
- onMouseDown: (e) => e.nativeEvent.stopImmediatePropagation(),
3125
+ "aria-label": isSelected ? "Deselect row" : "Select row",
3126
+ "aria-checked": isSelected,
3127
+ role: "checkbox",
2904
3128
  onClick: (e) => {
2905
3129
  e.stopPropagation();
2906
- const rect = e.currentTarget.getBoundingClientRect();
2907
- const coords = {
2908
- top: rect.bottom + 4,
2909
- right: window.innerWidth - rect.right
2910
- };
2911
- setAnchor(openMenuId === id ? null : coords);
2912
- setOpenMenuId(openMenuId === id ? null : id);
3130
+ toggleSelect(id);
2913
3131
  },
2914
- children: /* @__PURE__ */ jsx("span", {
2915
- className: "text-xl leading-none select-none",
2916
- children: /* @__PURE__ */ jsx(EllipsisVertical, { className: "h-5 w-5" })
3132
+ className: `
3133
+ h-4 w-4 rounded border flex items-center justify-center transition-colors cursor-pointer
3134
+ ${isSelected ? "bg-login border-login text-white" : "border-login-400 bg-transparent hover:border-login-100"}
3135
+ `,
3136
+ children: isSelected && /* @__PURE__ */ jsx("svg", {
3137
+ className: "h-3 w-3",
3138
+ viewBox: "0 0 12 12",
3139
+ fill: "none",
3140
+ children: /* @__PURE__ */ jsx("path", {
3141
+ d: "M2 6l3 3 5-5",
3142
+ stroke: "currentColor",
3143
+ strokeWidth: "1.5",
3144
+ strokeLinecap: "round",
3145
+ strokeLinejoin: "round"
3146
+ })
2917
3147
  })
2918
- }), openMenuId === id && anchor && /* @__PURE__ */ jsx(Menu, {
2919
- ref: menuRef,
2920
- anchor,
2921
- onClose: () => setOpenMenuId(null),
2922
- children: menuItems?.(item, id)
2923
- })]
3148
+ })
3149
+ }),
3150
+ columns.map((col) => /* @__PURE__ */ jsx(Cell, {
3151
+ col,
3152
+ row,
3153
+ density
3154
+ }, col.key)),
3155
+ hasExpand && /* @__PURE__ */ jsx("td", {
3156
+ className: "flex items-center justify-center",
3157
+ style: {
3158
+ width: "2.5rem",
3159
+ minWidth: "2.5rem",
3160
+ flexShrink: 0
3161
+ },
3162
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: `
3163
+ h-4 w-4 transition-transform duration-200
3164
+ ${isExpanded ? "rotate-180 text-login" : "text-login-400"}
3165
+ ` })
3166
+ }),
3167
+ hasMenu && /* @__PURE__ */ jsx("td", {
3168
+ className: "flex items-center justify-end pr-3",
3169
+ style: {
3170
+ width: "3.5rem",
3171
+ minWidth: "3.5rem",
3172
+ flexShrink: 0
3173
+ },
3174
+ children: /* @__PURE__ */ jsxs("div", {
3175
+ className: "relative",
3176
+ children: [/* @__PURE__ */ jsx("button", {
3177
+ type: "button",
3178
+ "aria-label": "Row actions",
3179
+ "aria-expanded": isMenuOpen,
3180
+ "aria-haspopup": "menu",
3181
+ onMouseDown: (e) => e.nativeEvent.stopImmediatePropagation(),
3182
+ onClick: (e) => {
3183
+ e.stopPropagation();
3184
+ if (isMenuOpen) setOpenMenuId(null);
3185
+ else {
3186
+ const rect = e.currentTarget.getBoundingClientRect();
3187
+ openMenu(id, {
3188
+ top: rect.bottom + 4,
3189
+ right: window.innerWidth - rect.right
3190
+ });
3191
+ }
3192
+ },
3193
+ className: `
3194
+ p-1.5 rounded flex items-center justify-center transition-colors
3195
+ ${isMenuOpen ? "bg-login-500 text-login-75" : "text-login-300 hover:bg-login-500/60 hover:text-login-75"}
3196
+ `,
3197
+ children: /* @__PURE__ */ jsx(EllipsisVertical, { className: "h-4 w-4" })
3198
+ }), isMenuOpen && anchor && /* @__PURE__ */ jsx(Menu, {
3199
+ ref: menuRef,
3200
+ anchor,
3201
+ onClose: closeMenu,
3202
+ children: menuItems(row, id)
3203
+ })]
3204
+ })
2924
3205
  })
2925
- })]
2926
- }, index);
3206
+ ]
3207
+ }), hasExpand && isExpanded && /* @__PURE__ */ jsx("tr", {
3208
+ className: "flex w-full bg-login-700/25 border-b border-login-600/20",
3209
+ children: /* @__PURE__ */ jsx("td", {
3210
+ className: "w-full px-6 py-4",
3211
+ onClick: (e) => e.stopPropagation(),
3212
+ children: expandedContent
3213
+ })
3214
+ })] }, id + rowIdx);
2927
3215
  })
2928
3216
  });
2929
3217
  }
2930
3218
  //#endregion
2931
- //#region src/components/table/header.tsx
2932
- function parseOrder(value) {
2933
- return value === "asc" || value === "desc" ? value : void 0;
2934
- }
2935
- function Header({ columns, hideMenu, variant = "default" }) {
2936
- const router = useRouter();
2937
- const pathname = usePathname();
2938
- const searchParams = useSearchParams();
2939
- const [column, setColumn] = useState(searchParams.get("column") ?? "");
2940
- const [order, setOrder] = useState(parseOrder(searchParams.get("order")));
2941
- useEffect(() => {
2942
- if (!column || !order) return;
2943
- const params = new URLSearchParams(searchParams.toString());
2944
- if (searchParams.get("order") !== order || searchParams.get("column") !== column) {
2945
- params.set("order", order);
2946
- params.set("column", column);
2947
- params.set("page", "1");
2948
- router.push(`${pathname}?${params.toString()}`);
2949
- }
2950
- }, [
2951
- order,
2952
- column,
2953
- pathname,
2954
- router,
2955
- searchParams
2956
- ]);
2957
- function handleChange(key) {
2958
- setColumn(key);
2959
- setOrder((prev) => key === column && prev === "asc" ? "desc" : "asc");
2960
- }
2961
- return /* @__PURE__ */ jsx("thead", {
2962
- className: `
2963
- block w-full
2964
- ${variant === "default" ? "bg-login-700" : "bg-transparent border-b border-login-600"}
2965
- `,
2966
- children: /* @__PURE__ */ jsxs("tr", {
3219
+ //#region src/components/table/skeleton.tsx
3220
+ function Skeleton({ columns, rows, variant, density, hasMenu, hasSelect }) {
3221
+ return /* @__PURE__ */ jsx("tbody", {
3222
+ className: `block divide-y divide-login-600/15 ${VARIANT_TBODY[variant]}`,
3223
+ children: Array.from({ length: rows }).map((_, rowIdx) => /* @__PURE__ */ jsxs("tr", {
2967
3224
  className: "flex w-full",
2968
- children: [columns.map((col) => {
2969
- const key = col.key;
2970
- const value = col.label || (key.length < 3 ? key.toUpperCase() : `${key[0].toUpperCase()}${key.slice(1).replaceAll("_", " ")}`);
2971
- return /* @__PURE__ */ jsx("th", {
3225
+ children: [
3226
+ hasSelect && /* @__PURE__ */ jsx("td", {
3227
+ className: "flex items-center justify-center",
3228
+ style: {
3229
+ width: "3rem",
3230
+ minWidth: "3rem",
3231
+ flexShrink: 0
3232
+ },
3233
+ children: /* @__PURE__ */ jsx("span", { className: "block h-4 w-4 rounded animate-shimmer" })
3234
+ }),
3235
+ columns.map((col, colIdx) => /* @__PURE__ */ jsx("td", {
3236
+ style: col.width ? {
3237
+ width: col.width,
3238
+ flexShrink: 0
3239
+ } : void 0,
2972
3240
  className: `
2973
- flex-1 min-w-0 px-6 py-3 text-xs font-medium uppercase tracking-wider text-left
2974
- ${variant === "default" ? "text-login-200" : "text-login-100"}
2975
- ${variant === "minimal" ? "px-4!" : ""}
3241
+ flex-1 min-w-0 flex items-center
3242
+ ${DENSITY_TD[density]}
2976
3243
  `,
2977
- children: /* @__PURE__ */ jsxs("button", {
2978
- className: "flex w-full min-w-0 flex-row items-center gap-2 group uppercase whitespace-nowrap",
2979
- onClick: () => handleChange(key),
2980
- children: [/* @__PURE__ */ jsx("span", {
2981
- className: "min-w-0 truncate",
2982
- children: value
2983
- }), /* @__PURE__ */ jsx("span", {
2984
- className: "flex flex-col",
2985
- children: column === key ? order === "asc" ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(ChevronUp, { className: "h-4 w-4 stroke-login-200 opacity-0 group-hover:opacity-100" })
2986
- })]
3244
+ children: /* @__PURE__ */ jsx("span", {
3245
+ className: "block h-4 rounded animate-shimmer",
3246
+ style: {
3247
+ width: colIdx === 0 ? `${55 + (rowIdx * 17 + colIdx * 31) % 30}%` : `${40 + (rowIdx * 13 + colIdx * 23) % 40}%`,
3248
+ animationDelay: `${(rowIdx * columns.length + colIdx) * 40}ms`
3249
+ }
2987
3250
  })
2988
- }, key);
2989
- }), !hideMenu && /* @__PURE__ */ jsx("th", { className: "shrink-0 w-16 px-6 py-3" })]
2990
- })
3251
+ }, col.key)),
3252
+ hasMenu && /* @__PURE__ */ jsx("td", {
3253
+ className: "flex items-center justify-end pr-3",
3254
+ style: {
3255
+ width: "3.5rem",
3256
+ minWidth: "3.5rem",
3257
+ flexShrink: 0
3258
+ },
3259
+ children: /* @__PURE__ */ jsx("span", { className: "block h-5 w-5 rounded animate-shimmer" })
3260
+ })
3261
+ ]
3262
+ }, rowIdx))
2991
3263
  });
2992
3264
  }
2993
3265
  //#endregion
2994
- //#region src/components/table/table.tsx
2995
- function Table({ data, columns, menuItems, redirectPath, variant = "default", idKey }) {
2996
- if (data.length === 0) return /* @__PURE__ */ jsx("div", {
2997
- className: "p-4 text-center text-login-200",
2998
- children: "No data found"
3266
+ //#region src/components/table/empty.tsx
3267
+ function Empty({ emptyState }) {
3268
+ if (emptyState) return /* @__PURE__ */ jsx("tbody", {
3269
+ className: "block w-full",
3270
+ children: /* @__PURE__ */ jsx("tr", {
3271
+ className: "flex w-full",
3272
+ children: /* @__PURE__ */ jsx("td", {
3273
+ className: "w-full",
3274
+ children: /* @__PURE__ */ jsx("div", {
3275
+ className: "flex items-center justify-center min-h-[160px] py-8 px-6",
3276
+ children: emptyState
3277
+ })
3278
+ })
3279
+ })
2999
3280
  });
3000
- return /* @__PURE__ */ jsx("div", {
3001
- className: `
3002
- flex-1 flex flex-col min-h-0 overflow-x-auto overflow-y-hidden h-full w-full
3003
- ${variant === "default" ? "bg-login-800 rounded-lg border border-login-600/30" : ""}
3004
- ${variant === "minimal" ? "bg-transparent" : ""}
3005
- `,
3006
- children: /* @__PURE__ */ jsxs("table", {
3007
- className: "min-w-full w-max divide-y divide-login-600/30 flex flex-col flex-1 min-h-0",
3008
- children: [/* @__PURE__ */ jsx(Header, {
3009
- columns,
3010
- hideMenu: !menuItems,
3011
- variant
3012
- }), /* @__PURE__ */ jsx(Body, {
3013
- list: data,
3014
- columns,
3015
- menuItems,
3016
- redirectPath,
3017
- variant,
3018
- idKey
3019
- })]
3281
+ return /* @__PURE__ */ jsx("tbody", {
3282
+ className: "block w-full",
3283
+ children: /* @__PURE__ */ jsx("tr", {
3284
+ className: "flex w-full",
3285
+ children: /* @__PURE__ */ jsx("td", {
3286
+ className: "w-full",
3287
+ children: /* @__PURE__ */ jsxs("div", {
3288
+ className: "flex flex-col items-center justify-center gap-3 min-h-[160px] py-8 text-login-300",
3289
+ children: [/* @__PURE__ */ jsx(TableIcon, { className: "h-10 w-10 opacity-30" }), /* @__PURE__ */ jsx("span", {
3290
+ className: "text-sm",
3291
+ children: "No data found"
3292
+ })]
3293
+ })
3294
+ })
3020
3295
  })
3021
3296
  });
3022
3297
  }
3023
3298
  //#endregion
3024
3299
  //#region src/components/table/pagination.tsx
3025
- function Pagination({ pageSize = 10, totalRows }) {
3026
- const router = useRouter();
3027
- const pathname = usePathname();
3028
- const searchParams = useSearchParams();
3029
- const rawPage = parseInt(searchParams.get("page") || "1", 10);
3030
- const [current, setCurrent] = useState(Math.max(1, Number.isNaN(rawPage) ? 1 : rawPage));
3031
- const totalPages = Math.max(1, Math.ceil(totalRows !== void 0 && pageSize > 0 ? totalRows / pageSize : 1));
3032
- useEffect(() => {
3033
- const raw = parseInt(searchParams.get("page") || "1", 10);
3034
- setCurrent(Math.max(1, Math.min(totalPages, Math.max(1, Number.isNaN(raw) ? 1 : raw))));
3035
- }, [searchParams, totalPages]);
3036
- function updateQuery(p) {
3037
- const params = new URLSearchParams(searchParams.toString());
3038
- params.set("page", String(p));
3039
- router.push(`${pathname}?${params.toString()}`);
3040
- }
3041
- function goPrevious() {
3042
- if (current <= 1) return;
3043
- const next = current - 1;
3044
- setCurrent(next);
3045
- updateQuery(next);
3300
+ function computeTotalPages(totalRows, pageSize) {
3301
+ return Math.max(1, pageSize > 0 ? Math.ceil(totalRows / pageSize) : 1);
3302
+ }
3303
+ function pageRange(current, total) {
3304
+ const delta = 2;
3305
+ const left = Math.max(1, current - delta);
3306
+ const right = Math.min(total, current + delta);
3307
+ const pages = [];
3308
+ if (left > 1) {
3309
+ pages.push(1);
3310
+ if (left > 2) pages.push("…");
3046
3311
  }
3047
- function goNext() {
3048
- if (current >= totalPages) return;
3049
- const next = current + 1;
3050
- setCurrent(next);
3051
- updateQuery(next);
3312
+ for (let i = left; i <= right; i++) pages.push(i);
3313
+ if (right < total) {
3314
+ if (right < total - 1) pages.push("…");
3315
+ pages.push(total);
3052
3316
  }
3053
- function setPage(p) {
3054
- if (p === current) return;
3055
- setCurrent(p);
3056
- updateQuery(p);
3057
- }
3058
- function getPages(curr, total) {
3059
- const delta = 2;
3060
- const left = Math.max(1, curr - delta);
3061
- const right = Math.min(total, curr + delta);
3062
- const pages = [];
3063
- if (left > 1) {
3064
- pages.push(1);
3065
- if (left > 2) pages.push("...");
3066
- }
3067
- for (let i = left; i <= right; i++) pages.push(i);
3068
- if (right < total) {
3069
- if (right < total - 1) pages.push("...");
3070
- pages.push(total);
3071
- }
3072
- return pages;
3073
- }
3074
- const pages = getPages(current, totalPages);
3075
- const start = Math.max(1, (current - 1) * pageSize + 1);
3076
- const end = Math.min(current * pageSize, totalRows !== void 0 ? totalRows : current * pageSize);
3317
+ return pages;
3318
+ }
3319
+ function PaginationShell({ page, totalPages, totalRows, pageSize, onPageChange, variant }) {
3320
+ const start = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
3321
+ const end = Math.min(page * pageSize, totalRows);
3322
+ const pages = pageRange(page, totalPages);
3323
+ const btnBase = `
3324
+ flex items-center justify-center rounded-md border text-sm transition-colors duration-100
3325
+ disabled:opacity-35 disabled:cursor-not-allowed
3326
+ `;
3327
+ const btnStyle = variant === "original" ? "border-login-500/50 bg-login-700 hover:bg-login-600 text-login-100" : "border-login-500/40 bg-transparent hover:bg-login-700/60 text-login-100";
3328
+ const pageActive = variant === "original" ? "border-login bg-login/10 text-login" : "border-login text-login bg-transparent";
3329
+ const pageInactive = variant === "original" ? "border-login-500/50 bg-login-700 text-login-100 hover:bg-login-600" : "border-transparent bg-transparent text-login-200 hover:text-login-75 hover:bg-login-700/60";
3077
3330
  return /* @__PURE__ */ jsxs("div", {
3078
- className: "flex items-center justify-between w-full pt-4",
3079
- children: [/* @__PURE__ */ jsx("div", {
3080
- className: "text-sm /70",
3081
- children: totalRows !== void 0 ? totalRows === 0 ? /* @__PURE__ */ jsx("span", { children: "Showing 0 results" }) : /* @__PURE__ */ jsxs("span", { children: [
3082
- "Showing ",
3083
- start,
3084
- " to ",
3085
- end,
3086
- " of ",
3087
- totalRows,
3088
- " results"
3089
- ] }) : null
3090
- }), /* @__PURE__ */ jsxs("div", {
3091
- className: "flex items-center gap-3",
3331
+ className: "flex items-center justify-between w-full gap-4 flex-wrap",
3332
+ children: [/* @__PURE__ */ jsx("span", {
3333
+ className: "text-xs text-login-300 tabular-nums",
3334
+ children: totalRows === 0 ? "No results" : `${start}-${end} of ${totalRows}`
3335
+ }), /* @__PURE__ */ jsxs("nav", {
3336
+ className: "flex items-center gap-1.5",
3337
+ "aria-label": "Pagination",
3092
3338
  children: [
3093
3339
  /* @__PURE__ */ jsx("button", {
3094
3340
  type: "button",
3095
- onClick: goPrevious,
3096
- disabled: current <= 1,
3097
- className: `
3098
- flex items-center gap-2 p-1 rounded-lg
3099
- bg-login-700 hover:bg-login-600 disabled:opacity-40
3100
- border border-login-500/50 text-sm transition-colors duration-150
3101
- `,
3102
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" })
3103
- }),
3104
- /* @__PURE__ */ jsx("nav", {
3105
- className: "flex items-center gap-1",
3106
- "aria-label": "Pagination",
3107
- children: pages.map((p, i) => typeof p === "string" ? /* @__PURE__ */ jsx("span", {
3108
- className: "px-3 py-1 text-sm",
3109
- children: p
3110
- }, `e-${i}`) : /* @__PURE__ */ jsx("button", {
3111
- type: "button",
3112
- onClick: () => setPage(p),
3113
- "aria-current": p === current ? "page" : void 0,
3114
- className: `
3115
- px-3 py-1 rounded-lg text-sm border transition-colors duration-150
3116
- ${p === current ? "bg-login text-white border-login" : "bg-login-700 border-login-500/50 hover:bg-login-600"}
3117
- `,
3118
- children: p
3119
- }, p))
3341
+ "aria-label": "Previous page",
3342
+ disabled: page <= 1,
3343
+ onClick: () => onPageChange(page - 1),
3344
+ className: `${btnBase} ${btnStyle} h-8 w-8`,
3345
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" })
3120
3346
  }),
3347
+ pages.map((p, i) => p === "…" ? /* @__PURE__ */ jsx("span", {
3348
+ className: "w-8 text-center text-sm text-login-400 select-none",
3349
+ children: "…"
3350
+ }, `ellipsis-${i}`) : /* @__PURE__ */ jsx("button", {
3351
+ type: "button",
3352
+ "aria-current": p === page ? "page" : void 0,
3353
+ onClick: () => onPageChange(p),
3354
+ className: `${btnBase} h-8 min-w-8 px-2 tabular-nums ${p === page ? pageActive : pageInactive}`,
3355
+ children: p
3356
+ }, p)),
3121
3357
  /* @__PURE__ */ jsx("button", {
3122
3358
  type: "button",
3123
- onClick: goNext,
3124
- disabled: current >= totalPages,
3125
- className: `
3126
- flex items-center gap-2 p-1 rounded-lg select-none
3127
- bg-login-700 hover:bg-login-600 disabled:opacity-40
3128
- border border-login-500/50 text-sm transition-colors duration-150
3129
- `,
3130
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-5 w-5" })
3359
+ "aria-label": "Next page",
3360
+ disabled: page >= totalPages,
3361
+ onClick: () => onPageChange(page + 1),
3362
+ className: `${btnBase} ${btnStyle} h-8 w-8`,
3363
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" })
3131
3364
  })
3132
3365
  ]
3133
3366
  })]
3134
3367
  });
3135
3368
  }
3369
+ function PaginationLocalState({ totalRows, pageSize, variant = "original" }) {
3370
+ const [page, setPage] = useState(1);
3371
+ return /* @__PURE__ */ jsx(PaginationShell, {
3372
+ page,
3373
+ totalPages: computeTotalPages(totalRows, pageSize),
3374
+ totalRows,
3375
+ pageSize,
3376
+ onPageChange: setPage,
3377
+ variant
3378
+ });
3379
+ }
3380
+ function PaginationURLState({ totalRows, pageSize, variant = "original" }) {
3381
+ const router = useRouter();
3382
+ const pathname = usePathname();
3383
+ const searchParams = useSearchParams();
3384
+ const page = Math.max(1, parseInt(searchParams.get("page") ?? "1", 10) || 1);
3385
+ function onPageChange(p) {
3386
+ const params = new URLSearchParams(searchParams.toString());
3387
+ params.set("page", String(p));
3388
+ router.replace(`${pathname}?${params.toString()}`);
3389
+ }
3390
+ return /* @__PURE__ */ jsx(PaginationShell, {
3391
+ page,
3392
+ totalPages: computeTotalPages(totalRows, pageSize),
3393
+ totalRows,
3394
+ pageSize,
3395
+ onPageChange,
3396
+ variant
3397
+ });
3398
+ }
3399
+ function Pagination(props) {
3400
+ const { totalRows, pageSize, variant = "original", urlState, page, onPageChange } = props;
3401
+ if (urlState) return /* @__PURE__ */ jsx(Suspense, {
3402
+ fallback: /* @__PURE__ */ jsx(PaginationShell, {
3403
+ page: 1,
3404
+ totalPages: computeTotalPages(totalRows, pageSize),
3405
+ totalRows,
3406
+ pageSize,
3407
+ onPageChange: () => {},
3408
+ variant
3409
+ }),
3410
+ children: /* @__PURE__ */ jsx(PaginationURLState, { ...props })
3411
+ });
3412
+ if (page !== void 0 && onPageChange !== void 0) return /* @__PURE__ */ jsx(PaginationShell, {
3413
+ page,
3414
+ totalPages: computeTotalPages(totalRows, pageSize),
3415
+ totalRows,
3416
+ pageSize,
3417
+ onPageChange,
3418
+ variant
3419
+ });
3420
+ return /* @__PURE__ */ jsx(PaginationLocalState, { ...props });
3421
+ }
3422
+ //#endregion
3423
+ //#region src/components/table/table.tsx
3424
+ function TableShell({ data, columns, idKey, variant, density, striped, loading, loadingRows, emptyState, redirectPath, onRowClick, renderExpandedRow, selectable, selectedIds, onSelectionChange, sort, onSort, page, onPageChange, totalRows, pageSize, hidePagination, menuItems, className }) {
3425
+ const allIds = data.map((row) => resolveId(row, idKey, columns));
3426
+ const allIdsSet = useMemo(() => new Set(allIds), [allIds]);
3427
+ const allSelected = allIds.length > 0 && allIds.every((id) => selectedIds.includes(id));
3428
+ const someSelected = !allSelected && allIds.some((id) => selectedIds.includes(id));
3429
+ function handleSelectAll() {
3430
+ if (allSelected) onSelectionChange(selectedIds.filter((id) => !allIdsSet.has(id)));
3431
+ else onSelectionChange([.../* @__PURE__ */ new Set([...selectedIds, ...allIds])]);
3432
+ }
3433
+ const isEmpty = !loading && data.length === 0;
3434
+ const showPagination = !hidePagination && pageSize > 0 && totalRows > 0;
3435
+ const hasMenu = Boolean(menuItems);
3436
+ const hasSelect = Boolean(selectable);
3437
+ const hasExpand = Boolean(renderExpandedRow);
3438
+ const paginationPad = variant === "original" ? "px-4 pb-4 pt-3" : "pt-4";
3439
+ return /* @__PURE__ */ jsxs("div", {
3440
+ className: [
3441
+ "flex flex-col min-h-0 w-full",
3442
+ VARIANT_CONTAINER[variant],
3443
+ className
3444
+ ].filter(Boolean).join(" "),
3445
+ children: [/* @__PURE__ */ jsx("div", {
3446
+ className: "flex-1 min-h-0 overflow-x-auto",
3447
+ children: /* @__PURE__ */ jsxs("table", {
3448
+ className: "min-w-full w-max flex flex-col flex-1 min-h-0 divide-y divide-login-600/20",
3449
+ children: [/* @__PURE__ */ jsx(Header, {
3450
+ columns,
3451
+ sort,
3452
+ onSort,
3453
+ hasMenu,
3454
+ hasSelect,
3455
+ hasExpand,
3456
+ allSelected,
3457
+ someSelected,
3458
+ onSelectAll: handleSelectAll,
3459
+ variant,
3460
+ density
3461
+ }), loading ? /* @__PURE__ */ jsx(Skeleton, {
3462
+ columns,
3463
+ rows: loadingRows,
3464
+ variant,
3465
+ density,
3466
+ hasMenu,
3467
+ hasSelect
3468
+ }) : isEmpty ? /* @__PURE__ */ jsx(Empty, { emptyState }) : /* @__PURE__ */ jsx(Body, {
3469
+ data,
3470
+ columns,
3471
+ idKey,
3472
+ variant,
3473
+ density,
3474
+ striped,
3475
+ redirectPath,
3476
+ onRowClick,
3477
+ renderExpandedRow,
3478
+ selectable,
3479
+ selectedIds,
3480
+ onSelectionChange,
3481
+ menuItems
3482
+ })]
3483
+ })
3484
+ }), showPagination && /* @__PURE__ */ jsx("div", {
3485
+ className: paginationPad,
3486
+ children: /* @__PURE__ */ jsx(Pagination, {
3487
+ page,
3488
+ totalRows,
3489
+ pageSize,
3490
+ onPageChange,
3491
+ variant
3492
+ })
3493
+ })]
3494
+ });
3495
+ }
3496
+ function useInternalSelection(props) {
3497
+ const [internal, setInternal] = useState([]);
3498
+ return {
3499
+ selectedIds: props.selectedIds ?? internal,
3500
+ onSelectionChange: props.onSelectionChange ?? setInternal
3501
+ };
3502
+ }
3503
+ function applyDefaults(props) {
3504
+ return {
3505
+ variant: props.variant ?? "original",
3506
+ density: props.density ?? "comfortable",
3507
+ selectable: props.selectable ?? false,
3508
+ loading: props.loading ?? false,
3509
+ loadingRows: props.loadingRows ?? props.pageSize ?? 5,
3510
+ striped: props.striped ?? false,
3511
+ hidePagination: props.hidePagination ?? false,
3512
+ pageSize: props.pageSize ?? 0
3513
+ };
3514
+ }
3515
+ function TableLocalState(props) {
3516
+ const defaults = applyDefaults(props);
3517
+ const selection = useInternalSelection(props);
3518
+ const [sort, setSort] = useState(void 0);
3519
+ const [page, setPage] = useState(1);
3520
+ const ps = defaults.pageSize;
3521
+ const allRows = props.data.length;
3522
+ const sorted = useMemo(() => {
3523
+ if (!sort) return props.data;
3524
+ return [...props.data].sort((a, b) => {
3525
+ const av = a[sort.column];
3526
+ const bv = b[sort.column];
3527
+ const cmp = String(av ?? "").localeCompare(String(bv ?? ""), void 0, {
3528
+ numeric: true,
3529
+ sensitivity: "base"
3530
+ });
3531
+ return sort.order === "asc" ? cmp : -cmp;
3532
+ });
3533
+ }, [props.data, sort]);
3534
+ const displayData = ps > 0 ? sorted.slice((page - 1) * ps, page * ps) : sorted;
3535
+ function handleSort(newSort) {
3536
+ setSort(newSort);
3537
+ setPage(1);
3538
+ }
3539
+ return /* @__PURE__ */ jsx(TableShell, {
3540
+ ...props,
3541
+ ...defaults,
3542
+ ...selection,
3543
+ data: displayData,
3544
+ sort,
3545
+ onSort: handleSort,
3546
+ page,
3547
+ onPageChange: setPage,
3548
+ totalRows: ps > 0 ? allRows : props.totalRows ?? allRows
3549
+ });
3550
+ }
3551
+ function TableURLState(props) {
3552
+ const defaults = applyDefaults(props);
3553
+ const selection = useInternalSelection(props);
3554
+ const router = useRouter();
3555
+ const pathname = usePathname();
3556
+ const searchParams = useSearchParams();
3557
+ const urlColumn = searchParams.get("column") ?? "";
3558
+ const urlOrder = searchParams.get("order") ?? "asc";
3559
+ const urlPage = Math.max(1, parseInt(searchParams.get("page") ?? "1", 10) || 1);
3560
+ const sort = urlColumn ? {
3561
+ column: urlColumn,
3562
+ order: urlOrder
3563
+ } : void 0;
3564
+ function handleSort(newSort) {
3565
+ const p = new URLSearchParams(searchParams.toString());
3566
+ p.set("column", newSort.column);
3567
+ p.set("order", newSort.order);
3568
+ p.set("page", "1");
3569
+ router.replace(`${pathname}?${p.toString()}`);
3570
+ }
3571
+ function handlePageChange(newPage) {
3572
+ const p = new URLSearchParams(searchParams.toString());
3573
+ p.set("page", String(newPage));
3574
+ router.replace(`${pathname}?${p.toString()}`);
3575
+ }
3576
+ return /* @__PURE__ */ jsx(TableShell, {
3577
+ ...props,
3578
+ ...defaults,
3579
+ ...selection,
3580
+ sort,
3581
+ onSort: handleSort,
3582
+ page: urlPage,
3583
+ onPageChange: handlePageChange,
3584
+ totalRows: props.totalRows ?? props.data.length
3585
+ });
3586
+ }
3587
+ function TableControlled(props) {
3588
+ const defaults = applyDefaults(props);
3589
+ const selection = useInternalSelection(props);
3590
+ return /* @__PURE__ */ jsx(TableShell, {
3591
+ ...props,
3592
+ ...defaults,
3593
+ ...selection,
3594
+ sort: props.sort,
3595
+ onSort: props.onSort,
3596
+ page: props.page ?? 1,
3597
+ onPageChange: props.onPageChange ?? (() => {}),
3598
+ totalRows: props.totalRows ?? props.data.length
3599
+ });
3600
+ }
3601
+ function Table(props) {
3602
+ if (props.sort !== void 0 || props.onSort !== void 0) return /* @__PURE__ */ jsx(TableControlled, { ...props });
3603
+ if (props.urlState) {
3604
+ const defaults = applyDefaults(props);
3605
+ return /* @__PURE__ */ jsx(Suspense, {
3606
+ fallback: /* @__PURE__ */ jsx(TableShell, {
3607
+ ...props,
3608
+ ...defaults,
3609
+ selectedIds: props.selectedIds ?? [],
3610
+ onSelectionChange: props.onSelectionChange ?? (() => {}),
3611
+ sort: void 0,
3612
+ onSort: () => {},
3613
+ page: 1,
3614
+ onPageChange: () => {},
3615
+ totalRows: props.totalRows ?? props.data.length
3616
+ }),
3617
+ children: /* @__PURE__ */ jsx(TableURLState, { ...props })
3618
+ });
3619
+ }
3620
+ return /* @__PURE__ */ jsx(TableLocalState, { ...props });
3621
+ }
3136
3622
  //#endregion
3137
3623
  //#region src/components/confirm/confirmPopup.tsx
3138
3624
  function ConfirmPopup({ isOpen, header, description, confirmText = "Confirm", cancelText = "Cancel", onConfirm, onCancel, variant = "default" }) {