hs-uix 1.6.4 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/utils.mjs CHANGED
@@ -4,6 +4,2938 @@ var sumBy = (items, keyOrFn) => (items || []).reduce((total, item) => {
4
4
  return total + Number(value || 0);
5
5
  }, 0);
6
6
 
7
+ // src/utils/crmSearchAdapters.js
8
+ import React4, { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
9
+ import { useCrmSearch, Flex as Flex3, Text as Text3 } from "@hubspot/ui-extensions";
10
+
11
+ // packages/datatable/src/DataTable.jsx
12
+ import React, { useState, useMemo, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
13
+
14
+ // src/utils/query.js
15
+ import Fuse from "fuse.js";
16
+ var getEmptyFilterValue = (filter) => {
17
+ const type = filter.type || "select";
18
+ if (type === "multiselect") return [];
19
+ if (type === "dateRange") return { from: null, to: null };
20
+ return "";
21
+ };
22
+ var isFilterActive = (filter, value) => {
23
+ const type = filter.type || "select";
24
+ if (type === "multiselect") return Array.isArray(value) && value.length > 0;
25
+ if (type === "dateRange") return value && (value.from || value.to);
26
+ return !!value;
27
+ };
28
+ var formatDateChip = (dateObj) => {
29
+ if (!dateObj) return "";
30
+ const { year, month, date } = dateObj;
31
+ return new Intl.DateTimeFormat("en-US", {
32
+ month: "short",
33
+ day: "numeric",
34
+ year: "numeric"
35
+ }).format(new Date(year, month, date));
36
+ };
37
+ var dateToTimestamp = (dateObj) => {
38
+ if (!dateObj) return null;
39
+ return new Date(dateObj.year, dateObj.month, dateObj.date).getTime();
40
+ };
41
+ var toStableKey = (value) => {
42
+ try {
43
+ return JSON.stringify(value);
44
+ } catch (_error) {
45
+ return String(value);
46
+ }
47
+ };
48
+ var filterRows = (rows, filters, values) => {
49
+ let result = rows;
50
+ for (const filter of filters || []) {
51
+ const value = values[filter.name];
52
+ if (!isFilterActive(filter, value)) continue;
53
+ const type = filter.type || "select";
54
+ if (filter.filterFn) {
55
+ result = result.filter((row) => filter.filterFn(row, value));
56
+ } else if (type === "multiselect") {
57
+ result = result.filter((row) => value.includes(row[filter.name]));
58
+ } else if (type === "dateRange") {
59
+ const fromTs = dateToTimestamp(value.from);
60
+ const toTs = value.to ? dateToTimestamp(value.to) + 864e5 - 1 : null;
61
+ result = result.filter((row) => {
62
+ const rowTs = new Date(row[filter.name]).getTime();
63
+ if (Number.isNaN(rowTs)) return false;
64
+ if (fromTs && rowTs < fromTs) return false;
65
+ if (toTs && rowTs > toTs) return false;
66
+ return true;
67
+ });
68
+ } else {
69
+ result = result.filter((row) => row[filter.name] === value);
70
+ }
71
+ }
72
+ return result;
73
+ };
74
+ var searchRows = (rows, term, fields, opts = {}) => {
75
+ const { fuzzy = false, fuzzyOptions } = opts;
76
+ const t = String(term ?? "").toLowerCase();
77
+ if (!t || !fields || fields.length === 0) return rows;
78
+ if (fuzzy) {
79
+ const fuse = new Fuse(rows, {
80
+ keys: fields,
81
+ threshold: 0.4,
82
+ distance: 100,
83
+ ignoreLocation: true,
84
+ ...fuzzyOptions
85
+ });
86
+ return fuse.search(t).map((r) => r.item);
87
+ }
88
+ return rows.filter(
89
+ (row) => fields.some((field) => {
90
+ const val = row[field];
91
+ return val && String(val).toLowerCase().includes(t);
92
+ })
93
+ );
94
+ };
95
+
96
+ // src/utils/interactionHooks.js
97
+ import { useRef, useEffect, useCallback } from "react";
98
+ import { useDebounce } from "@hubspot/ui-extensions";
99
+ var useDebouncedDispatch = (value, debounceMs, dispatch) => {
100
+ const debounced = useDebounce(value, debounceMs > 0 ? debounceMs : 300);
101
+ const pendingRef = useRef(null);
102
+ useEffect(() => {
103
+ if (debounceMs <= 0) return;
104
+ if (pendingRef.current == null) return;
105
+ if (debounced !== pendingRef.current) return;
106
+ const next = pendingRef.current;
107
+ pendingRef.current = null;
108
+ dispatch(next);
109
+ }, [debounceMs, debounced, dispatch]);
110
+ return useCallback(
111
+ (next) => {
112
+ if (debounceMs > 0) {
113
+ pendingRef.current = next;
114
+ } else {
115
+ pendingRef.current = null;
116
+ dispatch(next);
117
+ }
118
+ },
119
+ [debounceMs, dispatch]
120
+ );
121
+ };
122
+ var useSelectionReset = ({ resetKey, enabled, isControlled, clearSelection }) => {
123
+ const ref = useRef("");
124
+ useEffect(() => {
125
+ if (!enabled || isControlled) {
126
+ ref.current = resetKey;
127
+ return;
128
+ }
129
+ if (ref.current && ref.current !== resetKey) {
130
+ clearSelection();
131
+ }
132
+ ref.current = resetKey;
133
+ }, [resetKey, enabled, isControlled, clearSelection]);
134
+ };
135
+
136
+ // packages/datatable/src/editValidation.js
137
+ var editValidationError = (result) => {
138
+ if (result === true || result === void 0 || result === null) return null;
139
+ return typeof result === "string" ? result : "Invalid value";
140
+ };
141
+
142
+ // packages/datatable/src/DataTable.jsx
143
+ import {
144
+ Box,
145
+ Button,
146
+ Checkbox,
147
+ CurrencyInput,
148
+ DateInput,
149
+ EmptyState,
150
+ ErrorState,
151
+ Flex,
152
+ Icon,
153
+ Input,
154
+ Link,
155
+ MultiSelect,
156
+ NumberInput,
157
+ SearchInput,
158
+ Select,
159
+ StepperInput,
160
+ Table,
161
+ TableBody,
162
+ TableCell,
163
+ TableFooter,
164
+ TableHead,
165
+ TableHeader,
166
+ TableRow,
167
+ Tag,
168
+ Tile,
169
+ Text,
170
+ TextArea,
171
+ TimeInput,
172
+ Toggle,
173
+ Tooltip
174
+ } from "@hubspot/ui-extensions";
175
+ var NARROW_EDIT_TYPES = /* @__PURE__ */ new Set(["checkbox", "toggle"]);
176
+ var DATE_PATTERN = /^\d{4}[-/]\d{2}[-/]\d{2}/;
177
+ var BOOL_VALUES = /* @__PURE__ */ new Set(["true", "false", "yes", "no", "0", "1"]);
178
+ var SORT_DIRECTIONS = /* @__PURE__ */ new Set(["ascending", "descending", "none"]);
179
+ var normalizeSortState = (columns, sort) => {
180
+ const normalized = {};
181
+ columns.forEach((col) => {
182
+ if (col.sortable) normalized[col.field] = "none";
183
+ });
184
+ if (!sort) return normalized;
185
+ if (sort.field && SORT_DIRECTIONS.has(sort.direction) && sort.field in normalized) {
186
+ normalized[sort.field] = sort.direction;
187
+ return normalized;
188
+ }
189
+ Object.keys(normalized).forEach((field) => {
190
+ const direction = sort[field];
191
+ if (SORT_DIRECTIONS.has(direction)) normalized[field] = direction;
192
+ });
193
+ return normalized;
194
+ };
195
+ var serializeSortState = (sortState) => {
196
+ const activeField = Object.keys(sortState).find((field) => sortState[field] !== "none");
197
+ if (!activeField) return null;
198
+ return { field: activeField, direction: sortState[activeField] };
199
+ };
200
+ var computeAutoWidths = (columns, data) => {
201
+ if (!data || data.length === 0) return {};
202
+ const sample = data.slice(0, 50);
203
+ const results = {};
204
+ columns.forEach((col) => {
205
+ if (col.width && col.cellWidth) return;
206
+ const values = sample.map((row) => row[col.field]).filter((v) => v != null);
207
+ const strings = values.map((v) => {
208
+ const s = String(v);
209
+ const truncLen = typeof col.truncate === "number" ? col.truncate : col.truncate && typeof col.truncate === "object" ? col.truncate.maxLength : null;
210
+ return truncLen && s.length > truncLen ? s.slice(0, truncLen) : s;
211
+ });
212
+ let widthHint = null;
213
+ let cellWidthHint = null;
214
+ if (col.editable && col.editType && NARROW_EDIT_TYPES.has(col.editType)) {
215
+ cellWidthHint = "min";
216
+ }
217
+ if (col.truncate === true) {
218
+ cellWidthHint = cellWidthHint || "min";
219
+ }
220
+ if (strings.length > 0) {
221
+ const lengths = strings.map((s) => s.length);
222
+ const maxLen = Math.max(...lengths);
223
+ const uniqueCount = new Set(strings).size;
224
+ if (values.every((v) => typeof v === "boolean") || strings.every((s) => BOOL_VALUES.has(s.toLowerCase()))) {
225
+ widthHint = widthHint || "min";
226
+ cellWidthHint = cellWidthHint || "min";
227
+ } else if (strings.every((s) => DATE_PATTERN.test(s))) {
228
+ widthHint = widthHint || "min";
229
+ cellWidthHint = cellWidthHint || "auto";
230
+ } else if (values.every((v) => typeof v === "number")) {
231
+ widthHint = widthHint || "auto";
232
+ cellWidthHint = cellWidthHint || "auto";
233
+ } else if (uniqueCount <= 5 && maxLen <= 15) {
234
+ widthHint = widthHint || "min";
235
+ cellWidthHint = cellWidthHint || "auto";
236
+ } else {
237
+ widthHint = widthHint || "auto";
238
+ cellWidthHint = cellWidthHint || "auto";
239
+ }
240
+ }
241
+ if (col.editable && !NARROW_EDIT_TYPES.has(col.editType) && widthHint === "min") {
242
+ widthHint = "auto";
243
+ }
244
+ results[col.field] = {
245
+ width: widthHint || "auto",
246
+ cellWidth: cellWidthHint || "auto"
247
+ };
248
+ });
249
+ return results;
250
+ };
251
+ var BOOLEAN_SELECT_OPTIONS = [
252
+ { label: "Yes", value: true },
253
+ { label: "No", value: false }
254
+ ];
255
+ var resolveEditOptions = (col, data) => {
256
+ if (col.editOptions && col.editOptions.length > 0) return col.editOptions;
257
+ const sample = data.find((row) => row[col.field] != null);
258
+ if (sample && typeof sample[col.field] === "boolean") return BOOLEAN_SELECT_OPTIONS;
259
+ return [];
260
+ };
261
+ var DataTable = ({
262
+ // Data
263
+ data,
264
+ columns,
265
+ renderRow,
266
+ // Search
267
+ searchFields = [],
268
+ searchPlaceholder = "Search...",
269
+ fuzzySearch = false,
270
+ // enable fuzzy matching via Fuse.js
271
+ fuzzyOptions,
272
+ // custom Fuse.js options (threshold, distance, etc.)
273
+ showSearch = true,
274
+ // show the SearchInput in the toolbar
275
+ // Filters
276
+ filters = [],
277
+ showFilterBadges = true,
278
+ // show active filter chips/badges
279
+ showClearFiltersButton,
280
+ // show "Clear all" reset button; defaults to showFilterBadges when omitted
281
+ filterInlineLimit = 2,
282
+ // number of filters shown inline before overflow
283
+ // Pagination
284
+ pageSize = 10,
285
+ maxVisiblePageButtons,
286
+ // max page number buttons to show
287
+ showButtonLabels = true,
288
+ // show First/Prev/Next/Last text labels
289
+ showFirstLastButtons,
290
+ // show First/Last page buttons (default: auto when pageCount > 5)
291
+ // Row count
292
+ title,
293
+ // optional title shown as demibold text above the table toolbar
294
+ showRowCount = true,
295
+ // show "X records" / "X of Y records" text
296
+ rowCountBold = false,
297
+ // bold the row count text
298
+ rowCountText,
299
+ // custom formatter: (shownOnPage, totalMatching) => string
300
+ // Table appearance
301
+ bordered = true,
302
+ // show table borders
303
+ flush = true,
304
+ // remove bottom margin
305
+ scrollable = false,
306
+ // allow horizontal overflow with scrollbar
307
+ // Sorting
308
+ defaultSort = {},
309
+ // Grouping
310
+ groupBy,
311
+ // Footer
312
+ footer,
313
+ // Empty state
314
+ emptyTitle,
315
+ emptyMessage,
316
+ // -----------------------------------------------------------------------
317
+ // Server-side mode
318
+ // -----------------------------------------------------------------------
319
+ serverSide = false,
320
+ loading = false,
321
+ // show loading spinner over the table
322
+ error,
323
+ // error message string or boolean — shows ErrorState
324
+ totalCount,
325
+ // server total (server-side only)
326
+ page: externalPage,
327
+ // controlled page (server-side only)
328
+ searchValue,
329
+ // controlled search term (server-side only)
330
+ filterValues: externalFilterValues,
331
+ // controlled filter values (server-side only)
332
+ sort: externalSort,
333
+ // controlled sort state, e.g. { field: "ascending" }
334
+ searchDebounce = 0,
335
+ // ms to debounce onSearchChange callback
336
+ resetPageOnChange = true,
337
+ // auto-reset to page 1 on search/filter/sort change
338
+ onSearchChange,
339
+ // (searchTerm) => void
340
+ onFilterChange,
341
+ // (filterValues) => void
342
+ onSortChange,
343
+ // (field, direction) => void
344
+ onPageChange,
345
+ // (page) => void
346
+ onParamsChange,
347
+ // ({ search, filters, sort, page }) => void
348
+ // -----------------------------------------------------------------------
349
+ // Row selection
350
+ // -----------------------------------------------------------------------
351
+ selectable = false,
352
+ rowIdField = "id",
353
+ // field name used as unique row identifier
354
+ selectedIds: externalSelectedIds,
355
+ // controlled selection — array of row IDs
356
+ onSelectionChange,
357
+ // (selectedIds[]) => void
358
+ onSelectAllRequest,
359
+ // server-side: ({ selectedIds, pageIds, totalCount }) => void
360
+ selectionActions = [],
361
+ // [{ label, onClick(selectedIds[]), icon?, variant? }]
362
+ selectionResetKey,
363
+ // optional key to force clear uncontrolled selection memory
364
+ resetSelectionOnQueryChange = true,
365
+ // clear uncontrolled selection on search/filter/sort changes
366
+ showSelectionBar = true,
367
+ // show the selection action bar when rows are selected
368
+ recordLabel,
369
+ // { singular: "Contact", plural: "Contacts" } — defaults to Record/Records
370
+ // -----------------------------------------------------------------------
371
+ // Row actions
372
+ // -----------------------------------------------------------------------
373
+ rowActions,
374
+ // [{ label, onClick(row), icon?, variant? }] or (row) => actions[]
375
+ hideRowActionsWhenSelectionActive = false,
376
+ // hide row action column while selected-row action bar is visible
377
+ // -----------------------------------------------------------------------
378
+ // Inline editing
379
+ // -----------------------------------------------------------------------
380
+ editMode,
381
+ // "discrete" (click-to-edit) | "inline" (always show inputs)
382
+ editingRowId,
383
+ // controlled — row ID currently in full-row edit mode
384
+ onRowEdit,
385
+ // (row, field, newValue) => void
386
+ onRowEditInput,
387
+ // optional live-input callback: (row, field, inputValue) => void
388
+ onEditStart,
389
+ // (row, field, currentValue) => void — fires when editing begins
390
+ onEditCancel,
391
+ // (row, field) => void — fires when editing is cancelled without commit
392
+ // -----------------------------------------------------------------------
393
+ // Auto-width
394
+ // -----------------------------------------------------------------------
395
+ autoWidth = true,
396
+ // auto-compute column widths from content analysis
397
+ // -----------------------------------------------------------------------
398
+ // Labels / i18n
399
+ // -----------------------------------------------------------------------
400
+ labels,
401
+ // override hardcoded UI strings for i18n
402
+ // -----------------------------------------------------------------------
403
+ // Render overrides (Phase 3 — full replacement escape hatches)
404
+ // -----------------------------------------------------------------------
405
+ renderSelectionBar,
406
+ // ({ selectedIds, selectedCount, displayCount, countLabel, onSelectAll, onDeselectAll, selectionActions }) => ReactNode
407
+ renderEmptyState,
408
+ // ({ title, message }) => ReactNode
409
+ renderLoadingState,
410
+ // ({ label }) => ReactNode
411
+ renderErrorState
412
+ // ({ error, title, message }) => ReactNode
413
+ }) => {
414
+ const initialSortState = useMemo(() => {
415
+ return normalizeSortState(columns, defaultSort);
416
+ }, [columns, defaultSort]);
417
+ const [internalSearchTerm, setInternalSearchTerm] = useState(
418
+ () => serverSide && searchValue != null ? searchValue : ""
419
+ );
420
+ const [internalFilterValues, setInternalFilterValues] = useState(() => {
421
+ const init = {};
422
+ filters.forEach((f) => {
423
+ init[f.name] = getEmptyFilterValue(f);
424
+ });
425
+ return init;
426
+ });
427
+ const [internalSortState, setInternalSortState] = useState(initialSortState);
428
+ const [currentPage, setCurrentPage] = useState(1);
429
+ const [showMoreFilters, setShowMoreFilters] = useState(false);
430
+ const lastAppliedSearchRef = useRef2(
431
+ serverSide && searchValue != null ? searchValue : ""
432
+ );
433
+ const searchTerm = serverSide && searchValue != null ? searchValue : internalSearchTerm;
434
+ useEffect2(() => {
435
+ if (!serverSide || searchValue == null) return;
436
+ if (searchValue === lastAppliedSearchRef.current) return;
437
+ lastAppliedSearchRef.current = searchValue;
438
+ setInternalSearchTerm(searchValue);
439
+ }, [serverSide, searchValue]);
440
+ const filterValues = serverSide && externalFilterValues != null ? externalFilterValues : internalFilterValues;
441
+ const externalSortState = useMemo(
442
+ () => normalizeSortState(columns, externalSort),
443
+ [columns, externalSort]
444
+ );
445
+ const sortState = serverSide && externalSort != null ? externalSortState : internalSortState;
446
+ const activePage = serverSide && externalPage != null ? externalPage : currentPage;
447
+ useEffect2(() => {
448
+ if (!serverSide) setCurrentPage(1);
449
+ }, [internalSearchTerm, internalFilterValues, internalSortState, serverSide]);
450
+ const fireSearchCallback = useCallback2((term) => {
451
+ if (serverSide && onSearchChange) onSearchChange(term);
452
+ }, [serverSide, onSearchChange]);
453
+ const fireParamsChange = useCallback2((overrides) => {
454
+ if (!onParamsChange) return;
455
+ const nextSortState = overrides.sort != null ? normalizeSortState(columns, overrides.sort) : sortState;
456
+ onParamsChange({
457
+ search: overrides.search != null ? overrides.search : searchTerm,
458
+ filters: overrides.filters != null ? overrides.filters : filterValues,
459
+ sort: serializeSortState(nextSortState),
460
+ page: overrides.page != null ? overrides.page : activePage
461
+ });
462
+ }, [onParamsChange, columns, searchTerm, filterValues, sortState, activePage]);
463
+ const resetPage = useCallback2(() => {
464
+ if (resetPageOnChange) {
465
+ setCurrentPage(1);
466
+ if (serverSide && onPageChange) onPageChange(1);
467
+ }
468
+ }, [resetPageOnChange, serverSide, onPageChange]);
469
+ const dispatchSearchChange = useCallback2((term) => {
470
+ lastAppliedSearchRef.current = term;
471
+ fireSearchCallback(term);
472
+ fireParamsChange({ search: term, page: resetPageOnChange ? 1 : void 0 });
473
+ }, [fireSearchCallback, fireParamsChange, resetPageOnChange]);
474
+ const dispatchSearchDebounced = useDebouncedDispatch(
475
+ internalSearchTerm,
476
+ searchDebounce,
477
+ dispatchSearchChange
478
+ );
479
+ const handleSearchChange = useCallback2((term) => {
480
+ setInternalSearchTerm(term);
481
+ resetPage();
482
+ dispatchSearchDebounced(term);
483
+ }, [dispatchSearchDebounced, resetPage]);
484
+ const handleFilterChange = useCallback2((name, value) => {
485
+ const next = { ...filterValues, [name]: value };
486
+ setInternalFilterValues(next);
487
+ if (serverSide && onFilterChange) onFilterChange(next);
488
+ resetPage();
489
+ fireParamsChange({ filters: next, page: resetPageOnChange ? 1 : void 0 });
490
+ }, [filterValues, serverSide, onFilterChange, fireParamsChange, resetPage, resetPageOnChange]);
491
+ const handleSortChange = useCallback2((field) => {
492
+ const current = sortState[field] || "none";
493
+ const nextDirection = current === "none" ? "ascending" : current === "ascending" ? "descending" : "none";
494
+ const reset = {};
495
+ columns.forEach((col) => {
496
+ if (col.sortable) reset[col.field] = "none";
497
+ });
498
+ const next = nextDirection === "none" ? reset : { ...reset, [field]: nextDirection };
499
+ setInternalSortState(next);
500
+ if (serverSide && onSortChange) onSortChange(field, nextDirection);
501
+ resetPage();
502
+ fireParamsChange({ sort: next, page: resetPageOnChange ? 1 : void 0 });
503
+ }, [sortState, columns, serverSide, onSortChange, fireParamsChange, resetPage, resetPageOnChange]);
504
+ const handlePageChange = useCallback2((page) => {
505
+ setCurrentPage(page);
506
+ if (serverSide && onPageChange) onPageChange(page);
507
+ fireParamsChange({ page });
508
+ }, [serverSide, onPageChange, fireParamsChange]);
509
+ const filteredData = useMemo(() => {
510
+ if (serverSide) return data;
511
+ let result = filterRows(data, filters, filterValues);
512
+ if (searchTerm && searchFields.length > 0) {
513
+ result = searchRows(result, searchTerm, searchFields, {
514
+ fuzzy: fuzzySearch,
515
+ fuzzyOptions
516
+ });
517
+ }
518
+ return result;
519
+ }, [data, filterValues, searchTerm, filters, searchFields, serverSide, fuzzySearch, fuzzyOptions]);
520
+ const sortedData = useMemo(() => {
521
+ if (serverSide) return filteredData;
522
+ const activeField = Object.keys(sortState).find((k) => sortState[k] !== "none");
523
+ if (!activeField) return filteredData;
524
+ const activeCol = columns.find((c) => c.field === activeField);
525
+ const sortOrder = Array.isArray(activeCol == null ? void 0 : activeCol.sortOrder) ? activeCol.sortOrder : null;
526
+ const sortOrderIndex = (val) => {
527
+ const idx = sortOrder.indexOf(val);
528
+ return idx === -1 ? sortOrder.length : idx;
529
+ };
530
+ return [...filteredData].sort((a, b) => {
531
+ const dir = sortState[activeField] === "ascending" ? 1 : -1;
532
+ const aVal = a[activeField];
533
+ const bVal = b[activeField];
534
+ if (typeof (activeCol == null ? void 0 : activeCol.sortComparator) === "function") {
535
+ return dir * activeCol.sortComparator(aVal, bVal, a, b);
536
+ }
537
+ if (sortOrder) {
538
+ const diff = sortOrderIndex(aVal) - sortOrderIndex(bVal);
539
+ if (diff !== 0) return dir * diff;
540
+ }
541
+ if (aVal == null && bVal == null) return 0;
542
+ if (aVal == null) return 1;
543
+ if (bVal == null) return -1;
544
+ if (aVal < bVal) return -dir;
545
+ if (aVal > bVal) return dir;
546
+ return 0;
547
+ });
548
+ }, [filteredData, sortState, serverSide, columns]);
549
+ const groupedData = useMemo(() => {
550
+ if (!groupBy) return null;
551
+ const source = serverSide ? data : sortedData;
552
+ const groups = {};
553
+ source.forEach((row) => {
554
+ const key = row[groupBy.field] ?? "--";
555
+ if (!groups[key]) groups[key] = [];
556
+ groups[key].push(row);
557
+ });
558
+ let groupKeys = Object.keys(groups);
559
+ if (groupBy.sort) {
560
+ if (typeof groupBy.sort === "function") {
561
+ groupKeys.sort(groupBy.sort);
562
+ } else {
563
+ const dir = groupBy.sort === "desc" ? -1 : 1;
564
+ groupKeys.sort((a, b) => a < b ? -dir : a > b ? dir : 0);
565
+ }
566
+ }
567
+ return groupKeys.map((key) => ({
568
+ key,
569
+ label: groupBy.label ? groupBy.label(key, groups[key]) : key,
570
+ rows: groups[key]
571
+ }));
572
+ }, [sortedData, data, groupBy, serverSide]);
573
+ const [expandedGroups, setExpandedGroups] = useState(() => {
574
+ if (!groupBy) return /* @__PURE__ */ new Set();
575
+ const defaultExpanded = groupBy.defaultExpanded !== false;
576
+ if (defaultExpanded && groupedData) {
577
+ return new Set(groupedData.map((g) => g.key));
578
+ }
579
+ return /* @__PURE__ */ new Set();
580
+ });
581
+ useEffect2(() => {
582
+ if (!groupedData) return;
583
+ const defaultExpanded = (groupBy == null ? void 0 : groupBy.defaultExpanded) !== false;
584
+ if (defaultExpanded) {
585
+ setExpandedGroups((prev) => {
586
+ const next = new Set(prev);
587
+ groupedData.forEach((g) => next.add(g.key));
588
+ return next;
589
+ });
590
+ }
591
+ }, [groupedData, groupBy]);
592
+ const toggleGroup = useCallback2((key) => {
593
+ setExpandedGroups((prev) => {
594
+ const next = new Set(prev);
595
+ if (next.has(key)) next.delete(key);
596
+ else next.add(key);
597
+ return next;
598
+ });
599
+ }, []);
600
+ const datasetRows = useMemo(() => {
601
+ if (!groupedData) return serverSide ? data : sortedData;
602
+ return groupedData.flatMap((group) => group.rows);
603
+ }, [groupedData, sortedData, data, serverSide]);
604
+ const totalItems = serverSide ? totalCount || data.length : datasetRows.length;
605
+ const pageCount = Math.ceil(totalItems / pageSize);
606
+ const paginatedRows = useMemo(() => {
607
+ if (serverSide) return datasetRows;
608
+ return datasetRows.slice(
609
+ (activePage - 1) * pageSize,
610
+ activePage * pageSize
611
+ );
612
+ }, [serverSide, datasetRows, activePage, pageSize]);
613
+ const displayRows = useMemo(() => {
614
+ if (!groupedData) return paginatedRows.map((row) => ({ type: "data", row }));
615
+ const pageRows = new Set(paginatedRows);
616
+ const rows = [];
617
+ groupedData.forEach((group) => {
618
+ const groupPageRows = group.rows.filter((row) => pageRows.has(row));
619
+ if (groupPageRows.length === 0) return;
620
+ rows.push({ type: "group-header", group });
621
+ if (expandedGroups.has(group.key)) {
622
+ groupPageRows.forEach((row) => rows.push({ type: "data", row }));
623
+ }
624
+ });
625
+ return rows;
626
+ }, [groupedData, paginatedRows, expandedGroups]);
627
+ const footerData = serverSide ? data : filteredData;
628
+ const activeChips = useMemo(() => {
629
+ const chips = [];
630
+ filters.forEach((filter) => {
631
+ const value = filterValues[filter.name];
632
+ if (!isFilterActive(filter, value)) return;
633
+ const type = filter.type || "select";
634
+ const prefix = filter.chipLabel || filter.placeholder || filter.name;
635
+ if (type === "multiselect") {
636
+ const labels2 = value.map((v) => {
637
+ var _a;
638
+ return ((_a = filter.options.find((o) => o.value === v)) == null ? void 0 : _a.label) || v;
639
+ }).join(", ");
640
+ chips.push({ key: filter.name, label: `${prefix}: ${labels2}` });
641
+ } else if (type === "dateRange") {
642
+ const parts = [];
643
+ if (value.from) parts.push(`from ${formatDateChip(value.from)}`);
644
+ if (value.to) parts.push(`to ${formatDateChip(value.to)}`);
645
+ chips.push({ key: filter.name, label: `${prefix}: ${parts.join(" ")}` });
646
+ } else {
647
+ const option = filter.options.find((o) => o.value === value);
648
+ chips.push({ key: filter.name, label: `${prefix}: ${(option == null ? void 0 : option.label) || value}` });
649
+ }
650
+ });
651
+ return chips;
652
+ }, [filterValues, filters]);
653
+ const handleFilterRemove = useCallback2((key) => {
654
+ if (key === "all") {
655
+ const cleared = {};
656
+ filters.forEach((f) => {
657
+ cleared[f.name] = getEmptyFilterValue(f);
658
+ });
659
+ setInternalFilterValues(cleared);
660
+ if (serverSide && onFilterChange) onFilterChange(cleared);
661
+ resetPage();
662
+ fireParamsChange({ filters: cleared, page: resetPageOnChange ? 1 : void 0 });
663
+ } else {
664
+ const filter = filters.find((f) => f.name === key);
665
+ const emptyVal = filter ? getEmptyFilterValue(filter) : "";
666
+ const next = { ...filterValues, [key]: emptyVal };
667
+ setInternalFilterValues(next);
668
+ if (serverSide && onFilterChange) onFilterChange(next);
669
+ resetPage();
670
+ fireParamsChange({ filters: next, page: resetPageOnChange ? 1 : void 0 });
671
+ }
672
+ }, [filters, filterValues, serverSide, onFilterChange, resetPage, fireParamsChange, resetPageOnChange]);
673
+ const displayCount = serverSide ? totalCount || data.length : filteredData.length;
674
+ const totalDataCount = serverSide ? totalCount || data.length : data.length;
675
+ const shownOnPageCount = displayRows.filter((item) => item.type === "data").length;
676
+ const pluralLabel = ((recordLabel == null ? void 0 : recordLabel.plural) || "records").toLowerCase();
677
+ const singularLabel = ((recordLabel == null ? void 0 : recordLabel.singular) || "record").toLowerCase();
678
+ const countLabel = (n) => n === 1 ? singularLabel : pluralLabel;
679
+ const resolvedEmptyTitle = emptyTitle || "No results found";
680
+ const resolvedEmptyMessage = emptyMessage || `No ${pluralLabel} match your search or filter criteria.`;
681
+ const resolvedSelectedLabel = (labels == null ? void 0 : labels.selected) || ((count, label) => `${count}\xA0${label}\xA0selected`);
682
+ const resolvedSelectAllLabel = (labels == null ? void 0 : labels.selectAll) || ((count, label) => `Select all ${count} ${label}`);
683
+ const resolvedDeselectAllLabel = (labels == null ? void 0 : labels.deselectAll) || "Deselect all";
684
+ const resolvedFiltersButtonLabel = (labels == null ? void 0 : labels.filtersButton) || "Filters";
685
+ const resolvedClearAllLabel = (labels == null ? void 0 : labels.clearAll) || "Clear all";
686
+ const resolvedDateFromLabel = (labels == null ? void 0 : labels.dateFrom) || "From";
687
+ const resolvedDateToLabel = (labels == null ? void 0 : labels.dateTo) || "To";
688
+ const resolvedLoadingLabel = (labels == null ? void 0 : labels.loading) || `Loading ${pluralLabel}...`;
689
+ const resolvedLoadingMessage = (labels == null ? void 0 : labels.loadingMessage) || "This should only take a moment.";
690
+ const resolvedErrorTitle = (labels == null ? void 0 : labels.errorTitle) || "Something went wrong.";
691
+ const resolvedErrorMessage = (labels == null ? void 0 : labels.errorMessage) || "An error occurred while loading data.";
692
+ const resolvedRetryMessage = (labels == null ? void 0 : labels.retryMessage) || "Please try again.";
693
+ const resolvedShowClearFiltersButton = showClearFiltersButton ?? showFilterBadges;
694
+ const recordCountLabel = rowCountText ? rowCountText(shownOnPageCount, displayCount) : displayCount === totalDataCount ? `${totalDataCount} ${countLabel(totalDataCount)}` : `${displayCount} of ${totalDataCount} ${countLabel(totalDataCount)}`;
695
+ const [internalSelectedIds, setInternalSelectedIds] = useState(/* @__PURE__ */ new Set());
696
+ useEffect2(() => {
697
+ if (externalSelectedIds != null) {
698
+ setInternalSelectedIds(new Set(externalSelectedIds));
699
+ }
700
+ }, [externalSelectedIds]);
701
+ const selectionQueryKey = useMemo(() => {
702
+ if (!resetSelectionOnQueryChange) return "";
703
+ return toStableKey({
704
+ search: searchTerm,
705
+ filters: filterValues,
706
+ sort: serializeSortState(sortState)
707
+ });
708
+ }, [searchTerm, filterValues, sortState, resetSelectionOnQueryChange]);
709
+ const combinedSelectionResetKey = useMemo(
710
+ () => `${selectionQueryKey}::${selectionResetKey == null ? "" : toStableKey(selectionResetKey)}`,
711
+ [selectionQueryKey, selectionResetKey]
712
+ );
713
+ const clearSelection = useCallback2(() => setInternalSelectedIds(/* @__PURE__ */ new Set()), []);
714
+ useSelectionReset({
715
+ resetKey: combinedSelectionResetKey,
716
+ enabled: selectable,
717
+ isControlled: externalSelectedIds != null,
718
+ clearSelection
719
+ });
720
+ const selectedIds = externalSelectedIds != null ? new Set(externalSelectedIds) : internalSelectedIds;
721
+ const showToolbarCount = showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0);
722
+ const hasToolbarLeft = showSearch && searchFields.length > 0 || filters.length > 0 || activeChips.length > 0 && (showFilterBadges || resolvedShowClearFiltersButton);
723
+ const countInTitleRow = !!title && showToolbarCount && !hasToolbarLeft;
724
+ const countInToolbar = showToolbarCount && !countInTitleRow;
725
+ const hasToolbarContent = hasToolbarLeft || countInToolbar;
726
+ const showRowActionsColumn = !!rowActions && !(hideRowActionsWhenSelectionActive && selectable && selectedIds.size > 0);
727
+ const applySelection = useCallback2((nextSet) => {
728
+ if (externalSelectedIds == null) {
729
+ setInternalSelectedIds(nextSet);
730
+ }
731
+ if (onSelectionChange) onSelectionChange([...nextSet]);
732
+ }, [externalSelectedIds, onSelectionChange]);
733
+ const pageRowIds = useMemo(() => {
734
+ if (serverSide) {
735
+ return data.map((row) => row[rowIdField]).filter((id) => id != null);
736
+ }
737
+ return displayRows.filter((r) => r.type === "data").map((r) => r.row[rowIdField]).filter((id) => id != null);
738
+ }, [serverSide, data, displayRows, rowIdField]);
739
+ const allRowIds = useMemo(
740
+ () => datasetRows.map((row) => row[rowIdField]).filter((id) => id != null),
741
+ [datasetRows, rowIdField]
742
+ );
743
+ const handleSelectRow = useCallback2((rowId, checked) => {
744
+ const next = new Set(selectedIds);
745
+ if (checked) next.add(rowId);
746
+ else next.delete(rowId);
747
+ applySelection(next);
748
+ }, [selectedIds, applySelection]);
749
+ const handleSelectAll = useCallback2((checked) => {
750
+ const next = new Set(selectedIds);
751
+ pageRowIds.forEach((id) => {
752
+ if (checked) next.add(id);
753
+ else next.delete(id);
754
+ });
755
+ applySelection(next);
756
+ }, [selectedIds, pageRowIds, applySelection]);
757
+ const allVisibleSelected = useMemo(() => {
758
+ return pageRowIds.length > 0 && pageRowIds.every((id) => selectedIds.has(id));
759
+ }, [pageRowIds, selectedIds]);
760
+ const handleSelectAllRows = useCallback2(() => {
761
+ const idsToAdd = serverSide ? pageRowIds : allRowIds;
762
+ const next = new Set(selectedIds);
763
+ idsToAdd.forEach((id) => next.add(id));
764
+ applySelection(next);
765
+ if (serverSide && onSelectAllRequest) {
766
+ onSelectAllRequest({
767
+ selectedIds: [...next],
768
+ pageIds: pageRowIds,
769
+ totalCount: totalCount || data.length
770
+ });
771
+ }
772
+ }, [serverSide, pageRowIds, allRowIds, selectedIds, applySelection, onSelectAllRequest, totalCount, data.length]);
773
+ const handleDeselectAll = useCallback2(() => {
774
+ applySelection(/* @__PURE__ */ new Set());
775
+ }, [applySelection]);
776
+ const [editingCell, setEditingCell] = useState(null);
777
+ const [editValue, setEditValue] = useState(null);
778
+ const [editError, setEditError] = useState(null);
779
+ const startEditing = useCallback2((rowId, field, currentValue) => {
780
+ setEditingCell({ rowId, field });
781
+ setEditValue(currentValue);
782
+ setEditError(null);
783
+ if (onEditStart) {
784
+ const row = data.find((r) => r[rowIdField] === rowId);
785
+ if (row) onEditStart(row, field, currentValue);
786
+ }
787
+ }, [onEditStart, data, rowIdField]);
788
+ const commitEdit = useCallback2((row, field, value, options = {}) => {
789
+ const { keepEditing = false } = options;
790
+ const col = columns.find((c) => c.field === field);
791
+ if (col == null ? void 0 : col.editValidate) {
792
+ const err = editValidationError(col.editValidate(value, row));
793
+ if (err) {
794
+ setEditError(err);
795
+ return false;
796
+ }
797
+ }
798
+ if (onRowEdit) onRowEdit(row, field, value);
799
+ if (!keepEditing) {
800
+ setEditingCell(null);
801
+ setEditValue(null);
802
+ } else {
803
+ setEditValue(value);
804
+ }
805
+ setEditError(null);
806
+ return true;
807
+ }, [onRowEdit, columns]);
808
+ const renderEditControl = (col, row) => {
809
+ const type = col.editType || "text";
810
+ const rowId = row[rowIdField];
811
+ const fieldName = `edit-${rowId}-${col.field}`;
812
+ const commit = (val) => commitEdit(row, col.field, val);
813
+ const exitEdit = () => {
814
+ if (editError) return;
815
+ if (onEditCancel) onEditCancel(row, col.field);
816
+ setEditingCell(null);
817
+ setEditValue(null);
818
+ };
819
+ const extra = col.editProps || {};
820
+ const validate = col.editValidate;
821
+ const validationProps = validate && editError ? { error: true, validationMessage: editError } : {};
822
+ const onInputValidate = validate ? (val) => setEditError(editValidationError(validate(val, row))) : void 0;
823
+ const handleInput = (val) => {
824
+ setEditValue(val);
825
+ if (onInputValidate) onInputValidate(val);
826
+ if (onRowEditInput) onRowEditInput(row, col.field, val);
827
+ };
828
+ const maybeExitDatetimeEdit = () => {
829
+ if (typeof document === "undefined") return;
830
+ setTimeout(() => {
831
+ var _a, _b;
832
+ const activeName = (_b = (_a = document.activeElement) == null ? void 0 : _a.getAttribute) == null ? void 0 : _b.call(_a, "name");
833
+ if (activeName !== `${fieldName}-date` && activeName !== `${fieldName}-time`) {
834
+ exitEdit();
835
+ }
836
+ }, 0);
837
+ };
838
+ switch (type) {
839
+ case "textarea":
840
+ return /* @__PURE__ */ React.createElement(TextArea, { ...extra, name: fieldName, label: "", value: editValue ?? "", onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
841
+ case "number":
842
+ return /* @__PURE__ */ React.createElement(NumberInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
843
+ case "currency":
844
+ return /* @__PURE__ */ React.createElement(CurrencyInput, { currencyCode: "USD", ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
845
+ case "stepper":
846
+ return /* @__PURE__ */ React.createElement(StepperInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
847
+ case "select":
848
+ return /* @__PURE__ */ React.createElement(Select, { variant: "transparent", ...extra, name: fieldName, label: "", value: editValue, onChange: commit, options: resolveEditOptions(col, data) });
849
+ case "multiselect":
850
+ return /* @__PURE__ */ React.createElement(MultiSelect, { ...extra, name: fieldName, label: "", value: editValue || [], onChange: commit, options: resolveEditOptions(col, data) });
851
+ case "date":
852
+ return /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit });
853
+ case "time":
854
+ return /* @__PURE__ */ React.createElement(TimeInput, { ...extra, name: fieldName, label: "", value: editValue, onChange: commit });
855
+ case "datetime":
856
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: `${fieldName}-date`, label: "", value: editValue == null ? void 0 : editValue.date, onChange: (val) => {
857
+ const next = { ...editValue, date: val };
858
+ handleInput(next);
859
+ commitEdit(row, col.field, next, { keepEditing: true });
860
+ }, onBlur: maybeExitDatetimeEdit }), /* @__PURE__ */ React.createElement(TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: "", value: editValue == null ? void 0 : editValue.time, onChange: (val) => {
861
+ const next = { ...editValue, time: val };
862
+ handleInput(next);
863
+ commitEdit(row, col.field, next, { keepEditing: true });
864
+ }, onBlur: maybeExitDatetimeEdit }));
865
+ case "toggle":
866
+ return /* @__PURE__ */ React.createElement(Toggle, { ...extra, name: fieldName, label: "", checked: !!editValue, onChange: commit });
867
+ case "checkbox":
868
+ return /* @__PURE__ */ React.createElement(Checkbox, { ...extra, name: fieldName, checked: !!editValue, onChange: commit });
869
+ default:
870
+ return /* @__PURE__ */ React.createElement(Input, { ...extra, name: fieldName, label: "", value: editValue ?? "", onChange: commit, onBlur: exitEdit, ...validationProps, onInput: handleInput });
871
+ }
872
+ };
873
+ const resolvedEditMode = editMode || (columns.some((col) => col.editable) ? "discrete" : null);
874
+ const useColumnRendering = selectable || !!resolvedEditMode || editingRowId != null || showRowActionsColumn || !renderRow;
875
+ const autoWidths = useMemo(
876
+ () => autoWidth ? computeAutoWidths(columns, data) : {},
877
+ [columns, data, autoWidth]
878
+ );
879
+ const defaultWidth = scrollable ? "min" : "auto";
880
+ const getHeaderWidth = (col) => {
881
+ var _a;
882
+ return col.width || ((_a = autoWidths[col.field]) == null ? void 0 : _a.width) || defaultWidth;
883
+ };
884
+ const getCellWidth = (col) => {
885
+ var _a;
886
+ return col.cellWidth || col.width || ((_a = autoWidths[col.field]) == null ? void 0 : _a.cellWidth) || defaultWidth;
887
+ };
888
+ const [inlineErrors, setInlineErrors] = useState({});
889
+ const renderInlineControl = (col, row) => {
890
+ const type = col.editType || "text";
891
+ const rowId = row[rowIdField];
892
+ const fieldName = `inline-${rowId}-${col.field}`;
893
+ const cellKey = `${rowId}-${col.field}`;
894
+ const value = row[col.field];
895
+ const validate = col.editValidate;
896
+ const fire = (val) => {
897
+ if (validate) {
898
+ const err = editValidationError(validate(val, row));
899
+ if (err) {
900
+ setInlineErrors((prev) => ({ ...prev, [cellKey]: err }));
901
+ return;
902
+ }
903
+ setInlineErrors((prev) => {
904
+ const next = { ...prev };
905
+ delete next[cellKey];
906
+ return next;
907
+ });
908
+ }
909
+ if (onRowEdit) onRowEdit(row, col.field, val);
910
+ };
911
+ const extra = col.editProps || {};
912
+ const cellError = inlineErrors[cellKey];
913
+ const validationProps = cellError ? { error: true, validationMessage: cellError } : {};
914
+ const onInputValidate = validate ? (val) => {
915
+ const err = editValidationError(validate(val, row));
916
+ setInlineErrors((prev) => {
917
+ if (err) return { ...prev, [cellKey]: err };
918
+ const next = { ...prev };
919
+ delete next[cellKey];
920
+ return next;
921
+ });
922
+ } : void 0;
923
+ const emitInput = (val) => {
924
+ if (onInputValidate) onInputValidate(val);
925
+ if (onRowEditInput) onRowEditInput(row, col.field, val);
926
+ };
927
+ switch (type) {
928
+ case "textarea":
929
+ return /* @__PURE__ */ React.createElement(TextArea, { ...extra, name: fieldName, label: "", value: value ?? "", onChange: fire, ...validationProps, onInput: emitInput });
930
+ case "number":
931
+ return /* @__PURE__ */ React.createElement(NumberInput, { ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
932
+ case "currency":
933
+ return /* @__PURE__ */ React.createElement(CurrencyInput, { currencyCode: "USD", ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
934
+ case "stepper":
935
+ return /* @__PURE__ */ React.createElement(StepperInput, { ...extra, name: fieldName, label: "", value, onChange: fire, ...validationProps, onInput: emitInput });
936
+ case "select":
937
+ return /* @__PURE__ */ React.createElement(Select, { ...extra, name: fieldName, label: "", value, onChange: fire, options: resolveEditOptions(col, data) });
938
+ case "multiselect":
939
+ return /* @__PURE__ */ React.createElement(MultiSelect, { ...extra, name: fieldName, label: "", value: value || [], onChange: fire, options: resolveEditOptions(col, data) });
940
+ case "date":
941
+ return /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: fieldName, label: "", value, onChange: fire });
942
+ case "time":
943
+ return /* @__PURE__ */ React.createElement(TimeInput, { ...extra, name: fieldName, label: "", value, onChange: fire });
944
+ case "datetime":
945
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(DateInput, { ...extra, name: `${fieldName}-date`, label: "", value: value == null ? void 0 : value.date, onChange: (val) => {
946
+ fire({ ...value, date: val });
947
+ } }), /* @__PURE__ */ React.createElement(TimeInput, { ...extra.timeProps || {}, name: `${fieldName}-time`, label: "", value: value == null ? void 0 : value.time, onChange: (val) => {
948
+ fire({ ...value, time: val });
949
+ } }));
950
+ case "toggle":
951
+ return /* @__PURE__ */ React.createElement(Toggle, { ...extra, name: fieldName, label: "", checked: !!value, onChange: fire });
952
+ case "checkbox":
953
+ return /* @__PURE__ */ React.createElement(Checkbox, { ...extra, name: fieldName, checked: !!value, onChange: fire });
954
+ default:
955
+ return /* @__PURE__ */ React.createElement(Input, { ...extra, name: fieldName, label: "", value: value ?? "", onChange: fire, ...validationProps, onInput: emitInput });
956
+ }
957
+ };
958
+ const renderCellContent = (row, col) => {
959
+ const rowId = row[rowIdField];
960
+ if (resolvedEditMode === "inline" && col.editable) {
961
+ return renderInlineControl(col, row);
962
+ }
963
+ if (editingRowId != null && rowId === editingRowId && col.editable) {
964
+ return renderInlineControl(col, row);
965
+ }
966
+ const isEditing = (editingCell == null ? void 0 : editingCell.rowId) === rowId && (editingCell == null ? void 0 : editingCell.field) === col.field;
967
+ if (isEditing && col.editable) return renderEditControl(col, row);
968
+ const rawValue = row[col.field];
969
+ const rawStr = String(rawValue ?? "");
970
+ if (col.truncate && rawStr.length > 0) {
971
+ if (col.truncate === true) {
972
+ if (col.renderCell) {
973
+ const content2 = col.renderCell(rawValue, row);
974
+ if (col.editable) {
975
+ return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
976
+ }
977
+ return content2;
978
+ }
979
+ if (col.editable) {
980
+ return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, rawStr || "--"));
981
+ }
982
+ return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, rawStr);
983
+ }
984
+ const maxLen = typeof col.truncate === "number" ? col.truncate : col.truncate.maxLength || 100;
985
+ if (rawStr.length > maxLen) {
986
+ const truncatedStr = rawStr.slice(0, maxLen) + "\u2026";
987
+ const content2 = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
988
+ if (col.editable) {
989
+ return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
990
+ }
991
+ return col.renderCell ? content2 : /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, content2 || "--");
992
+ }
993
+ }
994
+ const content = col.renderCell ? col.renderCell(rawValue, row) : rawValue;
995
+ const isEmpty = content == null || content === "";
996
+ if (col.editable) {
997
+ return /* @__PURE__ */ React.createElement(
998
+ Link,
999
+ {
1000
+ variant: "dark",
1001
+ onClick: () => startEditing(rowId, col.field, rawValue)
1002
+ },
1003
+ isEmpty ? "--" : content
1004
+ );
1005
+ }
1006
+ return isEmpty ? "--" : content;
1007
+ };
1008
+ const renderFilterControl2 = (filter) => {
1009
+ const type = filter.type || "select";
1010
+ if (type === "multiselect") {
1011
+ return /* @__PURE__ */ React.createElement(
1012
+ MultiSelect,
1013
+ {
1014
+ key: filter.name,
1015
+ name: `filter-${filter.name}`,
1016
+ label: "",
1017
+ placeholder: filter.placeholder || "All",
1018
+ value: filterValues[filter.name] || [],
1019
+ onChange: (val) => handleFilterChange(filter.name, val),
1020
+ options: filter.options
1021
+ }
1022
+ );
1023
+ }
1024
+ if (type === "dateRange") {
1025
+ const rangeVal = filterValues[filter.name] || { from: null, to: null };
1026
+ return /* @__PURE__ */ React.createElement(Flex, { key: filter.name, direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React.createElement(
1027
+ DateInput,
1028
+ {
1029
+ name: `filter-${filter.name}-from`,
1030
+ label: "",
1031
+ placeholder: resolvedDateFromLabel,
1032
+ format: "medium",
1033
+ value: rangeVal.from,
1034
+ onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, from: val })
1035
+ }
1036
+ ), /* @__PURE__ */ React.createElement(Icon, { name: "dataSync", size: "sm" }), /* @__PURE__ */ React.createElement(
1037
+ DateInput,
1038
+ {
1039
+ size: "sm",
1040
+ name: `filter-${filter.name}-to`,
1041
+ label: "",
1042
+ placeholder: resolvedDateToLabel,
1043
+ format: "medium",
1044
+ value: rangeVal.to,
1045
+ onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, to: val })
1046
+ }
1047
+ ));
1048
+ }
1049
+ return /* @__PURE__ */ React.createElement(
1050
+ Select,
1051
+ {
1052
+ key: filter.name,
1053
+ name: `filter-${filter.name}`,
1054
+ variant: "transparent",
1055
+ placeholder: filter.placeholder || "All",
1056
+ value: filterValues[filter.name],
1057
+ onChange: (val) => handleFilterChange(filter.name, val),
1058
+ options: [
1059
+ { label: filter.placeholder || "All", value: "" },
1060
+ ...filter.options
1061
+ ]
1062
+ }
1063
+ );
1064
+ };
1065
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, title && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", justify: "between", gap: "sm" }, /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, title), countInTitleRow && /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)), hasToolbarContent && /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ React.createElement(
1066
+ SearchInput,
1067
+ {
1068
+ name: "datatable-search",
1069
+ placeholder: searchPlaceholder,
1070
+ value: internalSearchTerm,
1071
+ onChange: handleSearchChange
1072
+ }
1073
+ ), filters.slice(0, filterInlineLimit).map(renderFilterControl2), filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(
1074
+ Button,
1075
+ {
1076
+ variant: "transparent",
1077
+ size: "small",
1078
+ onClick: () => setShowMoreFilters((prev) => !prev)
1079
+ },
1080
+ /* @__PURE__ */ React.createElement(Icon, { name: "filter", size: "sm" }),
1081
+ " ",
1082
+ resolvedFiltersButtonLabel
1083
+ )), showMoreFilters && filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, filters.slice(filterInlineLimit).map(renderFilterControl2)), activeChips.length > 0 && (showFilterBadges || resolvedShowClearFiltersButton) && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ React.createElement(Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), resolvedShowClearFiltersButton && /* @__PURE__ */ React.createElement(
1084
+ Button,
1085
+ {
1086
+ variant: "transparent",
1087
+ size: "extra-small",
1088
+ onClick: () => handleFilterRemove("all")
1089
+ },
1090
+ resolvedClearAllLabel
1091
+ )))), countInToolbar && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), showSelectionBar && selectable && selectedIds.size > 0 && (renderSelectionBar ? renderSelectionBar({
1092
+ selectedIds,
1093
+ selectedCount: selectedIds.size,
1094
+ displayCount,
1095
+ countLabel,
1096
+ allSelected: selectedIds.size >= (serverSide ? totalCount || data.length : allRowIds.length),
1097
+ onSelectAll: handleSelectAllRows,
1098
+ onDeselectAll: handleDeselectAll,
1099
+ selectionActions
1100
+ }) : /* @__PURE__ */ React.createElement(Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ React.createElement(Box, { flex: 3 }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(Text, { inline: true, format: { fontWeight: "demibold" } }, typeof resolvedSelectedLabel === "function" ? resolvedSelectedLabel(selectedIds.size, countLabel(selectedIds.size)) : resolvedSelectedLabel), selectedIds.size < (serverSide ? totalCount || data.length : allRowIds.length) && /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, typeof resolvedSelectAllLabel === "function" ? resolvedSelectAllLabel(displayCount, countLabel(displayCount)) : resolvedSelectAllLabel), /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, resolvedDeselectAllLabel), selectionActions.map((action, i) => /* @__PURE__ */ React.createElement(
1101
+ Button,
1102
+ {
1103
+ key: i,
1104
+ variant: action.variant || "transparent",
1105
+ size: "extra-small",
1106
+ onClick: () => action.onClick([...selectedIds])
1107
+ },
1108
+ action.icon && /* @__PURE__ */ React.createElement(Icon, { name: action.icon, size: "sm" }),
1109
+ " ",
1110
+ action.label
1111
+ )))), showRowCount && displayCount > 0 && /* @__PURE__ */ React.createElement(Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ React.createElement(Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel))))), loading ? renderLoadingState ? renderLoadingState({ label: resolvedLoadingLabel }) : (
1112
+ // Same EmptyState layout as the empty state, just the "building" image
1113
+ // + a loading message — so loading and empty match with no layout shift.
1114
+ /* @__PURE__ */ React.createElement(Tile, null, /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedLoadingLabel, imageName: "building", layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedLoadingMessage))))
1115
+ ) : error ? renderErrorState ? renderErrorState({
1116
+ error,
1117
+ title: typeof error === "string" ? error : resolvedErrorTitle,
1118
+ message: typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage
1119
+ }) : /* @__PURE__ */ React.createElement(ErrorState, { title: typeof error === "string" ? error : resolvedErrorTitle }, /* @__PURE__ */ React.createElement(Text, null, typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage)) : displayRows.length === 0 ? renderEmptyState ? renderEmptyState({ title: resolvedEmptyTitle, message: resolvedEmptyMessage }) : /* @__PURE__ */ React.createElement(Tile, null, /* @__PURE__ */ React.createElement(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedEmptyMessage)))) : /* @__PURE__ */ React.createElement(
1120
+ Table,
1121
+ {
1122
+ bordered,
1123
+ flush,
1124
+ paginated: pageCount > 1,
1125
+ page: activePage,
1126
+ pageCount,
1127
+ onPageChange: handlePageChange,
1128
+ showFirstLastButtons: showFirstLastButtons != null ? showFirstLastButtons : pageCount > 5,
1129
+ showButtonLabels,
1130
+ ...maxVisiblePageButtons != null ? { maxVisiblePageButtons } : {}
1131
+ },
1132
+ /* @__PURE__ */ React.createElement(TableHead, null, /* @__PURE__ */ React.createElement(TableRow, null, selectable && /* @__PURE__ */ React.createElement(TableHeader, { width: "min" }, /* @__PURE__ */ React.createElement(
1133
+ Checkbox,
1134
+ {
1135
+ name: "datatable-select-all",
1136
+ "aria-label": "Select all rows",
1137
+ checked: allVisibleSelected,
1138
+ onChange: handleSelectAll
1139
+ }
1140
+ )), columns.map((col) => {
1141
+ const headerAlign = resolvedEditMode === "inline" && col.editable ? void 0 : col.align;
1142
+ return /* @__PURE__ */ React.createElement(
1143
+ TableHeader,
1144
+ {
1145
+ key: col.field,
1146
+ width: getHeaderWidth(col),
1147
+ align: headerAlign,
1148
+ sortDirection: col.sortable ? sortState[col.field] || "none" : "never",
1149
+ onSortChange: col.sortable ? () => handleSortChange(col.field) : void 0
1150
+ },
1151
+ col.description ? /* @__PURE__ */ React.createElement(React.Fragment, null, col.label, "\xA0", /* @__PURE__ */ React.createElement(Link, { inline: true, variant: "dark", overlay: /* @__PURE__ */ React.createElement(Tooltip, null, col.description) }, /* @__PURE__ */ React.createElement(Icon, { name: "info", screenReaderText: typeof col.description === "string" ? col.description : void 0 }))) : col.label
1152
+ );
1153
+ }), showRowActionsColumn && /* @__PURE__ */ React.createElement(TableHeader, { width: "min" }))),
1154
+ /* @__PURE__ */ React.createElement(TableBody, null, displayRows.map(
1155
+ (item, idx) => item.type === "group-header" ? /* @__PURE__ */ React.createElement(TableRow, { key: `group-${item.group.key}` }, selectable && /* @__PURE__ */ React.createElement(TableCell, { width: "min" }), columns.map((col, colIdx) => {
1156
+ var _a, _b, _c;
1157
+ return /* @__PURE__ */ React.createElement(TableCell, { key: col.field, width: getCellWidth(col), align: colIdx === 0 ? void 0 : col.align }, colIdx === 0 ? /* @__PURE__ */ React.createElement(
1158
+ Link,
1159
+ {
1160
+ variant: "dark",
1161
+ onClick: () => toggleGroup(item.group.key)
1162
+ },
1163
+ /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, /* @__PURE__ */ React.createElement(Icon, { name: expandedGroups.has(item.group.key) ? "downCarat" : "right" }), /* @__PURE__ */ React.createElement(Text, { format: { fontWeight: "demibold" } }, item.group.label))
1164
+ ) : ((_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]) ?? "");
1165
+ }), showRowActionsColumn && /* @__PURE__ */ React.createElement(TableCell, { width: "min" })) : useColumnRendering ? /* @__PURE__ */ React.createElement(TableRow, { key: item.row[rowIdField] ?? idx }, selectable && /* @__PURE__ */ React.createElement(TableCell, { width: "min" }, /* @__PURE__ */ React.createElement(
1166
+ Checkbox,
1167
+ {
1168
+ name: `select-${item.row[rowIdField]}`,
1169
+ "aria-label": "Select row",
1170
+ checked: selectedIds.has(item.row[rowIdField]),
1171
+ onChange: (checked) => handleSelectRow(item.row[rowIdField], checked)
1172
+ }
1173
+ )), columns.map((col) => {
1174
+ const rowId = item.row[rowIdField];
1175
+ const isDiscreteEditing = resolvedEditMode === "discrete" && (editingCell == null ? void 0 : editingCell.rowId) === rowId && (editingCell == null ? void 0 : editingCell.field) === col.field;
1176
+ const isRowEditing = editingRowId != null && rowId === editingRowId && col.editable;
1177
+ const isShowingInput = isDiscreteEditing || isRowEditing || resolvedEditMode === "inline" && col.editable;
1178
+ const cellAlign = isShowingInput ? void 0 : col.align;
1179
+ return /* @__PURE__ */ React.createElement(TableCell, { key: col.field, width: isDiscreteEditing || isRowEditing ? "auto" : getCellWidth(col), align: cellAlign }, renderCellContent(item.row, col));
1180
+ }), showRowActionsColumn && /* @__PURE__ */ React.createElement(TableCell, { width: "min" }, /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "center", gap: "xs", wrap: "nowrap" }, (() => {
1181
+ const resolvedRowActions = typeof rowActions === "function" ? rowActions(item.row) : rowActions;
1182
+ const actions = Array.isArray(resolvedRowActions) ? resolvedRowActions : [];
1183
+ return actions.map((action, i) => /* @__PURE__ */ React.createElement(
1184
+ Button,
1185
+ {
1186
+ key: i,
1187
+ variant: action.variant || "transparent",
1188
+ size: "extra-small",
1189
+ onClick: () => action.onClick(item.row)
1190
+ },
1191
+ action.icon && /* @__PURE__ */ React.createElement(Icon, { name: action.icon, size: "sm" }),
1192
+ action.label && ` ${action.label}`
1193
+ ));
1194
+ })()))) : renderRow(item.row)
1195
+ )),
1196
+ (footer || columns.some((col) => col.footer)) && /* @__PURE__ */ React.createElement(TableFooter, null, typeof footer === "function" ? footer(footerData) : /* @__PURE__ */ React.createElement(TableRow, null, selectable && /* @__PURE__ */ React.createElement(TableHeader, { width: "min" }), columns.map((col) => {
1197
+ const footerDef = col.footer;
1198
+ const content = typeof footerDef === "function" ? footerDef(footerData) : footerDef || "";
1199
+ return /* @__PURE__ */ React.createElement(TableHeader, { key: col.field, align: col.align }, content);
1200
+ }), showRowActionsColumn && /* @__PURE__ */ React.createElement(TableHeader, { width: "min" })))
1201
+ ));
1202
+ };
1203
+
1204
+ // packages/kanban/src/Kanban.jsx
1205
+ import React3, { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
1206
+
1207
+ // src/common-components/StyledText.js
1208
+ import React2 from "react";
1209
+ import { Image, Tag as Tag2 } from "@hubspot/ui-extensions";
1210
+
1211
+ // src/common-components/svgDefaults.js
1212
+ var HS_FONT_FAMILY = '"Lexend Deca", Helvetica, Arial, sans-serif';
1213
+ var HS_TEXT_COLOR = "#33475b";
1214
+ var HS_SUBTLE_BG = "#F5F8FA";
1215
+ var HS_TAG_SUBTLE_BORDER = "#7C98B6";
1216
+ var HS_TAG_TEXT_COLOR = HS_TEXT_COLOR;
1217
+ var HS_TAG_FONT_SIZE = 12;
1218
+ var HS_TAG_LINE_HEIGHT = 22;
1219
+ var HS_TAG_PADDING_X = 8;
1220
+ var HS_TAG_PADDING_Y = 0;
1221
+ var HS_TAG_BORDER_RADIUS = 0;
1222
+ var HS_TAG_BORDER_WIDTH = 1;
1223
+
1224
+ // src/common-components/StyledText.js
1225
+ var VARIANT_PRESETS = {
1226
+ bodytext: { fontSize: 14, lineHeight: 24, fontWeight: 400 },
1227
+ microcopy: { fontSize: 12, lineHeight: 18, fontWeight: 400 }
1228
+ };
1229
+ var WEIGHT_ALIASES = {
1230
+ bold: 700,
1231
+ demibold: 600,
1232
+ regular: 400
1233
+ };
1234
+ var LINE_DECORATION = {
1235
+ strikethrough: "line-through",
1236
+ underline: "underline"
1237
+ };
1238
+ var ORIENTATION_ROTATION = {
1239
+ horizontal: 0,
1240
+ "vertical-up": -90,
1241
+ "vertical-down": 90
1242
+ };
1243
+ var BACKGROUND_PRESETS = {
1244
+ tag: {
1245
+ color: HS_SUBTLE_BG,
1246
+ borderColor: HS_TAG_SUBTLE_BORDER,
1247
+ borderWidth: HS_TAG_BORDER_WIDTH,
1248
+ radius: HS_TAG_BORDER_RADIUS,
1249
+ paddingX: HS_TAG_PADDING_X,
1250
+ paddingY: HS_TAG_PADDING_Y,
1251
+ height: HS_TAG_LINE_HEIGHT,
1252
+ textColor: HS_TAG_TEXT_COLOR,
1253
+ fontSize: HS_TAG_FONT_SIZE,
1254
+ canvasPaddingX: 0,
1255
+ canvasPaddingY: 0
1256
+ }
1257
+ };
1258
+ var TAG_VARIANTS = {
1259
+ default: {
1260
+ color: HS_SUBTLE_BG,
1261
+ borderColor: HS_TAG_SUBTLE_BORDER,
1262
+ textColor: HS_TAG_TEXT_COLOR
1263
+ },
1264
+ success: {
1265
+ color: "#E5F8F6",
1266
+ borderColor: "#00BDA5",
1267
+ textColor: "#00BDA5"
1268
+ },
1269
+ warning: {
1270
+ color: "#FEF8F0",
1271
+ borderColor: "#F5C26B",
1272
+ textColor: "#D39913"
1273
+ },
1274
+ error: {
1275
+ color: "#FDEDEE",
1276
+ borderColor: "#F2545B",
1277
+ textColor: "#F2545B"
1278
+ },
1279
+ danger: {
1280
+ color: "#FDEDEE",
1281
+ borderColor: "#F2545B",
1282
+ textColor: "#F2545B"
1283
+ },
1284
+ info: {
1285
+ color: "#E5F5F8",
1286
+ borderColor: "#00A4BD",
1287
+ textColor: "#00A4BD"
1288
+ }
1289
+ };
1290
+ var escapeSvgText = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1291
+ var applyTextTransform = (text, transform) => {
1292
+ if (!transform || transform === "none") return String(text);
1293
+ const s = String(text);
1294
+ switch (transform) {
1295
+ case "uppercase":
1296
+ return s.toUpperCase();
1297
+ case "lowercase":
1298
+ return s.toLowerCase();
1299
+ case "capitalize":
1300
+ return s.replace(/\b\w/g, (c) => c.toUpperCase());
1301
+ case "sentenceCase":
1302
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
1303
+ default:
1304
+ return s;
1305
+ }
1306
+ };
1307
+ var estimateTextWidth = (text, fontSize) => Math.max(fontSize, Math.round(String(text).length * fontSize * 0.58));
1308
+ var resolveBackground = (background) => {
1309
+ if (!background) return null;
1310
+ const preset = background.preset ? BACKGROUND_PRESETS[background.preset] : null;
1311
+ const variant = background.preset === "tag" && background.variant ? TAG_VARIANTS[background.variant] || null : null;
1312
+ return {
1313
+ ...preset || {},
1314
+ ...variant || {},
1315
+ ...background
1316
+ };
1317
+ };
1318
+ var buildBackgroundRect = ({ background, x, y, width, height }) => {
1319
+ const radius = (background == null ? void 0 : background.radius) ?? 3;
1320
+ const fill = (background == null ? void 0 : background.color) ?? "transparent";
1321
+ const borderWidth = (background == null ? void 0 : background.borderWidth) ?? 0;
1322
+ const borderColor = background == null ? void 0 : background.borderColor;
1323
+ if (!borderColor || borderWidth <= 0) {
1324
+ return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" fill="${fill}" />`;
1325
+ }
1326
+ const isTagPreset = (background == null ? void 0 : background.preset) === "tag";
1327
+ const fillRect = `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" fill="${fill}" />`;
1328
+ const strokeInset = borderWidth / 2;
1329
+ const strokeX = x + strokeInset;
1330
+ const strokeY = y + strokeInset;
1331
+ const strokeW = Math.max(0, width - borderWidth);
1332
+ const strokeH = Math.max(0, height - borderWidth);
1333
+ return fillRect + `<rect x="${strokeX}" y="${strokeY}" width="${strokeW}" height="${strokeH}" rx="${Math.max(
1334
+ 0,
1335
+ radius - strokeInset
1336
+ )}" fill="none" stroke="${borderColor}" stroke-width="${borderWidth}"${isTagPreset ? ` shape-rendering="crispEdges"` : ""} />`;
1337
+ };
1338
+ var makeStyledTextDataUri = (text, opts = {}) => {
1339
+ const {
1340
+ variant = "bodytext",
1341
+ format = {},
1342
+ orientation = "horizontal",
1343
+ color: colorProp = HS_TEXT_COLOR,
1344
+ fontFamily = HS_FONT_FAMILY,
1345
+ background: backgroundProp = null,
1346
+ paddingX: paddingXProp = 4,
1347
+ paddingY: paddingYProp = 2,
1348
+ width: widthOverride,
1349
+ height: heightOverride,
1350
+ fontSize: fontSizeOverride
1351
+ } = opts;
1352
+ const preset = VARIANT_PRESETS[variant] || VARIANT_PRESETS.bodytext;
1353
+ const background = resolveBackground(backgroundProp);
1354
+ const fontSize = fontSizeOverride ?? (background == null ? void 0 : background.fontSize) ?? preset.fontSize;
1355
+ const rawWeight = format.fontWeight;
1356
+ const fontWeight = rawWeight ? WEIGHT_ALIASES[rawWeight] ?? rawWeight : preset.fontWeight;
1357
+ const fontStyle = format.italic ? "italic" : "normal";
1358
+ const textDecoration = LINE_DECORATION[format.lineDecoration] || "none";
1359
+ const transformed = applyTextTransform(text, format.textTransform);
1360
+ const lineHeight = (background == null ? void 0 : background.height) ?? preset.lineHeight ?? fontSize;
1361
+ const color = (background == null ? void 0 : background.textColor) ?? colorProp;
1362
+ const paddingX = (background == null ? void 0 : background.canvasPaddingX) ?? paddingXProp;
1363
+ const paddingY = (background == null ? void 0 : background.canvasPaddingY) ?? paddingYProp;
1364
+ const rotate = typeof orientation === "number" ? orientation : ORIENTATION_ROTATION[orientation] ?? 0;
1365
+ const textW = estimateTextWidth(transformed, fontSize);
1366
+ let pillW = 0;
1367
+ let pillH = 0;
1368
+ if (background) {
1369
+ const bgPadX = background.paddingX ?? 6;
1370
+ const bgPadY = background.paddingY ?? 3;
1371
+ pillW = textW + bgPadX * 2;
1372
+ pillH = background.height ?? Math.max(lineHeight, fontSize + bgPadY * 2);
1373
+ }
1374
+ const intrinsicW = (background ? pillW : textW) + paddingX * 2;
1375
+ const intrinsicH = (background ? pillH : lineHeight) + paddingY * 2;
1376
+ const isOrthoRotation = rotate === 90 || rotate === -90 || rotate === 270;
1377
+ const canvasW = widthOverride ?? (isOrthoRotation ? intrinsicH : intrinsicW);
1378
+ const canvasH = heightOverride ?? (isOrthoRotation ? intrinsicW : intrinsicH);
1379
+ const cx = canvasW / 2;
1380
+ const cy = canvasH / 2;
1381
+ const rectX = cx - pillW / 2;
1382
+ const rectY = cy - pillH / 2;
1383
+ const group = (background ? buildBackgroundRect({
1384
+ background,
1385
+ x: rectX,
1386
+ y: rectY,
1387
+ width: pillW,
1388
+ height: pillH
1389
+ }) : "") + `<text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="central" font-family="${fontFamily.replace(/"/g, "&quot;")}" font-size="${fontSize}" font-weight="${fontWeight}" font-style="${fontStyle}" text-decoration="${textDecoration}" fill="${color}">${escapeSvgText(transformed)}</text>`;
1390
+ const wrapped = rotate ? `<g transform="rotate(${rotate} ${cx} ${cy})">${group}</g>` : group;
1391
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${canvasW}" height="${canvasH}">` + wrapped + `</svg>`;
1392
+ return {
1393
+ src: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`,
1394
+ width: canvasW,
1395
+ height: canvasH
1396
+ };
1397
+ };
1398
+
1399
+ // packages/kanban/src/Kanban.jsx
1400
+ import {
1401
+ Alert,
1402
+ AutoGrid,
1403
+ Box as Box2,
1404
+ Button as Button2,
1405
+ Checkbox as Checkbox2,
1406
+ DateInput as DateInput2,
1407
+ DescriptionList,
1408
+ DescriptionListItem,
1409
+ Divider,
1410
+ Dropdown,
1411
+ EmptyState as EmptyState2,
1412
+ Flex as Flex2,
1413
+ Icon as Icon2,
1414
+ Image as Image2,
1415
+ Inline,
1416
+ Link as Link2,
1417
+ LoadingSpinner,
1418
+ Modal,
1419
+ ModalBody,
1420
+ MultiSelect as MultiSelect2,
1421
+ SearchInput as SearchInput2,
1422
+ Select as Select2,
1423
+ Statistics,
1424
+ StatisticsItem,
1425
+ StatisticsTrend,
1426
+ Tag as Tag3,
1427
+ Text as Text2,
1428
+ Tile as Tile2
1429
+ } from "@hubspot/ui-extensions";
1430
+ var DEFAULT_DENSITY = "compact";
1431
+ var DEFAULT_MAX_CARDS = 10;
1432
+ var DEFAULT_MAX_EXPANDED = 50;
1433
+ var DEFAULT_COLUMN_WIDTH = 350;
1434
+ var MIN_COLUMN_WIDTH = 350;
1435
+ var DEFAULT_FILTER_INLINE_LIMIT = 4;
1436
+ var DEFAULT_SEARCH_DEBOUNCE = 250;
1437
+ var DEFAULT_TITLE_TRUNCATE = 60;
1438
+ var applyTruncate = (value, truncate, fallback) => {
1439
+ if (truncate === false) return value;
1440
+ if (typeof value !== "string") return value;
1441
+ const limit = typeof truncate === "number" ? truncate : truncate === true ? fallback || DEFAULT_TITLE_TRUNCATE : fallback || DEFAULT_TITLE_TRUNCATE;
1442
+ if (value.length <= limit) return value;
1443
+ return value.slice(0, limit).trimEnd() + "\u2026";
1444
+ };
1445
+ var DEFAULT_LABELS = {
1446
+ search: "Search cards...",
1447
+ // Only the total is surfaced — callers asked for a single headline number
1448
+ // rather than a "loaded / total" fraction. Fall back to the bare label when
1449
+ // no total is known.
1450
+ showMore: (_shown, total) => total ? `Show more (${total})` : "Show more",
1451
+ showLess: "Show less",
1452
+ loadMore: (_loaded, total) => total ? `Load more (${total})` : "Load more",
1453
+ loadingMore: "Loading...",
1454
+ retryLoadMore: "Retry",
1455
+ emptyColumn: "\u2014",
1456
+ emptyTitle: "No cards",
1457
+ emptyMessage: "Nothing matches the current filters.",
1458
+ loading: "Loading board...",
1459
+ loadingMessage: "This should only take a moment.",
1460
+ errorTitle: "Something went wrong.",
1461
+ errorMessage: "An error occurred while loading data.",
1462
+ cardCount: (n) => String(n),
1463
+ moveTo: "Move",
1464
+ clearAll: "Clear all",
1465
+ selectAll: (count, label) => `Select all ${count} ${label}`,
1466
+ deselectAll: "Deselect all",
1467
+ selected: (count, label) => `${count}\xA0${label}\xA0selected`,
1468
+ filtersButton: "Filters",
1469
+ dateFrom: "From",
1470
+ dateTo: "To",
1471
+ sortButton: "Sort",
1472
+ sortAscending: "Ascending",
1473
+ sortDescending: "Descending",
1474
+ metricsButton: "Metrics"
1475
+ };
1476
+ var makeRotatedTagDataUri = (label) => makeStyledTextDataUri(label, {
1477
+ variant: "microcopy",
1478
+ format: { fontWeight: "demibold" },
1479
+ orientation: "vertical-down",
1480
+ background: { preset: "tag" }
1481
+ });
1482
+ var makeRotatedLabelDataUri = (label) => makeStyledTextDataUri(label, {
1483
+ variant: "bodytext",
1484
+ format: { fontWeight: "demibold" },
1485
+ orientation: "vertical-down"
1486
+ });
1487
+ var canStageReceiveRow = (stage, row, canMove) => {
1488
+ if (!stage) return false;
1489
+ if (typeof canMove === "function" && !canMove(row, stage.value)) return false;
1490
+ if (typeof stage.canEnter === "function" && !stage.canEnter(row)) return false;
1491
+ return true;
1492
+ };
1493
+ var isFieldDirectionSortOption = (option) => !!(option && option.field && (option.direction === "asc" || option.direction === "desc"));
1494
+ var resolveDividers = (cardDividers, density) => {
1495
+ if (cardDividers === true) {
1496
+ return { afterTitle: true, afterSubtitle: true, afterBody: true, afterFooter: true };
1497
+ }
1498
+ if (cardDividers === false) {
1499
+ return { afterTitle: false, afterSubtitle: false, afterBody: false, afterFooter: false };
1500
+ }
1501
+ if (cardDividers && typeof cardDividers === "object") {
1502
+ return {
1503
+ afterTitle: cardDividers.afterTitle ?? false,
1504
+ afterSubtitle: cardDividers.afterSubtitle ?? false,
1505
+ afterBody: cardDividers.afterBody ?? false,
1506
+ afterFooter: cardDividers.afterFooter ?? false
1507
+ };
1508
+ }
1509
+ if (density === "comfortable") {
1510
+ return { afterTitle: true, afterSubtitle: true, afterBody: true, afterFooter: true };
1511
+ }
1512
+ return { afterTitle: false, afterSubtitle: false, afterBody: true, afterFooter: false };
1513
+ };
1514
+ var partitionFields = (cardFields) => {
1515
+ const buckets = { title: null, subtitle: null, meta: [], body: [], footer: [] };
1516
+ for (const field of cardFields || []) {
1517
+ const placement = field.placement || "body";
1518
+ if (placement === "title" && !buckets.title) buckets.title = field;
1519
+ else if (placement === "subtitle" && !buckets.subtitle) buckets.subtitle = field;
1520
+ else if (placement === "meta") buckets.meta.push(field);
1521
+ else if (placement === "footer") buckets.footer.push(field);
1522
+ else buckets.body.push(field);
1523
+ }
1524
+ return buckets;
1525
+ };
1526
+ var resolveFieldValue = (field, row) => {
1527
+ if (!field) return void 0;
1528
+ if (field.field && row && Object.prototype.hasOwnProperty.call(row, field.field)) {
1529
+ return row[field.field];
1530
+ }
1531
+ return void 0;
1532
+ };
1533
+ var resolveHref = (href, row) => {
1534
+ if (!href) return null;
1535
+ if (typeof href === "function") return href(row);
1536
+ return href;
1537
+ };
1538
+ var KanbanCard = ({
1539
+ row,
1540
+ rowId,
1541
+ stage,
1542
+ stages,
1543
+ fields,
1544
+ density,
1545
+ dividers,
1546
+ bodyAs,
1547
+ maxBodyLines,
1548
+ stageControl,
1549
+ stageControlPlacement,
1550
+ canMove,
1551
+ onStageChangeRequest,
1552
+ isChanging,
1553
+ selectable,
1554
+ selected,
1555
+ onToggleSelect,
1556
+ labels
1557
+ }) => {
1558
+ const titleHref = fields.title ? resolveHref(fields.title.href, row) : null;
1559
+ const rawTitleValue = fields.title ? fields.title.render ? fields.title.render(resolveFieldValue(fields.title, row), row) : resolveFieldValue(fields.title, row) : null;
1560
+ const titleValue = fields.title && typeof rawTitleValue === "string" ? applyTruncate(rawTitleValue, fields.title.truncate, DEFAULT_TITLE_TRUNCATE) : rawTitleValue;
1561
+ const titleNode = titleHref ? /* @__PURE__ */ React3.createElement(Link2, { href: titleHref }, titleValue) : /* @__PURE__ */ React3.createElement(Text2, { format: { fontWeight: "demibold" } }, titleValue);
1562
+ const metaNodes = fields.meta.filter((f) => !f.visible || f.visible(row)).map((f) => {
1563
+ const val = resolveFieldValue(f, row);
1564
+ return /* @__PURE__ */ React3.createElement(Text2, { key: f.field || f.label, variant: "microcopy" }, f.render ? f.render(val, row) : val);
1565
+ });
1566
+ const showSubtitle = density === "comfortable" && fields.subtitle;
1567
+ const subtitleNode = showSubtitle ? fields.subtitle.render ? fields.subtitle.render(resolveFieldValue(fields.subtitle, row), row) : resolveFieldValue(fields.subtitle, row) : null;
1568
+ const bodyFields = fields.body.filter((f) => !f.visible || f.visible(row)).slice(0, maxBodyLines);
1569
+ const footerFields = fields.footer.filter((f) => !f.visible || f.visible(row));
1570
+ const footerAlerts = footerFields.slice(0, -1);
1571
+ const footerActionsField = footerFields.length > 0 ? footerFields[footerFields.length - 1] : null;
1572
+ const renderFooterField = (f, idx) => {
1573
+ const val = resolveFieldValue(f, row);
1574
+ const rendered = f.render ? f.render(val, row) : val;
1575
+ const key = f.key || f.field || f.label || `footer-${idx}`;
1576
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, { key }, rendered);
1577
+ };
1578
+ const stageControlNode = stageControl === "none" ? null : /* @__PURE__ */ React3.createElement(
1579
+ StageControl,
1580
+ {
1581
+ row,
1582
+ rowId,
1583
+ currentStage: stage,
1584
+ stages,
1585
+ canMove,
1586
+ isChanging,
1587
+ mode: stageControl,
1588
+ onStageChangeRequest,
1589
+ labels
1590
+ }
1591
+ );
1592
+ const titleRow = /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "between", align: "center", gap: "sm" }, /* @__PURE__ */ React3.createElement(Box2, { flex: 1 }, titleNode), selectable ? /* @__PURE__ */ React3.createElement(
1593
+ Checkbox2,
1594
+ {
1595
+ name: `kanban-select-${rowId}`,
1596
+ checked: selected,
1597
+ onChange: () => onToggleSelect(rowId)
1598
+ }
1599
+ ) : null);
1600
+ const metaRow = metaNodes.length === 0 ? null : /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "end", align: "center", gap: "xs" }, metaNodes);
1601
+ const bodyRow = bodyFields.length === 0 ? null : bodyAs === "descriptionList" ? /* @__PURE__ */ React3.createElement(DescriptionList, { direction: "row" }, bodyFields.map((f, idx) => {
1602
+ const val = resolveFieldValue(f, row);
1603
+ const rendered = f.render ? f.render(val, row) : val ?? "\u2014";
1604
+ const key = f.key || f.field || f.label || `body-${idx}`;
1605
+ return /* @__PURE__ */ React3.createElement(DescriptionListItem, { key, label: f.label || "" }, rendered);
1606
+ })) : /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "flush" }, bodyFields.map((f, idx) => {
1607
+ const val = resolveFieldValue(f, row);
1608
+ const rendered = f.render ? f.render(val, row) : val ?? "\u2014";
1609
+ const key = f.key || f.field || f.label || `body-${idx}`;
1610
+ return /* @__PURE__ */ React3.createElement(Text2, { key, variant: "microcopy" }, f.label ? /* @__PURE__ */ React3.createElement(Text2, { inline: true, variant: "microcopy" }, `${f.label}: `) : null, rendered);
1611
+ }));
1612
+ const footerAlertsNode = footerAlerts.length === 0 ? null : /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, footerAlerts.map((f, idx) => renderFooterField(f, idx)));
1613
+ const footerActionsNode = footerActionsField ? renderFooterField(footerActionsField, footerFields.length - 1) : null;
1614
+ const inlineStageControl = stageControlPlacement === "inline" ? stageControlNode : null;
1615
+ const separateRowStageControl = stageControlPlacement === "separateRow" ? stageControlNode : null;
1616
+ const footerMainRow = inlineStageControl || footerActionsNode ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "between", align: "start", gap: "sm" }, inlineStageControl ? /* @__PURE__ */ React3.createElement(Box2, { alignSelf: "center" }, inlineStageControl) : /* @__PURE__ */ React3.createElement(Box2, null), footerActionsNode ? /* @__PURE__ */ React3.createElement(Box2, { flex: 1 }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "end", align: "start" }, footerActionsNode)) : null) : null;
1617
+ const footerRow = !footerAlertsNode && !footerMainRow ? null : /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, footerAlertsNode, footerMainRow);
1618
+ return /* @__PURE__ */ React3.createElement(Tile2, { compact: density === "compact" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: density === "compact" ? "xs" : "sm" }, titleRow, dividers.afterTitle && (metaRow || bodyRow || footerRow || separateRowStageControl) ? /* @__PURE__ */ React3.createElement(Divider, null) : null, subtitleNode ? /* @__PURE__ */ React3.createElement(Text2, { variant: "microcopy" }, subtitleNode) : null, dividers.afterSubtitle && subtitleNode && (metaRow || bodyRow || footerRow || separateRowStageControl) ? /* @__PURE__ */ React3.createElement(Divider, null) : null, metaRow, bodyRow, dividers.afterBody && bodyRow && (footerRow || separateRowStageControl) ? /* @__PURE__ */ React3.createElement(Divider, null) : null, footerRow, dividers.afterFooter && footerRow && separateRowStageControl ? /* @__PURE__ */ React3.createElement(Divider, null) : null, separateRowStageControl));
1619
+ };
1620
+ var StageControl = ({
1621
+ row,
1622
+ rowId,
1623
+ currentStage,
1624
+ stages,
1625
+ canMove,
1626
+ isChanging,
1627
+ mode,
1628
+ onStageChangeRequest,
1629
+ labels
1630
+ }) => {
1631
+ if (isChanging) {
1632
+ return /* @__PURE__ */ React3.createElement(LoadingSpinner, { size: "xs" });
1633
+ }
1634
+ const availableStages = (stages || []).filter(
1635
+ (stage) => stage.value === currentStage.value || canStageReceiveRow(stage, row, canMove)
1636
+ );
1637
+ if (mode === "menu") {
1638
+ const targetStages = availableStages.filter((stage) => stage.value !== currentStage.value);
1639
+ if (targetStages.length === 0) {
1640
+ return /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "extra-small", disabled: true }, labels.moveTo);
1641
+ }
1642
+ return /* @__PURE__ */ React3.createElement(
1643
+ Dropdown,
1644
+ {
1645
+ variant: "transparent",
1646
+ buttonText: labels.moveTo,
1647
+ buttonSize: "xs"
1648
+ },
1649
+ targetStages.map((stage) => /* @__PURE__ */ React3.createElement(
1650
+ Dropdown.ButtonItem,
1651
+ {
1652
+ key: stage.value,
1653
+ onClick: () => onStageChangeRequest(row, stage.value, currentStage.value)
1654
+ },
1655
+ stage.shortLabel || stage.label
1656
+ ))
1657
+ );
1658
+ }
1659
+ return /* @__PURE__ */ React3.createElement(
1660
+ Select2,
1661
+ {
1662
+ name: `stage-${rowId}`,
1663
+ label: "",
1664
+ value: currentStage.value,
1665
+ onChange: (val) => {
1666
+ if (val !== currentStage.value) onStageChangeRequest(row, val, currentStage.value);
1667
+ },
1668
+ options: availableStages.map((stage) => ({
1669
+ label: stage.shortLabel || stage.label,
1670
+ value: stage.value
1671
+ }))
1672
+ }
1673
+ );
1674
+ };
1675
+ var KanbanColumn = ({
1676
+ stage,
1677
+ rows,
1678
+ bucketCount,
1679
+ totalCount,
1680
+ hasMore,
1681
+ loading,
1682
+ error,
1683
+ onLoadMore,
1684
+ expanded,
1685
+ onToggleExpanded,
1686
+ collapsed,
1687
+ onToggleCollapsed,
1688
+ columnFooter,
1689
+ countDisplay,
1690
+ labels,
1691
+ children
1692
+ }) => {
1693
+ const countLabel = labels.cardCount(totalCount != null ? totalCount : bucketCount);
1694
+ const countNode = countDisplay === "text" ? /* @__PURE__ */ React3.createElement(Text2, { format: { fontWeight: "demibold" } }, countLabel) : countDisplay === "none" ? null : /* @__PURE__ */ React3.createElement(Tag3, { variant: "default" }, countLabel);
1695
+ if (collapsed) {
1696
+ const rotated = makeRotatedLabelDataUri(stage.label);
1697
+ const rotatedCount = countDisplay === "none" ? null : makeRotatedTagDataUri(countLabel);
1698
+ const stageIdentifier = stage.icon ? /* @__PURE__ */ React3.createElement(Icon2, { name: stage.icon, size: "sm", screenReaderText: stage.label }) : /* @__PURE__ */ React3.createElement(
1699
+ Image2,
1700
+ {
1701
+ src: rotated.src,
1702
+ width: rotated.width,
1703
+ height: rotated.height,
1704
+ alt: stage.label
1705
+ }
1706
+ );
1707
+ return /* @__PURE__ */ React3.createElement(Tile2, { compact: true }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs", align: "center" }, /* @__PURE__ */ React3.createElement(
1708
+ Button2,
1709
+ {
1710
+ variant: "transparent",
1711
+ size: "sm",
1712
+ onClick: onToggleCollapsed,
1713
+ tooltip: `Expand ${stage.label}`
1714
+ },
1715
+ /* @__PURE__ */ React3.createElement(Icon2, { name: "right", size: "sm", screenReaderText: `Expand ${stage.label}` })
1716
+ ), stageIdentifier, rotatedCount ? /* @__PURE__ */ React3.createElement(
1717
+ Image2,
1718
+ {
1719
+ src: rotatedCount.src,
1720
+ width: rotatedCount.width,
1721
+ height: rotatedCount.height,
1722
+ alt: `${bucketCount} items`
1723
+ }
1724
+ ) : null));
1725
+ }
1726
+ const footerContent = stage.footer ? stage.footer(rows) : columnFooter ? columnFooter(rows, stage) : null;
1727
+ return /* @__PURE__ */ React3.createElement(Tile2, { compact: true }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", justify: "between", gap: "xs" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React3.createElement(Text2, { format: { fontWeight: "demibold" } }, stage.shortLabel || stage.label), countNode, loading ? /* @__PURE__ */ React3.createElement(LoadingSpinner, { size: "xs" }) : null), /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "sm", onClick: onToggleCollapsed, tooltip: "Collapse" }, /* @__PURE__ */ React3.createElement(Icon2, { name: "left", size: "sm", screenReaderText: `Collapse ${stage.label}` }))), footerContent ? /* @__PURE__ */ React3.createElement(Text2, { variant: "microcopy" }, footerContent) : null, /* @__PURE__ */ React3.createElement(Divider, null), children, error ? /* @__PURE__ */ React3.createElement(Alert, { variant: "danger", title: labels.errorTitle }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", gap: "xs", align: "center" }, /* @__PURE__ */ React3.createElement(Text2, { variant: "microcopy" }, error), onLoadMore ? /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "xs", onClick: () => onLoadMore(stage.value) }, labels.retryLoadMore) : null)) : null, !error && hasMore && onLoadMore && !loading && bucketCount > 0 ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "center" }, /* @__PURE__ */ React3.createElement(Link2, { onClick: () => onLoadMore(stage.value) }, labels.loadMore(bucketCount, totalCount))) : null, !error && loading && hasMore ? /* @__PURE__ */ React3.createElement(LoadingSpinner, { size: "sm", layout: "centered", label: labels.loadingMore }) : null, !error && !hasMore && bucketCount > rows.length ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", justify: "center" }, /* @__PURE__ */ React3.createElement(Link2, { onClick: onToggleExpanded }, expanded ? labels.showLess : labels.showMore(rows.length, bucketCount))) : null, rows.length === 0 && bucketCount === 0 && !loading ? /* @__PURE__ */ React3.createElement(Text2, { variant: "microcopy", format: { italic: true } }, labels.emptyColumn) : null));
1728
+ };
1729
+ var SortModalBody = ({ sortOptions, sortValue, onSortChange, labels }) => {
1730
+ const hasFieldDirection = Array.isArray(sortOptions) && sortOptions.length > 0 && sortOptions.every(isFieldDirectionSortOption);
1731
+ if (!hasFieldDirection) {
1732
+ return /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, sortOptions.map((opt) => /* @__PURE__ */ React3.createElement(
1733
+ Button2,
1734
+ {
1735
+ key: opt.value,
1736
+ variant: sortValue === opt.value ? "primary" : "secondary",
1737
+ onClick: () => onSortChange(opt.value)
1738
+ },
1739
+ opt.label
1740
+ )));
1741
+ }
1742
+ const currentOption = sortOptions.find((o) => o.value === sortValue) || sortOptions[0];
1743
+ const currentField = currentOption.field;
1744
+ const currentDirection = currentOption.direction;
1745
+ const uniqueFields = [];
1746
+ const seen = /* @__PURE__ */ new Set();
1747
+ for (const opt of sortOptions) {
1748
+ if (!seen.has(opt.field)) {
1749
+ seen.add(opt.field);
1750
+ uniqueFields.push({ value: opt.field, label: opt.fieldLabel || opt.field });
1751
+ }
1752
+ }
1753
+ const fieldOpts = sortOptions.filter((o) => o.field === currentField);
1754
+ const ascOption = fieldOpts.find((o) => o.direction === "asc");
1755
+ const descOption = fieldOpts.find((o) => o.direction === "desc");
1756
+ const handleFieldChange = (newField) => {
1757
+ const next = sortOptions.find((o) => o.field === newField && o.direction === currentDirection) || sortOptions.find((o) => o.field === newField && o.direction === "desc") || sortOptions.find((o) => o.field === newField);
1758
+ if (next) onSortChange(next.value);
1759
+ };
1760
+ return /* @__PURE__ */ React3.createElement(Inline, { align: "center", gap: "small" }, /* @__PURE__ */ React3.createElement(
1761
+ Select2,
1762
+ {
1763
+ name: "kanban-sort-field",
1764
+ label: "",
1765
+ value: currentField,
1766
+ onChange: handleFieldChange,
1767
+ options: uniqueFields
1768
+ }
1769
+ ), /* @__PURE__ */ React3.createElement(Inline, { align: "center", gap: "flush" }, descOption ? /* @__PURE__ */ React3.createElement(
1770
+ Button2,
1771
+ {
1772
+ variant: currentDirection === "desc" ? "primary" : "secondary",
1773
+ onClick: () => onSortChange(descOption.value)
1774
+ },
1775
+ labels.sortDescending
1776
+ ) : null, ascOption ? /* @__PURE__ */ React3.createElement(
1777
+ Button2,
1778
+ {
1779
+ variant: currentDirection === "asc" ? "primary" : "secondary",
1780
+ onClick: () => onSortChange(ascOption.value)
1781
+ },
1782
+ labels.sortAscending
1783
+ ) : null));
1784
+ };
1785
+ var renderFilterControl = ({ filter, value, onChange, labels }) => {
1786
+ const type = filter.type || "select";
1787
+ if (type === "multiselect") {
1788
+ return /* @__PURE__ */ React3.createElement(
1789
+ MultiSelect2,
1790
+ {
1791
+ key: filter.name,
1792
+ name: `kanban-filter-${filter.name}`,
1793
+ label: "",
1794
+ placeholder: filter.placeholder || "All",
1795
+ value: value || [],
1796
+ onChange: (val) => onChange(filter.name, val),
1797
+ options: filter.options
1798
+ }
1799
+ );
1800
+ }
1801
+ if (type === "dateRange") {
1802
+ const rangeVal = value || { from: null, to: null };
1803
+ return /* @__PURE__ */ React3.createElement(Flex2, { key: filter.name, direction: "row", align: "center", gap: "xs" }, /* @__PURE__ */ React3.createElement(
1804
+ DateInput2,
1805
+ {
1806
+ size: "sm",
1807
+ name: `kanban-filter-${filter.name}-from`,
1808
+ label: "",
1809
+ placeholder: labels.dateFrom,
1810
+ format: "medium",
1811
+ value: rangeVal.from,
1812
+ onChange: (val) => onChange(filter.name, { ...rangeVal, from: val })
1813
+ }
1814
+ ), /* @__PURE__ */ React3.createElement(Icon2, { name: "dataSync", size: "sm" }), /* @__PURE__ */ React3.createElement(
1815
+ DateInput2,
1816
+ {
1817
+ size: "sm",
1818
+ name: `kanban-filter-${filter.name}-to`,
1819
+ label: "",
1820
+ placeholder: labels.dateTo,
1821
+ format: "medium",
1822
+ value: rangeVal.to,
1823
+ onChange: (val) => onChange(filter.name, { ...rangeVal, to: val })
1824
+ }
1825
+ ));
1826
+ }
1827
+ return /* @__PURE__ */ React3.createElement(
1828
+ Select2,
1829
+ {
1830
+ key: filter.name,
1831
+ name: `kanban-filter-${filter.name}`,
1832
+ variant: "transparent",
1833
+ placeholder: filter.placeholder || "All",
1834
+ value,
1835
+ onChange: (val) => onChange(filter.name, val),
1836
+ options: [
1837
+ { label: filter.placeholder || "All", value: "" },
1838
+ ...filter.options
1839
+ ]
1840
+ }
1841
+ );
1842
+ };
1843
+ var renderMetricsPanel = (metrics) => {
1844
+ if (!metrics) return null;
1845
+ if (!Array.isArray(metrics)) return metrics;
1846
+ if (metrics.length === 0) return null;
1847
+ return /* @__PURE__ */ React3.createElement(Statistics, null, metrics.map((m, i) => /* @__PURE__ */ React3.createElement(
1848
+ StatisticsItem,
1849
+ {
1850
+ key: m.id || m.label || i,
1851
+ label: m.label,
1852
+ number: m.number != null ? String(m.number) : ""
1853
+ },
1854
+ m.trend ? /* @__PURE__ */ React3.createElement(
1855
+ StatisticsTrend,
1856
+ {
1857
+ direction: m.trend.direction || "increase",
1858
+ value: m.trend.value,
1859
+ color: m.trend.color
1860
+ }
1861
+ ) : null
1862
+ )));
1863
+ };
1864
+ var KanbanToolbar = ({
1865
+ showSearch,
1866
+ searchValue,
1867
+ searchPlaceholder,
1868
+ onSearchChange,
1869
+ filters,
1870
+ filterValues,
1871
+ onFilterChange,
1872
+ filterInlineLimit,
1873
+ showFilterBadges,
1874
+ showClearFiltersButton,
1875
+ activeChips,
1876
+ onFilterRemove,
1877
+ sortOptions,
1878
+ sortValue,
1879
+ onSortChange,
1880
+ metrics,
1881
+ showMetrics,
1882
+ onToggleMetrics,
1883
+ labels
1884
+ }) => {
1885
+ const [showMoreFilters, setShowMoreFilters] = useState2(false);
1886
+ const inlineFilters = (filters || []).slice(0, filterInlineLimit);
1887
+ const overflowFilters = (filters || []).slice(filterInlineLimit);
1888
+ return /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "xs" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", gap: "sm" }, /* @__PURE__ */ React3.createElement(Box2, { flex: 3 }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "sm" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch ? /* @__PURE__ */ React3.createElement(
1889
+ SearchInput2,
1890
+ {
1891
+ name: "kanban-search",
1892
+ placeholder: searchPlaceholder,
1893
+ value: searchValue,
1894
+ onChange: onSearchChange
1895
+ }
1896
+ ) : null, inlineFilters.map(
1897
+ (filter) => renderFilterControl({
1898
+ filter,
1899
+ value: filterValues[filter.name],
1900
+ onChange: onFilterChange,
1901
+ labels
1902
+ })
1903
+ ), overflowFilters.length > 0 ? /* @__PURE__ */ React3.createElement(
1904
+ Button2,
1905
+ {
1906
+ variant: "transparent",
1907
+ size: "small",
1908
+ onClick: () => setShowMoreFilters((prev) => !prev)
1909
+ },
1910
+ /* @__PURE__ */ React3.createElement(Icon2, { name: "filter", size: "sm" }),
1911
+ " ",
1912
+ labels.filtersButton
1913
+ ) : null), showMoreFilters && overflowFilters.length > 0 ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, overflowFilters.map(
1914
+ (filter) => renderFilterControl({
1915
+ filter,
1916
+ value: filterValues[filter.name],
1917
+ onChange: onFilterChange,
1918
+ labels
1919
+ })
1920
+ )) : null, activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) ? /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges ? activeChips.map((chip) => /* @__PURE__ */ React3.createElement(
1921
+ Tag3,
1922
+ {
1923
+ key: chip.key,
1924
+ variant: "default",
1925
+ onDelete: () => onFilterRemove(chip.key)
1926
+ },
1927
+ chip.label
1928
+ )) : null, showClearFiltersButton ? /* @__PURE__ */ React3.createElement(
1929
+ Button2,
1930
+ {
1931
+ variant: "transparent",
1932
+ size: "extra-small",
1933
+ onClick: () => onFilterRemove("all")
1934
+ },
1935
+ labels.clearAll
1936
+ ) : null) : null)), (sortOptions == null ? void 0 : sortOptions.length) > 0 || metrics ? /* @__PURE__ */ React3.createElement(Box2, { flex: 1, alignSelf: "start" }, /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", align: "center", gap: "sm", justify: "end" }, sortOptions && sortOptions.length > 0 ? /* @__PURE__ */ React3.createElement(
1937
+ Button2,
1938
+ {
1939
+ variant: "secondary",
1940
+ size: "small",
1941
+ overlay: /* @__PURE__ */ React3.createElement(Modal, { id: "kanban-sort-modal", title: labels.sortButton }, /* @__PURE__ */ React3.createElement(ModalBody, null, /* @__PURE__ */ React3.createElement(
1942
+ SortModalBody,
1943
+ {
1944
+ sortOptions,
1945
+ sortValue,
1946
+ onSortChange,
1947
+ labels
1948
+ }
1949
+ )))
1950
+ },
1951
+ /* @__PURE__ */ React3.createElement(Icon2, { name: "sortAmtDesc", size: "sm" }),
1952
+ " ",
1953
+ labels.sortButton
1954
+ ) : null, metrics ? /* @__PURE__ */ React3.createElement(Button2, { variant: "secondary", size: "small", onClick: onToggleMetrics }, /* @__PURE__ */ React3.createElement(Icon2, { name: "reports", size: "sm" }), " ", labels.metricsButton) : null)) : null), showMetrics && metrics ? renderMetricsPanel(metrics) : null);
1955
+ };
1956
+ var DefaultSelectionBar = ({
1957
+ selectedIds,
1958
+ selectedCount,
1959
+ displayCount,
1960
+ countLabel,
1961
+ allSelected,
1962
+ onSelectAll,
1963
+ onDeselectAll,
1964
+ selectionActions,
1965
+ labels
1966
+ }) => {
1967
+ const pluralForCount = (n) => countLabel(n);
1968
+ return /* @__PURE__ */ React3.createElement(Tile2, { compact: true }, /* @__PURE__ */ React3.createElement(Inline, { align: "center", justify: "between", gap: "small" }, /* @__PURE__ */ React3.createElement(Inline, { align: "center", gap: "small" }, /* @__PURE__ */ React3.createElement(Text2, { inline: true, format: { fontWeight: "demibold" } }, typeof labels.selected === "function" ? labels.selected(selectedCount, pluralForCount(selectedCount)) : `${selectedCount} selected`), !allSelected ? /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "extra-small", onClick: onSelectAll }, typeof labels.selectAll === "function" ? labels.selectAll(displayCount, pluralForCount(displayCount)) : labels.selectAll) : null, /* @__PURE__ */ React3.createElement(Button2, { variant: "transparent", size: "extra-small", onClick: onDeselectAll }, labels.deselectAll)), (selectionActions || []).length > 0 ? /* @__PURE__ */ React3.createElement(Inline, { align: "center", gap: "extra-small" }, selectionActions.map((action, i) => /* @__PURE__ */ React3.createElement(
1969
+ Button2,
1970
+ {
1971
+ key: action.key || action.label || i,
1972
+ variant: action.variant || "transparent",
1973
+ size: "extra-small",
1974
+ onClick: () => action.onClick([...selectedIds])
1975
+ },
1976
+ action.icon ? /* @__PURE__ */ React3.createElement(Icon2, { name: action.icon, size: "sm" }) : null,
1977
+ " ",
1978
+ action.label
1979
+ ))) : null));
1980
+ };
1981
+ var Kanban = ({
1982
+ // --- Data ---
1983
+ data = [],
1984
+ stages = [],
1985
+ groupBy = "status",
1986
+ rowIdField = "id",
1987
+ // --- Card rendering ---
1988
+ renderCard,
1989
+ cardFields,
1990
+ cardDensity = DEFAULT_DENSITY,
1991
+ cardDividers,
1992
+ cardBodyAs = "descriptionList",
1993
+ maxBodyLines,
1994
+ maxCardsPerColumn = DEFAULT_MAX_CARDS,
1995
+ maxCardsExpanded = DEFAULT_MAX_EXPANDED,
1996
+ expandedStages,
1997
+ onExpandedStagesChange,
1998
+ // --- Per-stage pagination ---
1999
+ stageMeta,
2000
+ onLoadMore,
2001
+ // --- Selection ---
2002
+ selectable = false,
2003
+ selectedIds,
2004
+ onSelectionChange,
2005
+ selectionActions,
2006
+ recordLabel,
2007
+ selectionResetKey,
2008
+ resetSelectionOnQueryChange = true,
2009
+ showSelectionBar = true,
2010
+ renderSelectionBar,
2011
+ // --- Stage transitions ---
2012
+ stageControl,
2013
+ stageControlPlacement,
2014
+ onStageChange,
2015
+ isStageChanging,
2016
+ canMove,
2017
+ // --- Toolbar ---
2018
+ showSearch = true,
2019
+ searchFields,
2020
+ searchPlaceholder,
2021
+ searchDebounce = DEFAULT_SEARCH_DEBOUNCE,
2022
+ fuzzySearch = false,
2023
+ fuzzyOptions,
2024
+ filters,
2025
+ filterInlineLimit = DEFAULT_FILTER_INLINE_LIMIT,
2026
+ showFilterBadges = true,
2027
+ showClearFiltersButton,
2028
+ sortOptions,
2029
+ defaultSort,
2030
+ sort,
2031
+ onSortChange,
2032
+ // --- Column level ---
2033
+ columnFooter,
2034
+ columnWidth = DEFAULT_COLUMN_WIDTH,
2035
+ countDisplay = "tag",
2036
+ collapsedStages,
2037
+ onCollapsedStagesChange,
2038
+ // --- Metrics panel ---
2039
+ metrics,
2040
+ // Array of stat items or a ReactNode for full override
2041
+ showMetrics: controlledShowMetrics,
2042
+ onMetricsToggle,
2043
+ // --- State (controlled) ---
2044
+ searchValue,
2045
+ onSearchChange,
2046
+ filterValues,
2047
+ onFilterChange,
2048
+ onParamsChange,
2049
+ loading = false,
2050
+ error,
2051
+ // --- Labels ---
2052
+ labels: labelsProp,
2053
+ renderEmptyState,
2054
+ renderLoadingState,
2055
+ renderErrorState
2056
+ }) => {
2057
+ var _a;
2058
+ const labels = useMemo2(() => ({ ...DEFAULT_LABELS, ...labelsProp || {} }), [labelsProp]);
2059
+ const [internalSearch, setInternalSearch] = useState2(searchValue != null ? searchValue : "");
2060
+ const [internalFilters, setInternalFilters] = useState2(() => {
2061
+ const init = {};
2062
+ (filters || []).forEach((f) => {
2063
+ init[f.name] = getEmptyFilterValue(f);
2064
+ });
2065
+ return init;
2066
+ });
2067
+ const [internalSort, setInternalSort] = useState2(defaultSort || (((_a = sortOptions == null ? void 0 : sortOptions[0]) == null ? void 0 : _a.value) ?? ""));
2068
+ const [internalCollapsed, setInternalCollapsed] = useState2([]);
2069
+ const [internalExpanded, setInternalExpanded] = useState2([]);
2070
+ const [internalSelection, setInternalSelection] = useState2([]);
2071
+ const [internalShowMetrics, setInternalShowMetrics] = useState2(false);
2072
+ const [transitionPrompts, setTransitionPrompts] = useState2({});
2073
+ const resolvedShowMetrics = controlledShowMetrics != null ? controlledShowMetrics : internalShowMetrics;
2074
+ const toggleMetrics = useCallback3(() => {
2075
+ const next = !resolvedShowMetrics;
2076
+ if (onMetricsToggle) onMetricsToggle(next);
2077
+ if (controlledShowMetrics == null) setInternalShowMetrics(next);
2078
+ }, [resolvedShowMetrics, onMetricsToggle, controlledShowMetrics]);
2079
+ const effectiveColumnWidth = Math.max(MIN_COLUMN_WIDTH, columnWidth || DEFAULT_COLUMN_WIDTH);
2080
+ const resolvedSearch = searchValue != null ? searchValue : internalSearch;
2081
+ const searchInputValue = searchDebounce > 0 ? internalSearch : resolvedSearch;
2082
+ const resolvedFilters = filterValues != null ? filterValues : internalFilters;
2083
+ const resolvedSort = sort != null ? sort : internalSort;
2084
+ const resolvedCollapsed = collapsedStages != null ? collapsedStages : internalCollapsed;
2085
+ const resolvedExpanded = expandedStages != null ? expandedStages : internalExpanded;
2086
+ const resolvedSelection = selectedIds != null ? selectedIds : internalSelection;
2087
+ const searchEnabled = showSearch && Array.isArray(searchFields) && searchFields.length > 0;
2088
+ const stagesByValue = useMemo2(() => {
2089
+ const map = {};
2090
+ for (const stage of stages || []) {
2091
+ map[stage.value] = stage;
2092
+ }
2093
+ return map;
2094
+ }, [stages]);
2095
+ const fireParamsChange = useCallback3((overrides = {}) => {
2096
+ if (!onParamsChange) return;
2097
+ onParamsChange({
2098
+ search: overrides.search != null ? overrides.search : resolvedSearch,
2099
+ filters: overrides.filters != null ? overrides.filters : resolvedFilters,
2100
+ sort: overrides.sort != null ? overrides.sort : resolvedSort || null,
2101
+ collapsedStages: overrides.collapsedStages != null ? overrides.collapsedStages : resolvedCollapsed
2102
+ });
2103
+ }, [onParamsChange, resolvedCollapsed, resolvedFilters, resolvedSearch, resolvedSort]);
2104
+ const lastAppliedSearchRef = useRef3(searchValue != null ? searchValue : "");
2105
+ useEffect3(() => {
2106
+ if (searchValue == null) return;
2107
+ if (searchValue === lastAppliedSearchRef.current) return;
2108
+ lastAppliedSearchRef.current = searchValue;
2109
+ setInternalSearch(searchValue);
2110
+ }, [searchValue]);
2111
+ const dispatchSearch = useCallback3(
2112
+ (val) => {
2113
+ lastAppliedSearchRef.current = val;
2114
+ if (onSearchChange) onSearchChange(val);
2115
+ fireParamsChange({ search: val });
2116
+ },
2117
+ [fireParamsChange, onSearchChange]
2118
+ );
2119
+ const dispatchSearchDebounced = useDebouncedDispatch(internalSearch, searchDebounce, dispatchSearch);
2120
+ const handleSearch = useCallback3(
2121
+ (val) => {
2122
+ setInternalSearch(val);
2123
+ dispatchSearchDebounced(val);
2124
+ },
2125
+ [dispatchSearchDebounced]
2126
+ );
2127
+ const handleFilter = useCallback3(
2128
+ (name, val) => {
2129
+ const next = { ...resolvedFilters, [name]: val };
2130
+ if (filterValues == null) setInternalFilters(next);
2131
+ if (onFilterChange) onFilterChange(next);
2132
+ fireParamsChange({ filters: next });
2133
+ },
2134
+ [fireParamsChange, onFilterChange, filterValues, resolvedFilters]
2135
+ );
2136
+ const handleFilterRemove = useCallback3(
2137
+ (key) => {
2138
+ if (key === "all") {
2139
+ const cleared = {};
2140
+ (filters || []).forEach((f) => {
2141
+ cleared[f.name] = getEmptyFilterValue(f);
2142
+ });
2143
+ if (filterValues == null) setInternalFilters(cleared);
2144
+ if (onFilterChange) onFilterChange(cleared);
2145
+ fireParamsChange({ filters: cleared });
2146
+ return;
2147
+ }
2148
+ const filter = (filters || []).find((f) => f.name === key);
2149
+ const emptyVal = filter ? getEmptyFilterValue(filter) : "";
2150
+ const next = { ...resolvedFilters, [key]: emptyVal };
2151
+ if (filterValues == null) setInternalFilters(next);
2152
+ if (onFilterChange) onFilterChange(next);
2153
+ fireParamsChange({ filters: next });
2154
+ },
2155
+ [filters, filterValues, fireParamsChange, onFilterChange, resolvedFilters]
2156
+ );
2157
+ const handleSort = useCallback3(
2158
+ (val) => {
2159
+ if (onSortChange) onSortChange(val);
2160
+ if (sort == null) setInternalSort(val);
2161
+ fireParamsChange({ sort: val });
2162
+ },
2163
+ [fireParamsChange, onSortChange, sort]
2164
+ );
2165
+ const handleCollapsed = useCallback3(
2166
+ (stageValue) => {
2167
+ const next = resolvedCollapsed.includes(stageValue) ? resolvedCollapsed.filter((v) => v !== stageValue) : [...resolvedCollapsed, stageValue];
2168
+ if (onCollapsedStagesChange) onCollapsedStagesChange(next);
2169
+ if (collapsedStages == null) setInternalCollapsed(next);
2170
+ fireParamsChange({ collapsedStages: next });
2171
+ },
2172
+ [fireParamsChange, resolvedCollapsed, collapsedStages, onCollapsedStagesChange]
2173
+ );
2174
+ const handleExpanded = useCallback3(
2175
+ (stageValue) => {
2176
+ const next = resolvedExpanded.includes(stageValue) ? resolvedExpanded.filter((v) => v !== stageValue) : [...resolvedExpanded, stageValue];
2177
+ if (onExpandedStagesChange) onExpandedStagesChange(next);
2178
+ if (expandedStages == null) setInternalExpanded(next);
2179
+ },
2180
+ [resolvedExpanded, expandedStages, onExpandedStagesChange]
2181
+ );
2182
+ const handleToggleSelect = useCallback3(
2183
+ (rowId) => {
2184
+ const next = resolvedSelection.includes(rowId) ? resolvedSelection.filter((id) => id !== rowId) : [...resolvedSelection, rowId];
2185
+ if (onSelectionChange) onSelectionChange(next);
2186
+ if (selectedIds == null) setInternalSelection(next);
2187
+ },
2188
+ [resolvedSelection, selectedIds, onSelectionChange]
2189
+ );
2190
+ const clearTransitionPrompt = useCallback3((rowId) => {
2191
+ setTransitionPrompts((prev) => {
2192
+ if (!Object.prototype.hasOwnProperty.call(prev, rowId)) return prev;
2193
+ const next = { ...prev };
2194
+ delete next[rowId];
2195
+ return next;
2196
+ });
2197
+ }, []);
2198
+ const commitStageChange = useCallback3(
2199
+ (row, newStage, oldStage, result) => {
2200
+ clearTransitionPrompt(row[rowIdField]);
2201
+ if (onStageChange) onStageChange(row, newStage, oldStage, result);
2202
+ },
2203
+ [clearTransitionPrompt, onStageChange, rowIdField]
2204
+ );
2205
+ const selectionQueryKey = useMemo2(() => {
2206
+ if (!resetSelectionOnQueryChange) return "";
2207
+ return toStableKey({
2208
+ search: resolvedSearch,
2209
+ filters: resolvedFilters,
2210
+ sort: resolvedSort || null
2211
+ });
2212
+ }, [resetSelectionOnQueryChange, resolvedFilters, resolvedSearch, resolvedSort]);
2213
+ const combinedSelectionResetKey = useMemo2(
2214
+ () => `${selectionQueryKey}::${selectionResetKey == null ? "" : toStableKey(selectionResetKey)}`,
2215
+ [selectionQueryKey, selectionResetKey]
2216
+ );
2217
+ const clearSelection = useCallback3(() => setInternalSelection([]), []);
2218
+ useSelectionReset({
2219
+ resetKey: combinedSelectionResetKey,
2220
+ enabled: selectable,
2221
+ isControlled: selectedIds != null,
2222
+ clearSelection
2223
+ });
2224
+ const getStageFor = useCallback3(
2225
+ (row) => {
2226
+ if (typeof groupBy === "function") return groupBy(row);
2227
+ return row[groupBy];
2228
+ },
2229
+ [groupBy]
2230
+ );
2231
+ const filteredData = useMemo2(() => {
2232
+ let result = filterRows(data, filters, resolvedFilters);
2233
+ const searchLower = (resolvedSearch || "").toLowerCase().trim();
2234
+ if (searchEnabled && searchLower) {
2235
+ result = searchRows(result, searchLower, searchFields, {
2236
+ fuzzy: fuzzySearch,
2237
+ fuzzyOptions
2238
+ });
2239
+ }
2240
+ return result;
2241
+ }, [data, resolvedSearch, resolvedFilters, filters, searchEnabled, searchFields, fuzzySearch, fuzzyOptions]);
2242
+ const buckets = useMemo2(() => {
2243
+ const map = {};
2244
+ for (const stage of stages) map[stage.value] = [];
2245
+ for (const row of filteredData) {
2246
+ const key = getStageFor(row);
2247
+ if (map[key]) {
2248
+ map[key].push(row);
2249
+ } else if (stages.length > 0) {
2250
+ if (!map.__unknown) map.__unknown = [];
2251
+ map.__unknown.push(row);
2252
+ }
2253
+ }
2254
+ return map;
2255
+ }, [filteredData, stages, getStageFor]);
2256
+ const sortComparator = useMemo2(() => {
2257
+ if (!sortOptions || !resolvedSort) return null;
2258
+ const opt = sortOptions.find((s) => s.value === resolvedSort);
2259
+ return (opt == null ? void 0 : opt.comparator) || null;
2260
+ }, [sortOptions, resolvedSort]);
2261
+ const sortedBuckets = useMemo2(() => {
2262
+ if (!sortComparator) return buckets;
2263
+ const out = {};
2264
+ for (const key of Object.keys(buckets)) {
2265
+ out[key] = [...buckets[key]].sort(sortComparator);
2266
+ }
2267
+ return out;
2268
+ }, [buckets, sortComparator]);
2269
+ const activeChips = useMemo2(() => {
2270
+ const chips = [];
2271
+ for (const filter of filters || []) {
2272
+ const val = resolvedFilters[filter.name];
2273
+ if (!isFilterActive(filter, val)) continue;
2274
+ const type = filter.type || "select";
2275
+ const prefix = filter.chipLabel || filter.placeholder || filter.name;
2276
+ if (type === "multiselect") {
2277
+ const labelList = val.map((v) => {
2278
+ var _a2;
2279
+ return ((_a2 = filter.options.find((o) => o.value === v)) == null ? void 0 : _a2.label) || v;
2280
+ }).join(", ");
2281
+ chips.push({ key: filter.name, label: `${prefix}: ${labelList}` });
2282
+ } else if (type === "dateRange") {
2283
+ const parts = [];
2284
+ if (val.from) parts.push(`from ${formatDateChip(val.from)}`);
2285
+ if (val.to) parts.push(`to ${formatDateChip(val.to)}`);
2286
+ chips.push({ key: filter.name, label: `${prefix}: ${parts.join(" ")}` });
2287
+ } else {
2288
+ const opt = filter.options.find((o) => o.value === val);
2289
+ chips.push({ key: filter.name, label: `${prefix}: ${(opt == null ? void 0 : opt.label) || val}` });
2290
+ }
2291
+ }
2292
+ return chips;
2293
+ }, [filters, resolvedFilters]);
2294
+ const partitioned = useMemo2(() => partitionFields(cardFields || []), [cardFields]);
2295
+ const dividers = useMemo2(() => resolveDividers(cardDividers, cardDensity), [cardDividers, cardDensity]);
2296
+ const resolvedMaxBody = maxBodyLines || (cardDensity === "comfortable" ? 5 : 3);
2297
+ const resolvedStageControl = stageControl || (cardDensity === "comfortable" ? "select" : "menu");
2298
+ const resolvedStageControlPlacement = stageControlPlacement || (resolvedStageControl === "menu" ? "inline" : "separateRow");
2299
+ const handleStageChangeRequest = useCallback3(
2300
+ (row, newStage, oldStage) => {
2301
+ var _a2;
2302
+ if (!newStage || newStage === oldStage) return;
2303
+ const targetStage = stagesByValue[newStage];
2304
+ if (!targetStage || !canStageReceiveRow(targetStage, row, canMove)) return;
2305
+ const rowId = row[rowIdField];
2306
+ if ((_a2 = targetStage.onEnterRequired) == null ? void 0 : _a2.render) {
2307
+ setTransitionPrompts((prev) => ({
2308
+ ...prev,
2309
+ [rowId]: {
2310
+ row,
2311
+ fromStage: oldStage,
2312
+ toStage: newStage
2313
+ }
2314
+ }));
2315
+ return;
2316
+ }
2317
+ commitStageChange(row, newStage, oldStage);
2318
+ },
2319
+ [canMove, commitStageChange, rowIdField, stagesByValue]
2320
+ );
2321
+ const renderCardNode = useCallback3(
2322
+ (row, stage) => {
2323
+ var _a2;
2324
+ const rowId = row[rowIdField];
2325
+ const activePrompt = transitionPrompts[rowId];
2326
+ const promptStage = activePrompt ? stagesByValue[activePrompt.toStage] : null;
2327
+ if ((_a2 = promptStage == null ? void 0 : promptStage.onEnterRequired) == null ? void 0 : _a2.render) {
2328
+ return /* @__PURE__ */ React3.createElement(Tile2, { key: rowId, compact: cardDensity === "compact" }, promptStage.onEnterRequired.render({
2329
+ row: activePrompt.row,
2330
+ fromStage: activePrompt.fromStage,
2331
+ toStage: activePrompt.toStage,
2332
+ onConfirm: (result) => commitStageChange(activePrompt.row, activePrompt.toStage, activePrompt.fromStage, result),
2333
+ onCancel: () => clearTransitionPrompt(rowId)
2334
+ }));
2335
+ }
2336
+ if (renderCard) {
2337
+ return renderCard(row, {
2338
+ stage,
2339
+ isChanging: isStageChanging ? isStageChanging(row) : false,
2340
+ density: cardDensity,
2341
+ onStageChange: (newStage) => handleStageChangeRequest(row, newStage, stage.value)
2342
+ });
2343
+ }
2344
+ return /* @__PURE__ */ React3.createElement(
2345
+ KanbanCard,
2346
+ {
2347
+ key: rowId,
2348
+ row,
2349
+ rowId,
2350
+ stage,
2351
+ stages,
2352
+ fields: partitioned,
2353
+ density: cardDensity,
2354
+ dividers,
2355
+ bodyAs: cardBodyAs,
2356
+ maxBodyLines: resolvedMaxBody,
2357
+ stageControl: resolvedStageControl,
2358
+ stageControlPlacement: resolvedStageControlPlacement,
2359
+ canMove,
2360
+ onStageChangeRequest: handleStageChangeRequest,
2361
+ isChanging: isStageChanging ? isStageChanging(row) : false,
2362
+ selectable,
2363
+ selected: resolvedSelection.includes(rowId),
2364
+ onToggleSelect: handleToggleSelect,
2365
+ labels
2366
+ }
2367
+ );
2368
+ },
2369
+ [
2370
+ clearTransitionPrompt,
2371
+ commitStageChange,
2372
+ renderCard,
2373
+ rowIdField,
2374
+ partitioned,
2375
+ cardDensity,
2376
+ dividers,
2377
+ resolvedMaxBody,
2378
+ resolvedStageControl,
2379
+ resolvedStageControlPlacement,
2380
+ canMove,
2381
+ isStageChanging,
2382
+ selectable,
2383
+ resolvedSelection,
2384
+ handleStageChangeRequest,
2385
+ handleToggleSelect,
2386
+ labels,
2387
+ stages,
2388
+ stagesByValue,
2389
+ transitionPrompts
2390
+ ]
2391
+ );
2392
+ const totalMatching = filteredData.length;
2393
+ const selectedCount = resolvedSelection.length;
2394
+ const singular = ((recordLabel == null ? void 0 : recordLabel.singular) || "card").toLowerCase();
2395
+ const plural = ((recordLabel == null ? void 0 : recordLabel.plural) || "cards").toLowerCase();
2396
+ const countLabel = (n) => n === 1 ? singular : plural;
2397
+ const resolvedSearchPlaceholder = searchPlaceholder ?? ((recordLabel == null ? void 0 : recordLabel.plural) ? `Search ${plural}...` : labels.search);
2398
+ const selectionBarProps = {
2399
+ selectedIds: resolvedSelection,
2400
+ selectedCount,
2401
+ displayCount: totalMatching,
2402
+ countLabel,
2403
+ allSelected: selectedCount >= totalMatching && totalMatching > 0,
2404
+ onSelectAll: () => {
2405
+ const allIds = filteredData.map((r) => r[rowIdField]);
2406
+ if (onSelectionChange) onSelectionChange(allIds);
2407
+ if (selectedIds == null) setInternalSelection(allIds);
2408
+ },
2409
+ onDeselectAll: () => {
2410
+ if (onSelectionChange) onSelectionChange([]);
2411
+ if (selectedIds == null) setInternalSelection([]);
2412
+ },
2413
+ selectionActions: selectionActions || [],
2414
+ labels
2415
+ };
2416
+ const mainContent = error ? renderErrorState ? renderErrorState({
2417
+ error,
2418
+ title: labels.errorTitle,
2419
+ message: typeof error === "string" ? error : labels.errorMessage
2420
+ }) : /* @__PURE__ */ React3.createElement(Alert, { variant: "danger", title: labels.errorTitle }, typeof error === "string" ? error : labels.errorMessage) : loading && data.length === 0 ? renderLoadingState ? renderLoadingState({ label: labels.loading }) : (
2421
+ // Same EmptyState layout as the empty state (just the "building" image +
2422
+ // a loading message) so loading and empty match with no layout shift.
2423
+ /* @__PURE__ */ React3.createElement(Tile2, null, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React3.createElement(EmptyState2, { title: labels.loading, imageName: "building", layout: "vertical" }, /* @__PURE__ */ React3.createElement(Text2, null, labels.loadingMessage))))
2424
+ ) : filteredData.length === 0 ? renderEmptyState ? renderEmptyState({
2425
+ title: labels.emptyTitle,
2426
+ message: labels.emptyMessage
2427
+ }) : /* @__PURE__ */ React3.createElement(Tile2, null, /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React3.createElement(EmptyState2, { title: labels.emptyTitle, layout: "vertical" }, /* @__PURE__ */ React3.createElement(Text2, null, labels.emptyMessage)))) : /* @__PURE__ */ React3.createElement(Flex2, { direction: "row", gap: "sm", wrap: "nowrap" }, stages.map((stage) => {
2428
+ const stageRows = sortedBuckets[stage.value] || [];
2429
+ const meta = stageMeta == null ? void 0 : stageMeta[stage.value];
2430
+ const isExpanded = resolvedExpanded.includes(stage.value);
2431
+ const clamp = isExpanded ? maxCardsExpanded : maxCardsPerColumn;
2432
+ const visibleRows = stageRows.slice(0, clamp);
2433
+ const isCollapsed = resolvedCollapsed.includes(stage.value);
2434
+ return /* @__PURE__ */ React3.createElement(AutoGrid, { key: stage.value, columnWidth: isCollapsed ? 72 : effectiveColumnWidth }, /* @__PURE__ */ React3.createElement(
2435
+ KanbanColumn,
2436
+ {
2437
+ stage,
2438
+ rows: visibleRows,
2439
+ bucketCount: stageRows.length,
2440
+ totalCount: meta == null ? void 0 : meta.totalCount,
2441
+ hasMore: meta == null ? void 0 : meta.hasMore,
2442
+ loading: meta == null ? void 0 : meta.loading,
2443
+ error: meta == null ? void 0 : meta.error,
2444
+ onLoadMore,
2445
+ expanded: isExpanded,
2446
+ onToggleExpanded: () => handleExpanded(stage.value),
2447
+ collapsed: isCollapsed,
2448
+ onToggleCollapsed: () => handleCollapsed(stage.value),
2449
+ columnFooter,
2450
+ countDisplay,
2451
+ labels
2452
+ },
2453
+ visibleRows.map((row) => renderCardNode(row, stage))
2454
+ ));
2455
+ }));
2456
+ const resolvedShowClearFiltersButton = showClearFiltersButton ?? showFilterBadges;
2457
+ return /* @__PURE__ */ React3.createElement(Flex2, { direction: "column", gap: "sm" }, /* @__PURE__ */ React3.createElement(
2458
+ KanbanToolbar,
2459
+ {
2460
+ showSearch: searchEnabled,
2461
+ searchValue: searchInputValue,
2462
+ searchPlaceholder: resolvedSearchPlaceholder,
2463
+ onSearchChange: handleSearch,
2464
+ filters,
2465
+ filterValues: resolvedFilters,
2466
+ onFilterChange: handleFilter,
2467
+ filterInlineLimit,
2468
+ showFilterBadges,
2469
+ showClearFiltersButton: resolvedShowClearFiltersButton,
2470
+ activeChips,
2471
+ onFilterRemove: handleFilterRemove,
2472
+ sortOptions,
2473
+ sortValue: resolvedSort,
2474
+ onSortChange: handleSort,
2475
+ metrics,
2476
+ showMetrics: resolvedShowMetrics,
2477
+ onToggleMetrics: toggleMetrics,
2478
+ labels
2479
+ }
2480
+ ), showSelectionBar && selectable && selectedCount > 0 ? renderSelectionBar ? renderSelectionBar(selectionBarProps) : /* @__PURE__ */ React3.createElement(DefaultSelectionBar, { ...selectionBarProps }) : null, mainContent);
2481
+ };
2482
+
2483
+ // src/utils/objectPath.js
2484
+ var getByPath = (obj, path) => {
2485
+ if (!path) return void 0;
2486
+ if (typeof path === "function") return path(obj);
2487
+ return String(path).split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
2488
+ };
2489
+
2490
+ // src/utils/crmSearchAdapters.js
2491
+ var EMPTY_ARRAY = [];
2492
+ var EMPTY_OBJECT = {};
2493
+ var EMPTY_CRM_PARAMS = { search: "", filters: {}, sort: null };
2494
+ var isPlainObject = (value) => value != null && Object.prototype.toString.call(value) === "[object Object]";
2495
+ var coerceError = (error) => {
2496
+ if (!error) return false;
2497
+ if (typeof error === "string") return error;
2498
+ if (error.message) return error.message;
2499
+ return true;
2500
+ };
2501
+ var pickArray = (response) => {
2502
+ if (Array.isArray(response)) return response;
2503
+ if (!response) return EMPTY_ARRAY;
2504
+ return response.results || response.data || response.items || response.records || response.objects || EMPTY_ARRAY;
2505
+ };
2506
+ var pickTotal = (response, fallbackLength) => {
2507
+ var _a;
2508
+ if (!response || Array.isArray(response)) return fallbackLength;
2509
+ return response.total ?? response.totalCount ?? response.totalResults ?? ((_a = response.paging) == null ? void 0 : _a.total) ?? fallbackLength;
2510
+ };
2511
+ var normalizeCrmSearchRecord = (record, options = EMPTY_OBJECT) => {
2512
+ const {
2513
+ idField = "id",
2514
+ objectIdField = "objectId",
2515
+ propertiesKey = "properties",
2516
+ flattenProperties = true,
2517
+ propertyValueKey,
2518
+ mapRecord
2519
+ } = options;
2520
+ if (mapRecord) return mapRecord(record);
2521
+ 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`);
2522
+ const properties = (record == null ? void 0 : record[propertiesKey]) || EMPTY_OBJECT;
2523
+ const flattened = {};
2524
+ if (flattenProperties && isPlainObject(properties)) {
2525
+ for (const [key, value] of Object.entries(properties)) {
2526
+ flattened[key] = propertyValueKey && isPlainObject(value) ? value[propertyValueKey] : value;
2527
+ }
2528
+ }
2529
+ return {
2530
+ ...flattenProperties ? flattened : EMPTY_OBJECT,
2531
+ ...record,
2532
+ [idField]: objectId,
2533
+ [objectIdField]: objectId,
2534
+ [propertiesKey]: properties
2535
+ };
2536
+ };
2537
+ var normalizeCrmSearchRows = (response, options = EMPTY_OBJECT) => {
2538
+ const records = pickArray(response);
2539
+ return records.map((record) => normalizeCrmSearchRecord(record, options));
2540
+ };
2541
+ var STABLE_SORT_TIEBREAKER = { propertyName: "hs_object_id", direction: "ASCENDING" };
2542
+ var withStableSort = (sorts) => {
2543
+ const base = Array.isArray(sorts) ? sorts : [];
2544
+ if (base.some((s) => s && s.propertyName === STABLE_SORT_TIEBREAKER.propertyName)) return base;
2545
+ return [...base, STABLE_SORT_TIEBREAKER];
2546
+ };
2547
+ var buildCrmSearchConfig = (params = EMPTY_OBJECT, options = EMPTY_OBJECT) => {
2548
+ const {
2549
+ objectType,
2550
+ properties = EMPTY_ARRAY,
2551
+ query,
2552
+ filterGroups,
2553
+ sorts,
2554
+ pageLength,
2555
+ propertyMap = EMPTY_OBJECT,
2556
+ filterMap,
2557
+ sortMap,
2558
+ baseConfig = EMPTY_OBJECT
2559
+ } = options;
2560
+ const mappedFilters = filterMap ? filterMap(params.filters || EMPTY_OBJECT, params) : filterGroups;
2561
+ const mappedSorts = sortMap ? sortMap(params.sort || EMPTY_OBJECT, params) : sorts;
2562
+ const config = {
2563
+ ...baseConfig,
2564
+ objectType: objectType || baseConfig.objectType,
2565
+ properties: properties.length ? properties : baseConfig.properties,
2566
+ query: query ?? params.search ?? baseConfig.query,
2567
+ filterGroups: mappedFilters ?? baseConfig.filterGroups,
2568
+ sorts: withStableSort(mappedSorts ?? baseConfig.sorts),
2569
+ pageLength: pageLength ?? params.pageLength ?? baseConfig.pageLength
2570
+ };
2571
+ if (propertyMap && Object.keys(propertyMap).length && params.filters) {
2572
+ const filters = Object.entries(params.filters).filter(([, value]) => value !== void 0 && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0)).map(([name, value]) => {
2573
+ const propertyName = propertyMap[name] || name;
2574
+ if (Array.isArray(value)) return { propertyName, operator: "IN", values: value };
2575
+ if (isPlainObject(value) && (value.from || value.to)) {
2576
+ const rangeFilters = [];
2577
+ if (value.from) rangeFilters.push({ propertyName, operator: "GTE", value: value.from });
2578
+ if (value.to) rangeFilters.push({ propertyName, operator: "LTE", value: value.to });
2579
+ return rangeFilters;
2580
+ }
2581
+ return { propertyName, operator: "EQ", value };
2582
+ }).flat();
2583
+ if (filters.length) config.filterGroups = [{ filters }];
2584
+ }
2585
+ Object.keys(config).forEach((key) => config[key] === void 0 && delete config[key]);
2586
+ return config;
2587
+ };
2588
+ var useCrmSearchDataSource = (params = EMPTY_OBJECT, options = EMPTY_OBJECT) => {
2589
+ const {
2590
+ format,
2591
+ row,
2592
+ rowIdField = "id",
2593
+ totalCount,
2594
+ loading,
2595
+ error,
2596
+ mapResponse
2597
+ } = options;
2598
+ const config = useMemo3(() => buildCrmSearchConfig(params, options), [params, options]);
2599
+ const response = useCrmSearch(config, format);
2600
+ return useMemo3(() => {
2601
+ const rows = mapResponse ? mapResponse(response) : normalizeCrmSearchRows(response, { idField: rowIdField, ...row || EMPTY_OBJECT });
2602
+ const resolvedTotal = typeof totalCount === "function" ? totalCount(response) : totalCount ?? pickTotal(response, rows.length);
2603
+ return {
2604
+ data: rows,
2605
+ rows,
2606
+ response,
2607
+ loading: typeof loading === "function" ? loading(response) : loading ?? !!(response == null ? void 0 : response.isLoading),
2608
+ isLoading: typeof loading === "function" ? loading(response) : loading ?? !!(response == null ? void 0 : response.isLoading),
2609
+ error: typeof error === "function" ? error(response) : error ?? coerceError(response == null ? void 0 : response.error),
2610
+ totalCount: resolvedTotal,
2611
+ rowIdField
2612
+ };
2613
+ }, [response, mapResponse, row, rowIdField, totalCount, loading, error]);
2614
+ };
2615
+ var crmSearchResultToOption = (row, options = EMPTY_OBJECT) => {
2616
+ const {
2617
+ label = "name",
2618
+ value = "objectId",
2619
+ description,
2620
+ fallbackLabel = "Untitled record",
2621
+ mapOption
2622
+ } = options;
2623
+ if (mapOption) return mapOption(row);
2624
+ const option = {
2625
+ label: getByPath(row, label) ?? getByPath(row, "properties.name") ?? fallbackLabel,
2626
+ value: getByPath(row, value) ?? getByPath(row, "id") ?? getByPath(row, "objectId")
2627
+ };
2628
+ const desc = getByPath(row, description);
2629
+ if (desc != null && desc !== "") option.description = desc;
2630
+ return option;
2631
+ };
2632
+ var useCrmSearchOptions = (params = EMPTY_OBJECT, options = EMPTY_OBJECT) => {
2633
+ const dataSource = useCrmSearchDataSource(params, options);
2634
+ const optionConfig = options.option || options;
2635
+ return useMemo3(() => ({
2636
+ ...dataSource,
2637
+ options: dataSource.rows.map((row) => crmSearchResultToOption(row, optionConfig))
2638
+ }), [dataSource, optionConfig]);
2639
+ };
2640
+ var makeCrmSearchSelectField = (field, searchOptions) => ({
2641
+ type: "select",
2642
+ ...field,
2643
+ options: searchOptions.options || EMPTY_ARRAY,
2644
+ loading: searchOptions.loading || searchOptions.isLoading || (field == null ? void 0 : field.loading)
2645
+ });
2646
+ var makeCrmSearchMultiSelectField = (field, searchOptions) => ({
2647
+ type: "multiselect",
2648
+ ...field,
2649
+ options: searchOptions.options || EMPTY_ARRAY,
2650
+ loading: searchOptions.loading || searchOptions.isLoading || (field == null ? void 0 : field.loading)
2651
+ });
2652
+ var CRM_OBJECT_TYPES = {
2653
+ contact: "0-1",
2654
+ contacts: "0-1",
2655
+ company: "0-2",
2656
+ companies: "0-2",
2657
+ deal: "0-3",
2658
+ deals: "0-3"
2659
+ };
2660
+ var prettifyPropertyName = (name) => String(name || "").replace(/^hs_/, "").replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
2661
+ var inferCrmColumns = (properties = EMPTY_ARRAY) => properties.map((property) => ({
2662
+ field: property,
2663
+ label: prettifyPropertyName(property),
2664
+ sortable: true
2665
+ }));
2666
+ var normalizeAutoFilterFields = (autoFilters, properties = EMPTY_ARRAY) => {
2667
+ if (!autoFilters) return EMPTY_ARRAY;
2668
+ if (Array.isArray(autoFilters)) return autoFilters;
2669
+ if (typeof autoFilters === "object" && Array.isArray(autoFilters.fields)) return autoFilters.fields;
2670
+ return properties.filter((property) => !["id", "objectId", "hs_object_id", "email", "firstname", "lastname", "name", "domain"].includes(property));
2671
+ };
2672
+ var buildAutoFiltersFromRows = ({ rows, fields, labelsRef, maxOptions = 25 }) => {
2673
+ if (!fields.length) return EMPTY_ARRAY;
2674
+ for (const row of rows || EMPTY_ARRAY) {
2675
+ for (const field of fields) {
2676
+ const value = getByPath(row, field);
2677
+ if (value == null || value === "" || Array.isArray(value) || isPlainObject(value)) continue;
2678
+ if (!labelsRef.current[field]) labelsRef.current[field] = /* @__PURE__ */ new Map();
2679
+ const map = labelsRef.current[field];
2680
+ if (map.size < maxOptions || map.has(value)) map.set(value, String(value));
2681
+ }
2682
+ }
2683
+ return fields.map((field) => {
2684
+ const map = labelsRef.current[field];
2685
+ if (!map || map.size === 0 || map.size > maxOptions) return null;
2686
+ return {
2687
+ name: field,
2688
+ label: prettifyPropertyName(field),
2689
+ placeholder: `Any ${prettifyPropertyName(field).toLowerCase()}`,
2690
+ options: Array.from(map.entries()).map(([value, label]) => ({ value, label }))
2691
+ };
2692
+ }).filter(Boolean);
2693
+ };
2694
+ var resolveCrmObjectType = (objectType) => CRM_OBJECT_TYPES[objectType] || objectType;
2695
+ var DEFAULT_CRM_FORMAT = { propertiesToFormat: "all" };
2696
+ var defaultCrmMapRecord = (record) => ({ objectId: record.objectId, ...record.properties });
2697
+ var crmSortsFromState = (sort, propertyMap) => {
2698
+ if (!sort || !sort.field || !sort.direction) return void 0;
2699
+ const propertyName = propertyMap && propertyMap[sort.field] || sort.field;
2700
+ return [{ propertyName, direction: sort.direction === "descending" ? "DESCENDING" : "ASCENDING" }];
2701
+ };
2702
+ var CrmDataTable = ({
2703
+ objectType,
2704
+ properties = EMPTY_ARRAY,
2705
+ columns,
2706
+ title,
2707
+ pageLength = 100,
2708
+ // CRM batch fetched per request (CRM search max)
2709
+ pageSize = 10,
2710
+ // client-side page size
2711
+ // Hybrid model: fetch ONE batch and do everything client-side while the whole
2712
+ // result set fits in the batch (no refetch). Once a fetch comes back capped
2713
+ // (more matches than the batch), search / filter / sort start refetching a
2714
+ // fresh batch server-side so they reach the whole dataset — pagination always
2715
+ // stays client-side (the broken useCrmSearch cursor is never used). Set
2716
+ // `serverSide` to force server-side querying from the first render.
2717
+ serverSide = false,
2718
+ filters,
2719
+ autoFilters = false,
2720
+ autoFilterMaxOptions = 25,
2721
+ filterMap,
2722
+ propertyMap,
2723
+ sortMap,
2724
+ searchFields,
2725
+ searchPlaceholder,
2726
+ format = DEFAULT_CRM_FORMAT,
2727
+ mapRecord,
2728
+ rowIdField = "objectId",
2729
+ dataTableProps = EMPTY_OBJECT,
2730
+ ...props
2731
+ }) => {
2732
+ var _a;
2733
+ const [params, setParams] = useState3({ search: "", filters: {}, sort: null });
2734
+ const resolvedProperties = useMemo3(() => properties, [properties]);
2735
+ const resolvedColumns = useMemo3(
2736
+ () => columns || inferCrmColumns(resolvedProperties),
2737
+ [columns, resolvedProperties]
2738
+ );
2739
+ const resolvedSearchFields = searchFields || resolvedProperties;
2740
+ const autoFilterFields = useMemo3(
2741
+ () => normalizeAutoFilterFields(autoFilters, resolvedProperties),
2742
+ [autoFilters, resolvedProperties]
2743
+ );
2744
+ const autoFilterLabelsRef = useRef4({});
2745
+ const defaultPropertyMap = useMemo3(
2746
+ () => Object.fromEntries(resolvedProperties.map((property) => [property, property])),
2747
+ [resolvedProperties]
2748
+ );
2749
+ const effectivePropertyMap = propertyMap || defaultPropertyMap;
2750
+ const resolvedSortMap = useMemo3(
2751
+ () => sortMap || ((sort) => crmSortsFromState(sort, effectivePropertyMap)),
2752
+ [sortMap, effectivePropertyMap]
2753
+ );
2754
+ const resolvedMapRecord = mapRecord || defaultCrmMapRecord;
2755
+ const dataSourceOptions = useMemo3(
2756
+ () => ({
2757
+ objectType: resolveCrmObjectType(objectType),
2758
+ properties: resolvedProperties,
2759
+ pageLength,
2760
+ format,
2761
+ filterMap,
2762
+ propertyMap: effectivePropertyMap,
2763
+ sortMap: resolvedSortMap,
2764
+ rowIdField,
2765
+ row: { idField: rowIdField, mapRecord: resolvedMapRecord }
2766
+ }),
2767
+ [objectType, resolvedProperties, pageLength, format, filterMap, effectivePropertyMap, resolvedSortMap, rowIdField, resolvedMapRecord]
2768
+ );
2769
+ const [serverQuerying, setServerQuerying] = useState3(!!serverSide);
2770
+ const effectiveParams = serverQuerying ? params : EMPTY_CRM_PARAMS;
2771
+ const dataSource = useCrmSearchDataSource(effectiveParams, dataSourceOptions);
2772
+ useEffect4(() => {
2773
+ if (!serverQuerying && typeof dataSource.totalCount === "number" && dataSource.totalCount > dataSource.data.length) {
2774
+ setServerQuerying(true);
2775
+ }
2776
+ }, [serverQuerying, dataSource.totalCount, dataSource.data.length]);
2777
+ const generatedFilters = useMemo3(
2778
+ () => buildAutoFiltersFromRows({
2779
+ rows: dataSource.data,
2780
+ fields: autoFilterFields,
2781
+ labelsRef: autoFilterLabelsRef,
2782
+ maxOptions: autoFilterMaxOptions
2783
+ }),
2784
+ [dataSource.data, autoFilterFields, autoFilterMaxOptions]
2785
+ );
2786
+ const resolvedFilters = filters || generatedFilters;
2787
+ const table = React4.createElement(DataTable, {
2788
+ title: title || `${prettifyPropertyName(objectType)} records`,
2789
+ data: dataSource.data,
2790
+ loading: dataSource.loading || ((_a = dataSource.response) == null ? void 0 : _a.isRefetching),
2791
+ error: dataSource.error,
2792
+ columns: resolvedColumns,
2793
+ rowIdField,
2794
+ pageSize,
2795
+ filters: resolvedFilters,
2796
+ searchFields: resolvedSearchFields,
2797
+ searchPlaceholder: searchPlaceholder || `Search ${prettifyPropertyName(objectType).toLowerCase()}...`,
2798
+ searchDebounce: 300,
2799
+ onParamsChange: (next) => {
2800
+ setParams((prev) => ({ ...prev, search: next.search, filters: next.filters, sort: next.sort }));
2801
+ },
2802
+ ...dataTableProps,
2803
+ ...props
2804
+ });
2805
+ const total = dataSource.totalCount;
2806
+ const capped = typeof total === "number" && total > dataSource.data.length;
2807
+ if (!capped) return table;
2808
+ return React4.createElement(
2809
+ Flex3,
2810
+ { direction: "column", gap: "xs" },
2811
+ React4.createElement(
2812
+ Text3,
2813
+ { variant: "microcopy" },
2814
+ `Showing the first ${dataSource.data.length} of ${total} matching. Refine your search or filters to narrow the results.`
2815
+ ),
2816
+ table
2817
+ );
2818
+ };
2819
+ var CrmKanban = ({
2820
+ objectType,
2821
+ properties = EMPTY_ARRAY,
2822
+ groupBy,
2823
+ stages,
2824
+ stageLabels,
2825
+ // object { value: label } or (value) => label
2826
+ title,
2827
+ pageLength = 100,
2828
+ serverSide = false,
2829
+ filters,
2830
+ autoFilters = false,
2831
+ autoFilterMaxOptions = 25,
2832
+ filterMap,
2833
+ propertyMap,
2834
+ sortMap,
2835
+ searchFields,
2836
+ searchPlaceholder,
2837
+ format = DEFAULT_CRM_FORMAT,
2838
+ mapRecord,
2839
+ rowIdField = "objectId",
2840
+ kanbanProps = EMPTY_OBJECT,
2841
+ ...props
2842
+ }) => {
2843
+ var _a;
2844
+ const [params, setParams] = useState3(EMPTY_CRM_PARAMS);
2845
+ const resolvedProperties = useMemo3(() => properties, [properties]);
2846
+ const resolvedSearchFields = searchFields || resolvedProperties;
2847
+ const autoFilterFields = useMemo3(
2848
+ () => normalizeAutoFilterFields(autoFilters, resolvedProperties),
2849
+ [autoFilters, resolvedProperties]
2850
+ );
2851
+ const autoFilterLabelsRef = useRef4({});
2852
+ const defaultPropertyMap = useMemo3(
2853
+ () => Object.fromEntries(resolvedProperties.map((property) => [property, property])),
2854
+ [resolvedProperties]
2855
+ );
2856
+ const effectivePropertyMap = propertyMap || defaultPropertyMap;
2857
+ const resolvedSortMap = useMemo3(
2858
+ () => sortMap || ((sort) => crmSortsFromState(sort, effectivePropertyMap)),
2859
+ [sortMap, effectivePropertyMap]
2860
+ );
2861
+ const resolvedMapRecord = mapRecord || defaultCrmMapRecord;
2862
+ const dataSourceOptions = useMemo3(
2863
+ () => ({
2864
+ objectType: resolveCrmObjectType(objectType),
2865
+ properties: resolvedProperties,
2866
+ pageLength,
2867
+ format,
2868
+ filterMap,
2869
+ propertyMap: effectivePropertyMap,
2870
+ sortMap: resolvedSortMap,
2871
+ rowIdField,
2872
+ row: { idField: rowIdField, mapRecord: resolvedMapRecord }
2873
+ }),
2874
+ [objectType, resolvedProperties, pageLength, format, filterMap, effectivePropertyMap, resolvedSortMap, rowIdField, resolvedMapRecord]
2875
+ );
2876
+ const [serverQuerying, setServerQuerying] = useState3(!!serverSide);
2877
+ const effectiveParams = serverQuerying ? params : EMPTY_CRM_PARAMS;
2878
+ const dataSource = useCrmSearchDataSource(effectiveParams, dataSourceOptions);
2879
+ useEffect4(() => {
2880
+ if (!serverQuerying && typeof dataSource.totalCount === "number" && dataSource.totalCount > dataSource.data.length) {
2881
+ setServerQuerying(true);
2882
+ }
2883
+ }, [serverQuerying, dataSource.totalCount, dataSource.data.length]);
2884
+ const generatedFilters = useMemo3(
2885
+ () => buildAutoFiltersFromRows({
2886
+ rows: dataSource.data,
2887
+ fields: autoFilterFields,
2888
+ labelsRef: autoFilterLabelsRef,
2889
+ maxOptions: autoFilterMaxOptions
2890
+ }),
2891
+ [dataSource.data, autoFilterFields, autoFilterMaxOptions]
2892
+ );
2893
+ const resolvedFilters = filters || generatedFilters;
2894
+ const resolvedStages = useMemo3(() => {
2895
+ if (stages) return stages;
2896
+ const seen = [];
2897
+ for (const row of dataSource.data) {
2898
+ const value = typeof groupBy === "function" ? groupBy(row) : row[groupBy];
2899
+ if (value != null && value !== "" && !seen.includes(value)) seen.push(value);
2900
+ }
2901
+ return seen.map((value) => ({
2902
+ value,
2903
+ label: typeof stageLabels === "function" ? stageLabels(value) : stageLabels && stageLabels[value] || prettifyPropertyName(String(value))
2904
+ }));
2905
+ }, [stages, stageLabels, dataSource.data, groupBy]);
2906
+ const board = React4.createElement(Kanban, {
2907
+ title: title || `${prettifyPropertyName(objectType)} board`,
2908
+ data: dataSource.data,
2909
+ loading: dataSource.loading || ((_a = dataSource.response) == null ? void 0 : _a.isRefetching),
2910
+ error: dataSource.error,
2911
+ rowIdField,
2912
+ groupBy,
2913
+ stages: resolvedStages,
2914
+ filters: resolvedFilters,
2915
+ searchFields: resolvedSearchFields,
2916
+ searchPlaceholder: searchPlaceholder || `Search ${prettifyPropertyName(objectType).toLowerCase()}...`,
2917
+ searchDebounce: 300,
2918
+ onParamsChange: (next) => {
2919
+ setParams((prev) => ({ ...prev, search: next.search, filters: next.filters }));
2920
+ },
2921
+ ...kanbanProps,
2922
+ ...props
2923
+ });
2924
+ const total = dataSource.totalCount;
2925
+ const capped = typeof total === "number" && total > dataSource.data.length;
2926
+ if (!capped) return board;
2927
+ return React4.createElement(
2928
+ Flex3,
2929
+ { direction: "column", gap: "xs" },
2930
+ React4.createElement(
2931
+ Text3,
2932
+ { variant: "microcopy" },
2933
+ `Showing the first ${dataSource.data.length} of ${total} matching. Refine your search or filters to narrow the results.`
2934
+ ),
2935
+ board
2936
+ );
2937
+ };
2938
+
7
2939
  // src/utils/formatters.js
