hs-uix 2.1.1 → 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 +355 -57
  4. package/dist/calendar.mjs +356 -57
  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 +3208 -287
  18. package/dist/index.mjs +3156 -283
  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 +74 -5
  31. package/src/calendar/index.d.ts +95 -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
package/dist/kanban.mjs CHANGED
@@ -167,6 +167,109 @@ var useSelectionReset = ({ resetKey, enabled, isControlled, clearSelection }) =>
167
167
  }, [resetKey, enabled, isControlled, clearSelection]);
168
168
  };
169
169
 
170
+ // src/kanban/kanbanLanes.js
171
+ var UNASSIGNED_LANE_KEY = "__unassigned";
172
+ var UNKNOWN_STAGE_KEY = "__unknown";
173
+ var getLaneKey = (row, swimlaneBy) => {
174
+ const raw = typeof swimlaneBy === "function" ? swimlaneBy(row) : row == null ? void 0 : row[swimlaneBy];
175
+ if (raw == null || raw === "") return UNASSIGNED_LANE_KEY;
176
+ return String(raw);
177
+ };
178
+ var orderLaneKeys = (seenKeys, swimlaneOrder) => {
179
+ const seen = Array.isArray(seenKeys) ? seenKeys : [];
180
+ if (!Array.isArray(swimlaneOrder) || swimlaneOrder.length === 0) return [...seen];
181
+ const explicit = [];
182
+ for (const key of swimlaneOrder) {
183
+ const normalized = String(key);
184
+ if (!explicit.includes(normalized)) explicit.push(normalized);
185
+ }
186
+ const rest = seen.filter((key) => !explicit.includes(key));
187
+ return [...explicit, ...rest];
188
+ };
189
+ var partitionLanes = (rows, { swimlaneBy, swimlaneOrder } = {}) => {
190
+ const rowsByLane = {};
191
+ const firstSeen = [];
192
+ for (const row of rows || []) {
193
+ const key = getLaneKey(row, swimlaneBy);
194
+ if (!rowsByLane[key]) {
195
+ rowsByLane[key] = [];
196
+ firstSeen.push(key);
197
+ }
198
+ rowsByLane[key].push(row);
199
+ }
200
+ const laneKeys = orderLaneKeys(firstSeen, swimlaneOrder);
201
+ for (const key of laneKeys) {
202
+ if (!rowsByLane[key]) rowsByLane[key] = [];
203
+ }
204
+ return { laneKeys, rowsByLane };
205
+ };
206
+ var resolveLaneLabel = (laneKey, swimlaneLabels, rows, unassignedLabel) => {
207
+ if (typeof swimlaneLabels === "function") {
208
+ const out = swimlaneLabels(laneKey, rows || []);
209
+ if (out != null) return out;
210
+ } else if (swimlaneLabels && typeof swimlaneLabels === "object") {
211
+ const out = swimlaneLabels[laneKey];
212
+ if (out != null) return out;
213
+ }
214
+ if (laneKey === UNASSIGNED_LANE_KEY) return unassignedLabel || "Unassigned";
215
+ return String(laneKey);
216
+ };
217
+ var bucketRowsByStage = (rows, stages, getStage) => {
218
+ const map = {};
219
+ for (const stage of stages || []) map[stage.value] = [];
220
+ for (const row of rows || []) {
221
+ const key = getStage(row);
222
+ if (map[key]) {
223
+ map[key].push(row);
224
+ } else if ((stages || []).length > 0) {
225
+ if (!map[UNKNOWN_STAGE_KEY]) map[UNKNOWN_STAGE_KEY] = [];
226
+ map[UNKNOWN_STAGE_KEY].push(row);
227
+ }
228
+ }
229
+ return map;
230
+ };
231
+ var sortBuckets = (buckets, comparator) => {
232
+ if (!comparator) return buckets;
233
+ const out = {};
234
+ for (const key of Object.keys(buckets || {})) {
235
+ out[key] = [...buckets[key]].sort(comparator);
236
+ }
237
+ return out;
238
+ };
239
+ var resolveWipLimit = (stage, wipLimits) => {
240
+ const override = wipLimits ? wipLimits[stage == null ? void 0 : stage.value] : void 0;
241
+ const limit = override != null ? override : stage == null ? void 0 : stage.wipLimit;
242
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit < 0) return null;
243
+ return limit;
244
+ };
245
+ var computeStageCounts = (stages, buckets, stageMeta) => {
246
+ const counts = {};
247
+ for (const stage of stages || []) {
248
+ const meta = stageMeta ? stageMeta[stage.value] : void 0;
249
+ counts[stage.value] = meta && meta.totalCount != null ? meta.totalCount : ((buckets == null ? void 0 : buckets[stage.value]) || []).length;
250
+ }
251
+ return counts;
252
+ };
253
+ var evaluateWip = (stages, counts, wipLimits) => {
254
+ const out = {};
255
+ for (const stage of stages || []) {
256
+ const limit = resolveWipLimit(stage, wipLimits);
257
+ const count = (counts == null ? void 0 : counts[stage.value]) || 0;
258
+ out[stage.value] = { count, limit, exceeded: limit != null && count > limit };
259
+ }
260
+ return out;
261
+ };
262
+ var findNewlyExceededWip = (prev, next) => {
263
+ const events = [];
264
+ for (const stageId of Object.keys(next || {})) {
265
+ const entry = next[stageId];
266
+ if (!entry || !entry.exceeded) continue;
267
+ if (prev && prev[stageId] && prev[stageId].exceeded) continue;
268
+ events.push({ stageId, count: entry.count, limit: entry.limit });
269
+ }
270
+ return events;
271
+ };
272
+
170
273
  // src/common-components/CollectionSortSelect.js
