uibee 3.1.2 → 3.1.4

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));
@@ -2705,138 +2705,387 @@ function Alert({ children, variant = "warning", className = "" }) {
2705
2705
  });
2706
2706
  }
2707
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
2708
2904
  //#region src/components/table/menu.tsx
2709
- const MenuContext = createContext({});
2905
+ const MenuCtx = createContext({});
2710
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]);
2711
2914
  return createPortal(/* @__PURE__ */ jsx("div", {
2712
2915
  ref,
2916
+ role: "menu",
2917
+ "aria-orientation": "vertical",
2713
2918
  style: {
2714
2919
  top: anchor.top,
2715
2920
  right: anchor.right
2716
2921
  },
2717
- className: "fixed bg-login-500 border border-login-600 rounded-lg shadow-lg z-9999 w-44",
2718
- 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, {
2719
2924
  value: { onClose },
2720
2925
  children
2721
2926
  })
2722
2927
  }), document.body);
2723
2928
  }
2724
- function MenuButton({ icon, text, hotKey, onClick, className }) {
2725
- const { onClose } = useContext(MenuContext);
2929
+ function MenuButton({ icon, text, hotKey, onClick, className = "" }) {
2930
+ const { onClose } = useContext(MenuCtx);
2726
2931
  useEffect(() => {
2727
2932
  if (!hotKey) return;
2728
- function handleKeyDown(e) {
2933
+ function handleKey(e) {
2934
+ const tag = e.target.tagName;
2935
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
2729
2936
  if (e.key.toLowerCase() === hotKey.toLowerCase()) {
2730
2937
  e.preventDefault();
2731
2938
  onClick();
2732
2939
  onClose?.();
2733
2940
  }
2734
2941
  }
2735
- window.addEventListener("keydown", handleKeyDown);
2736
- return () => window.removeEventListener("keydown", handleKeyDown);
2942
+ window.addEventListener("keydown", handleKey);
2943
+ return () => window.removeEventListener("keydown", handleKey);
2737
2944
  }, [
2738
2945
  hotKey,
2739
2946
  onClick,
2740
2947
  onClose
2741
2948
  ]);
2742
2949
  return /* @__PURE__ */ jsxs("button", {
2950
+ role: "menuitem",
2743
2951
  onClick: () => {
2744
2952
  onClick();
2745
2953
  onClose?.();
2746
2954
  },
2747
- className: `flex items-center justify-between w-full px-3 py-2 text-sm hover:bg-login-600 cursor-pointer
2748
- ${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}
2749
2961
  `,
2750
- children: [/* @__PURE__ */ jsxs("div", {
2751
- className: "flex items-center",
2752
- children: [React.cloneElement(icon, { className: "w-4 h-4 mr-2" }), text]
2753
- }), /* @__PURE__ */ jsx("span", {
2754
- 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",
2755
2970
  children: hotKey
2756
2971
  })]
2757
2972
  });
2758
2973
  }
2759
2974
  //#endregion
2760
- //#region src/components/table/format.ts
2761
- const nullableTimeKeys = [
2762
- "date",
2763
- "last_sent",
2764
- "time"
2765
- ];
2766
- const ISODateTimeReg = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
2767
- const ISODateReg = /^\d{4}-\d{2}-\d{2}$/;
2768
- const ISOTimeReg = /^\d{2}:\d{2}(:\d{2})?$/;
2769
- const OsloTZ = { timeZone: "Europe/Oslo" };
2770
- const OsloTime = {
2771
- ...OsloTZ,
2772
- hour: "2-digit",
2773
- minute: "2-digit"
2774
- };
2775
- const OsloDateTime = {
2776
- ...OsloTime,
2777
- year: "numeric",
2778
- month: "2-digit",
2779
- day: "2-digit"
2780
- };
2781
- function formatValue(key, value) {
2782
- if (nullableTimeKeys.includes(key) && !value) return "Never";
2783
- if (typeof value === "string") {
2784
- if (ISODateTimeReg.test(value)) return new Date(value).toLocaleString("nb-NO", OsloDateTime);
2785
- if (ISODateReg.test(value)) return new Date(value).toLocaleDateString("nb-NO", OsloTZ);
2786
- 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
+ });
2787
3024
  }
2788
- if (key.includes("capacity")) return value === 0 ? "Unlimited" : value;
2789
- 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"} ${shouldTruncate ? "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
+ });
2790
3039
  }