8
2940
  var DEFAULT_LOCALE = "en-US";
9
2941
  var formatCurrency = (value, { locale = DEFAULT_LOCALE, currency = "USD", maximumFractionDigits = 0, ...options } = {}) => new Intl.NumberFormat(locale, {
@@ -311,8 +3243,12 @@ var deriveCardFieldsFromColumns = (columns, opts = {}) => {
311
3243
  return out;
312
3244
  };
313
3245
  export {
3246
+ CrmDataTable,
3247
+ CrmKanban,
3248
+ buildCrmSearchConfig,
314
3249
  buildOptions,
315
3250
  createStatusTagSortComparator,
3251
+ crmSearchResultToOption,
316
3252
  deriveCardFieldsFromColumns,
317
3253
  findOptionLabel,
318
3254
  formatCurrency,
@@ -326,5 +3262,12 @@ export {
326
3262
  isDateTimeValueObject,
327
3263
  isDateValueObject,
328
3264
  isTimeValueObject,
329
- sumBy
3265
+ makeCrmSearchMultiSelectField,
3266
+ makeCrmSearchSelectField,
3267
+ normalizeCrmSearchRecord,
3268
+ normalizeCrmSearchRows,
3269
+ resolveCrmObjectType,
3270
+ sumBy,
3271
+ useCrmSearchDataSource,
3272
+ useCrmSearchOptions
330
3273
  };