twico-ui 1.25.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
@@ -1938,7 +1938,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1938
1938
  onStateChange?: (state: DatatableState) => void;
1939
1939
  /** Show a "Go to" page jumper in the footer when there are more than 5 pages. @default true */
1940
1940
  showPageJumper?: boolean;
1941
- /** Click-to-select mode: "row" highlights the clicked row, "cell" highlights a single cell. @default "none" */
1941
+ /** Click-to-select mode: "row" highlights the clicked row, "cell" enables spreadsheet-like cell selection.
1942
+ * In "cell" mode the grid supports keyboard navigation and **rectangular range selection** (#317):
1943
+ * Shift+Click / Shift+Arrow extend a range from an anchor; selected cells expose `aria-selected` and the
1944
+ * grid is `aria-multiselectable` with `aria-activedescendant` on the active cell. @default "none" */
1942
1945
  selectionMode?: "none" | "row" | "cell";
1943
1946
  /** Fired when a row is clicked in "row" selection mode: (row, key). */
1944
1947
  onRowClick?: (row: T, key: string | number) => void;
@@ -1946,6 +1949,22 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1946
1949
  onCellClick?: (value: any, row: T, field: string) => void;
1947
1950
  /** Fired when the active cell changes: ({ key, field } | null). */
1948
1951
  onActiveCellChange?: (cell: { key: string | number; field: string } | null) => void;
