@diwauhris/ui 1.7.0 → 1.7.1

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/lib/assets/ui.css CHANGED
@@ -482,6 +482,11 @@
482
482
  margin: auto;
483
483
  }
484
484
 
485
+ .-mx-5 {
486
+ margin-left: -1.25rem;
487
+ margin-right: -1.25rem;
488
+ }
489
+
485
490
  .mx-1 {
486
491
  margin-left: 0.25rem;
487
492
  margin-right: 0.25rem;
@@ -510,6 +515,10 @@
510
515
  margin-top: -0.125rem;
511
516
  }
512
517
 
518
+ .-mt-4 {
519
+ margin-top: -1rem;
520
+ }
521
+
513
522
  .mb-0\.5 {
514
523
  margin-bottom: 0.125rem;
515
524
  }
@@ -610,10 +619,6 @@
610
619
  margin-top: 2.5rem;
611
620
  }
612
621
 
613
- .mt-12 {
614
- margin-top: 3rem;
615
- }
616
-
617
622
  .mt-2 {
618
623
  margin-top: 0.5rem;
619
624
  }
@@ -3806,6 +3811,14 @@
3806
3811
  text-decoration-line: line-through;
3807
3812
  }
3808
3813
 
3814
+ .decoration-amber-500\/60 {
3815
+ text-decoration-color: rgb(245 158 11 / 0.6);
3816
+ }
3817
+
3818
+ .underline-offset-2 {
3819
+ text-underline-offset: 2px;
3820
+ }
3821
+
3809
3822
  .antialiased {
3810
3823
  -webkit-font-smoothing: antialiased;
3811
3824
  -moz-osx-font-smoothing: grayscale;
@@ -3821,6 +3834,10 @@
3821
3834
  color: rgb(148 163 184 / var(--tw-placeholder-opacity, 1));
3822
3835
  }
3823
3836
 
3837
+ .accent-brand-blue {
3838
+ accent-color: #034EA2;
3839
+ }
3840
+
3824
3841
  .opacity-0 {
3825
3842
  opacity: 0;
3826
3843
  }
@@ -4689,6 +4706,9 @@ body {
4689
4706
  .hover\:underline:hover {
4690
4707
  text-decoration-line: underline;
4691
4708
  }
4709
+ .hover\:decoration-amber-700:hover {
4710
+ text-decoration-color: #b45309;
4711
+ }
4692
4712
  .hover\:opacity-100:hover {
4693
4713
  opacity: 1;
4694
4714
  }
@@ -4800,6 +4820,10 @@ body {
4800
4820
  --tw-ring-opacity: 1;
4801
4821
  --tw-ring-color: rgb(253 230 138 / var(--tw-ring-opacity, 1));
4802
4822
  }
4823
+ .focus-visible\:ring-amber-600:focus-visible {
4824
+ --tw-ring-opacity: 1;
4825
+ --tw-ring-color: rgb(217 119 6 / var(--tw-ring-opacity, 1));
4826
+ }
4803
4827
  .focus-visible\:ring-brand-blue\/15:focus-visible {
4804
4828
  --tw-ring-color: rgb(3 78 162 / 0.15);
4805
4829
  }
package/lib/index.cjs CHANGED
@@ -1475,7 +1475,17 @@ function SegmentedControl({ options, defaultValue, value: controlledValue, onVal
1475
1475
  }
1476
1476
  //#endregion
1477
1477
  //#region src/ui-library/gallery/validation-summary/ValidationSummary.tsx
1478
- function ValidationSummary({ issues, heading = "Review these fields before saving", className = "" }) {
1478
+ function scrollToField(domId) {
1479
+ const el = document.getElementById(domId);
1480
+ if (!el) return;
1481
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1482
+ el.scrollIntoView({
1483
+ behavior: prefersReducedMotion ? "auto" : "smooth",
1484
+ block: "center"
1485
+ });
1486
+ (el.querySelector("input,select,textarea,[tabindex]") ?? el).focus();
1487
+ }
1488
+ function ValidationSummary({ issues, heading = "Review these fields before saving", fieldIds, fieldLabels, className = "" }) {
1479
1489
  if (issues.length === 0) return null;
1480
1490
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1481
1491
  role: "alert",
@@ -1485,14 +1495,24 @@ function ValidationSummary({ issues, heading = "Review these fields before savin
1485
1495
  children: heading
1486
1496
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1487
1497
  className: "mt-2 list-disc space-y-1 pl-5",
1488
- children: issues.map((issue) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
1489
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1490
- className: "font-semibold",
1491
- children: [issue.field, ":"]
1492
- }),
1493
- " ",
1494
- issue.message
1495
- ] }, `${issue.field}:${issue.message}`))
1498
+ children: issues.map((issue) => {
1499
+ const displayLabel = fieldLabels?.[issue.field] ?? issue.field;
1500
+ const domId = fieldIds?.[issue.field];
1501
+ const label = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1503
+ className: "font-semibold",
1504
+ children: [displayLabel, ":"]
1505
+ }),
1506
+ " ",
1507
+ issue.message
1508
+ ] });
1509
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: domId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1510
+ type: "button",
1511
+ onClick: () => scrollToField(domId),
1512
+ className: "underline decoration-amber-500/60 underline-offset-2 hover:decoration-amber-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-amber-600 rounded text-left",
1513
+ children: label
1514
+ }) : label }, `${issue.field}:${issue.message}`);
1515
+ })
1496
1516
  })]
1497
1517
  });
1498
1518
  }
@@ -2879,53 +2899,72 @@ function getPageNumbers(current, total) {
2879
2899
  pages.push(total);
2880
2900
  return pages;
2881
2901
  }
