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/dist/index.d.cts CHANGED
@@ -1843,6 +1843,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1843
1843
  * full row objects server-side rather than relying on `selectedRows`.
1844
1844
  */
1845
1845
  batchActions?: DatatableBatchAction<T>[];
1846
+ /** #322: fired when the checkbox row selection changes — `(keys, rows)`. The row-mode analogue of
1847
+ * `onCellSelectionChange`, so a consumer can track the selection and drive a batch action's own
1848
+ * `disabled`/`hidden` (which also accept predicates). */
1849
+ onRowSelectionChange?: (keys: Array<string | number>, rows: T[]) => void;
1846
1850
  /** Row height preset. With `showDensity`, the toolbar density button cycles it locally; changing this prop re-applies it. @default "comfortable" */
1847
1851
  density?: "compact" | "standard" | "comfortable";
1848
1852
  /**
@@ -1956,8 +1960,15 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1956
1960
  * TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
1957
1961
  * cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
1958
1962
  * through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
1959
- * @default false */
1963
+ * Copy/cut/paste also show a brief **visible** in-grid confirmation toast + a flash on the affected cells
1964
+ * (#320), so sighted users get feedback too. @default false */
1960
1965
  enableClipboard?: boolean;
1966
+ /** #320: fired on a clipboard copy/cut with the copied cells (row-major `{key,field}[]`) and `{ cut }`.
1967
+ * Use it to surface your own toast / integrate an app notification system. */
1968
+ onCellsCopy?: (cells: Array<{ key: string | number; field: string }>, meta: { cut: boolean }) => void;
1969
+ /** #320: fired after a clipboard paste with the outcome — `written` cells committed, `skipped` cells
1970
+ * rejected (read-only or incompatible `copyType`). */
1971
+ onCellsPaste?: (result: { written: number; skipped: number }) => void;
1961
1972
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1962
1973
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1963
1974
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
@@ -2222,7 +2233,12 @@ interface DatatableBatchAction<T = any> {
2222
2233
  ) => void;
2223
2234
  /** Render in danger color. */
2224
2235
  danger?: boolean;
2225
- disabled?: boolean;
2236
+ /** #322: disable the action — a static boolean, or a predicate evaluated against the current selection so
2237
+ * an action that only applies to a subset of the selected rows can grey out when it doesn't apply. */
2238
+ disabled?: boolean | ((keys: Array<string | number>, rows: T[]) => boolean);
2239
+ /** #322: hide the action entirely when the predicate returns true (e.g. `(_, rows) => rows.every(r => r.isGroup)`
2240
+ * drops "Mark as new thread" when only group rows are selected). A static `true` always hides it. */
2241
+ hidden?: boolean | ((keys: Array<string | number>, rows: T[]) => boolean);
2226
2242
  }
2227
2243
 
2228
2244
  declare function Datatable<T = any>(props: DatatableProps<T>): React.JSX.Element;
package/dist/index.d.ts CHANGED
@@ -1843,6 +1843,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1843
1843
  * full row objects server-side rather than relying on `selectedRows`.
1844
1844
  */
1845
1845
  batchActions?: DatatableBatchAction<T>[];
1846
+ /** #322: fired when the checkbox row selection changes — `(keys, rows)`. The row-mode analogue of
1847
+ * `onCellSelectionChange`, so a consumer can track the selection and drive a batch action's own
1848
+ * `disabled`/`hidden` (which also accept predicates). */
1849
+ onRowSelectionChange?: (keys: Array<string | number>, rows: T[]) => void;
1846
1850
  /** Row height preset. With `showDensity`, the toolbar density button cycles it locally; changing this prop re-applies it. @default "comfortable" */
1847
1851
  density?: "compact" | "standard" | "comfortable";
1848
1852
  /**
@@ -1956,8 +1960,15 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1956
1960
  * TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
1957
1961
  * cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
1958
1962
  * through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
1959
- * @default false */
1963
+ * Copy/cut/paste also show a brief **visible** in-grid confirmation toast + a flash on the affected cells
1964
+ * (#320), so sighted users get feedback too. @default false */
1960
1965
  enableClipboard?: boolean;
1966
+ /** #320: fired on a clipboard copy/cut with the copied cells (row-major `{key,field}[]`) and `{ cut }`.
1967
+ * Use it to surface your own toast / integrate an app notification system. */
1968
+ onCellsCopy?: (cells: Array<{ key: string | number; field: string }>, meta: { cut: boolean }) => void;
1969
+ /** #320: fired after a clipboard paste with the outcome — `written` cells committed, `skipped` cells
1970
+ * rejected (read-only or incompatible `copyType`). */
1971
+ onCellsPaste?: (result: { written: number; skipped: number }) => void;
1961
1972
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1962
1973
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1963
1974
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
@@ -2222,7 +2233,12 @@ interface DatatableBatchAction<T = any> {
2222
2233
  ) => void;
2223
2234
  /** Render in danger color. */
2224
2235
  danger?: boolean;
2225
- disabled?: boolean;
2236
+ /** #322: disable the action — a static boolean, or a predicate evaluated against the current selection so
2237
+ * an action that only applies to a subset of the selected rows can grey out when it doesn't apply. */
2238
+ disabled?: boolean | ((keys: Array<string | number>, rows: T[]) => boolean);
2239
+ /** #322: hide the action entirely when the predicate returns true (e.g. `(_, rows) => rows.every(r => r.isGroup)`
2240
+ * drops "Mark as new thread" when only group rows are selected). A static `true` always hides it. */
2241
+ hidden?: boolean | ((keys: Array<string | number>, rows: T[]) => boolean);
2226
2242
  }