171
274
  import React, { useId } from "react";
172
275
  import { Select } from "@hubspot/ui-extensions";
@@ -526,7 +629,7 @@ var GENERATED_ICONS = {
526
629
  "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"] }
527
630
  };
528
631
 
529
- // src/common-components/Icon.js
632
+ // src/common-components/nativeIconNames.js
530
633
  var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
531
634
  "add",
532
635
  "appointment",
@@ -538,12 +641,12 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
538
641
  "block",
539
642
  "book",
540
643
  "bulb",
644
+ "callTranscript",
541
645
  "calling",
542
646
  "callingHangup",
543
647
  "callingMade",
544
648
  "callingMissed",
545
649
  "callingVoicemail",
546
- "callTranscript",
547
650
  "campaigns",
548
651
  "cap",
549
652
  "checkCircle",
@@ -572,13 +675,13 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
572
675
  "enroll",
573
676
  "exclamation",
574
677
  "exclamationCircle",
575
- "facebook",
576
678
  "faceHappy",
577
679
  "faceHappyFilled",
578
680
  "faceNeutral",
579
681
  "faceNeutralFilled",
580
682
  "faceSad",
581
683
  "faceSadFilled",
684
+ "facebook",
582
685
  "favoriteHollow",
583
686
  "file",
584
687
  "filledXCircleIcon",
@@ -719,6 +822,8 @@ var NATIVE_ICON_NAMES = /* @__PURE__ */ new Set([
719
822
  "zoomIn",
720
823
  "zoomOut"
721
824
  ]);
825
+
826
+ // src/common-components/Icon.js
722
827
  var NATIVE_COLORS = /* @__PURE__ */ new Set(["inherit", "alert", "warning", "success"]);