2791
- //#endregion
2792
- //#region src/components/table/body.tsx
2793
- 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();
2794
3042
  const [openMenuId, setOpenMenuId] = useState(null);
2795
3043
  const [anchor, setAnchor] = useState(null);
2796
- const router = useRouter();
3044
+ const [expandedId, setExpandedId] = useState(null);
2797
3045
  const menuRef = useRef(null);
2798
3046
  const tbodyRef = useRef(null);
2799
3047
  const menuWasOpenOnMouseDown = useRef(false);
2800
- useClickOutside(menuRef, () => setOpenMenuId(null));
3048
+ const closeMenu = useCallback(() => setOpenMenuId(null), []);
3049
+ useClickOutside(menuRef, closeMenu, openMenuId !== null);
2801
3050
  useEffect(() => {
2802
3051
  const el = tbodyRef.current;
2803
3052
  if (!el) return;
2804
- const close = () => setOpenMenuId(null);
2805
- el.addEventListener("scroll", close);
2806
- return () => el.removeEventListener("scroll", close);
2807
- }, []);
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);
2808
3066
  return /* @__PURE__ */ jsx("tbody", {
2809
3067
  ref: tbodyRef,
2810
- className: `
2811
- block overflow-y-auto flex-1 min-h-0 divide-y
2812
- ${variant === "default" ? "bg-login-800 divide-login-600/25" : "bg-transparent divide-login-600/20"}
2813
- `,
2814
- children: list.map((item, index) => {
2815
- const itemRecord = item;
2816
- let id = "";
2817
- if (idKey && itemRecord[idKey] !== void 0) id = String(itemRecord[idKey]);
2818
- else if (itemRecord["id"] !== void 0) id = String(itemRecord["id"]);
2819
- else {
2820
- const firstKey = columns[0]?.key || Object.keys(itemRecord)[0];
2821
- id = String(itemRecord[firstKey]);
2822
- }
2823
- const redirectConfig = typeof redirectPath === "object" && redirectPath ? redirectPath : { path: redirectPath };
2824
- const redirectId = redirectConfig.key ? String(itemRecord[redirectConfig.key]) : id;
2825
- const menuButtonColors = variant === "minimal" ? {
2826
- active: "bg-login-600 text-login-100",
2827
- inactive: "hover:bg-login-600 text-login-200 hover:text-login-100"
2828
- } : {
2829
- active: "bg-login-400 text-login-100",
2830
- inactive: "hover:bg-login-400 text-login-200 hover:text-login-100"
2831
- };
2832
- const buttonClass = openMenuId === id ? menuButtonColors.active : menuButtonColors.inactive;
2833
- return /* @__PURE__ */ jsxs("tr", {
2834
- className: `
2835
- flex w-full group/row transition-colors
2836
- ${redirectConfig.path ? "cursor-pointer" : ""}
2837
- ${variant === "default" && redirectConfig.path ? "hover:bg-login-600/80" : ""}
2838
- ${variant === "minimal" ? "hover:bg-login-600/70 border-b border-login-600/20 last:border-0" : ""}
2839
- `,
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
+ },
2840
3089
  onMouseDown: () => {
2841
3090
  menuWasOpenOnMouseDown.current = openMenuId !== null;
2842
3091
  },
@@ -2845,297 +3094,531 @@ function Body({ list, columns, menuItems, redirectPath, variant = "default", idK
2845
3094
  menuWasOpenOnMouseDown.current = false;
2846
3095
  return;
2847
3096
  }
2848
- if (redirectConfig.path) if (redirectConfig.path.includes("?")) router.push(`${redirectConfig.path}${redirectId}`);
2849
- 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);
2850
3106
  },
2851
3107
  onContextMenu: (e) => {
3108
+ if (!hasMenu) return;
2852
3109
  e.preventDefault();
2853
- setAnchor({
3110
+ openMenu(id, {
2854
3111
  top: e.clientY,
2855
3112
  right: window.innerWidth - e.clientX
2856
3113
  });
2857
- setOpenMenuId(id);
2858
3114
  },
2859
- children: [columns.map((col) => {
2860
- const value = itemRecord[col.key];
2861
- let badgeClass = "";
2862
- if (col.highlight) {
2863
- const highlightKey = String(value);
2864
- const colorName = col.highlight[highlightKey] || col.highlight.default;
2865
- if (colorName) {
2866
- switch (colorName) {
2867
- case "green":
2868
- badgeClass = "bg-green-500/20 text-green-400";
2869
- break;
2870
- case "yellow":
2871
- badgeClass = "bg-yellow-500/20 text-yellow-400";
2872
- break;
2873
- case "red":
2874
- badgeClass = "bg-red-500/20 text-red-400";
2875
- break;
2876
- case "blue":
2877
- badgeClass = "bg-blue-500/20 text-blue-400";
2878
- break;
2879
- case "gray":
2880
- badgeClass = "bg-gray-500/20 text-gray-400";
2881
- break;
2882
- }
2883
- badgeClass += " px-2 py-1 rounded";
2884
- }
2885
- }
2886
- return /* @__PURE__ */ jsx("td", {
2887
- className: `
2888
- flex-1 min-w-0 px-6 py-4 whitespace-nowrap text-sm flex items-center text-login-100
2889
- ${variant === "minimal" ? "px-4! py-2!" : ""}
2890
- `,
2891
- children: /* @__PURE__ */ jsx("div", {
2892
- className: "relative w-full min-w-0",
2893
- children: /* @__PURE__ */ jsx("span", {
2894
- className: `block max-w-full truncate ${badgeClass}`,
2895
- children: formatValue(col.key, value)
2896
- })
2897
- })
2898
- }, col.key);
2899
- }), menuItems && /* @__PURE__ */ jsx("td", {
2900
- 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",
2901
- children: /* @__PURE__ */ jsxs("div", {
2902
- className: "relative",
2903
- 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", {
2904
3124
  type: "button",
2905
- className: `p-1.5 rounded flex items-center justify-center transition-colors ${buttonClass}`,
2906
- onMouseDown: (e) => e.nativeEvent.stopImmediatePropagation(),
3125
+ "aria-label": isSelected ? "Deselect row" : "Select row",
3126
+ "aria-checked": isSelected,
3127
+ role: "checkbox",
2907
3128
  onClick: (e) => {
2908
3129
  e.stopPropagation();
2909
- const rect = e.currentTarget.getBoundingClientRect();
2910
- const coords = {
2911
- top: rect.bottom + 4,
2912
- right: window.innerWidth - rect.right
2913
- };
2914
- setAnchor(openMenuId === id ? null : coords);
2915
- setOpenMenuId(openMenuId === id ? null : id);
3130
+ toggleSelect(id);
2916
3131
  },
2917
- children: /* @__PURE__ */ jsx("span", {
2918
- className: "text-xl leading-none select-none",
2919
- 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
+ })
2920
3147
  })
2921
- }), openMenuId === id && anchor && /* @__PURE__ */ jsx(Menu, {
2922
- ref: menuRef,
2923
- anchor,
2924
- onClose: () => setOpenMenuId(null),
2925
- children: menuItems?.(item, id)
2926
- })]
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
+ })
2927
3205
  })
2928
- })]
2929
- }, 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);
2930
3215
  })
