twico-ui 1.26.0 → 1.27.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,13 @@
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.27.0](https://github.com/EncryptionL/twico-ui/compare/v1.26.0...v1.27.0) (2026-08-03)
6
+
7
+
8
+ ### Features
9
+
10
+ * **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))
11
+
5
12
  # [1.26.0](https://github.com/EncryptionL/twico-ui/compare/v1.25.0...v1.26.0) (2026-08-03)
6
13
 
7
14
 
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;
@@ -9176,6 +9183,8 @@ function Datatable({
9176
9183
  onActiveCellChange,
9177
9184
  onCellSelectionChange,
9178
9185
  enableClipboard = false,
9186
+ onCellsCopy,
9187
+ onCellsPaste,
9179
9188
  showAggregation = false,
9180
9189
  ariaLabel = "Data table",
9181
9190
  "aria-label": ariaLabelAttr,
@@ -10513,6 +10522,21 @@ function Datatable({
10513
10522
  }
10514
10523
  const [clipboardMsg, setClipboardMsg] = React51__namespace.default.useState("");
10515
10524
  const clipboardRef = React51__namespace.default.useRef(null);
10525
+ const [clipFx, setClipFx] = React51__namespace.default.useState(null);
10526
+ const clipFxTimer = React51__namespace.default.useRef(null);
10527
+ const onCellsCopyRef = React51__namespace.default.useRef(onCellsCopy);
10528
+ onCellsCopyRef.current = onCellsCopy;
10529
+ const onCellsPasteRef = React51__namespace.default.useRef(onCellsPaste);
10530
+ onCellsPasteRef.current = onCellsPaste;
10531
+ React51__namespace.default.useEffect(() => () => {
10532
+ if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
10533
+ }, []);
10534
+ const announceClip = (msg, rect) => {
10535
+ setClipboardMsg(msg);
10536
+ setClipFx({ msg, rect: rect || null });
10537
+ if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
10538
+ clipFxTimer.current = setTimeout(() => setClipFx(null), 1600);
10539
+ };
10516
10540
  const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10517
10541
  const writeClipboard = (text) => {
10518
10542
  const execFallback = () => {
@@ -10577,6 +10601,7 @@ function Datatable({
10577
10601
  clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
10578
10602
  writeClipboard(tsv);
10579
10603
  const n = grid.reduce((a, line) => a + line.length, 0);
10604
+ onCellsCopyRef.current?.(grid.flatMap((line) => line.map((cell) => ({ key: cell.key, field: cell.field }))), { cut: !!cut });
10580
10605
  if (cut) {
10581
10606
  const patchByKey = /* @__PURE__ */ new Map();
10582
10607
  let clearable = 0;
@@ -10588,9 +10613,9 @@ function Datatable({
10588
10613
  patchByKey.set(cell.key, p);
10589
10614
  }
10590
10615
  writeCellPatches(patchByKey);
10591
- setClipboardMsg(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`);
10616
+ announceClip(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`, cellRect);
10592
10617
  } else {
10593
- setClipboardMsg(`Copied ${n} cell${n === 1 ? "" : "s"}`);
10618
+ announceClip(`Copied ${n} cell${n === 1 ? "" : "s"}`, cellRect);
10594
10619
  }
10595
10620
  };
10596
10621
  const pasteSelection = async () => {
@@ -10645,7 +10670,10 @@ function Datatable({
10645
10670
  }
10646
10671
  }
10647
10672
  writeCellPatches(patchByKey);
10648
- setClipboardMsg(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`);
10673
+ onCellsPasteRef.current?.({ written, skipped });
10674
+ const maxCols = matrix.reduce((a, m) => Math.max(a, m.length), 0);
10675
+ 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;
10676
+ announceClip(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`, pasteRect);
10649
10677
  };
10650
10678
  React51__namespace.default.useEffect(() => {
10651
10679
  if (!editing) return;
@@ -11179,6 +11207,7 @@ function Datatable({
11179
11207
  const isEditing = editing && editing.key === k && editing.field === c.field;
11180
11208
  const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
11181
11209
  const cellSelected = selectionMode === "cell" && cellRect && ri >= cellRect.r0 && ri <= cellRect.r1 && ci >= cellRect.c0 && ci <= cellRect.c1;
11210
+ const cellFlash = clipFx && clipFx.rect && ri >= clipFx.rect.r0 && ri <= clipFx.rect.r1 && ci >= clipFx.rect.c0 && ci <= clipFx.rect.c1;
11182
11211
  const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
11183
11212
  const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
11184
11213
  const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
@@ -11199,6 +11228,7 @@ function Datatable({
11199
11228
  "data-editing": isEditing || void 0,
11200
11229
  "data-cell-active": cellActive || void 0,
11201
11230
  "data-cell-selected": cellSelected || void 0,
11231
+ "data-copied": cellFlash || void 0,
11202
11232
  "data-wrap": wrapped.has(c.field) || void 0,
11203
11233
  "data-pin": st.pin,
11204
11234
  "data-pin-edge": st.edge,
@@ -11561,6 +11591,7 @@ function Datatable({
11561
11591
  ] }) : null,
11562
11592
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
11563
11593
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: clipboardMsg }),
11594
+ clipFx ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__clip-toast", "aria-hidden": "true", children: clipFx.msg }) : null,
11564
11595
  pivotActive ? renderPivot() : /* @__PURE__ */ jsxRuntime.jsx(
11565
11596
  "div",
11566
11597
  {