@mshuja288/aksuicore 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -517,31 +517,48 @@ function Snackbar({
517
517
  );
518
518
  }
519
519
  var StatusMessage = Snackbar;
520
+ function MenuIcon({ open }) {
521
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", "aria-hidden": "true", children: open ? /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 5l10 10M15 5 5 15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" }) : /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.5 5.5h13M3.5 10h13M3.5 14.5h13", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" }) });
522
+ }
520
523
  function Topbar({
521
524
  title,
522
525
  logoSrc,
523
526
  logoAlt = "Logo",
524
527
  username = "",
525
528
  onLogout,
529
+ showMenuButton = false,
530
+ menuOpen = false,
531
+ onMenuClick,
526
532
  children
527
533
  }) {
528
- const [menuOpen, setMenuOpen] = react.useState(false);
534
+ const [accountOpen, setAccountOpen] = react.useState(false);
529
535
  const menuRef = react.useRef(null);
530
536
  const initial = (username || "U").slice(0, 1).toUpperCase();
531
537
  react.useEffect(() => {
532
- if (!menuOpen) {
538
+ if (!accountOpen) {
533
539
  return;
534
540
  }
535
541
  function onPointerDown(event) {
536
542
  if (menuRef.current && !menuRef.current.contains(event.target)) {
537
- setMenuOpen(false);
543
+ setAccountOpen(false);
538
544
  }
539
545
  }
540
546
  document.addEventListener("mousedown", onPointerDown);
541
547
  return () => document.removeEventListener("mousedown", onPointerDown);
542
- }, [menuOpen]);
548
+ }, [accountOpen]);
543
549
  return /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "topbar", children: [
544
550
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "topbar-brand", children: [
551
+ showMenuButton && onMenuClick ? /* @__PURE__ */ jsxRuntime.jsx(
552
+ "button",
553
+ {
554
+ type: "button",
555
+ className: "ghost topbar-menu-button",
556
+ "aria-label": menuOpen ? "Close navigation menu" : "Open navigation menu",
557
+ "aria-expanded": menuOpen,
558
+ onClick: onMenuClick,
559
+ children: /* @__PURE__ */ jsxRuntime.jsx(MenuIcon, { open: menuOpen })
560
+ }
561
+ ) : null,
545
562
  logoSrc ? /* @__PURE__ */ jsxRuntime.jsx(
546
563
  "img",
547
564
  {
@@ -564,13 +581,13 @@ function Topbar({
564
581
  type: "button",
565
582
  className: "avatar-button",
566
583
  "aria-haspopup": "menu",
567
- "aria-expanded": menuOpen,
568
- onClick: () => setMenuOpen((open) => !open),
584
+ "aria-expanded": accountOpen,
585
+ onClick: () => setAccountOpen((open) => !open),
569
586
  title: username || "Account",
570
587
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "avatar", children: initial })
571
588
  }
572
589
  ),
573
- menuOpen ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "avatar-menu", role: "menu", children: [
590
+ accountOpen ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "avatar-menu", role: "menu", children: [
574
591
  username ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "avatar-name", children: username }) : null,
575
592
  /* @__PURE__ */ jsxRuntime.jsx(
576
593
  Button,
@@ -578,7 +595,7 @@ function Topbar({
578
595
  variant: "ghost",
579
596
  role: "menuitem",
580
597
  onClick: () => {
581
- setMenuOpen(false);
598
+ setAccountOpen(false);
582
599
  onLogout();
583
600
  },
584
601
  children: "Logout"
@@ -593,9 +610,10 @@ function SideNav({
593
610
  items,
594
611
  activeId,
595
612
  onSelect,
596
- label = "Sections"
613
+ label = "Sections",
614
+ className = ""
597
615
  }) {
598
- return /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "side-nav", "aria-label": label, children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
616
+ return /* @__PURE__ */ jsxRuntime.jsx("nav", { className: ["side-nav", className].filter(Boolean).join(" "), "aria-label": label, children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
599
617
  "button",
600
618
  {
601
619
  type: "button",
@@ -697,28 +715,131 @@ function AppShell({
697
715
  activeNavId,
698
716
  onNavSelect,
699
717
  navLabel = "Sections",
700
- showBottomNav = true,
718
+ mobileNav = "drawer",
719
+ showBottomNav,
720
+ navDrawerOpen,
721
+ onNavDrawerOpenChange,
701
722
  topbarActions,
702
723
  children,
703
724
  screenClassName = ""
704
725
  }) {
705
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "aksui-root app-shell", children: [
706
- /* @__PURE__ */ jsxRuntime.jsx(Topbar, { title, logoSrc, logoAlt, username, onLogout, children: topbarActions }),
707
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "app-body", children: [
708
- /* @__PURE__ */ jsxRuntime.jsx(SideNav, { items: navItems, activeId: activeNavId, onSelect: onNavSelect, label: navLabel }),
709
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: ["screen", screenClassName].filter(Boolean).join(" "), children })
710
- ] }),
711
- showBottomNav ? /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "bottom-nav", "aria-label": navLabel, children: navItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
712
- "button",
713
- {
714
- type: "button",
715
- className: activeNavId === item.id ? "active" : "",
716
- onClick: () => onNavSelect(item.id),
717
- children: item.short ?? item.label
718
- },
719
- item.id
720
- )) }) : null
721
- ] });
726
+ const [internalDrawerOpen, setInternalDrawerOpen] = react.useState(false);
727
+ const drawerControlled = navDrawerOpen !== void 0;
728
+ const drawerOpen = drawerControlled ? navDrawerOpen : internalDrawerOpen;
729
+ const useBottomNav = showBottomNav ?? (mobileNav === "bottom" || mobileNav === "hybrid");
730
+ const useDrawer = mobileNav === "drawer" || mobileNav === "hybrid";
731
+ const activeItem = navItems.find((item) => item.id === activeNavId);
732
+ function setDrawerOpen(open) {
733
+ if (!drawerControlled) {
734
+ setInternalDrawerOpen(open);
735
+ }
736
+ onNavDrawerOpenChange?.(open);
737
+ }
738
+ react.useEffect(() => {
739
+ if (!drawerOpen) {
740
+ return;
741
+ }
742
+ function onKeyDown(event) {
743
+ if (event.key === "Escape") {
744
+ if (!drawerControlled) {
745
+ setInternalDrawerOpen(false);
746
+ }
747
+ onNavDrawerOpenChange?.(false);
748
+ }
749
+ }
750
+ document.addEventListener("keydown", onKeyDown);
751
+ const previousOverflow = document.body.style.overflow;
752
+ document.body.style.overflow = "hidden";
753
+ return () => {
754
+ document.removeEventListener("keydown", onKeyDown);
755
+ document.body.style.overflow = previousOverflow;
756
+ };
757
+ }, [drawerControlled, drawerOpen, onNavDrawerOpenChange]);
758
+ function selectNav(id) {
759
+ onNavSelect(id);
760
+ setDrawerOpen(false);
761
+ }
762
+ return /* @__PURE__ */ jsxRuntime.jsxs(
763
+ "div",
764
+ {
765
+ className: [
766
+ "aksui-root",
767
+ "app-shell",
768
+ `mobile-nav-${mobileNav}`,
769
+ useBottomNav ? "has-bottom-nav" : "",
770
+ drawerOpen ? "nav-drawer-open" : ""
771
+ ].filter(Boolean).join(" "),
772
+ children: [
773
+ /* @__PURE__ */ jsxRuntime.jsx(
774
+ Topbar,
775
+ {
776
+ title,
777
+ logoSrc,
778
+ logoAlt,
779
+ username,
780
+ onLogout,
781
+ showMenuButton: useDrawer,
782
+ menuOpen: drawerOpen,
783
+ onMenuClick: () => setDrawerOpen(!drawerOpen),
784
+ children: topbarActions
785
+ }
786
+ ),
787
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "app-body", children: [
788
+ /* @__PURE__ */ jsxRuntime.jsx(SideNav, { items: navItems, activeId: activeNavId, onSelect: onNavSelect, label: navLabel }),
789
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ["screen", screenClassName].filter(Boolean).join(" "), children })
790
+ ] }),
791
+ useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
792
+ /* @__PURE__ */ jsxRuntime.jsx(
793
+ "button",
794
+ {
795
+ type: "button",
796
+ className: drawerOpen ? "nav-drawer-backdrop open" : "nav-drawer-backdrop",
797
+ "aria-label": "Close navigation menu",
798
+ tabIndex: drawerOpen ? 0 : -1,
799
+ onClick: () => setDrawerOpen(false)
800
+ }
801
+ ),
802
+ /* @__PURE__ */ jsxRuntime.jsxs(
803
+ "aside",
804
+ {
805
+ className: drawerOpen ? "nav-drawer open" : "nav-drawer",
806
+ "aria-label": navLabel,
807
+ "aria-hidden": !drawerOpen,
808
+ children: [
809
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nav-drawer-head", children: [
810
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nav-drawer-title", children: navLabel }),
811
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ghost nav-drawer-close", onClick: () => setDrawerOpen(false), children: "Close" })
812
+ ] }),
813
+ /* @__PURE__ */ jsxRuntime.jsx(
814
+ SideNav,
815
+ {
816
+ className: "nav-drawer-nav",
817
+ items: navItems,
818
+ activeId: activeNavId,
819
+ onSelect: selectNav,
820
+ label: navLabel
821
+ }
822
+ )
823
+ ]
824
+ }
825
+ )
826
+ ] }) : null,
827
+ useBottomNav ? /* @__PURE__ */ jsxRuntime.jsx("nav", { className: "bottom-nav", "aria-label": navLabel, children: mobileNav === "hybrid" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
828
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "active bottom-nav-current", disabled: true, children: activeItem?.short ?? activeItem?.label ?? "Section" }),
829
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "bottom-nav-menu", onClick: () => setDrawerOpen(true), children: "Menu" })
830
+ ] }) : navItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
831
+ "button",
832
+ {
833
+ type: "button",
834
+ className: activeNavId === item.id ? "active" : "",
835
+ onClick: () => onNavSelect(item.id),
836
+ children: item.short ?? item.label
837
+ },
838
+ item.id
839
+ )) }) : null
840
+ ]
841
+ }
842
+ );
722
843
  }