723
828
  var NATIVE_SIZE_TOKENS = {
724
829
  sm: "sm",
@@ -931,6 +1036,7 @@ var CollectionFilterControl = ({
931
1036
  { key: name, direction: "row", align: "center", gap: "xs" },
932
1037
  h3(DateInput, {
933
1038
  name: `${controlName}-from`,
1039
+ label: filter.fromLabel ?? labels.dateFrom,
934
1040
  placeholder: filter.fromLabel ?? labels.dateFrom,
935
1041
  format: "medium",
936
1042
  value: rangeValue.from ?? null,
@@ -939,6 +1045,7 @@ var CollectionFilterControl = ({
939
1045
  h3(Icon, { name: "right", size: "sm" }),
940
1046
  h3(DateInput, {
941
1047
  name: `${controlName}-to`,
1048
+ label: filter.toLabel ?? labels.dateTo,
942
1049
  placeholder: filter.toLabel ?? labels.dateTo,
943
1050
  format: "medium",
944
1051
  value: rangeValue.to ?? null,
@@ -1269,6 +1376,7 @@ import {
1269
1376
  Statistics,
1270
1377
  StatisticsItem,
1271
1378
  StatisticsTrend,
1379
+ StatusTag,
1272
1380
  Tag as Tag3,
1273
1381
  Text,
1274
1382
  Tile
@@ -1306,6 +1414,12 @@ var DEFAULT_LABELS3 = {
1306
1414
  errorTitle: "Something went wrong.",
1307
1415
  errorMessage: "An error occurred while loading data.",
1308
1416
  cardCount: (n) => String(n),
1417
+ // "5 / 4" — count vs WIP limit in the stage header. The slash format is the
1418
+ // standard kanban convention; override for tighter ("5/4") or verbose forms.
1419
+ wipCount: (count, limit) => `${count} / ${limit}`,
1420
+ overWip: "Over WIP",
1421
+ laneCount: (n) => String(n),
1422
+ unassignedLane: "Unassigned",
1309
1423
  moveTo: "Move",
1310
1424
  clearAll: "Clear all",
1311
1425
  selectAll: (count, label) => `Select all ${count} ${label}`,
@@ -1532,10 +1646,14 @@ var KanbanColumn = ({
1532
1646
  onToggleCollapsed,
1533
1647
  columnFooter,
1534
1648
  countDisplay,
1649
+ wip,
1650
+ compactEmpty,
1535
1651
  labels,
1536
1652
  children
1537
1653
  }) => {
1538
- const countLabel = labels.cardCount(totalCount != null ? totalCount : bucketCount);
1654
+ const hasWipLimit = wip != null && wip.limit != null;
1655
+ const countLabel = hasWipLimit ? labels.wipCount(wip.count, wip.limit) : labels.cardCount(totalCount != null ? totalCount : bucketCount);
1656
+ const overWipNode = wip && wip.exceeded ? /* @__PURE__ */ React7.createElement(StatusTag, { variant: "warning" }, labels.overWip) : null;
1539
1657
  const countNode = countDisplay === "text" ? /* @__PURE__ */ React7.createElement(Text, { format: { fontWeight: "demibold" } }, countLabel) : countDisplay === "none" ? null : /* @__PURE__ */ React7.createElement(Tag3, { variant: "default" }, countLabel);
1540
1658
  if (collapsed) {
1541
1659
  const rotated = makeRotatedLabelDataUri(stage.label);
@@ -1568,8 +1686,11 @@ var KanbanColumn = ({
1568
1686
  }
1569
1687
  ) : null));
1570
1688
  }
1689
+ if (compactEmpty && !loading && rows.length === 0 && bucketCount === 0) {
1690
+ return /* @__PURE__ */ React7.createElement(Tile, { compact: true }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", align: "center", justify: "between", gap: "xs" }, /* @__PURE__ */ React7.createElement(Text, { variant: "microcopy", format: { fontWeight: "demibold" } }, stage.shortLabel || stage.label), /* @__PURE__ */ React7.createElement(Text, { variant: "microcopy", format: { italic: true } }, labels.emptyColumn)));
1691
+ }
1571
1692
  const footerContent = stage.footer ? stage.footer(rows) : columnFooter ? columnFooter(rows, stage) : null;
1572
- return /* @__PURE__ */ React7.createElement(Tile, { compact: true }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "column", gap: "xs" }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", align: "center", justify: "between", gap: "xs" }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React7.createElement(Text, { format: { fontWeight: "demibold" } }, stage.shortLabel || stage.label), countNode, loading ? /* @__PURE__ */ React7.createElement(LoadingSpinner, { size: "xs" }) : null), /* @__PURE__ */ React7.createElement(Button3, { variant: "transparent", size: "sm", onClick: onToggleCollapsed, tooltip: "Collapse" }, /* @__PURE__ */ React7.createElement(Icon, { name: "left", size: "sm", screenReaderText: `Collapse ${stage.label}` }))), footerContent ? /* @__PURE__ */ React7.createElement(Text, { variant: "microcopy" }, footerContent) : null, /* @__PURE__ */ React7.createElement(Divider, null), children, error ? /* @__PURE__ */ React7.createElement(Alert, { variant: "danger", title: labels.errorTitle }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", gap: "xs", align: "center" }, /* @__PURE__ */ React7.createElement(Text, { variant: "microcopy" }, error), onLoadMore ? /* @__PURE__ */ React7.createElement(Button3, { variant: "transparent", size: "xs", onClick: () => onLoadMore(stage.value) }, labels.retryLoadMore) : null)) : null, !error && hasMore && onLoadMore && !loading && bucketCount > 0 ? /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", justify: "center" }, /* @__PURE__ */ React7.createElement(Link2, { onClick: () => onLoadMore(stage.value) }, labels.loadMore(bucketCount, totalCount))) : null, !error && loading && hasMore ? /* @__PURE__ */ React7.createElement(LoadingSpinner, { size: "sm", layout: "centered", label: labels.loadingMore }) : null, !error && !hasMore && bucketCount > rows.length ? /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", justify: "center" }, /* @__PURE__ */ React7.createElement(Link2, { onClick: onToggleExpanded }, expanded ? labels.showLess : labels.showMore(rows.length, bucketCount))) : null, rows.length === 0 && bucketCount === 0 && !loading ? /* @__PURE__ */ React7.createElement(Text, { variant: "microcopy", format: { italic: true } }, labels.emptyColumn) : null));
1693
+ return /* @__PURE__ */ React7.createElement(Tile, { compact: true }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "column", gap: "xs" }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", align: "center", justify: "between", gap: "xs" }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React7.createElement(Text, { format: { fontWeight: "demibold" } }, stage.shortLabel || stage.label), countNode, overWipNode, loading ? /* @__PURE__ */ React7.createElement(LoadingSpinner, { size: "xs" }) : null), /* @__PURE__ */ React7.createElement(Button3, { variant: "transparent", size: "sm", onClick: onToggleCollapsed, tooltip: "Collapse" }, /* @__PURE__ */ React7.createElement(Icon, { name: "left", size: "sm", screenReaderText: `Collapse ${stage.label}` }))), footerContent ? /* @__PURE__ */ React7.createElement(Text, { variant: "microcopy" }, footerContent) : null, /* @__PURE__ */ React7.createElement(Divider, null), children, error ? /* @__PURE__ */ React7.createElement(Alert, { variant: "danger", title: labels.errorTitle }, /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", gap: "xs", align: "center" }, /* @__PURE__ */ React7.createElement(Text, { variant: "microcopy" }, error), onLoadMore ? /* @__PURE__ */ React7.createElement(Button3, { variant: "transparent", size: "xs", onClick: () => onLoadMore(stage.value) }, labels.retryLoadMore) : null)) : null, !error && hasMore && onLoadMore && !loading && bucketCount > 0 ? /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", justify: "center" }, /* @__PURE__ */ React7.createElement(Link2, { onClick: () => onLoadMore(stage.value) }, labels.loadMore(bucketCount, totalCount))) : null, !error && loading && hasMore ? /* @__PURE__ */ React7.createElement(LoadingSpinner, { size: "sm", layout: "centered", label: labels.loadingMore }) : null, !error && !hasMore && bucketCount > rows.length ? /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", justify: "center" }, /* @__PURE__ */ React7.createElement(Link2, { onClick: onToggleExpanded }, expanded ? labels.showLess : labels.showMore(rows.length, bucketCount))) : null, rows.length === 0 && bucketCount === 0 && !loading ? /* @__PURE__ */ React7.createElement(Text, { variant: "microcopy", format: { italic: true } }, labels.emptyColumn) : null));
1573
1694
  };
