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/dist/index.d.cts CHANGED
@@ -1956,8 +1956,15 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1956
1956
  * TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
1957
1957
  * cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
1958
1958
  * through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
1959
- * @default false */
1959
+ * Copy/cut/paste also show a brief **visible** in-grid confirmation toast + a flash on the affected cells
1960
+ * (#320), so sighted users get feedback too. @default false */
1960
1961
  enableClipboard?: boolean;
1962
+ /** #320: fired on a clipboard copy/cut with the copied cells (row-major `{key,field}[]`) and `{ cut }`.
1963
+ * Use it to surface your own toast / integrate an app notification system. */
1964
+ onCellsCopy?: (cells: Array<{ key: string | number; field: string }>, meta: { cut: boolean }) => void;
1965
+ /** #320: fired after a clipboard paste with the outcome — `written` cells committed, `skipped` cells
1966
+ * rejected (read-only or incompatible `copyType`). */
1967
+ onCellsPaste?: (result: { written: number; skipped: number }) => void;
1961
1968
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1962
1969
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1963
1970
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
package/dist/index.d.ts CHANGED
@@ -1956,8 +1956,15 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1956
1956
  * TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
1957
1957
  * cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
1958
1958
  * through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
1959
- * @default false */
1959
+ * Copy/cut/paste also show a brief **visible** in-grid confirmation toast + a flash on the affected cells
1960
+ * (#320), so sighted users get feedback too. @default false */
1960
1961
  enableClipboard?: boolean;
1962
+ /** #320: fired on a clipboard copy/cut with the copied cells (row-major `{key,field}[]`) and `{ cut }`.
1963
+ * Use it to surface your own toast / integrate an app notification system. */
1964
+ onCellsCopy?: (cells: Array<{ key: string | number; field: string }>, meta: { cut: boolean }) => void;
1965
+ /** #320: fired after a clipboard paste with the outcome — `written` cells committed, `skipped` cells
1966
+ * rejected (read-only or incompatible `copyType`). */
1967
+ onCellsPaste?: (result: { written: number; skipped: number }) => void;
1961
1968
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1962
1969
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1963
1970
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
package/dist/index.mjs CHANGED
@@ -8138,7 +8138,14 @@ var DT_CSS = `
8138
8138
  /* Stay inside the parent even in a flex/grid container: min-width:0 lets a wide
8139
8139
  grid shrink (default min-width:auto would force it to its content width and
8140
8140
  overflow), so the internal __scroll handles horizontal scrolling. */
8141
- width: 100%; min-width: 0; max-width: 100%; }
8141
+ width: 100%; min-width: 0; max-width: 100%; position: relative; }
8142
+ /* #320: visible clipboard feedback \u2014 a transient in-grid toast + a brief flash on the copied/pasted cells,
8143
+ alongside the SR-only aria-live message (position: relative on the root anchors the toast; it does NOT
8144
+ create a containing block for the position: fixed popovers, so those stay viewport-anchored). */
8145
+ .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; }
8146
+ @keyframes twc-dt-copyflash { from { box-shadow: inset 0 0 0 2px var(--color-primary); } to { box-shadow: inset 0 0 0 2px transparent; } }
8147
+ .twc-dt__td[data-copied="true"] { animation: twc-dt-copyflash 1.3s var(--ease-standard); }
8148
+ @media (prefers-reduced-motion: reduce) { .twc-dt__td[data-copied="true"] { animation: none; } }
8142
8149
 
8143
8150
  /* Toolbar */
8144
8151
  .twc-dt__toolbar { position: relative; z-index: 10; display: flex; align-items: center; gap: 6px; padding: 8px 10px;
@@ -9155,6 +9162,8 @@ function Datatable({
9155
9162
  onActiveCellChange,
9156
9163
  onCellSelectionChange,
9157
9164
  enableClipboard = false,
9165
+ onCellsCopy,
9166
+ onCellsPaste,
9158
9167
  showAggregation = false,
9159
9168
  ariaLabel = "Data table",
9160
9169
  "aria-label": ariaLabelAttr,
@@ -10492,6 +10501,21 @@ function Datatable({
10492
10501
  }
10493
10502
  const [clipboardMsg, setClipboardMsg] = React51__default.useState("");
10494
10503
  const clipboardRef = React51__default.useRef(null);
10504
+ const [clipFx, setClipFx] = React51__default.useState(null);
10505
+ const clipFxTimer = React51__default.useRef(null);
10506
+ const onCellsCopyRef = React51__default.useRef(onCellsCopy);
10507
+ onCellsCopyRef.current = onCellsCopy;
10508
+ const onCellsPasteRef = React51__default.useRef(onCellsPaste);
10509
+ onCellsPasteRef.current = onCellsPaste;
10510
+ React51__default.useEffect(() => () => {
10511
+ if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
10512
+ }, []);
10513
+ const announceClip = (msg, rect) => {
10514
+ setClipboardMsg(msg);
10515
+ setClipFx({ msg, rect: rect || null });
10516
+ if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
10517
+ clipFxTimer.current = setTimeout(() => setClipFx(null), 1600);
10518
+ };
10495
10519
  const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10496
10520
  const writeClipboard = (text) => {
10497
10521
  const execFallback = () => {
@@ -10556,6 +10580,7 @@ function Datatable({
10556
10580
  clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
10557
10581
  writeClipboard(tsv);
10558
10582
  const n = grid.reduce((a, line) => a + line.length, 0);
10583
+ onCellsCopyRef.current?.(grid.flatMap((line) => line.map((cell) => ({ key: cell.key, field: cell.field }))), { cut: !!cut });
10559
10584
  if (cut) {
10560
10585
  const patchByKey = /* @__PURE__ */ new Map();
10561
10586
  let clearable = 0;
@@ -10567,9 +10592,9 @@ function Datatable({
10567
10592
  patchByKey.set(cell.key, p);
10568
10593
  }
10569
10594
  writeCellPatches(patchByKey);
10570
- setClipboardMsg(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`);
10595
+ announceClip(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`, cellRect);
10571
10596
  } else {
10572
- setClipboardMsg(`Copied ${n} cell${n === 1 ? "" : "s"}`);
10597
+ announceClip(`Copied ${n} cell${n === 1 ? "" : "s"}`, cellRect);
10573
10598
  }
10574
10599
  };
10575
10600
  const pasteSelection = async () => {
@@ -10624,7 +10649,10 @@ function Datatable({
10624
10649
  }
10625
10650
  }
10626
10651
  writeCellPatches(patchByKey);
10627
- setClipboardMsg(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`);
10652
+ onCellsPasteRef.current?.({ written, skipped });
10653
+ const maxCols = matrix.reduce((a, m) => Math.max(a, m.length), 0);
10654
+ 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;
10655
+ announceClip(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`, pasteRect);
10628
10656
  };
10629
10657
  React51__default.useEffect(() => {
10630
10658
  if (!editing) return;
@@ -11158,6 +11186,7 @@ function Datatable({
11158
11186
  const isEditing = editing && editing.key === k && editing.field === c.field;
11159
11187
  const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
11160
11188
  const cellSelected = selectionMode === "cell" && cellRect && ri >= cellRect.r0 && ri <= cellRect.r1 && ci >= cellRect.c0 && ci <= cellRect.c1;
11189
+ const cellFlash = clipFx && clipFx.rect && ri >= clipFx.rect.r0 && ri <= clipFx.rect.r1 && ci >= clipFx.rect.c0 && ci <= clipFx.rect.c1;
11161
11190
  const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
11162
11191
  const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
11163
11192
  const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
@@ -11178,6 +11207,7 @@ function Datatable({
11178
11207
  "data-editing": isEditing || void 0,
11179
11208
  "data-cell-active": cellActive || void 0,
11180
11209
  "data-cell-selected": cellSelected || void 0,
11210
+ "data-copied": cellFlash || void 0,
11181
11211
  "data-wrap": wrapped.has(c.field) || void 0,
11182
11212
  "data-pin": st.pin,
11183
11213
  "data-pin-edge": st.edge,
@@ -11540,6 +11570,7 @@ function Datatable({
11540
11570
  ] }) : null,
11541
11571
  /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
11542
11572
  /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: clipboardMsg }),
11573
+ clipFx ? /* @__PURE__ */ jsx("div", { className: "twc-dt__clip-toast", "aria-hidden": "true", children: clipFx.msg }) : null,
11543
11574
  pivotActive ? renderPivot() : /* @__PURE__ */ jsx(
11544
11575
  "div",
11545
11576
  {