twico-ui 1.26.0 → 1.28.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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to Twico UI are documented here (newest first). This file is generated automatically on each release by [semantic-release](https://semantic-release.gitbook.io/) from [Conventional Commits](https://www.conventionalcommits.org/) — do not edit it by hand. The same notes appear on each [GitHub Release](https://github.com/EncryptionL/twico-ui/releases).
4
4
 
5
+ # [1.28.0](https://github.com/EncryptionL/twico-ui/compare/v1.27.0...v1.28.0) (2026-08-03)
6
+
7
+
8
+ ### Features
9
+
10
+ * **datatable:** selection-aware batch actions (predicate disabled/hidden) + onRowSelectionChange ([#322](https://github.com/EncryptionL/twico-ui/issues/322)) ([33a1c36](https://github.com/EncryptionL/twico-ui/commit/33a1c36340e36cf0c9cc763cda7ed5d26fd46baa))
11
+
12
+ # [1.27.0](https://github.com/EncryptionL/twico-ui/compare/v1.26.0...v1.27.0) (2026-08-03)
13
+
14
+
15
+ ### Features
16
+
17
+ * **datatable:** visible clipboard copy/cut feedback + onCellsCopy/onCellsPaste ([#320](https://github.com/EncryptionL/twico-ui/issues/320)) ([cff0aaa](https://github.com/EncryptionL/twico-ui/commit/cff0aaa1d31241c3772bc7c38c975d5033a9e6f7))
18
+
5
19
  # [1.26.0](https://github.com/EncryptionL/twico-ui/compare/v1.25.0...v1.26.0) (2026-08-03)
6
20
 
7
21
 
package/dist/index.cjs CHANGED
@@ -8159,7 +8159,14 @@ var DT_CSS = `
8159
8159
  /* Stay inside the parent even in a flex/grid container: min-width:0 lets a wide
8160
8160
  grid shrink (default min-width:auto would force it to its content width and
8161
8161
  overflow), so the internal __scroll handles horizontal scrolling. */
8162
- width: 100%; min-width: 0; max-width: 100%; }
8162
+ width: 100%; min-width: 0; max-width: 100%; position: relative; }
8163
+ /* #320: visible clipboard feedback \u2014 a transient in-grid toast + a brief flash on the copied/pasted cells,
8164
+ alongside the SR-only aria-live message (position: relative on the root anchors the toast; it does NOT
8165
+ create a containing block for the position: fixed popovers, so those stay viewport-anchored). */
8166
+ .twc-dt__clip-toast { position: absolute; inset-block-end: 12px; inset-inline-end: 12px; z-index: 6; background: var(--color-text); color: var(--color-surface); font-size: var(--text-xs); font-weight: var(--font-semibold); padding: 6px 12px; border-radius: var(--radius-md); box-shadow: var(--shadow-md); pointer-events: none; }
8167
+ @keyframes twc-dt-copyflash { from { box-shadow: inset 0 0 0 2px var(--color-primary); } to { box-shadow: inset 0 0 0 2px transparent; } }
8168
+ .twc-dt__td[data-copied="true"] { animation: twc-dt-copyflash 1.3s var(--ease-standard); }
8169
+ @media (prefers-reduced-motion: reduce) { .twc-dt__td[data-copied="true"] { animation: none; } }
8163
8170
 
8164
8171
  /* Toolbar */
8165
8172
  .twc-dt__toolbar { position: relative; z-index: 10; display: flex; align-items: center; gap: 6px; padding: 8px 10px;
@@ -9151,6 +9158,7 @@ function Datatable({
9151
9158
  onServerChange,
9152
9159
  onColumnVisibilityChange,
9153
9160
  batchActions = [],
9161
+ onRowSelectionChange,
9154
9162
  showExport = false,
9155
9163
  showDensity = false,
9156
9164
  showPivot = false,
@@ -9176,6 +9184,8 @@ function Datatable({
9176
9184
  onActiveCellChange,
9177
9185
  onCellSelectionChange,
9178
9186
  enableClipboard = false,
9187
+ onCellsCopy,
9188
+ onCellsPaste,
9179
9189
  showAggregation = false,
9180
9190
  ariaLabel = "Data table",
9181
9191
  "aria-label": ariaLabelAttr,
@@ -10401,6 +10411,18 @@ function Datatable({
10401
10411
  setFocus((f) => ({ r: Math.min(f.r, Math.max(0, leafRows.length - 1)), c: Math.min(f.c, Math.max(0, ordered.length - 1)) }));
10402
10412
  }, [leafRows.length, ordered.length, pageVal]);
10403
10413
  const selectedRows = React51__namespace.default.useMemo(() => rows.filter((r2, i) => selected.has(keyOf(r2, i))), [rows, selected]);
10414
+ const selKeys = React51__namespace.default.useMemo(() => [...selected], [selected]);
10415
+ const onRowSelRef = React51__namespace.default.useRef(onRowSelectionChange);
10416
+ onRowSelRef.current = onRowSelectionChange;
10417
+ const prevRowSelSigRef = React51__namespace.default.useRef("");
10418
+ React51__namespace.default.useEffect(() => {
10419
+ const cb = onRowSelRef.current;
10420
+ if (!cb) return;
10421
+ const sig = selKeys.join(",");
10422
+ if (sig === prevRowSelSigRef.current) return;
10423
+ prevRowSelSigRef.current = sig;
10424
+ cb(selKeys, selectedRows);
10425
+ }, [selKeys, selectedRows]);
10404
10426
  function clearSelection() {
10405
10427
  setSelected(/* @__PURE__ */ new Set());
10406
10428
  }
@@ -10497,11 +10519,11 @@ function Datatable({
10497
10519
  }
10498
10520
  patch[c.field] = v;
10499
10521
  });
10500
- const selKeys = new Set(selected);
10522
+ const selKeys2 = new Set(selected);
10501
10523
  const changedRows = [];
10502
10524
  const nextAll = rows.map((r2, i) => {
10503
10525
  const k = keyOf(r2, i);
10504
- if (!selKeys.has(k)) return r2;
10526
+ if (!selKeys2.has(k)) return r2;
10505
10527
  const updated = { ...r2, ...patch };
10506
10528
  changedRows.push(updated);
10507
10529
  return updated;
@@ -10513,6 +10535,21 @@ function Datatable({
10513
10535
  }
10514
10536
  const [clipboardMsg, setClipboardMsg] = React51__namespace.default.useState("");
10515
10537
  const clipboardRef = React51__namespace.default.useRef(null);
10538
+ const [clipFx, setClipFx] = React51__namespace.default.useState(null);
10539
+ const clipFxTimer = React51__namespace.default.useRef(null);
10540
+ const onCellsCopyRef = React51__namespace.default.useRef(onCellsCopy);
10541
+ onCellsCopyRef.current = onCellsCopy;
10542
+ const onCellsPasteRef = React51__namespace.default.useRef(onCellsPaste);
10543
+ onCellsPasteRef.current = onCellsPaste;
10544
+ React51__namespace.default.useEffect(() => () => {
10545
+ if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
10546
+ }, []);
10547
+ const announceClip = (msg, rect) => {
10548
+ setClipboardMsg(msg);
10549
+ setClipFx({ msg, rect: rect || null });
10550
+ if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
10551
+ clipFxTimer.current = setTimeout(() => setClipFx(null), 1600);
10552
+ };
10516
10553
  const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10517
10554
  const writeClipboard = (text) => {
10518
10555
  const execFallback = () => {
@@ -10577,6 +10614,7 @@ function Datatable({
10577
10614
  clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
10578
10615
  writeClipboard(tsv);
10579
10616
  const n = grid.reduce((a, line) => a + line.length, 0);
10617
+ onCellsCopyRef.current?.(grid.flatMap((line) => line.map((cell) => ({ key: cell.key, field: cell.field }))), { cut: !!cut });
10580
10618
  if (cut) {
10581
10619
  const patchByKey = /* @__PURE__ */ new Map();
10582
10620
  let clearable = 0;
@@ -10588,9 +10626,9 @@ function Datatable({
10588
10626
  patchByKey.set(cell.key, p);
10589
10627
  }
10590
10628
  writeCellPatches(patchByKey);
10591
- setClipboardMsg(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`);
10629
+ announceClip(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`, cellRect);
10592
10630
  } else {
10593
- setClipboardMsg(`Copied ${n} cell${n === 1 ? "" : "s"}`);
10631
+ announceClip(`Copied ${n} cell${n === 1 ? "" : "s"}`, cellRect);
10594
10632
  }
10595
10633
  };
10596
10634
  const pasteSelection = async () => {
@@ -10645,7 +10683,10 @@ function Datatable({
10645
10683
  }
10646
10684
  }
10647
10685
  writeCellPatches(patchByKey);
10648
- setClipboardMsg(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`);
10686
+ onCellsPasteRef.current?.({ written, skipped });
10687
+ const maxCols = matrix.reduce((a, m) => Math.max(a, m.length), 0);
10688
+ const pasteRect = written ? { r0: cellRect.r0, c0: cellRect.c0, r1: Math.min(cellRect.r0 + matrix.length - 1, leafRows.length - 1), c1: Math.min(cellRect.c0 + maxCols - 1, ordered.length - 1) } : null;
10689
+ announceClip(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`, pasteRect);
10649
10690
  };
10650
10691
  React51__namespace.default.useEffect(() => {
10651
10692
  if (!editing) return;
@@ -11179,6 +11220,7 @@ function Datatable({
11179
11220
  const isEditing = editing && editing.key === k && editing.field === c.field;
11180
11221
  const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
11181
11222
  const cellSelected = selectionMode === "cell" && cellRect && ri >= cellRect.r0 && ri <= cellRect.r1 && ci >= cellRect.c0 && ci <= cellRect.c1;
11223
+ const cellFlash = clipFx && clipFx.rect && ri >= clipFx.rect.r0 && ri <= clipFx.rect.r1 && ci >= clipFx.rect.c0 && ci <= clipFx.rect.c1;
11182
11224
  const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
11183
11225
  const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
11184
11226
  const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
@@ -11199,6 +11241,7 @@ function Datatable({
11199
11241
  "data-editing": isEditing || void 0,
11200
11242
  "data-cell-active": cellActive || void 0,
11201
11243
  "data-cell-selected": cellSelected || void 0,
11244
+ "data-copied": cellFlash || void 0,
11202
11245
  "data-wrap": wrapped.has(c.field) || void 0,
11203
11246
  "data-pin": st.pin,
11204
11247
  "data-pin-edge": st.edge,
@@ -11370,21 +11413,26 @@ function Datatable({
11370
11413
  /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.pencil }),
11371
11414
  "Edit"
11372
11415
  ] }) : null,
11373
- batchActions.map((a, i) => /* @__PURE__ */ jsxRuntime.jsxs(
11374
- "button",
11375
- {
11376
- type: "button",
11377
- className: "twc-dt__batch-btn",
11378
- "data-danger": a.danger || void 0,
11379
- disabled: a.disabled,
11380
- onClick: (e) => a.onClick?.([...selected], selectedRows, clearSelection, { anchorEl: e.currentTarget }),
11381
- children: [
11382
- a.icon,
11383
- a.label
11384
- ]
11385
- },
11386
- i
11387
- ))
11416
+ batchActions.map((a, i) => {
11417
+ const isHidden = typeof a.hidden === "function" ? a.hidden(selKeys, selectedRows) : !!a.hidden;
11418
+ if (isHidden) return null;
11419
+ const isDisabled = typeof a.disabled === "function" ? a.disabled(selKeys, selectedRows) : !!a.disabled;
11420
+ return /* @__PURE__ */ jsxRuntime.jsxs(
11421
+ "button",
11422
+ {
11423
+ type: "button",
11424
+ className: "twc-dt__batch-btn",
11425
+ "data-danger": a.danger || void 0,
11426
+ disabled: isDisabled,
11427
+ onClick: (e) => a.onClick?.(selKeys, selectedRows, clearSelection, { anchorEl: e.currentTarget }),
11428
+ children: [
11429
+ a.icon,
11430
+ a.label
11431
+ ]
11432
+ },
11433
+ i
11434
+ );
11435
+ })
11388
11436
  ] })
11389
11437
  ] }) : null,
11390
11438
  isDiff && diff.showSummary !== false ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-dt__diff-summary", "aria-label": "Diff summary", children: [
@@ -11561,6 +11609,7 @@ function Datatable({
11561
11609
  ] }) : null,
11562
11610
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
11563
11611
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: clipboardMsg }),
11612
+ clipFx ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__clip-toast", "aria-hidden": "true", children: clipFx.msg }) : null,
11564
11613
  pivotActive ? renderPivot() : /* @__PURE__ */ jsxRuntime.jsx(
11565
11614
  "div",
11566
11615
  {