1574
1695
  var renderMetricsPanel = (metrics) => {
1575
1696
  if (!metrics) return null;
@@ -1608,14 +1729,14 @@ var KanbanToolbar = ({
1608
1729
  sortOptions,
1609
1730
  sortValue,
1610
1731
  onSortChange,
1611
- metrics,
1612
- showMetrics,
1732
+ showMetricsButton,
1733
+ metricsPanel,
1613
1734
  onToggleMetrics,
1614
1735
  labels,
1615
1736
  toolbarLeftFlex,
1616
1737
  toolbarRightFlex
1617
1738
  }) => {
1618
- const rightControls = (sortOptions == null ? void 0 : sortOptions.length) > 0 || metrics ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, sortOptions && sortOptions.length > 0 ? /* @__PURE__ */ React7.createElement(
1739
+ const rightControls = (sortOptions == null ? void 0 : sortOptions.length) > 0 || showMetricsButton ? /* @__PURE__ */ React7.createElement(React7.Fragment, null, sortOptions && sortOptions.length > 0 ? /* @__PURE__ */ React7.createElement(
1619
1740
  CollectionSortSelect,
1620
1741
  {
1621
1742
  name: "kanban-sort",
@@ -1624,7 +1745,7 @@ var KanbanToolbar = ({
1624
1745
  options: sortOptions,
1625
1746
  onChange: onSortChange
1626
1747
  }
1627
- ) : null, metrics ? /* @__PURE__ */ React7.createElement(Button3, { variant: "secondary", size: "small", onClick: onToggleMetrics }, /* @__PURE__ */ React7.createElement(Icon, { name: "gauge", size: "sm" }), " ", labels.metricsButton) : null) : null;
1748
+ ) : null, showMetricsButton ? /* @__PURE__ */ React7.createElement(Button3, { variant: "secondary", size: "small", onClick: onToggleMetrics }, /* @__PURE__ */ React7.createElement(Icon, { name: "gauge", size: "sm" }), " ", labels.metricsButton) : null) : null;
1628
1749
  return /* @__PURE__ */ React7.createElement(
1629
1750
  CollectionToolbar,
1630
1751
  {
@@ -1651,7 +1772,7 @@ var KanbanToolbar = ({
1651
1772
  onRemove: onFilterRemove
1652
1773
  },
1653
1774
  right: rightControls,
1654
- footer: showMetrics && metrics ? renderMetricsPanel(metrics) : null,
1775
+ footer: metricsPanel,
1655
1776
  labels,
1656
1777
  leftFlex: toolbarLeftFlex,
1657
1778
  rightFlex: toolbarRightFlex
@@ -1703,6 +1824,18 @@ var Kanban = ({
1703
1824
  // --- Per-stage pagination ---
1704
1825
  stageMeta,
1705
1826
  onLoadMore,
1827
+ // --- WIP limits ---
1828
+ wipLimits,
1829
+ onWipExceeded,
1830
+ // --- Swimlanes ---
1831
+ swimlaneBy,
1832
+ swimlaneLabels,
1833
+ swimlaneOrder,
1834
+ collapseLanes = true,
1835
+ collapsedLanes,
1836
+ defaultCollapsedLanes,
1837
+ onCollapsedLanesChange,
1838
+ metricsPerLane = false,
1706
1839
  // --- Selection ---
1707
1840
  selectable = false,
1708
1841
  selectedIds,
@@ -1767,6 +1900,9 @@ var Kanban = ({
1767
1900
  const [internalFilters, setInternalFilters] = useState2(() => getEmptyFilterValues(filters));
1768
1901
  const [internalSort, setInternalSort] = useState2(defaultSort || (((_a = sortOptions == null ? void 0 : sortOptions[0]) == null ? void 0 : _a.value) ?? ""));
1769
1902
  const [internalCollapsed, setInternalCollapsed] = useState2([]);
1903
+ const [internalCollapsedLanes, setInternalCollapsedLanes] = useState2(
1904
+ () => defaultCollapsedLanes || []
1905
+ );
1770
1906
  const [internalExpanded, setInternalExpanded] = useState2([]);
1771
1907
  const [internalSelection, setInternalSelection] = useState2([]);
1772
1908
  const [internalShowMetrics, setInternalShowMetrics] = useState2(false);
@@ -1783,6 +1919,7 @@ var Kanban = ({
1783
1919
  const resolvedFilters = filterValues != null ? filterValues : internalFilters;
1784
1920
  const resolvedSort = sort != null ? sort : internalSort;
1785
1921
  const resolvedCollapsed = collapsedStages != null ? collapsedStages : internalCollapsed;
1922
+ const resolvedCollapsedLanes = collapsedLanes != null ? collapsedLanes : internalCollapsedLanes;
1786
1923
  const resolvedExpanded = expandedStages != null ? expandedStages : internalExpanded;
1787
1924
  const resolvedSelection = selectedIds != null ? selectedIds : internalSelection;
1788
1925
  const searchEnabled = showSearch && Array.isArray(searchFields) && searchFields.length > 0;
@@ -1799,9 +1936,10 @@ var Kanban = ({
1799
1936
  search: overrides.search != null ? overrides.search : resolvedSearch,
1800
1937
  filters: overrides.filters != null ? overrides.filters : resolvedFilters,
1801
1938
  sort: overrides.sort != null ? overrides.sort : resolvedSort || null,
1802
- collapsedStages: overrides.collapsedStages != null ? overrides.collapsedStages : resolvedCollapsed
1939
+ collapsedStages: overrides.collapsedStages != null ? overrides.collapsedStages : resolvedCollapsed,
1940
+ collapsedLanes: overrides.collapsedLanes != null ? overrides.collapsedLanes : resolvedCollapsedLanes
1803
1941
  });
1804
- }, [onParamsChange, resolvedCollapsed, resolvedFilters, resolvedSearch, resolvedSort]);
1942
+ }, [onParamsChange, resolvedCollapsed, resolvedCollapsedLanes, resolvedFilters, resolvedSearch, resolvedSort]);
1805
1943
  const lastAppliedSearchRef = useRef2(searchValue != null ? searchValue : "");
1806
1944
  useEffect2(() => {
1807
1945
  if (searchValue == null) return;
@@ -1860,6 +1998,15 @@ var Kanban = ({
1860
1998
  },
1861
1999
  [fireParamsChange, resolvedCollapsed, collapsedStages, onCollapsedStagesChange]
1862
2000
  );
2001
+ const handleLaneCollapsed = useCallback2(
2002
+ (laneKey) => {
2003
+ const next = resolvedCollapsedLanes.includes(laneKey) ? resolvedCollapsedLanes.filter((k) => k !== laneKey) : [...resolvedCollapsedLanes, laneKey];
2004
+ if (onCollapsedLanesChange) onCollapsedLanesChange(next);
2005
+ if (collapsedLanes == null) setInternalCollapsedLanes(next);
2006
+ fireParamsChange({ collapsedLanes: next });
2007
+ },
2008
+ [fireParamsChange, resolvedCollapsedLanes, collapsedLanes, onCollapsedLanesChange]
2009
+ );
1863
2010
  const handleExpanded = useCallback2(
1864
2011
  (stageValue) => {
1865
2012
  const next = resolvedExpanded.includes(stageValue) ? resolvedExpanded.filter((v) => v !== stageValue) : [...resolvedExpanded, stageValue];
@@ -1928,33 +2075,45 @@ var Kanban = ({
1928
2075
  }
1929
2076
  return result;
1930
2077
  }, [data, resolvedSearch, resolvedFilters, filters, searchEnabled, searchFields, fuzzySearch, fuzzyOptions]);
1931
- const buckets = useMemo(() => {
1932
- const map = {};
1933
- for (const stage of stages) map[stage.value] = [];
1934
- for (const row of filteredData) {
1935
- const key = getStageFor(row);
1936
- if (map[key]) {
1937
- map[key].push(row);
1938
- } else if (stages.length > 0) {
1939
- if (!map.__unknown) map.__unknown = [];
1940
- map.__unknown.push(row);
1941
- }
1942
- }
1943
- return map;
1944
- }, [filteredData, stages, getStageFor]);
2078
+ const buckets = useMemo(
2079
+ () => bucketRowsByStage(filteredData, stages, getStageFor),
2080
+ [filteredData, stages, getStageFor]
2081
+ );
1945
2082
  const sortComparator = useMemo(() => {
1946
2083
  if (!sortOptions || !resolvedSort) return null;
1947
2084
  const opt = sortOptions.find((s) => s.value === resolvedSort);
1948
2085
  return (opt == null ? void 0 : opt.comparator) || null;
1949
2086
  }, [sortOptions, resolvedSort]);
1950
- const sortedBuckets = useMemo(() => {
1951
- if (!sortComparator) return buckets;
2087
+ const sortedBuckets = useMemo(() => sortBuckets(buckets, sortComparator), [buckets, sortComparator]);
2088
+ const hasLanes = swimlaneBy != null;
2089
+ const laneData = useMemo(() => {
2090
+ if (!hasLanes) return null;
2091
+ return partitionLanes(filteredData, { swimlaneBy, swimlaneOrder });
2092
+ }, [hasLanes, filteredData, swimlaneBy, swimlaneOrder]);
2093
+ const laneBuckets = useMemo(() => {
2094
+ if (!laneData) return null;
1952
2095
  const out = {};
1953
- for (const key of Object.keys(buckets)) {
1954
- out[key] = [...buckets[key]].sort(sortComparator);
2096
+ for (const laneKey of laneData.laneKeys) {
2097
+ out[laneKey] = sortBuckets(
2098
+ bucketRowsByStage(laneData.rowsByLane[laneKey] || [], stages, getStageFor),
2099
+ sortComparator
2100
+ );
1955
2101
  }
1956
2102
  return out;
1957
- }, [buckets, sortComparator]);
2103
+ }, [laneData, stages, getStageFor, sortComparator]);
2104
+ const wipByStage = useMemo(
2105
+ () => evaluateWip(stages, computeStageCounts(stages, buckets, stageMeta), wipLimits),
2106
+ [stages, buckets, stageMeta, wipLimits]
2107
+ );
2108
+ const prevWipRef = useRef2({});
2109
+ useEffect2(() => {
2110
+ const newlyExceeded = findNewlyExceededWip(prevWipRef.current, wipByStage);
2111
+ prevWipRef.current = wipByStage;
2112
+ if (!onWipExceeded) return;
2113
+ for (const event of newlyExceeded) {
2114
+ onWipExceeded(event.stageId, event.count, event.limit);
2115
+ }
2116
+ }, [wipByStage, onWipExceeded]);
1958
2117
  const activeChips = useMemo(
1959
2118
  () => buildActiveFilterChips(filters, resolvedFilters),
1960
2119
  [filters, resolvedFilters]
@@ -2081,6 +2240,52 @@ var Kanban = ({
2081
2240
  selectionActions: selectionActions || [],
2082
2241
  labels
2083
2242
  };
2243
+ const metricsProvided = metrics != null && (!Array.isArray(metrics) || metrics.length > 0);
2244
+ const perLaneMetricsActive = hasLanes && metricsPerLane && typeof metrics === "function";
2245
+ const globalMetricsContent = metricsProvided && !perLaneMetricsActive ? typeof metrics === "function" ? metrics(filteredData, null) : metrics : null;
2246
+ const renderStageColumns = (bucketMap, laneKey) => {
2247
+ const inLane = laneKey != null;
2248
+ return /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", gap: "sm", wrap: "nowrap" }, stages.map((stage) => {
2249
+ const stageRows = bucketMap[stage.value] || [];
2250
+ const meta = inLane ? void 0 : stageMeta == null ? void 0 : stageMeta[stage.value];
2251
+ const isExpanded = resolvedExpanded.includes(stage.value);
2252
+ const clamp = isExpanded ? maxCardsExpanded : maxCardsPerColumn;
2253
+ const visibleRows = stageRows.slice(0, clamp);
2254
+ const isCollapsed = resolvedCollapsed.includes(stage.value);
2255
+ const stageWip = wipByStage[stage.value];
2256
+ const wip = inLane ? (stageWip == null ? void 0 : stageWip.exceeded) ? { count: stageWip.count, limit: null, exceeded: true } : null : stageWip;
2257
+ return /* @__PURE__ */ React7.createElement(
2258
+ AutoGrid,
2259
+ {
2260
+ key: inLane ? `${laneKey}::${stage.value}` : stage.value,
2261
+ columnWidth: isCollapsed ? 72 : effectiveColumnWidth
2262
+ },
2263
+ /* @__PURE__ */ React7.createElement(
2264
+ KanbanColumn,
2265
+ {
2266
+ stage,
2267
+ rows: visibleRows,
2268
+ bucketCount: stageRows.length,
2269
+ totalCount: meta == null ? void 0 : meta.totalCount,
2270
+ hasMore: meta == null ? void 0 : meta.hasMore,
2271
+ loading: meta == null ? void 0 : meta.loading,
2272
+ error: meta == null ? void 0 : meta.error,
2273
+ onLoadMore: inLane ? void 0 : onLoadMore,
2274
+ expanded: isExpanded,
2275
+ onToggleExpanded: () => handleExpanded(stage.value),
2276
+ collapsed: isCollapsed,
2277
+ onToggleCollapsed: () => handleCollapsed(stage.value),
2278
+ columnFooter,
2279
+ countDisplay,
2280
+ wip,
2281
+ compactEmpty: inLane,
2282
+ labels
2283
+ },
2284
+ visibleRows.map((row) => renderCardNode(row, stage))
2285
+ )
2286
+ );
2287
+ }));
2288
+ };
2084
2289
  const mainContent = error ? renderErrorState ? renderErrorState({
2085
2290
  error,
2086
2291
  title: labels.errorTitle,
@@ -2092,35 +2297,32 @@ var Kanban = ({
2092
2297
  ) : filteredData.length === 0 ? renderEmptyState ? renderEmptyState({
2093
2298
  title: labels.emptyTitle,
2094
2299
  message: labels.emptyMessage
2095
- }) : /* @__PURE__ */ React7.createElement(Tile, null, /* @__PURE__ */ React7.createElement(Flex4, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React7.createElement(EmptyState, { title: labels.emptyTitle, layout: "vertical" }, /* @__PURE__ */ React7.createElement(Text, null, labels.emptyMessage)))) : /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", gap: "sm", wrap: "nowrap" }, stages.map((stage) => {
2096
- const stageRows = sortedBuckets[stage.value] || [];
2097
- const meta = stageMeta == null ? void 0 : stageMeta[stage.value];
2098
- const isExpanded = resolvedExpanded.includes(stage.value);
2099
- const clamp = isExpanded ? maxCardsExpanded : maxCardsPerColumn;
2100
- const visibleRows = stageRows.slice(0, clamp);
2101
- const isCollapsed = resolvedCollapsed.includes(stage.value);
2102
- return /* @__PURE__ */ React7.createElement(AutoGrid, { key: stage.value, columnWidth: isCollapsed ? 72 : effectiveColumnWidth }, /* @__PURE__ */ React7.createElement(
2103
- KanbanColumn,
2300
+ }) : /* @__PURE__ */ React7.createElement(Tile, null, /* @__PURE__ */ React7.createElement(Flex4, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React7.createElement(EmptyState, { title: labels.emptyTitle, layout: "vertical" }, /* @__PURE__ */ React7.createElement(Text, null, labels.emptyMessage)))) : hasLanes ? /* @__PURE__ */ React7.createElement(Flex4, { direction: "column", gap: "md" }, ((laneData == null ? void 0 : laneData.laneKeys) || []).map((laneKey, laneIndex) => {
2301
+ const laneRows = laneData.rowsByLane[laneKey] || [];
2302
+ const laneLabel = resolveLaneLabel(laneKey, swimlaneLabels, laneRows, labels.unassignedLane);
2303
+ const laneLabelText = typeof laneLabel === "string" ? laneLabel : String(laneKey);
2304
+ const isLaneCollapsed = collapseLanes && resolvedCollapsedLanes.includes(laneKey);
2305
+ const laneCountLabel = labels.laneCount(laneRows.length);
2306
+ const laneCountNode = countDisplay === "none" ? null : countDisplay === "text" ? /* @__PURE__ */ React7.createElement(Text, { format: { fontWeight: "demibold" } }, laneCountLabel) : /* @__PURE__ */ React7.createElement(Tag3, { variant: "default" }, laneCountLabel);
2307
+ const laneMetricsNode = !isLaneCollapsed && perLaneMetricsActive && resolvedShowMetrics ? renderMetricsPanel(metrics(laneRows, laneKey)) : null;
2308
+ return /* @__PURE__ */ React7.createElement(Flex4, { key: laneKey, direction: "column", gap: "xs" }, laneIndex > 0 ? /* @__PURE__ */ React7.createElement(Divider, null) : null, /* @__PURE__ */ React7.createElement(Flex4, { direction: "row", align: "center", gap: "xs" }, collapseLanes ? /* @__PURE__ */ React7.createElement(
2309
+ Button3,
2104
2310
  {
2105
- stage,
2106
- rows: visibleRows,
2107
- bucketCount: stageRows.length,
2108
- totalCount: meta == null ? void 0 : meta.totalCount,
2109
- hasMore: meta == null ? void 0 : meta.hasMore,
2110
- loading: meta == null ? void 0 : meta.loading,
2111
- error: meta == null ? void 0 : meta.error,
2112
- onLoadMore,
2113
- expanded: isExpanded,
2114
- onToggleExpanded: () => handleExpanded(stage.value),
2115
- collapsed: isCollapsed,
2116
- onToggleCollapsed: () => handleCollapsed(stage.value),
2117
- columnFooter,
2118
- countDisplay,
2119
- labels
2311
+ variant: "transparent",
2312
+ size: "sm",
2313
+ onClick: () => handleLaneCollapsed(laneKey),
2314
+ tooltip: isLaneCollapsed ? `Expand ${laneLabelText}` : `Collapse ${laneLabelText}`
2120
2315
  },
2121
- visibleRows.map((row) => renderCardNode(row, stage))
2122
- ));
2123
- }));
2316
+ /* @__PURE__ */ React7.createElement(
2317
+ Icon,
2318
+ {
2319
+ name: isLaneCollapsed ? "right" : "down",
2320
+ size: "sm",
2321
+ screenReaderText: isLaneCollapsed ? `Expand ${laneLabelText}` : `Collapse ${laneLabelText}`
2322
+ }
2323
+ )
2324
+ ) : null, /* @__PURE__ */ React7.createElement(Text, { format: { fontWeight: "demibold" } }, laneLabel), laneCountNode), laneMetricsNode, !isLaneCollapsed ? renderStageColumns((laneBuckets == null ? void 0 : laneBuckets[laneKey]) || {}, laneKey) : null);
2325
+ })) : renderStageColumns(sortedBuckets, null);
2124
2326
  const resolvedShowClearFiltersButton = showClearFiltersButton ?? showFilterBadges;
2125
2327
  return /* @__PURE__ */ React7.createElement(Flex4, { direction: "column", gap: "sm" }, /* @__PURE__ */ React7.createElement(
2126
2328
  KanbanToolbar,
@@ -2140,8 +2342,8 @@ var Kanban = ({
2140
2342
  sortOptions,
2141
2343
  sortValue: resolvedSort,
2142
2344
  onSortChange: handleSort,
2143
- metrics,
2144
- showMetrics: resolvedShowMetrics,
2345
+ showMetricsButton: metricsProvided,
2346
+ metricsPanel: resolvedShowMetrics && globalMetricsContent ? renderMetricsPanel(globalMetricsContent) : null,
2145
2347
  onToggleMetrics: toggleMetrics,
2146
2348
  labels,
2147
2349
  toolbarLeftFlex,
@@ -2149,6 +2351,7 @@ var Kanban = ({
2149
2351
  }
2150
2352
  ), showSelectionBar && selectable && selectedCount > 0 ? renderSelectionBar ? renderSelectionBar(selectionBarProps) : /* @__PURE__ */ React7.createElement(DefaultSelectionBar, { ...selectionBarProps }) : null, mainContent);
2151
2353
  };
2354
+ Kanban.displayName = "Kanban";
2152
2355
 
2153
2356
  // src/kanban/KanbanCardActions.jsx
2154
2357
  import React8 from "react";
@@ -2215,5 +2418,14 @@ var KanbanCardActions = ({
2215
2418
  };
2216
2419
  export {
2217
2420
  Kanban,
2218
- KanbanCardActions
2421
+ KanbanCardActions,
2422
+ UNASSIGNED_LANE_KEY,
2423
+ computeStageCounts,
2424
+ evaluateWip,
2425
+ findNewlyExceededWip,
2426
+ getLaneKey,
2427
+ orderLaneKeys,
2428
+ partitionLanes,
2429
+ resolveLaneLabel,
2430
+ resolveWipLimit
2219
2431
  };