723
844
  function useIsClient2() {
724
845
  return react.useSyncExternalStore(
@@ -728,6 +849,42 @@ function useIsClient2() {
728
849
  () => false
729
850
  );
730
851
  }
852
+ var EXPORT_FORMAT_OPTIONS = [
853
+ { format: "csv", label: "CSV" },
854
+ { format: "xlsx", label: "Excel" },
855
+ { format: "json", label: "JSON" }
856
+ ];
857
+ function defaultVisibleKeys(columns) {
858
+ return columns.filter((column) => column.defaultVisible !== false).map((column) => column.key);
859
+ }
860
+ function readStoredColumnKeys(prefsKey, fallback, allowed) {
861
+ if (!prefsKey || typeof window === "undefined") {
862
+ return fallback;
863
+ }
864
+ try {
865
+ const raw = window.localStorage.getItem(prefsKey);
866
+ if (!raw) {
867
+ return fallback;
868
+ }
869
+ const parsed = JSON.parse(raw);
870
+ if (!Array.isArray(parsed)) {
871
+ return fallback;
872
+ }
873
+ const keys = parsed.filter((entry) => typeof entry === "string" && allowed.has(entry));
874
+ return keys.length > 0 ? keys : fallback;
875
+ } catch {
876
+ return fallback;
877
+ }
878
+ }
879
+ function writeStoredColumnKeys(prefsKey, keys) {
880
+ if (!prefsKey || typeof window === "undefined") {
881
+ return;
882
+ }
883
+ try {
884
+ window.localStorage.setItem(prefsKey, JSON.stringify(keys));
885
+ } catch {
886
+ }
887
+ }
731
888
  function compareColumnValues(column, left, right) {
732
889
  const leftRaw = column.display ? column.display(left) : column.value(left);
733
890
  const rightRaw = column.display ? column.display(right) : column.value(right);
@@ -761,6 +918,9 @@ function ResetIcon() {
761
918
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4.5 4.2v4h4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" })
762
919
  ] });