2882
- function Pagination({ page, totalPages, onPageChange, ariaLabel = "Pagination" }) {
2883
- if (totalPages <= 1) return null;
2902
+ function Pagination({ page, totalPages, onPageChange, ariaLabel = "Pagination", totalItems, pageSize, itemLabel = "items" }) {
2903
+ if (totalPages <= 1 && !totalItems) return null;
2904
+ const showRecordCount = typeof totalItems === "number" && typeof pageSize === "number";
2905
+ const rangeStart = showRecordCount ? (page - 1) * pageSize + 1 : null;
2906
+ const rangeEnd = showRecordCount ? Math.min(page * pageSize, totalItems) : null;
2884
2907
  const pageNumbers = getPageNumbers(page, totalPages);
2885
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
2886
- "aria-label": ariaLabel,
2887
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
2888
- className: "list-none flex items-center gap-1",
2889
- role: "list",
2908
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2909
+ className: "flex flex-col items-center gap-1.5",
2910
+ children: [totalPages > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
2911
+ "aria-label": ariaLabel,
2912
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
2913
+ className: "list-none flex items-center gap-1",
2914
+ role: "list",
2915
+ children: [
2916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2917
+ type: "button",
2918
+ onClick: () => onPageChange(page - 1),
2919
+ disabled: page <= 1,
2920
+ "aria-label": "Previous page",
2921
+ className: "flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 disabled:cursor-not-allowed disabled:opacity-40",
2922
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronLeft, {
2923
+ size: 16,
2924
+ "aria-hidden": "true"
2925
+ })
2926
+ }) }),
2927
+ pageNumbers.map((n, idx) => n === "ellipsis" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
2928
+ "aria-hidden": "true",
2929
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2930
+ className: "flex h-9 w-9 items-center justify-center text-xs font-bold text-slate-300",
2931
+ children: "…"
2932
+ })
2933
+ }, `ellipsis-${idx}`) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2934
+ type: "button",
2935
+ onClick: () => onPageChange(n),
2936
+ "aria-label": `Page ${n}`,
2937
+ "aria-current": n === page ? "page" : void 0,
2938
+ className: ["flex h-9 w-9 items-center justify-center rounded-md text-xs font-bold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30", n === page ? "bg-brand-blue text-white shadow-md shadow-brand-blue/20" : "border border-slate-200 bg-white text-slate-500 hover:border-slate-300 hover:text-slate-800"].filter(Boolean).join(" "),
2939
+ children: n
2940
+ }) }, n)),
2941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2942
+ type: "button",
2943
+ onClick: () => onPageChange(page + 1),
2944
+ disabled: page >= totalPages,
2945
+ "aria-label": "Next page",
2946
+ className: "flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 disabled:cursor-not-allowed disabled:opacity-40",
2947
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
2948
+ size: 16,
2949
+ "aria-hidden": "true"
2950
+ })
2951
+ }) })
2952
+ ]
2953
+ })
2954
+ }), showRecordCount && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2955
+ className: "text-[11px] font-bold tabular-nums text-slate-400",
2956
+ "aria-live": "polite",
2890
2957
  children: [
2891
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2892
- type: "button",
2893
- onClick: () => onPageChange(page - 1),
2894
- disabled: page <= 1,
2895
- "aria-label": "Previous page",
2896
- className: "flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 disabled:cursor-not-allowed disabled:opacity-40",
2897
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronLeft, {
2898
- size: 16,
2899
- "aria-hidden": "true"
2900
- })
2901
- }) }),
2902
- pageNumbers.map((n, idx) => n === "ellipsis" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
2903
- "aria-hidden": "true",
2904
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2905
- className: "flex h-9 w-9 items-center justify-center text-xs font-bold text-slate-300",
2906
- children: "…"
2907
- })
2908
- }, `ellipsis-${idx}`) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2909
- type: "button",
2910
- onClick: () => onPageChange(n),
2911
- "aria-label": `Page ${n}`,
2912
- "aria-current": n === page ? "page" : void 0,
2913
- className: ["flex h-9 w-9 items-center justify-center rounded-md text-xs font-bold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30", n === page ? "bg-brand-blue text-white shadow-md shadow-brand-blue/20" : "border border-slate-200 bg-white text-slate-500 hover:border-slate-300 hover:text-slate-800"].filter(Boolean).join(" "),
2914
- children: n
2915
- }) }, n)),
2916
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2917
- type: "button",
2918
- onClick: () => onPageChange(page + 1),
2919
- disabled: page >= totalPages,
2920
- "aria-label": "Next page",
2921
- className: "flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 disabled:cursor-not-allowed disabled:opacity-40",
2922
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
2923
- size: 16,
2924
- "aria-hidden": "true"
2925
- })
2926
- }) })
2958
+ "Showing ",
2959
+ rangeStart,
2960
+ "–",
2961
+ rangeEnd,
2962
+ " of ",
2963
+ totalItems,
2964
+ " ",
2965
+ itemLabel
2927
2966
  ]
2928
- })
2967
+ })]
2929
2968
  });
2930
2969
  }
2931
2970
  //#endregion
@@ -7528,6 +7567,211 @@ function StatusDot({ status, label, className = "" }) {
7528
7567
  });
7529
7568
  }
7530
7569
  //#endregion