1952
+ /** #317: fired when the rectangular cell selection changes ("cell" mode) — every cell inside the
1953
+ * anchor→focus rectangle, in row-major order. Empty array when the selection clears. */
1954
+ onCellSelectionChange?: (cells: Array<{ key: string | number; field: string }>) => void;
1955
+ /** #318: enable spreadsheet clipboard on cells ("cell" mode): Ctrl/Cmd+C copies the active cell/range as
1956
+ * TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
1957
+ * cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
1958
+ * through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
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 */
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;
1949
1968
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1950
1969
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1951
1970
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
@@ -2157,6 +2176,11 @@ interface DatatableColumn<T = any> {
2157
2176
  loadValueOptions?: (query: string) => Promise<Array<string | { value: string; label: string }>>;
2158
2177
  /** Format the raw value to a string/number for display. */
2159
2178
  valueFormatter?: (value: any, row: T) => React.ReactNode;
2179
+ /** #318: opaque copy/paste compatibility token for the cell clipboard (`enableClipboard`). On an in-app
2180
+ * paste, a source cell is written to a target only when the two columns' `copyType` match — so e.g. a
2181
+ * "part-name" value never lands in a "measurement" column. Falls back to a number-vs-text bucket derived
2182
+ * from `type` when omitted. Examples: "text", "number", "part-name", "master:suppliers". */
2183
+ copyType?: string;
2160
2184
  /** Fully custom cell renderer (badges, avatars, etc.). */
2161
2185
  renderCell?: (value: any, row: T) => React.ReactNode;
2162
2186
  /** Diff-mode (#239) per-column equality override — return true when the two values are equal.
package/dist/index.d.ts CHANGED
@@ -1938,7 +1938,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1938
1938
  onStateChange?: (state: DatatableState) => void;
1939
1939
  /** Show a "Go to" page jumper in the footer when there are more than 5 pages. @default true */
1940
1940
  showPageJumper?: boolean;
1941
- /** Click-to-select mode: "row" highlights the clicked row, "cell" highlights a single cell. @default "none" */
1941
+ /** Click-to-select mode: "row" highlights the clicked row, "cell" enables spreadsheet-like cell selection.
1942
+ * In "cell" mode the grid supports keyboard navigation and **rectangular range selection** (#317):
1943
+ * Shift+Click / Shift+Arrow extend a range from an anchor; selected cells expose `aria-selected` and the
1944
+ * grid is `aria-multiselectable` with `aria-activedescendant` on the active cell. @default "none" */
1942
1945
  selectionMode?: "none" | "row" | "cell";
1943
1946
  /** Fired when a row is clicked in "row" selection mode: (row, key). */
1944
1947
  onRowClick?: (row: T, key: string | number) => void;
@@ -1946,6 +1949,22 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1946
1949
  onCellClick?: (value: any, row: T, field: string) => void;
1947
1950
  /** Fired when the active cell changes: ({ key, field } | null). */
1948
1951
  onActiveCellChange?: (cell: { key: string | number; field: string } | null) => void;
1952
+ /** #317: fired when the rectangular cell selection changes ("cell" mode) — every cell inside the
1953
+ * anchor→focus rectangle, in row-major order. Empty array when the selection clears. */
1954
+ onCellSelectionChange?: (cells: Array<{ key: string | number; field: string }>) => void;
1955
+ /** #318: enable spreadsheet clipboard on cells ("cell" mode): Ctrl/Cmd+C copies the active cell/range as
1956
+ * TSV, Ctrl/Cmd+X cuts (copy then clear), Ctrl/Cmd+V pastes onto the target rectangle from the active
1957
+ * cell. Paste is format-restricted by column `copyType` (incompatible cells are skipped) and commits
1958
+ * through `onRowUpdate`/`onRowsChange` in one batched change; outcomes are announced via `aria-live`.
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 */
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;
1949
1968
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1950
1969
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1951
1970
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
@@ -2157,6 +2176,11 @@ interface DatatableColumn<T = any> {
2157
2176
  loadValueOptions?: (query: string) => Promise<Array<string | { value: string; label: string }>>;
2158
2177
  /** Format the raw value to a string/number for display. */
2159
2178
  valueFormatter?: (value: any, row: T) => React.ReactNode;
2179
+ /** #318: opaque copy/paste compatibility token for the cell clipboard (`enableClipboard`). On an in-app
2180
+ * paste, a source cell is written to a target only when the two columns' `copyType` match — so e.g. a
2181
+ * "part-name" value never lands in a "measurement" column. Falls back to a number-vs-text bucket derived
2182
+ * from `type` when omitted. Examples: "text", "number", "part-name", "master:suppliers". */
2183
+ copyType?: string;
2160
2184
  /** Fully custom cell renderer (badges, avatars, etc.). */
2161
2185
  renderCell?: (value: any, row: T) => React.ReactNode;
2162
2186
  /** Diff-mode (#239) per-column equality override — return true when the two values are equal.
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;
@@ -8440,6 +8447,8 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8440
8447
  .twc-dt__row[data-selectable="true"] { cursor: pointer; }
8441
8448
  .twc-dt__row[data-active="true"] .twc-dt__td { background: var(--color-primary-subtle); box-shadow: inset 0 0 0 9999px transparent; }
8442
8449
  .twc-dt__row[data-active="true"] .twc-dt__td[data-pin] { background: var(--color-primary-subtle); }
8450
+ /* #317: data-cell-selected fills the range rectangle; data-cell-active keeps the ring on the moving corner. */
8451
+ .twc-dt__td[data-cell-selected="true"] { background: var(--color-primary-subtle); }
8443
8452
  .twc-dt__td[data-cell-active="true"] { box-shadow: inset 0 0 0 2px var(--color-primary); background: var(--color-primary-subtle); }
8444
8453
  .twc-dt__row:last-child .twc-dt__td { border-bottom: none; }
8445
8454
 
@@ -8594,6 +8603,9 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8594
8603
  .twc-dt__pop-grip:hover, .twc-dt__pop-grip:focus-visible, .twc-dt__pop-grip[data-active="true"] { opacity: 1; }
8595
8604
  .twc-dt__pop-grip:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
8596
8605
  [dir="rtl"] .twc-dt__pop-grip { cursor: nesw-resize; }
8606
+ /* #316: a second grip in the bottom-inline-start corner grows a right-anchored panel toward the empty side (right edge pinned). */
8607
+ .twc-dt__pop-grip[data-side="start"] { inset-inline-start: 2px; inset-inline-end: auto; cursor: nesw-resize; background: linear-gradient(225deg, transparent 0 55%, var(--color-border-strong) 55% 64%, transparent 64% 73%, var(--color-border-strong) 73% 82%, transparent 82%); }
8608
+ [dir="rtl"] .twc-dt__pop-grip[data-side="start"] { cursor: nwse-resize; }
8597
8609
  @media (pointer: coarse) {
8598
8610
  .twc-dt__f-rz { width: 16px; inset-inline-end: -6px; opacity: 0.4; }
8599
8611
  .twc-dt__pop-grip { width: 20px; height: 20px; opacity: 0.6; }
@@ -9148,6 +9160,10 @@ function Datatable({
9148
9160
  onRowClick,
9149
9161
  onCellClick,
9150
9162
  onActiveCellChange,
9163
+ onCellSelectionChange,
9164
+ enableClipboard = false,
9165
+ onCellsCopy,
9166
+ onCellsPaste,
9151
9167
  showAggregation = false,
9152
9168
  ariaLabel = "Data table",
9153
9169
  "aria-label": ariaLabelAttr,
@@ -9278,6 +9294,8 @@ function Datatable({
9278
9294
  const [selected, setSelected] = React51__default.useState(() => /* @__PURE__ */ new Set());
9279
9295
  const [activeRow, setActiveRow] = React51__default.useState(null);
9280
9296
  const [activeCell, setActiveCell] = React51__default.useState(null);
9297
+ const [anchorCell, setAnchorCell] = React51__default.useState(null);
9298
+ const gridId = React51__default.useId();
9281
9299
  const [internalPage, setInternalPage] = React51__default.useState(0);
9282
9300
  const [internalRpp, setInternalRpp] = React51__default.useState(pageSize > 0 ? pageSize : 10);
9283
9301
  const pageControlled = page !== void 0;
@@ -9377,6 +9395,7 @@ function Datatable({
9377
9395
  for (const [id, sz] of Object.entries(s.popoverSizes)) {
9378
9396
  if (!sz || typeof sz !== "object" || typeof sz.w !== "number" || typeof sz.h !== "number") continue;
9379
9397
  next[id] = { w: clampNum(sz.w, popMinW(id), fMaxW()), h: clampNum(sz.h, 120, fMaxH()) };
9398
+ if (typeof sz.left === "number") next[id].left = clampNum(sz.left, 8, fMaxW());
9380
9399
  }
9381
9400
  if (Object.keys(next).length) setPopSizes(next);
9382
9401
  }
@@ -9828,6 +9847,38 @@ function Datatable({
9828
9847
  middleRows.forEach((r2, i) => m.set(keyOf(r2, keyIndex.get(keyOf(r2)) ?? i), i));
9829
9848
  return m;
9830
9849
  }, [middleRows, keyIndex]);
9850
+ const cellColIndex = React51__default.useCallback((field) => ordered.findIndex((c) => c.field === field), [ordered]);
9851
+ const cellRect = React51__default.useMemo(() => {
9852
+ if (selectionMode !== "cell" || !activeCell) return null;
9853
+ const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
9854
+ if (ar == null || ac < 0) return null;
9855
+ const anchor = anchorCell || activeCell;
9856
+ const nr = keyIndex.get(anchor.key), nc = cellColIndex(anchor.field);
9857
+ const r0 = nr == null ? ar : Math.min(ar, nr), r1 = nr == null ? ar : Math.max(ar, nr);
9858
+ const c0 = nc < 0 ? ac : Math.min(ac, nc), c1 = nc < 0 ? ac : Math.max(ac, nc);
9859
+ return { r0, r1, c0, c1 };
9860
+ }, [selectionMode, activeCell, anchorCell, keyIndex, cellColIndex]);
9861
+ const activeCellId = React51__default.useMemo(() => {
9862
+ if (selectionMode !== "cell" || !activeCell) return void 0;
9863
+ const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
9864
+ return ar == null || ac < 0 ? void 0 : `${gridId}-${ar}-${ac}`;
9865
+ }, [selectionMode, activeCell, keyIndex, cellColIndex, gridId]);
9866
+ const onCellSelRef = React51__default.useRef(onCellSelectionChange);
9867
+ onCellSelRef.current = onCellSelectionChange;
9868
+ const prevSelSigRef = React51__default.useRef("");
9869
+ React51__default.useEffect(() => {
9870
+ const cb = onCellSelRef.current;
9871
+ if (selectionMode !== "cell" || !cb) return;
9872
+ const cells = [];
9873
+ if (cellRect) for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) for (let c = cellRect.c0; c <= cellRect.c1; c++) {
9874
+ const row = leafRows[r2], col = ordered[c];
9875
+ if (row && col) cells.push({ key: keyOf(row, r2), field: col.field });
9876
+ }
9877
+ const sig = cells.map((x) => `${x.key}|${x.field}`).join(",");
9878
+ if (sig === prevSelSigRef.current) return;
9879
+ prevSelSigRef.current = sig;
9880
+ cb(cells);
9881
+ }, [cellRect, selectionMode, leafRows, ordered]);
9831
9882
  const canReorderRows = rowReorder && !activeGroupBy.length && !sort;
9832
9883
  function onRowDrop(targetKey) {
9833
9884
  const fromKey = rowDrag.from;
@@ -10240,6 +10291,7 @@ function Datatable({
10240
10291
  const next = { key: k, field: col.field };
10241
10292
  setActiveCell(next);
10242
10293
  setActiveRow(k);
10294
+ if (!(e.shiftKey && anchorCell)) setAnchorCell(next);
10243
10295
  onCellClick?.(row[col.field], row, col.field);
10244
10296
  onActiveCellChange?.(next);
10245
10297
  }
@@ -10251,6 +10303,24 @@ function Datatable({
10251
10303
  if (editing) return;
10252
10304
  const td = e.target.closest(".twc-dt__td[data-r]");
10253
10305
  if (!td) return;
10306
+ if (enableClipboard && selectionMode === "cell" && (e.ctrlKey || e.metaKey)) {
10307
+ const k = e.key.toLowerCase();
10308
+ if (k === "c") {
10309
+ e.preventDefault();
10310
+ copySelection(false);
10311
+ return;
10312
+ }
10313
+ if (k === "x") {
10314
+ e.preventDefault();
10315
+ copySelection(true);
10316
+ return;
10317
+ }
10318
+ if (k === "v") {
10319
+ e.preventDefault();
10320
+ pasteSelection();
10321
+ return;
10322
+ }
10323
+ }
10254
10324
  let r2 = +td.getAttribute("data-r"), c = +td.getAttribute("data-c");
10255
10325
  const maxR = leafRows.length - 1, maxC = ordered.length - 1;
10256
10326
  let handled = true;
@@ -10303,6 +10373,16 @@ function Datatable({
10303
10373
  e.preventDefault();
10304
10374
  setFocus({ r: r2, c });
10305
10375
  focusCell(r2, c);
10376
+ if (selectionMode === "cell") {
10377
+ const nk = keyOf(leafRows[r2], r2), col = ordered[c];
10378
+ if (col) {
10379
+ const nc = { key: nk, field: col.field };
10380
+ setActiveCell(nc);
10381
+ setActiveRow(nk);
10382
+ if (!e.shiftKey) setAnchorCell(nc);
10383
+ onActiveCellChange?.(nc);
10384
+ }
10385
+ }
10306
10386
  }
10307
10387
  }
10308
10388
  React51__default.useEffect(() => {
@@ -10419,6 +10499,161 @@ function Datatable({
10419
10499
  setBatchEdit(null);
10420
10500
  closeBatchEdit();
10421
10501
  }
10502
+ const [clipboardMsg, setClipboardMsg] = React51__default.useState("");
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
+ };
10519
+ const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10520
+ const writeClipboard = (text) => {
10521
+ const execFallback = () => {
10522
+ try {
10523
+ if (typeof document !== "undefined") {
10524
+ const ta = document.createElement("textarea");
10525
+ ta.value = text;
10526
+ ta.style.position = "fixed";
10527
+ ta.style.opacity = "0";
10528
+ document.body.appendChild(ta);
10529
+ ta.focus();
10530
+ ta.select();
10531
+ document.execCommand("copy");
10532
+ document.body.removeChild(ta);
10533
+ }
10534
+ } catch {
10535
+ }
10536
+ };
10537
+ try {
10538
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text).catch(execFallback);
10539
+ } catch {
10540
+ }
10541
+ execFallback();
10542
+ return Promise.resolve();
10543
+ };
10544
+ const writeCellPatches = (patchByKey) => {
10545
+ if (!patchByKey.size) return 0;
10546
+ const changed = [];
10547
+ const nextAll = rows.map((r2, i) => {
10548
+ const k = keyOf(r2, i);
10549
+ const patch = patchByKey.get(k);
10550
+ if (!patch) return r2;
10551
+ const updated = { ...r2, ...patch };
10552
+ changed.push([updated, r2, patch]);
10553
+ return updated;
10554
+ });
10555
+ changed.forEach(([updated, orig, patch]) => {
10556
+ for (const f of Object.keys(patch)) onRowUpdate?.(updated, orig, f);
10557
+ });
10558
+ if (onRowsChange && changed.length) onRowsChange(nextAll);
10559
+ return changed.length;
10560
+ };
10561
+ const rectCells = () => {
10562
+ if (!cellRect) return null;
10563
+ const out = [];
10564
+ for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) {
10565
+ const row = leafRows[r2];
10566
+ if (!row) continue;
10567
+ const line = [];
10568
+ for (let c = cellRect.c0; c <= cellRect.c1; c++) {
10569
+ const col = ordered[c];
10570
+ if (col) line.push({ key: keyOf(row, r2), field: col.field, col, value: getColVal(col, row) });
10571
+ }
10572
+ out.push(line);
10573
+ }
10574
+ return out.length ? out : null;
10575
+ };
10576
+ const copySelection = (cut) => {
10577
+ const grid = rectCells();
10578
+ if (!grid) return;
10579
+ const tsv = grid.map((line) => line.map((cell) => cell.value == null ? "" : String(cell.value)).join(" ")).join("\n");
10580
+ clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
10581
+ writeClipboard(tsv);
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 });
10584
+ if (cut) {
10585
+ const patchByKey = /* @__PURE__ */ new Map();
10586
+ let clearable = 0;
10587
+ for (const line of grid) for (const cell of line) {
10588
+ if (!isColEditable(cell.col)) continue;
10589
+ clearable++;
10590
+ const p = patchByKey.get(cell.key) || {};
10591
+ p[cell.field] = cell.col.type === "number" ? null : "";
10592
+ patchByKey.set(cell.key, p);
10593
+ }
10594
+ writeCellPatches(patchByKey);
10595
+ announceClip(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`, cellRect);
10596
+ } else {
10597
+ announceClip(`Copied ${n} cell${n === 1 ? "" : "s"}`, cellRect);
10598
+ }
10599
+ };
10600
+ const pasteSelection = async () => {
10601
+ if (!cellRect) return;
10602
+ let text = null;
10603
+ try {
10604
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) text = await navigator.clipboard.readText();
10605
+ } catch {
10606
+ setClipboardMsg("Paste unavailable \u2014 clipboard permission denied");
10607
+ return;
10608
+ }
10609
+ if (text == null) {
10610
+ setClipboardMsg("Nothing to paste");
10611
+ return;
10612
+ }
10613
+ const inApp = clipboardRef.current && clipboardRef.current.tsv === text ? clipboardRef.current : null;
10614
+ let matrix;
10615
+ if (inApp) matrix = inApp.matrix;
10616
+ else {
10617
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
10618
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
10619
+ matrix = lines.map((line) => line.split(" "));
10620
+ }
10621
+ const patchByKey = /* @__PURE__ */ new Map();
10622
+ let written = 0, skipped = 0;
10623
+ for (let i = 0; i < matrix.length; i++) {
10624
+ const rr = cellRect.r0 + i;
10625
+ const row = leafRows[rr];
10626
+ if (!row) break;
10627
+ for (let j = 0; j < matrix[i].length; j++) {
10628
+ const cc = cellRect.c0 + j;
10629
+ const col = ordered[cc];
10630
+ if (!col) break;
10631
+ if (!isColEditable(col)) {
10632
+ skipped++;
10633
+ continue;
10634
+ }
10635
+ if (inApp && inApp.colTypes[j] != null && inApp.colTypes[j] !== copyTypeOf(col)) {
10636
+ skipped++;
10637
+ continue;
10638
+ }
10639
+ let v = matrix[i][j];
10640
+ if (col.type === "number") {
10641
+ v = v === "" ? null : Number(v);
10642
+ if (Number.isNaN(v)) v = null;
10643
+ }
10644
+ const k = keyOf(row, rr);
10645
+ const p = patchByKey.get(k) || {};
10646
+ p[col.field] = v;
10647
+ patchByKey.set(k, p);
10648
+ written++;
10649
+ }
10650
+ }
10651
+ writeCellPatches(patchByKey);
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);
10656
+ };
10422
10657
  React51__default.useEffect(() => {
10423
10658
  if (!editing) return;
10424
10659
  const onDown = (e) => {
@@ -10675,6 +10910,7 @@ function Datatable({
10675
10910
  if (node) {
10676
10911
  node.style.removeProperty("--twc-dt-pop-w");
10677
10912
  node.style.removeProperty("--twc-dt-pop-h");
10913
+ node.style.removeProperty("left");
10678
10914
  node.removeAttribute("data-pop-sized");
10679
10915
  }
10680
10916
  setPopSizes((s) => {
@@ -10684,7 +10920,7 @@ function Datatable({
10684
10920
  return n;
10685
10921
  });
10686
10922
  };
10687
- const startPopResize = (id) => (e) => {
10923
+ const startPopResize = (id, side) => (e) => {
10688
10924
  if (e.button != null && e.button !== 0) return;
10689
10925
  e.preventDefault();
10690
10926
  e.stopPropagation();
@@ -10693,21 +10929,27 @@ function Datatable({
10693
10929
  const grip = e.currentTarget;
10694
10930
  const startX = e.clientX, startY = e.clientY;
10695
10931
  const r2 = pop.getBoundingClientRect();
10696
- const startW = r2.width, startH = r2.height;
10932
+ const startW = r2.width, startH = r2.height, startLeft = r2.left, startRight = r2.right;
10697
10933
  const dir = typeof getComputedStyle !== "undefined" && getComputedStyle(pop).direction === "rtl" ? -1 : 1;
10934
+ const toStart = side === "start";
10698
10935
  pop.style.setProperty("--twc-dt-pop-w", `${startW}px`);
10699
10936
  pop.style.setProperty("--twc-dt-pop-h", `${startH}px`);
10700
10937
  grip.setAttribute("data-active", "true");
10701
10938
  pop.setAttribute("data-pop-sized", "");
10702
- let lw = startW, lh = startH;
10939
+ let lw = startW, lh = startH, ll = startLeft;
10703
10940
  const onMove = (ev) => {
10704
- lw = clampNum(startW + (ev.clientX - startX) * dir, popMinW(id), fMaxW());
10941
+ const dw = toStart ? (startX - ev.clientX) * dir : (ev.clientX - startX) * dir;
10942
+ lw = clampNum(startW + dw, popMinW(id), fMaxW());
10705
10943
  lh = clampNum(startH + (ev.clientY - startY), 120, fMaxH());
10706
10944
  pop.style.setProperty("--twc-dt-pop-w", `${lw}px`);
10707
10945
  pop.style.setProperty("--twc-dt-pop-h", `${lh}px`);
10946
+ if (toStart) {
10947
+ ll = Math.max(8, startRight - lw);
10948
+ pop.style.left = `${ll}px`;
10949
+ }
10708
10950
  };
10709
10951
  const onUp = () => {
10710
- setPopSizes((s) => ({ ...s, [id]: { w: lw, h: lh } }));
10952
+ setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w: lw, h: lh, ...toStart ? { left: ll } : {} } }));
10711
10953
  grip.removeAttribute("data-active");
10712
10954
  window.removeEventListener("pointermove", onMove);
10713
10955
  window.removeEventListener("pointerup", onUp);
@@ -10715,7 +10957,7 @@ function Datatable({
10715
10957
  window.addEventListener("pointermove", onMove);
10716
10958
  window.addEventListener("pointerup", onUp);
10717
10959
  };
10718
- const onPopGripKey = (id) => (e) => {
10960
+ const onPopGripKey = (id, side) => (e) => {
10719
10961
  if (e.key === "Enter" || e.key === " " || e.key === "Backspace") {
10720
10962
  e.preventDefault();
10721
10963
  clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
@@ -10740,25 +10982,54 @@ function Datatable({
10740
10982
  w = fMaxW();
10741
10983
  h = fMaxH();
10742
10984
  }
10743
- setPopSizes((s) => ({ ...s, [id]: { w: clampNum(w, popMinW(id), fMaxW()), h: clampNum(h, 120, fMaxH()) } }));
10985
+ w = clampNum(w, popMinW(id), fMaxW());
10986
+ h = clampNum(h, 120, fMaxH());
10987
+ const left = side === "start" ? Math.max(8, r2.right - w) : void 0;
10988
+ setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w, h, ...left != null ? { left } : {} } }));
10744
10989
  };
10745
- const popStyle = (id) => popSizes[id] ? { "--twc-dt-pop-w": `${popSizes[id].w}px`, "--twc-dt-pop-h": `${popSizes[id].h}px` } : null;
10746
- const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsx(
10747
- "span",
10748
- {
10749
- className: "twc-dt__pop-grip",
10750
- role: "slider",
10751
- tabIndex: 0,
10752
- "aria-label": "Resize panel (Enter or double-click to reset)",
10753
- "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
10754
- onPointerDown: startPopResize(id),
10755
- onKeyDown: onPopGripKey(id),
10756
- onDoubleClick: (e) => {
10757
- e.preventDefault();
10758
- clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
10990
+ const popStyle = (id) => {
10991
+ const s = popSizes[id];
10992
+ if (!s) return null;
10993
+ const st = { "--twc-dt-pop-w": `${s.w}px`, "--twc-dt-pop-h": `${s.h}px` };
10994
+ if (s.left != null) st.left = `${s.left}px`;
10995
+ return st;
10996
+ };
10997
+ const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsxs(Fragment, { children: [
10998
+ /* @__PURE__ */ jsx(
10999
+ "span",
11000
+ {
11001
+ className: "twc-dt__pop-grip",
11002
+ "data-side": "end",
11003
+ role: "slider",
11004
+ tabIndex: 0,
11005
+ "aria-label": "Resize panel (Enter or double-click to reset)",
11006
+ "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
11007
+ onPointerDown: startPopResize(id, "end"),
11008
+ onKeyDown: onPopGripKey(id, "end"),
11009
+ onDoubleClick: (e) => {
11010
+ e.preventDefault();
11011
+ clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
11012
+ }
10759
11013
  }
10760
- }
10761
- ) : null;
11014
+ ),
11015
+ /* @__PURE__ */ jsx(
11016
+ "span",
11017
+ {
11018
+ className: "twc-dt__pop-grip",
11019
+ "data-side": "start",
11020
+ role: "slider",
11021
+ tabIndex: 0,
11022
+ "aria-label": "Resize panel leftward (Enter or double-click to reset)",
11023
+ "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
11024
+ onPointerDown: startPopResize(id, "start"),
11025
+ onKeyDown: onPopGripKey(id, "start"),
11026
+ onDoubleClick: (e) => {
11027
+ e.preventDefault();
11028
+ clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
11029
+ }
11030
+ }
11031
+ )
11032
+ ] }) : null;
10762
11033
  const fieldHandle = (field, rowIdx) => resizableFilters && rowIdx === 0 ? /* @__PURE__ */ jsx(
10763
11034
  "span",
10764
11035
  {
@@ -10914,22 +11185,29 @@ function Datatable({
10914
11185
  const editable = isColEditable(c);
10915
11186
  const isEditing = editing && editing.key === k && editing.field === c.field;
10916
11187
  const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
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;
11190
+ const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
10917
11191
  const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
10918
11192
  const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
10919
11193
  return /* @__PURE__ */ jsxs(
10920
11194
  "td",
10921
11195
  {
11196
+ id: cellId,
10922
11197
  className: "twc-dt__td",
10923
11198
  role: "gridcell",
10924
11199
  "data-r": ri,
10925
11200
  "data-c": ci,
10926
11201
  "aria-colindex": ci + 1 + (checkboxSelection ? 1 : 0),
10927
11202
  tabIndex: focus.r === ri && focus.c === ci ? 0 : -1,
11203
+ "aria-selected": cellSelected || void 0,
10928
11204
  "data-num": c.type === "number" || void 0,
10929
11205
  "data-actions": isActions || void 0,
10930
11206
  "data-editable": editable && !isEditing || void 0,
10931
11207
  "data-editing": isEditing || void 0,
10932
11208
  "data-cell-active": cellActive || void 0,
11209
+ "data-cell-selected": cellSelected || void 0,
11210
+ "data-copied": cellFlash || void 0,
10933
11211
  "data-wrap": wrapped.has(c.field) || void 0,
10934
11212
  "data-pin": st.pin,
10935
11213
  "data-pin-edge": st.edge,
@@ -11291,6 +11569,8 @@ function Datatable({
11291
11569
  /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__groupbar-clear", onClick: () => setGroupBy([]), children: "Clear all" })
11292
11570
  ] }) : null,
11293
11571
  /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
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,
11294
11574
  pivotActive ? renderPivot() : /* @__PURE__ */ jsx(
11295
11575
  "div",
11296
11576
  {
@@ -11312,6 +11592,8 @@ function Datatable({
11312
11592
  "aria-label": ariaLabelAttr || ariaLabel,
11313
11593
  "aria-rowcount": totalRows + 1,
11314
11594
  "aria-colcount": ordered.length + (checkboxSelection ? 1 : 0),
11595
+ "aria-multiselectable": selectionMode === "cell" || void 0,
11596
+ "aria-activedescendant": activeCellId,
11315
11597
  "aria-busy": loading || void 0,
11316
11598
  onKeyDown: onGridKeyDown,
11317
11599
  children: [