763
920
  }
921
+ function ColumnsIcon() {
922
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 4.5h3.5v11H4zM8.25 4.5h3.5v11h-3.5zM12.5 4.5H16v11h-3.5z", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }) });
923
+ }
764
924
  function ChevronIcon({ open }) {
765
925
  return /* @__PURE__ */ jsxRuntime.jsx("svg", { className: open ? "side-rail-chevron open" : "side-rail-chevron", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 4.5 13.5 10 8 15.5", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) });
766
926
  }
@@ -770,6 +930,20 @@ function PreviewIcon() {
770
930
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "10", cy: "10", r: "2.4", fill: "none", stroke: "currentColor", strokeWidth: "1.6" })
771
931
  ] });
772
932
  }
933
+ function DownloadIcon() {
934
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 20 20", "aria-hidden": "true", children: [
935
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 3.5v8.2", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" }),
936
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6.5 9.2 10 12.8l3.5-3.6", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }),
937
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 15.5h12", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" })
938
+ ] });
939
+ }
940
+ function MoreIcon() {
941
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 20 20", "aria-hidden": "true", children: [
942
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4.5", cy: "10", r: "1.5", fill: "currentColor" }),
943
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "10", cy: "10", r: "1.5", fill: "currentColor" }),
944
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15.5", cy: "10", r: "1.5", fill: "currentColor" })
945
+ ] });
946
+ }
773
947
  function DetailList({ lines }) {
774
948
  return /* @__PURE__ */ jsxRuntime.jsx("dl", { className: "record-detail-list", children: lines.map((line) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "record-detail-row", children: [
775
949
  /* @__PURE__ */ jsxRuntime.jsx("dt", { children: line.label }),
@@ -790,23 +964,86 @@ function RecordWorkspace({
790
964
  onDelete,
791
965
  canUpdate,
792
966
  canDelete,
967
+ onDownload,
968
+ canDownload = true,
969
+ downloadBusy = false,
970
+ showColumnPicker = true,
971
+ columnPrefsKey,
972
+ visibleColumnKeys,
973
+ defaultVisibleColumnKeys,
974
+ onVisibleColumnKeysChange,
793
975
  toolbar,
794
976
  form
795
977
  }) {
796
- const filterableColumns = columns.filter((column) => column.filterable !== false);
978
+ const allowedKeys = react.useMemo(() => new Set(columns.map((column) => column.key)), [columns]);
979
+ const fallbackKeys = react.useMemo(
980
+ () => defaultVisibleColumnKeys ?? defaultVisibleKeys(columns),
981
+ [columns, defaultVisibleColumnKeys]
982
+ );
983
+ const isControlled = visibleColumnKeys !== void 0;
984
+ const [internalVisibleKeys, setInternalVisibleKeys] = react.useState(
985
+ () => readStoredColumnKeys(columnPrefsKey, fallbackKeys, allowedKeys)
986
+ );
797
987
  const [filters, setFilters] = react.useState({});
798
988
  const [sort, setSort] = react.useState(null);
799
989
  const [sideOpen, setSideOpen] = react.useState(false);
990
+ const [downloadOpen, setDownloadOpen] = react.useState(false);
991
+ const [columnsOpen, setColumnsOpen] = react.useState(false);
800
992
  const [previewEnabled, setPreviewEnabled] = react.useState(false);
801
993
  const [previewKey, setPreviewKey] = react.useState(null);
802
994
  const [tooltip, setTooltip] = react.useState(null);
803
995
  const [pageSize, setPageSize] = react.useState("15");
804
996
  const [page, setPage] = react.useState(1);
997
+ const [actionsMenuKey, setActionsMenuKey] = react.useState(null);
998
+ const actionsMenuRef = react.useRef(null);
805
999
  const isClient = useIsClient2();
1000
+ react.useEffect(() => {
1001
+ if (isControlled) {
1002
+ return;
1003
+ }
1004
+ setInternalVisibleKeys(readStoredColumnKeys(columnPrefsKey, fallbackKeys, allowedKeys));
1005
+ }, [allowedKeys, columnPrefsKey, fallbackKeys, isControlled]);
1006
+ const resolvedVisibleKeys = react.useMemo(() => {
1007
+ const source = isControlled ? visibleColumnKeys : internalVisibleKeys;
1008
+ const filtered2 = source.filter((key) => allowedKeys.has(key));
1009
+ return filtered2.length > 0 ? filtered2 : fallbackKeys.filter((key) => allowedKeys.has(key));
1010
+ }, [allowedKeys, fallbackKeys, internalVisibleKeys, isControlled, visibleColumnKeys]);
1011
+ const visibleColumns = react.useMemo(() => {
1012
+ const byKey = new Map(columns.map((column) => [column.key, column]));
1013
+ return resolvedVisibleKeys.map((key) => byKey.get(key)).filter((column) => Boolean(column));
1014
+ }, [columns, resolvedVisibleKeys]);
1015
+ function setVisibleKeys(next) {
1016
+ const unique = Array.from(new Set(next.filter((key) => allowedKeys.has(key))));
1017
+ const safe = unique.length > 0 ? unique : fallbackKeys.filter((key) => allowedKeys.has(key));
1018
+ if (!isControlled) {
1019
+ setInternalVisibleKeys(safe);
1020
+ writeStoredColumnKeys(columnPrefsKey, safe);
1021
+ }
1022
+ onVisibleColumnKeysChange?.(safe);
1023
+ }
1024
+ function toggleColumnVisible(key, checked) {
1025
+ if (checked) {
1026
+ if (resolvedVisibleKeys.includes(key)) {
1027
+ return;
1028
+ }
1029
+ setVisibleKeys([...resolvedVisibleKeys, key]);
1030
+ return;
1031
+ }
1032
+ if (resolvedVisibleKeys.length <= 1) {
1033
+ return;
1034
+ }
1035
+ setVisibleKeys(resolvedVisibleKeys.filter((entry) => entry !== key));
1036
+ }
1037
+ function selectAllColumns() {
1038
+ setVisibleKeys(columns.map((column) => column.key));
1039
+ }
1040
+ function resetColumns() {
1041
+ setVisibleKeys(fallbackKeys);
1042
+ }
1043
+ const filterableColumns = visibleColumns.filter((column) => column.filterable !== false);
806
1044
  const filtered = react.useMemo(() => {
807
- const activeFilters = columns.filter((column) => column.filterable !== false);
808
1045
  return items.filter(
809
- (item) => activeFilters.every((column) => {
1046
+ (item) => filterableColumns.every((column) => {
810
1047
  const query = (filters[column.key] ?? "").trim();
811
1048
  if (!query) {
812
1049
  return true;
@@ -819,22 +1056,23 @@ function RecordWorkspace({
819
1056
  return text.toLowerCase().includes(query.toLowerCase());
820
1057
  })
821
1058
  );
822
- }, [items, filters, columns]);
1059
+ }, [items, filters, filterableColumns]);
823
1060
  const sorted = react.useMemo(() => {
824
1061
  if (!sort) {
825
1062
  return filtered;
826
1063
  }
827
- const column = columns.find((entry) => entry.key === sort.key);
1064
+ const column = visibleColumns.find((entry) => entry.key === sort.key);
828
1065
  if (!column || column.sortable === false) {
829
1066
  return filtered;
830
1067
  }
831
1068
  const direction = sort.direction === "asc" ? 1 : -1;
832
1069
  return [...filtered].sort((left, right) => compareColumnValues(column, left, right) * direction);
833
- }, [filtered, sort, columns]);
1070
+ }, [filtered, sort, visibleColumns]);
834
1071
  const activeFilterCount = react.useMemo(
835
1072
  () => filterableColumns.reduce((count, column) => count + ((filters[column.key] ?? "").trim() ? 1 : 0), 0),
836
1073
  [filterableColumns, filters]
837
1074
  );
1075
+ const hiddenColumnCount = columns.length - visibleColumns.length;
838
1076
  const showAll = pageSize === "all";
839
1077
  const numericPageSize = showAll ? Math.max(sorted.length, 1) : Number(pageSize);
840
1078
  const pageCount = showAll ? 1 : Math.max(1, Math.ceil(sorted.length / numericPageSize));
@@ -842,19 +1080,44 @@ function RecordWorkspace({
842
1080
  const start = showAll ? 0 : (currentPage - 1) * numericPageSize;
843
1081
  const pageItems = showAll ? sorted : sorted.slice(start, start + numericPageSize);
844
1082
  const showAdd = canAdd && Boolean(onAdd);
1083
+ const showDownload = Boolean(onDownload) && canDownload;
845
1084
  const showFilters = filterableColumns.length > 0;
1085
+ const showColumns = showColumnPicker && columns.length > 0;
846
1086
  const previewItem = previewKey === null ? null : sorted.find((item) => rowKey(item) === previewKey) ?? null;
1087
+ const exportColumns = visibleColumns.map((column) => ({
1088
+ key: column.exportKey ?? column.key,
1089
+ header: column.header
1090
+ }));
1091
+ react.useEffect(() => {
1092
+ if (actionsMenuKey === null) {
1093
+ return;
1094
+ }
1095
+ function onPointerDown(event) {
1096
+ if (actionsMenuRef.current && !actionsMenuRef.current.contains(event.target)) {
1097
+ setActionsMenuKey(null);
1098
+ }
1099
+ }
1100
+ document.addEventListener("mousedown", onPointerDown);
1101
+ return () => document.removeEventListener("mousedown", onPointerDown);
1102
+ }, [actionsMenuKey]);
1103
+ function requestDownload(format) {
1104
+ if (!onDownload || downloadBusy) {
1105
+ return;
1106
+ }
1107
+ onDownload(format, exportColumns);
1108
+ setDownloadOpen(false);
1109
+ }
847
1110
  function cellText(column, item) {
848
1111
  const raw = column.display ? column.display(item) : column.value(item);
849
1112
  return raw === null || raw === void 0 || raw === "" ? "\u2014" : String(raw);
850
1113
  }
851
1114
  function detailLines(item) {
852
- return columns.map((column) => ({
1115
+ return visibleColumns.map((column) => ({
853
1116
  label: column.header,
854
1117
  value: cellText(column, item)
855
1118
  }));
856
1119
  }
857
- const previewTitle = previewItem && columns[0] ? cellText(columns[0], previewItem) : null;
1120
+ const previewTitle = previewItem && visibleColumns[0] ? cellText(visibleColumns[0], previewItem) : null;
858
1121
  function changeFilter(key, value) {
859
1122
  setFilters((current) => ({ ...current, [key]: value }));
860
1123
  setPage(1);
@@ -898,6 +1161,99 @@ function RecordWorkspace({
898
1161
  setTooltip({ x, y, lines: detailLines(item) });
899
1162
  }
900
1163
  const summary = sorted.length === 0 ? "0 records" : showAll ? `1\u2013${sorted.length} of ${sorted.length}` : `${start + 1}\u2013${Math.min(start + numericPageSize, sorted.length)} of ${sorted.length}`;
1164
+ function renderRowActions(item) {
1165
+ const key = rowKey(item);
1166
+ const updateAllowed = canUpdate ? canUpdate(item) : Boolean(onUpdate);
1167
+ const deleteAllowed = canDelete ? canDelete(item) : Boolean(onDelete);
1168
+ const hasActions = Boolean(onView || onUpdate || onDelete);
1169
+ if (!hasActions) {
1170
+ return null;
1171
+ }
1172
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1173
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "row-actions row-actions-desktop", children: [
1174
+ onView ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ghost row-action", onClick: () => onView(item), children: "View" }) : null,
1175
+ onUpdate ? /* @__PURE__ */ jsxRuntime.jsx(
1176
+ "button",
1177
+ {
1178
+ type: "button",
1179
+ className: "ghost row-action",
1180
+ disabled: !updateAllowed,
1181
+ title: updateAllowed ? "Update" : "Update is not available",
1182
+ onClick: () => onUpdate(item),
1183
+ children: "Update"
1184
+ }
1185
+ ) : null,
1186
+ onDelete ? /* @__PURE__ */ jsxRuntime.jsx(
1187
+ "button",
1188
+ {
1189
+ type: "button",
1190
+ className: "ghost row-action danger",
1191
+ disabled: !deleteAllowed,
1192
+ title: deleteAllowed ? "Delete" : "Delete is not available",
1193
+ onClick: () => onDelete(item),
1194
+ children: "Delete"
1195
+ }
1196
+ ) : null
1197
+ ] }),
1198
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "row-actions row-actions-mobile", ref: actionsMenuKey === key ? actionsMenuRef : void 0, children: [
1199
+ /* @__PURE__ */ jsxRuntime.jsx(
1200
+ "button",
1201
+ {
1202
+ type: "button",
1203
+ className: "ghost row-action-menu-trigger",
1204
+ "aria-haspopup": "menu",
1205
+ "aria-expanded": actionsMenuKey === key,
1206
+ "aria-label": "Row actions",
1207
+ onClick: () => setActionsMenuKey((current) => current === key ? null : key),
1208
+ children: /* @__PURE__ */ jsxRuntime.jsx(MoreIcon, {})
1209
+ }
1210
+ ),
1211
+ actionsMenuKey === key ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "row-action-menu", role: "menu", children: [
1212
+ onView ? /* @__PURE__ */ jsxRuntime.jsx(
1213
+ "button",
1214
+ {
1215
+ type: "button",
1216
+ role: "menuitem",
1217
+ className: "ghost",
1218
+ onClick: () => {
1219
+ setActionsMenuKey(null);
1220
+ onView(item);
1221
+ },
1222
+ children: "View"
1223
+ }
1224
+ ) : null,
1225
+ onUpdate ? /* @__PURE__ */ jsxRuntime.jsx(
1226
+ "button",
1227
+ {
1228
+ type: "button",
1229
+ role: "menuitem",
1230
+ className: "ghost",
1231
+ disabled: !updateAllowed,
1232
+ onClick: () => {
1233
+ setActionsMenuKey(null);
1234
+ onUpdate(item);
1235
+ },
1236
+ children: "Update"
1237
+ }
1238
+ ) : null,
1239
+ onDelete ? /* @__PURE__ */ jsxRuntime.jsx(
1240
+ "button",
1241
+ {
1242
+ type: "button",
1243
+ role: "menuitem",
1244
+ className: "ghost danger",
1245
+ disabled: !deleteAllowed,
1246
+ onClick: () => {
1247
+ setActionsMenuKey(null);
1248
+ onDelete(item);
1249
+ },
1250
+ children: "Delete"
1251
+ }
1252
+ ) : null
1253
+ ] }) : null
1254
+ ] })
1255
+ ] });
1256
+ }
901
1257
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "list-workspace", "aria-label": title, children: [
902
1258
  /* @__PURE__ */ jsxRuntime.jsxs(
903
1259
  "div",
@@ -908,7 +1264,7 @@ function RecordWorkspace({
908
1264
  toolbar ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "table-card-bar", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "table-card-context", children: toolbar }) }) : null,
909
1265
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "table-wrap", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "data-table", children: [
910
1266
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
911
- columns.map((column) => {
1267
+ visibleColumns.map((column) => {
912
1268
  const sortable = column.sortable !== false;
913
1269
  const active = sort?.key === column.key ? sort.direction : null;
914
1270
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -933,10 +1289,8 @@ function RecordWorkspace({
933
1289
  }),
934
1290
  /* @__PURE__ */ jsxRuntime.jsx("th", { className: "actions-col", children: "Actions" })
935
1291
  ] }) }),
936
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: pageItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: columns.length + 1, children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "hint table-empty", children: items.length === 0 ? empty : "No records match the current filters." }) }) }) : pageItems.map((item) => {
1292
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: pageItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: visibleColumns.length + 1, children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "hint table-empty", children: items.length === 0 ? empty : "No records match the current filters." }) }) }) : pageItems.map((item) => {
937
1293
  const key = rowKey(item);
938
- const updateAllowed = canUpdate ? canUpdate(item) : Boolean(onUpdate);
939
- const deleteAllowed = canDelete ? canDelete(item) : Boolean(onDelete);
940
1294
  const selected = previewEnabled && previewKey === key;
941
1295
  return /* @__PURE__ */ jsxRuntime.jsxs(
942
1296
  "tr",
@@ -956,32 +1310,8 @@ function RecordWorkspace({
956
1310
  onMouseLeave: () => setTooltip(null),
957
1311
  onClick: () => focusRecord(item),
958
1312
  children: [
959
- columns.map((column) => /* @__PURE__ */ jsxRuntime.jsx("td", { children: cellText(column, item) }, column.key)),
960
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "actions-col", onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "row-actions", children: [
961
- onView ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ghost row-action", onClick: () => onView(item), children: "View" }) : null,
962
- onUpdate ? /* @__PURE__ */ jsxRuntime.jsx(
963
- "button",
964
- {
965
- type: "button",
966
- className: "ghost row-action",
967
- disabled: !updateAllowed,
968
- title: updateAllowed ? "Update" : "Update is not available",
969
- onClick: () => onUpdate(item),
970
- children: "Update"
971
- }
972
- ) : null,
973
- onDelete ? /* @__PURE__ */ jsxRuntime.jsx(
974
- "button",
975
- {
976
- type: "button",
977
- className: "ghost row-action danger",
978
- disabled: !deleteAllowed,
979
- title: deleteAllowed ? "Delete" : "Delete is not available",
980
- onClick: () => onDelete(item),
981
- children: "Delete"
982
- }
983
- ) : null
984
- ] }) })
1313
+ visibleColumns.map((column) => /* @__PURE__ */ jsxRuntime.jsx("td", { children: cellText(column, item) }, column.key)),
1314
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "actions-col", onClick: (event) => event.stopPropagation(), children: renderRowActions(item) })
985
1315
  ]