7570
+ //#region src/ui-library/gallery/column-manager/ColumnManager.tsx
7571
+ /**
7572
+ * ColumnManager — Design System Component
7573
+ *
7574
+ * A dropdown panel for managing table column visibility and order.
7575
+ * Provides checkboxes to show/hide individual columns and up/down
7576
+ * buttons to reorder them. Includes a reset action.
7577
+ *
7578
+ * Purely presentational — all state is owned by the caller.
7579
+ *
7580
+ * Usage:
7581
+ * const [hidden, setHidden] = useState<string[]>([]);
7582
+ * const [order, setOrder] = useState<string[] | null>(null);
7583
+ *
7584
+ * <ColumnManager
7585
+ * columns={[{ key: 'name', label: 'Name' }, { key: 'dept', label: 'Department' }]}
7586
+ * hiddenColumnKeys={hidden}
7587
+ * columnOrder={order}
7588
+ * onHiddenColumnKeysChange={setHidden}
7589
+ * onColumnOrderChange={setOrder}
7590
+ * onReset={() => { setHidden([]); setOrder(null); }}
7591
+ * />
7592
+ *
7593
+ * Accessibility:
7594
+ * - Trigger button has aria-expanded and aria-controls.
7595
+ * - Panel has role="region" and aria-label.
7596
+ * - Each checkbox is labelled via htmlFor + id.
7597
+ * - Reorder buttons have descriptive aria-labels.
7598
+ * - Panel closes on Escape, focus returns to the trigger.
7599
+ */
7600
+ /**
7601
+ * Apply a stored column order to the runtime column array.
7602
+ *
7603
+ * Rules:
7604
+ * - Keys in `order` that don't exist in `columns` are silently ignored
7605
+ * (forward compatibility when columns are removed).
7606
+ * - Columns not present in `order` are appended at the end in their
7607
+ * original relative order (forward compatibility when columns are added).
7608
+ * - When `order` is null the original array is returned unchanged.
7609
+ */
7610
+ function applyColumnOrder(columns, order) {
7611
+ if (!order) return columns;
7612
+ const byKey = new Map(columns.map((c) => [c.key, c]));
7613
+ const seen = /* @__PURE__ */ new Set();
7614
+ const result = [];
7615
+ for (const key of order) {
7616
+ const col = byKey.get(key);
7617
+ if (col) {
7618
+ result.push(col);
7619
+ seen.add(key);
7620
+ }
7621
+ }
7622
+ for (const col of columns) if (!seen.has(col.key)) result.push(col);
7623
+ return result;
7624
+ }
7625
+ function ColumnManager({ columns, hiddenColumnKeys, columnOrder, onHiddenColumnKeysChange, onColumnOrderChange, onReset, triggerLabel = "Columns" }) {
7626
+ const [open, setOpen] = (0, react.useState)(false);
7627
+ const triggerRef = (0, react.useRef)(null);
7628
+ const panelRef = (0, react.useRef)(null);
7629
+ const panelId = "cm-panel";
7630
+ const orderedColumns = applyColumnOrder(columns, columnOrder);
7631
+ const hiddenSet = new Set(hiddenColumnKeys);
7632
+ const hiddenCount = hiddenColumnKeys.length;
7633
+ (0, react.useEffect)(() => {
7634
+ if (!open) return;
7635
+ const handlePointerDown = (e) => {
7636
+ if (triggerRef.current?.contains(e.target) || panelRef.current?.contains(e.target)) return;
7637
+ setOpen(false);
7638
+ };
7639
+ document.addEventListener("pointerdown", handlePointerDown);
7640
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
7641
+ }, [open]);
7642
+ (0, react.useEffect)(() => {
7643
+ if (!open) return;
7644
+ const handleKeyDown = (e) => {
7645
+ if (e.key === "Escape") {
7646
+ setOpen(false);
7647
+ triggerRef.current?.focus();
7648
+ }
7649
+ };
7650
+ document.addEventListener("keydown", handleKeyDown);
7651
+ return () => document.removeEventListener("keydown", handleKeyDown);
7652
+ }, [open]);
7653
+ function toggleColumn(key) {
7654
+ onHiddenColumnKeysChange(hiddenSet.has(key) ? hiddenColumnKeys.filter((k) => k !== key) : [...hiddenColumnKeys, key]);
7655
+ }
7656
+ function moveUp(index) {
7657
+ if (index === 0) return;
7658
+ const next = [...orderedColumns];
7659
+ [next[index - 1], next[index]] = [next[index], next[index - 1]];
7660
+ onColumnOrderChange(next.map((c) => c.key));
7661
+ }
7662
+ function moveDown(index) {
7663
+ if (index === orderedColumns.length - 1) return;
7664
+ const next = [...orderedColumns];
7665
+ [next[index], next[index + 1]] = [next[index + 1], next[index]];
7666
+ onColumnOrderChange(next.map((c) => c.key));
7667
+ }
7668
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7669
+ className: "relative inline-block",
7670
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7671
+ ref: triggerRef,
7672
+ type: "button",
7673
+ "aria-expanded": open,
7674
+ "aria-controls": open ? panelId : void 0,
7675
+ onClick: () => setOpen((v) => !v),
7676
+ className: "inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-bold text-slate-600 shadow-sm transition hover:border-slate-300 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
7677
+ children: [
7678
+ triggerLabel,
7679
+ hiddenCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7680
+ className: "rounded-full bg-brand-blue/10 px-1.5 py-0.5 font-bold text-brand-blue",
7681
+ children: [hiddenCount, " hidden"]
7682
+ }),
7683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {
7684
+ size: 13,
7685
+ className: `transition-transform ${open ? "rotate-180" : ""}`,
7686
+ "aria-hidden": "true"
7687
+ })
7688
+ ]
7689
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7690
+ id: panelId,
7691
+ ref: panelRef,
7692
+ role: "region",
7693
+ "aria-label": "Manage columns",
7694
+ className: "absolute right-0 top-full z-30 mt-1 flex w-64 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-xl",
7695
+ style: { maxHeight: 400 },
7696
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7697
+ className: "flex shrink-0 items-center justify-between border-b border-slate-100 px-4 py-2.5",
7698
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7699
+ className: "text-xs font-bold text-slate-700",
7700
+ children: "Columns"
7701
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7702
+ type: "button",
7703
+ onClick: () => {
7704
+ onReset();
7705
+ setOpen(false);
7706
+ },
7707
+ className: "rounded-md px-2 py-1 text-[11px] font-bold text-slate-400 hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-blue/30 transition",
7708
+ children: "Reset"
7709
+ })]
7710
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
7711
+ className: "overflow-y-auto",
7712
+ role: "list",
7713
+ children: orderedColumns.map((col, idx) => {
7714
+ const visible = !hiddenSet.has(col.key);
7715
+ const checkboxId = `cm-col-${col.key}`;
7716
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
7717
+ className: "flex items-center gap-2 border-b border-slate-50 px-3 py-2 last:border-0",
7718
+ children: [
7719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7720
+ type: "checkbox",
7721
+ id: checkboxId,
7722
+ checked: visible,
7723
+ onChange: () => toggleColumn(col.key),
7724
+ className: "h-3.5 w-3.5 rounded accent-brand-blue focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-blue/30"
7725
+ }),
7726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
7727
+ htmlFor: checkboxId,
7728
+ className: `flex-1 cursor-pointer text-xs font-medium ${visible ? "text-slate-700" : "text-slate-400"}`,
7729
+ children: col.label
7730
+ }),
7731
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7732
+ className: "flex shrink-0 gap-0.5",
7733
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7734
+ type: "button",
7735
+ "aria-label": `Move ${col.label} up`,
7736
+ disabled: idx === 0,
7737
+ onClick: () => moveUp(idx),
7738
+ className: "rounded p-1 text-slate-300 hover:bg-slate-100 hover:text-slate-600 disabled:cursor-not-allowed disabled:opacity-30 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-blue/30 transition",
7739
+ children: "▲"
7740
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7741
+ type: "button",
7742
+ "aria-label": `Move ${col.label} down`,
7743
+ disabled: idx === orderedColumns.length - 1,
7744
+ onClick: () => moveDown(idx),
7745
+ className: "rounded p-1 text-slate-300 hover:bg-slate-100 hover:text-slate-600 disabled:cursor-not-allowed disabled:opacity-30 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-blue/30 transition",
7746
+ children: "▼"
7747
+ })]
7748
+ })
7749
+ ]
7750
+ }, col.key);
7751
+ })
7752
+ })]
7753
+ })]
7754
+ });
7755
+ }
7756
+ //#endregion
7757
+ //#region src/ui-library/gallery/status-dot/MaskedValue.tsx
7758
+ function MaskedValue({ value, masked, maskText = "••••• •••••", maskLabel = "Value restricted", className = "" }) {
7759
+ if (!masked) return className ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7760
+ className,
7761
+ children: value
7762
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: value });
7763
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7764
+ "aria-label": maskLabel,
7765
+ title: maskLabel,
7766
+ className: [
7767
+ "inline-flex select-none items-center rounded-md bg-slate-100 px-2 py-0.5",
7768
+ "font-mono text-xs font-medium text-slate-400 tracking-wider",
7769
+ className
7770
+ ].filter(Boolean).join(" "),
7771
+ children: maskText
7772
+ });
7773
+ }
7774
+ //#endregion
7531
7775
  //#region src/ui-library/gallery/status-actions/statusActions.ts
