hs-uix 2.1.0 → 2.2.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.
Files changed (49) hide show
  1. package/README.md +3 -1
  2. package/common-components.d.ts +319 -68
  3. package/dist/calendar.js +397 -119
  4. package/dist/calendar.mjs +399 -119
  5. package/dist/common-components.js +3546 -88
  6. package/dist/common-components.mjs +3530 -84
  7. package/dist/datatable.js +108 -18
  8. package/dist/datatable.mjs +108 -18
  9. package/dist/experimental.js +2876 -0
  10. package/dist/experimental.mjs +2883 -0
  11. package/dist/feed.js +267 -38
  12. package/dist/feed.mjs +260 -37
  13. package/dist/filter.js +1379 -0
  14. package/dist/filter.mjs +1334 -0
  15. package/dist/form.js +222 -26
  16. package/dist/form.mjs +227 -27
  17. package/dist/index.js +3255 -353
  18. package/dist/index.mjs +3199 -344
  19. package/dist/kanban.js +282 -62
  20. package/dist/kanban.mjs +273 -61
  21. package/dist/safe.js +9207 -0
  22. package/dist/safe.mjs +9298 -0
  23. package/dist/utils.js +491 -75
  24. package/dist/utils.mjs +491 -75
  25. package/experimental.d.ts +1 -0
  26. package/filter.d.ts +1 -0
  27. package/index.d.ts +45 -3
  28. package/package.json +19 -1
  29. package/safe.d.ts +1 -0
  30. package/src/calendar/README.md +76 -5
  31. package/src/calendar/index.d.ts +108 -1
  32. package/src/common-components/README.md +140 -1
  33. package/src/datatable/README.md +0 -2
  34. package/src/experimental/README.md +126 -0
  35. package/src/experimental/index.d.ts +346 -0
  36. package/src/feed/README.md +69 -0
  37. package/src/feed/index.d.ts +103 -0
  38. package/src/filter/README.md +148 -0
  39. package/src/filter/index.d.ts +221 -0
  40. package/src/form/README.md +132 -4
  41. package/src/form/index.d.ts +82 -1
  42. package/src/kanban/README.md +119 -6
  43. package/src/kanban/index.d.ts +153 -2
  44. package/src/safe/README.md +108 -0
  45. package/src/safe/index.d.ts +158 -0
  46. package/src/utils/README.md +39 -0
  47. package/src/wizard/README.md +158 -0
  48. package/src/wizard/index.d.ts +138 -0
  49. package/utils.d.ts +17 -0
@@ -246,6 +246,7 @@ var HS_TEXT_COLOR = "#33475b";
246
246
  var HS_SUBTLE_BG = "#F5F8FA";
247
247
  var HS_MUTED_TEXT = "#7C98B6";
248
248
  var HS_NEUTRAL_CHIP = "#CBD6E2";
249
+ var SKELETON_FILL = "#DFE3EB";
249
250
  var HS_TAG_SUBTLE_BORDER = "#7C98B6";
250
251
  var HS_TAG_TEXT_COLOR = HS_TEXT_COLOR;
251
252
  var HS_TAG_FONT_SIZE = 12;
@@ -404,10 +405,169 @@ import React9, { useState as useState2, useMemo, useEffect as useEffect2, useCal
404
405
 
405
406
  // src/utils/query.js
406
407
  import Fuse from "fuse.js";
408
+ var getEmptyFilterValue = (filter) => {
409
+ const type = filter.type || "select";
410
+ if (type === "multiselect") return [];
411
+ if (type === "dateRange") return { from: null, to: null };
412
+ if (Object.prototype.hasOwnProperty.call(filter, "emptyValue")) return filter.emptyValue;
413
+ return "";
414
+ };
415
+ var isFilterActive = (filter, value) => {
416
+ const type = filter.type || "select";
417
+ if (type === "multiselect") return Array.isArray(value) && value.length > 0;
418
+ if (type === "dateRange") return value && (value.from || value.to);
419
+ if (value == null) return false;
420
+ if (Object.prototype.hasOwnProperty.call(filter, "emptyValue")) return value !== filter.emptyValue;
421
+ return !!value;
422
+ };
423
+ var formatDateChip = (dateObj) => {
424
+ if (!dateObj) return "";
425
+ const { year, month, date } = dateObj;
426
+ return new Intl.DateTimeFormat("en-US", {
427
+ month: "short",
428
+ day: "numeric",
429
+ year: "numeric"
430
+ }).format(new Date(year, month, date));
431
+ };
432
+ var dateToTimestamp = (dateObj) => {
433
+ if (!dateObj) return null;
434
+ return new Date(dateObj.year, dateObj.month, dateObj.date).getTime();
435
+ };
436
+ var getEmptyFilterValues = (filters, options = {}) => {
437
+ const out = {};
438
+ for (const filter of filters || []) {
439
+ out[filter.name] = typeof options.getEmptyValue === "function" ? options.getEmptyValue(filter) : getEmptyFilterValue(filter);
440
+ }
441
+ return out;
442
+ };
443
+ var resetFilterValues = (filters, values = {}, key = "all", options = {}) => {
444
+ if (key === "all") return getEmptyFilterValues(filters, options);
445
+ const filter = (filters || []).find((item) => item.name === key);
446
+ const emptyValue = filter ? typeof options.getEmptyValue === "function" ? options.getEmptyValue(filter) : getEmptyFilterValue(filter) : options.fallbackEmptyValue ?? "";
447
+ return { ...values || {}, [key]: emptyValue };
448
+ };
449
+ var findOptionLabel = (filter, value) => {
450
+ var _a;
451
+ return ((_a = (filter.options || []).find((option) => option.value === value)) == null ? void 0 : _a.label) || value;
452
+ };
453
+ var buildActiveFilterChips = (filters, values = {}, options = {}) => {
454
+ const chips = [];
455
+ const isActive = options.isFilterActive || isFilterActive;
456
+ const dateFormatter = options.formatDate || formatDateChip;
457
+ const dateJoiner = options.dateJoiner ?? " ";
458
+ for (const filter of filters || []) {
459
+ const value = values == null ? void 0 : values[filter.name];
460
+ if (!isActive(filter, value)) continue;
461
+ const type = filter.type || "select";
462
+ const prefix = filter.chipLabel || filter.placeholder || filter.label || filter.name;
463
+ if (type === "multiselect") {
464
+ const labels = (Array.isArray(value) ? value : []).map((item) => findOptionLabel(filter, item)).join(", ");
465
+ chips.push({ key: filter.name, label: `${prefix}: ${labels}` });
466
+ } else if (type === "dateRange") {
467
+ const parts = [];
468
+ if (value == null ? void 0 : value.from) parts.push(`${options.dateFromPrefix ?? "from "}${dateFormatter(value.from)}`);
469
+ if (value == null ? void 0 : value.to) parts.push(`${options.dateToPrefix ?? "to "}${dateFormatter(value.to)}`);
470
+ chips.push({ key: filter.name, label: `${prefix}: ${parts.join(dateJoiner)}` });
471
+ } else {
472
+ chips.push({ key: filter.name, label: `${prefix}: ${findOptionLabel(filter, value)}` });
473
+ }
474
+ }
475
+ return chips;
476
+ };
477
+ var toStableKey = (value) => {
478
+ try {
479
+ return JSON.stringify(value);
480
+ } catch (_error) {
481
+ return String(value);
482
+ }
483
+ };
484
+ var filterRows = (rows, filters, values = {}) => {
485
+ let result = rows;
486
+ for (const filter of filters || []) {
487
+ const value = values[filter.name];
488
+ if (!isFilterActive(filter, value)) continue;
489
+ const type = filter.type || "select";
490
+ if (filter.filterFn) {
491
+ result = result.filter((row) => filter.filterFn(row, value));
492
+ } else if (type === "multiselect") {
493
+ result = result.filter((row) => value.includes(row[filter.name]));
494
+ } else if (type === "dateRange") {
495
+ const fromTs = dateToTimestamp(value.from);
496
+ const toTs = value.to ? dateToTimestamp(value.to) + 864e5 - 1 : null;
497
+ result = result.filter((row) => {
498
+ const rowTs = new Date(row[filter.name]).getTime();
499
+ if (Number.isNaN(rowTs)) return false;
500
+ if (fromTs && rowTs < fromTs) return false;
501
+ if (toTs && rowTs > toTs) return false;
502
+ return true;
503
+ });
504
+ } else {
505
+ result = result.filter((row) => row[filter.name] === value);
506
+ }
507
+ }
508
+ return result;
509
+ };
510
+ var searchRows = (rows, term, fields, opts = {}) => {
511
+ const { fuzzy = false, fuzzyOptions } = opts;
512
+ const t = String(term ?? "").toLowerCase();
513
+ if (!t || !fields || fields.length === 0) return rows;
514
+ if (fuzzy) {
515
+ const fuse = new Fuse(rows, {
516
+ keys: fields,
517
+ threshold: 0.4,
518
+ distance: 100,
519
+ ignoreLocation: true,
520
+ ...fuzzyOptions
521
+ });
522
+ return fuse.search(t).map((r) => r.item);
523
+ }
524
+ return rows.filter(
525
+ (row) => fields.some((field) => {
526
+ const val = row[field];
527
+ return val && String(val).toLowerCase().includes(t);
528
+ })
529
+ );
530
+ };
407
531
 
408
532
  // src/utils/interactionHooks.js
409
533
  import { useRef, useEffect, useCallback } from "react";
410
534
  import { useDebounce } from "@hubspot/ui-extensions";
535
+ var useDebouncedDispatch = (value, debounceMs, dispatch) => {
536
+ const debounced = useDebounce(value, debounceMs > 0 ? debounceMs : 300);
537
+ const pendingRef = useRef(null);
538
+ useEffect(() => {
539
+ if (debounceMs <= 0) return;
540
+ if (pendingRef.current == null) return;
541
+ if (debounced !== pendingRef.current) return;
542
+ const next = pendingRef.current;
543
+ pendingRef.current = null;
544
+ dispatch(next);
545
+ }, [debounceMs, debounced, dispatch]);
546
+ return useCallback(
547
+ (next) => {
548
+ if (debounceMs > 0) {
549
+ pendingRef.current = next;
550
+ } else {
551
+ pendingRef.current = null;
552
+ dispatch(next);
553
+ }
554
+ },
555
+ [debounceMs, dispatch]
556
+ );
557
+ };
558
+ var useSelectionReset = ({ resetKey, enabled, isControlled, clearSelection }) => {
559
+ const ref = useRef("");
560
+ useEffect(() => {
561
+ if (!enabled || isControlled) {
562
+ ref.current = resetKey;
563
+ return;
564
+ }
565
+ if (ref.current && ref.current !== resetKey) {
566
+ clearSelection();
567
+ }
568
+ ref.current = resetKey;
569
+ }, [resetKey, enabled, isControlled, clearSelection]);
570
+ };
411
571
 
412
572
  // src/common-components/CollectionCount.js
413
573
  import React5 from "react";
@@ -717,7 +877,7 @@ var GENERATED_ICONS = {
717
877
  "ZoomOut": { "viewBox": "0 0 32 32", "paths": ["M14.42 26.75c2.85 0 5.47-.97 7.56-2.6l-.03.02 5.28 5.34a1.619 1.619 0 0 0 2.76-1.15c0-.45-.18-.85-.47-1.14l-5.33-5.33c1.59-2.06 2.55-4.68 2.55-7.52C26.74 7.54 21.2 2 14.37 2S2 7.55 2 14.38s5.54 12.37 12.37 12.37h.05m0-21.55c5.06 0 9.16 4.1 9.16 9.16s-4.1 9.16-9.16 9.16-9.16-4.1-9.16-9.16c.01-5.05 4.11-9.14 9.16-9.15Zm-4.31 10.78h8.62c.89 0 1.62-.72 1.62-1.62s-.72-1.62-1.62-1.62h-8.62c-.89 0-1.62.72-1.62 1.62s.72 1.62 1.62 1.62"] }
718
878
  };
719
879
 
720
- // src/common-components/Icon.js
880
+ // src/common-components/nativeIconNames.js
721
881
  var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
722
882
  "add",
723
883
  "appointment",
@@ -729,12 +889,12 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
729
889
  "block",
730
890
  "book",
731
891
  "bulb",
892
+ "callTranscript",
732
893
  "calling",
733
894
  "callingHangup",
734
895
  "callingMade",
735
896
  "callingMissed",
736
897
  "callingVoicemail",
737
- "callTranscript",
738
898
  "campaigns",
739
899
  "cap",
740
900
  "checkCircle",
@@ -763,13 +923,13 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
763
923
  "enroll",
764
924
  "exclamation",
765
925
  "exclamationCircle",
766
- "facebook",
767
926
  "faceHappy",
768
927
  "faceHappyFilled",
769
928
  "faceNeutral",
770
929
  "faceNeutralFilled",
771
930
  "faceSad",
772
931
  "faceSadFilled",
932
+ "facebook",
773
933
  "favoriteHollow",
774
934
  "file",
775
935
  "filledXCircleIcon",
@@ -910,6 +1070,8 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
910
1070
  "zoomIn",
911
1071
  "zoomOut"
912
1072
  ]);
1073
+
1074
+ // src/common-components/Icon.js
913
1075
  var NATIVE_COLORS = /* @__PURE__ */ new Set(["inherit", "alert", "warning", "success"]);