986
1316
  },
987
1317
  key
@@ -1043,6 +1373,73 @@ function RecordWorkspace({
1043
1373
  /* @__PURE__ */ jsxRuntime.jsx(PlusIcon, {}),
1044
1374
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: addLabel })
1045
1375
  ] }) : null,
1376
+ showColumns ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "side-rail-columns", children: [
1377
+ /* @__PURE__ */ jsxRuntime.jsxs(
1378
+ "button",
1379
+ {
1380
+ type: "button",
1381
+ className: "ghost side-rail-columns-toggle",
1382
+ "aria-expanded": columnsOpen,
1383
+ onClick: () => setColumnsOpen((open) => !open),
1384
+ children: [
1385
+ /* @__PURE__ */ jsxRuntime.jsx(ColumnsIcon, {}),
1386
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1387
+ "Columns",
1388
+ hiddenColumnCount > 0 ? ` (${visibleColumns.length}/${columns.length})` : ""
1389
+ ] })
1390
+ ]
1391
+ }
1392
+ ),
1393
+ columnsOpen ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "side-rail-columns-panel", role: "group", "aria-label": "Visible columns", children: [
1394
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "side-rail-columns-actions", children: [
1395
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ghost", onClick: selectAllColumns, children: "Select all" }),
1396
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ghost", onClick: resetColumns, children: "Reset" })
1397
+ ] }),
1398
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "side-rail-columns-list", children: columns.map((column) => {
1399
+ const checked = resolvedVisibleKeys.includes(column.key);
1400
+ const disableUncheck = checked && resolvedVisibleKeys.length <= 1;
1401
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "side-rail-column-option", children: [
1402
+ /* @__PURE__ */ jsxRuntime.jsx(
1403
+ "input",
1404
+ {
1405
+ type: "checkbox",
1406
+ checked,
1407
+ disabled: disableUncheck,
1408
+ onChange: (event) => toggleColumnVisible(column.key, event.target.checked)
1409
+ }
1410
+ ),
1411
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: column.header })
1412
+ ] }, column.key);
1413
+ }) })
1414
+ ] }) : null
1415
+ ] }) : null,
1416
+ showDownload ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "side-rail-download", children: [
1417
+ /* @__PURE__ */ jsxRuntime.jsxs(
1418
+ "button",
1419
+ {
1420
+ type: "button",
1421
+ className: "ghost side-rail-download-toggle",
1422
+ "aria-expanded": downloadOpen,
1423
+ disabled: downloadBusy,
1424
+ onClick: () => setDownloadOpen((open) => !open),
1425
+ children: [
1426
+ /* @__PURE__ */ jsxRuntime.jsx(DownloadIcon, {}),
1427
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: downloadBusy ? "Downloading\u2026" : "Download" })
1428
+ ]
1429
+ }
1430
+ ),
1431
+ downloadOpen ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "side-rail-download-formats", role: "group", "aria-label": "Download format", children: EXPORT_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ jsxRuntime.jsx(
1432
+ "button",
1433
+ {
1434
+ type: "button",
1435
+ className: "ghost side-rail-download-format",
1436
+ disabled: downloadBusy,
1437
+ onClick: () => requestDownload(option.format),
1438
+ children: option.label
1439
+ },
1440
+ option.format
1441
+ )) }) : null
1442
+ ] }) : null,
1046
1443
  showFilters ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "side-rail-filters", children: [
1047
1444
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "filter-panel-head", children: [
1048
1445
  /* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Filters" }),
@@ -1096,6 +1493,39 @@ function RecordWorkspace({
1096
1493
  /* @__PURE__ */ jsxRuntime.jsx(PreviewIcon, {})
1097
1494
  ] }),