7532
7776
  /**
7533
7777
  * statusActions — Gallery-native lifecycle transition utilities.
@@ -7732,6 +7976,7 @@ exports.CardContent = CardContent;
7732
7976
  exports.CardFooter = CardFooter;
7733
7977
  exports.CardHeader = CardHeader;
7734
7978
  exports.Checkbox = Checkbox;
7979
+ exports.ColumnManager = ColumnManager;
7735
7980
  exports.Combobox = Combobox;
7736
7981
  exports.CommandPalette = CommandPalette;
7737
7982
  exports.ConfirmAction = ConfirmAction;
@@ -7759,6 +8004,7 @@ exports.Input = Input;
7759
8004
  exports.IsoCubeBlock = IsoCubeBlock;
7760
8005
  exports.Kanban = Kanban;
7761
8006
  exports.LineChart = LineChart;
8007
+ exports.MaskedValue = MaskedValue;
7762
8008
  exports.Menu = Menu;
7763
8009
  exports.Modal = Modal;
7764
8010
  exports.OrgChart = OrgChart;
@@ -7818,6 +8064,7 @@ exports.Toolbar = Toolbar;
7818
8064
  exports.Tooltip = Tooltip;
7819
8065
  exports.TreeView = TreeView;
7820
8066
  exports.ValidationSummary = ValidationSummary;
8067
+ exports.applyColumnOrder = applyColumnOrder;
7821
8068
  Object.defineProperty(exports, "colors", {
7822
8069
  enumerable: true,
7823
8070
  get: function() {
package/lib/index.mjs CHANGED
@@ -1450,7 +1450,17 @@ function SegmentedControl({ options, defaultValue, value: controlledValue, onVal
1450
1450
  }
1451
1451
  //#endregion
1452
1452
  //#region src/ui-library/gallery/validation-summary/ValidationSummary.tsx
1453
- function ValidationSummary({ issues, heading = "Review these fields before saving", className = "" }) {
1453
+ function scrollToField(domId) {
1454
+ const el = document.getElementById(domId);
1455
+ if (!el) return;
1456
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1457
+ el.scrollIntoView({
1458
+ behavior: prefersReducedMotion ? "auto" : "smooth",
1459
+ block: "center"
1460
+ });
1461
+ (el.querySelector("input,select,textarea,[tabindex]") ?? el).focus();
1462
+ }
1463
+ function ValidationSummary({ issues, heading = "Review these fields before saving", fieldIds, fieldLabels, className = "" }) {
1454
1464
  if (issues.length === 0) return null;
1455
1465
  return /* @__PURE__ */ jsxs("div", {
1456
1466
  role: "alert",
@@ -1460,14 +1470,24 @@ function ValidationSummary({ issues, heading = "Review these fields before savin
1460
1470
  children: heading
1461
1471
  }), /* @__PURE__ */ jsx("ul", {
1462
1472
  className: "mt-2 list-disc space-y-1 pl-5",
1463
- children: issues.map((issue) => /* @__PURE__ */ jsxs("li", { children: [
1464
- /* @__PURE__ */ jsxs("span", {
1465
- className: "font-semibold",
1466
- children: [issue.field, ":"]
1467
- }),
1468
- " ",
1469
- issue.message
1470
- ] }, `${issue.field}:${issue.message}`))
1473
+ children: issues.map((issue) => {
1474
+ const displayLabel = fieldLabels?.[issue.field] ?? issue.field;
1475
+ const domId = fieldIds?.[issue.field];
1476
+ const label = /* @__PURE__ */ jsxs(Fragment, { children: [
1477
+ /* @__PURE__ */ jsxs("span", {
1478
+ className: "font-semibold",
1479
+ children: [displayLabel, ":"]
1480
+ }),
1481
+ " ",
1482
+ issue.message
1483
+ ] });
1484
+ return /* @__PURE__ */ jsx("li", { children: domId ? /* @__PURE__ */ jsx("button", {
1485
+ type: "button",
1486
+ onClick: () => scrollToField(domId),
1487
+ className: "underline decoration-amber-500/60 underline-offset-2 hover:decoration-amber-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-amber-600 rounded text-left",
1488
+ children: label
1489
+ }) : label }, `${issue.field}:${issue.message}`);
1490
+ })
1471
1491
  })]
1472
1492
  });
1473
1493
  }
@@ -2854,53 +2874,72 @@ function getPageNumbers(current, total) {
2854
2874
  pages.push(total);
2855
2875
  return pages;
2856
2876
  }
