hs-uix 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,13 @@ import { DataTable, FormBuilder } from "hs-uix";
21
21
 
22
22
  Requires `react` >= 18.0.0 and `@hubspot/ui-extensions` >= 0.12.0 as peer dependencies (already present in any HubSpot UI Extensions project).
23
23
 
24
+ ## Components
25
+
26
+ | Component | Description | Docs |
27
+ |-----------|-------------|------|
28
+ | **DataTable** | Filterable, sortable, paginated table with auto-sized columns, inline editing, row grouping, and more | [Full documentation](https://github.com/05bmckay/hs-uix/blob/main/packages/datatable/README.md) |
29
+ | **FormBuilder** | Declarative, config-driven form with validation, multi-step wizards, and 20+ field types | [Full documentation](https://github.com/05bmckay/hs-uix/blob/main/packages/form/README.md) |
30
+
24
31
  ---
25
32
 
26
33
  # DataTable
@@ -90,8 +97,6 @@ Two edit modes: **discrete** (click-to-edit, default) and **inline** (always-vis
90
97
 
91
98
  Connect live CRM data (contacts, deals, tickets, etc.) to a DataTable with `useAssociations` from `@hubspot/ui-extensions/crm`.
92
99
 
93
- > **Full documentation:** [DataTable README](https://github.com/05bmckay/hs-uix/blob/main/packages/datatable/README.md) — includes full API reference, all examples, server-side mode, and more.
94
-
95
100
  ---
96
101
 
97
102
  # FormBuilder
@@ -167,8 +172,6 @@ const fields = [
167
172
 
168
173
  ![Read-Only Mode](https://raw.githubusercontent.com/05bmckay/hs-uix/main/packages/form/assets/readonly-autosave-dirty.png)
169
174
 
170
- > **Full documentation:** [FormBuilder README](https://github.com/05bmckay/hs-uix/blob/main/packages/form/README.md) — includes full API reference, all field types, validation details, props tables, and more.
171
-
172
175
  ---
173
176
 
174
177
  ## Migrating from `@hs-uix/datatable` or `@hs-uix/form`
package/dist/datatable.js CHANGED
@@ -89,12 +89,19 @@ var computeAutoWidths = (columns, data) => {
89
89
  columns.forEach((col) => {
90
90
  if (col.width && col.cellWidth) return;
91
91
  const values = sample.map((row) => row[col.field]).filter((v) => v != null);
92
- const strings = values.map((v) => String(v));
92
+ const strings = values.map((v) => {
93
+ const s = String(v);
94
+ const truncLen = typeof col.truncate === "number" ? col.truncate : col.truncate && typeof col.truncate === "object" ? col.truncate.maxLength : null;
95
+ return truncLen && s.length > truncLen ? s.slice(0, truncLen) : s;
96
+ });
93
97
  let widthHint = null;
94
98
  let cellWidthHint = null;
95
99
  if (col.editable && col.editType && NARROW_EDIT_TYPES.has(col.editType)) {
96
100
  cellWidthHint = "min";
97
101
  }
102
+ if (col.truncate === true) {
103
+ cellWidthHint = cellWidthHint || "min";
104
+ }
98
105
  if (strings.length > 0) {
99
106
  const lengths = strings.map((s) => s.length);
100
107
  const maxLen = Math.max(...lengths);
@@ -160,12 +167,16 @@ var DataTable = ({
160
167
  // enable fuzzy matching via Fuse.js
161
168
  fuzzyOptions,
162
169
  // custom Fuse.js options (threshold, distance, etc.)
170
+ showSearch = true,
171
+ // show the SearchInput in the toolbar
163
172
  // Filters
164
173
  filters = [],
165
174
  showFilterBadges = true,
166
175
  // show active filter chips/badges
167
176
  showClearFiltersButton = true,
168
177
  // show "Clear all" filters reset button
178
+ filterInlineLimit = 2,
179
+ // number of filters shown inline before overflow
169
180
  // Pagination
170
181
  pageSize = 10,
171
182
  maxVisiblePageButtons,
@@ -247,6 +258,8 @@ var DataTable = ({
247
258
  // optional key to force clear uncontrolled selection memory
248
259
  resetSelectionOnQueryChange = true,
249
260
  // clear uncontrolled selection on search/filter/sort changes
261
+ showSelectionBar = true,
262
+ // show the selection action bar when rows are selected
250
263
  recordLabel,
251
264
  // { singular: "Contact", plural: "Contacts" } — defaults to Record/Records
252
265
  // -----------------------------------------------------------------------
@@ -267,11 +280,31 @@ var DataTable = ({
267
280
  // (row, field, newValue) => void
268
281
  onRowEditInput,
269
282
  // optional live-input callback: (row, field, inputValue) => void
283
+ onEditStart,
284
+ // (row, field, currentValue) => void — fires when editing begins
285
+ onEditCancel,
286
+ // (row, field) => void — fires when editing is cancelled without commit
270
287
  // -----------------------------------------------------------------------
271
288
  // Auto-width
272
289
  // -----------------------------------------------------------------------
273
- autoWidth = true
290
+ autoWidth = true,
274
291
  // auto-compute column widths from content analysis
292
+ // -----------------------------------------------------------------------
293
+ // Labels / i18n
294
+ // -----------------------------------------------------------------------
295
+ labels,
296
+ // override hardcoded UI strings for i18n
297
+ // -----------------------------------------------------------------------
298
+ // Render overrides (Phase 3 — full replacement escape hatches)
299
+ // -----------------------------------------------------------------------
300
+ renderSelectionBar,
301
+ // ({ selectedIds, selectedCount, displayCount, countLabel, onSelectAll, onDeselectAll, selectionActions }) => ReactNode
302
+ renderEmptyState,
303
+ // ({ title, message }) => ReactNode
304
+ renderLoadingState,
305
+ // ({ label }) => ReactNode
306
+ renderErrorState
307
+ // ({ error, title, message }) => ReactNode
275
308
  }) => {
276
309
  const initialSortState = (0, import_react.useMemo)(() => {
277
310
  return normalizeSortState(columns, defaultSort);
@@ -505,11 +538,11 @@ var DataTable = ({
505
538
  const type = filter.type || "select";
506
539
  const prefix = filter.chipLabel || filter.placeholder || filter.name;
507
540
  if (type === "multiselect") {
508
- const labels = value.map((v) => {
541
+ const labels2 = value.map((v) => {
509
542
  var _a;
510
543
  return ((_a = filter.options.find((o) => o.value === v)) == null ? void 0 : _a.label) || v;
511
544
  }).join(", ");
512
- chips.push({ key: filter.name, label: `${prefix}: ${labels}` });
545
+ chips.push({ key: filter.name, label: `${prefix}: ${labels2}` });
513
546
  } else if (type === "dateRange") {
514
547
  const parts = [];
515
548
  if (value.from) parts.push(`from ${formatDateChip(value.from)}`);
@@ -550,7 +583,17 @@ var DataTable = ({
550
583
  const countLabel = (n) => n === 1 ? singularLabel : pluralLabel;
551
584
  const resolvedEmptyTitle = emptyTitle || "No results found";
552
585
  const resolvedEmptyMessage = emptyMessage || `No ${pluralLabel} match your search or filter criteria.`;
553
- const resolvedLoadingLabel = `Loading ${pluralLabel}...`;
586
+ const resolvedSelectedLabel = (labels == null ? void 0 : labels.selected) || ((count, label) => `${count}\xA0${label}\xA0selected`);
587
+ const resolvedSelectAllLabel = (labels == null ? void 0 : labels.selectAll) || ((count, label) => `Select all ${count} ${label}`);
588
+ const resolvedDeselectAllLabel = (labels == null ? void 0 : labels.deselectAll) || "Deselect all";
589
+ const resolvedFiltersButtonLabel = (labels == null ? void 0 : labels.filtersButton) || "Filters";
590
+ const resolvedClearAllLabel = (labels == null ? void 0 : labels.clearAll) || "Clear all";
591
+ const resolvedDateFromLabel = (labels == null ? void 0 : labels.dateFrom) || "From";
592
+ const resolvedDateToLabel = (labels == null ? void 0 : labels.dateTo) || "To";
593
+ const resolvedLoadingLabel = (labels == null ? void 0 : labels.loading) || `Loading ${pluralLabel}...`;
594
+ const resolvedErrorTitle = (labels == null ? void 0 : labels.errorTitle) || "Something went wrong.";
595
+ const resolvedErrorMessage = (labels == null ? void 0 : labels.errorMessage) || "An error occurred while loading data.";
596
+ const resolvedRetryMessage = (labels == null ? void 0 : labels.retryMessage) || "Please try again.";
554
597
  const recordCountLabel = rowCountText ? rowCountText(shownOnPageCount, displayCount) : displayCount === totalDataCount ? `${totalDataCount} ${countLabel(totalDataCount)}` : `${displayCount} of ${totalDataCount} ${countLabel(totalDataCount)}`;
555
598
  const [internalSelectedIds, setInternalSelectedIds] = (0, import_react.useState)(/* @__PURE__ */ new Set());
556
599
  const selectionResetRef = (0, import_react.useRef)("");
@@ -639,7 +682,11 @@ var DataTable = ({
639
682
  setEditingCell({ rowId, field });
640
683
  setEditValue(currentValue);
641
684
  setEditError(null);
642
- }, []);
685
+ if (onEditStart) {
686
+ const row = data.find((r) => r[rowIdField] === rowId);
687
+ if (row) onEditStart(row, field, currentValue);
688
+ }
689
+ }, [onEditStart, data, rowIdField]);
643
690
  const commitEdit = (0, import_react.useCallback)((row, field, value) => {
644
691
  const col = columns.find((c) => c.field === field);
645
692
  if (col == null ? void 0 : col.editValidate) {
@@ -661,6 +708,7 @@ var DataTable = ({
661
708
  const commit = (val) => commitEdit(row, col.field, val);
662
709
  const exitEdit = () => {
663
710
  if (editError) return;
711
+ if (onEditCancel) onEditCancel(row, col.field);
664
712
  setEditingCell(null);
665
713
  setEditValue(null);
666
714
  };
@@ -827,20 +875,26 @@ var DataTable = ({
827
875
  const rawStr = String(rawValue ?? "");
828
876
  if (col.truncate && rawStr.length > 0) {
829
877
  if (col.truncate === true) {
830
- const content2 = col.renderCell ? col.renderCell(rawValue, row) : rawStr;
878
+ if (col.renderCell) {
879
+ const content2 = col.renderCell(rawValue, row);
880
+ if (col.editable) {
881
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
882
+ }
883
+ return content2;
884
+ }
831
885
  if (col.editable) {
832
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--"));
886
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, rawStr || "--"));
833
887
  }
834
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, content2);
888
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, rawStr);
835
889
  }
836
- const maxLen = col.truncate.maxLength || 100;
890
+ const maxLen = typeof col.truncate === "number" ? col.truncate : col.truncate.maxLength || 100;
837
891
  if (rawStr.length > maxLen) {
838
892
  const truncatedStr = rawStr.slice(0, maxLen) + "\u2026";
839
- const truncatedContent = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
893
+ const content2 = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
840
894
  if (col.editable) {
841
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, truncatedContent || "--");
895
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
842
896
  }
843
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, truncatedContent || "--");
897
+ return col.renderCell ? content2 : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { truncate: { tooltipText: rawStr } }, content2 || "--");
844
898
  }
845
899
  }
846
900
  const content = col.renderCell ? col.renderCell(rawValue, row) : rawValue;
@@ -880,7 +934,7 @@ var DataTable = ({
880
934
  {
881
935
  name: `filter-${filter.name}-from`,
882
936
  label: "",
883
- placeholder: "From",
937
+ placeholder: resolvedDateFromLabel,
884
938
  format: "medium",
885
939
  value: rangeVal.from,
886
940
  onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, from: val })
@@ -891,7 +945,7 @@ var DataTable = ({
891
945
  size: "sm",
892
946
  name: `filter-${filter.name}-to`,
893
947
  label: "",
894
- placeholder: "To",
948
+ placeholder: resolvedDateToLabel,
895
949
  format: "medium",
896
950
  value: rangeVal.to,
897
951
  onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, to: val })
@@ -914,7 +968,7 @@ var DataTable = ({
914
968
  }
915
969
  );
916
970
  };
917
- return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, searchFields.length > 0 && /* @__PURE__ */ import_react.default.createElement(
971
+ return /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "xs" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showSearch && searchFields.length > 0 && /* @__PURE__ */ import_react.default.createElement(
918
972
  import_ui_extensions.SearchInput,
919
973
  {
920
974
  name: "datatable-search",
@@ -922,7 +976,7 @@ var DataTable = ({
922
976
  value: searchTerm,
923
977
  onChange: handleSearchChange
924
978
  }
925
- ), filters.slice(0, 2).map(renderFilterControl), filters.length > 2 && /* @__PURE__ */ import_react.default.createElement(
979
+ ), filters.slice(0, filterInlineLimit).map(renderFilterControl), filters.length > filterInlineLimit && /* @__PURE__ */ import_react.default.createElement(
926
980
  import_ui_extensions.Button,
927
981
  {
928
982
  variant: "transparent",
@@ -930,16 +984,25 @@ var DataTable = ({
930
984
  onClick: () => setShowMoreFilters((prev) => !prev)
931
985
  },
932
986
  /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: "filter", size: "sm" }),
933
- " Filters"
934
- )), showMoreFilters && filters.length > 2 && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, filters.slice(2).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), showClearFiltersButton && /* @__PURE__ */ import_react.default.createElement(
987
+ " ",
988
+ resolvedFiltersButtonLabel
989
+ )), showMoreFilters && filters.length > filterInlineLimit && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, filters.slice(filterInlineLimit).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "wrap" }, showFilterBadges && activeChips.map((chip) => /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Tag, { key: chip.key, variant: "default", onDelete: () => handleFilterRemove(chip.key) }, chip.label)), showClearFiltersButton && /* @__PURE__ */ import_react.default.createElement(
935
990
  import_ui_extensions.Button,
936
991
  {
937
992
  variant: "transparent",
938
993
  size: "extra-small",
939
994
  onClick: () => handleFilterRemove("all")
940
995
  },
941
- "Clear all"
942
- )))), showRowCount && displayCount > 0 && !(selectable && selectedIds.size > 0) && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), selectable && selectedIds.size > 0 && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { inline: true, format: { fontWeight: "demibold" } }, selectedIds.size, "\xA0", countLabel(selectedIds.size), "\xA0selected"), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, "Select all ", displayCount, " ", countLabel(displayCount)), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, "Deselect all"), selectionActions.map((action, i) => /* @__PURE__ */ import_react.default.createElement(
996
+ resolvedClearAllLabel
997
+ )))), showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0) && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1, alignSelf: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), showSelectionBar && selectable && selectedIds.size > 0 && (renderSelectionBar ? renderSelectionBar({
998
+ selectedIds,
999
+ selectedCount: selectedIds.size,
1000
+ displayCount,
1001
+ countLabel,
1002
+ onSelectAll: handleSelectAllRows,
1003
+ onDeselectAll: handleDeselectAll,
1004
+ selectionActions
1005
+ }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", gap: "sm" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", align: "center", gap: "sm", wrap: "nowrap" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { inline: true, format: { fontWeight: "demibold" } }, typeof resolvedSelectedLabel === "function" ? resolvedSelectedLabel(selectedIds.size, countLabel(selectedIds.size)) : resolvedSelectedLabel), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, typeof resolvedSelectAllLabel === "function" ? resolvedSelectAllLabel(displayCount, countLabel(displayCount)) : resolvedSelectAllLabel), /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, resolvedDeselectAllLabel), selectionActions.map((action, i) => /* @__PURE__ */ import_react.default.createElement(
943
1006
  import_ui_extensions.Button,
944
1007
  {
945
1008
  key: i,
@@ -950,7 +1013,11 @@ var DataTable = ({
950
1013
  action.icon && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Icon, { name: action.icon, size: "sm" }),
951
1014
  " ",
952
1015
  action.label
953
- )))), showRowCount && displayCount > 0 && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel)))), loading ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.ErrorState, { title: typeof error === "string" ? error : "Something went wrong." }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, typeof error === "string" ? "Please try again." : "An error occurred while loading data.")) : displayRows.length === 0 ? /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, resolvedEmptyMessage))) : /* @__PURE__ */ import_react.default.createElement(
1016
+ )))), showRowCount && displayCount > 0 && /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Box, { flex: 1, alignSelf: "center" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "row", justify: "end" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, { variant: "microcopy", format: rowCountBold ? { fontWeight: "bold" } : void 0 }, recordCountLabel))))), loading ? renderLoadingState ? renderLoadingState({ label: resolvedLoadingLabel }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? renderErrorState ? renderErrorState({
1017
+ error,
1018
+ title: typeof error === "string" ? error : resolvedErrorTitle,
1019
+ message: typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage
1020
+ }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.ErrorState, { title: typeof error === "string" ? error : resolvedErrorTitle }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage)) : displayRows.length === 0 ? renderEmptyState ? renderEmptyState({ title: resolvedEmptyTitle, message: resolvedEmptyMessage }) : /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ import_react.default.createElement(import_ui_extensions.Text, null, resolvedEmptyMessage))) : /* @__PURE__ */ import_react.default.createElement(
954
1021
  import_ui_extensions.Table,
955
1022
  {
956
1023
  bordered,
@@ -84,12 +84,19 @@ var computeAutoWidths = (columns, data) => {
84
84
  columns.forEach((col) => {
85
85
  if (col.width && col.cellWidth) return;
86
86
  const values = sample.map((row) => row[col.field]).filter((v) => v != null);
87
- const strings = values.map((v) => String(v));
87
+ const strings = values.map((v) => {
88
+ const s = String(v);
89
+ const truncLen = typeof col.truncate === "number" ? col.truncate : col.truncate && typeof col.truncate === "object" ? col.truncate.maxLength : null;
90
+ return truncLen && s.length > truncLen ? s.slice(0, truncLen) : s;
91
+ });
88
92
  let widthHint = null;
89
93
  let cellWidthHint = null;
90
94
  if (col.editable && col.editType && NARROW_EDIT_TYPES.has(col.editType)) {
91
95
  cellWidthHint = "min";
92
96
  }
97
+ if (col.truncate === true) {
98
+ cellWidthHint = cellWidthHint || "min";
99
+ }
93
100
  if (strings.length > 0) {
94
101
  const lengths = strings.map((s) => s.length);
95
102
  const maxLen = Math.max(...lengths);
@@ -155,12 +162,16 @@ var DataTable = ({
155
162
  // enable fuzzy matching via Fuse.js
156
163
  fuzzyOptions,
157
164
  // custom Fuse.js options (threshold, distance, etc.)
165
+ showSearch = true,
166
+ // show the SearchInput in the toolbar
158
167
  // Filters
159
168
  filters = [],
160
169
  showFilterBadges = true,
161
170
  // show active filter chips/badges
162
171
  showClearFiltersButton = true,
163
172
  // show "Clear all" filters reset button
173
+ filterInlineLimit = 2,
174
+ // number of filters shown inline before overflow
164
175
  // Pagination
165
176
  pageSize = 10,
166
177
  maxVisiblePageButtons,
@@ -242,6 +253,8 @@ var DataTable = ({
242
253
  // optional key to force clear uncontrolled selection memory
243
254
  resetSelectionOnQueryChange = true,
244
255
  // clear uncontrolled selection on search/filter/sort changes
256
+ showSelectionBar = true,
257
+ // show the selection action bar when rows are selected
245
258
  recordLabel,
246
259
  // { singular: "Contact", plural: "Contacts" } — defaults to Record/Records
247
260
  // -----------------------------------------------------------------------
@@ -262,11 +275,31 @@ var DataTable = ({
262
275
  // (row, field, newValue) => void
263
276
  onRowEditInput,
264
277
  // optional live-input callback: (row, field, inputValue) => void
278
+ onEditStart,
279
+ // (row, field, currentValue) => void — fires when editing begins
280
+ onEditCancel,
281
+ // (row, field) => void — fires when editing is cancelled without commit
265
282
  // -----------------------------------------------------------------------
266
283
  // Auto-width
267
284
  // -----------------------------------------------------------------------
268
- autoWidth = true
285
+ autoWidth = true,
269
286
  // auto-compute column widths from content analysis
287
+ // -----------------------------------------------------------------------
288
+ // Labels / i18n
289
+ // -----------------------------------------------------------------------
290
+ labels,
291
+ // override hardcoded UI strings for i18n
292
+ // -----------------------------------------------------------------------
293
+ // Render overrides (Phase 3 — full replacement escape hatches)
294
+ // -----------------------------------------------------------------------
295
+ renderSelectionBar,
296
+ // ({ selectedIds, selectedCount, displayCount, countLabel, onSelectAll, onDeselectAll, selectionActions }) => ReactNode
297
+ renderEmptyState,
298
+ // ({ title, message }) => ReactNode
299
+ renderLoadingState,
300
+ // ({ label }) => ReactNode
301
+ renderErrorState
302
+ // ({ error, title, message }) => ReactNode
270
303
  }) => {
271
304
  const initialSortState = useMemo(() => {
272
305
  return normalizeSortState(columns, defaultSort);
@@ -500,11 +533,11 @@ var DataTable = ({
500
533
  const type = filter.type || "select";
501
534
  const prefix = filter.chipLabel || filter.placeholder || filter.name;
502
535
  if (type === "multiselect") {
503
- const labels = value.map((v) => {
536
+ const labels2 = value.map((v) => {
504
537
  var _a;
505
538
  return ((_a = filter.options.find((o) => o.value === v)) == null ? void 0 : _a.label) || v;
506
539
  }).join(", ");
507
- chips.push({ key: filter.name, label: `${prefix}: ${labels}` });
540
+ chips.push({ key: filter.name, label: `${prefix}: ${labels2}` });
508
541
  } else if (type === "dateRange") {
509
542
  const parts = [];
510
543
  if (value.from) parts.push(`from ${formatDateChip(value.from)}`);
@@ -545,7 +578,17 @@ var DataTable = ({
545
578
  const countLabel = (n) => n === 1 ? singularLabel : pluralLabel;
546
579
  const resolvedEmptyTitle = emptyTitle || "No results found";
547
580
  const resolvedEmptyMessage = emptyMessage || `No ${pluralLabel} match your search or filter criteria.`;
548
- const resolvedLoadingLabel = `Loading ${pluralLabel}...`;
581
+ const resolvedSelectedLabel = (labels == null ? void 0 : labels.selected) || ((count, label) => `${count}\xA0${label}\xA0selected`);
582
+ const resolvedSelectAllLabel = (labels == null ? void 0 : labels.selectAll) || ((count, label) => `Select all ${count} ${label}`);
583
+ const resolvedDeselectAllLabel = (labels == null ? void 0 : labels.deselectAll) || "Deselect all";
584
+ const resolvedFiltersButtonLabel = (labels == null ? void 0 : labels.filtersButton) || "Filters";
585
+ const resolvedClearAllLabel = (labels == null ? void 0 : labels.clearAll) || "Clear all";
586
+ const resolvedDateFromLabel = (labels == null ? void 0 : labels.dateFrom) || "From";
587
+ const resolvedDateToLabel = (labels == null ? void 0 : labels.dateTo) || "To";
588
+ const resolvedLoadingLabel = (labels == null ? void 0 : labels.loading) || `Loading ${pluralLabel}...`;
589
+ const resolvedErrorTitle = (labels == null ? void 0 : labels.errorTitle) || "Something went wrong.";
590
+ const resolvedErrorMessage = (labels == null ? void 0 : labels.errorMessage) || "An error occurred while loading data.";
591
+ const resolvedRetryMessage = (labels == null ? void 0 : labels.retryMessage) || "Please try again.";
549
592
  const recordCountLabel = rowCountText ? rowCountText(shownOnPageCount, displayCount) : displayCount === totalDataCount ? `${totalDataCount} ${countLabel(totalDataCount)}` : `${displayCount} of ${totalDataCount} ${countLabel(totalDataCount)}`;
550
593
  const [internalSelectedIds, setInternalSelectedIds] = useState(/* @__PURE__ */ new Set());
551
594
  const selectionResetRef = useRef("");
@@ -634,7 +677,11 @@ var DataTable = ({
634
677
  setEditingCell({ rowId, field });
635
678
  setEditValue(currentValue);
636
679
  setEditError(null);
637
- }, []);
680
+ if (onEditStart) {
681
+ const row = data.find((r) => r[rowIdField] === rowId);
682
+ if (row) onEditStart(row, field, currentValue);
683
+ }
684
+ }, [onEditStart, data, rowIdField]);
638
685
  const commitEdit = useCallback((row, field, value) => {
639
686
  const col = columns.find((c) => c.field === field);
640
687
  if (col == null ? void 0 : col.editValidate) {
@@ -656,6 +703,7 @@ var DataTable = ({
656
703
  const commit = (val) => commitEdit(row, col.field, val);
657
704
  const exitEdit = () => {
658
705
  if (editError) return;
706
+ if (onEditCancel) onEditCancel(row, col.field);
659
707
  setEditingCell(null);
660
708
  setEditValue(null);
661
709
  };
@@ -822,20 +870,26 @@ var DataTable = ({
822
870
  const rawStr = String(rawValue ?? "");
823
871
  if (col.truncate && rawStr.length > 0) {
824
872
  if (col.truncate === true) {
825
- const content2 = col.renderCell ? col.renderCell(rawValue, row) : rawStr;
873
+ if (col.renderCell) {
874
+ const content2 = col.renderCell(rawValue, row);
875
+ if (col.editable) {
876
+ return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
877
+ }
878
+ return content2;
879
+ }
826
880
  if (col.editable) {
827
- return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--"));
881
+ return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, rawStr || "--"));
828
882
  }
829
- return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, content2);
883
+ return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, rawStr);
830
884
  }
831
- const maxLen = col.truncate.maxLength || 100;
885
+ const maxLen = typeof col.truncate === "number" ? col.truncate : col.truncate.maxLength || 100;
832
886
  if (rawStr.length > maxLen) {
833
887
  const truncatedStr = rawStr.slice(0, maxLen) + "\u2026";
834
- const truncatedContent = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
888
+ const content2 = col.renderCell ? col.renderCell(truncatedStr, row) : truncatedStr;
835
889
  if (col.editable) {
836
- return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, truncatedContent || "--");
890
+ return /* @__PURE__ */ React.createElement(Link, { variant: "dark", onClick: () => startEditing(rowId, col.field, rawValue) }, content2 || "--");
837
891
  }
838
- return /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, truncatedContent || "--");
892
+ return col.renderCell ? content2 : /* @__PURE__ */ React.createElement(Text, { truncate: { tooltipText: rawStr } }, content2 || "--");
839
893
  }
840
894
  }
841
895
  const content = col.renderCell ? col.renderCell(rawValue, row) : rawValue;
@@ -875,7 +929,7 @@ var DataTable = ({
875
929
  {
876
930
  name: `filter-${filter.name}-from`,
877
931
  label: "",
878
- placeholder: "From",
932
+ placeholder: resolvedDateFromLabel,
879
933
  format: "medium",
880
934
  value: rangeVal.from,
881
935
  onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, from: val })
@@ -886,7 +940,7 @@ var DataTable = ({
886
940
  size: "sm",
887
941
  name: `filter-${filter.name}-to`,
888
942
  label: "",
889
- placeholder: "To",
943
+ placeholder: resolvedDateToLabel,
890
944
  format: "medium",
891
945
  value: rangeVal.to,
892
946
  onChange: (val) => handleFilterChange(filter.name, { ...rangeVal, to: val })
@@ -909,7 +963,7 @@ var DataTable = ({
909
963
  }
910
964
  );
911
965
  };
912
- return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, /* @__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" }, searchFields.length > 0 && /* @__PURE__ */ React.createElement(
966
+ return /* @__PURE__ */ React.createElement(Flex, { direction: "column", gap: "xs" }, /* @__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(
913
967
  SearchInput,
914
968
  {
915
969
  name: "datatable-search",
@@ -917,7 +971,7 @@ var DataTable = ({
917
971
  value: searchTerm,
918
972
  onChange: handleSearchChange
919
973
  }
920
- ), filters.slice(0, 2).map(renderFilterControl), filters.length > 2 && /* @__PURE__ */ React.createElement(
974
+ ), filters.slice(0, filterInlineLimit).map(renderFilterControl), filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(
921
975
  Button,
922
976
  {
923
977
  variant: "transparent",
@@ -925,16 +979,25 @@ var DataTable = ({
925
979
  onClick: () => setShowMoreFilters((prev) => !prev)
926
980
  },
927
981
  /* @__PURE__ */ React.createElement(Icon, { name: "filter", size: "sm" }),
928
- " Filters"
929
- )), showMoreFilters && filters.length > 2 && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, filters.slice(2).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__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)), showClearFiltersButton && /* @__PURE__ */ React.createElement(
982
+ " ",
983
+ resolvedFiltersButtonLabel
984
+ )), showMoreFilters && filters.length > filterInlineLimit && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "end", gap: "sm", wrap: "wrap" }, filters.slice(filterInlineLimit).map(renderFilterControl)), activeChips.length > 0 && (showFilterBadges || showClearFiltersButton) && /* @__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)), showClearFiltersButton && /* @__PURE__ */ React.createElement(
930
985
  Button,
931
986
  {
932
987
  variant: "transparent",
933
988
  size: "extra-small",
934
989
  onClick: () => handleFilterRemove("all")
935
990
  },
936
- "Clear all"
937
- )))), showRowCount && displayCount > 0 && !(selectable && selectedIds.size > 0) && /* @__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)))), selectable && selectedIds.size > 0 && /* @__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" } }, selectedIds.size, "\xA0", countLabel(selectedIds.size), "\xA0selected"), /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleSelectAllRows }, "Select all ", displayCount, " ", countLabel(displayCount)), /* @__PURE__ */ React.createElement(Button, { variant: "transparent", size: "extra-small", onClick: handleDeselectAll }, "Deselect all"), selectionActions.map((action, i) => /* @__PURE__ */ React.createElement(
991
+ resolvedClearAllLabel
992
+ )))), showRowCount && displayCount > 0 && !(showSelectionBar && selectable && selectedIds.size > 0) && /* @__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({
993
+ selectedIds,
994
+ selectedCount: selectedIds.size,
995
+ displayCount,
996
+ countLabel,
997
+ onSelectAll: handleSelectAllRows,
998
+ onDeselectAll: handleDeselectAll,
999
+ selectionActions
1000
+ }) : /* @__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), /* @__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(
938
1001
  Button,
939
1002
  {
940
1003
  key: i,
@@ -945,7 +1008,11 @@ var DataTable = ({
945
1008
  action.icon && /* @__PURE__ */ React.createElement(Icon, { name: action.icon, size: "sm" }),
946
1009
  " ",
947
1010
  action.label
948
- )))), 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 ? /* @__PURE__ */ React.createElement(LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? /* @__PURE__ */ React.createElement(ErrorState, { title: typeof error === "string" ? error : "Something went wrong." }, /* @__PURE__ */ React.createElement(Text, null, typeof error === "string" ? "Please try again." : "An error occurred while loading data.")) : displayRows.length === 0 ? /* @__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(
1011
+ )))), 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 }) : /* @__PURE__ */ React.createElement(LoadingSpinner, { label: resolvedLoadingLabel, layout: "centered" }) : error ? renderErrorState ? renderErrorState({
1012
+ error,
1013
+ title: typeof error === "string" ? error : resolvedErrorTitle,
1014
+ message: typeof error === "string" ? resolvedRetryMessage : resolvedErrorMessage
1015
+ }) : /* @__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(Flex, { direction: "column", align: "center", justify: "center" }, /* @__PURE__ */ React.createElement(EmptyState, { title: resolvedEmptyTitle, layout: "vertical" }, /* @__PURE__ */ React.createElement(Text, null, resolvedEmptyMessage))) : /* @__PURE__ */ React.createElement(
949
1016
  Table,
950
1017
  {
951
1018
  bordered,