2227
2243
 
2228
2244
  declare function Datatable<T = any>(props: DatatableProps<T>): React.JSX.Element;
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;
@@ -9130,6 +9137,7 @@ function Datatable({
9130
9137
  onServerChange,
9131
9138
  onColumnVisibilityChange,
9132
9139
  batchActions = [],
9140
+ onRowSelectionChange,
9133
9141
  showExport = false,
9134
9142
  showDensity = false,
9135
9143
  showPivot = false,
@@ -9155,6 +9163,8 @@ function Datatable({
9155
9163
  onActiveCellChange,
9156
9164
  onCellSelectionChange,
9157
9165
  enableClipboard = false,
9166
+ onCellsCopy,
9167
+ onCellsPaste,
9158
9168
  showAggregation = false,
9159
9169
  ariaLabel = "Data table",
9160
9170
  "aria-label": ariaLabelAttr,
@@ -10380,6 +10390,18 @@ function Datatable({
10380
10390
  setFocus((f) => ({ r: Math.min(f.r, Math.max(0, leafRows.length - 1)), c: Math.min(f.c, Math.max(0, ordered.length - 1)) }));
10381
10391
  }, [leafRows.length, ordered.length, pageVal]);
10382
10392
  const selectedRows = React51__default.useMemo(() => rows.filter((r2, i) => selected.has(keyOf(r2, i))), [rows, selected]);
10393
+ const selKeys = React51__default.useMemo(() => [...selected], [selected]);
10394
+ const onRowSelRef = React51__default.useRef(onRowSelectionChange);
10395
+ onRowSelRef.current = onRowSelectionChange;
10396
+ const prevRowSelSigRef = React51__default.useRef("");
10397
+ React51__default.useEffect(() => {
10398
+ const cb = onRowSelRef.current;
10399
+ if (!cb) return;
10400
+ const sig = selKeys.join(",");
10401
+ if (sig === prevRowSelSigRef.current) return;
10402
+ prevRowSelSigRef.current = sig;
10403
+ cb(selKeys, selectedRows);
10404
+ }, [selKeys, selectedRows]);
10383
10405
  function clearSelection() {
10384
10406
  setSelected(/* @__PURE__ */ new Set());
10385
10407
  }
@@ -10476,11 +10498,11 @@ function Datatable({
10476
10498
  }
10477
10499
  patch[c.field] = v;
10478
10500
  });
10479
- const selKeys = new Set(selected);
10501
+ const selKeys2 = new Set(selected);
10480
10502
  const changedRows = [];
10481
10503
  const nextAll = rows.map((r2, i) => {
10482
10504
  const k = keyOf(r2, i);
10483
- if (!selKeys.has(k)) return r2;
10505
+ if (!selKeys2.has(k)) return r2;
10484
10506
  const updated = { ...r2, ...patch };
10485
10507
  changedRows.push(updated);
10486
10508
  return updated;
@@ -10492,6 +10514,21 @@ function Datatable({
10492
10514
  }
10493
10515
  const [clipboardMsg, setClipboardMsg] = React51__default.useState("");
10494
10516
  const clipboardRef = React51__default.useRef(null);
10517
+ const [clipFx, setClipFx] = React51__default.useState(null);
10518
+ const clipFxTimer = React51__default.useRef(null);
10519
+ const onCellsCopyRef = React51__default.useRef(onCellsCopy);
10520
+ onCellsCopyRef.current = onCellsCopy;
10521
+ const onCellsPasteRef = React51__default.useRef(onCellsPaste);
10522
+ onCellsPasteRef.current = onCellsPaste;
10523
+ React51__default.useEffect(() => () => {
10524
+ if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
10525
+ }, []);
10526
+ const announceClip = (msg, rect) => {
10527
+ setClipboardMsg(msg);
10528
+ setClipFx({ msg, rect: rect || null });
10529
+ if (clipFxTimer.current) clearTimeout(clipFxTimer.current);
10530
+ clipFxTimer.current = setTimeout(() => setClipFx(null), 1600);
10531
+ };
10495
10532
  const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10496
10533
  const writeClipboard = (text) => {
10497
10534
  const execFallback = () => {
@@ -10556,6 +10593,7 @@ function Datatable({
10556
10593
  clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
10557
10594
  writeClipboard(tsv);
10558
10595
  const n = grid.reduce((a, line) => a + line.length, 0);
10596
+ onCellsCopyRef.current?.(grid.flatMap((line) => line.map((cell) => ({ key: cell.key, field: cell.field }))), { cut: !!cut });
10559
10597
  if (cut) {
10560
10598
  const patchByKey = /* @__PURE__ */ new Map();
10561
10599
  let clearable = 0;
@@ -10567,9 +10605,9 @@ function Datatable({
10567
10605
  patchByKey.set(cell.key, p);
10568
10606
  }
10569
10607
  writeCellPatches(patchByKey);
10570
- setClipboardMsg(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`);
10608
+ announceClip(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`, cellRect);
10571
10609
  } else {
10572
- setClipboardMsg(`Copied ${n} cell${n === 1 ? "" : "s"}`);
10610
+ announceClip(`Copied ${n} cell${n === 1 ? "" : "s"}`, cellRect);
10573
10611
  }
10574
10612
  };
10575
10613
  const pasteSelection = async () => {
@@ -10624,7 +10662,10 @@ function Datatable({
10624
10662
  }
10625
10663
  }
10626
10664
  writeCellPatches(patchByKey);
10627
- setClipboardMsg(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`);
10665
+ onCellsPasteRef.current?.({ written, skipped });
10666
+ const maxCols = matrix.reduce((a, m) => Math.max(a, m.length), 0);
10667
+ 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;
10668
+ announceClip(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`, pasteRect);
10628
10669
  };
10629
10670
  React51__default.useEffect(() => {
10630
10671
  if (!editing) return;
@@ -11158,6 +11199,7 @@ function Datatable({
11158
11199
  const isEditing = editing && editing.key === k && editing.field === c.field;
11159
11200
  const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
11160
11201
  const cellSelected = selectionMode === "cell" && cellRect && ri >= cellRect.r0 && ri <= cellRect.r1 && ci >= cellRect.c0 && ci <= cellRect.c1;
11202
+ const cellFlash = clipFx && clipFx.rect && ri >= clipFx.rect.r0 && ri <= clipFx.rect.r1 && ci >= clipFx.rect.c0 && ci <= clipFx.rect.c1;
11161
11203
  const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
11162
11204
  const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
11163
11205
  const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
@@ -11178,6 +11220,7 @@ function Datatable({
11178
11220
  "data-editing": isEditing || void 0,
11179
11221
  "data-cell-active": cellActive || void 0,
11180
11222
  "data-cell-selected": cellSelected || void 0,
11223
+ "data-copied": cellFlash || void 0,
11181
11224
  "data-wrap": wrapped.has(c.field) || void 0,
11182
11225
  "data-pin": st.pin,
11183
11226
  "data-pin-edge": st.edge,
@@ -11349,21 +11392,26 @@ function Datatable({
11349
11392
  /* @__PURE__ */ jsx(Svg, { d: I.pencil }),
11350
11393
  "Edit"
11351
11394
  ] }) : null,
11352
- batchActions.map((a, i) => /* @__PURE__ */ jsxs(
11353
- "button",
11354
- {
11355
- type: "button",
11356
- className: "twc-dt__batch-btn",
11357
- "data-danger": a.danger || void 0,
11358
- disabled: a.disabled,
11359
- onClick: (e) => a.onClick?.([...selected], selectedRows, clearSelection, { anchorEl: e.currentTarget }),
11360
- children: [
11361
- a.icon,
11362
- a.label
11363
- ]
11364
- },
11365
- i
11366
- ))
11395
+ batchActions.map((a, i) => {
11396
+ const isHidden = typeof a.hidden === "function" ? a.hidden(selKeys, selectedRows) : !!a.hidden;
11397
+ if (isHidden) return null;
11398
+ const isDisabled = typeof a.disabled === "function" ? a.disabled(selKeys, selectedRows) : !!a.disabled;
11399
+ return /* @__PURE__ */ jsxs(
11400
+ "button",
11401
+ {
11402
+ type: "button",
11403
+ className: "twc-dt__batch-btn",
11404
+ "data-danger": a.danger || void 0,
11405
+ disabled: isDisabled,
11406
+ onClick: (e) => a.onClick?.(selKeys, selectedRows, clearSelection, { anchorEl: e.currentTarget }),
11407
+ children: [
11408
+ a.icon,
11409
+ a.label
11410
+ ]
11411
+ },
11412
+ i
11413
+ );
11414
+ })
11367
11415
  ] })
11368
11416
  ] }) : null,
11369
11417
  isDiff && diff.showSummary !== false ? /* @__PURE__ */ jsxs("span", { className: "twc-dt__diff-summary", "aria-label": "Diff summary", children: [
@@ -11540,6 +11588,7 @@ function Datatable({
11540
11588
  ] }) : null,
11541
11589
  /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
11542
11590
  /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: clipboardMsg }),
11591
+ clipFx ? /* @__PURE__ */ jsx("div", { className: "twc-dt__clip-toast", "aria-hidden": "true", children: clipFx.msg }) : null,
11543
11592
  pivotActive ? renderPivot() : /* @__PURE__ */ jsx(
11544
11593
  "div",
11545
11594
  {