2857
- function Pagination({ page, totalPages, onPageChange, ariaLabel = "Pagination" }) {
2858
- if (totalPages <= 1) return null;
2877
+ function Pagination({ page, totalPages, onPageChange, ariaLabel = "Pagination", totalItems, pageSize, itemLabel = "items" }) {
2878
+ if (totalPages <= 1 && !totalItems) return null;
2879
+ const showRecordCount = typeof totalItems === "number" && typeof pageSize === "number";
2880
+ const rangeStart = showRecordCount ? (page - 1) * pageSize + 1 : null;
2881
+ const rangeEnd = showRecordCount ? Math.min(page * pageSize, totalItems) : null;
2859
2882
  const pageNumbers = getPageNumbers(page, totalPages);
2860
- return /* @__PURE__ */ jsx("nav", {
2861
- "aria-label": ariaLabel,
2862
- children: /* @__PURE__ */ jsxs("ol", {
2863
- className: "list-none flex items-center gap-1",
2864
- role: "list",
2883
+ return /* @__PURE__ */ jsxs("div", {
2884
+ className: "flex flex-col items-center gap-1.5",
2885
+ children: [totalPages > 1 && /* @__PURE__ */ jsx("nav", {
2886
+ "aria-label": ariaLabel,
2887
+ children: /* @__PURE__ */ jsxs("ol", {
2888
+ className: "list-none flex items-center gap-1",
2889
+ role: "list",
2890
+ children: [
2891
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
2892
+ type: "button",
2893
+ onClick: () => onPageChange(page - 1),
2894
+ disabled: page <= 1,
2895
+ "aria-label": "Previous page",
2896
+ className: "flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 disabled:cursor-not-allowed disabled:opacity-40",
2897
+ children: /* @__PURE__ */ jsx(ChevronLeft, {
2898
+ size: 16,
2899
+ "aria-hidden": "true"
2900
+ })
2901
+ }) }),
2902
+ pageNumbers.map((n, idx) => n === "ellipsis" ? /* @__PURE__ */ jsx("li", {
2903
+ "aria-hidden": "true",
2904
+ children: /* @__PURE__ */ jsx("span", {
2905
+ className: "flex h-9 w-9 items-center justify-center text-xs font-bold text-slate-300",
2906
+ children: "…"
2907
+ })
2908
+ }, `ellipsis-${idx}`) : /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
2909
+ type: "button",
2910
+ onClick: () => onPageChange(n),
2911
+ "aria-label": `Page ${n}`,
2912
+ "aria-current": n === page ? "page" : void 0,
2913
+ className: ["flex h-9 w-9 items-center justify-center rounded-md text-xs font-bold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30", n === page ? "bg-brand-blue text-white shadow-md shadow-brand-blue/20" : "border border-slate-200 bg-white text-slate-500 hover:border-slate-300 hover:text-slate-800"].filter(Boolean).join(" "),
2914
+ children: n
2915
+ }) }, n)),
2916
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
2917
+ type: "button",
2918
+ onClick: () => onPageChange(page + 1),
2919
+ disabled: page >= totalPages,
2920
+ "aria-label": "Next page",
2921
+ className: "flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 disabled:cursor-not-allowed disabled:opacity-40",
2922
+ children: /* @__PURE__ */ jsx(ChevronRight, {
2923
+ size: 16,
2924
+ "aria-hidden": "true"
2925
+ })
2926
+ }) })
2927
+ ]
2928
+ })
2929
+ }), showRecordCount && /* @__PURE__ */ jsxs("p", {
2930
+ className: "text-[11px] font-bold tabular-nums text-slate-400",
2931
+ "aria-live": "polite",
2865
2932
  children: [
2866
- /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
2867
- type: "button",
2868
- onClick: () => onPageChange(page - 1),
2869
- disabled: page <= 1,
2870
- "aria-label": "Previous page",
2871
- className: "flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 disabled:cursor-not-allowed disabled:opacity-40",
2872
- children: /* @__PURE__ */ jsx(ChevronLeft, {
2873
- size: 16,
2874
- "aria-hidden": "true"
2875
- })
2876
- }) }),
2877
- pageNumbers.map((n, idx) => n === "ellipsis" ? /* @__PURE__ */ jsx("li", {
2878
- "aria-hidden": "true",
2879
- children: /* @__PURE__ */ jsx("span", {
2880
- className: "flex h-9 w-9 items-center justify-center text-xs font-bold text-slate-300",
2881
- children: "…"
2882
- })
2883
- }, `ellipsis-${idx}`) : /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
2884
- type: "button",
2885
- onClick: () => onPageChange(n),
2886
- "aria-label": `Page ${n}`,
2887
- "aria-current": n === page ? "page" : void 0,
2888
- className: ["flex h-9 w-9 items-center justify-center rounded-md text-xs font-bold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30", n === page ? "bg-brand-blue text-white shadow-md shadow-brand-blue/20" : "border border-slate-200 bg-white text-slate-500 hover:border-slate-300 hover:text-slate-800"].filter(Boolean).join(" "),
2889
- children: n
2890
- }) }, n)),
2891
- /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
2892
- type: "button",
2893
- onClick: () => onPageChange(page + 1),
2894
- disabled: page >= totalPages,
2895
- "aria-label": "Next page",
2896
- className: "flex h-9 w-9 items-center justify-center rounded-md border border-slate-200 bg-white text-slate-500 transition hover:border-slate-300 hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30 disabled:cursor-not-allowed disabled:opacity-40",
2897
- children: /* @__PURE__ */ jsx(ChevronRight, {
2898
- size: 16,
2899
- "aria-hidden": "true"
2900
- })
2901
- }) })
2933
+ "Showing ",
2934
+ rangeStart,
2935
+ "–",
2936
+ rangeEnd,
2937
+ " of ",
2938
+ totalItems,
2939
+ " ",
2940
+ itemLabel
2902
2941
  ]
2903
- })
2942
+ })]
2904
2943
  });
2905
2944
  }
2906
2945
  //#endregion
@@ -7503,6 +7542,211 @@ function StatusDot({ status, label, className = "" }) {
7503
7542
  });
7504
7543
  }
7505
7544
  //#endregion