914
1076
  var NATIVE_SIZE_TOKENS = {
915
1077
  sm: "sm",
@@ -1151,6 +1313,7 @@ var CollectionFilterControl = ({
1151
1313
  { key: name, direction: "row", align: "center", gap: "xs" },
1152
1314
  h3(DateInput, {
1153
1315
  name: `${controlName}-from`,
1316
+ label: filter.fromLabel ?? labels.dateFrom,
1154
1317
  placeholder: filter.fromLabel ?? labels.dateFrom,
1155
1318
  format: "medium",
1156
1319
  value: rangeValue.from ?? null,
@@ -1159,6 +1322,7 @@ var CollectionFilterControl = ({
1159
1322
  h3(Icon, { name: "right", size: "sm" }),
1160
1323
  h3(DateInput, {
1161
1324
  name: `${controlName}-to`,
1325
+ label: filter.toLabel ?? labels.dateTo,
1162
1326
  placeholder: filter.toLabel ?? labels.dateTo,
1163
1327
  format: "medium",
1164
1328
  value: rangeValue.to ?? null,
@@ -1291,6 +1455,51 @@ var CollectionToolbar = ({
1291
1455
  );
1292
1456
  };
1293
1457
 
1458
+ // src/datatable/editValidation.js
1459
+ var editValidationError = (result) => {
1460
+ if (result === true || result === void 0 || result === null) return null;
1461
+ return typeof result === "string" ? result : "Invalid value";
1462
+ };
1463
+
1464
+ // src/datatable/rowExpansion.js
1465
+ var extractRowId = (row, rowIdField = "id", fallback = void 0) => {
1466
+ const id = row == null ? void 0 : row[rowIdField];
1467
+ return id != null ? id : fallback;
1468
+ };
1469
+ var normalizeExpandedIds = (ids) => {
1470
+ if (ids == null) return /* @__PURE__ */ new Set();
1471
+ const list = ids instanceof Set ? [...ids] : Array.isArray(ids) ? ids : [ids];
1472
+ return new Set(list.filter((id) => id != null));
1473
+ };
1474
+ var expandRowId = (expandedIds, rowId, expandSingle = false) => {
1475
+ if (rowId == null) return expandedIds;
1476
+ if (expandSingle) return /* @__PURE__ */ new Set([rowId]);
1477
+ const next = new Set(expandedIds);
1478
+ next.add(rowId);
1479
+ return next;
1480
+ };
1481
+ var collapseRowId = (expandedIds, rowId) => {
1482
+ if (rowId == null || !expandedIds.has(rowId)) return expandedIds;
1483
+ const next = new Set(expandedIds);
1484
+ next.delete(rowId);
1485
+ return next;
1486
+ };
1487
+ var toggleExpandedId = (expandedIds, rowId, expandSingle = false) => {
1488
+ if (rowId == null) return expandedIds;
1489
+ return expandedIds.has(rowId) ? collapseRowId(expandedIds, rowId) : expandRowId(expandedIds, rowId, expandSingle);
1490
+ };
1491
+ var withDetailRows = (items, expandedIds, rowIdField = "id") => {
1492
+ if (!expandedIds || expandedIds.size === 0) return items;
1493
+ const out = [];
1494
+ items.forEach((item) => {
1495
+ out.push(item);
1496
+ if (item.type === "data" && expandedIds.has(extractRowId(item.row, rowIdField))) {
1497
+ out.push({ type: "detail", row: item.row });
1498
+ }
1499
+ });
1500
+ return out;
1501
+ };
1502
+
1294
1503
  // src/datatable/DataTable.jsx
1295
1504
  import {
1296
1505
  Box as Box2,
@@ -1321,10 +1530,1123 @@ import {
1321
1530
  Toggle,
1322
1531
  Tooltip
1323
1532
  } from "@hubspot/ui-extensions";
1533
+ var NARROW_EDIT_TYPES = /* @__PURE__ */ new Set(["checkbox", "toggle"]);
1534
+ var DATE_PATTERN = /^\d{4}[-/]\d{2}[-/]\d{2}/;
1535
+ var BOOL_VALUES = /* @__PURE__ */ new Set(["true", "false", "yes", "no", "0", "1"]);
1536
+ var SORT_DIRECTIONS = /* @__PURE__ */ new Set(["ascending", "descending", "none"]);
1537
+ var normalizeSortState = (columns, sort) => {
1538
+ const normalized = {};
1539
+ columns.forEach((col) => {
1540
+ if (col.sortable) normalized[col.field] = "none";
1541
+ });
1542
+ if (!sort) return normalized;
1543
+ if (sort.field && SORT_DIRECTIONS.has(sort.direction) && sort.field in normalized) {
1544
+ normalized[sort.field] = sort.direction;
1545
+ return normalized;
1546
+ }
1547
+ Object.keys(normalized).forEach((field) => {
1548
+ const direction = sort[field];
1549
+ if (SORT_DIRECTIONS.has(direction)) normalized[field] = direction;
1550
+ });
1551
+ return normalized;
1552
+ };
1553
+ var serializeSortState = (sortState) => {
1554
+ const activeField = Object.keys(sortState).find((field) => sortState[field] !== "none");
1555
+ if (!activeField) return null;
1556
+ return { field: activeField, direction: sortState[activeField] };
1557
+ };
1558
+ var computeAutoWidths = (columns, data) => {
1559
+ if (!data || data.length === 0) return {};
1560
+ const sample = data.slice(0, 50);
1561
+ const results = {};
1562
+ columns.forEach((col) => {
1563
+ if (col.width && col.cellWidth) return;
1564
+ const values = sample.map((row) => row[col.field]).filter((v) => v != null);
1565
+ const strings = values.map((v) => {
1566
+ const s = String(v);
1567
+ const truncLen = typeof col.truncate === "number" ? col.truncate : col.truncate && typeof col.truncate === "object" ? col.truncate.maxLength : null;
1568
+ return truncLen && s.length > truncLen ? s.slice(0, truncLen) : s;
1569
+ });
1570
+ let widthHint = null;
1571
+ let cellWidthHint = null;
1572
+ if (col.editable && col.editType && NARROW_EDIT_TYPES.has(col.editType)) {
1573
+ cellWidthHint = "min";
1574
+ }
1575
+ if (col.truncate === true) {
1576
+ cellWidthHint = cellWidthHint || "min";
1577
+ }
1578
+ if (strings.length > 0) {
1579
+ const lengths = strings.map((s) => s.length);
1580
+ const maxLen = Math.max(...lengths);
1581
+ const uniqueCount = new Set(strings).size;
1582
+ if (values.every((v) => typeof v === "boolean") || strings.every((s) => BOOL_VALUES.has(s.toLowerCase()))) {
1583
+ widthHint = widthHint || "min";
1584
+ cellWidthHint = cellWidthHint || "min";
1585
+ } else if (strings.every((s) => DATE_PATTERN.test(s))) {
1586
+ widthHint = widthHint || "min";
1587
+ cellWidthHint = cellWidthHint || "auto";
1588
+ } else if (values.every((v) => typeof v === "number")) {
1589
+ widthHint = widthHint || "auto";
1590
+ cellWidthHint = cellWidthHint || "auto";
1591
+ } else if (uniqueCount <= 5 && maxLen <= 15) {
1592
+ widthHint = widthHint || "min";
1593
+ cellWidthHint = cellWidthHint || "auto";
1594
+ } else {
1595
+ widthHint = widthHint || "auto";
1596
+ cellWidthHint = cellWidthHint || "auto";
1597
+ }
1598
+ }
1599
+ if (col.editable && !NARROW_EDIT_TYPES.has(col.editType) && widthHint === "min") {
1600
+ widthHint = "auto";
1601
+ }
1602
+ results[col.field] = {
1603
+ width: widthHint || "auto",
1604
+ cellWidth: cellWidthHint || "auto"
1605
+ };
1606
+ });
1607
+ return results;
1608
+ };
1609
+ var BOOLEAN_SELECT_OPTIONS = [
1610
+ { label: "Yes", value: true },
1611
+ { label: "No", value: false }
1612
+ ];
1613
+ var resolveEditOptions = (col, data) => {
1614
+ if (col.editOptions && col.editOptions.length > 0) return col.editOptions;
1615
+ const sample = data.find((row) => row[col.field] != null);
1616
+ if (sample && typeof sample[col.field] === "boolean") return BOOLEAN_SELECT_OPTIONS;
1617
+ return [];
1618
+ };
1619
+ var DataTable = ({
1620
+ // Data
1621
+ data,
1622
+ columns,
1623
+ renderRow,
1624
+ // Search
1625
+ searchFields = [],
1626
+ searchPlaceholder = "Search...",
1627
+ fuzzySearch = false,
1628
+ // enable fuzzy matching via Fuse.js
1629
+ fuzzyOptions,
1630
+ // custom Fuse.js options (threshold, distance, etc.)
1631
+ showSearch = true,
1632
+ // show the SearchInput in the toolbar
1633
+ // Filters
1634
+ filters = [],
1635
+ showFilterBadges = true,
1636
+ // show active filter chips/badges
1637
+ showClearFiltersButton,
1638
+ // show "Clear all" reset button; defaults to showFilterBadges when omitted
1639
+ filterInlineLimit = 2,
1640
+ // number of filters shown inline before overflow
1641
+ toolbarLeftFlex = 3,
1642
+ // left toolbar column flex weight (search/filters/chips)
1643
+ toolbarRightFlex = 1,
1644
+ // right toolbar column flex weight (count/actions)
1645
+ // Pagination
1646
+ pageSize = 10,
1647
+ maxVisiblePageButtons,
1648
+ // max page number buttons to show
1649
+ showButtonLabels = true,
1650
+ // show First/Prev/Next/Last text labels
1651
+ showFirstLastButtons,
1652
+ // show First/Last page buttons (default: auto when pageCount > 5)
1653
+ // Row count
1654
+ title,
1655
+ // optional title shown as demibold text above the table toolbar
1656
+ showRowCount = true,
1657
+ // show "X records" / "X of Y records" text
1658
+ rowCountBold = false,
1659
+ // bold the row count text
1660
+ rowCountText,
1661
+ // custom formatter: (shownOnPage, totalMatching) => string
1662
+ // Table appearance
1663
+ bordered = true,
1664
+ // show table borders
1665
+ flush = true,
1666
+ // remove bottom margin
1667
+ scrollable = false,
1668
+ // allow horizontal overflow with scrollbar
1669
+ // Sorting
1670
+ defaultSort = {},
1671
+ // Grouping
1672
+ groupBy,
1673
+ // Footer
1674
+ footer,
1675
+ // Empty state
1676
+ emptyTitle,
1677
+ emptyMessage,
1678
+ // -----------------------------------------------------------------------
1679
+ // Server-side mode
1680
+ // -----------------------------------------------------------------------
1681
+ serverSide = false,
1682
+ loading = false,
1683
+ // show loading spinner over the table
1684
+ error,
1685
+ // error message string or boolean — shows ErrorState
1686
+ totalCount,
1687
+ // server total (server-side only)
1688
+ clientTotalCount,
1689
+ // optional client-mode total when data is lazy-loaded
1690
+ page: externalPage,
1691
+ // controlled page (server-side only)
1692
+ searchValue,
1693
+ // controlled search term (server-side only)
1694
+ filterValues: externalFilterValues,
1695
+ // controlled filter values (server-side only)
1696
+ sort: externalSort,
1697
+ // controlled sort state, e.g. { field: "ascending" }
1698
+ searchDebounce = 0,
1699
+ // ms to debounce onSearchChange callback
1700
+ resetPageOnChange = true,
1701
+ // auto-reset to page 1 on search/filter/sort change
1702
+ onSearchChange,
1703
+ // (searchTerm) => void
1704
+ onFilterChange,
1705
+ // (filterValues) => void
1706
+ onSortChange,
1707
+ // (field, direction) => void
1708
+ onPageChange,
1709
+ // (page) => void
1710
+ onParamsChange,
1711
+ // ({ search, filters, sort, page }) => void
1712
+ // -----------------------------------------------------------------------
1713
+ // Row selection
1714
+ // -----------------------------------------------------------------------
1715
+ selectable = false,
1716
+ rowIdField = "id",
1717
+ // field name used as unique row identifier
1718
+ selectedIds: externalSelectedIds,
1719
+ // controlled selection — array of row IDs
1720
+ onSelectionChange,
1721
+ // (selectedIds[]) => void
1722
+ onSelectAllRequest,
1723
+ // server-side: ({ selectedIds, pageIds, totalCount }) => void
1724
+ selectionActions = [],
1725
+ // [{ label, onClick(selectedIds[]), icon?, variant? }]
1726
+ selectionResetKey,
1727
+ // optional key to force clear uncontrolled selection memory
1728
+ resetSelectionOnQueryChange = true,
1729
+ // clear uncontrolled selection on search/filter/sort changes
1730
+ showSelectionBar = true,
1731
+ // show the selection action bar when rows are selected
1732
+ recordLabel,
1733
+ // { singular: "Contact", plural: "Contacts" } — defaults to Record/Records
1734
+ // -----------------------------------------------------------------------
1735
+ // Row actions
1736
+ // -----------------------------------------------------------------------
1737
+ rowActions,
1738
+ // [{ label, onClick(row), icon?, variant? }] or (row) => actions[]
1739
+ hideRowActionsWhenSelectionActive = false,
1740
+ // hide row action column while selected-row action bar is visible
1741
+ // -----------------------------------------------------------------------
1742
+ // Row expansion (detail rows)
1743
+ // -----------------------------------------------------------------------
1744
+ renderExpandedRow,
1745
+ // (row) => ReactNode — providing this enables the feature
1746
+ expandedRowIds: externalExpandedRowIds,
1747
+ // controlled — array of expanded row IDs
1748
+ defaultExpandedRowIds,
1749
+ // uncontrolled — initially expanded row IDs
1750
+ onExpandedRowsChange,
1751
+ // (expandedRowIds[]) => void
1752
+ expandOn = "icon",
1753
+ // "icon" (chevron toggle column) | "row" (click row content)
1754
+ expandSingle = false,
1755
+ // accordion mode — expanding a row collapses the others
1756
+ // -----------------------------------------------------------------------
1757
+ // Inline editing
1758
+ // -----------------------------------------------------------------------
1759
+ editMode,
1760
+ // "discrete" (click-to-edit) | "inline" (always show inputs)
1761
+ editingRowId,
1762
+ // controlled — row ID currently in full-row edit mode
1763
+ onRowEdit,
1764
+ // (row, field, newValue) => void
1765
+ onRowEditInput,
1766
+ // optional live-input callback: (row, field, inputValue) => void
1767
+ onEditStart,
1768
+ // (row, field, currentValue) => void — fires when editing begins
1769
+ onEditCancel,
1770
+ // (row, field) => void — fires when editing is cancelled without commit
1771
+ // -----------------------------------------------------------------------
1772
+ // Auto-width
1773
+ // -----------------------------------------------------------------------
1774
+ autoWidth = true,
1775
+ // auto-compute column widths from content analysis
1776
+ // -----------------------------------------------------------------------
1777
+ // Labels / i18n
1778
+ // -----------------------------------------------------------------------
1779
+ labels,
1780
+ // override hardcoded UI strings for i18n
1781
+ // -----------------------------------------------------------------------
1782
+ // Render overrides (Phase 3 — full replacement escape hatches)
1783
+ // -----------------------------------------------------------------------
1784
+ renderSelectionBar,
1785
+ // ({ selectedIds, selectedCount, displayCount, countLabel, onSelectAll, onDeselectAll, selectionActions }) => ReactNode
1786
+ renderEmptyState,
1787
+ // ({ title, message }) => ReactNode
1788
+ renderLoadingState,
1789
+ // ({ label }) => ReactNode
1790
+ renderErrorState
1791
+ // ({ error, title, message }) => ReactNode
1792
+ }) => {
1793
+ const initialSortState = useMemo(() => {
1794
+ return normalizeSortState(columns, defaultSort);
1795
+ }, [columns, defaultSort]);
1796
+ const [internalSearchTerm, setInternalSearchTerm] = useState2(
1797
+ () => serverSide && searchValue != null ? searchValue : ""
1798
+ );
1799
+ const [internalFilterValues, setInternalFilterValues] = useState2(() => getEmptyFilterValues(filters));
1800
+ const [internalSortState, setInternalSortState] = useState2(initialSortState);
1801
+ const [currentPage, setCurrentPage] = useState2(1);
1802
+ const lastAppliedSearchRef = useRef2(
1803
+ serverSide && searchValue != null ? searchValue : ""
1804
+ );
1805
+ const searchTerm = serverSide && searchValue != null ? searchValue : internalSearchTerm;
1806
+ useEffect2(() => {
1807
+ if (!serverSide || searchValue == null) return;
1808
+ if (searchValue === lastAppliedSearchRef.current) return;
1809
+ lastAppliedSearchRef.current = searchValue;
1810
+ setInternalSearchTerm(searchValue);
1811
+ }, [serverSide, searchValue]);
1812
+ const filterValues = serverSide && externalFilterValues != null ? externalFilterValues : internalFilterValues;
1813
+ const externalSortState = useMemo(
1814
+ () => normalizeSortState(columns, externalSort),
1815
+ [columns, externalSort]
1816
+ );
1817
+ const sortState = serverSide && externalSort != null ? externalSortState : internalSortState;
1818
+ const activePage = serverSide && externalPage != null ? externalPage : currentPage;
1819
+ useEffect2(() => {
1820
+ if (!serverSide) setCurrentPage(1);
1821
+ }, [internalSearchTerm, internalFilterValues, internalSortState, serverSide]);
1822
+ const fireSearchCallback = useCallback2((term) => {
1823
+ if (serverSide && onSearchChange) onSearchChange(term);
1824
+ }, [serverSide, onSearchChange]);
1825
+ const fireParamsChange = useCallback2((overrides) => {
1826
+ if (!onParamsChange) return;
1827
+ const nextSortState = overrides.sort != null ? normalizeSortState(columns, overrides.sort) : sortState;
1828
+ onParamsChange({
1829
+ search: overrides.search != null ? overrides.search : searchTerm,
1830
+ filters: overrides.filters != null ? overrides.filters : filterValues,
1831
+ sort: serializeSortState(nextSortState),
1832
+ page: overrides.page != null ? overrides.page : activePage
1833
+ });
1834
+ }, [onParamsChange, columns, searchTerm, filterValues, sortState, activePage]);
1835
+ const resetPage = useCallback2(() => {
1836
+ if (resetPageOnChange) {
1837
+ setCurrentPage(1);
1838
+ if (serverSide && onPageChange) onPageChange(1);
1839
+ }
1840
+ }, [resetPageOnChange, serverSide, onPageChange]);
1841
+ const dispatchSearchChange = useCallback2((term) => {
1842
+ lastAppliedSearchRef.current = term;
1843
+ fireSearchCallback(term);
1844
+ fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
1845
+ }, [fireSearchCallback, fireParamsChange, resetPageOnChange]);
1846
+ const dispatchSearchDebounced = useDebouncedDispatch(
1847
+ internalSearchTerm,
1848
+ searchDebounce,
1849
+ dispatchSearchChange
1850
+ );
1851
+ const handleSearchChange = useCallback2((term) => {
1852
+ setInternalSearchTerm(term);
1853
+ resetPage();
1854
+ dispatchSearchDebounced(term);
1855
+ }, [dispatchSearchDebounced, resetPage]);
1856
+ const handleFilterChange = useCallback2((name, value) => {
1857
+ const next = { ...filterValues, [name]: value };
1858
+ setInternalFilterValues(next);
1859
+ if (serverSide && onFilterChange) onFilterChange(next);
1860
+ resetPage();
1861
+ fireParamsChange({ filters: next, page: resetPageOnChange ? 1 : void 0 });
1862
+ }, [filterValues, serverSide, onFilterChange, fireParamsChange, resetPage, resetPageOnChange]);
1863
+ const handleSortChange = useCallback2((field) => {
1864
+ const current = sortState[field] || "none";
1865
+ const nextDirection = current === "none" ? "ascending" : current === "ascending" ? "descending" : "none";
1866
+ const reset = {};
1867
+ columns.forEach((col) => {
1868
+ if (col.sortable) reset[col.field] = "none";
1869
+ });
1870
+ const next = nextDirection === "none" ? reset : { ...reset, [field]: nextDirection };
1871
+ setInternalSortState(next);
1872
+ if (serverSide && onSortChange) onSortChange(field, nextDirection);
1873
+ resetPage();
1874
+ fireParamsChange({ sort: next, page: resetPageOnChange ? 1 : void 0 });
1875
+ }, [sortState, columns, serverSide, onSortChange, fireParamsChange, resetPage, resetPageOnChange]);
1876
+ const handlePageChange = useCallback2((page) => {
1877
+ setCurrentPage(page);
1878
+ if (serverSide && onPageChange) onPageChange(page);
1879
+ fireParamsChange({ page });
1880
+ }, [serverSide, onPageChange, fireParamsChange]);
1881
+ const filteredData = useMemo(() => {
1882
+ if (serverSide) return data;
1883
+ let result = filterRows(data, filters, filterValues);
1884
+ if (searchTerm && searchFields.length > 0) {
1885
+ result = searchRows(result, searchTerm, searchFields, {
1886
+ fuzzy: fuzzySearch,
1887
+ fuzzyOptions
1888
+ });
1889
+ }
1890
+ return result;
1891
+ }, [data, filterValues, searchTerm, filters, searchFields, serverSide, fuzzySearch, fuzzyOptions]);
1892
+ const sortedData = useMemo(() => {
1893
+ if (serverSide) return filteredData;
1894
+ const activeField = Object.keys(sortState).find((k) => sortState[k] !== "none");
1895
+ if (!activeField) return filteredData;
1896
+ const activeCol = columns.find((c) => c.field === activeField);
1897
+ const sortOrder = Array.isArray(activeCol == null ? void 0 : activeCol.sortOrder) ? activeCol.sortOrder : null;
1898
+ const sortOrderIndex = (val) => {
1899
+ const idx = sortOrder.indexOf(val);
1900
+ return idx === -1 ? sortOrder.length : idx;
1901
+ };
1902
+ return [...filteredData].sort((a, b) => {
1903
+ const dir = sortState[activeField] === "ascending" ? 1 : -1;
1904
+ const aVal = a[activeField];
1905
+ const bVal = b[activeField];
1906
+ if (typeof (activeCol == null ? void 0 : activeCol.sortComparator) === "function") {
1907
+ return dir * activeCol.sortComparator(aVal, bVal, a, b);
1908
+ }
1909
+ if (sortOrder) {
1910
+ const diff = sortOrderIndex(aVal) - sortOrderIndex(bVal);
1911
+ if (diff !== 0) return dir * diff;
1912
+ }
1913
+ if (aVal == null && bVal == null) return 0;
1914
+ if (aVal == null) return 1;
1915
+ if (bVal == null) return -1;
1916
+ if (aVal < bVal) return -dir;
1917
+ if (aVal > bVal) return dir;
1918
+ return 0;
1919
+ });
1920
+ }, [filteredData, sortState, serverSide, columns]);
1921
+ const groupedData = useMemo(() => {
1922
+ if (!groupBy) return null;
1923
+ const source = serverSide ? data : sortedData;
1924
+ const groups = {};
1925
+ source.forEach((row) => {
1926
+ const key = row[groupBy.field] ?? "--";
1927
+ if (!groups[key]) groups[key] = [];
1928
+ groups[key].push(row);
1929
+ });
1930
+ let groupKeys = Object.keys(groups);
1931
+ if (groupBy.sort) {
1932
+ if (typeof groupBy.sort === "function") {
1933
+ groupKeys.sort(groupBy.sort);
1934
+ } else {
1935
+ const dir = groupBy.sort === "desc" ? -1 : 1;
1936
+ groupKeys.sort((a, b) => a < b ? -dir : a > b ? dir : 0);
1937
+ }
1938
+ }
1939
+ return groupKeys.map((key) => ({
1940
+ key,
1941
+ label: groupBy.label ? groupBy.label(key, groups[key]) : key,
1942
+ rows: groups[key]
1943
+ }));
1944
+ }, [sortedData, data, groupBy, serverSide]);
1945
+ const [expandedGroups, setExpandedGroups] = useState2(() => {
1946
+ if (!groupBy) return /* @__PURE__ */ new Set();
1947
+ const defaultExpanded = groupBy.defaultExpanded !== false;
1948
+ if (defaultExpanded && groupedData) {
1949
+ return new Set(groupedData.map((g) => g.key));
1950
+ }
1951
+ return /* @__PURE__ */ new Set();
1952
+ });
1953
+ useEffect2(() => {
1954
+ if (!groupedData) return;
1955
+ const defaultExpanded = (groupBy == null ? void 0 : groupBy.defaultExpanded) !== false;
1956
+ if (defaultExpanded) {
1957
+ setExpandedGroups((prev) => {
1958
+ const next = new Set(prev);
1959
+ groupedData.forEach((g) => next.add(g.key));
1960
+ return next;
1961
+ });
1962
+ }
1963
+ }, [groupedData, groupBy]);
1964
+ const toggleGroup = useCallback2((key) => {
1965
+ setExpandedGroups((prev) => {
1966
+ const next = new Set(prev);
1967
+ if (next.has(key)) next.delete(key);
1968
+ else next.add(key);
1969
+ return next;
1970
+ });
1971
+ }, []);
1972
+ const datasetRows = useMemo(() => {
1973
+ if (!groupedData) return serverSide ? data : sortedData;
1974
+ return groupedData.flatMap((group) => group.rows);
1975
+ }, [groupedData, sortedData, data, serverSide]);
1976
+ const resolvedTotalCount = typeof totalCount === "number" ? totalCount : void 0;
1977
+ const resolvedClientTotalCount = typeof clientTotalCount === "number" ? clientTotalCount : void 0;
1978
+ const totalItems = serverSide ? resolvedTotalCount || data.length : Math.max(datasetRows.length, resolvedClientTotalCount || 0);
1979
+ const pageCount = Math.ceil(totalItems / pageSize);
1980
+ const paginatedRows = useMemo(() => {
1981
+ if (serverSide) return datasetRows;
1982
+ return datasetRows.slice(
1983
+ (activePage - 1) * pageSize,
1984
+ activePage * pageSize
1985
+ );
1986
+ }, [serverSide, datasetRows, activePage, pageSize]);
1987
+ const displayRows = useMemo(() => {
1988
+ if (!groupedData) return paginatedRows.map((row) => ({ type: "data", row }));
1989
+ const pageRows = new Set(paginatedRows);
1990
+ const rows = [];
1991
+ groupedData.forEach((group) => {
1992
+ const groupPageRows = group.rows.filter((row) => pageRows.has(row));
1993
+ if (groupPageRows.length === 0) return;
1994
+ rows.push({ type: "group-header", group });
1995
+ if (expandedGroups.has(group.key)) {
1996
+ groupPageRows.forEach((row) => rows.push({ type: "data", row }));
1997
+ }
1998
+ });
1999
+ return rows;
2000
+ }, [groupedData, paginatedRows, expandedGroups]);
2001
+ const expandable = typeof renderExpandedRow === "function";
2002
+ const showExpandColumn = expandable && expandOn === "icon";
2003
+ const [internalExpandedRowIds, setInternalExpandedRowIds] = useState2(
2004
+ () => normalizeExpandedIds(defaultExpandedRowIds)
2005
+ );
2006
+ const expandedRowIds = useMemo(
2007
+ () => externalExpandedRowIds != null ? normalizeExpandedIds(externalExpandedRowIds) : internalExpandedRowIds,
2008
+ [externalExpandedRowIds, internalExpandedRowIds]
2009
+ );
2010
+ const toggleRowExpanded = useCallback2((rowId) => {
2011
+ if (rowId == null) return;
2012
+ const next = toggleExpandedId(expandedRowIds, rowId, expandSingle);
2013
+ if (externalExpandedRowIds == null) setInternalExpandedRowIds(next);
2014
+ if (onExpandedRowsChange) onExpandedRowsChange([...next]);
2015
+ }, [expandedRowIds, expandSingle, externalExpandedRowIds, onExpandedRowsChange]);
2016
+ const renderedRows = useMemo(() => {
2017
+ if (!expandable) return displayRows;
2018
+ return withDetailRows(displayRows, expandedRowIds, rowIdField);
2019
+ }, [expandable, displayRows, expandedRowIds, rowIdField]);
2020
+ const footerData = serverSide ? data : filteredData;
2021
+ const activeChips = useMemo(
2022
+ () => buildActiveFilterChips(filters, filterValues),
2023
+ [filterValues, filters]
2024
+ );
2025
+ const handleFilterRemove = useCallback2((key) => {
2026
+ const next = resetFilterValues(filters, filterValues, key);
2027
+ setInternalFilterValues(next);
2028
+ if (serverSide && onFilterChange) onFilterChange(next);
2029
+ resetPage();
2030
+ fireParamsChange({ filters: next, page: resetPageOnChange ? 1 : void 0 });
2031
+ }, [filters, filterValues, serverSide, onFilterChange, resetPage, fireParamsChange, resetPageOnChange]);
2032
+ const displayCount = serverSide ? resolvedTotalCount || data.length : Math.max(filteredData.length, resolvedClientTotalCount || 0);
2033
+ const totalDataCount = serverSide ? resolvedTotalCount || data.length : Math.max(data.length, resolvedClientTotalCount || 0);
2034
+ const shownOnPageCount = displayRows.filter((item) => item.type === "data").length;
2035
+ const pluralLabel = ((recordLabel == null ? void 0 : recordLabel.plural) || "records").toLowerCase();
2036
+ const singularLabel = ((recordLabel == null ? void 0 : recordLabel.singular) || "record").toLowerCase();
2037
+ const countLabel = (n) => n === 1 ? singularLabel : pluralLabel;
2038
+ const resolvedEmptyTitle = emptyTitle || "No results found";
2039
+ const resolvedEmptyMessage = emptyMessage || `No ${pluralLabel} match your search or filter criteria.`;
2040
+ const resolvedSelectedLabel = (labels == null ? void 0 : labels.selected) || ((count, label) => `${count}\xA0${label}\xA0selected`);
2041
+ const resolvedSelectAllLabel = (labels == null ? void 0 : labels.selectAll) || ((count, label) => `Select all ${count} ${label}`);
2042
+ const resolvedDeselectAllLabel = (labels == null ? void 0 : labels.deselectAll) || "Deselect all";
2043
+ const resolvedFiltersButtonLabel = (labels == null ? void 0 : labels.filtersButton) || "Filters";
2044
+ const resolvedClearAllLabel = (labels == null ? void 0 : labels.clearAll) || "Clear all";
2045
+ const resolvedDateFromLabel = (labels == null ? void 0 : labels.dateFrom) || "From";
2046
+ const resolvedDateToLabel = (labels == null ? void 0 : labels.dateTo) || "To";
2047
+ const resolvedLoadingLabel = (labels == null ? void 0 : labels.loading) || `Loading ${pluralLabel}...`;
2048
+ const resolvedLoadingMessage = (labels == null ? void 0 : labels.loadingMessage) || "This should only take a moment.";
2049
+ const resolvedErrorTitle = (labels == null ? void 0 : labels.errorTitle) || "Something went wrong.";
2050
+ const resolvedErrorMessage = (labels == null ? void 0 : labels.errorMessage) || "An error occurred while loading data.";
2051
+ const resolvedRetryMessage = (labels == null ? void 0 : labels.retryMessage) || "Please try again.";
2052
+ const resolvedShowClearFiltersButton = showClearFiltersButton ?? showFilterBadges;
2053
+ const recordCountLabel = rowCountText ? rowCountText(shownOnPageCount, displayCount) : displayCount === totalDataCount ? `${totalDataCount} ${countLabel(totalDataCount)}` : `${displayCount} of ${totalDataCount} ${countLabel(totalDataCount)}`;
2054
+ const [internalSelectedIds, setInternalSelectedIds] = useState2(/* @__PURE__ */ new Set());
2055
+ useEffect2(() => {
2056
+ if (externalSelectedIds != null) {
2057
+ setInternalSelectedIds(new Set(externalSelectedIds));
2058
+ }
2059
+ }, [externalSelectedIds]);
2060
+ const selectionQueryKey = useMemo(() => {
2061
+ if (!resetSelectionOnQueryChange) return "";
2062
+ return toStableKey({
2063
+ search: searchTerm,
2064
+ filters: filterValues,
2065
+ sort: serializeSortState(sortState)
2066
+ });
2067
+ }, [searchTerm, filterValues, sortState, resetSelectionOnQueryChange]);
2068
+ const combinedSelectionResetKey = useMemo(
2069
+ () => `${selectionQueryKey}::${selectionResetKey == null ? "" : toStableKey(selectionResetKey)}`,
2070
+ [selectionQueryKey, selectionResetKey]
2071
+ );
2072
+ const clearSelection = useCallback2(() => setInternalSelectedIds(/* @__PURE__ */ new Set()), []);
2073
+ useSelectionReset({
2074
+ resetKey: combinedSelectionResetKey,
2075
+ enabled: selectable,
2076
+ isControlled: externalSelectedIds != null,
2077
+ clearSelection
2078
+ });
2079
+ const selectedIds = externalSelectedIds != null ? new Set(externalSelectedIds) : internalSelectedIds;
2080
+ const showToolbarCount = showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
2081
+ const hasToolbarLeft = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || resolvedShowClearFiltersButton);
2082
+ const countInTitleRow = !!title && showToolbarCount && !hasToolbarLeft;
2083
+ const countInToolbar = showToolbarCount && !countInTitleRow;
2084
+ const hasToolbarContent = hasToolbarLeft || countInToolbar;
2085
+ const showRowActionsColumn = !!rowActions && !(hideRowActionsWhenSelectionActive && selectable && selectedIds.size > 0);
2086
+ const applySelection = useCallback2((nextSet) => {
2087
+ if (externalSelectedIds == null) {
2088
+ setInternalSelectedIds(nextSet);
2089
+ }
2090
+ if (onSelectionChange) onSelectionChange([...nextSet]);
2091
+ }, [externalSelectedIds, onSelectionChange]);
2092
+ const pageRowIds = useMemo(() => {
2093
+ if (serverSide) {
2094
+ return data.map((row) => row[rowIdField]).filter((id) => id != null);
2095
+ }
2096
+ return displayRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null);
2097
+ }, [serverSide, data, displayRows, rowIdField]);
2098
+ const allRowIds = useMemo(
2099
+ () => datasetRows.map((row) => row[rowIdField]).filter((id) => id != null),
2100
+ [datasetRows, rowIdField]
2101
+ );
2102
+ const handleSelectRow = useCallback2((rowId, checked) => {
2103
+ const next = new Set(selectedIds);
2104
+ if (checked) next.add(rowId);
2105
+ else next.delete(rowId);
2106
+ applySelection(next);
2107
+ }, [selectedIds, applySelection]);
2108
+ const handleSelectAll = useCallback2((checked) => {
2109
+ const next = new Set(selectedIds);
2110
+ pageRowIds.forEach((id) => {
2111
+ if (checked) next.add(id);
2112
+ else next.delete(id);
2113
+ });
2114
+ applySelection(next);
2115
+ }, [selectedIds, pageRowIds, applySelection]);
2116
+ const allVisibleSelected = useMemo(() => {
2117
+ return pageRowIds.length > 0 && pageRowIds.every((id) => selectedIds.has(id));
2118
+ }, [pageRowIds, selectedIds]);
2119
+ const handleSelectAllRows = useCallback2(() => {
2120
+ const idsToAdd = serverSide ? pageRowIds : allRowIds;
2121
+ const next = new Set(selectedIds);
2122
+ idsToAdd.forEach((id) => next.add(id));
2123
+ applySelection(next);
2124
+ if (serverSide && onSelectAllRequest) {
2125
+ onSelectAllRequest({
2126
+ selectedIds: [...next],
2127
+ pageIds: pageRowIds,
2128
+ totalCount: totalCount || data.length
2129
+ });
2130
+ }
2131
+ }, [serverSide, pageRowIds, allRowIds, selectedIds, applySelection, onSelectAllRequest, totalCount, data.length]);
2132
+ const handleDeselectAll = useCallback2(() => {
2133
+ applySelection(/* @__PURE__ */ new Set());
2134
+ }, [applySelection]);
2135
+ const [editingCell, setEditingCell] = useState2(null);
2136
+ const [editValue, setEditValue] = useState2(null);
2137
+ const [editError, setEditError] = useState2(null);
2138
+ const startEditing = useCallback2((rowId, field, currentValue) => {
2139
+ setEditingCell({ rowId, field });
2140
+ setEditValue(currentValue);
2141
+ setEditError(null);
2142
+ if (onEditStart) {
2143
+ const row = data.find((r) => r[rowIdField] === rowId);
2144
+ if (row) onEditStart(row, field, currentValue);
2145
+ }
2146
+ }, [onEditStart, data, rowIdField]);
2147
+ const commitEdit = useCallback2((row, field, value, options = {}) => {
2148
+ const { keepEditing = false } = options;
2149
+ const col = columns.find((c) => c.field === field);
2150
+ if (col == null ? void 0 : col.editValidate) {
2151
+ const err = editValidationError(col.editValidate(value, row));
2152
+ if (err) {
2153
+ setEditError(err);
2154
+ return false;
2155
+ }
2156
+ }
2157
+ if (onRowEdit) onRowEdit(row, field, value);
2158
+ if (!keepEditing) {
2159
+ setEditingCell(null);
2160
+ setEditValue(null);
2161
+ } else {
2162
+ setEditValue(value);
2163
+ }
2164
+ setEditError(null);
2165
+ return true;
2166
+ }, [onRowEdit, columns]);
2167
+ const renderEditControl = (col, row) => {
2168
+ const type = col.editType || "text";
2169
+ const rowId = row[rowIdField];
2170
+ const fieldName = `edit-${rowId}-${col.field}`;
2171
+ const inputLabel = typeof col.label === "string" ? col.label : col.field;
2172
+ const commit = (val) => commitEdit(row, col.field, val);
2173
+ const exitEdit = () => {
2174
+ if (editError) return;
2175
+ if (onEditCancel) onEditCancel(row, col.field);
2176
+ setEditingCell(null);
2177
+ setEditValue(null);
2178
+ };
2179
+ const extra = col.editProps || {};
2180
+ const validate = col.editValidate;
2181
+ const validationProps = validate && editError ? { error: true, validationMessage: editError } : {};
2182
+ const onInputValidate = validate ? (val) => setEditError(editValidationError(validate(val, row))) : void 0;
2183
+ const handleInput = (val) => {
2184
+ setEditValue(val);
2185
+ if (onInputValidate) onInputValidate(val);
2186
+ if (onRowEditInput) onRowEditInput(row, col.field, val);
2187
+ };
2188
+ const maybeExitDatetimeEdit = () => {
2189
+ if (typeof document === "undefined") return;
2190
+ setTimeout(() => {
2191
+ var _a, _b;
2192
+ const activeName = (_b = (_a = document.activeElement) == null ? void 0 : _a.getAttribute) == null ? void 0 : _b.call(_a, "name");
2193
+ if (activeName !== `${fieldName}-date` && activeName !== `${fieldName}-time`) {
2194
+ exitEdit();
2195
+ }
2196
+ }, 0);
2197
+ };
2198
+ switch (type) {
2199
+ case "textarea":
2200
+ return /* @__PURE__ */ React9.createElement(TextArea, { ...extra, name: fieldName, label: "", value: editValue ?? "", onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
2201
+ case "number":
2202
+ return /* @__PURE__ */ React9.createElement(NumberInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
2203
+ case "currency":
2204
+ return /* @__PURE__ */ React9.createElement(CurrencyInput, { currencyCode: "USD", ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
2205
+ case "stepper":
2206
+ return /* @__PURE__ */ React9.createElement(StepperInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
2207
+ case "select":
2208
+ return /* @__PURE__ */ React9.createElement(Select2, { variant: "transparent", ...extra, name: fieldName, label: "", value: editValue, onChange: commit, options: resolveEditOptions(col, data) });
2209
+ case "multiselect":
2210
+ return /* @__PURE__ */ React9.createElement(MultiSelect2, { ...extra, name: fieldName, label: "", value: editValue || [], onChange: commit, options: resolveEditOptions(col, data) });
2211
+ case "date":
2212
+ return /* @__PURE__ */ React9.createElement(DateInput2, { ...extra, name: fieldName, label: inputLabel, value: editValue, onChange: commit });
2213
+ case "time":
2214
+ return /* @__PURE__ */ React9.createElement(TimeInput, { ...extra, name: fieldName, label: inputLabel, value: editValue, onChange: commit });
2215
+ case "datetime":
2216
+ return /* @__PURE__ */ React9.createElement(Flex4, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React9.createElement(DateInput2, { ...extra, name: `${fieldName}-date`, label: `${inputLabel} date`, value: editValue == null ? void 0 : editValue.date, onChange: (val) => {
2217
+ const next = { ...editValue, date: val };
2218
+ handleInput(next);
2219
+ commitEdit(row, col.field, next, { keepEditing: true });
2220
+ }, onBlur: maybeExitDatetimeEdit }), /* @__PURE__ */ React9.createElement(TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: `${inputLabel} time`, value: editValue == null ? void 0 : editValue.time, onChange: (val) => {
2221
+ const next = { ...editValue, time: val };
2222
+ handleInput(next);
2223
+ commitEdit(row, col.field, next, { keepEditing: true });
2224
+ }, onBlur: maybeExitDatetimeEdit }));
2225
+ case "toggle":
2226
+ return /* @__PURE__ */ React9.createElement(Toggle, { ...extra, name: fieldName, label: "", checked: !!editValue, onChange: commit });
2227
+ case "checkbox":
2228
+ return /* @__PURE__ */ React9.createElement(Checkbox, { ...extra, name: fieldName, checked: !!editValue, onChange: commit });
2229
+ default:
2230
+ return /* @__PURE__ */ React9.createElement(Input, { ...extra, name: fieldName, label: "", value: editValue ?? "", onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
2231
+ }
2232
+ };
2233
+ const resolvedEditMode = editMode || (columns.some((col) => col.editable) ? "discrete" : null);
2234
+ const useColumnRendering = selectable || !!resolvedEditMode || editingRowId != null || showRowActionsColumn || expandable || !renderRow;
2235
+ const totalColumnCount = columns.length + (selectable ? 1 : 0) + (showExpandColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
2236
+ const autoWidths = useMemo(
2237
+ () => autoWidth ? computeAutoWidths(columns, data) : {},
2238
+ [columns, data, autoWidth]
2239
+ );
2240
+ const defaultWidth = scrollable ? "min" : "auto";
2241
+ const getHeaderWidth = (col) => {
2242
+ var _a;
2243
+ return col.width || ((_a = autoWidths[col.field]) == null ? void 0 : _a.width) || defaultWidth;
2244
+ };
2245
+ const getCellWidth = (col) => {
2246
+ var _a;
2247
+ return col.cellWidth || col.width || ((_a = autoWidths[col.field]) == null ? void 0 : _a.cellWidth) || defaultWidth;
2248
+ };
2249
+ const [inlineErrors, setInlineErrors] = useState2({});
2250
+ const renderInlineControl = (col, row) => {
2251
+ const type = col.editType || "text";
2252
+ const rowId = row[rowIdField];
2253
+ const fieldName = `inline-${rowId}-${col.field}`;
2254
+ const inputLabel = typeof col.label === "string" ? col.label : col.field;
2255
+ const cellKey = `${rowId}-${col.field}`;
2256
+ const value = row[col.field];
2257
+ const validate = col.editValidate;
2258
+ const fire = (val) => {
2259
+ if (validate) {
2260
+ const err = editValidationError(validate(val, row));
2261
+ if (err) {
2262
+ setInlineErrors((prev) => ({ ...prev, [cellKey]: err }));
2263
+ return;
2264
+ }
2265
+ setInlineErrors((prev) => {
2266
+ const next = { ...prev };
2267
+ delete next[cellKey];
2268
+ return next;
2269
+ });
2270
+ }
2271
+ if (onRowEdit) onRowEdit(row, col.field, val);
2272
+ };
2273
+ const extra = col.editProps || {};
2274
+ const cellError = inlineErrors[cellKey];
2275
+ const validationProps = cellError ? { error: true, validationMessage: cellError } : {};
2276
+ const onInputValidate = validate ? (val) => {
2277
+ const err = editValidationError(validate(val, row));
2278
+ setInlineErrors((prev) => {
2279
+ if (err) return { ...prev, [cellKey]: err };
2280
+ const next = { ...prev };
2281
+ delete next[cellKey];
2282
+ return next;
2283
+ });
2284
+ } : void 0;
2285
+ const emitInput = (val) => {
2286
+ if (onInputValidate) onInputValidate(val);
2287
+ if (onRowEditInput) onRowEditInput(row, col.field, val);
2288
+ };
2289
+ switch (type) {
2290
+ case "textarea":
2291
+ return /* @__PURE__ */ React9.createElement(TextArea, { ...extra, name: fieldName, label: "", value: value ?? "", onChange: fire, ...validationProps, onInput: emitInput });
2292
+ case "number":
2293
+ return /* @__PURE__ */ React9.createElement(NumberInput, { ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
2294
+ case "currency":
2295
+ return /* @__PURE__ */ React9.createElement(CurrencyInput, { currencyCode: "USD", ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
2296
+ case "stepper":
2297
+ return /* @__PURE__ */ React9.createElement(StepperInput, { ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
2298
+ case "select":
2299
+ return /* @__PURE__ */ React9.createElement(Select2, { ...extra, name: fieldName, label: "", value, onChange: fire, options: resolveEditOptions(col, data) });
2300
+ case "multiselect":
2301
+ return /* @__PURE__ */ React9.createElement(MultiSelect2, { ...extra, name: fieldName, label: "", value: value || [], onChange: fire, options: resolveEditOptions(col, data) });
2302
+ case "date":
2303
+ return /* @__PURE__ */ React9.createElement(DateInput2, { ...extra, name: fieldName, label: inputLabel, value, onChange: fire });
2304
+ case "time":
2305
+ return /* @__PURE__ */ React9.createElement(TimeInput, { ...extra, name: fieldName, label: inputLabel, value, onChange: fire });
2306
+ case "datetime":
2307
+ return /* @__PURE__ */ React9.createElement(Flex4, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React9.createElement(DateInput2, { ...extra, name: `${fieldName}-date`, label: `${inputLabel} date`, value: value == null ? void 0 : value.date, onChange: (val) => {
2308
+ fire({ ...value, date: val });
2309
+ } }), /* @__PURE__ */ React9.createElement(TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: `${inputLabel} time`, value: value == null ? void 0 : value.time, onChange: (val) => {
2310
+ fire({ ...value, time: val });
2311
+ } }));
2312
+ case "toggle":
2313
+ return /* @__PURE__ */ React9.createElement(Toggle, { ...extra, name: fieldName, label: "", checked: !!value, onChange: fire });
2314
+ case "checkbox":
2315
+ return /* @__PURE__ */ React9.createElement(Checkbox, { ...extra, name: fieldName, checked: !!value, onChange: fire });
2316
+ default:
2317
+ return /* @__PURE__ */ React9.createElement(Input, { ...extra, name: fieldName, label: "", value: value ?? "", onChange: fire, ...validationProps, onInput: emitInput });
2318
+ }
2319
+ };
2320
+ const renderCellContent = (row, col) => {
2321
+ const rowId = row[rowIdField];
2322
+ if (resolvedEditMode === "inline" && col.editable) {
2323
+ return renderInlineControl(col, row);
2324
+ }
2325
+ if (editingRowId != null && rowId === editingRowId && col.editable) {
2326
+ return renderInlineControl(col, row);
2327
+ }
2328
+ const isEditing = (editingCell == null ? void 0 : editingCell.rowId) === rowId && (editingCell == null ? void 0 : editingCell.field) === col.field;
2329
+ if (isEditing && col.editable) return renderEditControl(col, row);
2330
+ if (resolvedEditMode === "discrete" && col.editable && (col.editType === "select" || col.editType === "multiselect")) {
2331
+ const extra = col.editProps || {};
2332
+ const fieldName = `edit-${rowId}-${col.field}`;
2333
+ const value = row[col.field];
2334
+ const commit = (val) => commitEdit(row, col.field, val);
2335
+ const options = resolveEditOptions(col, data);
2336
+ const placeholder = extra.placeholder ?? "Select";
2337
+ return col.editType === "select" ? /* @__PURE__ */ React9.createElement(Select2, { variant: "transparent", placeholder, ...extra, name: fieldName, label: "", value, onChange: commit, options }) : /* @__PURE__ */ React9.createElement(MultiSelect2, { placeholder, ...extra, name: fieldName, label: "", value: value || [], onChange: commit, options });
2338
+ }
2339
+ const rawValue = row[col.field];
2340
+ const rawStr = String(rawValue ?? "");
2341
+ if (col.truncate && rawStr.length > 0) {
2342
+ if (col.truncate === true) {
2343
+ if (col.renderCell) {
2344
+ const content2 = col.renderCell(rawValue, row);
2345
+ if (col.editable) {
2346
+ return /* @__PURE__ */ React9.createElement(Link2, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
2347
+ }
2348
+ return content2;
2349
+ }
2350
+ if (col.editable) {
2351
+ return /* @__PURE__ */ React9.createElement(Text2, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ React9.createElement(Link2, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, rawStr || "--"));
2352
+ }
2353
+ return /* @__PURE__ */ React9.createElement(Text2, { truncate: { tooltipText: rawStr } }, rawStr);
2354
+ }
2355
+ const maxLen = typeof col.truncate === "number" ? col.truncate : col.truncate.maxLength || 100;
2356
+ if (rawStr.length > maxLen) {
2357
+ const truncatedStr = rawStr.slice(0, maxLen) + "\u2026";
2358
+ const content2 = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
2359
+ if (col.editable) {
2360
+ return /* @__PURE__ */ React9.createElement(Link2, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
2361
+ }
2362
+ return col.renderCell ? content2 : /* @__PURE__ */ React9.createElement(Text2, { truncate: { tooltipText: rawStr } }, content2 || "--");
2363
+ }
2364
+ }
2365
+ const content = col.renderCell ? col.renderCell(rawValue, row) : rawValue;
2366
+ const isEmpty = content == null || content === "";
2367
+ if (col.editable) {
2368
+ return /* @__PURE__ */ React9.createElement(
2369
+ Link2,
2370
+ {
2371
+ variant: "dark",
2372
+ onClick: () => startEditing(rowId, col.field, rawValue)
2373
+ },
2374
+ isEmpty ? "--" : content
2375
+ );
2376
+ }
2377
+ return isEmpty ? "--" : content;
2378
+ };
2379
+ const toolbarCount = countInToolbar ? /* @__PURE__ */ React9.createElement(CollectionCount, { text: recordCountLabel, bold: rowCountBold }) : null;
2380
+ return /* @__PURE__ */ React9.createElement(Flex4, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ React9.createElement(Flex4, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ React9.createElement(Text2, { format: { fontWeight: "demibold" } }, title), countInTitleRow && /* @__PURE__ */ React9.createElement(CollectionCount, { text: recordCountLabel, bold: rowCountBold })), hasToolbarContent && /* @__PURE__ */ React9.createElement(
2381
+ CollectionToolbar,
2382
+ {
2383
+ search: {
2384
+ visible: showSearch && searchFields.length > 0,
2385
+ name: "datatable-search",
2386
+ placeholder: searchPlaceholder,
2387
+ value: internalSearchTerm,
2388
+ onChange: handleSearchChange
2389
+ },
2390
+ filters: {
2391
+ items: filters,
2392
+ values: filterValues,
2393
+ inlineLimit: filterInlineLimit,
2394
+ namePrefix: "datatable-filter",
2395
+ onChange: handleFilterChange,
2396
+ filtersButtonLabel: resolvedFiltersButtonLabel,
2397
+ labels: {
2398
+ all: "All",
2399
+ dateFrom: resolvedDateFromLabel,
2400
+ dateTo: resolvedDateToLabel
2401
+ }
2402
+ },
2403
+ chips: {
2404
+ items: activeChips,
2405
+ showBadges: showFilterBadges,
2406
+ showClearAll: resolvedShowClearFiltersButton,
2407
+ clearAllLabel: resolvedClearAllLabel,
2408
+ onRemove: handleFilterRemove
2409
+ },
2410
+ right: toolbarCount,
2411
+ leftFlex: toolbarLeftFlex,
2412
+ rightFlex: toolbarRightFlex
2413
+ }
2414
+ ), showSelectionBar && selectable && selectedIds.size > 0 && (renderSelectionBar ? renderSelectionBar({
2415
+ selectedIds,
2416
+ selectedCount: selectedIds.size,
2417
+ displayCount,
2418
+ countLabel,
2419
+ allSelected: selectedIds.size >= (serverSide ? totalCount || data.length : allRowIds.length),
2420
+ onSelectAll: handleSelectAllRows,
2421
+ onDeselectAll: handleDeselectAll,
2422
+ selectionActions
2423
+ }) : /* @__PURE__ */ React9.createElement(Flex4, { direction: "row", gap: "sm" }, /* @__PURE__ */ React9.createElement(Box2, { flex: 3 }, /* @__PURE__ */ React9.createElement(Flex4, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ React9.createElement(Text2, { inline: true, format: { fontWeight: "demibold" } }, typeof resolvedSelectedLabel === "function" ? resolvedSelectedLabel(selectedIds.size, countLabel(selectedIds.size)) : resolvedSelectedLabel), selectedIds.size < (serverSide ? totalCount || data.length : allRowIds.length) && /* @__PURE__ */ React9.createElement(Button3, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, typeof resolvedSelectAllLabel === "function" ? resolvedSelectAllLabel(displayCount, countLabel(displayCount)) : resolvedSelectAllLabel), /* @__PURE__ */ React9.createElement(Button3, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, resolvedDeselectAllLabel), selectionActions.map((action, i) => /* @__PURE__ */ React9.createElement(
2424
+ Button3,
2425
+ {
2426
+ key: i,
2427
+ variant: action.variant || "transparent",
2428
+ size: "extra-small",
2429
+ onClick: () => action.onClick([...selectedIds])
2430
+ },
2431
+ action.icon && /* @__PURE__ */ React9.createElement(Icon, { name: action.icon, size: "sm" }),
2432
+ " ",
2433
+ action.label
2434
+ )))), showRowCount && displayCount > 0 && /* @__PURE__ */ React9.createElement(Box2, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ React9.createElement(Flex4, { direction: "row", justify: "end" }, /* @__PURE__ */ React9.createElement(CollectionCount, { text: recordCountLabel, bold: rowCountBold }))))), loading ? renderLoadingState ? renderLoadingState({ label: resolvedLoadingLabel }) : (
2435
+ // Same EmptyState layout as the empty state, just the "building" image
2436
+ // + a loading message — so loading and empty match with no layout shift.
2437
+ /* @__PURE__ */ React9.createElement(Tile, null, /* @__PURE__ */ React9.createElement(Flex4, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React9.createElement(EmptyState, { title: resolvedLoadingLabel, imageName: "building", layout: "vertical" }, /* @__PURE__ */ React9.createElement(Text2, null, resolvedLoadingMessage))))
2438
+ ) : error ? renderErrorState ? renderErrorState({
2439
+ error,
2440
+ title: typeof error === "string" ? error : resolvedErrorTitle,
2441
+ message: typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage
2442
+ }) : /* @__PURE__ */ React9.createElement(ErrorState, { title: typeof error === "string" ? error : resolvedErrorTitle }, /* @__PURE__ */ React9.createElement(Text2, null, typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage)) : displayRows.length === 0 ? renderEmptyState ? renderEmptyState({ title: resolvedEmptyTitle, message: resolvedEmptyMessage }) : /* @__PURE__ */ React9.createElement(Tile, null, /* @__PURE__ */ React9.createElement(Flex4, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React9.createElement(EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ React9.createElement(Text2, null, resolvedEmptyMessage)))) : /* @__PURE__ */ React9.createElement(
2443
+ Table,
2444
+ {
2445
+ bordered,
2446
+ flush,
2447
+ paginated: pageCount > 1,
2448
+ page: activePage,
2449
+ pageCount,
2450
+ onPageChange: handlePageChange,
2451
+ showFirstLastButtons: showFirstLastButtons != null ? showFirstLastButtons : pageCount > 5,
2452
+ showButtonLabels,
2453
+ ...maxVisiblePageButtons != null ? { maxVisiblePageButtons } : {}
2454
+ },
2455
+ /* @__PURE__ */ React9.createElement(TableHead, null, /* @__PURE__ */ React9.createElement(TableRow, null, selectable && /* @__PURE__ */ React9.createElement(TableHeader, { width: "min" }, /* @__PURE__ */ React9.createElement(
2456
+ Checkbox,
2457
+ {
2458
+ name: "datatable-select-all",
2459
+ "aria-label": "Select all rows",
2460
+ checked: allVisibleSelected,
2461
+ onChange: handleSelectAll
2462
+ }
2463
+ )), showExpandColumn && /* @__PURE__ */ React9.createElement(TableHeader, { width: "min" }), columns.map((col) => {
2464
+ const headerAlign = resolvedEditMode === "inline" && col.editable ? void 0 : col.align;
2465
+ return /* @__PURE__ */ React9.createElement(
2466
+ TableHeader,
2467
+ {
2468
+ key: col.field,
2469
+ width: getHeaderWidth(col),
2470
+ align: headerAlign,
2471
+ sortDirection: col.sortable ? sortState[col.field] || "none" : "never",
2472
+ onSortChange: col.sortable ? () => handleSortChange(col.field) : void 0
2473
+ },
2474
+ col.description ? /* @__PURE__ */ React9.createElement(React9.Fragment, null, col.label, "\xA0", /* @__PURE__ */ React9.createElement(Link2, { inline: true, variant: "dark", overlay: /* @__PURE__ */ React9.createElement(Tooltip, null, col.description) }, /* @__PURE__ */ React9.createElement(Icon, { name: "info", screenReaderText: typeof col.description === "string" ? col.description : void 0 }))) : col.label
2475
+ );
2476
+ }), showRowActionsColumn && /* @__PURE__ */ React9.createElement(TableHeader, { width: "min" }))),
2477
+ /* @__PURE__ */ React9.createElement(TableBody, null, renderedRows.map(
2478
+ (item, idx) => item.type === "group-header" ? /* @__PURE__ */ React9.createElement(TableRow, { key: `group-${item.group.key}` }, selectable && /* @__PURE__ */ React9.createElement(TableCell, { width: "min" }), showExpandColumn && /* @__PURE__ */ React9.createElement(TableCell, { width: "min" }), columns.map((col, colIdx) => {
2479
+ var _a, _b, _c;
2480
+ return /* @__PURE__ */ React9.createElement(TableCell, { key: col.field, width: getCellWidth(col), align: colIdx === 0 ? void 0 : col.align }, colIdx === 0 ? /* @__PURE__ */ React9.createElement(Flex4, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React9.createElement(
2481
+ Icon,
2482
+ {
2483
+ name: expandedGroups.has(item.group.key) ? "Down" : "Right",
2484
+ onClick: () => toggleGroup(item.group.key),
2485
+ screenReaderText: expandedGroups.has(item.group.key) ? "Collapse group" : "Expand group"
2486
+ }
2487
+ ), /* @__PURE__ */ React9.createElement(
2488
+ Link2,
2489
+ {
2490
+ variant: "dark",
2491
+ onClick: () => toggleGroup(item.group.key)
2492
+ },
2493
+ /* @__PURE__ */ React9.createElement(Text2, { format: { fontWeight: "demibold" } }, item.group.label)
2494
+ )) : ((_a = groupBy.aggregations) == null ? void 0 : _a[col.field]) ? groupBy.aggregations[col.field](item.group.rows, item.group.key) : ((_c = (_b = groupBy.groupValues) == null ? void 0 : _b[item.group.key]) == null ? void 0 : _c[col.field]) ?? "");
2495
+ }), showRowActionsColumn && /* @__PURE__ */ React9.createElement(TableCell, { width: "min" })) : item.type === "detail" ? /* @__PURE__ */ React9.createElement(TableRow, { key: `detail-${item.row[rowIdField] ?? idx}` }, /* @__PURE__ */ React9.createElement(TableCell, { colSpan: totalColumnCount }, renderExpandedRow(item.row))) : useColumnRendering ? /* @__PURE__ */ React9.createElement(TableRow, { key: item.row[rowIdField] ?? idx }, selectable && /* @__PURE__ */ React9.createElement(TableCell, { width: "min" }, /* @__PURE__ */ React9.createElement(
2496
+ Checkbox,
2497
+ {
2498
+ name: `select-${item.row[rowIdField]}`,
2499
+ "aria-label": "Select row",
2500
+ checked: selectedIds.has(item.row[rowIdField]),
2501
+ onChange: (checked) => handleSelectRow(item.row[rowIdField], checked)
2502
+ }
2503
+ )), showExpandColumn && /* @__PURE__ */ React9.createElement(TableCell, { width: "min" }, /* @__PURE__ */ React9.createElement(
2504
+ Icon,
2505
+ {
2506
+ name: expandedRowIds.has(item.row[rowIdField]) ? "upCarat" : "downCarat",
2507
+ onClick: () => toggleRowExpanded(item.row[rowIdField]),
2508
+ screenReaderText: expandedRowIds.has(item.row[rowIdField]) ? "Collapse row" : "Expand row"
2509
+ }
2510
+ )), columns.map((col) => {
2511
+ const rowId = item.row[rowIdField];
2512
+ const isDiscreteEditing = resolvedEditMode === "discrete" && (editingCell == null ? void 0 : editingCell.rowId) === rowId && (editingCell == null ? void 0 : editingCell.field) === col.field;
2513
+ const isRowEditing = editingRowId != null && rowId === editingRowId && col.editable;
2514
+ const isShowingInput = isDiscreteEditing || isRowEditing || resolvedEditMode === "inline" && col.editable;
2515
+ const cellAlign = isShowingInput ? void 0 : col.align;
2516
+ const cellContent = renderCellContent(item.row, col);
2517
+ const wrapInRowToggle = expandable && expandOn === "row" && !col.editable && !isShowingInput;
2518
+ return /* @__PURE__ */ React9.createElement(TableCell, { key: col.field, width: isDiscreteEditing || isRowEditing ? "auto" : getCellWidth(col), align: cellAlign }, wrapInRowToggle ? /* @__PURE__ */ React9.createElement(Link2, { variant: "dark", onClick: () => toggleRowExpanded(rowId) }, cellContent) : cellContent);
2519
+ }), showRowActionsColumn && /* @__PURE__ */ React9.createElement(TableCell, { width: "min" }, /* @__PURE__ */ React9.createElement(Flex4, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, (() => {
2520
+ const resolvedRowActions = typeof rowActions === "function" ? rowActions(item.row) : rowActions;
2521
+ const actions = Array.isArray(resolvedRowActions) ? resolvedRowActions : [];
2522
+ return actions.map((action, i) => /* @__PURE__ */ React9.createElement(
2523
+ Button3,
2524
+ {
2525
+ key: i,
2526
+ variant: action.variant || "transparent",
2527
+ size: "extra-small",
2528
+ onClick: () => action.onClick(item.row)
2529
+ },
2530
+ action.icon && /* @__PURE__ */ React9.createElement(Icon, { name: action.icon, size: "sm" }),
2531
+ action.label && ` ${action.label}`
2532
+ ));
2533
+ })()))) : renderRow(item.row)
2534
+ )),
2535
+ (footer || columns.some((col) => col.footer)) && /* @__PURE__ */ React9.createElement(TableFooter, null, typeof footer === "function" ? footer(footerData) : /* @__PURE__ */ React9.createElement(TableRow, null, selectable && /* @__PURE__ */ React9.createElement(TableHeader, { width: "min" }), showExpandColumn && /* @__PURE__ */ React9.createElement(TableHeader, { width: "min" }), columns.map((col) => {
2536
+ const footerDef = col.footer;
2537
+ const content = typeof footerDef === "function" ? footerDef(footerData) : footerDef || "";
2538
+ return /* @__PURE__ */ React9.createElement(TableHeader, { key: col.field, align: col.align }, content);
2539
+ }), showRowActionsColumn && /* @__PURE__ */ React9.createElement(TableHeader, { width: "min" })))
2540
+ ));
2541
+ };
2542
+ DataTable.displayName = "DataTable";
1324
2543
 
1325
2544
  // src/kanban/Kanban.jsx
1326
2545
  import React12, { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
1327
2546
 
2547
+ // src/kanban/kanbanLanes.js
2548
+ var UNASSIGNED_LANE_KEY = "__unassigned";
2549
+ var UNKNOWN_STAGE_KEY = "__unknown";
2550
+ var getLaneKey = (row, swimlaneBy) => {
2551
+ const raw = typeof swimlaneBy === "function" ? swimlaneBy(row) : row == null ? void 0 : row[swimlaneBy];
2552
+ if (raw == null || raw === "") return UNASSIGNED_LANE_KEY;
2553
+ return String(raw);
2554
+ };
2555
+ var orderLaneKeys = (seenKeys, swimlaneOrder) => {
2556
+ const seen = Array.isArray(seenKeys) ? seenKeys : [];
2557
+ if (!Array.isArray(swimlaneOrder) || swimlaneOrder.length === 0) return [...seen];
2558
+ const explicit = [];
2559
+ for (const key of swimlaneOrder) {
2560
+ const normalized = String(key);
2561
+ if (!explicit.includes(normalized)) explicit.push(normalized);
2562
+ }
2563
+ const rest = seen.filter((key) => !explicit.includes(key));
2564
+ return [...explicit, ...rest];
2565
+ };
2566
+ var partitionLanes = (rows, { swimlaneBy, swimlaneOrder } = {}) => {
2567
+ const rowsByLane = {};
2568
+ const firstSeen = [];
2569
+ for (const row of rows || []) {
2570
+ const key = getLaneKey(row, swimlaneBy);
2571
+ if (!rowsByLane[key]) {
2572
+ rowsByLane[key] = [];
2573
+ firstSeen.push(key);
2574
+ }
2575
+ rowsByLane[key].push(row);
2576
+ }
2577
+ const laneKeys = orderLaneKeys(firstSeen, swimlaneOrder);
2578
+ for (const key of laneKeys) {
2579
+ if (!rowsByLane[key]) rowsByLane[key] = [];
2580
+ }
2581
+ return { laneKeys, rowsByLane };
2582
+ };
2583
+ var resolveLaneLabel = (laneKey, swimlaneLabels, rows, unassignedLabel) => {
2584
+ if (typeof swimlaneLabels === "function") {
2585
+ const out = swimlaneLabels(laneKey, rows || []);
2586
+ if (out != null) return out;
2587
+ } else if (swimlaneLabels && typeof swimlaneLabels === "object") {
2588
+ const out = swimlaneLabels[laneKey];
2589
+ if (out != null) return out;
2590
+ }
2591
+ if (laneKey === UNASSIGNED_LANE_KEY) return unassignedLabel || "Unassigned";
2592
+ return String(laneKey);
2593
+ };
2594
+ var bucketRowsByStage = (rows, stages, getStage) => {
2595
+ const map = {};
2596
+ for (const stage of stages || []) map[stage.value] = [];
2597
+ for (const row of rows || []) {
2598
+ const key = getStage(row);
2599
+ if (map[key]) {
2600
+ map[key].push(row);
2601
+ } else if ((stages || []).length > 0) {
2602
+ if (!map[UNKNOWN_STAGE_KEY]) map[UNKNOWN_STAGE_KEY] = [];
2603
+ map[UNKNOWN_STAGE_KEY].push(row);
2604
+ }
2605
+ }
2606
+ return map;
2607
+ };
2608
+ var sortBuckets = (buckets, comparator) => {
2609
+ if (!comparator) return buckets;
2610
+ const out = {};
2611
+ for (const key of Object.keys(buckets || {})) {
2612
+ out[key] = [...buckets[key]].sort(comparator);
2613
+ }
2614
+ return out;
2615
+ };
2616
+ var resolveWipLimit = (stage, wipLimits) => {
2617
+ const override = wipLimits ? wipLimits[stage == null ? void 0 : stage.value] : void 0;
2618
+ const limit = override != null ? override : stage == null ? void 0 : stage.wipLimit;
2619
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit < 0) return null;
2620
+ return limit;
2621
+ };
2622
+ var computeStageCounts = (stages, buckets, stageMeta) => {
2623
+ const counts = {};
2624
+ for (const stage of stages || []) {
2625
+ const meta = stageMeta ? stageMeta[stage.value] : void 0;
2626
+ counts[stage.value] = meta && meta.totalCount != null ? meta.totalCount : ((buckets == null ? void 0 : buckets[stage.value]) || []).length;
2627
+ }
2628
+ return counts;
2629
+ };
2630
+ var evaluateWip = (stages, counts, wipLimits) => {
2631
+ const out = {};
2632
+ for (const stage of stages || []) {
2633
+ const limit = resolveWipLimit(stage, wipLimits);
2634
+ const count = (counts == null ? void 0 : counts[stage.value]) || 0;
2635
+ out[stage.value] = { count, limit, exceeded: limit != null && count > limit };
2636
+ }
2637
+ return out;
2638
+ };
2639
+ var findNewlyExceededWip = (prev, next) => {
2640
+ const events = [];
2641
+ for (const stageId of Object.keys(next || {})) {
2642
+ const entry = next[stageId];
2643
+ if (!entry || !entry.exceeded) continue;
2644
+ if (prev && prev[stageId] && prev[stageId].exceeded) continue;
2645
+ events.push({ stageId, count: entry.count, limit: entry.limit });
2646
+ }
2647
+ return events;
2648
+ };
2649
+
1328
2650
  // src/common-components/CollectionSortSelect.js
1329
2651
  import React10, { useId as useId2 } from "react";
1330
2652
  import { Select as Select3 } from "@hubspot/ui-extensions";
@@ -1596,7 +2918,7 @@ var StyledText = ({
1596
2918
  const nativeVariant = NATIVE_TAG_VARIANT_ALIASES[background == null ? void 0 : background.variant] ?? (background == null ? void 0 : background.variant) ?? "default";
1597
2919
  return React11.createElement(Tag3, { variant: nativeVariant }, resolvedText);
1598
2920
  }
1599
- const { src, width: w, height: h6 } = makeStyledTextDataUri(resolvedText, {
2921
+ const { src, width: w, height: h7 } = makeStyledTextDataUri(resolvedText, {
1600
2922
  variant,
1601
2923
  format,
1602
2924
  orientation,
@@ -1612,7 +2934,7 @@ var StyledText = ({
1612
2934
  return React11.createElement(Image3, {
1613
2935
  src,
1614
2936
  width: w,
1615
- height: h6,
2937
+ height: h7,
1616
2938
  alt: alt ?? String(resolvedText)
1617
2939
  });
1618
2940
  };
@@ -1638,75 +2960,1048 @@ import {
1638
2960
  Statistics,
1639
2961
  StatisticsItem,
1640
2962
  StatisticsTrend,
2963
+ StatusTag as StatusTag2,
1641
2964
  Tag as Tag4,
1642
2965
  Text as Text3,
1643
2966
  Tile as Tile2
1644
2967
  } from "@hubspot/ui-extensions";
1645
-
1646
- // src/utils/objectPath.js
1647
- var getByPath = (obj, path) => {
1648
- if (!path) return void 0;
1649
- if (typeof path === "function") return path(obj);
1650
- return String(path).split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
2968
+ var DEFAULT_DENSITY = "compact";
2969
+ var DEFAULT_MAX_CARDS = 10;
2970
+ var DEFAULT_MAX_EXPANDED = 50;
2971
+ var DEFAULT_COLUMN_WIDTH = 350;
2972
+ var MIN_COLUMN_WIDTH = 350;
2973
+ var DEFAULT_FILTER_INLINE_LIMIT = 4;
2974
+ var DEFAULT_SEARCH_DEBOUNCE = 250;
2975
+ var DEFAULT_TITLE_TRUNCATE = 60;
2976
+ var applyTruncate = (value, truncate, fallback) => {
2977
+ if (truncate === false) return value;
2978
+ if (typeof value !== "string") return value;
2979
+ const limit = typeof truncate === "number" ? truncate : truncate === true ? fallback || DEFAULT_TITLE_TRUNCATE : fallback || DEFAULT_TITLE_TRUNCATE;
2980
+ if (value.length <= limit) return value;
2981
+ return value.slice(0, limit).trimEnd() + "\u2026";
1651
2982
  };
1652
-
1653
- // src/utils/crmSearchAdapters.js
1654
- var EMPTY_ARRAY = [];
1655
- var EMPTY_OBJECT = {};
1656
- var isPlainObject = (value) => value != null && Object.prototype.toString.call(value) === "[object Object]";
1657
- var coerceError = (error) => {
1658
- if (!error) return false;
1659
- if (typeof error === "string") return error;
1660
- if (error.message) return error.message;
1661
- return true;
2983
+ var DEFAULT_LABELS3 = {
2984
+ search: "Search cards...",
2985
+ // Only the total is surfaced — callers asked for a single headline number
2986
+ // rather than a "loaded / total" fraction. Fall back to the bare label when
2987
+ // no total is known.
2988
+ showMore: (_shown, total) => total ? `Show more (${total})` : "Show more",
2989
+ showLess: "Show less",
2990
+ loadMore: (_loaded, total) => total ? `Load more (${total})` : "Load more",
2991
+ loadingMore: "Loading...",
2992
+ retryLoadMore: "Retry",
2993
+ emptyColumn: "\u2014",
2994
+ emptyTitle: "No cards",
2995
+ emptyMessage: "Nothing matches the current filters.",
2996
+ loading: "Loading board...",
2997
+ loadingMessage: "This should only take a moment.",
2998
+ errorTitle: "Something went wrong.",
2999
+ errorMessage: "An error occurred while loading data.",
3000
+ cardCount: (n) => String(n),
3001
+ // "5 / 4" — count vs WIP limit in the stage header. The slash format is the
3002
+ // standard kanban convention; override for tighter ("5/4") or verbose forms.
3003
+ wipCount: (count, limit) => `${count} / ${limit}`,
3004
+ overWip: "Over WIP",
3005
+ laneCount: (n) => String(n),
3006
+ unassignedLane: "Unassigned",
3007
+ moveTo: "Move",
3008
+ clearAll: "Clear all",
3009
+ selectAll: (count, label) => `Select all ${count} ${label}`,
3010
+ deselectAll: "Deselect all",
3011
+ selected: (count, label) => `${count}\xA0${label}\xA0selected`,
3012
+ filtersButton: "Filters",
3013
+ dateFrom: "From",
3014
+ dateTo: "To",
3015
+ sortButton: "Sort",
3016
+ sortAscending: "Ascending",
3017
+ sortDescending: "Descending",
3018
+ metricsButton: "Metrics"
1662
3019
  };
1663
- var pickArray = (response) => {
1664
- if (Array.isArray(response)) return response;
1665
- if (!response) return EMPTY_ARRAY;
1666
- return response.results || response.data || response.items || response.records || response.objects || EMPTY_ARRAY;
3020
+ var makeRotatedTagDataUri = (label) => makeStyledTextDataUri(label, {
3021
+ variant: "microcopy",
3022
+ format: { fontWeight: "demibold" },
3023
+ orientation: "vertical-down",
3024
+ background: { preset: "tag" }
3025
+ });
3026
+ var makeRotatedLabelDataUri = (label) => makeStyledTextDataUri(label, {
3027
+ variant: "bodytext",
3028
+ format: { fontWeight: "demibold" },
3029
+ orientation: "vertical-down"
3030
+ });
3031
+ var canStageReceiveRow = (stage, row, canMove) => {
3032
+ if (!stage) return false;
3033
+ if (typeof canMove === "function" && !canMove(row, stage.value)) return false;
3034
+ if (typeof stage.canEnter === "function" && !stage.canEnter(row)) return false;
3035
+ return true;
1667
3036
  };
1668
- var pickTotal = (response, fallbackLength) => {
1669
- var _a;
1670
- if (!response || Array.isArray(response)) return fallbackLength;
1671
- return response.total ?? response.totalCount ?? response.totalResults ?? ((_a = response.paging) == null ? void 0 : _a.total) ?? fallbackLength;
3037
+ var resolveDividers = (cardDividers, density) => {
3038
+ if (cardDividers === true) {
3039
+ return { afterTitle: true, afterSubtitle: true, afterBody: true, afterFooter: true };
3040
+ }
3041
+ if (cardDividers === false) {
3042
+ return { afterTitle: false, afterSubtitle: false, afterBody: false, afterFooter: false };
3043
+ }
3044
+ if (cardDividers && typeof cardDividers === "object") {
3045
+ return {
3046
+ afterTitle: cardDividers.afterTitle ?? false,
3047
+ afterSubtitle: cardDividers.afterSubtitle ?? false,
3048
+ afterBody: cardDividers.afterBody ?? false,
3049
+ afterFooter: cardDividers.afterFooter ?? false
3050
+ };
3051
+ }
3052
+ if (density === "comfortable") {
3053
+ return { afterTitle: true, afterSubtitle: true, afterBody: true, afterFooter: true };
3054
+ }
3055
+ return { afterTitle: false, afterSubtitle: false, afterBody: true, afterFooter: false };
1672
3056
  };
1673
- var normalizeCrmSearchRecord = (record, options = EMPTY_OBJECT) => {
1674
- const {
1675
- idField = "id",
1676
- objectIdField = "objectId",
1677
- propertiesKey = "properties",
1678
- flattenProperties = true,
1679
- propertyValueKey,
1680
- mapRecord
1681
- } = options;
1682
- if (mapRecord) return mapRecord(record);
1683
- const objectId = (record == null ? void 0 : record.objectId) ?? (record == null ? void 0 : record.id) ?? (record == null ? void 0 : record.hs_object_id) ?? getByPath(record, `${propertiesKey}.hs_object_id`);
1684
- const properties = (record == null ? void 0 : record[propertiesKey]) || EMPTY_OBJECT;
1685
- const flattened = {};
1686
- if (flattenProperties && isPlainObject(properties)) {
1687
- for (const [key, value] of Object.entries(properties)) {
1688
- flattened[key] = propertyValueKey && isPlainObject(value) ? value[propertyValueKey] : value;
1689
- }
3057
+ var partitionFields = (cardFields) => {
3058
+ const buckets = { title: null, subtitle: null, meta: [], body: [], footer: [] };
3059
+ for (const field of cardFields || []) {
3060
+ const placement = field.placement || "body";
3061
+ if (placement === "title" && !buckets.title) buckets.title = field;
3062
+ else if (placement === "subtitle" && !buckets.subtitle) buckets.subtitle = field;
3063
+ else if (placement === "meta") buckets.meta.push(field);
3064
+ else if (placement === "footer") buckets.footer.push(field);
3065
+ else buckets.body.push(field);
1690
3066
  }
1691
- return {
1692
- ...flattenProperties ? flattened : EMPTY_OBJECT,
1693
- ...record,
1694
- [idField]: objectId,
1695
- [objectIdField]: objectId,
1696
- [propertiesKey]: properties
1697
- };
3067
+ return buckets;
1698
3068
  };
1699
- var normalizeCrmSearchRows = (response, options = EMPTY_OBJECT) => {
1700
- const records = pickArray(response);
1701
- return records.map((record) => normalizeCrmSearchRecord(record, options));
3069
+ var resolveFieldValue = (field, row) => {
3070
+ if (!field) return void 0;
3071
+ if (field.field && row && Object.prototype.hasOwnProperty.call(row, field.field)) {
3072
+ return row[field.field];
3073
+ }
3074
+ return void 0;
1702
3075
  };
1703
- var STABLE_SORT_TIEBREAKER = { propertyName: "hs_object_id", direction: "ASCENDING" };
1704
- var withStableSort = (sorts) => {
1705
- const base = Array.isArray(sorts) ? sorts : [];
1706
- if (base.some((s) => s && s.propertyName === STABLE_SORT_TIEBREAKER.propertyName)) return base;
1707
- return [...base, STABLE_SORT_TIEBREAKER];
3076
+ var resolveHref = (href, row) => {
3077
+ if (!href) return null;
3078
+ if (typeof href === "function") return href(row);
3079
+ return href;
1708
3080
  };
1709
- var buildCrmSearchConfig = (params = EMPTY_OBJECT, options = EMPTY_OBJECT) => {
3081
+ var KanbanCard = ({
3082
+ row,
3083
+ rowId,
3084
+ stage,
3085
+ stages,
3086
+ fields,
3087
+ density,
3088
+ dividers,
3089
+ bodyAs,
3090
+ maxBodyLines,
3091
+ stageControl,
3092
+ stageControlPlacement,
3093
+ canMove,
3094
+ onStageChangeRequest,
3095
+ isChanging,
3096
+ selectable,
3097
+ selected,
3098
+ onToggleSelect,
3099
+ labels
3100
+ }) => {
3101
+ const titleHref = fields.title ? resolveHref(fields.title.href, row) : null;
3102
+ const rawTitleValue = fields.title ? fields.title.render ? fields.title.render(resolveFieldValue(fields.title, row), row) : resolveFieldValue(fields.title, row) : null;
3103
+ const titleValue = fields.title && typeof rawTitleValue === "string" ? applyTruncate(rawTitleValue, fields.title.truncate, DEFAULT_TITLE_TRUNCATE) : rawTitleValue;
3104
+ const titleNode = titleHref ? /* @__PURE__ */ React12.createElement(Link3, { href: titleHref }, titleValue) : /* @__PURE__ */ React12.createElement(Text3, { format: { fontWeight: "demibold" } }, titleValue);
3105
+ const metaNodes = fields.meta.filter((f) => !f.visible || f.visible(row)).map((f) => {
3106
+ const val = resolveFieldValue(f, row);
3107
+ return /* @__PURE__ */ React12.createElement(Text3, { key: f.field || f.label, variant: "microcopy" }, f.render ? f.render(val, row) : val);
3108
+ });
3109
+ const showSubtitle = density === "comfortable" && fields.subtitle;
3110
+ const subtitleNode = showSubtitle ? fields.subtitle.render ? fields.subtitle.render(resolveFieldValue(fields.subtitle, row), row) : resolveFieldValue(fields.subtitle, row) : null;
3111
+ const bodyFields = fields.body.filter((f) => !f.visible || f.visible(row)).slice(0, maxBodyLines);
3112
+ const footerFields = fields.footer.filter((f) => !f.visible || f.visible(row));
3113
+ const footerAlerts = footerFields.slice(0, -1);
3114
+ const footerActionsField = footerFields.length > 0 ? footerFields[footerFields.length - 1] : null;
3115
+ const renderFooterField = (f, idx) => {
3116
+ const val = resolveFieldValue(f, row);
3117
+ const rendered = f.render ? f.render(val, row) : val;
3118
+ const key = f.key || f.field || f.label || `footer-${idx}`;
3119
+ return /* @__PURE__ */ React12.createElement(React12.Fragment, { key }, rendered);
3120
+ };
3121
+ const stageControlNode = stageControl === "none" ? null : /* @__PURE__ */ React12.createElement(
3122
+ StageControl,
3123
+ {
3124
+ row,
3125
+ rowId,
3126
+ currentStage: stage,
3127
+ stages,
3128
+ canMove,
3129
+ isChanging,
3130
+ mode: stageControl,
3131
+ onStageChangeRequest,
3132
+ labels
3133
+ }
3134
+ );
3135
+ const titleRow = /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", justify: "between", align: "center", gap: "sm" }, /* @__PURE__ */ React12.createElement(Box3, { flex: 1 }, titleNode), selectable ? /* @__PURE__ */ React12.createElement(
3136
+ Checkbox2,
3137
+ {
3138
+ name: `kanban-select-${rowId}`,
3139
+ checked: selected,
3140
+ onChange: () => onToggleSelect(rowId)
3141
+ }
3142
+ ) : null);
3143
+ const metaRow = metaNodes.length === 0 ? null : /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", justify: "end", align: "center", gap: "xs" }, metaNodes);
3144
+ const bodyRow = bodyFields.length === 0 ? null : bodyAs === "descriptionList" ? /* @__PURE__ */ React12.createElement(DescriptionList, { direction: "row" }, bodyFields.map((f, idx) => {
3145
+ const val = resolveFieldValue(f, row);
3146
+ const rendered = f.render ? f.render(val, row) : val ?? "\u2014";
3147
+ const key = f.key || f.field || f.label || `body-${idx}`;
3148
+ return /* @__PURE__ */ React12.createElement(DescriptionListItem, { key, label: f.label || "" }, rendered);
3149
+ })) : /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", gap: "flush" }, bodyFields.map((f, idx) => {
3150
+ const val = resolveFieldValue(f, row);
3151
+ const rendered = f.render ? f.render(val, row) : val ?? "\u2014";
3152
+ const key = f.key || f.field || f.label || `body-${idx}`;
3153
+ return /* @__PURE__ */ React12.createElement(Text3, { key, variant: "microcopy" }, f.label ? /* @__PURE__ */ React12.createElement(Text3, { inline: true, variant: "microcopy" }, `${f.label}: `) : null, rendered);
3154
+ }));
3155
+ const footerAlertsNode = footerAlerts.length === 0 ? null : /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", gap: "xs" }, footerAlerts.map((f, idx) => renderFooterField(f, idx)));
3156
+ const footerActionsNode = footerActionsField ? renderFooterField(footerActionsField, footerFields.length - 1) : null;
3157
+ const inlineStageControl = stageControlPlacement === "inline" ? stageControlNode : null;
3158
+ const separateRowStageControl = stageControlPlacement === "separateRow" ? stageControlNode : null;
3159
+ const footerMainRow = inlineStageControl || footerActionsNode ? /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", justify: "between", align: "start", gap: "sm" }, inlineStageControl ? /* @__PURE__ */ React12.createElement(Box3, { alignSelf: "center" }, inlineStageControl) : /* @__PURE__ */ React12.createElement(Box3, null), footerActionsNode ? /* @__PURE__ */ React12.createElement(Box3, { flex: 1 }, /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", justify: "end", align: "start" }, footerActionsNode)) : null) : null;
3160
+ const footerRow = !footerAlertsNode && !footerMainRow ? null : /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", gap: "xs" }, footerAlertsNode, footerMainRow);
3161
+ return /* @__PURE__ */ React12.createElement(Tile2, { compact: density === "compact" }, /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", gap: density === "compact" ? "xs" : "sm" }, titleRow, dividers.afterTitle && (metaRow || bodyRow || footerRow || separateRowStageControl) ? /* @__PURE__ */ React12.createElement(Divider, null) : null, subtitleNode ? /* @__PURE__ */ React12.createElement(Text3, { variant: "microcopy" }, subtitleNode) : null, dividers.afterSubtitle && subtitleNode && (metaRow || bodyRow || footerRow || separateRowStageControl) ? /* @__PURE__ */ React12.createElement(Divider, null) : null, metaRow, bodyRow, dividers.afterBody && bodyRow && (footerRow || separateRowStageControl) ? /* @__PURE__ */ React12.createElement(Divider, null) : null, footerRow, dividers.afterFooter && footerRow && separateRowStageControl ? /* @__PURE__ */ React12.createElement(Divider, null) : null, separateRowStageControl));
3162
+ };
3163
+ var StageControl = ({
3164
+ row,
3165
+ rowId,
3166
+ currentStage,
3167
+ stages,
3168
+ canMove,
3169
+ isChanging,
3170
+ mode,
3171
+ onStageChangeRequest,
3172
+ labels
3173
+ }) => {
3174
+ if (isChanging) {
3175
+ return /* @__PURE__ */ React12.createElement(LoadingSpinner, { size: "xs" });
3176
+ }
3177
+ const availableStages = (stages || []).filter(
3178
+ (stage) => stage.value === currentStage.value || canStageReceiveRow(stage, row, canMove)
3179
+ );
3180
+ if (mode === "menu") {
3181
+ const targetStages = availableStages.filter((stage) => stage.value !== currentStage.value);
3182
+ if (targetStages.length === 0) {
3183
+ return /* @__PURE__ */ React12.createElement(Button4, { variant: "transparent", size: "extra-small", disabled: true }, labels.moveTo);
3184
+ }
3185
+ return /* @__PURE__ */ React12.createElement(
3186
+ Dropdown,
3187
+ {
3188
+ variant: "transparent",
3189
+ buttonText: labels.moveTo,
3190
+ buttonSize: "xs"
3191
+ },
3192
+ targetStages.map((stage) => /* @__PURE__ */ React12.createElement(
3193
+ Dropdown.ButtonItem,
3194
+ {
3195
+ key: stage.value,
3196
+ onClick: () => onStageChangeRequest(row, stage.value, currentStage.value)
3197
+ },
3198
+ stage.shortLabel || stage.label
3199
+ ))
3200
+ );
3201
+ }
3202
+ return /* @__PURE__ */ React12.createElement(
3203
+ Select4,
3204
+ {
3205
+ name: `stage-${rowId}`,
3206
+ label: "",
3207
+ value: currentStage.value,
3208
+ onChange: (val) => {
3209
+ if (val !== currentStage.value) onStageChangeRequest(row, val, currentStage.value);
3210
+ },
3211
+ options: availableStages.map((stage) => ({
3212
+ label: stage.shortLabel || stage.label,
3213
+ value: stage.value
3214
+ }))
3215
+ }
3216
+ );
3217
+ };
3218
+ var KanbanColumn = ({
3219
+ stage,
3220
+ rows,
3221
+ bucketCount,
3222
+ totalCount,
3223
+ hasMore,
3224
+ loading,
3225
+ error,
3226
+ onLoadMore,
3227
+ expanded,
3228
+ onToggleExpanded,
3229
+ collapsed,
3230
+ onToggleCollapsed,
3231
+ columnFooter,
3232
+ countDisplay,
3233
+ wip,
3234
+ compactEmpty,
3235
+ labels,
3236
+ children
3237
+ }) => {
3238
+ const hasWipLimit = wip != null && wip.limit != null;
3239
+ const countLabel = hasWipLimit ? labels.wipCount(wip.count, wip.limit) : labels.cardCount(totalCount != null ? totalCount : bucketCount);
3240
+ const overWipNode = wip && wip.exceeded ? /* @__PURE__ */ React12.createElement(StatusTag2, { variant: "warning" }, labels.overWip) : null;
3241
+ const countNode = countDisplay === "text" ? /* @__PURE__ */ React12.createElement(Text3, { format: { fontWeight: "demibold" } }, countLabel) : countDisplay === "none" ? null : /* @__PURE__ */ React12.createElement(Tag4, { variant: "default" }, countLabel);
3242
+ if (collapsed) {
3243
+ const rotated = makeRotatedLabelDataUri(stage.label);
3244
+ const rotatedCount = countDisplay === "none" ? null : makeRotatedTagDataUri(countLabel);
3245
+ const stageIdentifier = stage.icon ? /* @__PURE__ */ React12.createElement(Icon, { name: stage.icon, size: "sm", screenReaderText: stage.label }) : /* @__PURE__ */ React12.createElement(
3246
+ Image4,
3247
+ {
3248
+ src: rotated.src,
3249
+ width: rotated.width,
3250
+ height: rotated.height,
3251
+ alt: stage.label
3252
+ }
3253
+ );
3254
+ return /* @__PURE__ */ React12.createElement(Tile2, { compact: true }, /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", gap: "xs", align: "center" }, /* @__PURE__ */ React12.createElement(
3255
+ Button4,
3256
+ {
3257
+ variant: "transparent",
3258
+ size: "sm",
3259
+ onClick: onToggleCollapsed,
3260
+ tooltip: `Expand ${stage.label}`
3261
+ },
3262
+ /* @__PURE__ */ React12.createElement(Icon, { name: "right", size: "sm", screenReaderText: `Expand ${stage.label}` })
3263
+ ), stageIdentifier, rotatedCount ? /* @__PURE__ */ React12.createElement(
3264
+ Image4,
3265
+ {
3266
+ src: rotatedCount.src,
3267
+ width: rotatedCount.width,
3268
+ height: rotatedCount.height,
3269
+ alt: `${bucketCount} items`
3270
+ }
3271
+ ) : null));
3272
+ }
3273
+ if (compactEmpty && !loading && rows.length === 0 && bucketCount === 0) {
3274
+ return /* @__PURE__ */ React12.createElement(Tile2, { compact: true }, /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", align: "center", justify: "between", gap: "xs" }, /* @__PURE__ */ React12.createElement(Text3, { variant: "microcopy", format: { fontWeight: "demibold" } }, stage.shortLabel || stage.label), /* @__PURE__ */ React12.createElement(Text3, { variant: "microcopy", format: { italic: true } }, labels.emptyColumn)));
3275
+ }
3276
+ const footerContent = stage.footer ? stage.footer(rows) : columnFooter ? columnFooter(rows, stage) : null;
3277
+ return /* @__PURE__ */ React12.createElement(Tile2, { compact: true }, /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", gap: "xs" }, /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", align: "center", justify: "between", gap: "xs" }, /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React12.createElement(Text3, { format: { fontWeight: "demibold" } }, stage.shortLabel || stage.label), countNode, overWipNode, loading ? /* @__PURE__ */ React12.createElement(LoadingSpinner, { size: "xs" }) : null), /* @__PURE__ */ React12.createElement(Button4, { variant: "transparent", size: "sm", onClick: onToggleCollapsed, tooltip: "Collapse" }, /* @__PURE__ */ React12.createElement(Icon, { name: "left", size: "sm", screenReaderText: `Collapse ${stage.label}` }))), footerContent ? /* @__PURE__ */ React12.createElement(Text3, { variant: "microcopy" }, footerContent) : null, /* @__PURE__ */ React12.createElement(Divider, null), children, error ? /* @__PURE__ */ React12.createElement(Alert, { variant: "danger", title: labels.errorTitle }, /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", gap: "xs", align: "center" }, /* @__PURE__ */ React12.createElement(Text3, { variant: "microcopy" }, error), onLoadMore ? /* @__PURE__ */ React12.createElement(Button4, { variant: "transparent", size: "xs", onClick: () => onLoadMore(stage.value) }, labels.retryLoadMore) : null)) : null, !error && hasMore && onLoadMore && !loading && bucketCount > 0 ? /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", justify: "center" }, /* @__PURE__ */ React12.createElement(Link3, { onClick: () => onLoadMore(stage.value) }, labels.loadMore(bucketCount, totalCount))) : null, !error && loading && hasMore ? /* @__PURE__ */ React12.createElement(LoadingSpinner, { size: "sm", layout: "centered", label: labels.loadingMore }) : null, !error && !hasMore && bucketCount > rows.length ? /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", justify: "center" }, /* @__PURE__ */ React12.createElement(Link3, { onClick: onToggleExpanded }, expanded ? labels.showLess : labels.showMore(rows.length, bucketCount))) : null, rows.length === 0 && bucketCount === 0 && !loading ? /* @__PURE__ */ React12.createElement(Text3, { variant: "microcopy", format: { italic: true } }, labels.emptyColumn) : null));
3278
+ };
3279
+ var renderMetricsPanel = (metrics) => {
3280
+ if (!metrics) return null;
3281
+ if (!Array.isArray(metrics)) return metrics;
3282
+ if (metrics.length === 0) return null;
3283
+ return /* @__PURE__ */ React12.createElement(Statistics, null, metrics.map((m, i) => /* @__PURE__ */ React12.createElement(
3284
+ StatisticsItem,
3285
+ {
3286
+ key: m.id || m.label || i,
3287
+ label: m.label,
3288
+ number: m.number != null ? String(m.number) : ""
3289
+ },
3290
+ m.trend ? /* @__PURE__ */ React12.createElement(
3291
+ StatisticsTrend,
3292
+ {
3293
+ direction: m.trend.direction || "increase",
3294
+ value: m.trend.value,
3295
+ color: m.trend.color
3296
+ }
3297
+ ) : null
3298
+ )));
3299
+ };
3300
+ var KanbanToolbar = ({
3301
+ showSearch,
3302
+ searchValue,
3303
+ searchPlaceholder,
3304
+ onSearchChange,
3305
+ filters,
3306
+ filterValues,
3307
+ onFilterChange,
3308
+ filterInlineLimit,
3309
+ showFilterBadges,
3310
+ showClearFiltersButton,
3311
+ activeChips,
3312
+ onFilterRemove,
3313
+ sortOptions,
3314
+ sortValue,
3315
+ onSortChange,
3316
+ showMetricsButton,
3317
+ metricsPanel,
3318
+ onToggleMetrics,
3319
+ labels,
3320
+ toolbarLeftFlex,
3321
+ toolbarRightFlex
3322
+ }) => {
3323
+ const rightControls = (sortOptions == null ? void 0 : sortOptions.length) > 0 || showMetricsButton ? /* @__PURE__ */ React12.createElement(React12.Fragment, null, sortOptions && sortOptions.length > 0 ? /* @__PURE__ */ React12.createElement(
3324
+ CollectionSortSelect,
3325
+ {
3326
+ name: "kanban-sort",
3327
+ value: sortValue,
3328
+ placeholder: labels.sortButton,
3329
+ options: sortOptions,
3330
+ onChange: onSortChange
3331
+ }
3332
+ ) : null, showMetricsButton ? /* @__PURE__ */ React12.createElement(Button4, { variant: "secondary", size: "small", onClick: onToggleMetrics }, /* @__PURE__ */ React12.createElement(Icon, { name: "gauge", size: "sm" }), " ", labels.metricsButton) : null) : null;
3333
+ return /* @__PURE__ */ React12.createElement(
3334
+ CollectionToolbar,
3335
+ {
3336
+ search: {
3337
+ visible: showSearch,
3338
+ name: "kanban-search",
3339
+ placeholder: searchPlaceholder,
3340
+ value: searchValue,
3341
+ onChange: onSearchChange
3342
+ },
3343
+ filters: {
3344
+ items: filters,
3345
+ values: filterValues,
3346
+ inlineLimit: filterInlineLimit,
3347
+ namePrefix: "kanban-filter",
3348
+ onChange: onFilterChange,
3349
+ labels
3350
+ },
3351
+ chips: {
3352
+ items: activeChips,
3353
+ showBadges: showFilterBadges,
3354
+ showClearAll: showClearFiltersButton,
3355
+ clearAllLabel: labels.clearAll,
3356
+ onRemove: onFilterRemove
3357
+ },
3358
+ right: rightControls,
3359
+ footer: metricsPanel,
3360
+ labels,
3361
+ leftFlex: toolbarLeftFlex,
3362
+ rightFlex: toolbarRightFlex
3363
+ }
3364
+ );
3365
+ };
3366
+ var DefaultSelectionBar = ({
3367
+ selectedIds,
3368
+ selectedCount,
3369
+ displayCount,
3370
+ countLabel,
3371
+ allSelected,
3372
+ onSelectAll,
3373
+ onDeselectAll,
3374
+ selectionActions,
3375
+ labels
3376
+ }) => {
3377
+ const pluralForCount = (n) => countLabel(n);
3378
+ return /* @__PURE__ */ React12.createElement(Tile2, { compact: true }, /* @__PURE__ */ React12.createElement(Inline, { align: "center", justify: "between", gap: "small" }, /* @__PURE__ */ React12.createElement(Inline, { align: "center", gap: "small" }, /* @__PURE__ */ React12.createElement(Text3, { inline: true, format: { fontWeight: "demibold" } }, typeof labels.selected === "function" ? labels.selected(selectedCount, pluralForCount(selectedCount)) : `${selectedCount} selected`), !allSelected ? /* @__PURE__ */ React12.createElement(Button4, { variant: "transparent", size: "extra-small", onClick: onSelectAll }, typeof labels.selectAll === "function" ? labels.selectAll(displayCount, pluralForCount(displayCount)) : labels.selectAll) : null, /* @__PURE__ */ React12.createElement(Button4, { variant: "transparent", size: "extra-small", onClick: onDeselectAll }, labels.deselectAll)), (selectionActions || []).length > 0 ? /* @__PURE__ */ React12.createElement(Inline, { align: "center", gap: "extra-small" }, selectionActions.map((action, i) => /* @__PURE__ */ React12.createElement(
3379
+ Button4,
3380
+ {
3381
+ key: action.key || action.label || i,
3382
+ variant: action.variant || "transparent",
3383
+ size: "extra-small",
3384
+ onClick: () => action.onClick([...selectedIds])
3385
+ },
3386
+ action.icon ? /* @__PURE__ */ React12.createElement(Icon, { name: action.icon, size: "sm" }) : null,
3387
+ " ",
3388
+ action.label
3389
+ ))) : null));
3390
+ };
3391
+ var Kanban = ({
3392
+ // --- Data ---
3393
+ data = [],
3394
+ stages = [],
3395
+ groupBy = "status",
3396
+ rowIdField = "id",
3397
+ // --- Card rendering ---
3398
+ renderCard,
3399
+ cardFields,
3400
+ cardDensity = DEFAULT_DENSITY,
3401
+ cardDividers,
3402
+ cardBodyAs = "descriptionList",
3403
+ maxBodyLines,
3404
+ maxCardsPerColumn = DEFAULT_MAX_CARDS,
3405
+ maxCardsExpanded = DEFAULT_MAX_EXPANDED,
3406
+ expandedStages,
3407
+ onExpandedStagesChange,
3408
+ // --- Per-stage pagination ---
3409
+ stageMeta,
3410
+ onLoadMore,
3411
+ // --- WIP limits ---
3412
+ wipLimits,
3413
+ onWipExceeded,
3414
+ // --- Swimlanes ---
3415
+ swimlaneBy,
3416
+ swimlaneLabels,
3417
+ swimlaneOrder,
3418
+ collapseLanes = true,
3419
+ collapsedLanes,
3420
+ defaultCollapsedLanes,
3421
+ onCollapsedLanesChange,
3422
+ metricsPerLane = false,
3423
+ // --- Selection ---
3424
+ selectable = false,
3425
+ selectedIds,
3426
+ onSelectionChange,
3427
+ selectionActions,
3428
+ recordLabel,
3429
+ selectionResetKey,
3430
+ resetSelectionOnQueryChange = true,
3431
+ showSelectionBar = true,
3432
+ renderSelectionBar,
3433
+ // --- Stage transitions ---
3434
+ stageControl,
3435
+ stageControlPlacement,
3436
+ onStageChange,
3437
+ isStageChanging,
3438
+ canMove,
3439
+ // --- Toolbar ---
3440
+ showSearch = true,
3441
+ searchFields,
3442
+ searchPlaceholder,
3443
+ searchDebounce = DEFAULT_SEARCH_DEBOUNCE,
3444
+ fuzzySearch = false,
3445
+ fuzzyOptions,
3446
+ filters,
3447
+ filterInlineLimit = DEFAULT_FILTER_INLINE_LIMIT,
3448
+ showFilterBadges = true,
3449
+ showClearFiltersButton,
3450
+ toolbarLeftFlex = 3,
3451
+ toolbarRightFlex = 1,
3452
+ sortOptions,
3453
+ defaultSort,
3454
+ sort,
3455
+ onSortChange,
3456
+ // --- Column level ---
3457
+ columnFooter,
3458
+ columnWidth = DEFAULT_COLUMN_WIDTH,
3459
+ countDisplay = "tag",
3460
+ collapsedStages,
3461
+ onCollapsedStagesChange,
3462
+ // --- Metrics panel ---
3463
+ metrics,
3464
+ // Array of stat items or a ReactNode for full override
3465
+ showMetrics: controlledShowMetrics,
3466
+ onMetricsToggle,
3467
+ // --- State (controlled) ---
3468
+ searchValue,
3469
+ onSearchChange,
3470
+ filterValues,
3471
+ onFilterChange,
3472
+ onParamsChange,
3473
+ loading = false,
3474
+ error,
3475
+ // --- Labels ---
3476
+ labels: labelsProp,
3477
+ renderEmptyState,
3478
+ renderLoadingState,
3479
+ renderErrorState
3480
+ }) => {
3481
+ var _a;
3482
+ const labels = useMemo2(() => ({ ...DEFAULT_LABELS3, ...labelsProp || {} }), [labelsProp]);
3483
+ const [internalSearch, setInternalSearch] = useState3(searchValue != null ? searchValue : "");
3484
+ const [internalFilters, setInternalFilters] = useState3(() => getEmptyFilterValues(filters));
3485
+ const [internalSort, setInternalSort] = useState3(defaultSort || (((_a = sortOptions == null ? void 0 : sortOptions[0]) == null ? void 0 : _a.value) ?? ""));
3486
+ const [internalCollapsed, setInternalCollapsed] = useState3([]);
3487
+ const [internalCollapsedLanes, setInternalCollapsedLanes] = useState3(
3488
+ () => defaultCollapsedLanes || []
3489
+ );
3490
+ const [internalExpanded, setInternalExpanded] = useState3([]);
3491
+ const [internalSelection, setInternalSelection] = useState3([]);
3492
+ const [internalShowMetrics, setInternalShowMetrics] = useState3(false);
3493
+ const [transitionPrompts, setTransitionPrompts] = useState3({});
3494
+ const resolvedShowMetrics = controlledShowMetrics != null ? controlledShowMetrics : internalShowMetrics;
3495
+ const toggleMetrics = useCallback3(() => {
3496
+ const next = !resolvedShowMetrics;
3497
+ if (onMetricsToggle) onMetricsToggle(next);
3498
+ if (controlledShowMetrics == null) setInternalShowMetrics(next);
3499
+ }, [resolvedShowMetrics, onMetricsToggle, controlledShowMetrics]);
3500
+ const effectiveColumnWidth = Math.max(MIN_COLUMN_WIDTH, columnWidth || DEFAULT_COLUMN_WIDTH);
3501
+ const resolvedSearch = searchValue != null ? searchValue : internalSearch;
3502
+ const searchInputValue = searchDebounce > 0 ? internalSearch : resolvedSearch;
3503
+ const resolvedFilters = filterValues != null ? filterValues : internalFilters;
3504
+ const resolvedSort = sort != null ? sort : internalSort;
3505
+ const resolvedCollapsed = collapsedStages != null ? collapsedStages : internalCollapsed;
3506
+ const resolvedCollapsedLanes = collapsedLanes != null ? collapsedLanes : internalCollapsedLanes;
3507
+ const resolvedExpanded = expandedStages != null ? expandedStages : internalExpanded;
3508
+ const resolvedSelection = selectedIds != null ? selectedIds : internalSelection;
3509
+ const searchEnabled = showSearch && Array.isArray(searchFields) && searchFields.length > 0;
3510
+ const stagesByValue = useMemo2(() => {
3511
+ const map = {};
3512
+ for (const stage of stages || []) {
3513
+ map[stage.value] = stage;
3514
+ }
3515
+ return map;
3516
+ }, [stages]);
3517
+ const fireParamsChange = useCallback3((overrides = {}) => {
3518
+ if (!onParamsChange) return;
3519
+ onParamsChange({
3520
+ search: overrides.search != null ? overrides.search : resolvedSearch,
3521
+ filters: overrides.filters != null ? overrides.filters : resolvedFilters,
3522
+ sort: overrides.sort != null ? overrides.sort : resolvedSort || null,
3523
+ collapsedStages: overrides.collapsedStages != null ? overrides.collapsedStages : resolvedCollapsed,
3524
+ collapsedLanes: overrides.collapsedLanes != null ? overrides.collapsedLanes : resolvedCollapsedLanes
3525
+ });
3526
+ }, [onParamsChange, resolvedCollapsed, resolvedCollapsedLanes, resolvedFilters, resolvedSearch, resolvedSort]);
3527
+ const lastAppliedSearchRef = useRef3(searchValue != null ? searchValue : "");
3528
+ useEffect3(() => {
3529
+ if (searchValue == null) return;
3530
+ if (searchValue === lastAppliedSearchRef.current) return;
3531
+ lastAppliedSearchRef.current = searchValue;
3532
+ setInternalSearch(searchValue);
3533
+ }, [searchValue]);
3534
+ const dispatchSearch = useCallback3(
3535
+ (val) => {
3536
+ lastAppliedSearchRef.current = val;
3537
+ if (onSearchChange) onSearchChange(val);
3538
+ fireParamsChange({ search: val });
3539
+ },
3540
+ [fireParamsChange, onSearchChange]
3541
+ );
3542
+ const dispatchSearchDebounced = useDebouncedDispatch(internalSearch, searchDebounce, dispatchSearch);
3543
+ const handleSearch = useCallback3(
3544
+ (val) => {
3545
+ setInternalSearch(val);
3546
+ dispatchSearchDebounced(val);
3547
+ },
3548
+ [dispatchSearchDebounced]
3549
+ );
3550
+ const handleFilter = useCallback3(
3551
+ (name, val) => {
3552
+ const next = { ...resolvedFilters, [name]: val };
3553
+ if (filterValues == null) setInternalFilters(next);
3554
+ if (onFilterChange) onFilterChange(next);
3555
+ fireParamsChange({ filters: next });
3556
+ },
3557
+ [fireParamsChange, onFilterChange, filterValues, resolvedFilters]
3558
+ );
3559
+ const handleFilterRemove = useCallback3(
3560
+ (key) => {
3561
+ const next = resetFilterValues(filters, resolvedFilters, key);
3562
+ if (filterValues == null) setInternalFilters(next);
3563
+ if (onFilterChange) onFilterChange(next);
3564
+ fireParamsChange({ filters: next });
3565
+ },
3566
+ [filters, filterValues, fireParamsChange, onFilterChange, resolvedFilters]
3567
+ );
3568
+ const handleSort = useCallback3(
3569
+ (val) => {
3570
+ if (onSortChange) onSortChange(val);
3571
+ if (sort == null) setInternalSort(val);
3572
+ fireParamsChange({ sort: val });
3573
+ },
3574
+ [fireParamsChange, onSortChange, sort]
3575
+ );
3576
+ const handleCollapsed = useCallback3(
3577
+ (stageValue) => {
3578
+ const next = resolvedCollapsed.includes(stageValue) ? resolvedCollapsed.filter((v) => v !== stageValue) : [...resolvedCollapsed, stageValue];
3579
+ if (onCollapsedStagesChange) onCollapsedStagesChange(next);
3580
+ if (collapsedStages == null) setInternalCollapsed(next);
3581
+ fireParamsChange({ collapsedStages: next });
3582
+ },
3583
+ [fireParamsChange, resolvedCollapsed, collapsedStages, onCollapsedStagesChange]
3584
+ );
3585
+ const handleLaneCollapsed = useCallback3(
3586
+ (laneKey) => {
3587
+ const next = resolvedCollapsedLanes.includes(laneKey) ? resolvedCollapsedLanes.filter((k) => k !== laneKey) : [...resolvedCollapsedLanes, laneKey];
3588
+ if (onCollapsedLanesChange) onCollapsedLanesChange(next);
3589
+ if (collapsedLanes == null) setInternalCollapsedLanes(next);
3590
+ fireParamsChange({ collapsedLanes: next });
3591
+ },
3592
+ [fireParamsChange, resolvedCollapsedLanes, collapsedLanes, onCollapsedLanesChange]
3593
+ );
3594
+ const handleExpanded = useCallback3(
3595
+ (stageValue) => {
3596
+ const next = resolvedExpanded.includes(stageValue) ? resolvedExpanded.filter((v) => v !== stageValue) : [...resolvedExpanded, stageValue];
3597
+ if (onExpandedStagesChange) onExpandedStagesChange(next);
3598
+ if (expandedStages == null) setInternalExpanded(next);
3599
+ },
3600
+ [resolvedExpanded, expandedStages, onExpandedStagesChange]
3601
+ );
3602
+ const handleToggleSelect = useCallback3(
3603
+ (rowId) => {
3604
+ const next = resolvedSelection.includes(rowId) ? resolvedSelection.filter((id) => id !== rowId) : [...resolvedSelection, rowId];
3605
+ if (onSelectionChange) onSelectionChange(next);
3606
+ if (selectedIds == null) setInternalSelection(next);
3607
+ },
3608
+ [resolvedSelection, selectedIds, onSelectionChange]
3609
+ );
3610
+ const clearTransitionPrompt = useCallback3((rowId) => {
3611
+ setTransitionPrompts((prev) => {
3612
+ if (!Object.prototype.hasOwnProperty.call(prev, rowId)) return prev;
3613
+ const next = { ...prev };
3614
+ delete next[rowId];
3615
+ return next;
3616
+ });
3617
+ }, []);
3618
+ const commitStageChange = useCallback3(
3619
+ (row, newStage, oldStage, result) => {
3620
+ clearTransitionPrompt(row[rowIdField]);
3621
+ if (onStageChange) onStageChange(row, newStage, oldStage, result);
3622
+ },
3623
+ [clearTransitionPrompt, onStageChange, rowIdField]
3624
+ );
3625
+ const selectionQueryKey = useMemo2(() => {
3626
+ if (!resetSelectionOnQueryChange) return "";
3627
+ return toStableKey({
3628
+ search: resolvedSearch,
3629
+ filters: resolvedFilters,
3630
+ sort: resolvedSort || null
3631
+ });
3632
+ }, [resetSelectionOnQueryChange, resolvedFilters, resolvedSearch, resolvedSort]);
3633
+ const combinedSelectionResetKey = useMemo2(
3634
+ () => `${selectionQueryKey}::${selectionResetKey == null ? "" : toStableKey(selectionResetKey)}`,
3635
+ [selectionQueryKey, selectionResetKey]
3636
+ );
3637
+ const clearSelection = useCallback3(() => setInternalSelection([]), []);
3638
+ useSelectionReset({
3639
+ resetKey: combinedSelectionResetKey,
3640
+ enabled: selectable,
3641
+ isControlled: selectedIds != null,
3642
+ clearSelection
3643
+ });
3644
+ const getStageFor = useCallback3(
3645
+ (row) => {
3646
+ if (typeof groupBy === "function") return groupBy(row);
3647
+ return row[groupBy];
3648
+ },
3649
+ [groupBy]
3650
+ );
3651
+ const filteredData = useMemo2(() => {
3652
+ let result = filterRows(data, filters, resolvedFilters);
3653
+ const searchLower = (resolvedSearch || "").toLowerCase().trim();
3654
+ if (searchEnabled && searchLower) {
3655
+ result = searchRows(result, searchLower, searchFields, {
3656
+ fuzzy: fuzzySearch,
3657
+ fuzzyOptions
3658
+ });
3659
+ }
3660
+ return result;
3661
+ }, [data, resolvedSearch, resolvedFilters, filters, searchEnabled, searchFields, fuzzySearch, fuzzyOptions]);
3662
+ const buckets = useMemo2(
3663
+ () => bucketRowsByStage(filteredData, stages, getStageFor),
3664
+ [filteredData, stages, getStageFor]
3665
+ );
3666
+ const sortComparator = useMemo2(() => {
3667
+ if (!sortOptions || !resolvedSort) return null;
3668
+ const opt = sortOptions.find((s) => s.value === resolvedSort);
3669
+ return (opt == null ? void 0 : opt.comparator) || null;
3670
+ }, [sortOptions, resolvedSort]);
3671
+ const sortedBuckets = useMemo2(() => sortBuckets(buckets, sortComparator), [buckets, sortComparator]);
3672
+ const hasLanes = swimlaneBy != null;
3673
+ const laneData = useMemo2(() => {
3674
+ if (!hasLanes) return null;
3675
+ return partitionLanes(filteredData, { swimlaneBy, swimlaneOrder });
3676
+ }, [hasLanes, filteredData, swimlaneBy, swimlaneOrder]);
3677
+ const laneBuckets = useMemo2(() => {
3678
+ if (!laneData) return null;
3679
+ const out = {};
3680
+ for (const laneKey of laneData.laneKeys) {
3681
+ out[laneKey] = sortBuckets(
3682
+ bucketRowsByStage(laneData.rowsByLane[laneKey] || [], stages, getStageFor),
3683
+ sortComparator
3684
+ );
3685
+ }
3686
+ return out;
3687
+ }, [laneData, stages, getStageFor, sortComparator]);
3688
+ const wipByStage = useMemo2(
3689
+ () => evaluateWip(stages, computeStageCounts(stages, buckets, stageMeta), wipLimits),
3690
+ [stages, buckets, stageMeta, wipLimits]
3691
+ );
3692
+ const prevWipRef = useRef3({});
3693
+ useEffect3(() => {
3694
+ const newlyExceeded = findNewlyExceededWip(prevWipRef.current, wipByStage);
3695
+ prevWipRef.current = wipByStage;
3696
+ if (!onWipExceeded) return;
3697
+ for (const event of newlyExceeded) {
3698
+ onWipExceeded(event.stageId, event.count, event.limit);
3699
+ }
3700
+ }, [wipByStage, onWipExceeded]);
3701
+ const activeChips = useMemo2(
3702
+ () => buildActiveFilterChips(filters, resolvedFilters),
3703
+ [filters, resolvedFilters]
3704
+ );
3705
+ const partitioned = useMemo2(() => partitionFields(cardFields || []), [cardFields]);
3706
+ const dividers = useMemo2(() => resolveDividers(cardDividers, cardDensity), [cardDividers, cardDensity]);
3707
+ const resolvedMaxBody = maxBodyLines || (cardDensity === "comfortable" ? 5 : 3);
3708
+ const resolvedStageControl = stageControl || (cardDensity === "comfortable" ? "select" : "menu");
3709
+ const resolvedStageControlPlacement = stageControlPlacement || (resolvedStageControl === "menu" ? "inline" : "separateRow");
3710
+ const handleStageChangeRequest = useCallback3(
3711
+ (row, newStage, oldStage) => {
3712
+ var _a2;
3713
+ if (!newStage || newStage === oldStage) return;
3714
+ const targetStage = stagesByValue[newStage];
3715
+ if (!targetStage || !canStageReceiveRow(targetStage, row, canMove)) return;
3716
+ const rowId = row[rowIdField];
3717
+ if ((_a2 = targetStage.onEnterRequired) == null ? void 0 : _a2.render) {
3718
+ setTransitionPrompts((prev) => ({
3719
+ ...prev,
3720
+ [rowId]: {
3721
+ row,
3722
+ fromStage: oldStage,
3723
+ toStage: newStage
3724
+ }
3725
+ }));
3726
+ return;
3727
+ }
3728
+ commitStageChange(row, newStage, oldStage);
3729
+ },
3730
+ [canMove, commitStageChange, rowIdField, stagesByValue]
3731
+ );
3732
+ const renderCardNode = useCallback3(
3733
+ (row, stage) => {
3734
+ var _a2;
3735
+ const rowId = row[rowIdField];
3736
+ const activePrompt = transitionPrompts[rowId];
3737
+ const promptStage = activePrompt ? stagesByValue[activePrompt.toStage] : null;
3738
+ if ((_a2 = promptStage == null ? void 0 : promptStage.onEnterRequired) == null ? void 0 : _a2.render) {
3739
+ return /* @__PURE__ */ React12.createElement(Tile2, { key: rowId, compact: cardDensity === "compact" }, promptStage.onEnterRequired.render({
3740
+ row: activePrompt.row,
3741
+ fromStage: activePrompt.fromStage,
3742
+ toStage: activePrompt.toStage,
3743
+ onConfirm: (result) => commitStageChange(activePrompt.row, activePrompt.toStage, activePrompt.fromStage, result),
3744
+ onCancel: () => clearTransitionPrompt(rowId)
3745
+ }));
3746
+ }
3747
+ if (renderCard) {
3748
+ return renderCard(row, {
3749
+ stage,
3750
+ isChanging: isStageChanging ? isStageChanging(row) : false,
3751
+ density: cardDensity,
3752
+ onStageChange: (newStage) => handleStageChangeRequest(row, newStage, stage.value)
3753
+ });
3754
+ }
3755
+ return /* @__PURE__ */ React12.createElement(
3756
+ KanbanCard,
3757
+ {
3758
+ key: rowId,
3759
+ row,
3760
+ rowId,
3761
+ stage,
3762
+ stages,
3763
+ fields: partitioned,
3764
+ density: cardDensity,
3765
+ dividers,
3766
+ bodyAs: cardBodyAs,
3767
+ maxBodyLines: resolvedMaxBody,
3768
+ stageControl: resolvedStageControl,
3769
+ stageControlPlacement: resolvedStageControlPlacement,
3770
+ canMove,
3771
+ onStageChangeRequest: handleStageChangeRequest,
3772
+ isChanging: isStageChanging ? isStageChanging(row) : false,
3773
+ selectable,
3774
+ selected: resolvedSelection.includes(rowId),
3775
+ onToggleSelect: handleToggleSelect,
3776
+ labels
3777
+ }
3778
+ );
3779
+ },
3780
+ [
3781
+ clearTransitionPrompt,
3782
+ commitStageChange,
3783
+ renderCard,
3784
+ rowIdField,
3785
+ partitioned,
3786
+ cardDensity,
3787
+ dividers,
3788
+ resolvedMaxBody,
3789
+ resolvedStageControl,
3790
+ resolvedStageControlPlacement,
3791
+ canMove,
3792
+ isStageChanging,
3793
+ selectable,
3794
+ resolvedSelection,
3795
+ handleStageChangeRequest,
3796
+ handleToggleSelect,
3797
+ labels,
3798
+ stages,
3799
+ stagesByValue,
3800
+ transitionPrompts
3801
+ ]
3802
+ );
3803
+ const totalMatching = filteredData.length;
3804
+ const selectedCount = resolvedSelection.length;
3805
+ const singular = ((recordLabel == null ? void 0 : recordLabel.singular) || "card").toLowerCase();
3806
+ const plural = ((recordLabel == null ? void 0 : recordLabel.plural) || "cards").toLowerCase();
3807
+ const countLabel = (n) => n === 1 ? singular : plural;
3808
+ const resolvedSearchPlaceholder = searchPlaceholder ?? ((recordLabel == null ? void 0 : recordLabel.plural) ? `Search ${plural}...` : labels.search);
3809
+ const selectionBarProps = {
3810
+ selectedIds: resolvedSelection,
3811
+ selectedCount,
3812
+ displayCount: totalMatching,
3813
+ countLabel,
3814
+ allSelected: selectedCount >= totalMatching && totalMatching > 0,
3815
+ onSelectAll: () => {
3816
+ const allIds = filteredData.map((r) => r[rowIdField]);
3817
+ if (onSelectionChange) onSelectionChange(allIds);
3818
+ if (selectedIds == null) setInternalSelection(allIds);
3819
+ },
3820
+ onDeselectAll: () => {
3821
+ if (onSelectionChange) onSelectionChange([]);
3822
+ if (selectedIds == null) setInternalSelection([]);
3823
+ },
3824
+ selectionActions: selectionActions || [],
3825
+ labels
3826
+ };
3827
+ const metricsProvided = metrics != null && (!Array.isArray(metrics) || metrics.length > 0);
3828
+ const perLaneMetricsActive = hasLanes && metricsPerLane && typeof metrics === "function";
3829
+ const globalMetricsContent = metricsProvided && !perLaneMetricsActive ? typeof metrics === "function" ? metrics(filteredData, null) : metrics : null;
3830
+ const renderStageColumns = (bucketMap, laneKey) => {
3831
+ const inLane = laneKey != null;
3832
+ return /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", gap: "sm", wrap: "nowrap" }, stages.map((stage) => {
3833
+ const stageRows = bucketMap[stage.value] || [];
3834
+ const meta = inLane ? void 0 : stageMeta == null ? void 0 : stageMeta[stage.value];
3835
+ const isExpanded = resolvedExpanded.includes(stage.value);
3836
+ const clamp = isExpanded ? maxCardsExpanded : maxCardsPerColumn;
3837
+ const visibleRows = stageRows.slice(0, clamp);
3838
+ const isCollapsed = resolvedCollapsed.includes(stage.value);
3839
+ const stageWip = wipByStage[stage.value];
3840
+ const wip = inLane ? (stageWip == null ? void 0 : stageWip.exceeded) ? { count: stageWip.count, limit: null, exceeded: true } : null : stageWip;
3841
+ return /* @__PURE__ */ React12.createElement(
3842
+ AutoGrid,
3843
+ {
3844
+ key: inLane ? `${laneKey}::${stage.value}` : stage.value,
3845
+ columnWidth: isCollapsed ? 72 : effectiveColumnWidth
3846
+ },
3847
+ /* @__PURE__ */ React12.createElement(
3848
+ KanbanColumn,
3849
+ {
3850
+ stage,
3851
+ rows: visibleRows,
3852
+ bucketCount: stageRows.length,
3853
+ totalCount: meta == null ? void 0 : meta.totalCount,
3854
+ hasMore: meta == null ? void 0 : meta.hasMore,
3855
+ loading: meta == null ? void 0 : meta.loading,
3856
+ error: meta == null ? void 0 : meta.error,
3857
+ onLoadMore: inLane ? void 0 : onLoadMore,
3858
+ expanded: isExpanded,
3859
+ onToggleExpanded: () => handleExpanded(stage.value),
3860
+ collapsed: isCollapsed,
3861
+ onToggleCollapsed: () => handleCollapsed(stage.value),
3862
+ columnFooter,
3863
+ countDisplay,
3864
+ wip,
3865
+ compactEmpty: inLane,
3866
+ labels
3867
+ },
3868
+ visibleRows.map((row) => renderCardNode(row, stage))
3869
+ )
3870
+ );
3871
+ }));
3872
+ };
3873
+ const mainContent = error ? renderErrorState ? renderErrorState({
3874
+ error,
3875
+ title: labels.errorTitle,
3876
+ message: typeof error === "string" ? error : labels.errorMessage
3877
+ }) : /* @__PURE__ */ React12.createElement(Alert, { variant: "danger", title: labels.errorTitle }, typeof error === "string" ? error : labels.errorMessage) : loading && data.length === 0 ? renderLoadingState ? renderLoadingState({ label: labels.loading }) : (
3878
+ // Same EmptyState layout as the empty state (just the "building" image +
3879
+ // a loading message) so loading and empty match with no layout shift.
3880
+ /* @__PURE__ */ React12.createElement(Tile2, null, /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React12.createElement(EmptyState2, { title: labels.loading, imageName: "building", layout: "vertical" }, /* @__PURE__ */ React12.createElement(Text3, null, labels.loadingMessage))))
3881
+ ) : filteredData.length === 0 ? renderEmptyState ? renderEmptyState({
3882
+ title: labels.emptyTitle,
3883
+ message: labels.emptyMessage
3884
+ }) : /* @__PURE__ */ React12.createElement(Tile2, null, /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React12.createElement(EmptyState2, { title: labels.emptyTitle, layout: "vertical" }, /* @__PURE__ */ React12.createElement(Text3, null, labels.emptyMessage)))) : hasLanes ? /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", gap: "md" }, ((laneData == null ? void 0 : laneData.laneKeys) || []).map((laneKey, laneIndex) => {
3885
+ const laneRows = laneData.rowsByLane[laneKey] || [];
3886
+ const laneLabel = resolveLaneLabel(laneKey, swimlaneLabels, laneRows, labels.unassignedLane);
3887
+ const laneLabelText = typeof laneLabel === "string" ? laneLabel : String(laneKey);
3888
+ const isLaneCollapsed = collapseLanes && resolvedCollapsedLanes.includes(laneKey);
3889
+ const laneCountLabel = labels.laneCount(laneRows.length);
3890
+ const laneCountNode = countDisplay === "none" ? null : countDisplay === "text" ? /* @__PURE__ */ React12.createElement(Text3, { format: { fontWeight: "demibold" } }, laneCountLabel) : /* @__PURE__ */ React12.createElement(Tag4, { variant: "default" }, laneCountLabel);
3891
+ const laneMetricsNode = !isLaneCollapsed && perLaneMetricsActive && resolvedShowMetrics ? renderMetricsPanel(metrics(laneRows, laneKey)) : null;
3892
+ return /* @__PURE__ */ React12.createElement(Flex5, { key: laneKey, direction: "column", gap: "xs" }, laneIndex > 0 ? /* @__PURE__ */ React12.createElement(Divider, null) : null, /* @__PURE__ */ React12.createElement(Flex5, { direction: "row", align: "center", gap: "xs" }, collapseLanes ? /* @__PURE__ */ React12.createElement(
3893
+ Button4,
3894
+ {
3895
+ variant: "transparent",
3896
+ size: "sm",
3897
+ onClick: () => handleLaneCollapsed(laneKey),
3898
+ tooltip: isLaneCollapsed ? `Expand ${laneLabelText}` : `Collapse ${laneLabelText}`
3899
+ },
3900
+ /* @__PURE__ */ React12.createElement(
3901
+ Icon,
3902
+ {
3903
+ name: isLaneCollapsed ? "right" : "down",
3904
+ size: "sm",
3905
+ screenReaderText: isLaneCollapsed ? `Expand ${laneLabelText}` : `Collapse ${laneLabelText}`
3906
+ }
3907
+ )
3908
+ ) : null, /* @__PURE__ */ React12.createElement(Text3, { format: { fontWeight: "demibold" } }, laneLabel), laneCountNode), laneMetricsNode, !isLaneCollapsed ? renderStageColumns((laneBuckets == null ? void 0 : laneBuckets[laneKey]) || {}, laneKey) : null);
3909
+ })) : renderStageColumns(sortedBuckets, null);
3910
+ const resolvedShowClearFiltersButton = showClearFiltersButton ?? showFilterBadges;
3911
+ return /* @__PURE__ */ React12.createElement(Flex5, { direction: "column", gap: "sm" }, /* @__PURE__ */ React12.createElement(
3912
+ KanbanToolbar,
3913
+ {
3914
+ showSearch: searchEnabled,
3915
+ searchValue: searchInputValue,
3916
+ searchPlaceholder: resolvedSearchPlaceholder,
3917
+ onSearchChange: handleSearch,
3918
+ filters,
3919
+ filterValues: resolvedFilters,
3920
+ onFilterChange: handleFilter,
3921
+ filterInlineLimit,
3922
+ showFilterBadges,
3923
+ showClearFiltersButton: resolvedShowClearFiltersButton,
3924
+ activeChips,
3925
+ onFilterRemove: handleFilterRemove,
3926
+ sortOptions,
3927
+ sortValue: resolvedSort,
3928
+ onSortChange: handleSort,
3929
+ showMetricsButton: metricsProvided,
3930
+ metricsPanel: resolvedShowMetrics && globalMetricsContent ? renderMetricsPanel(globalMetricsContent) : null,
3931
+ onToggleMetrics: toggleMetrics,
3932
+ labels,
3933
+ toolbarLeftFlex,
3934
+ toolbarRightFlex
3935
+ }
3936
+ ), showSelectionBar && selectable && selectedCount > 0 ? renderSelectionBar ? renderSelectionBar(selectionBarProps) : /* @__PURE__ */ React12.createElement(DefaultSelectionBar, { ...selectionBarProps }) : null, mainContent);
3937
+ };
3938
+ Kanban.displayName = "Kanban";
3939
+
3940
+ // src/utils/objectPath.js
3941
+ var getByPath = (obj, path) => {
3942
+ if (!path) return void 0;
3943
+ if (typeof path === "function") return path(obj);
3944
+ return String(path).split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
3945
+ };
3946
+
3947
+ // src/utils/crmSearchAdapters.js
3948
+ var EMPTY_ARRAY = [];
3949
+ var EMPTY_OBJECT = {};
3950
+ var EMPTY_CRM_PARAMS = { search: "", filters: {}, sort: null };
3951
+ var isPlainObject = (value) => value != null && Object.prototype.toString.call(value) === "[object Object]";
3952
+ var coerceError = (error) => {
3953
+ if (!error) return false;
3954
+ if (typeof error === "string") return error;
3955
+ if (error.message) return error.message;
3956
+ return true;
3957
+ };
3958
+ var pickArray = (response) => {
3959
+ if (Array.isArray(response)) return response;
3960
+ if (!response) return EMPTY_ARRAY;
3961
+ return response.results || response.data || response.items || response.records || response.objects || EMPTY_ARRAY;
3962
+ };
3963
+ var pickTotal = (response, fallbackLength) => {
3964
+ var _a;
3965
+ if (!response || Array.isArray(response)) return fallbackLength;
3966
+ return response.total ?? response.totalCount ?? response.totalResults ?? ((_a = response.paging) == null ? void 0 : _a.total) ?? fallbackLength;
3967
+ };
3968
+ var normalizeCrmSearchRecord = (record, options = EMPTY_OBJECT) => {
3969
+ const {
3970
+ idField = "id",
3971
+ objectIdField = "objectId",
3972
+ propertiesKey = "properties",
3973
+ flattenProperties = true,
3974
+ propertyValueKey,
3975
+ mapRecord
3976
+ } = options;
3977
+ if (mapRecord) return mapRecord(record);
3978
+ const objectId = (record == null ? void 0 : record.objectId) ?? (record == null ? void 0 : record.id) ?? (record == null ? void 0 : record.hs_object_id) ?? getByPath(record, `${propertiesKey}.hs_object_id`);
3979
+ const properties = (record == null ? void 0 : record[propertiesKey]) || EMPTY_OBJECT;
3980
+ const flattened = {};
3981
+ if (flattenProperties && isPlainObject(properties)) {
3982
+ for (const [key, value] of Object.entries(properties)) {
3983
+ flattened[key] = propertyValueKey && isPlainObject(value) ? value[propertyValueKey] : value;
3984
+ }
3985
+ }
3986
+ return {
3987
+ ...flattenProperties ? flattened : EMPTY_OBJECT,
3988
+ ...record,
3989
+ [idField]: objectId,
3990
+ [objectIdField]: objectId,
3991
+ [propertiesKey]: properties
3992
+ };
3993
+ };
3994
+ var normalizeCrmSearchRows = (response, options = EMPTY_OBJECT) => {
3995
+ const records = pickArray(response);
3996
+ return records.map((record) => normalizeCrmSearchRecord(record, options));
3997
+ };
3998
+ var STABLE_SORT_TIEBREAKER = { propertyName: "hs_object_id", direction: "ASCENDING" };
3999
+ var withStableSort = (sorts) => {
4000
+ const base = Array.isArray(sorts) ? sorts : [];
4001
+ if (base.some((s) => s && s.propertyName === STABLE_SORT_TIEBREAKER.propertyName)) return base;
4002
+ return [...base, STABLE_SORT_TIEBREAKER];
4003
+ };
4004
+ var buildCrmSearchConfig = (params = EMPTY_OBJECT, options = EMPTY_OBJECT) => {
1710
4005
  const {
1711
4006
  objectType,
1712
4007
  properties = EMPTY_ARRAY,
@@ -1810,7 +4105,393 @@ var CRM_OBJECT_TYPES = {
1810
4105
  deal: "0-3",
1811
4106
  deals: "0-3"
1812
4107
  };
4108
+ var prettifyPropertyName = (name) => String(name || "").replace(/^hs_/, "").replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
4109
+ var inferCrmColumns = (properties = EMPTY_ARRAY) => properties.map((property) => ({
4110
+ field: property,
4111
+ label: prettifyPropertyName(property),
4112
+ sortable: true
4113
+ }));
4114
+ var normalizeAutoFilterFields = (autoFilters, properties = EMPTY_ARRAY) => {
4115
+ if (!autoFilters) return EMPTY_ARRAY;
4116
+ if (Array.isArray(autoFilters)) return autoFilters;
4117
+ if (typeof autoFilters === "object" && Array.isArray(autoFilters.fields)) return autoFilters.fields;
4118
+ return properties.filter((property) => !["id", "objectId", "hs_object_id", "email", "firstname", "lastname", "name", "domain"].includes(property));
4119
+ };
4120
+ var buildAutoFiltersFromRows = ({ rows, fields, labelsRef, maxOptions = 25 }) => {
4121
+ if (!fields.length) return EMPTY_ARRAY;
4122
+ for (const row of rows || EMPTY_ARRAY) {
4123
+ for (const field of fields) {
4124
+ const value = getByPath(row, field);
4125
+ if (value == null || value === "" || Array.isArray(value) || isPlainObject(value)) continue;
4126
+ if (!labelsRef.current[field]) labelsRef.current[field] = /* @__PURE__ */ new Map();
4127
+ const map = labelsRef.current[field];
4128
+ if (map.size < maxOptions || map.has(value)) map.set(value, String(value));
4129
+ }
4130
+ }
4131
+ return fields.map((field) => {
4132
+ const map = labelsRef.current[field];
4133
+ if (!map || map.size === 0 || map.size > maxOptions) return null;
4134
+ return {
4135
+ name: field,
4136
+ label: prettifyPropertyName(field),
4137
+ placeholder: `Any ${prettifyPropertyName(field).toLowerCase()}`,
4138
+ options: Array.from(map.entries()).map(([value, label]) => ({ value, label }))
4139
+ };
4140
+ }).filter(Boolean);
4141
+ };
1813
4142
  var resolveCrmObjectType = (objectType) => CRM_OBJECT_TYPES[objectType] || objectType;
4143
+ var DEFAULT_CRM_FORMAT = { propertiesToFormat: "all" };
4144
+ var defaultCrmMapRecord = (record) => ({ objectId: record.objectId, ...record.properties });
4145
+ var stableStringify = (value) => {
4146
+ const normalize = (item) => {
4147
+ if (Array.isArray(item)) return item.map(normalize);
4148
+ if (isPlainObject(item)) {
4149
+ return Object.keys(item).sort().reduce((acc, key) => {
4150
+ acc[key] = normalize(item[key]);
4151
+ return acc;
4152
+ }, {});
4153
+ }
4154
+ if (typeof item === "function") return item.name || "[function]";
4155
+ return item;
4156
+ };
4157
+ try {
4158
+ return JSON.stringify(normalize(value));
4159
+ } catch {
4160
+ return String(value);
4161
+ }
4162
+ };
4163
+ var appendUniqueRows = (previousRows, nextRows, rowIdField) => {
4164
+ if (!previousRows.length) return nextRows || EMPTY_ARRAY;
4165
+ if (!(nextRows == null ? void 0 : nextRows.length)) return previousRows;
4166
+ const seen = new Set(previousRows.map((row) => getByPath(row, rowIdField) ?? (row == null ? void 0 : row.objectId) ?? (row == null ? void 0 : row.id)));
4167
+ const additions = nextRows.filter((row) => {
4168
+ const id = getByPath(row, rowIdField) ?? (row == null ? void 0 : row.objectId) ?? (row == null ? void 0 : row.id);
4169
+ if (id == null) return true;
4170
+ if (seen.has(id)) return false;
4171
+ seen.add(id);
4172
+ return true;
4173
+ });
4174
+ return additions.length ? [...previousRows, ...additions] : previousRows;
4175
+ };
4176
+ var crmSortsFromState = (sort, propertyMap) => {
4177
+ if (!sort || !sort.field || !sort.direction) return void 0;
4178
+ const propertyName = propertyMap && propertyMap[sort.field] || sort.field;
4179
+ return [{ propertyName, direction: sort.direction === "descending" ? "DESCENDING" : "ASCENDING" }];
4180
+ };
4181
+ var CrmDataTable = ({
4182
+ objectType,
4183
+ properties = EMPTY_ARRAY,
4184
+ columns,
4185
+ title,
4186
+ pageLength = 100,
4187
+ // CRM batch fetched per request (CRM search max)
4188
+ pageSize = 10,
4189
+ // client-side page size
4190
+ // Hybrid model: fetch ONE batch and do everything client-side while the whole
4191
+ // result set fits in the batch (no refetch). Once a fetch comes back capped
4192
+ // (more matches than the batch), search / filter / sort start refetching a
4193
+ // fresh batch server-side so they reach the whole dataset. Pagination stays
4194
+ // client-side over the fetched batch; `serverSide` forces server-side querying
4195
+ // from the first render.
4196
+ serverSide = false,
4197
+ filters,
4198
+ autoFilters = false,
4199
+ autoFilterMaxOptions = 25,
4200
+ filterMap,
4201
+ propertyMap,
4202
+ sortMap,
4203
+ searchFields,
4204
+ searchPlaceholder,
4205
+ format = DEFAULT_CRM_FORMAT,
4206
+ mapRecord,
4207
+ rowIdField = "objectId",
4208
+ dataTableProps = EMPTY_OBJECT,
4209
+ ...props
4210
+ }) => {
4211
+ var _a, _b;
4212
+ const [params, setParams] = useState4({ search: "", filters: {}, sort: null });
4213
+ const resolvedProperties = useMemo3(() => properties, [properties]);
4214
+ const resolvedColumns = useMemo3(
4215
+ () => columns || inferCrmColumns(resolvedProperties),
4216
+ [columns, resolvedProperties]
4217
+ );
4218
+ const resolvedSearchFields = searchFields || resolvedProperties;
4219
+ const autoFilterFields = useMemo3(
4220
+ () => normalizeAutoFilterFields(autoFilters, resolvedProperties),
4221
+ [autoFilters, resolvedProperties]
4222
+ );
4223
+ const autoFilterLabelsRef = useRef4({});
4224
+ const defaultPropertyMap = useMemo3(
4225
+ () => Object.fromEntries(resolvedProperties.map((property) => [property, property])),
4226
+ [resolvedProperties]
4227
+ );
4228
+ const effectivePropertyMap = propertyMap || defaultPropertyMap;
4229
+ const resolvedSortMap = useMemo3(
4230
+ () => sortMap || ((sort) => crmSortsFromState(sort, effectivePropertyMap)),
4231
+ [sortMap, effectivePropertyMap]
4232
+ );
4233
+ const resolvedMapRecord = mapRecord || defaultCrmMapRecord;
4234
+ const dataSourceOptions = useMemo3(
4235
+ () => ({
4236
+ objectType: resolveCrmObjectType(objectType),
4237
+ properties: resolvedProperties,
4238
+ pageLength,
4239
+ format,
4240
+ filterMap,
4241
+ propertyMap: effectivePropertyMap,
4242
+ sortMap: resolvedSortMap,
4243
+ rowIdField,
4244
+ row: { idField: rowIdField, mapRecord: resolvedMapRecord }
4245
+ }),
4246
+ [objectType, resolvedProperties, pageLength, format, filterMap, effectivePropertyMap, resolvedSortMap, rowIdField, resolvedMapRecord]
4247
+ );
4248
+ const [serverQuerying, setServerQuerying] = useState4(!!serverSide);
4249
+ const effectiveParams = serverQuerying ? params : EMPTY_CRM_PARAMS;
4250
+ const dataSource = useCrmSearchDataSource(effectiveParams, dataSourceOptions);
4251
+ const queryKey = useMemo3(
4252
+ () => stableStringify({ effectiveParams, objectType, properties: resolvedProperties, pageLength }),
4253
+ [effectiveParams, objectType, resolvedProperties, pageLength]
4254
+ );
4255
+ const [accumulatedRows, setAccumulatedRows] = useState4(EMPTY_ARRAY);
4256
+ const [requestedPage, setRequestedPage] = useState4(1);
4257
+ const lastQueryKeyRef = useRef4(queryKey);
4258
+ const loadedRows = accumulatedRows.length ? accumulatedRows : dataSource.data;
4259
+ useEffect4(() => {
4260
+ var _a2;
4261
+ if (lastQueryKeyRef.current !== queryKey) {
4262
+ lastQueryKeyRef.current = queryKey;
4263
+ setAccumulatedRows(dataSource.data);
4264
+ return;
4265
+ }
4266
+ const currentPage = ((_a2 = dataSource.pagination) == null ? void 0 : _a2.currentPage) || 1;
4267
+ setAccumulatedRows((prev) => currentPage <= 1 ? dataSource.data : appendUniqueRows(prev, dataSource.data, rowIdField));
4268
+ }, [queryKey, dataSource.data, (_a = dataSource.pagination) == null ? void 0 : _a.currentPage, rowIdField]);
4269
+ useEffect4(() => {
4270
+ if (!serverQuerying && typeof dataSource.totalCount === "number" && dataSource.totalCount > loadedRows.length) {
4271
+ setServerQuerying(true);
4272
+ }
4273
+ }, [serverQuerying, dataSource.totalCount, loadedRows.length]);
4274
+ const ensurePageLoaded = useCallback4((page) => {
4275
+ var _a2, _b2, _c;
4276
+ const pageNumber = Number(page) || 1;
4277
+ const requiredRows = pageNumber * pageSize;
4278
+ if (requiredRows <= loadedRows.length) return;
4279
+ if (!dataSource.hasMore || dataSource.loading || ((_a2 = dataSource.response) == null ? void 0 : _a2.isRefetching)) return;
4280
+ (_c = (_b2 = dataSource.pagination) == null ? void 0 : _b2.nextPage) == null ? void 0 : _c.call(_b2);
4281
+ }, [pageSize, loadedRows.length, dataSource.hasMore, dataSource.loading, dataSource.response, dataSource.pagination]);
4282
+ useEffect4(() => {
4283
+ ensurePageLoaded(requestedPage);
4284
+ }, [requestedPage, ensurePageLoaded]);
4285
+ const generatedFilters = useMemo3(
4286
+ () => buildAutoFiltersFromRows({
4287
+ rows: loadedRows,
4288
+ fields: autoFilterFields,
4289
+ labelsRef: autoFilterLabelsRef,
4290
+ maxOptions: autoFilterMaxOptions
4291
+ }),
4292
+ [loadedRows, autoFilterFields, autoFilterMaxOptions]
4293
+ );
4294
+ const resolvedFilters = filters || generatedFilters;
4295
+ const table = React13.createElement(DataTable, {
4296
+ title: title || `${prettifyPropertyName(objectType)} records`,
4297
+ data: loadedRows,
4298
+ loading: dataSource.loading || ((_b = dataSource.response) == null ? void 0 : _b.isRefetching),
4299
+ error: dataSource.error,
4300
+ columns: resolvedColumns,
4301
+ rowIdField,
4302
+ pageSize,
4303
+ clientTotalCount: dataSource.totalCount,
4304
+ filters: resolvedFilters,
4305
+ searchFields: resolvedSearchFields,
4306
+ searchPlaceholder: searchPlaceholder || `Search ${prettifyPropertyName(objectType).toLowerCase()}...`,
4307
+ searchDebounce: 300,
4308
+ onParamsChange: (next) => {
4309
+ setParams((prev) => ({ ...prev, search: next.search, filters: next.filters, sort: next.sort }));
4310
+ setRequestedPage(next.page || 1);
4311
+ ensurePageLoaded(next.page);
4312
+ },
4313
+ ...dataTableProps,
4314
+ ...props
4315
+ });
4316
+ const total = dataSource.totalCount;
4317
+ const capped = typeof total === "number" && total > loadedRows.length;
4318
+ if (!capped) return table;
4319
+ return React13.createElement(
4320
+ Flex6,
4321
+ { direction: "column", gap: "xs" },
4322
+ React13.createElement(
4323
+ Text4,
4324
+ { variant: "microcopy" },
4325
+ dataSource.hasMore ? `Loaded ${loadedRows.length} of ${total} matching. Use the table pagination to load more CRM results.` : `Showing ${loadedRows.length} of ${total} matching. Refine your search or filters to narrow the results.`
4326
+ ),
4327
+ table
4328
+ );
4329
+ };
4330
+ var CrmKanban = ({
4331
+ objectType,
4332
+ properties = EMPTY_ARRAY,
4333
+ groupBy,
4334
+ stages,
4335
+ stageLabels,
4336
+ // object { value: label } or (value) => label
4337
+ title,
4338
+ pageLength = 100,
4339
+ serverSide = false,
4340
+ filters,
4341
+ autoFilters = false,
4342
+ autoFilterMaxOptions = 25,
4343
+ filterMap,
4344
+ propertyMap,
4345
+ sortMap,
4346
+ searchFields,
4347
+ searchPlaceholder,
4348
+ format = DEFAULT_CRM_FORMAT,
4349
+ mapRecord,
4350
+ rowIdField = "objectId",
4351
+ stageMeta,
4352
+ onLoadMore,
4353
+ kanbanProps = EMPTY_OBJECT,
4354
+ ...props
4355
+ }) => {
4356
+ var _a, _b;
4357
+ const [params, setParams] = useState4(EMPTY_CRM_PARAMS);
4358
+ const resolvedProperties = useMemo3(() => properties, [properties]);
4359
+ const resolvedSearchFields = searchFields || resolvedProperties;
4360
+ const autoFilterFields = useMemo3(
4361
+ () => normalizeAutoFilterFields(autoFilters, resolvedProperties),
4362
+ [autoFilters, resolvedProperties]
4363
+ );
4364
+ const autoFilterLabelsRef = useRef4({});
4365
+ const defaultPropertyMap = useMemo3(
4366
+ () => Object.fromEntries(resolvedProperties.map((property) => [property, property])),
4367
+ [resolvedProperties]
4368
+ );
4369
+ const effectivePropertyMap = propertyMap || defaultPropertyMap;
4370
+ const resolvedSortMap = useMemo3(
4371
+ () => sortMap || ((sort) => crmSortsFromState(sort, effectivePropertyMap)),
4372
+ [sortMap, effectivePropertyMap]
4373
+ );
4374
+ const resolvedMapRecord = mapRecord || defaultCrmMapRecord;
4375
+ const dataSourceOptions = useMemo3(
4376
+ () => ({
4377
+ objectType: resolveCrmObjectType(objectType),
4378
+ properties: resolvedProperties,
4379
+ pageLength,
4380
+ format,
4381
+ filterMap,
4382
+ propertyMap: effectivePropertyMap,
4383
+ sortMap: resolvedSortMap,
4384
+ rowIdField,
4385
+ row: { idField: rowIdField, mapRecord: resolvedMapRecord }
4386
+ }),
4387
+ [objectType, resolvedProperties, pageLength, format, filterMap, effectivePropertyMap, resolvedSortMap, rowIdField, resolvedMapRecord]
4388
+ );
4389
+ const [serverQuerying, setServerQuerying] = useState4(!!serverSide);
4390
+ const effectiveParams = serverQuerying ? params : EMPTY_CRM_PARAMS;
4391
+ const dataSource = useCrmSearchDataSource(effectiveParams, dataSourceOptions);
4392
+ const queryKey = useMemo3(
4393
+ () => stableStringify({ effectiveParams, objectType, properties: resolvedProperties, pageLength }),
4394
+ [effectiveParams, objectType, resolvedProperties, pageLength]
4395
+ );
4396
+ const [accumulatedRows, setAccumulatedRows] = useState4(EMPTY_ARRAY);
4397
+ const lastQueryKeyRef = useRef4(queryKey);
4398
+ const loadedRows = accumulatedRows.length ? accumulatedRows : dataSource.data;
4399
+ useEffect4(() => {
4400
+ var _a2;
4401
+ if (lastQueryKeyRef.current !== queryKey) {
4402
+ lastQueryKeyRef.current = queryKey;
4403
+ setAccumulatedRows(dataSource.data);
4404
+ return;
4405
+ }
4406
+ const currentPage = ((_a2 = dataSource.pagination) == null ? void 0 : _a2.currentPage) || 1;
4407
+ setAccumulatedRows((prev) => currentPage <= 1 ? dataSource.data : appendUniqueRows(prev, dataSource.data, rowIdField));
4408
+ }, [queryKey, dataSource.data, (_a = dataSource.pagination) == null ? void 0 : _a.currentPage, rowIdField]);
4409
+ useEffect4(() => {
4410
+ if (!serverQuerying && typeof dataSource.totalCount === "number" && dataSource.totalCount > loadedRows.length) {
4411
+ setServerQuerying(true);
4412
+ }
4413
+ }, [serverQuerying, dataSource.totalCount, loadedRows.length]);
4414
+ const handleLoadMore = useCallback4((stage) => {
4415
+ var _a2, _b2, _c;
4416
+ if (onLoadMore) {
4417
+ onLoadMore(stage);
4418
+ return;
4419
+ }
4420
+ if (!dataSource.hasMore || dataSource.loading || ((_a2 = dataSource.response) == null ? void 0 : _a2.isRefetching)) return;
4421
+ (_c = (_b2 = dataSource.pagination) == null ? void 0 : _b2.nextPage) == null ? void 0 : _c.call(_b2);
4422
+ }, [onLoadMore, dataSource.hasMore, dataSource.loading, dataSource.response, dataSource.pagination]);
4423
+ const generatedFilters = useMemo3(
4424
+ () => buildAutoFiltersFromRows({
4425
+ rows: loadedRows,
4426
+ fields: autoFilterFields,
4427
+ labelsRef: autoFilterLabelsRef,
4428
+ maxOptions: autoFilterMaxOptions
4429
+ }),
4430
+ [loadedRows, autoFilterFields, autoFilterMaxOptions]
4431
+ );
4432
+ const resolvedFilters = filters || generatedFilters;
4433
+ const resolvedStages = useMemo3(() => {
4434
+ if (stages) return stages;
4435
+ const seen = [];
4436
+ for (const row of loadedRows) {
4437
+ const value = typeof groupBy === "function" ? groupBy(row) : row[groupBy];
4438
+ if (value != null && value !== "" && !seen.includes(value)) seen.push(value);
4439
+ }
4440
+ return seen.map((value) => ({
4441
+ value,
4442
+ label: typeof stageLabels === "function" ? stageLabels(value) : stageLabels && stageLabels[value] || prettifyPropertyName(String(value))
4443
+ }));
4444
+ }, [stages, stageLabels, loadedRows, groupBy]);
4445
+ const resolvedStageMeta = useMemo3(() => {
4446
+ if (stageMeta || !dataSource.hasMore) return stageMeta;
4447
+ return Object.fromEntries(resolvedStages.map((stage) => {
4448
+ var _a2;
4449
+ return [
4450
+ stage.value,
4451
+ {
4452
+ hasMore: true,
4453
+ loading: dataSource.loading || ((_a2 = dataSource.response) == null ? void 0 : _a2.isRefetching),
4454
+ totalCount: dataSource.totalCount
4455
+ }
4456
+ ];
4457
+ }));
4458
+ }, [stageMeta, dataSource.hasMore, dataSource.loading, dataSource.response, dataSource.totalCount, resolvedStages]);
4459
+ const board = React13.createElement(Kanban, {
4460
+ title: title || `${prettifyPropertyName(objectType)} board`,
4461
+ data: loadedRows,
4462
+ loading: dataSource.loading || ((_b = dataSource.response) == null ? void 0 : _b.isRefetching),
4463
+ error: dataSource.error,
4464
+ rowIdField,
4465
+ groupBy,
4466
+ stages: resolvedStages,
4467
+ stageMeta: resolvedStageMeta,
4468
+ onLoadMore: dataSource.hasMore || onLoadMore ? handleLoadMore : void 0,
4469
+ filters: resolvedFilters,
4470
+ searchFields: resolvedSearchFields,
4471
+ searchPlaceholder: searchPlaceholder || `Search ${prettifyPropertyName(objectType).toLowerCase()}...`,
4472
+ searchDebounce: 300,
4473
+ onParamsChange: (next) => {
4474
+ setParams((prev) => ({ ...prev, search: next.search, filters: next.filters }));
4475
+ },
4476
+ ...kanbanProps,
4477
+ ...props
4478
+ });
4479
+ const total = dataSource.totalCount;
4480
+ const capped = typeof total === "number" && total > loadedRows.length;
4481
+ if (!capped) return board;
4482
+ return React13.createElement(
4483
+ Flex6,
4484
+ { direction: "column", gap: "xs" },
4485
+ React13.createElement(
4486
+ Text4,
4487
+ { variant: "microcopy" },
4488
+ dataSource.hasMore ? `Loaded ${loadedRows.length} of ${total} matching. Use Load more to fetch more CRM results.` : `Showing ${loadedRows.length} of ${total} matching. Refine your search or filters to narrow the results.`
4489
+ ),
4490
+ board
4491
+ );
4492
+ };
4493
+ CrmDataTable.displayName = "CrmDataTable";
4494
+ CrmKanban.displayName = "CrmKanban";
1814
4495
 
1815
4496
  // src/common-components/CrmLookupSelect.js
1816
4497
  var EMPTY_ARRAY2 = [];
@@ -1928,6 +4609,280 @@ var CrmLookupSelect = ({
1928
4609
  return React14.createElement(multiple ? MultiSelect3 : Select5, commonProps);
1929
4610
  };
1930
4611
 
4612
+ // src/common-components/CrmRecordPicker.js
4613
+ import React15, { useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
4614
+ import { Flex as Flex7, MultiSelect as MultiSelect4, SearchInput as SearchInput2, Select as Select6, useDebounce as useDebounce3 } from "@hubspot/ui-extensions";
4615
+
4616
+ // src/common-components/recordPickerCore.js
4617
+ var EMPTY_ARRAY3 = [];
4618
+ var CREATE_OPTION_VALUE = "__create__";
4619
+ var isRecordLike = (value) => value != null && typeof value === "object" && !Array.isArray(value);
4620
+ var getRecordId = (record) => {
4621
+ if (!isRecordLike(record)) return void 0;
4622
+ const id = record.objectId ?? record.id ?? record.hs_object_id ?? getByPath(record, "properties.hs_object_id");
4623
+ return id == null ? void 0 : String(id);
4624
+ };
4625
+ var toList = (value) => Array.isArray(value) ? value : value == null || value === "" ? EMPTY_ARRAY3 : [value];
4626
+ var normalizeRecordSelection = (value) => {
4627
+ const ids = [];
4628
+ const records = [];
4629
+ const seen = /* @__PURE__ */ new Set();
4630
+ for (const entry of toList(value)) {
4631
+ const rawId = isRecordLike(entry) ? getRecordId(entry) : entry;
4632
+ const id = rawId == null || rawId === "" ? rawId : String(rawId);
4633
+ if (id == null || id === "" || seen.has(id)) continue;
4634
+ seen.add(id);
4635
+ ids.push(id);
4636
+ if (isRecordLike(entry)) records.push(entry);
4637
+ }
4638
+ return { ids, records };
4639
+ };
4640
+ var recordToPickerOption = (record, config = {}) => {
4641
+ const { labelField, descriptionField, fallbackLabel = "Untitled record" } = config;
4642
+ const label = (labelField ? getByPath(record, labelField) : void 0) ?? (record == null ? void 0 : record.name) ?? getByPath(record, "properties.name") ?? fallbackLabel;
4643
+ const option = { label, value: getRecordId(record) };
4644
+ const description = descriptionField ? getByPath(record, descriptionField) : void 0;
4645
+ if (description != null && description !== "") option.description = description;
4646
+ return option;
4647
+ };
4648
+ var mergePickerOptions = (options, selectedOptions) => {
4649
+ const base = Array.isArray(options) ? options : EMPTY_ARRAY3;
4650
+ const selected = toList(selectedOptions);
4651
+ if (!selected.length) return base;
4652
+ const existing = new Set(base.map((option) => option == null ? void 0 : option.value));
4653
+ const missing = selected.filter((option) => option && !existing.has(option.value));
4654
+ return missing.length ? [...missing, ...base] : base;
4655
+ };
4656
+ var enforceSelectionMax = (ids, max) => {
4657
+ const list = toList(ids);
4658
+ if (!Number.isFinite(max) || max <= 0 || list.length <= max) return list;
4659
+ return list.slice(0, max);
4660
+ };
4661
+ var shouldShowCreateOption = ({
4662
+ allowCreate,
4663
+ searchTerm,
4664
+ options,
4665
+ searching = false,
4666
+ createPending = false,
4667
+ atMax = false
4668
+ } = {}) => {
4669
+ if (!allowCreate || typeof allowCreate.onCreate !== "function") return false;
4670
+ if (createPending || searching || atMax) return false;
4671
+ const term = String(searchTerm ?? "").trim();
4672
+ if (!term) return false;
4673
+ const lower = term.toLowerCase();
4674
+ return !(options || EMPTY_ARRAY3).some(
4675
+ (option) => String((option == null ? void 0 : option.label) ?? "").trim().toLowerCase() === lower
4676
+ );
4677
+ };
4678
+ var makeCreateOption = (term, label) => ({
4679
+ label: typeof label === "function" ? label(term) : label || `Create "${term}"`,
4680
+ value: CREATE_OPTION_VALUE
4681
+ });
4682
+ var splitCreateSelection = (next) => {
4683
+ const list = toList(next);
4684
+ const ids = list.filter((value) => value !== CREATE_OPTION_VALUE);
4685
+ return { ids, create: ids.length !== list.length };
4686
+ };
4687
+ var mapIdsToRecords = (ids, recordsById) => {
4688
+ const lookup = recordsById instanceof Map ? (id) => recordsById.get(id) : (id) => recordsById ? recordsById[id] : void 0;
4689
+ return toList(ids).map((id) => lookup(id) ?? { objectId: id });
4690
+ };
4691
+ var upsertRecords = (records, additions) => {
4692
+ const incoming = toList(additions).filter((record) => getRecordId(record) != null);
4693
+ if (!incoming.length) return Array.isArray(records) ? records : EMPTY_ARRAY3;
4694
+ const byId = new Map(toList(records).map((record) => [getRecordId(record), record]));
4695
+ for (const record of incoming) byId.set(getRecordId(record), record);
4696
+ return [...byId.values()];
4697
+ };
4698
+
4699
+ // src/common-components/CrmRecordPicker.js
4700
+ var EMPTY_ARRAY4 = [];
4701
+ var defaultMapRecord = (record) => ({ objectId: record.objectId, ...record.properties });
4702
+ var CrmRecordPicker = ({
4703
+ objectType,
4704
+ properties = EMPTY_ARRAY4,
4705
+ labelField,
4706
+ descriptionField,
4707
+ value,
4708
+ defaultValue,
4709
+ onChange,
4710
+ multi = true,
4711
+ max,
4712
+ placeholder,
4713
+ label,
4714
+ name,
4715
+ required,
4716
+ readOnly,
4717
+ error,
4718
+ validationMessage,
4719
+ description,
4720
+ tooltip,
4721
+ variant,
4722
+ pageLength = 20,
4723
+ debounce = 300,
4724
+ minSearchLength = 0,
4725
+ filterMap,
4726
+ allowCreate = false,
4727
+ fallbackLabel = "Untitled record",
4728
+ format,
4729
+ baseConfig,
4730
+ onSearchChange,
4731
+ ...rest
4732
+ }) => {
4733
+ const isControlled = value !== void 0;
4734
+ const [internalValue, setInternalValue] = useState6(defaultValue);
4735
+ const effectiveValue = isControlled ? value : internalValue;
4736
+ const selection = useMemo5(() => normalizeRecordSelection(effectiveValue), [effectiveValue]);
4737
+ const [inputValue, setInputValue] = useState6("");
4738
+ const [seenRecords, setSeenRecords] = useState6(EMPTY_ARRAY4);
4739
+ const [createPending, setCreatePending] = useState6(false);
4740
+ const [createError, setCreateError] = useState6(null);
4741
+ const createPendingRef = useRef5(false);
4742
+ const debouncedInput = useDebounce3(inputValue, debounce > 0 ? debounce : 1);
4743
+ const search = debounce > 0 ? debouncedInput : inputValue;
4744
+ const effectiveSearch = search && search.length >= minSearchLength ? search : "";
4745
+ const optionConfig = useMemo5(
4746
+ () => ({ labelField, descriptionField, fallbackLabel }),
4747
+ [labelField, descriptionField, fallbackLabel]
4748
+ );
4749
+ const searchParams = useMemo5(() => ({ search: effectiveSearch }), [effectiveSearch]);
4750
+ const dataSourceOptions = useMemo5(
4751
+ () => ({
4752
+ objectType: resolveCrmObjectType(objectType),
4753
+ properties,
4754
+ pageLength,
4755
+ format,
4756
+ filterMap,
4757
+ baseConfig,
4758
+ row: { mapRecord: defaultMapRecord },
4759
+ option: { mapOption: (row) => recordToPickerOption(row, optionConfig) }
4760
+ }),
4761
+ [objectType, properties, pageLength, format, filterMap, baseConfig, optionConfig]
4762
+ );
4763
+ const dataSource = useCrmSearchOptions(searchParams, dataSourceOptions);
4764
+ const recordsById = useMemo5(() => {
4765
+ const map = /* @__PURE__ */ new Map();
4766
+ for (const record of selection.records) map.set(getRecordId(record), record);
4767
+ for (const record of seenRecords) map.set(getRecordId(record), record);
4768
+ for (const row of dataSource.rows || EMPTY_ARRAY4) {
4769
+ const id = getRecordId(row);
4770
+ if (id != null) map.set(id, row);
4771
+ }
4772
+ return map;
4773
+ }, [selection.records, seenRecords, dataSource.rows]);
4774
+ const selectedOptions = useMemo5(
4775
+ () => selection.ids.map((id) => {
4776
+ const record = recordsById.get(id);
4777
+ return record ? recordToPickerOption(record, optionConfig) : { label: String(id), value: id };
4778
+ }),
4779
+ [selection.ids, recordsById, optionConfig]
4780
+ );
4781
+ const isSearching = dataSource.loading || inputValue.trim() !== (search || "").trim();
4782
+ const atMax = multi && Number.isFinite(max) && max > 0 && selection.ids.length >= max;
4783
+ const options = useMemo5(() => {
4784
+ const merged = mergePickerOptions(dataSource.options || EMPTY_ARRAY4, selectedOptions);
4785
+ const showCreate = shouldShowCreateOption({
4786
+ allowCreate,
4787
+ searchTerm: effectiveSearch,
4788
+ options: merged,
4789
+ searching: isSearching,
4790
+ createPending,
4791
+ atMax
4792
+ });
4793
+ if (!showCreate) return merged;
4794
+ return [...merged, makeCreateOption(effectiveSearch.trim(), allowCreate == null ? void 0 : allowCreate.label)];
4795
+ }, [dataSource.options, selectedOptions, allowCreate, effectiveSearch, isSearching, createPending, atMax]);
4796
+ const commitChange = (ids, extraRecords) => {
4797
+ let map = recordsById;
4798
+ if (extraRecords && extraRecords.length) {
4799
+ map = new Map(recordsById);
4800
+ for (const record of extraRecords) {
4801
+ const id = getRecordId(record);
4802
+ if (id != null) map.set(id, record);
4803
+ }
4804
+ }
4805
+ const trimmed = multi ? enforceSelectionMax(ids, max) : ids.slice(0, 1);
4806
+ const records = mapIdsToRecords(trimmed, map);
4807
+ if (!isControlled) setInternalValue(multi ? trimmed : trimmed[0] ?? null);
4808
+ if (onChange) {
4809
+ if (multi) onChange(trimmed, records);
4810
+ else onChange(trimmed[0] ?? null, records[0] ?? null);
4811
+ }
4812
+ };
4813
+ const startCreate = (term, baseIds) => {
4814
+ const onCreate = allowCreate && typeof allowCreate.onCreate === "function" ? allowCreate.onCreate : null;
4815
+ if (!onCreate || createPendingRef.current) return;
4816
+ createPendingRef.current = true;
4817
+ setCreatePending(true);
4818
+ setCreateError(null);
4819
+ Promise.resolve(onCreate(term)).then((created) => {
4820
+ const record = isRecordLike(created) ? created : created != null && created !== "" ? { objectId: created, name: term } : null;
4821
+ const id = getRecordId(record);
4822
+ if (id == null) return;
4823
+ setSeenRecords((prev) => upsertRecords(prev, [record]));
4824
+ const nextIds = multi ? [...baseIds.filter((v) => v !== id), id] : [id];
4825
+ commitChange(nextIds, [record]);
4826
+ }).catch((err) => {
4827
+ setCreateError((err == null ? void 0 : err.message) || "Could not create the record.");
4828
+ }).finally(() => {
4829
+ createPendingRef.current = false;
4830
+ setCreatePending(false);
4831
+ });
4832
+ };
4833
+ const handleChange = (next) => {
4834
+ if (createError) setCreateError(null);
4835
+ const { ids, create } = splitCreateSelection(next);
4836
+ const picked = ids.map((id) => recordsById.get(id)).filter(Boolean);
4837
+ if (picked.length) setSeenRecords((prev) => upsertRecords(prev, picked));
4838
+ if (create) {
4839
+ startCreate(effectiveSearch.trim(), ids);
4840
+ if (multi) commitChange(ids);
4841
+ return;
4842
+ }
4843
+ commitChange(ids);
4844
+ };
4845
+ const handleSearchInput = (next) => {
4846
+ setInputValue(next || "");
4847
+ if (onSearchChange) onSearchChange(next || "");
4848
+ };
4849
+ const commonProps = {
4850
+ name,
4851
+ label,
4852
+ value: multi ? selection.ids : selection.ids[0],
4853
+ options,
4854
+ placeholder: placeholder || (createPending ? "Creating record..." : dataSource.loading ? "Searching CRM..." : "Search CRM records..."),
4855
+ description,
4856
+ tooltip,
4857
+ required,
4858
+ readOnly: readOnly || createPending,
4859
+ error: error || !!createError || !!dataSource.error,
4860
+ validationMessage: validationMessage || createError || (typeof dataSource.error === "string" ? dataSource.error : void 0),
4861
+ variant,
4862
+ onChange: handleChange,
4863
+ ...rest
4864
+ };
4865
+ if (!multi) {
4866
+ return React15.createElement(Select6, { ...commonProps, onInput: handleSearchInput });
4867
+ }
4868
+ return React15.createElement(
4869
+ Flex7,
4870
+ { direction: "column", gap: "xs" },
4871
+ React15.createElement(SearchInput2, {
4872
+ name: name ? `${name}-search` : void 0,
4873
+ label: "",
4874
+ placeholder: "Search CRM records...",
4875
+ value: inputValue,
4876
+ readOnly: readOnly || createPending,
4877
+ onInput: handleSearchInput,
4878
+ // The clear "x" emits onChange (not onInput) — wire both so clearing
4879
+ // resets the term (same pattern as CollectionToolbar).
4880
+ onChange: handleSearchInput
4881
+ }),
4882
+ React15.createElement(MultiSelect4, commonProps)
4883
+ );
4884
+ };
4885
+
1931
4886
  // src/common-components/datePresets.js
1932
4887
  var HS_DATE_PRESETS = [
1933
4888
  { label: "Today", value: "today" },
@@ -1950,12 +4905,480 @@ var HS_DATE_DIRECTION_LABELS = {
1950
4905
  desc: "Descending"
1951
4906
  };
1952
4907
 
4908
+ // src/common-components/dateRangePresets.js
4909
+ var DATE_RANGE_CUSTOM_VALUE = "custom";
4910
+ var DATE_FILTER_OPERATORS = [
4911
+ { label: "is", value: "InRollingDateRange" },
4912
+ { label: "is equal to", value: "Equal" },
4913
+ { label: "is before", value: "BeforeDateStaticOrDynamic" },
4914
+ { label: "is after", value: "AfterDateStaticOrDynamic" },
4915
+ { label: "is between", value: "InRange" },
4916
+ { label: "is more than", value: "GreaterRolling" },
4917
+ { label: "is less than", value: "LessRolling" },
4918
+ { label: "is known", value: "Known" },
4919
+ { label: "is unknown", value: "NotKnown" }
4920
+ ];
4921
+ var DATE_ROLLING_UNIT_OPTIONS = [
4922
+ { label: "day ago", value: "day:backward" },
4923
+ { label: "days from now", value: "day:forward" },
4924
+ { label: "week ago", value: "week:backward" },
4925
+ { label: "weeks from now", value: "week:forward" },
4926
+ { label: "month ago", value: "month:backward" },
4927
+ { label: "months from now", value: "month:forward" },
4928
+ { label: "year ago", value: "year:backward" },
4929
+ { label: "years from now", value: "year:forward" }
4930
+ ];
4931
+ var toHsDateValue = (date) => {
4932
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
4933
+ return { year: date.getFullYear(), month: date.getMonth(), date: date.getDate() };
4934
+ };
4935
+ var compareHsDateValues = (a, b) => {
4936
+ if (!a || !b) return 0;
4937
+ return a.year - b.year || a.month - b.month || a.date - b.date;
4938
+ };
4939
+ var isValidDateRange = (range) => {
4940
+ if (!range) return true;
4941
+ return compareHsDateValues(range.from, range.to) <= 0;
4942
+ };
4943
+ var dayAt = (now, offset = 0) => toHsDateValue(new Date(now.getFullYear(), now.getMonth(), now.getDate() + offset));
4944
+ var monthStart = (now, monthOffset = 0) => toHsDateValue(new Date(now.getFullYear(), now.getMonth() + monthOffset, 1));
4945
+ var monthEnd = (now, monthOffset = 0) => toHsDateValue(new Date(now.getFullYear(), now.getMonth() + monthOffset + 1, 0));
4946
+ var quarterStartMonth = (now, quarterOffset = 0) => Math.floor(now.getMonth() / 3) * 3 + quarterOffset * 3;
4947
+ var presetToRange = (presetKey, now = /* @__PURE__ */ new Date()) => {
4948
+ if (!presetKey || presetKey === DATE_RANGE_CUSTOM_VALUE) return null;
4949
+ if (!(now instanceof Date) || Number.isNaN(now.getTime())) return null;
4950
+ const dow = now.getDay();
4951
+ const year = now.getFullYear();
4952
+ const qm = quarterStartMonth(now);
4953
+ switch (presetKey) {
4954
+ case "today":
4955
+ return { from: dayAt(now, 0), to: dayAt(now, 0) };
4956
+ case "yesterday":
4957
+ return { from: dayAt(now, -1), to: dayAt(now, -1) };
4958
+ case "tomorrow":
4959
+ return { from: dayAt(now, 1), to: dayAt(now, 1) };
4960
+ case "this_week":
4961
+ return { from: dayAt(now, -dow), to: dayAt(now, 6 - dow) };
4962
+ case "last_week":
4963
+ return { from: dayAt(now, -dow - 7), to: dayAt(now, -dow - 1) };
4964
+ case "7d":
4965
+ return { from: dayAt(now, -6), to: dayAt(now, 0) };
4966
+ case "30d":
4967
+ return { from: dayAt(now, -29), to: dayAt(now, 0) };
4968
+ case "90d":
4969
+ return { from: dayAt(now, -89), to: dayAt(now, 0) };
4970
+ case "this_month":
4971
+ return { from: monthStart(now, 0), to: monthEnd(now, 0) };
4972
+ case "last_month":
4973
+ return { from: monthStart(now, -1), to: monthEnd(now, -1) };
4974
+ case "this_quarter":
4975
+ return {
4976
+ from: toHsDateValue(new Date(year, qm, 1)),
4977
+ to: toHsDateValue(new Date(year, qm + 3, 0))
4978
+ };
4979
+ case "last_quarter":
4980
+ return {
4981
+ from: toHsDateValue(new Date(year, qm - 3, 1)),
4982
+ to: toHsDateValue(new Date(year, qm, 0))
4983
+ };
4984
+ case "this_year":
4985
+ return { from: { year, month: 0, date: 1 }, to: { year, month: 11, date: 31 } };
4986
+ case "last_year":
4987
+ return {
4988
+ from: { year: year - 1, month: 0, date: 1 },
4989
+ to: { year: year - 1, month: 11, date: 31 }
4990
+ };
4991
+ default:
4992
+ return null;
4993
+ }
4994
+ };
4995
+
4996
+ // src/common-components/DateRangePicker.js
4997
+ import React16, { useState as useState7 } from "react";
4998
+ import {
4999
+ AutoGrid as AutoGrid2,
5000
+ Box as Box4,
5001
+ DateInput as DateInput3,
5002
+ Flex as Flex8,
5003
+ Link as Link4,
5004
+ NumberInput as NumberInput2,
5005
+ Select as Select7,
5006
+ Text as Text5
5007
+ } from "@hubspot/ui-extensions";
5008
+ var h6 = React16.createElement;
5009
+ var IN_ROLLING = "InRollingDateRange";
5010
+ var EQUAL = "Equal";
5011
+ var BEFORE = "BeforeDateStaticOrDynamic";
5012
+ var AFTER = "AfterDateStaticOrDynamic";
5013
+ var GREATER_ROLLING = "GreaterRolling";
5014
+ var LESS_ROLLING = "LessRolling";
5015
+ var IN_RANGE = "InRange";
5016
+ var KNOWN = "Known";
5017
+ var NOT_KNOWN = "NotKnown";
5018
+ var EMPTY_RANGE = { from: null, to: null };
5019
+ var EMPTY_DATE = { date: null };
5020
+ var COMPACT_LABEL = "";
5021
+ var STATIC_DATE_OPERATORS = /* @__PURE__ */ new Set([EQUAL, BEFORE, AFTER]);
5022
+ var ROLLING_OPERATORS = /* @__PURE__ */ new Set([GREATER_ROLLING, LESS_ROLLING]);
5023
+ var PRESENCE_OPERATORS = /* @__PURE__ */ new Set([KNOWN, NOT_KNOWN]);
5024
+ var keyOfDate = (v) => v ? `${v.year}-${v.month}-${v.date}` : "";
5025
+ var keyOfRange = (r) => `${keyOfDate(r == null ? void 0 : r.from)}|${keyOfDate(r == null ? void 0 : r.to)}`;
5026
+ var isRangeLike = (value) => value && typeof value === "object" && ("from" in value || "to" in value) && !("operator" in value);
5027
+ var normalizeValue = (value) => {
5028
+ if (isRangeLike(value)) {
5029
+ return { operator: IN_RANGE, from: value.from ?? null, to: value.to ?? null };
5030
+ }
5031
+ if (!value || typeof value !== "object") {
5032
+ return { operator: IN_ROLLING, preset: "today" };
5033
+ }
5034
+ const operator = value.operator || IN_ROLLING;
5035
+ if (operator === IN_RANGE) {
5036
+ return { operator, from: value.from ?? null, to: value.to ?? null };
5037
+ }
5038
+ if (STATIC_DATE_OPERATORS.has(operator)) {
5039
+ return { operator, date: value.date ?? null };
5040
+ }
5041
+ if (ROLLING_OPERATORS.has(operator)) {
5042
+ return {
5043
+ operator,
5044
+ amount: Number.isFinite(Number(value.amount)) ? Number(value.amount) : 1,
5045
+ unit: value.unit || "day",
5046
+ direction: value.direction || "backward"
5047
+ };
5048
+ }
5049
+ if (PRESENCE_OPERATORS.has(operator)) {
5050
+ return { operator };
5051
+ }
5052
+ return { operator: IN_ROLLING, preset: value.preset || value.value || "today" };
5053
+ };
5054
+ var rangeFromValue = (value) => ({
5055
+ from: (value == null ? void 0 : value.from) ?? null,
5056
+ to: (value == null ? void 0 : value.to) ?? null
5057
+ });
5058
+ var getPresetOptions = (presets, customPresetLabel) => {
5059
+ const presetList = presets === true ? HS_DATE_PRESETS : Array.isArray(presets) ? presets : null;
5060
+ if (!presetList) return null;
5061
+ return [
5062
+ ...presetList.map((p) => ({ label: p.label, value: p.value })),
5063
+ ...presetList.some((p) => p.value === DATE_RANGE_CUSTOM_VALUE) ? [] : [{ label: customPresetLabel, value: DATE_RANGE_CUSTOM_VALUE }]
5064
+ ];
5065
+ };
5066
+ var DateRangePicker = ({
5067
+ value,
5068
+ defaultValue,
5069
+ onChange,
5070
+ label,
5071
+ name = "date-range",
5072
+ field,
5073
+ defaultField,
5074
+ onFieldChange,
5075
+ showFieldSelect = false,
5076
+ fieldOptions = [],
5077
+ operator,
5078
+ defaultOperator = IN_ROLLING,
5079
+ onOperatorChange,
5080
+ showOperatorSelect = true,
5081
+ operatorOptions = DATE_FILTER_OPERATORS,
5082
+ presets = true,
5083
+ rollingUnitOptions = DATE_ROLLING_UNIT_OPTIONS,
5084
+ direction = "row",
5085
+ clearable = false,
5086
+ min,
5087
+ max,
5088
+ fromLabel = "Start date",
5089
+ toLabel = "End date",
5090
+ dateLabel = "Date",
5091
+ showDateLabels = false,
5092
+ format = "medium",
5093
+ presetPlaceholder = "Enter value",
5094
+ customPresetLabel = "Custom",
5095
+ clearLabel = "Clear",
5096
+ invalidRangeMessage = "Start date must be on or before end date",
5097
+ readOnly = false,
5098
+ gap,
5099
+ gridColumnWidth = 260
5100
+ }) => {
5101
+ const isControlled = value !== void 0;
5102
+ const controlledValue = normalizeValue(value);
5103
+ const [internalValue, setInternalValue] = useState7(
5104
+ () => normalizeValue(defaultValue ?? { operator: defaultOperator })
5105
+ );
5106
+ const [internalField, setInternalField] = useState7(
5107
+ () => {
5108
+ var _a;
5109
+ return defaultField ?? ((_a = fieldOptions == null ? void 0 : fieldOptions[0]) == null ? void 0 : _a.value) ?? "";
5110
+ }
5111
+ );
5112
+ const current = normalizeValue(isControlled ? controlledValue : internalValue);
5113
+ const currentOperator = operator || current.operator || defaultOperator;
5114
+ const currentField = field !== void 0 ? field : internalField;
5115
+ const resolvedCurrent = normalizeValue({ ...current, operator: currentOperator });
5116
+ const [pending, setPending] = useState7(null);
5117
+ const [lastPreset, setLastPreset] = useState7({
5118
+ key: resolvedCurrent.preset || "",
5119
+ rangeKey: null
5120
+ });
5121
+ const isColumn = direction === "column";
5122
+ const presetOptions = getPresetOptions(presets, customPresetLabel);
5123
+ const showClear = clearable && !readOnly && (resolvedCurrent.preset || resolvedCurrent.date || resolvedCurrent.amount || resolvedCurrent.from || resolvedCurrent.to || pending);
5124
+ const emit = (next, meta = {}) => {
5125
+ const normalized = normalizeValue(next);
5126
+ if (!isControlled) setInternalValue(normalized);
5127
+ if (normalized.operator !== currentOperator) {
5128
+ onOperatorChange == null ? void 0 : onOperatorChange(normalized.operator);
5129
+ }
5130
+ onChange == null ? void 0 : onChange(normalized, {
5131
+ operator: normalized.operator,
5132
+ field: currentField || null,
5133
+ preset: normalized.operator === IN_ROLLING ? normalized.preset ?? null : null,
5134
+ ...meta
5135
+ });
5136
+ };
5137
+ const handleFieldChange = (nextField) => {
5138
+ if (field === void 0) setInternalField(nextField);
5139
+ onFieldChange == null ? void 0 : onFieldChange(nextField);
5140
+ onChange == null ? void 0 : onChange(resolvedCurrent, {
5141
+ operator: resolvedCurrent.operator,
5142
+ field: nextField || null,
5143
+ preset: resolvedCurrent.operator === IN_ROLLING ? resolvedCurrent.preset ?? null : null
5144
+ });
5145
+ };
5146
+ const handleOperatorChange = (nextOperator) => {
5147
+ setPending(null);
5148
+ if (nextOperator === IN_RANGE) {
5149
+ emit({ operator: IN_RANGE, ...EMPTY_RANGE }, { previousOperator: currentOperator });
5150
+ } else if (STATIC_DATE_OPERATORS.has(nextOperator)) {
5151
+ emit({ operator: nextOperator, ...EMPTY_DATE }, { previousOperator: currentOperator });
5152
+ } else if (ROLLING_OPERATORS.has(nextOperator)) {
5153
+ emit(
5154
+ { operator: nextOperator, amount: 1, unit: "day", direction: "backward" },
5155
+ { previousOperator: currentOperator }
5156
+ );
5157
+ } else if (PRESENCE_OPERATORS.has(nextOperator)) {
5158
+ emit({ operator: nextOperator }, { previousOperator: currentOperator });
5159
+ } else {
5160
+ emit({ operator: IN_ROLLING, preset: "today" }, { previousOperator: currentOperator });
5161
+ }
5162
+ };
5163
+ const handlePresetChange = (preset) => {
5164
+ if (!preset || preset === DATE_RANGE_CUSTOM_VALUE) {
5165
+ emit({ operator: IN_ROLLING, preset: DATE_RANGE_CUSTOM_VALUE });
5166
+ return;
5167
+ }
5168
+ const option = presets === true ? HS_DATE_PRESETS.find((p) => p.value === preset) : Array.isArray(presets) ? presets.find((p) => p.value === preset) : null;
5169
+ const range = option && typeof option.getRange === "function" ? option.getRange(/* @__PURE__ */ new Date()) : presetToRange(preset);
5170
+ setLastPreset({ key: preset, rangeKey: range ? keyOfRange(range) : null });
5171
+ emit({ operator: IN_ROLLING, preset }, { range });
5172
+ };
5173
+ const handleRollingUnitChange = (compound) => {
5174
+ const [unit, unitDirection] = String(compound || "day:backward").split(":");
5175
+ emit({
5176
+ operator: currentOperator,
5177
+ amount: resolvedCurrent.amount || 1,
5178
+ unit,
5179
+ direction: unitDirection || "backward"
5180
+ });
5181
+ };
5182
+ const handleDateChange = (side, next) => {
5183
+ const displayRange = {
5184
+ ...rangeFromValue(resolvedCurrent),
5185
+ ...pending ? { [pending.side]: pending.value } : {}
5186
+ };
5187
+ const candidate = { ...displayRange, [side]: next ?? null };
5188
+ if (isValidDateRange(candidate)) {
5189
+ setPending(null);
5190
+ setLastPreset({ key: "", rangeKey: null });
5191
+ emit({ operator: IN_RANGE, ...candidate });
5192
+ } else {
5193
+ setPending({ side, value: next ?? null });
5194
+ }
5195
+ };
5196
+ const handleStaticDateChange = (next) => {
5197
+ emit({ operator: currentOperator, date: next ?? null });
5198
+ };
5199
+ const handleClear = () => {
5200
+ setPending(null);
5201
+ setLastPreset({ key: "", rangeKey: null });
5202
+ if (currentOperator === IN_RANGE) {
5203
+ emit({ operator: IN_RANGE, ...EMPTY_RANGE });
5204
+ } else if (STATIC_DATE_OPERATORS.has(currentOperator)) {
5205
+ emit({ operator: currentOperator, ...EMPTY_DATE });
5206
+ } else if (ROLLING_OPERATORS.has(currentOperator)) {
5207
+ emit({ operator: currentOperator, amount: 1, unit: "day", direction: "backward" });
5208
+ } else if (PRESENCE_OPERATORS.has(currentOperator)) {
5209
+ emit({ operator: currentOperator });
5210
+ } else {
5211
+ emit({ operator: IN_ROLLING, preset: "" });
5212
+ }
5213
+ };
5214
+ const operatorSelect = showOperatorSelect ? h6(Select7, {
5215
+ key: "operator",
5216
+ name: `${name}-operator`,
5217
+ label: COMPACT_LABEL,
5218
+ options: operatorOptions,
5219
+ value: currentOperator,
5220
+ onChange: handleOperatorChange,
5221
+ readOnly
5222
+ }) : null;
5223
+ const fieldSelect = showFieldSelect ? h6(Select7, {
5224
+ key: "field",
5225
+ name: `${name}-field`,
5226
+ label: "",
5227
+ options: fieldOptions,
5228
+ value: currentField,
5229
+ onChange: handleFieldChange,
5230
+ readOnly
5231
+ }) : null;
5232
+ let valueInput = null;
5233
+ const fromInputLabel = showDateLabels ? fromLabel : COMPACT_LABEL;
5234
+ const toInputLabel = showDateLabels ? toLabel : COMPACT_LABEL;
5235
+ const singleDateInputLabel = showDateLabels ? dateLabel : COMPACT_LABEL;
5236
+ if (currentOperator === IN_RANGE) {
5237
+ const committed = rangeFromValue(resolvedCurrent);
5238
+ const display = {
5239
+ ...committed,
5240
+ ...pending ? { [pending.side]: pending.value } : {}
5241
+ };
5242
+ const invalidSide = pending ? pending.side : null;
5243
+ valueInput = [
5244
+ h6(DateInput3, {
5245
+ key: "from",
5246
+ name: `${name}-from`,
5247
+ label: fromInputLabel,
5248
+ format,
5249
+ value: display.from ?? null,
5250
+ onChange: (next) => handleDateChange("from", next),
5251
+ min,
5252
+ max,
5253
+ readOnly,
5254
+ error: invalidSide === "from",
5255
+ validationMessage: invalidSide === "from" ? invalidRangeMessage : void 0
5256
+ }),
5257
+ isColumn ? null : h6(Text5, { key: "to" }, "to"),
5258
+ h6(DateInput3, {
5259
+ key: "toDate",
5260
+ name: `${name}-to`,
5261
+ label: toInputLabel,
5262
+ format,
5263
+ value: display.to ?? null,
5264
+ onChange: (next) => handleDateChange("to", next),
5265
+ min,
5266
+ max,
5267
+ readOnly,
5268
+ error: invalidSide === "to",
5269
+ validationMessage: invalidSide === "to" ? invalidRangeMessage : void 0
5270
+ })
5271
+ ];
5272
+ } else if (STATIC_DATE_OPERATORS.has(currentOperator)) {
5273
+ valueInput = h6(DateInput3, {
5274
+ key: "date",
5275
+ name: `${name}-date`,
5276
+ label: singleDateInputLabel,
5277
+ format,
5278
+ value: resolvedCurrent.date ?? null,
5279
+ onChange: handleStaticDateChange,
5280
+ min,
5281
+ max,
5282
+ readOnly
5283
+ });
5284
+ } else if (ROLLING_OPERATORS.has(currentOperator)) {
5285
+ const compound = `${resolvedCurrent.unit || "day"}:${resolvedCurrent.direction || "backward"}`;
5286
+ valueInput = [
5287
+ h6(NumberInput2, {
5288
+ key: "amount",
5289
+ name: `${name}-amount`,
5290
+ label: COMPACT_LABEL,
5291
+ min: 0,
5292
+ value: resolvedCurrent.amount ?? 1,
5293
+ onChange: (amount) => emit({
5294
+ operator: currentOperator,
5295
+ amount: Number.isFinite(Number(amount)) ? Number(amount) : 0,
5296
+ unit: resolvedCurrent.unit || "day",
5297
+ direction: resolvedCurrent.direction || "backward"
5298
+ }),
5299
+ readOnly
5300
+ }),
5301
+ h6(Select7, {
5302
+ key: "unit",
5303
+ name: `${name}-rolling-unit`,
5304
+ label: COMPACT_LABEL,
5305
+ options: rollingUnitOptions,
5306
+ value: compound,
5307
+ onChange: handleRollingUnitChange,
5308
+ readOnly
5309
+ })
5310
+ ];
5311
+ } else if (PRESENCE_OPERATORS.has(currentOperator)) {
5312
+ valueInput = null;
5313
+ } else {
5314
+ const range = presetToRange(resolvedCurrent.preset);
5315
+ const presetValue = resolvedCurrent.preset || (lastPreset.rangeKey && range && lastPreset.rangeKey === keyOfRange(range) ? lastPreset.key : "");
5316
+ valueInput = h6(Select7, {
5317
+ key: "preset",
5318
+ name: `${name}-preset`,
5319
+ label: COMPACT_LABEL,
5320
+ placeholder: presetPlaceholder,
5321
+ options: presetOptions || [],
5322
+ value: presetValue,
5323
+ onChange: handlePresetChange,
5324
+ readOnly
5325
+ });
5326
+ }
5327
+ const valueChildren = [
5328
+ ...Array.isArray(valueInput) ? valueInput : valueInput ? [valueInput] : [],
5329
+ showClear ? h6(Link4, { key: "clear", onClick: handleClear }, clearLabel) : null
5330
+ ];
5331
+ const children = [operatorSelect, ...valueChildren];
5332
+ if (fieldSelect) {
5333
+ const rowChildren = [
5334
+ h6(Box4, { key: "field-box", flex: "auto", alignSelf: "stretch" }, fieldSelect),
5335
+ operatorSelect ? h6(Box4, { key: "operator-box", flex: "auto", alignSelf: "stretch" }, operatorSelect) : null,
5336
+ ...valueChildren.map(
5337
+ (child, index) => (child == null ? void 0 : child.type) === Text5 || (child == null ? void 0 : child.type) === Link4 ? child : h6(Box4, { key: `value-box-${index}`, flex: "auto", alignSelf: "stretch" }, child)
5338
+ )
5339
+ ].filter(Boolean);
5340
+ const fieldControl = h6(
5341
+ AutoGrid2,
5342
+ {
5343
+ columnWidth: gridColumnWidth,
5344
+ flexible: true,
5345
+ gap: gap ?? "xs"
5346
+ },
5347
+ ...rowChildren
5348
+ );
5349
+ if (!label) return fieldControl;
5350
+ return h6(
5351
+ Flex8,
5352
+ { direction: "column", gap: "xs" },
5353
+ h6(Text5, { format: { fontWeight: "demibold" } }, label),
5354
+ fieldControl
5355
+ );
5356
+ }
5357
+ const control = h6(
5358
+ Flex8,
5359
+ {
5360
+ direction: isColumn ? "column" : "row",
5361
+ align: isColumn ? "stretch" : "center",
5362
+ gap: gap ?? (isColumn ? "sm" : "xs"),
5363
+ wrap: isColumn ? void 0 : "wrap"
5364
+ },
5365
+ ...children
5366
+ );
5367
+ if (!label) return control;
5368
+ return h6(
5369
+ Flex8,
5370
+ { direction: "column", gap: "xs" },
5371
+ h6(Text5, { format: { fontWeight: "demibold" } }, label),
5372
+ control
5373
+ );
5374
+ };
5375
+
1953
5376
  // src/common-components/KeyValueList.js
1954
- import React15 from "react";
1955
- import { DescriptionList as DescriptionList2, DescriptionListItem as DescriptionListItem2, Flex as Flex7 } from "@hubspot/ui-extensions";
5377
+ import React17 from "react";
5378
+ import { DescriptionList as DescriptionList2, DescriptionListItem as DescriptionListItem2, Flex as Flex9 } from "@hubspot/ui-extensions";
1956
5379
  var KeyValueList = ({ items = [], direction = "row", gap = "sm" }) => {
1957
5380
  const rows = items.map(
1958
- (item, index) => React15.createElement(
5381
+ (item, index) => React17.createElement(
1959
5382
  DescriptionListItem2,
1960
5383
  {
1961
5384
  key: item.key ?? item.label ?? `kv-${index}`,
@@ -1964,16 +5387,17 @@ var KeyValueList = ({ items = [], direction = "row", gap = "sm" }) => {
1964
5387
  item.value
1965
5388
  )
1966
5389
  );
1967
- return React15.createElement(
1968
- Flex7,
5390
+ return React17.createElement(
5391
+ Flex9,
1969
5392
  { direction: "column", gap },
1970
- React15.createElement(DescriptionList2, { direction }, ...rows)
5393
+ React17.createElement(DescriptionList2, { direction }, ...rows)
1971
5394
  );
1972
5395
  };
5396
+ KeyValueList.displayName = "KeyValueList";
1973
5397
 
1974
5398
  // src/common-components/SectionHeader.js
1975
- import React16 from "react";
1976
- import { Flex as Flex8, Heading, Text as Text5 } from "@hubspot/ui-extensions";
5399
+ import React18 from "react";
5400
+ import { Flex as Flex10, Heading, Text as Text6 } from "@hubspot/ui-extensions";
1977
5401
  var SectionHeader = ({
1978
5402
  title,
1979
5403
  description,
@@ -1984,12 +5408,12 @@ var SectionHeader = ({
1984
5408
  }) => {
1985
5409
  const body = [];
1986
5410
  if (title != null) {
1987
- body.push(React16.createElement(Heading, { key: "title", as: titleAs }, title));
5411
+ body.push(React18.createElement(Heading, { key: "title", as: titleAs }, title));
1988
5412
  }
1989
5413
  if (description != null) {
1990
5414
  body.push(
1991
- React16.createElement(
1992
- Text5,
5415
+ React18.createElement(
5416
+ Text6,
1993
5417
  { key: "description", variant: "microcopy" },
1994
5418
  description
1995
5419
  )
@@ -1998,10 +5422,10 @@ var SectionHeader = ({
1998
5422
  if (children != null) {
1999
5423
  body.push(children);
2000
5424
  }
2001
- const content = React16.createElement(Flex8, { direction: "column", gap }, ...body);
5425
+ const content = React18.createElement(Flex10, { direction: "column", gap }, ...body);
2002
5426
  if (actions == null) return content;
2003
- return React16.createElement(
2004
- Flex8,
5427
+ return React18.createElement(
5428
+ Flex10,
2005
5429
  { direction: "row", justify: "between", align: "start", gap: "sm" },
2006
5430
  content,
2007
5431
  actions
@@ -2009,8 +5433,8 @@ var SectionHeader = ({
2009
5433
  };
2010
5434
 
2011
5435
  // src/common-components/Spinner.js
2012
- import React17, { useEffect as useEffect5, useRef as useRef5, useState as useState6 } from "react";
2013
- import { Text as Text6 } from "@hubspot/ui-extensions";
5436
+ import React19, { useEffect as useEffect5, useRef as useRef6, useState as useState8 } from "react";
5437
+ import { Text as Text7 } from "@hubspot/ui-extensions";
2014
5438
 
2015
5439
  // src/common-components/spinners.js
2016
5440
  var BRAILLE_DOT_MAP = [
@@ -2350,8 +5774,8 @@ var Spinner = ({
2350
5774
  const preset = SPINNERS[name] || SPINNERS[DEFAULT_NAME];
2351
5775
  const resolvedFrames = Array.isArray(frames) && frames.length > 0 ? frames : preset.frames;
2352
5776
  const resolvedInterval = Number.isFinite(interval) ? interval : preset.interval;
2353
- const [index, setIndex] = useState6(0);
2354
- const indexRef = useRef5(0);
5777
+ const [index, setIndex] = useState8(0);
5778
+ const indexRef = useRef6(0);
2355
5779
  indexRef.current = index;
2356
5780
  useEffect5(() => {
2357
5781
  if (paused || resolvedFrames.length <= 1) return void 0;
@@ -2368,10 +5792,10 @@ var Spinner = ({
2368
5792
  const frame = resolvedFrames[index % resolvedFrames.length];
2369
5793
  const suffix = children != null ? children : label;
2370
5794
  if (suffix == null || suffix === "") {
2371
- return React17.createElement(Text6, rest, frame);
5795
+ return React19.createElement(Text7, rest, frame);
2372
5796
  }
2373
- return React17.createElement(
2374
- Text6,
5797
+ return React19.createElement(
5798
+ Text7,
2375
5799
  rest,
2376
5800
  frame,
2377
5801
  gap,
@@ -2383,12 +5807,18 @@ export {
2383
5807
  AutoStatusTag,
2384
5808
  AutoTag,
2385
5809
  AvatarStack,
5810
+ CREATE_OPTION_VALUE,
2386
5811
  CollectionCount,
2387
5812
  CollectionFilterControl,
2388
5813
  CollectionSortSelect,
2389
5814
  CollectionToolbar,
2390
5815
  CrmLookupSelect,
5816
+ CrmRecordPicker,
5817
+ DATE_FILTER_OPERATORS,
5818
+ DATE_RANGE_CUSTOM_VALUE,
5819
+ DATE_ROLLING_UNIT_OPTIONS,
2391
5820
  DEFAULT_SVG_FONT_WEIGHT,
5821
+ DateRangePicker,
2392
5822
  HS_DATE_DIRECTION_LABELS,
2393
5823
  HS_DATE_PRESETS,
2394
5824
  HS_FONT_FAMILY,
@@ -2409,16 +5839,32 @@ export {
2409
5839
  Icon,
2410
5840
  KeyValueList,
2411
5841
  NATIVE_ICON_NAME_LIST,
5842
+ SKELETON_FILL,
2412
5843
  SPINNERS,
2413
5844
  SPINNER_NAMES,
2414
5845
  SectionHeader,
2415
5846
  Spinner,
2416
5847
  StyledText,
5848
+ compareHsDateValues,
5849
+ enforceSelectionMax,
2417
5850
  formatCollectionCount,
5851
+ getRecordId,
2418
5852
  gridToBraille,
5853
+ isRecordLike,
5854
+ isValidDateRange,
2419
5855
  makeAvatarStackDataUri,
5856
+ makeCreateOption,
2420
5857
  makeGrid,
2421
5858
  makeIconDataUri,
2422
5859
  makeStyledTextDataUri,
2423
- svgToIconEntry
5860
+ mapIdsToRecords,
5861
+ mergePickerOptions,
5862
+ normalizeRecordSelection,
5863
+ presetToRange,
5864
+ recordToPickerOption,
5865
+ shouldShowCreateOption,
5866
+ splitCreateSelection,
5867
+ svgToIconEntry,
5868
+ toHsDateValue,
5869
+ upsertRecords
2424
5870
  };