@ai-matrx/records-ui 0.102.21 → 0.103.2

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/index.js CHANGED
@@ -1115,6 +1115,7 @@ function PickChips({ chips }) {
1115
1115
  ) });
1116
1116
  }
1117
1117
  function PickOrAdd({
1118
+ triggerSize = "field",
1118
1119
  triggerLabel,
1119
1120
  triggerText,
1120
1121
  triggerId,
@@ -1173,7 +1174,7 @@ function PickOrAdd({
1173
1174
  }
1174
1175
  ) : null;
1175
1176
  return /* @__PURE__ */ jsxs5(Popover, { open, onOpenChange, children: [
1176
- anchored ? /* @__PURE__ */ jsx6(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsx6("div", { className: "flex min-h-6 w-full min-w-0 flex-wrap items-center gap-1", "aria-label": triggerLabel, children }) }) : /* @__PURE__ */ jsx6(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx6(Button2, { id: triggerId, type: "button", size: "sm", variant: "outline", "aria-label": triggerLabel, children: triggerText }) }),
1177
+ anchored ? /* @__PURE__ */ jsx6(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsx6("div", { "data-pick-anchor": "", className: "flex min-h-6 w-full min-w-0 flex-wrap items-center gap-1", "aria-label": triggerLabel, children }) }) : /* @__PURE__ */ jsx6(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx6(Button2, { id: triggerId, type: "button", size: "sm", variant: "outline", className: triggerSize === "field" ? "h-9" : void 0, "aria-label": triggerLabel, children: triggerText }) }),
1177
1178
  /* @__PURE__ */ jsxs5(
1178
1179
  PopoverContent,
1179
1180
  {
@@ -3595,6 +3596,7 @@ function PickOne({
3595
3596
  return /* @__PURE__ */ jsx14("span", { className: cn11("inline-flex min-w-0", className), "data-records-pick-one": "search", "data-pick-value": value, ...attrs ?? {}, children: /* @__PURE__ */ jsx14(
3596
3597
  PickOrAdd,
3597
3598
  {
3599
+ triggerSize: "compact",
3598
3600
  triggerLabel: label,
3599
3601
  triggerText: current?.label ?? placeholder ?? "",
3600
3602
  triggerId: id,
@@ -4680,7 +4682,7 @@ function TypedWordsControl({ field: field2, value, onChange, disabled, id }) {
4680
4682
  disabled,
4681
4683
  autoComplete: "off",
4682
4684
  "aria-invalid": refusal4 ? true : void 0,
4683
- placeholder: helper === "time" ? "2:30 PM" : helper === "datetime" ? `${typedDateText2("2026-10-03")} 2:30 PM` : helper ? typedDateText2("2026-10-03") : void 0,
4685
+ placeholder: helper ? typedDatePlaceholder(helper) : void 0,
4684
4686
  onChange: (e) => retype(e.target.value),
4685
4687
  onBlur: () => {
4686
4688
  if (!refusal4) return;
@@ -4694,6 +4696,12 @@ function TypedWordsControl({ field: field2, value, onChange, disabled, id }) {
4694
4696
  refusal4 ? /* @__PURE__ */ jsx19("span", { "data-records-typed-refusal": "", "data-records-cell-note": "", role: "status", title: why, className: "text-[11px] leading-snug text-amber-700 dark:text-amber-400", children: refusal4 }) : null
4695
4697
  ] });
4696
4698
  }
4699
+ function typedDatePlaceholder(helper) {
4700
+ const time = "h:mm AM";
4701
+ if (helper === "time") return time;
4702
+ const pattern = typedDateText2("2026-11-22").replace("2026", "YYYY").replace("11", "MM").replace("22", "DD");
4703
+ return helper === "datetime" ? `${pattern} ${time}` : pattern;
4704
+ }
4697
4705
  function CheckboxControl({ field: field2, value, onChange, disabled, id }) {
4698
4706
  const unanswered = value === null || value === void 0;
4699
4707
  return /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2", children: [
@@ -6347,16 +6355,16 @@ function cellMarkSentence(field2, document2, value) {
6347
6355
  const kept = keptAsideFor(document2, field2.key);
6348
6356
  return kept ? keptAsideReason(field2, kept) : null;
6349
6357
  }
6350
- var WORKED_OUT_ERROR_WORD = "#ERROR";
6351
- function WorkedOutError({ field: field2, sentence: sentence2 }) {
6358
+ function WorkedOutError({ field: field2, sentence: sentence2, floated = false }) {
6352
6359
  return /* @__PURE__ */ jsx26(
6353
6360
  "span",
6354
6361
  {
6355
6362
  "data-records-worked-out-error": field2.key,
6363
+ role: "img",
6356
6364
  title: sentence2,
6357
- "aria-label": `Error. ${sentence2}`,
6358
- className: "cursor-help font-medium text-amber-700 dark:text-amber-300",
6359
- children: WORKED_OUT_ERROR_WORD
6365
+ "aria-label": sentence2,
6366
+ className: (floated ? "absolute right-1 top-1/2 z-10 flex -translate-y-1/2 " : "inline-flex align-middle ") + "h-4 w-4 shrink-0 cursor-help items-center justify-center rounded-full bg-destructive text-[10px] font-bold leading-none text-destructive-foreground",
6367
+ children: "!"
6360
6368
  }
6361
6369
  );
6362
6370
  }
@@ -6585,7 +6593,7 @@ function RecordValue({
6585
6593
  if (withheld && (value === null || value === void 0)) return /* @__PURE__ */ jsx27(WithheldValue, { field: field2, notice: withheld });
6586
6594
  if (value === null || value === void 0) {
6587
6595
  const failed = workedOutErrorFor(document2, field2.key);
6588
- if (failed) return /* @__PURE__ */ jsx27(WorkedOutError, { field: field2, sentence: failed });
6596
+ if (failed) return /* @__PURE__ */ jsx27(WorkedOutError, { field: field2, sentence: failed, floated: Boolean(inCell) });
6589
6597
  }
6590
6598
  if (inCell) {
6591
6599
  const kept = keptAsideFor(document2, field2.key);
@@ -7208,6 +7216,22 @@ function GridCell({
7208
7216
  const open = Boolean(
7209
7217
  editing && editing.editing?.rowId === row.id && editing.editing.key === field2.key
7210
7218
  );
7219
+ const closedControl = useRef15(null);
7220
+ const wasOpen = useRef15(false);
7221
+ useEffect15(() => {
7222
+ if (open) {
7223
+ wasOpen.current = true;
7224
+ return;
7225
+ }
7226
+ if (!wasOpen.current) return;
7227
+ wasOpen.current = false;
7228
+ if (!clickOpens) return;
7229
+ const frame = requestAnimationFrame(() => {
7230
+ const active = document.activeElement;
7231
+ if (!active || active === document.body) closedControl.current?.focus({ preventScroll: true });
7232
+ });
7233
+ return () => cancelAnimationFrame(frame);
7234
+ }, [open, clickOpens]);
7211
7235
  const wasActive = useRef15(false);
7212
7236
  const singlePress = useRef15(null);
7213
7237
  const cancelSinglePress = () => {
@@ -7351,9 +7375,13 @@ function GridCell({
7351
7375
  /* @__PURE__ */ jsx28(
7352
7376
  "span",
7353
7377
  {
7378
+ ref: closedControl,
7354
7379
  role: editable ? "button" : void 0,
7355
7380
  "aria-label": editable ? `Edit ${fieldName(field2)}` : void 0,
7356
- title: cellMark ? cellMark : editable ? `${fieldName(field2)} \u2014 double-click or press Enter to edit` : fieldIsEditable(field2) ? (
7381
+ title: cellMark ? cellMark : editable ? (
7382
+ // The words match the gesture (WALK3-FIX): one click opens a record-page field.
7383
+ clickOpens ? `${fieldName(field2)} \u2014 click to edit` : `${fieldName(field2)} \u2014 double-click or press Enter to edit`
7384
+ ) : fieldIsEditable(field2) ? (
7357
7385
  // NOT A DEAD CONTROL AND NOT A SILENT ONE: the cell is plain
7358
7386
  // text, and hovering it says exactly why, in the level's own
7359
7387
  // words. The editor was never offered, so nothing is refused
@@ -8655,6 +8683,7 @@ function ChoiceValue({ field: field2, value, onChange }) {
8655
8683
  return /* @__PURE__ */ jsx34("span", { className: "inline-flex min-w-0", "data-records-condition-choice": "", "data-pick-value": value, children: /* @__PURE__ */ jsx34(
8656
8684
  PickOrAdd,
8657
8685
  {
8686
+ triggerSize: "compact",
8658
8687
  triggerLabel: `Choice for ${field2.label || field2.key}`,
8659
8688
  triggerText: value || "Choose",
8660
8689
  open,
@@ -8878,6 +8907,7 @@ function ConditionGroup({
8878
8907
  onChange,
8879
8908
  emptyLabel = "always",
8880
8909
  allowNesting = true,
8910
+ maxDepth,
8881
8911
  depth = 0,
8882
8912
  onRemove,
8883
8913
  className
@@ -8948,6 +8978,7 @@ function ConditionGroup({
8948
8978
  onChange: (next) => replaceAt(index, next),
8949
8979
  onRemove: () => replaceAt(index, null),
8950
8980
  emptyLabel,
8981
+ maxDepth,
8951
8982
  depth: depth + 1
8952
8983
  }
8953
8984
  ) }, key);
@@ -8991,7 +9022,7 @@ function ConditionGroup({
8991
9022
  children: "Add another condition"
8992
9023
  }
8993
9024
  ),
8994
- allowNesting ? /* @__PURE__ */ jsx34(
9025
+ allowNesting && (maxDepth === void 0 || depth < maxDepth) ? /* @__PURE__ */ jsx34(
8995
9026
  Button14,
8996
9027
  {
8997
9028
  size: "sm",
@@ -10046,8 +10077,29 @@ var MEASURE_LABEL = {
10046
10077
  median: "Median",
10047
10078
  filled: "Filled",
10048
10079
  empty: "Empty",
10049
- unique: "Unique"
10080
+ unique: "Unique",
10081
+ percent_empty: "Percent empty",
10082
+ percent_filled: "Percent filled",
10083
+ range: "Range"
10050
10084
  };
10085
+ var NOTION_CALCULATIONS = [
10086
+ { op: "count", menu: "Count all", footer: "Count" },
10087
+ { op: "unique", menu: "Count unique values", footer: "Unique" },
10088
+ { op: "empty", menu: "Count empty", footer: "Empty" },
10089
+ { op: "filled", menu: "Count not empty", footer: "Not empty" },
10090
+ { op: "percent_empty", menu: "Percent empty", footer: "Empty" },
10091
+ { op: "percent_filled", menu: "Percent not empty", footer: "Not empty" },
10092
+ { op: "sum", menu: "Sum", footer: "Sum" },
10093
+ { op: "avg", menu: "Average", footer: "Average" },
10094
+ { op: "median", menu: "Median", footer: "Median" },
10095
+ { op: "min", menu: "Min", footer: "Min" },
10096
+ { op: "max", menu: "Max", footer: "Max" },
10097
+ { op: "range", menu: "Range", footer: "Range" }
10098
+ ];
10099
+ function calculationsFor(field2, storeOps) {
10100
+ const offered = new Set(measuresFor(field2, storeOps, true));
10101
+ return NOTION_CALCULATIONS.filter((c) => offered.has(c.op));
10102
+ }
10051
10103
  var NUMERIC_ONLY = /* @__PURE__ */ new Set(["sum", "avg", "median"]);
10052
10104
  var ARITHMETIC2 = /* @__PURE__ */ new Set([
10053
10105
  "add",
@@ -10065,7 +10117,7 @@ var ARITHMETIC2 = /* @__PURE__ */ new Set([
10065
10117
  "ceiling",
10066
10118
  "floor"
10067
10119
  ]);
10068
- var ORDERED = /* @__PURE__ */ new Set(["min", "max"]);
10120
+ var ORDERED = /* @__PURE__ */ new Set(["min", "max", "range"]);
10069
10121
  function isNumericField(field2) {
10070
10122
  const kind = editorKindFor(field2);
10071
10123
  if (kind === "number" || kind === "currency" || kind === "percent" || kind === "rollup") return true;
@@ -10642,6 +10694,7 @@ function FieldEditor(props) {
10642
10694
  const [formulaWritten, setFormulaWritten] = useState31(() => startingFormulaText.trim() !== "" || Boolean(field2));
10643
10695
  const [formulaOk, setFormulaOk] = useState31(null);
10644
10696
  const [askingToRemove, setAskingToRemove] = useState31(false);
10697
+ const [attempted, setAttempted] = useState31(false);
10645
10698
  const [showsAs, setShowsAs] = useState31(() => field2 ? startingChoice(field2) : { format: null, retype: null });
10646
10699
  const [backfillError, setBackfillError] = useState31(null);
10647
10700
  const [notLanded, setNotLanded] = useState31(false);
@@ -10691,6 +10744,8 @@ function FieldEditor(props) {
10691
10744
  const missing = editing ? formulaMissing ?? (listsChoices ? whatTheChoiceListIsMissing(choiceList) : null) : formulaMissing ?? (listsChoices ? whatTheChoiceListIsMissing(newChoices) : null) ?? (type === "formula" && formulaWritten ? null : whatIsMissing(
10692
10745
  type === "count" ? { type: "rollup", label, options, via, pick, agg: "count", of, relations: totalSources, formulaFields, relationTarget, allowedTypes, noun } : { type, label, options, via, pick, agg, of, relations: sourcesFor(type), formulaFields, relationTarget, allowedTypes, noun }
10693
10746
  ));
10747
+ const missingShown = missing && (attempted || formulaMissing) ? missing : null;
10748
+ const removeConsequence = field2 ? `${archiveConfirmSentence(`the field \u201C${fieldName(field2)}\u201D`)} Its values stay in every record and come back with it.${linkReaders.length > 0 ? ` ${readersSentence(linkReaders.length, farTable.data?.name ?? null)}` : ""}` : "";
10694
10749
  const save = async () => {
10695
10750
  if (missing) return;
10696
10751
  if (editing && field2) {
@@ -10838,8 +10893,8 @@ function FieldEditor(props) {
10838
10893
  const remove = async () => {
10839
10894
  if (!field2) return;
10840
10895
  const gone = await tableMutation.retire({ field_id: field2.id });
10841
- setAskingToRemove(false);
10842
10896
  if (gone) (onRemoved ?? onSaved)?.();
10897
+ else setAskingToRemove(false);
10843
10898
  };
10844
10899
  return /* @__PURE__ */ jsxs35("div", { className: cn24("flex min-w-0 flex-col gap-3 text-xs", className), "data-records-field-editor": "", children: [
10845
10900
  /* @__PURE__ */ jsxs35("div", { className: "flex items-center gap-2", children: [
@@ -10856,7 +10911,12 @@ function FieldEditor(props) {
10856
10911
  autoFocus: true,
10857
10912
  "aria-label": "Field name",
10858
10913
  placeholder: "Day rate",
10859
- onChange: (e) => setLabel(e.target.value)
10914
+ onChange: (e) => setLabel(e.target.value),
10915
+ onBlur: (e) => {
10916
+ const to = e.relatedTarget;
10917
+ if (to?.textContent === "Cancel") return;
10918
+ setAttempted(true);
10919
+ }
10860
10920
  }
10861
10921
  )
10862
10922
  ] }),
@@ -11144,7 +11204,6 @@ function FieldEditor(props) {
11144
11204
  ] }),
11145
11205
  editing && field2 && !onEntity && rulesChanged(savedRules, editedRules) ? /* @__PURE__ */ jsx40(FieldRulePreview, { field: field2, tableId, rules: editedRules }) : null,
11146
11206
  mutation.error ? /* @__PURE__ */ jsx40(RefusalNotice, { error: mutation.error }) : null,
11147
- missing ? /* @__PURE__ */ jsx40("p", { id: "field-editor-missing", className: "text-[11px] text-muted-foreground", "data-field-editor-missing": "", children: missing }) : null,
11148
11207
  notLanded ? /* @__PURE__ */ jsx40(
11149
11208
  RefusalNotice,
11150
11209
  {
@@ -11189,27 +11248,61 @@ function FieldEditor(props) {
11189
11248
  /* @__PURE__ */ jsx40("p", { className: "text-[11px]", children: "The column is made. The records already in the table were not numbered:" }),
11190
11249
  /* @__PURE__ */ jsx40(RefusalNotice, { error: backfillError })
11191
11250
  ] }) : null,
11192
- /* @__PURE__ */ jsxs35("div", { className: "flex flex-wrap items-center gap-2", children: [
11251
+ /* @__PURE__ */ jsxs35("div", { className: "flex min-w-0 flex-nowrap items-center gap-2", "data-field-editor-footer": "", children: [
11193
11252
  /* @__PURE__ */ jsx40(
11194
11253
  Button17,
11195
11254
  {
11196
11255
  size: "sm",
11197
- disabled: mutation.saving || missing !== null,
11198
- ...missing ? { "aria-describedby": "field-editor-missing" } : {},
11199
- onClick: () => void save(),
11256
+ disabled: mutation.saving || Boolean(formulaMissing),
11257
+ ...missingShown ? { "aria-describedby": "field-editor-missing" } : {},
11258
+ onClick: () => {
11259
+ if (missing) {
11260
+ setAttempted(true);
11261
+ return;
11262
+ }
11263
+ void save();
11264
+ },
11200
11265
  children: mutation.saving ? "Saving\u2026" : field2 ? EDIT_FIELD_SAVE_LABEL : onEntity ? ADD_STANDARD_FIELD_SAVE_LABEL : ADD_FIELD_SAVE_LABEL
11201
11266
  }
11202
11267
  ),
11203
11268
  onCancel ? /* @__PURE__ */ jsx40(Button17, { size: "sm", variant: "ghost", onClick: onCancel, children: "Cancel" }) : null,
11204
- field2 ? /* @__PURE__ */ jsx40("div", { className: "ml-auto flex items-center gap-2", children: askingToRemove ? /* @__PURE__ */ jsxs35(Fragment12, { children: [
11205
- /* @__PURE__ */ jsxs35("span", { className: "text-destructive", children: [
11206
- archiveConfirmSentence(`the field \u201C${fieldName(field2)}\u201D`),
11207
- " Its values stay in every record and come back with it.",
11208
- linkReaders.length > 0 ? ` ${readersSentence(linkReaders.length, farTable.data?.name ?? null)}` : ""
11209
- ] }),
11210
- /* @__PURE__ */ jsx40(Button17, { size: "sm", variant: "destructive", disabled: mutation.saving, onClick: () => void remove(), children: "Remove it" }),
11211
- /* @__PURE__ */ jsx40(Button17, { size: "sm", variant: "ghost", onClick: () => setAskingToRemove(false), children: "Keep it" })
11212
- ] }) : /* @__PURE__ */ jsx40(Button17, { size: "sm", variant: "outline", onClick: () => setAskingToRemove(true), children: "Remove this field" }) }) : null
11269
+ /* @__PURE__ */ jsx40(
11270
+ "span",
11271
+ {
11272
+ id: "field-editor-missing",
11273
+ "data-field-editor-missing": missingShown ? "" : void 0,
11274
+ title: missingShown ?? void 0,
11275
+ className: "min-w-0 flex-1 truncate text-[11px] text-muted-foreground",
11276
+ children: missingShown
11277
+ }
11278
+ ),
11279
+ field2 ? (
11280
+ /* WALK3-FIX (2026-10-07): the confirm appears IN PLACE. Both states share one grid cell,
11281
+ the idle one drawn invisibly under the asking one and the other way round, so the
11282
+ footer is always as wide and as tall as its larger state: opening the confirm moved
11283
+ the dialog card 0.2278 and closing it 0.1303. The consequence is the tooltip of
11284
+ "Remove it" (interface-text: the slot holds a label, never a paragraph). */
11285
+ /* @__PURE__ */ jsxs35("div", { className: "ml-auto grid shrink-0 items-center", "data-field-editor-remove": askingToRemove ? "asking" : "idle", children: [
11286
+ /* @__PURE__ */ jsx40("div", { className: cn24("col-start-1 row-start-1 flex items-center justify-end gap-2", askingToRemove && "invisible"), "aria-hidden": askingToRemove || void 0, children: /* @__PURE__ */ jsx40(Button17, { size: "sm", variant: "outline", tabIndex: askingToRemove ? -1 : void 0, onClick: () => setAskingToRemove(true), children: "Remove this field" }) }),
11287
+ /* @__PURE__ */ jsxs35("div", { className: cn24("col-start-1 row-start-1 flex items-center justify-end gap-2", !askingToRemove && "invisible"), "aria-hidden": !askingToRemove || void 0, children: [
11288
+ /* @__PURE__ */ jsx40("span", { className: "whitespace-nowrap text-destructive", "data-field-editor-remove-ask": "", children: "Archive this field?" }),
11289
+ /* @__PURE__ */ jsx40(
11290
+ Button17,
11291
+ {
11292
+ size: "sm",
11293
+ variant: "destructive",
11294
+ tabIndex: askingToRemove ? void 0 : -1,
11295
+ disabled: mutation.saving,
11296
+ title: removeConsequence,
11297
+ "aria-description": removeConsequence,
11298
+ onClick: () => void remove(),
11299
+ children: "Remove it"
11300
+ }
11301
+ ),
11302
+ /* @__PURE__ */ jsx40(Button17, { size: "sm", variant: "ghost", tabIndex: askingToRemove ? void 0 : -1, onClick: () => setAskingToRemove(false), children: "Keep it" })
11303
+ ] })
11304
+ ] })
11305
+ ) : null
11213
11306
  ] })
11214
11307
  ] });
11215
11308
  }
@@ -13348,14 +13441,17 @@ function SummaryCell({
13348
13441
  value,
13349
13442
  rowCount,
13350
13443
  summaries,
13351
- onChoose
13444
+ onChoose,
13445
+ calculate = false
13352
13446
  }) {
13353
13447
  const offered = useMemo19(
13354
- () => summaries.operations ? measuresFor(field2, summaries.operations, firstColumn) : null,
13355
- [summaries.operations, field2, firstColumn]
13448
+ () => summaries.operations ? calculate ? calculationsFor(field2, summaries.operations).map((c) => c.op) : measuresFor(field2, summaries.operations, firstColumn) : null,
13449
+ [summaries.operations, field2, firstColumn, calculate]
13356
13450
  );
13357
- const label = chosen ? MEASURE_LABEL[chosen] ?? chosen : null;
13358
- const shown4 = chosen === void 0 ? null : summaries.loading && value === void 0 ? "\u2026" : value === null || value === void 0 ? "\u2014" : chosen === "count" || chosen === "filled" || chosen === "empty" || chosen === "unique" ? String(value) : null;
13451
+ const words2 = (op2, where) => calculate ? NOTION_CALCULATIONS.find((c) => c.op === op2)?.[where] ?? MEASURE_LABEL[op2] ?? op2 : MEASURE_LABEL[op2] ?? op2;
13452
+ const label = chosen ? words2(chosen, "footer") : null;
13453
+ const isDate4 = editorKindFor(field2) === "datetime";
13454
+ const shown4 = chosen === void 0 ? null : summaries.loading && value === void 0 ? "\u2026" : value === null || value === void 0 ? "\u2014" : chosen === "count" || chosen === "filled" || chosen === "empty" || chosen === "unique" ? String(value) : chosen === "percent_empty" || chosen === "percent_filled" ? `${Number(value.toFixed(2))}%` : chosen === "range" && isDate4 ? `${Math.round(value)} day${Math.round(value) === 1 ? "" : "s"}` : null;
13359
13455
  return /* @__PURE__ */ jsxs43(DropdownMenu, { onOpenChange: (open) => open ? summaries.askOperations() : void 0, children: [
13360
13456
  /* @__PURE__ */ jsx48(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx48(
13361
13457
  Button24,
@@ -13377,7 +13473,7 @@ function SummaryCell({
13377
13473
  // AGG-WITHHELD: the store withheld this column from her — say so, never a bare dash.
13378
13474
  /* @__PURE__ */ jsx48("span", { className: "font-medium", children: /* @__PURE__ */ jsx48(WithheldValue, { field: field2, notice: summaries.withheld[field2.key] }) })
13379
13475
  ) : shown4 !== null ? /* @__PURE__ */ jsx48("span", { className: "font-medium tabular-nums", children: shown4 }) : /* @__PURE__ */ jsx48("span", { className: "font-medium tabular-nums", children: /* @__PURE__ */ jsx48(RecordValue, { field: field2, value, format: formatForField(field2) }) })
13380
- ] }) : "Summarize"
13476
+ ] }) : calculate ? "Calculate" : "Summarize"
13381
13477
  }
13382
13478
  ) }),
13383
13479
  /* @__PURE__ */ jsxs43(DropdownMenuContent, { align: "start", children: [
@@ -13388,13 +13484,13 @@ function SummaryCell({
13388
13484
  "data-records-summary-choice": op2,
13389
13485
  onSelect: () => onChoose(op2),
13390
13486
  className: cn28(op2 === chosen && "font-medium"),
13391
- children: MEASURE_LABEL[op2] ?? op2
13487
+ children: words2(op2, "menu")
13392
13488
  },
13393
13489
  op2
13394
13490
  )),
13395
13491
  chosen ? /* @__PURE__ */ jsxs43(Fragment13, { children: [
13396
13492
  /* @__PURE__ */ jsx48(DropdownMenuSeparator, {}),
13397
- /* @__PURE__ */ jsx48(DropdownMenuItem, { onSelect: () => onChoose(null), children: "No summary" })
13493
+ /* @__PURE__ */ jsx48(DropdownMenuItem, { onSelect: () => onChoose(null), children: calculate ? "None" : "No summary" })
13398
13494
  ] }) : null
13399
13495
  ] })
13400
13496
  ] });
@@ -14113,6 +14209,13 @@ function useMergedGridControls(args) {
14113
14209
  const tableVersion = useTableVersion(client, tableId, table);
14114
14210
  const mutation = useFieldMutation2();
14115
14211
  const [dialog, setDialog] = useState42(null);
14212
+ const lastColumnDialog = useRef22(null);
14213
+ const insertEpoch = useRef22(0);
14214
+ if (dialog?.kind === "settings" || dialog?.kind === "insert") {
14215
+ if (dialog !== lastColumnDialog.current) insertEpoch.current += 1;
14216
+ lastColumnDialog.current = dialog;
14217
+ }
14218
+ const columnDialog = dialog?.kind === "settings" || dialog?.kind === "insert" ? dialog : lastColumnDialog.current;
14116
14219
  const [renameTo, setRenameTo] = useState42("");
14117
14220
  const [deleteBusy, setDeleteBusy] = useState42(false);
14118
14221
  const [labelling, setLabelling] = useState42(false);
@@ -14602,27 +14705,27 @@ function useMergedGridControls(args) {
14602
14705
  },
14603
14706
  children: /* @__PURE__ */ jsxs47(DialogContent8, { className: "max-h-[85vh] overflow-y-auto sm:max-w-2xl", children: [
14604
14707
  /* @__PURE__ */ jsxs47(DialogHeader8, { children: [
14605
- /* @__PURE__ */ jsx52(DialogTitle8, { children: dialog?.kind === "settings" ? `Column settings \xB7 ${fieldName(dialog.field)}` : dialog?.kind === "insert" && dialog.anchor ? `Add column ${dialog.side === "left" ? "left" : "right"} of ${fieldOf2(dialog.anchor) ? fieldName(fieldOf2(dialog.anchor)) : dialog.anchor}` : ADD_FIELD_OPEN_LABEL }),
14708
+ /* @__PURE__ */ jsx52(DialogTitle8, { children: columnDialog?.kind === "settings" ? `Column settings \xB7 ${fieldName(columnDialog.field)}` : columnDialog?.kind === "insert" && columnDialog.anchor ? `Add column ${columnDialog.side === "left" ? "left" : "right"} of ${fieldOf2(columnDialog.anchor) ? fieldName(fieldOf2(columnDialog.anchor)) : columnDialog.anchor}` : ADD_FIELD_OPEN_LABEL }),
14606
14709
  /* @__PURE__ */ jsx52(DialogDescription6, { className: "sr-only", children: "Name, type and look" })
14607
14710
  ] }),
14608
- dialog?.kind === "settings" ? /* @__PURE__ */ jsx52(
14711
+ columnDialog?.kind === "settings" ? /* @__PURE__ */ jsx52(
14609
14712
  FieldEditor,
14610
14713
  {
14611
14714
  tableId,
14612
- field: dialog.field,
14715
+ field: columnDialog.field,
14613
14716
  onSaved: () => {
14614
- notify?.success(`${fieldName(dialog.field)} is saved.`);
14717
+ notify?.success(`${fieldName(columnDialog.field)} is saved.`);
14615
14718
  close();
14616
14719
  },
14617
14720
  onRemoved: () => {
14618
- if (args.onColumnRetired) args.onColumnRetired(dialog.field);
14619
- else notify?.success(`${fieldName(dialog.field)} is gone from the table. It can be brought back from Archived.`);
14721
+ if (args.onColumnRetired) args.onColumnRetired(columnDialog.field);
14722
+ else notify?.success(`${fieldName(columnDialog.field)} is gone from the table. It can be brought back from Archived.`);
14620
14723
  close();
14621
14724
  },
14622
14725
  onCancel: close
14623
14726
  },
14624
- dialog.field.id
14625
- ) : dialog?.kind === "insert" ? /* @__PURE__ */ jsx52(
14727
+ `${columnDialog.field.id}-${insertEpoch.current}`
14728
+ ) : columnDialog?.kind === "insert" ? /* @__PURE__ */ jsx52(
14626
14729
  FieldEditor,
14627
14730
  {
14628
14731
  tableId,
@@ -14631,10 +14734,11 @@ function useMergedGridControls(args) {
14631
14734
  close();
14632
14735
  },
14633
14736
  onSaved: () => {
14634
- pendingInsert.current = { before: new Set(fieldKeys), anchor: dialog.anchor, side: dialog.side };
14737
+ pendingInsert.current = { before: new Set(fieldKeys), anchor: columnDialog.anchor, side: columnDialog.side };
14635
14738
  close();
14636
14739
  }
14637
- }
14740
+ },
14741
+ `insert-${insertEpoch.current}`
14638
14742
  ) : null
14639
14743
  ] })
14640
14744
  }
@@ -20204,7 +20308,7 @@ function UndoRedoPair({ face, onUndo, onRedo }) {
20204
20308
  )
20205
20309
  ] });
20206
20310
  }
20207
- function UndoNotice({ undo }) {
20311
+ function UndoNotice({ undo, placement = "frame" }) {
20208
20312
  const notice = undo.notice;
20209
20313
  const dismiss = undo.dismiss;
20210
20314
  useEffect44(() => {
@@ -20218,7 +20322,8 @@ function UndoNotice({ undo }) {
20218
20322
  {
20219
20323
  role: "status",
20220
20324
  "data-records-undo-notice": notice.refusal ? "refused" : "done",
20221
- className: "pointer-events-auto absolute bottom-3 left-1/2 z-30 flex max-w-[min(36rem,calc(100%-2rem))] -translate-x-1/2 items-center gap-2 rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-lg",
20325
+ "data-records-undo-placement": placement,
20326
+ className: `pointer-events-auto ${placement === "viewport" ? "fixed bottom-4 z-50" : "absolute bottom-3 z-30"} left-1/2 flex max-w-[min(36rem,calc(100%-2rem))] -translate-x-1/2 items-center gap-2 rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-lg`,
20222
20327
  children: [
20223
20328
  /* @__PURE__ */ jsx66("span", { children: notice.says }),
20224
20329
  notice.canUndo && undo.canUndo ? /* @__PURE__ */ jsx66(Button38, { size: "sm", variant: "outline", className: "ml-auto h-6", "data-records-undo-now": "", onClick: () => void undo.undo(), children: "Undo" }) : null,
@@ -21476,7 +21581,8 @@ function GridBody({
21476
21581
  newRowLabel,
21477
21582
  sortOverride,
21478
21583
  searchOverride,
21479
- onSearchChange
21584
+ onSearchChange,
21585
+ searchBox = true
21480
21586
  }) {
21481
21587
  const toolbarSlot = useTableToolbarSlot();
21482
21588
  const tableCard = useMatrxTableCard();
@@ -21572,6 +21678,11 @@ function GridBody({
21572
21678
  page: query.page
21573
21679
  });
21574
21680
  }, [reportQuery, query.sort, query.columnFilters, query.search, query.page, openingKey]);
21681
+ const liveKeys = useMemo35(() => fields.data ? new Set(fields.data.map((f) => f.key)) : null, [fields.data]);
21682
+ useEffect47(() => {
21683
+ if (!liveKeys) return;
21684
+ setQuery((held) => withoutGoneColumns(held, liveKeys, openingSort));
21685
+ }, [liveKeys]);
21575
21686
  const resetEpoch = address?.resetEpoch ?? 0;
21576
21687
  const resetSeen = useRef31(resetEpoch);
21577
21688
  useEffect47(() => {
@@ -21625,7 +21736,9 @@ function GridBody({
21625
21736
  page: classicInCard ? 0 : page,
21626
21737
  ...readFilter ? { filter: readFilter } : {}
21627
21738
  });
21628
- const pageAsked = !storePaged || Boolean(fields.data && !table.loading);
21739
+ const pageAskedFor = useRef31(null);
21740
+ if (fields.data && !table.loading) pageAskedFor.current = tableId;
21741
+ const pageAsked = !storePaged || Boolean(fields.data) && pageAskedFor.current === tableId;
21629
21742
  const serverRead = useRecordPage(storePaged && pageAsked ? tableId : null, {
21630
21743
  pageSize: query.pageSize,
21631
21744
  page: Math.max(0, query.page - 1),
@@ -22471,6 +22584,7 @@ function GridBody({
22471
22584
  value: summaries.values[columnId2],
22472
22585
  rowCount: summaries.rowCount,
22473
22586
  summaries,
22587
+ calculate: embedded,
22474
22588
  onChoose: (op2) => footerNow.current.choose(columnId2, op2)
22475
22589
  }
22476
22590
  );
@@ -22735,7 +22849,8 @@ function GridBody({
22735
22849
  [storePaged, query, queryTotal, classicInCard, records.data?.total]
22736
22850
  );
22737
22851
  const showAddLine = mayAddRow && rows.length > 0;
22738
- const showSummaryLine = rows.length > 0 && (embedded ? Object.keys(chosenSummaries).length > 0 : !merged || Object.keys(chosenSummaries).length > 0);
22852
+ const showSummaryLine = rows.length > 0 && (embedded || !merged || Object.keys(chosenSummaries).length > 0);
22853
+ const calculationChosen = Object.keys(chosenSummaries).length > 0;
22739
22854
  const footerFace = JSON.stringify([
22740
22855
  showAddLine ? added.busy : null,
22741
22856
  showSummaryLine ? [chosenSummaries, summaries.values, summaries.rowCount, summaries.loading] : null
@@ -22838,10 +22953,11 @@ function GridBody({
22838
22953
  search: !embedded,
22839
22954
  // EMBEDDED: Notion's magnifier — the design system's own search group, a button that
22840
22955
  // opens into a field. It is the same `query.search` the store's read asks.
22841
- ...embedded ? {
22956
+ ...embedded && searchBox ? {
22842
22957
  customSearch: /* @__PURE__ */ jsx70(
22843
22958
  TapTargetSearchGroup,
22844
22959
  {
22960
+ surface: "solid",
22845
22961
  placeholder: "Search",
22846
22962
  value: searchOverride ?? query.search,
22847
22963
  onValueChange: (term) => {
@@ -23053,7 +23169,8 @@ function GridBody({
23053
23169
  noun,
23054
23170
  embedded,
23055
23171
  searchOverride,
23056
- onSearchChange
23172
+ onSearchChange,
23173
+ searchBox
23057
23174
  ]
23058
23175
  );
23059
23176
  const labelOfField = useCallback26(
@@ -23073,7 +23190,7 @@ function GridBody({
23073
23190
  }, [rowsAnswered, rowMemoryKey, rows.length, embedded, tableId]);
23074
23191
  if (table.error) return /* @__PURE__ */ jsx70(RefusalNotice, { error: table.error, className });
23075
23192
  if (fields.error) return /* @__PURE__ */ jsx70(RefusalNotice, { error: fields.error, className });
23076
- const nothingToShowYet = table.loading || fields.loading || !rights.known || records.loading && rows.length === 0 || // The page read has neither answered nor refused: nothing has been said yet, so nothing is empty.
23193
+ const nothingToShowYet = table.loading && !table.data || fields.loading && !fields.data || !rights.known || records.loading && rows.length === 0 || // The page read has neither answered nor refused: nothing has been said yet, so nothing is empty.
23077
23194
  !records.data && !records.error || countedButNotReturned || manual && hand.loading && rows.length === 0;
23078
23195
  const refreshing = !nothingToShowYet && (records.loading || manual && hand.loading);
23079
23196
  const noColumnsYet = !nothingToShowYet && (fields.data ?? []).length === 0;
@@ -23275,7 +23392,7 @@ function GridBody({
23275
23392
  !look.wrap && "[&_tbody_td]:max-w-[28rem] [&_tbody_td]:overflow-hidden [&_tbody_td]:text-ellipsis [&_tbody_td]:whitespace-nowrap! [&_tbody_td_*]:whitespace-nowrap!",
23276
23393
  look.fit === "scroll" && "sm:[&>table]:w-max! sm:[&>table]:max-w-none! sm:[&>table]:min-w-full!",
23277
23394
  embedded && EMBEDDED_TABLE_CLASS,
23278
- embedded && !showSummaryLine && EMBEDDED_PLAIN_FOOTER_CLASS
23395
+ embedded && !calculationChosen && EMBEDDED_PLAIN_FOOTER_CLASS
23279
23396
  ),
23280
23397
  footerRows,
23281
23398
  ...embedded ? { copy: false } : copyConfig ? { copy: copyConfig } : {},
@@ -23528,6 +23645,15 @@ function withHeaderMenus(columns, menuFor, filterFor) {
23528
23645
  return filter ? { ...withMenu, ...filter } : withMenu;
23529
23646
  });
23530
23647
  }
23648
+ function withoutGoneColumns(query, liveKeys, openingSort) {
23649
+ const sortGone = Boolean(query.sort && !liveKeys.has(query.sort.id));
23650
+ const goneFilters = Object.keys(query.columnFilters ?? {}).filter((key) => !liveKeys.has(key));
23651
+ if (!sortGone && goneFilters.length === 0) return query;
23652
+ const columnFilters = { ...query.columnFilters };
23653
+ for (const key of goneFilters) delete columnFilters[key];
23654
+ const fallback = openingSort && liveKeys.has(openingSort.id) ? openingSort : null;
23655
+ return { ...query, sort: sortGone ? fallback : query.sort, columnFilters, page: 1 };
23656
+ }
23531
23657
 
23532
23658
  // src/RecordsTableCard.tsx
23533
23659
  import { MatrxTableCard, MatrxTableCardEmpty, MatrxTableCardShowMore as MatrxTableCardShowMore2, TABLE_CARD_DEFAULT_INITIAL_ROWS } from "@ai-matrx/design-system/data-table";
@@ -28751,7 +28877,7 @@ function RecordFieldsInPlace({
28751
28877
  /* @__PURE__ */ jsx94(
28752
28878
  "dd",
28753
28879
  {
28754
- className: "min-w-0 rounded hover:bg-muted/50 [&_[data-matrx-cell-editor]_input:not([type=checkbox]):not([type=radio])]:!h-7",
28880
+ className: "min-w-0 rounded hover:bg-muted/50 [&_[data-matrx-cell-editor]_input:not([type=checkbox]):not([type=radio])]:!h-7 [&_[data-pick-anchor]]:!w-fit",
28755
28881
  "data-records-in-place-field": field2.key,
28756
28882
  children: /* @__PURE__ */ jsx94(
28757
28883
  GridCell,
@@ -28769,7 +28895,7 @@ function RecordFieldsInPlace({
28769
28895
  )
28770
28896
  ] }, field2.id);
28771
28897
  }) }),
28772
- /* @__PURE__ */ jsx94(UndoNotice, { undo })
28898
+ /* @__PURE__ */ jsx94(UndoNotice, { undo, placement: "viewport" })
28773
28899
  ]
28774
28900
  }
28775
28901
  );
@@ -28995,6 +29121,7 @@ function Peek({ tableId, recordId, onClose, className, onPage = false }) {
28995
29121
  if (record.error) return /* @__PURE__ */ jsx97(RefusalNotice, { error: record.error, className });
28996
29122
  if (fields.error) return /* @__PURE__ */ jsx97(RefusalNotice, { error: fields.error, className });
28997
29123
  const document2 = record.data?.document;
29124
+ const computedShown = computedLinesWorthShowing(record.data?.computed, document2);
28998
29125
  const title = document2 ? recordNameIn(table.data, document2) : "";
28999
29126
  const bodyKey = onPage ? bodyFieldOf(fields.data ?? [])?.key : void 0;
29000
29127
  const shownFields = (fields.data ?? []).filter((field2) => !onPage || !drawnBelowOnPage(field2, bodyKey));
@@ -29094,15 +29221,20 @@ function Peek({ tableId, recordId, onClose, className, onPage = false }) {
29094
29221
  /* @__PURE__ */ jsx97(Separator9, {}),
29095
29222
  /* @__PURE__ */ jsx97("p", { className: "text-muted-foreground", children: "Nothing here can say which columns the system worked out for itself: that part of the record is not readable from your seat. Everything above is the record as it stands." })
29096
29223
  ] }) : null,
29097
- record.data?.computed?.length ? /* @__PURE__ */ jsxs88(Fragment36, { children: [
29224
+ computedShown.length ? /* @__PURE__ */ jsxs88(Fragment36, { children: [
29098
29225
  /* @__PURE__ */ jsx97(Separator9, {}),
29099
29226
  /* @__PURE__ */ jsxs88("div", { className: "flex flex-col gap-1", children: [
29100
29227
  /* @__PURE__ */ jsx97("span", { className: "font-medium", children: "Worked out by the system" }),
29101
- record.data.computed.map((row, i) => /* @__PURE__ */ jsx97("p", { className: "text-muted-foreground", children: computedInPlainWords(row, (fields.data ?? []).find((f) => f.key === row.field_key)) }, i))
29228
+ computedShown.map((row, i) => /* @__PURE__ */ jsx97("p", { className: "text-muted-foreground", children: computedInPlainWords(row, (fields.data ?? []).find((f) => f.key === row.field_key)) }, i))
29102
29229
  ] })
29103
29230
  ] }) : null
29104
29231
  ] });
29105
29232
  }
29233
+ function computedLinesWorthShowing(rows, document2) {
29234
+ return (rows ?? []).filter(
29235
+ (row) => !((row.value === null || row.value === void 0 || row.value === "") && workedOutErrorFor(document2, row.field_key))
29236
+ );
29237
+ }
29106
29238
  function computedInPlainWords(row, field2) {
29107
29239
  const name = field2 ? fieldName(field2) : humanizeIdentifier12(row.field_key);
29108
29240
  const shown4 = row.value === null || row.value === void 0 || row.value === "" ? "nothing" : field2 ? scalarText(field2, row.value) : String(row.value);
@@ -32732,6 +32864,7 @@ function ViewSwitcher({
32732
32864
  sortOverride,
32733
32865
  searchOverride,
32734
32866
  onSearchChange,
32867
+ searchBox,
32735
32868
  newRowLabel,
32736
32869
  className
32737
32870
  }) {
@@ -32799,6 +32932,7 @@ function ViewSwitcher({
32799
32932
  ...sortOverride !== void 0 ? { sortOverride } : {},
32800
32933
  ...searchOverride !== void 0 ? { searchOverride } : {},
32801
32934
  ...onSearchChange ? { onSearchChange } : {},
32935
+ ...searchBox === false ? { searchBox } : {},
32802
32936
  ...newRowLabel ? { newRowLabel } : {},
32803
32937
  ...filter ? { filter } : {},
32804
32938
  ...onClearFilter ? { onClearFilter } : {},
@@ -36248,7 +36382,8 @@ function HistoryPanel({ tableId, recordId, onAskAboutField, className }) {
36248
36382
  ref: listRef,
36249
36383
  role: "listbox",
36250
36384
  "aria-label": "Versions of this record, newest first",
36251
- className: "flex min-h-0 flex-col divide-y overflow-y-auto rounded border",
36385
+ "data-records-history-list": "",
36386
+ className: "flex max-h-[9.25rem] min-h-0 flex-col divide-y overflow-y-auto rounded border",
36252
36387
  onKeyDown: (event) => {
36253
36388
  const at = entries.findIndex((e) => e.version === open);
36254
36389
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
@@ -47577,6 +47712,7 @@ function TablePage({
47577
47712
  newRowLabel,
47578
47713
  searchOverride,
47579
47714
  onSearchChange,
47715
+ searchBox,
47580
47716
  pageSize: pageSizeAsked,
47581
47717
  seedViews,
47582
47718
  onLeave,
@@ -47680,7 +47816,7 @@ function TablePage({
47680
47816
  if (hostFromLink || opened.layout || layoutChosen) return;
47681
47817
  setHostLayout(face.hostLayout);
47682
47818
  }, [face.kind, face.kind === "designated" ? face.hostLayout : null, layoutChosen]);
47683
- const faceUnknown = !hostFromLink && !opened.layout && (views === void 0 || Array.isArray(views) && views.length > 0 && (view === null || looks === void 0));
47819
+ const faceUnknown = !hostFromLink && !opened.layout && (views === void 0 || looks === void 0 || Array.isArray(views) && views.length > 0 && view === null);
47684
47820
  const shownLayout = layoutFromLink ?? shown4?.layout ?? "grid";
47685
47821
  const shownFilter = filter ?? (shown4?.where ? shown4.where : null) ?? (shown4?.filters && Object.keys(shown4.filters).length > 0 ? shown4.filters : null);
47686
47822
  const press = (pressed) => {
@@ -48466,6 +48602,7 @@ function TablePage({
48466
48602
  ...newRowLabel ? { newRowLabel } : {},
48467
48603
  ...searchOverride !== void 0 ? { searchOverride } : {},
48468
48604
  ...onSearchChange ? { onSearchChange } : {},
48605
+ ...searchBox === false ? { searchBox } : {},
48469
48606
  footer,
48470
48607
  openExport: () => setRail("export"),
48471
48608
  view: {
@@ -48772,6 +48909,7 @@ function EntityTablePageBody({
48772
48909
  presentation = "page",
48773
48910
  searchOverride,
48774
48911
  onSearchChange,
48912
+ searchBox = true,
48775
48913
  pageSize = presentation === "embedded" ? EMBEDDED_PAGE_SIZE : 100,
48776
48914
  layout: opened = "grid",
48777
48915
  onLayoutChange,
@@ -48859,9 +48997,10 @@ function EntityTablePageBody({
48859
48997
  ] }) : null,
48860
48998
  addRefused ? /* @__PURE__ */ jsx156(ErrorBox9, { className: "rounded border border-destructive/40 px-1.5 py-1 text-xs text-destructive", message: addRefused, operation: "Add a row", origin: "records-ui/entityViews" }) : null,
48861
48999
  entity.error ? /* @__PURE__ */ jsx156("p", { className: "px-1 py-3 text-xs text-muted-foreground", children: `This table could not be read: ${entity.error}` }) : layout === "board" && boardable ? /* @__PURE__ */ jsx156(EntityBoard, { entity, groupBy, onOpenRecord }) : /* @__PURE__ */ jsxs145(Fragment65, { children: [
48862
- presentation === "embedded" ? /* @__PURE__ */ jsx156("div", { className: "flex min-w-0 items-center justify-end", "data-records-entity-search": "", children: /* @__PURE__ */ jsx156(TooltipProvider, { children: /* @__PURE__ */ jsx156(
49000
+ presentation === "embedded" && searchBox ? /* @__PURE__ */ jsx156("div", { className: "flex min-w-0 items-center justify-end", "data-records-entity-search": "", children: /* @__PURE__ */ jsx156(TooltipProvider, { children: /* @__PURE__ */ jsx156(
48863
49001
  TapTargetSearchGroup2,
48864
49002
  {
49003
+ surface: "solid",
48865
49004
  placeholder: "Search",
48866
49005
  value: term,
48867
49006
  onValueChange: (next) => {
@@ -48898,6 +49037,7 @@ function TablePage2(props) {
48898
49037
  ...rest.pageSize ? { pageSize: rest.pageSize } : {},
48899
49038
  ...rest.searchOverride !== void 0 ? { searchOverride: rest.searchOverride } : {},
48900
49039
  ...rest.onSearchChange ? { onSearchChange: rest.onSearchChange } : {},
49040
+ ...rest.searchBox === false ? { searchBox: false } : {},
48901
49041
  ...layout ? { layout } : {},
48902
49042
  ...onLayoutChange ? { onLayoutChange } : {},
48903
49043
  ...groupBy ? { groupBy } : {},