7545
+ //#region src/ui-library/gallery/column-manager/ColumnManager.tsx
7546
+ /**
7547
+ * ColumnManager — Design System Component
7548
+ *
7549
+ * A dropdown panel for managing table column visibility and order.
7550
+ * Provides checkboxes to show/hide individual columns and up/down
7551
+ * buttons to reorder them. Includes a reset action.
7552
+ *
7553
+ * Purely presentational — all state is owned by the caller.
7554
+ *
7555
+ * Usage:
7556
+ * const [hidden, setHidden] = useState<string[]>([]);
7557
+ * const [order, setOrder] = useState<string[] | null>(null);
7558
+ *
7559
+ * <ColumnManager
7560
+ * columns={[{ key: 'name', label: 'Name' }, { key: 'dept', label: 'Department' }]}
7561
+ * hiddenColumnKeys={hidden}
7562
+ * columnOrder={order}
7563
+ * onHiddenColumnKeysChange={setHidden}
7564
+ * onColumnOrderChange={setOrder}
7565
+ * onReset={() => { setHidden([]); setOrder(null); }}
7566
+ * />
7567
+ *
7568
+ * Accessibility:
7569
+ * - Trigger button has aria-expanded and aria-controls.
7570
+ * - Panel has role="region" and aria-label.
7571
+ * - Each checkbox is labelled via htmlFor + id.
7572
+ * - Reorder buttons have descriptive aria-labels.
7573
+ * - Panel closes on Escape, focus returns to the trigger.
7574
+ */
7575
+ /**
7576
+ * Apply a stored column order to the runtime column array.
7577
+ *
7578
+ * Rules:
7579
+ * - Keys in `order` that don't exist in `columns` are silently ignored
7580
+ * (forward compatibility when columns are removed).
7581
+ * - Columns not present in `order` are appended at the end in their
7582
+ * original relative order (forward compatibility when columns are added).
7583
+ * - When `order` is null the original array is returned unchanged.
7584
+ */
7585
+ function applyColumnOrder(columns, order) {
7586
+ if (!order) return columns;
7587
+ const byKey = new Map(columns.map((c) => [c.key, c]));
7588
+ const seen = /* @__PURE__ */ new Set();
7589
+ const result = [];
7590
+ for (const key of order) {
7591
+ const col = byKey.get(key);
7592
+ if (col) {
7593
+ result.push(col);
7594
+ seen.add(key);
7595
+ }
7596
+ }
7597
+ for (const col of columns) if (!seen.has(col.key)) result.push(col);
7598
+ return result;
7599
+ }
7600
+ function ColumnManager({ columns, hiddenColumnKeys, columnOrder, onHiddenColumnKeysChange, onColumnOrderChange, onReset, triggerLabel = "Columns" }) {
7601
+ const [open, setOpen] = useState(false);
7602
+ const triggerRef = useRef(null);
7603
+ const panelRef = useRef(null);
7604
+ const panelId = "cm-panel";
7605
+ const orderedColumns = applyColumnOrder(columns, columnOrder);
7606
+ const hiddenSet = new Set(hiddenColumnKeys);
7607
+ const hiddenCount = hiddenColumnKeys.length;
7608
+ useEffect(() => {
7609
+ if (!open) return;
7610
+ const handlePointerDown = (e) => {
7611
+ if (triggerRef.current?.contains(e.target) || panelRef.current?.contains(e.target)) return;
7612
+ setOpen(false);
7613
+ };
7614
+ document.addEventListener("pointerdown", handlePointerDown);
7615
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
7616
+ }, [open]);
7617
+ useEffect(() => {
7618
+ if (!open) return;
7619
+ const handleKeyDown = (e) => {
7620
+ if (e.key === "Escape") {
7621
+ setOpen(false);
7622
+ triggerRef.current?.focus();
7623
+ }
7624
+ };
7625
+ document.addEventListener("keydown", handleKeyDown);
7626
+ return () => document.removeEventListener("keydown", handleKeyDown);
7627
+ }, [open]);
7628
+ function toggleColumn(key) {
7629
+ onHiddenColumnKeysChange(hiddenSet.has(key) ? hiddenColumnKeys.filter((k) => k !== key) : [...hiddenColumnKeys, key]);
7630
+ }
7631
+ function moveUp(index) {
7632
+ if (index === 0) return;
7633
+ const next = [...orderedColumns];
7634
+ [next[index - 1], next[index]] = [next[index], next[index - 1]];
7635
+ onColumnOrderChange(next.map((c) => c.key));
7636
+ }
7637
+ function moveDown(index) {
7638
+ if (index === orderedColumns.length - 1) return;
7639
+ const next = [...orderedColumns];
7640
+ [next[index], next[index + 1]] = [next[index + 1], next[index]];
7641
+ onColumnOrderChange(next.map((c) => c.key));
7642
+ }
7643
+ return /* @__PURE__ */ jsxs("div", {
7644
+ className: "relative inline-block",
7645
+ children: [/* @__PURE__ */ jsxs("button", {
7646
+ ref: triggerRef,
7647
+ type: "button",
7648
+ "aria-expanded": open,
7649
+ "aria-controls": open ? panelId : void 0,
7650
+ onClick: () => setOpen((v) => !v),
7651
+ className: "inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-bold text-slate-600 shadow-sm transition hover:border-slate-300 hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
7652
+ children: [
7653
+ triggerLabel,
7654
+ hiddenCount > 0 && /* @__PURE__ */ jsxs("span", {
7655
+ className: "rounded-full bg-brand-blue/10 px-1.5 py-0.5 font-bold text-brand-blue",
7656
+ children: [hiddenCount, " hidden"]
7657
+ }),
7658
+ /* @__PURE__ */ jsx(ChevronDown, {
7659
+ size: 13,
7660
+ className: `transition-transform ${open ? "rotate-180" : ""}`,
7661
+ "aria-hidden": "true"
7662
+ })
7663
+ ]
7664
+ }), open && /* @__PURE__ */ jsxs("div", {
7665
+ id: panelId,
7666
+ ref: panelRef,
7667
+ role: "region",
7668
+ "aria-label": "Manage columns",
7669
+ className: "absolute right-0 top-full z-30 mt-1 flex w-64 flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-xl",
7670
+ style: { maxHeight: 400 },
7671
+ children: [/* @__PURE__ */ jsxs("div", {
7672
+ className: "flex shrink-0 items-center justify-between border-b border-slate-100 px-4 py-2.5",
7673
+ children: [/* @__PURE__ */ jsx("span", {
7674
+ className: "text-xs font-bold text-slate-700",
7675
+ children: "Columns"
7676
+ }), /* @__PURE__ */ jsx("button", {
7677
+ type: "button",
7678
+ onClick: () => {
7679
+ onReset();
7680
+ setOpen(false);
7681
+ },
7682
+ className: "rounded-md px-2 py-1 text-[11px] font-bold text-slate-400 hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-blue/30 transition",
7683
+ children: "Reset"
7684
+ })]
7685
+ }), /* @__PURE__ */ jsx("ul", {
7686
+ className: "overflow-y-auto",
7687
+ role: "list",
7688
+ children: orderedColumns.map((col, idx) => {
7689
+ const visible = !hiddenSet.has(col.key);
7690
+ const checkboxId = `cm-col-${col.key}`;
7691
+ return /* @__PURE__ */ jsxs("li", {
7692
+ className: "flex items-center gap-2 border-b border-slate-50 px-3 py-2 last:border-0",
7693
+ children: [
7694
+ /* @__PURE__ */ jsx("input", {
7695
+ type: "checkbox",
7696
+ id: checkboxId,
7697
+ checked: visible,
7698
+ onChange: () => toggleColumn(col.key),
7699
+ className: "h-3.5 w-3.5 rounded accent-brand-blue focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-blue/30"
7700
+ }),
7701
+ /* @__PURE__ */ jsx("label", {
7702
+ htmlFor: checkboxId,
7703
+ className: `flex-1 cursor-pointer text-xs font-medium ${visible ? "text-slate-700" : "text-slate-400"}`,
7704
+ children: col.label
7705
+ }),
7706
+ /* @__PURE__ */ jsxs("div", {
7707
+ className: "flex shrink-0 gap-0.5",
7708
+ children: [/* @__PURE__ */ jsx("button", {
7709
+ type: "button",
7710
+ "aria-label": `Move ${col.label} up`,
7711
+ disabled: idx === 0,
7712
+ onClick: () => moveUp(idx),
7713
+ className: "rounded p-1 text-slate-300 hover:bg-slate-100 hover:text-slate-600 disabled:cursor-not-allowed disabled:opacity-30 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-blue/30 transition",
7714
+ children: "▲"
7715
+ }), /* @__PURE__ */ jsx("button", {
7716
+ type: "button",
7717
+ "aria-label": `Move ${col.label} down`,
7718
+ disabled: idx === orderedColumns.length - 1,
7719
+ onClick: () => moveDown(idx),
7720
+ className: "rounded p-1 text-slate-300 hover:bg-slate-100 hover:text-slate-600 disabled:cursor-not-allowed disabled:opacity-30 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-brand-blue/30 transition",
7721
+ children: "▼"
7722
+ })]
7723
+ })
7724
+ ]
7725
+ }, col.key);
7726
+ })
7727
+ })]
7728
+ })]
7729
+ });
7730
+ }
7731
+ //#endregion
7732
+ //#region src/ui-library/gallery/status-dot/MaskedValue.tsx
7733
+ function MaskedValue({ value, masked, maskText = "••••• •••••", maskLabel = "Value restricted", className = "" }) {
7734
+ if (!masked) return className ? /* @__PURE__ */ jsx("span", {
7735
+ className,
7736
+ children: value
7737
+ }) : /* @__PURE__ */ jsx(Fragment, { children: value });
7738
+ return /* @__PURE__ */ jsx("span", {
7739
+ "aria-label": maskLabel,
7740
+ title: maskLabel,
7741
+ className: [
7742
+ "inline-flex select-none items-center rounded-md bg-slate-100 px-2 py-0.5",
7743
+ "font-mono text-xs font-medium text-slate-400 tracking-wider",
7744
+ className
7745
+ ].filter(Boolean).join(" "),
7746
+ children: maskText
7747
+ });
7748
+ }
7749
+ //#endregion
7506
7750
  //#region src/ui-library/gallery/status-actions/statusActions.ts
