twico-ui 1.41.0 → 1.41.1

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.d.cts CHANGED
@@ -2051,10 +2051,18 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
2051
2051
  /** Controlled-rows callback: receives the full next rows array after an edit (client mode). */
2052
2052
  onRowsChange?: (rows: T[]) => void;
2053
2053
  /** Fired when the built-in batch editor applies columns across selected rows:
2054
- * (changedRows, patch, selectedKeys). The selection-toolbar "Edit" button appears
2055
- * automatically when there are editable columns. Server-mode caveat: `changedRows`
2056
- * resolves only rows on the currently loaded page; for cross-page selections use the
2057
- * complete `selectedKeys` array and apply the `patch` server-side. */
2054
+ * `(changedRows, patch, selectedKeys)`. The selection-toolbar "Edit" button appears
2055
+ * automatically when there are editable columns.
2056
+ * - `changedRows` is **authoritative**: each row carries exactly the fields it may change
2057
+ * (a per-row `editable` predicate (#424/#428) is applied per cell). Persist these for a
2058
+ * complete, cell-precise write of the loaded page.
2059
+ * - `patch` is the column-uniform union of the picked field values.
2060
+ * - `selectedKeys` are the keys a `keys × patch` write can safely apply the **whole** patch
2061
+ * to — loaded rows on which **every** picked column is editable (so it never touches a
2062
+ * locked cell), plus any selected key on an **unloaded** page (which can't be predicate-
2063
+ * tested client-side — you apply and enforce the predicate server-side). For a mixed
2064
+ * batch where a row is editable on only some picked columns, that row is in `changedRows`
2065
+ * but not `selectedKeys`; use `changedRows` for the exact per-cell result. */
2058
2066
  onBatchUpdate?: (changedRows: T[], patch: Record<string, any>, selectedKeys: Array<string | number>) => void;
2059
2067
  /** Render the built-in "Edit" button in the selection toolbar (the batch editor). Set `false` to
2060
2068
  * suppress it and ship your own batch-edit action via `batchActions` — otherwise you'd get two
package/dist/index.d.ts CHANGED
@@ -2051,10 +2051,18 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
2051
2051
  /** Controlled-rows callback: receives the full next rows array after an edit (client mode). */
2052
2052
  onRowsChange?: (rows: T[]) => void;
2053
2053
  /** Fired when the built-in batch editor applies columns across selected rows:
2054
- * (changedRows, patch, selectedKeys). The selection-toolbar "Edit" button appears
2055
- * automatically when there are editable columns. Server-mode caveat: `changedRows`
2056
- * resolves only rows on the currently loaded page; for cross-page selections use the
2057
- * complete `selectedKeys` array and apply the `patch` server-side. */
2054
+ * `(changedRows, patch, selectedKeys)`. The selection-toolbar "Edit" button appears
2055
+ * automatically when there are editable columns.
2056
+ * - `changedRows` is **authoritative**: each row carries exactly the fields it may change
2057
+ * (a per-row `editable` predicate (#424/#428) is applied per cell). Persist these for a
2058
+ * complete, cell-precise write of the loaded page.
2059
+ * - `patch` is the column-uniform union of the picked field values.
2060
+ * - `selectedKeys` are the keys a `keys × patch` write can safely apply the **whole** patch
2061
+ * to — loaded rows on which **every** picked column is editable (so it never touches a
2062
+ * locked cell), plus any selected key on an **unloaded** page (which can't be predicate-
2063
+ * tested client-side — you apply and enforce the predicate server-side). For a mixed
2064
+ * batch where a row is editable on only some picked columns, that row is in `changedRows`
2065
+ * but not `selectedKeys`; use `changedRows` for the exact per-cell result. */
2058
2066
  onBatchUpdate?: (changedRows: T[], patch: Record<string, any>, selectedKeys: Array<string | number>) => void;
2059
2067
  /** Render the built-in "Edit" button in the selection toolbar (the batch editor). Set `false` to
2060
2068
  * suppress it and ship your own batch-edit action via `batchActions` — otherwise you'd get two
package/dist/index.mjs CHANGED
@@ -420,7 +420,10 @@ var ICONBTN_CSS = `
420
420
  .twc-iconbtn[aria-pressed="true"][data-pressed-tone="warning"] { --_accent: var(--color-warning); --_accent-subtle: var(--color-warning-subtle); --_accent-subtle-fg: var(--color-warning-subtle-fg); }
421
421
  .twc-iconbtn[aria-pressed="true"][data-pressed-tone="info"] { --_accent: var(--color-info); --_accent-subtle: var(--color-info-subtle); --_accent-subtle-fg: var(--color-info-subtle-fg); }
422
422
  .twc-iconbtn[aria-pressed="true"][data-pressed-tone="danger"] { --_accent: var(--color-danger); --_accent-subtle: var(--color-danger-subtle); --_accent-subtle-fg: var(--color-danger-subtle-fg); }
423
- .twc-iconbtn[aria-pressed="true"][data-pressed-tone="neutral"] { --_accent: var(--color-border-strong); --_accent-subtle: var(--color-surface-sunken); --_accent-subtle-fg: var(--color-text); }
423
+ /* #430: color-surface-sunken steps toward the page background in dark mode, so a neutral pressed fill was
424
+ invisible on a surface panel. Fill with color-border (steps away from the background in both themes) + a
425
+ stronger color-text-subtle edge so the outline also carries the "on" state. Mirrors Button. */
426
+ .twc-iconbtn[aria-pressed="true"][data-pressed-tone="neutral"] { --_accent: var(--color-text-subtle); --_accent-subtle: var(--color-border); --_accent-subtle-fg: var(--color-text); }
424
427
  /* #418: keep the pressed look on hover (matching-specificity, after the variant hover rules). */
425
428
  .twc-iconbtn[aria-pressed="true"]:hover:not(:disabled) { background: var(--_accent-subtle); color: var(--_accent-subtle-fg); border-color: var(--_accent); filter: brightness(0.97); }
426
429
  .dark .twc-iconbtn[aria-pressed="true"]:hover:not(:disabled) { filter: brightness(1.25); }
@@ -1718,7 +1721,11 @@ var CSS2 = `
1718
1721
  .twc-btn[aria-pressed="true"][data-pressed-tone="warning"] { --_accent: var(--color-warning); --_accent-subtle: var(--color-warning-subtle); --_accent-subtle-fg: var(--color-warning-subtle-fg); }
1719
1722
  .twc-btn[aria-pressed="true"][data-pressed-tone="info"] { --_accent: var(--color-info); --_accent-subtle: var(--color-info-subtle); --_accent-subtle-fg: var(--color-info-subtle-fg); }
1720
1723
  .twc-btn[aria-pressed="true"][data-pressed-tone="danger"] { --_accent: var(--color-danger); --_accent-subtle: var(--color-danger-subtle); --_accent-subtle-fg: var(--color-danger-subtle-fg); }
1721
- .twc-btn[aria-pressed="true"][data-pressed-tone="neutral"] { --_accent: var(--color-border-strong); --_accent-subtle: var(--color-surface-sunken); --_accent-subtle-fg: var(--color-text); }
1724
+ /* #430: color-surface-sunken steps TOWARD the page background in dark mode (0b1222 under 0f172a), so a neutral
1725
+ pressed fill was invisible on a surface panel. Fill with color-border instead \u2014 it steps AWAY from the
1726
+ background in both themes (lighter in dark, darker in light) \u2014 and strengthen the edge with color-text-subtle
1727
+ so the outline carries the "on" state too (neutral has no meaning-colour to recolour the label). */
1728
+ .twc-btn[aria-pressed="true"][data-pressed-tone="neutral"] { --_accent: var(--color-text-subtle); --_accent-subtle: var(--color-border); --_accent-subtle-fg: var(--color-text); }
1722
1729
  /* #418: keep the pressed look under the pointer \u2014 the variant hover rules (0,4,0) otherwise beat the base
1723
1730
  pressed rule (0,2,0). This matching-specificity rule, source-ordered after them, restores the fill/fg/border
1724
1731
  (resetting the text colour too, so solid stays legible \u2014 incl. tone="danger"). */
@@ -10462,7 +10469,7 @@ function Datatable({
10462
10469
  );
10463
10470
  const effectiveHidden = React53__default.useMemo(() => combineSources.size ? /* @__PURE__ */ new Set([...hidden, ...combineSources]) : hidden, [hidden, combineSources]);
10464
10471
  const effectiveWrapped = React53__default.useMemo(() => stackCombineTargets.size ? /* @__PURE__ */ new Set([...wrapped, ...stackCombineTargets]) : wrapped, [wrapped, stackCombineTargets]);
10465
- const visibleCols = cols.filter((c) => !effectiveHidden.has(c.field));
10472
+ const visibleCols = React53__default.useMemo(() => cols.filter((c) => !effectiveHidden.has(c.field)), [cols, effectiveHidden]);
10466
10473
  const colFieldsKey = JSON.stringify(
10467
10474
  cols.filter((c) => typeof c.field === "string" && c.type !== "actions").map((c) => c.field)
10468
10475
  );
@@ -11337,12 +11344,18 @@ function Datatable({
11337
11344
  const revealOn = !!scrollActiveCellIntoView;
11338
11345
  const revealBlock = scrollActiveCellIntoView && typeof scrollActiveCellIntoView === "object" && scrollActiveCellIntoView.block === "center" ? "center" : "nearest";
11339
11346
  const revealInline = scrollActiveCellIntoView && typeof scrollActiveCellIntoView === "object" && scrollActiveCellIntoView.inline === "center" ? "center" : "nearest";
11347
+ const revealRi = activeCellVal ? keyIndex.get(activeCellVal.key) : void 0;
11348
+ const revealCi = activeCellVal ? cellColIndex(activeCellVal.field) : -1;
11349
+ const revealOffsetsRef = React53__default.useRef(offsets);
11350
+ revealOffsetsRef.current = offsets;
11351
+ const revealMidRef = React53__default.useRef(keyIndexMid);
11352
+ revealMidRef.current = keyIndexMid;
11340
11353
  React53__default.useEffect(() => {
11341
11354
  if (!revealOn || !activeCellVal) return void 0;
11342
11355
  const block = revealBlock, inline = revealInline;
11343
- const ri = keyIndex.get(activeCellVal.key), ci = cellColIndex(activeCellVal.field);
11356
+ const ri = revealRi, ci = revealCi;
11344
11357
  if (ri == null || ci < 0) return void 0;
11345
- let raf = 0;
11358
+ let raf = 0, tries = 0;
11346
11359
  const reveal = () => {
11347
11360
  const sc = scrollRef.current;
11348
11361
  if (!sc) return;
@@ -11368,10 +11381,12 @@ function Datatable({
11368
11381
  } else if (tR.left < scR.left + leftInset) sc.scrollLeft -= scR.left + leftInset - tR.left;
11369
11382
  else if (tR.right > scR.right - rightInset) sc.scrollLeft += tR.right - (scR.right - rightInset);
11370
11383
  }
11371
- } else if (virtualizing && offsets) {
11372
- const mi = keyIndexMid.get(activeCellVal.key);
11373
- if (mi != null && offsets[mi] != null) {
11374
- sc.scrollTop = Math.max(0, offsets[mi] - (block === "center" ? sc.clientHeight / 2 : headH));
11384
+ } else if (virtualizing && revealOffsetsRef.current) {
11385
+ const off = revealOffsetsRef.current;
11386
+ const mi = revealMidRef.current.get(activeCellVal.key);
11387
+ if (mi != null && off[mi] != null && tries < 30) {
11388
+ tries += 1;
11389
+ sc.scrollTop = Math.max(0, off[mi] - (block === "center" ? sc.clientHeight / 2 : headH));
11375
11390
  raf = requestAnimationFrame(reveal);
11376
11391
  }
11377
11392
  }
@@ -11380,7 +11395,7 @@ function Datatable({
11380
11395
  return () => {
11381
11396
  if (raf) cancelAnimationFrame(raf);
11382
11397
  };
11383
- }, [activeCellVal?.key, activeCellVal?.field, revealOn, revealBlock, revealInline, keyIndex, cellColIndex, virtualizing, offsets, keyIndexMid, headH]);
11398
+ }, [activeCellVal?.key, activeCellVal?.field, revealOn, revealBlock, revealInline, revealRi, revealCi, virtualizing, headH]);
11384
11399
  React53__default.useEffect(() => {
11385
11400
  if (!activeCellControlled || !activeCellVal) {
11386
11401
  jumpAppliedRef.current = null;
@@ -11472,6 +11487,10 @@ function Datatable({
11472
11487
  [cols, editMode, batchEditKey]
11473
11488
  // eslint-disable-line react-hooks/exhaustive-deps
11474
11489
  );
11490
+ const batchEditableColsSel = React53__default.useMemo(
11491
+ () => batchEditableCols.filter((c) => typeof c.editable !== "function" || selectedRows.some((r2) => c.editable(r2, { field: c.field }))),
11492
+ [batchEditableCols, selectedRows]
11493
+ );
11475
11494
  const hasBatchEditor = showBatchEdit && batchEditableCols.length > 0;
11476
11495
  const [batchEdit, setBatchEdit] = React53__default.useState(null);
11477
11496
  const [batchEditPos, openBatchEdit, closeBatchEdit] = useFloating();
@@ -11506,7 +11525,7 @@ function Datatable({
11506
11525
  }, [batchEdit, selected.size, closeBatchEdit]);
11507
11526
  function applyBatchEdit() {
11508
11527
  if (!batchEdit) return;
11509
- const active = batchEditableCols.filter((c) => batchEdit.fields[c.field]);
11528
+ const active = batchEditableColsSel.filter((c) => batchEdit.fields[c.field]);
11510
11529
  if (!active.length) {
11511
11530
  setBatchEdit(null);
11512
11531
  closeBatchEdit();
@@ -11522,20 +11541,27 @@ function Datatable({
11522
11541
  patch[c.field] = v;
11523
11542
  });
11524
11543
  const selKeys2 = new Set(selected);
11544
+ const loadedSel = /* @__PURE__ */ new Set();
11525
11545
  const changedRows = [];
11546
+ const safeKeys = [];
11526
11547
  const nextAll = rows.map((r2, i) => {
11527
11548
  const k = keyOf(r2, i);
11528
11549
  if (!selKeys2.has(k)) return r2;
11550
+ loadedSel.add(k);
11529
11551
  const rowPatch = {};
11552
+ let allEditable = true;
11530
11553
  for (const c of active) {
11531
11554
  if (typeof c.editable !== "function" || c.editable(r2, { field: c.field })) rowPatch[c.field] = patch[c.field];
11555
+ else allEditable = false;
11532
11556
  }
11557
+ if (allEditable) safeKeys.push(k);
11533
11558
  if (!Object.keys(rowPatch).length) return r2;
11534
11559
  const updated = { ...r2, ...rowPatch };
11535
11560
  changedRows.push(updated);
11536
11561
  return updated;
11537
11562
  });
11538
- onBatchUpdate?.(changedRows, patch, [...selected]);
11563
+ for (const k of selKeys2) if (!loadedSel.has(k)) safeKeys.push(k);
11564
+ onBatchUpdate?.(changedRows, patch, safeKeys);
11539
11565
  onRowsChange?.(nextAll);
11540
11566
  setBatchEdit(null);
11541
11567
  closeBatchEdit();
@@ -13078,8 +13104,8 @@ function Datatable({
13078
13104
  }
13079
13105
  ) : null,
13080
13106
  batchEdit && batchEditPos ? (() => {
13081
- const pickedCols = Object.keys(batchEdit.fields).filter((f) => batchEdit.fields[f]).map((f) => batchEditableCols.find((c) => c.field === f)).filter(Boolean);
13082
- const unpicked = batchEditableCols.filter((c) => !batchEdit.fields[c.field]);
13107
+ const pickedCols = Object.keys(batchEdit.fields).filter((f) => batchEdit.fields[f]).map((f) => batchEditableColsSel.find((c) => c.field === f)).filter(Boolean);
13108
+ const unpicked = batchEditableColsSel.filter((c) => !batchEdit.fields[c.field]);
13083
13109
  const addField = (f) => {
13084
13110
  if (f) setBatchEdit((b) => ({ ...b, fields: { ...b.fields, [f]: true }, values: { ...b.values, [f]: b.values[f] ?? "" } }));
13085
13111
  };
@@ -13111,7 +13137,11 @@ function Datatable({
13111
13137
  onChange: addField
13112
13138
  }
13113
13139
  ) }) : null,
13114
- /* @__PURE__ */ jsx("div", { className: "twc-dt__cfg-list", children: pickedCols.length === 0 ? /* @__PURE__ */ jsx("div", { className: "twc-dt__be-empty", children: "Pick a column to set its value on all selected rows." }) : pickedCols.map((c, i) => {
13140
+ /* @__PURE__ */ jsx("div", { className: "twc-dt__cfg-list", children: pickedCols.length === 0 ? (
13141
+ // #428: when a predicate leaves no column editable for the current selection, say so instead of
13142
+ // inviting a pick that can't be made (the "Add a column…" picker is hidden when unpicked is empty).
13143
+ /* @__PURE__ */ jsx("div", { className: "twc-dt__be-empty", children: unpicked.length ? "Pick a column to set its value on all selected rows." : "No columns are editable for the current selection." })
13144
+ ) : pickedCols.map((c, i) => {
13115
13145
  const opts = c.valueOptions ? c.valueOptions.map((o) => typeof o === "string" ? { value: o, label: o } : o) : null;
13116
13146
  return /* @__PURE__ */ jsxs("div", { className: "twc-dt__be-row", children: [
13117
13147
  /* @__PURE__ */ jsxs("span", { className: "twc-dt__be-name", title: colLabel(c), children: [