2931
3216
  });
2932
3217
  }
2933
3218
  //#endregion
2934
- //#region src/components/table/header.tsx
2935
- function parseOrder(value) {
2936
- return value === "asc" || value === "desc" ? value : void 0;
2937
- }
2938
- function Header({ columns, hideMenu, variant = "default" }) {
2939
- const router = useRouter();
2940
- const pathname = usePathname();
2941
- const searchParams = useSearchParams();
2942
- const [column, setColumn] = useState(searchParams.get("column") ?? "");
2943
- const [order, setOrder] = useState(parseOrder(searchParams.get("order")));
2944
- useEffect(() => {
2945
- if (!column || !order) return;
2946
- const params = new URLSearchParams(searchParams.toString());
2947
- if (searchParams.get("order") !== order || searchParams.get("column") !== column) {
2948
- params.set("order", order);
2949
- params.set("column", column);
2950
- params.set("page", "1");
2951
- router.push(`${pathname}?${params.toString()}`);
2952
- }
2953
- }, [
2954
- order,
2955
- column,
2956
- pathname,
2957
- router,
2958
- searchParams
2959
- ]);
2960
- function handleChange(key) {
2961
- setColumn(key);
2962
- setOrder((prev) => key === column && prev === "asc" ? "desc" : "asc");
2963
- }
2964
- return /* @__PURE__ */ jsx("thead", {
2965
- className: `
2966
- block w-full
2967
- ${variant === "default" ? "bg-login-700" : "bg-transparent border-b border-login-600"}
2968
- `,
2969
- 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", {
2970
3224
  className: "flex w-full",
2971
- children: [columns.map((col) => {
2972
- const key = col.key;
2973
- const value = col.label || (key.length < 3 ? key.toUpperCase() : `${key[0].toUpperCase()}${key.slice(1).replaceAll("_", " ")}`);
2974
- 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,
2975
3240
  className: `
2976
- flex-1 min-w-0 px-6 py-3 text-xs font-medium uppercase tracking-wider text-left
2977
- ${variant === "default" ? "text-login-200" : "text-login-100"}
2978
- ${variant === "minimal" ? "px-4!" : ""}
3241
+ flex-1 min-w-0 flex items-center
3242
+ ${DENSITY_TD[density]}
2979
3243
  `,
2980
- children: /* @__PURE__ */ jsxs("button", {
2981
- className: "flex w-full min-w-0 flex-row items-center gap-2 group uppercase whitespace-nowrap",
2982
- onClick: () => handleChange(key),
2983
- children: [/* @__PURE__ */ jsx("span", {
2984
- className: "min-w-0 truncate",
2985
- children: value
2986
- }), /* @__PURE__ */ jsx("span", {
2987
- className: "flex flex-col",
2988
- 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" })
2989
- })]
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
+ }
2990
3250
  })
2991
- }, key);
2992
- }), !hideMenu && /* @__PURE__ */ jsx("th", { className: "shrink-0 w-16 px-6 py-3" })]
2993
- })
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))
2994
3263
  });