7507
7751
  /**
7508
7752
  * statusActions — Gallery-native lifecycle transition utilities.
@@ -7686,4 +7930,4 @@ var shadow = {
7686
7930
  overlay: "shadow-xl"
7687
7931
  };
7688
7932
  //#endregion
7689
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, ActivityFeed, Alert, ApprovalTimeline, AreaChart, Avatar, Badge, BarChart, BarList, Breadcrumb, Button, CHART_PALETTE, Calendar, Card, CardContent, CardFooter, CardHeader, Checkbox, Combobox, CommandPalette, ConfirmAction, ConfirmDialog, DatePicker, DescriptionItem, DescriptionList, Dialog, DialogBody, DialogFooter, Divider, Drawer, Dropdown, EmployeeCard, EmptyState, ErrorBanner, Field, FieldContext, FieldInput, FileUpload, FilterChip, Heatmap, IconButton, Input, IsoCubeBlock, Kanban, LineChart, Menu, Modal, OrgChart, OrgUnitTree, OtpInput, PafTemplate, PageHeader, Pagination, PayslipTemplate, PermissionMatrix, Popover, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioItem, ReportTemplate, STATUS_PAGE_PRESETS, Section, SegmentedControl, Select, SidebarGroup, SidebarItem, SidebarNav, SidebarSection, Skeleton, SkeletonAvatar, SkeletonCard, SkeletonTableRow, SkeletonText, Spinner, Statistic, StatusBadge, StatusDot, StatusPage, Stepper, Switch, Tab, TabList, TabPanel, TabPanels, Tabs, Textarea, TimekeepingTemplate, Timeline, TimelineEmptyState, TimelineFooter, TimelineItem, TimelineValueCard, Toaster, Toolbar, Tooltip, TreeView, ValidationSummary, colors_exports as colors, dimension_exports as dimension, radius_exports as radius, shadow_exports as shadow, statusMenuItems, toast, typography_exports as typography, useFieldContext };
7933
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, ActivityFeed, Alert, ApprovalTimeline, AreaChart, Avatar, Badge, BarChart, BarList, Breadcrumb, Button, CHART_PALETTE, Calendar, Card, CardContent, CardFooter, CardHeader, Checkbox, ColumnManager, Combobox, CommandPalette, ConfirmAction, ConfirmDialog, DatePicker, DescriptionItem, DescriptionList, Dialog, DialogBody, DialogFooter, Divider, Drawer, Dropdown, EmployeeCard, EmptyState, ErrorBanner, Field, FieldContext, FieldInput, FileUpload, FilterChip, Heatmap, IconButton, Input, IsoCubeBlock, Kanban, LineChart, MaskedValue, Menu, Modal, OrgChart, OrgUnitTree, OtpInput, PafTemplate, PageHeader, Pagination, PayslipTemplate, PermissionMatrix, Popover, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioItem, ReportTemplate, STATUS_PAGE_PRESETS, Section, SegmentedControl, Select, SidebarGroup, SidebarItem, SidebarNav, SidebarSection, Skeleton, SkeletonAvatar, SkeletonCard, SkeletonTableRow, SkeletonText, Spinner, Statistic, StatusBadge, StatusDot, StatusPage, Stepper, Switch, Tab, TabList, TabPanel, TabPanels, Tabs, Textarea, TimekeepingTemplate, Timeline, TimelineEmptyState, TimelineFooter, TimelineItem, TimelineValueCard, Toaster, Toolbar, Tooltip, TreeView, ValidationSummary, applyColumnOrder, colors_exports as colors, dimension_exports as dimension, radius_exports as radius, shadow_exports as shadow, statusMenuItems, toast, typography_exports as typography, useFieldContext };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ColumnManager — Design System Component
3
+ *
4
+ * A dropdown panel for managing table column visibility and order.
5
+ * Provides checkboxes to show/hide individual columns and up/down
6
+ * buttons to reorder them. Includes a reset action.
7
+ *
8
+ * Purely presentational — all state is owned by the caller.
9
+ *
10
+ * Usage:
11
+ * const [hidden, setHidden] = useState<string[]>([]);
12
+ * const [order, setOrder] = useState<string[] | null>(null);
13
+ *
14
+ * <ColumnManager
15
+ * columns={[{ key: 'name', label: 'Name' }, { key: 'dept', label: 'Department' }]}
16
+ * hiddenColumnKeys={hidden}
17
+ * columnOrder={order}
18
+ * onHiddenColumnKeysChange={setHidden}
19
+ * onColumnOrderChange={setOrder}
20
+ * onReset={() => { setHidden([]); setOrder(null); }}
21
+ * />
22
+ *
23
+ * Accessibility:
24
+ * - Trigger button has aria-expanded and aria-controls.
25
+ * - Panel has role="region" and aria-label.
26
+ * - Each checkbox is labelled via htmlFor + id.
27
+ * - Reorder buttons have descriptive aria-labels.
28
+ * - Panel closes on Escape, focus returns to the trigger.
29
+ */
30
+ export interface ColumnManagerColumn {
31
+ /** Unique key matching the data column identifier. */
32
+ key: string;
33
+ /** Human-readable column label shown in the panel. */
34
+ label: string;
35
+ }
36
+ export interface ColumnManagerProps {
37
+ /** All available columns in their default order. */
38
+ columns: ColumnManagerColumn[];
39
+ /** Keys of currently hidden columns. */
40
+ hiddenColumnKeys: string[];
41
+ /**
42
+ * Current column order as an array of keys.
43
+ * Pass null to use the default order from `columns`.
44
+ */
45
+ columnOrder: string[] | null;
46
+ /** Called with the new hidden-keys array when visibility changes. */
47
+ onHiddenColumnKeysChange: (keys: string[]) => void;
48
+ /** Called with the new key order when a column is moved. */
49
+ onColumnOrderChange: (order: string[]) => void;
50
+ /** Called when the user clicks Reset — caller restores defaults. */
51
+ onReset: () => void;
52
+ /** Label for the trigger button. Default: "Columns" */
53
+ triggerLabel?: string;
54
+ }
55
+ /**
56
+ * Apply a stored column order to the runtime column array.
57
+ *
58
+ * Rules:
59
+ * - Keys in `order` that don't exist in `columns` are silently ignored
60
+ * (forward compatibility when columns are removed).
61
+ * - Columns not present in `order` are appended at the end in their
62
+ * original relative order (forward compatibility when columns are added).
63
+ * - When `order` is null the original array is returned unchanged.
64
+ */
65
+ export declare function applyColumnOrder(columns: ColumnManagerColumn[], order: string[] | null): ColumnManagerColumn[];
66
+ export declare function ColumnManager({ columns, hiddenColumnKeys, columnOrder, onHiddenColumnKeysChange, onColumnOrderChange, onReset, triggerLabel, }: ColumnManagerProps): import("react").JSX.Element;
@@ -23,5 +23,21 @@ export interface PaginationProps {
23
23
  onPageChange: (page: number) => void;
24
24
  /** Accessible label for the navigation landmark (default: "Pagination") */
25
25
  ariaLabel?: string;
26
+ /**
27
+ * Total number of records across all pages.
28
+ * When provided alongside `pageSize`, renders a "Showing X–Y of Z {itemLabel}" line.
29
+ */
30
+ totalItems?: number;
31
+ /**
32
+ * Number of records per page.
33
+ * Required together with `totalItems` to compute the Showing X–Y range.
34
+ */
35
+ pageSize?: number;
36
+ /**
37
+ * Label for the item type shown in the record count footer.
38
+ * Example: "employees", "records", "results"
39
+ * Default: "items"
40
+ */
41
+ itemLabel?: string;
26
42
  }