1098
1495
  showAdd ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ghost side-rail-icon", title: addLabel, "aria-label": addLabel, onClick: onAdd, children: /* @__PURE__ */ jsxRuntime.jsx(PlusIcon, {}) }) : null,
1496
+ showColumns ? /* @__PURE__ */ jsxRuntime.jsxs(
1497
+ "button",
1498
+ {
1499
+ type: "button",
1500
+ className: "ghost side-rail-icon",
1501
+ title: "Columns",
1502
+ "aria-label": "Columns",
1503
+ onClick: () => {
1504
+ setSideOpen(true);
1505
+ setColumnsOpen(true);
1506
+ },
1507
+ children: [
1508
+ /* @__PURE__ */ jsxRuntime.jsx(ColumnsIcon, {}),
1509
+ hiddenColumnCount > 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "filter-badge", children: hiddenColumnCount }) : null
1510
+ ]
1511
+ }
1512
+ ) : null,
1513
+ showDownload ? /* @__PURE__ */ jsxRuntime.jsx(
1514
+ "button",
1515
+ {
1516
+ type: "button",
1517
+ className: "ghost side-rail-icon",
1518
+ title: "Download",
1519
+ "aria-label": "Download",
1520
+ "aria-expanded": downloadOpen,
1521
+ disabled: downloadBusy,
1522
+ onClick: () => {
1523
+ setSideOpen(true);
1524
+ setDownloadOpen(true);
1525
+ },
1526
+ children: /* @__PURE__ */ jsxRuntime.jsx(DownloadIcon, {})
1527
+ }
1528
+ ) : null,
1099
1529
  showFilters ? /* @__PURE__ */ jsxRuntime.jsxs(
1100
1530
  "button",
1101
1531
  {