2995
3264
  }
2996
3265
  //#endregion
2997
- //#region src/components/table/table.tsx
2998
- function Table({ data, columns, menuItems, redirectPath, variant = "default", idKey }) {
2999
- if (data.length === 0) return /* @__PURE__ */ jsx("div", {
3000
- className: "p-4 text-center text-login-200",
3001
- 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
+ })
3002
3280
  });
3003
- return /* @__PURE__ */ jsx("div", {
3004
- className: `
3005
- flex-1 flex flex-col min-h-0 overflow-x-auto overflow-y-hidden h-full w-full
3006
- ${variant === "default" ? "bg-login-800 rounded-lg border border-login-600/30" : ""}
3007
- ${variant === "minimal" ? "bg-transparent" : ""}
3008
- `,
3009
- children: /* @__PURE__ */ jsxs("table", {
3010
- className: "min-w-full w-max divide-y divide-login-600/30 flex flex-col flex-1 min-h-0",
3011
- children: [/* @__PURE__ */ jsx(Header, {
3012
- columns,
3013
- hideMenu: !menuItems,
3014
- variant
3015
- }), /* @__PURE__ */ jsx(Body, {
3016
- list: data,
3017
- columns,
3018
- menuItems,
3019
- redirectPath,
3020
- variant,
3021
- idKey
3022
- })]
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
+ })
3023
3295
  })
3024
3296
  });
3025
3297
  }
3026
3298
  //#endregion
3027
3299
  //#region src/components/table/pagination.tsx
3028
- function Pagination({ pageSize = 10, totalRows }) {
3029
- const router = useRouter();
3030
- const pathname = usePathname();
3031
- const searchParams = useSearchParams();
3032
- const rawPage = parseInt(searchParams.get("page") || "1", 10);
3033
- const [current, setCurrent] = useState(Math.max(1, Number.isNaN(rawPage) ? 1 : rawPage));
3034
- const totalPages = Math.max(1, Math.ceil(totalRows !== void 0 && pageSize > 0 ? totalRows / pageSize : 1));
3035
- useEffect(() => {
3036
- const raw = parseInt(searchParams.get("page") || "1", 10);
3037
- setCurrent(Math.max(1, Math.min(totalPages, Math.max(1, Number.isNaN(raw) ? 1 : raw))));
3038
- }, [searchParams, totalPages]);
3039
- function updateQuery(p) {
3040
- const params = new URLSearchParams(searchParams.toString());
3041
- params.set("page", String(p));
3042
- router.push(`${pathname}?${params.toString()}`);
3043
- }
3044
- function goPrevious() {
3045
- if (current <= 1) return;
3046
- const next = current - 1;
3047
- setCurrent(next);
3048
- 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("…");
3049
3311
  }
3050
- function goNext() {
3051
- if (current >= totalPages) return;
3052
- const next = current + 1;
3053
- setCurrent(next);
3054
- 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);
3055
3316
  }