27
- export declare function Pagination({ page, totalPages, onPageChange, ariaLabel, }: PaginationProps): import("react").JSX.Element | null;
43
+ export declare function Pagination({ page, totalPages, onPageChange, ariaLabel, totalItems, pageSize, itemLabel, }: PaginationProps): import("react").JSX.Element | null;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * MaskedValue — Design System Component
3
+ *
4
+ * Renders a value or a redacted placeholder based on a boolean condition.
5
+ * Used when a value exists but should be hidden from the current viewer
6
+ * (e.g. salary data only visible to users with the appropriate permission).
7
+ *
8
+ * The masking decision is made by the caller — MaskedValue is purely
9
+ * presentational. It does not know about permissions, roles, or data fetching.
10
+ *
11
+ * Usage:
12
+ * // Show real value
13
+ * <MaskedValue value={formatCurrency(salary)} masked={false} />
14
+ *
15
+ * // Hide value — consumer decides based on permission/context
16
+ * <MaskedValue
17
+ * value={formatCurrency(salary)}
18
+ * masked={!canViewSalary}
19
+ * maskLabel="Salary is restricted"
20
+ * />
21
+ *
22
+ * Accessibility:
23
+ * - When masked, the placeholder pill has aria-label={maskLabel} so
24
+ * screen readers announce the reason rather than "••••• •••••".
25
+ * - When unmasked, renders children directly — no extra ARIA.
26
+ */
27
+ import type { ReactNode } from 'react';
28
+ export interface MaskedValueProps {
29
+ /**
30
+ * The real value to display when not masked.
31
+ * Accepts any ReactNode — text, formatted currency, a Badge, etc.
32
+ */
33
+ value: ReactNode;
34
+ /**
35
+ * When true, renders the mask placeholder instead of value.
36
+ * When false, renders value directly.
37
+ */
38
+ masked: boolean;
39
+ /**
40
+ * Text shown in the masked placeholder pill.
41
+ * Default: '••••• •••••'
42
+ */
43
+ maskText?: string;
44
+ /**
45
+ * Accessible label for the masked placeholder — announces WHY
46
+ * the value is hidden, not just that it is hidden.
47
+ * Example: "Salary restricted — requires elevated access"
48
+ * Default: "Value restricted"
49
+ */
50
+ maskLabel?: string;
51
+ /** Additional class on the root element (both masked and unmasked states). */
52
+ className?: string;
53
+ }
54
+ export declare function MaskedValue({ value, masked, maskText, maskLabel, className, }: MaskedValueProps): import("react").JSX.Element;
@@ -17,6 +17,13 @@
17
17
  * setIssues([{ field: 'Employee ID', message: 'is required' }]);
18
18
  * // Render:
19
19
  * <ValidationSummary issues={issues} />
20
+ *
21
+ * // With scroll-to-field (wizard steps):
22
+ * <ValidationSummary
23
+ * issues={issues}
24
+ * fieldIds={{ 'Employee ID': 'employee-id-input', 'Position': 'position-select' }}
25
+ * fieldLabels={{ 'Employee ID': 'Employee ID', 'Position': 'Position Title' }}
26
+ * />
20
27
  */
21
28
  export interface ValidationIssue {
22
29
  /** Human-readable field label. */
@@ -32,8 +39,23 @@ export interface ValidationSummaryProps {
32
39
  * Default: "Review these fields before saving"
33
40
  */
34
41
  heading?: string;
42
+ /**
43
+ * Maps issue.field → the DOM element id that the user should be
44
+ * scrolled to when they click the error.
45
+ * When provided, each matching issue renders as a clickable link
46
+ * that scrolls to and focuses the target element.
47
+ * Issues with no matching key render as plain text.
48
+ */
49
+ fieldIds?: Record<string, string>;
50
+ /**
51
+ * Maps issue.field → a human-friendly display label.
52
+ * When provided, the display label replaces the raw field key in
53
+ * the rendered message. Useful when the field key is a code/ID and
54
+ * the label is the visible form field name.
55
+ */
56
+ fieldLabels?: Record<string, string>;
35
57
  /** Additional class on the root element. */
36
58
  className?: string;
37
59
  }
38
- export declare function ValidationSummary({ issues, heading, className, }: ValidationSummaryProps): import("react").JSX.Element | null;
60
+ export declare function ValidationSummary({ issues, heading, fieldIds, fieldLabels, className, }: ValidationSummaryProps): import("react").JSX.Element | null;
39
61
  export type { ValidationIssue as ValidationSummaryIssue };
@@ -144,6 +144,10 @@ export { Statistic } from './gallery/stat-card/Statistic';
144
144
  export type { StatisticProps, StatisticTrend, StatisticVariant } from './gallery/stat-card/Statistic';
145
145
  export { StatusDot } from './gallery/status-dot/StatusDot';
146
146
  export type { StatusDotProps, StatusDotStatus } from './gallery/status-dot/StatusDot';
147
+ export { ColumnManager, applyColumnOrder } from './gallery/column-manager/ColumnManager';
148
+ export type { ColumnManagerProps, ColumnManagerColumn } from './gallery/column-manager/ColumnManager';
149
+ export { MaskedValue } from './gallery/status-dot/MaskedValue';
150
+ export type { MaskedValueProps } from './gallery/status-dot/MaskedValue';
147
151
  export { Toaster, toast } from 'sonner';
148
152
  export { statusMenuItems } from './gallery/status-actions/statusActions';
149
153
  export type { ActionMenuItem, Lifecycle, StatusTransitionRequest, StatusMenuOptions } from './gallery/status-actions/statusActions';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@diwauhris/ui",
3
3
  "private": false,
4
- "version": "1.7.0",
4
+ "version": "1.7.1",
5
5
  "type": "module",
6
6
  "description": "DIWA UHRIS UI component library — React + Tailwind CSS",
7
7
  "keywords": [