3056
- function setPage(p) {
3057
- if (p === current) return;
3058
- setCurrent(p);
3059
- updateQuery(p);
3060
- }
3061
- function getPages(curr, total) {
3062
- const delta = 2;
3063
- const left = Math.max(1, curr - delta);
3064
- const right = Math.min(total, curr + delta);
3065
- const pages = [];
3066
- if (left > 1) {
3067
- pages.push(1);
3068
- if (left > 2) pages.push("...");
3069
- }
3070
- for (let i = left; i <= right; i++) pages.push(i);
3071
- if (right < total) {
3072
- if (right < total - 1) pages.push("...");
3073
- pages.push(total);
3074
- }
3075
- return pages;
3076
- }
3077
- const pages = getPages(current, totalPages);
3078
- const start = Math.max(1, (current - 1) * pageSize + 1);
3079
- 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";
3080
3330
  return /* @__PURE__ */ jsxs("div", {
3081
- className: "flex items-center justify-between w-full pt-4",
3082
- children: [/* @__PURE__ */ jsx("div", {
3083
- className: "text-sm /70",
3084
- children: totalRows !== void 0 ? totalRows === 0 ? /* @__PURE__ */ jsx("span", { children: "Showing 0 results" }) : /* @__PURE__ */ jsxs("span", { children: [
3085
- "Showing ",
3086
- start,
3087
- " to ",
3088
- end,
3089
- " of ",
3090
- totalRows,
3091
- " results"
3092
- ] }) : null
3093
- }), /* @__PURE__ */ jsxs("div", {
3094
- 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",
3095
3338
  children: [
3096
3339
  /* @__PURE__ */ jsx("button", {
3097
3340
  type: "button",
3098
- onClick: goPrevious,
3099
- disabled: current <= 1,
3100
- className: `
3101
- flex items-center gap-2 p-1 rounded-lg
3102
- bg-login-700 hover:bg-login-600 disabled:opacity-40
3103
- border border-login-500/50 text-sm transition-colors duration-150
3104
- `,
3105
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-5 w-5" })
3106
- }),
3107
- /* @__PURE__ */ jsx("nav", {
3108
- className: "flex items-center gap-1",
3109
- "aria-label": "Pagination",
3110
- children: pages.map((p, i) => typeof p === "string" ? /* @__PURE__ */ jsx("span", {
3111
- className: "px-3 py-1 text-sm",
3112
- children: p
3113
- }, `e-${i}`) : /* @__PURE__ */ jsx("button", {
3114
- type: "button",
3115
- onClick: () => setPage(p),
3116
- "aria-current": p === current ? "page" : void 0,
3117
- className: `
3118
- px-3 py-1 rounded-lg text-sm border transition-colors duration-150
3119
- ${p === current ? "bg-login text-white border-login" : "bg-login-700 border-login-500/50 hover:bg-login-600"}
3120
- `,
3121
- children: p
3122
- }, 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" })
3123
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)),
3124
3357
  /* @__PURE__ */ jsx("button", {
3125
3358
  type: "button",
3126
- onClick: goNext,
3127
- disabled: current >= totalPages,
3128
- className: `
3129
- flex items-center gap-2 p-1 rounded-lg select-none
3130
- bg-login-700 hover:bg-login-600 disabled:opacity-40
3131
- border border-login-500/50 text-sm transition-colors duration-150
3132
- `,
3133
- 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" })
3134
3364
  })
3135
3365
  ]
3136
3366
  })]
3137
3367
  });
3138
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
+ }
3139
3622
  //#endregion
3140
3623
  //#region src/components/confirm/confirmPopup.tsx
3141
3624
  function ConfirmPopup({ isOpen, header, description, confirmText = "Confirm", cancelText = "Cancel", onConfirm, onCancel, variant = "default" }) {