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/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.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
+
12
+ # [1.26.0](https://github.com/EncryptionL/twico-ui/compare/v1.25.0...v1.26.0) (2026-08-03)
13
+
14
+
15
+ ### Features
16
+
17
+ * **datatable:** bottom-left popover grip, cell range selection, keyboard clipboard ([#316](https://github.com/EncryptionL/twico-ui/issues/316), [#317](https://github.com/EncryptionL/twico-ui/issues/317), [#318](https://github.com/EncryptionL/twico-ui/issues/318)) ([0c1a583](https://github.com/EncryptionL/twico-ui/commit/0c1a583ef8b983410547f7f1fb92f8660ec926c3))
18
+
5
19
  # [1.25.0](https://github.com/EncryptionL/twico-ui/compare/v1.24.0...v1.25.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;
@@ -8461,6 +8468,8 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8461
8468
  .twc-dt__row[data-selectable="true"] { cursor: pointer; }
8462
8469
  .twc-dt__row[data-active="true"] .twc-dt__td { background: var(--color-primary-subtle); box-shadow: inset 0 0 0 9999px transparent; }
8463
8470
  .twc-dt__row[data-active="true"] .twc-dt__td[data-pin] { background: var(--color-primary-subtle); }
8471
+ /* #317: data-cell-selected fills the range rectangle; data-cell-active keeps the ring on the moving corner. */
8472
+ .twc-dt__td[data-cell-selected="true"] { background: var(--color-primary-subtle); }
8464
8473
  .twc-dt__td[data-cell-active="true"] { box-shadow: inset 0 0 0 2px var(--color-primary); background: var(--color-primary-subtle); }
8465
8474
  .twc-dt__row:last-child .twc-dt__td { border-bottom: none; }
8466
8475
 
@@ -8615,6 +8624,9 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8615
8624
  .twc-dt__pop-grip:hover, .twc-dt__pop-grip:focus-visible, .twc-dt__pop-grip[data-active="true"] { opacity: 1; }
8616
8625
  .twc-dt__pop-grip:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
8617
8626
  [dir="rtl"] .twc-dt__pop-grip { cursor: nesw-resize; }
8627
+ /* #316: a second grip in the bottom-inline-start corner grows a right-anchored panel toward the empty side (right edge pinned). */
8628
+ .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%); }
8629
+ [dir="rtl"] .twc-dt__pop-grip[data-side="start"] { cursor: nwse-resize; }
8618
8630
  @media (pointer: coarse) {
8619
8631
  .twc-dt__f-rz { width: 16px; inset-inline-end: -6px; opacity: 0.4; }
8620
8632
  .twc-dt__pop-grip { width: 20px; height: 20px; opacity: 0.6; }
@@ -9169,6 +9181,10 @@ function Datatable({
9169
9181
  onRowClick,
9170
9182
  onCellClick,
9171
9183
  onActiveCellChange,
9184
+ onCellSelectionChange,
9185
+ enableClipboard = false,
9186
+ onCellsCopy,
9187
+ onCellsPaste,
9172
9188
  showAggregation = false,
9173
9189
  ariaLabel = "Data table",
9174
9190
  "aria-label": ariaLabelAttr,
@@ -9299,6 +9315,8 @@ function Datatable({
9299
9315
  const [selected, setSelected] = React51__namespace.default.useState(() => /* @__PURE__ */ new Set());
9300
9316
  const [activeRow, setActiveRow] = React51__namespace.default.useState(null);
9301
9317
  const [activeCell, setActiveCell] = React51__namespace.default.useState(null);
9318
+ const [anchorCell, setAnchorCell] = React51__namespace.default.useState(null);
9319
+ const gridId = React51__namespace.default.useId();
9302
9320
  const [internalPage, setInternalPage] = React51__namespace.default.useState(0);
9303
9321
  const [internalRpp, setInternalRpp] = React51__namespace.default.useState(pageSize > 0 ? pageSize : 10);
9304
9322
  const pageControlled = page !== void 0;
@@ -9398,6 +9416,7 @@ function Datatable({
9398
9416
  for (const [id, sz] of Object.entries(s.popoverSizes)) {
9399
9417
  if (!sz || typeof sz !== "object" || typeof sz.w !== "number" || typeof sz.h !== "number") continue;
9400
9418
  next[id] = { w: clampNum(sz.w, popMinW(id), fMaxW()), h: clampNum(sz.h, 120, fMaxH()) };
9419
+ if (typeof sz.left === "number") next[id].left = clampNum(sz.left, 8, fMaxW());
9401
9420
  }
9402
9421
  if (Object.keys(next).length) setPopSizes(next);
9403
9422
  }
@@ -9849,6 +9868,38 @@ function Datatable({
9849
9868
  middleRows.forEach((r2, i) => m.set(keyOf(r2, keyIndex.get(keyOf(r2)) ?? i), i));
9850
9869
  return m;
9851
9870
  }, [middleRows, keyIndex]);
9871
+ const cellColIndex = React51__namespace.default.useCallback((field) => ordered.findIndex((c) => c.field === field), [ordered]);
9872
+ const cellRect = React51__namespace.default.useMemo(() => {
9873
+ if (selectionMode !== "cell" || !activeCell) return null;
9874
+ const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
9875
+ if (ar == null || ac < 0) return null;
9876
+ const anchor = anchorCell || activeCell;
9877
+ const nr = keyIndex.get(anchor.key), nc = cellColIndex(anchor.field);
9878
+ const r0 = nr == null ? ar : Math.min(ar, nr), r1 = nr == null ? ar : Math.max(ar, nr);
9879
+ const c0 = nc < 0 ? ac : Math.min(ac, nc), c1 = nc < 0 ? ac : Math.max(ac, nc);
9880
+ return { r0, r1, c0, c1 };
9881
+ }, [selectionMode, activeCell, anchorCell, keyIndex, cellColIndex]);
9882
+ const activeCellId = React51__namespace.default.useMemo(() => {
9883
+ if (selectionMode !== "cell" || !activeCell) return void 0;
9884
+ const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
9885
+ return ar == null || ac < 0 ? void 0 : `${gridId}-${ar}-${ac}`;
9886
+ }, [selectionMode, activeCell, keyIndex, cellColIndex, gridId]);
9887
+ const onCellSelRef = React51__namespace.default.useRef(onCellSelectionChange);
9888
+ onCellSelRef.current = onCellSelectionChange;
9889
+ const prevSelSigRef = React51__namespace.default.useRef("");
9890
+ React51__namespace.default.useEffect(() => {
9891
+ const cb = onCellSelRef.current;
9892
+ if (selectionMode !== "cell" || !cb) return;
9893
+ const cells = [];
9894
+ if (cellRect) for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) for (let c = cellRect.c0; c <= cellRect.c1; c++) {
9895
+ const row = leafRows[r2], col = ordered[c];
9896
+ if (row && col) cells.push({ key: keyOf(row, r2), field: col.field });
9897
+ }
9898
+ const sig = cells.map((x) => `${x.key}|${x.field}`).join(",");
9899
+ if (sig === prevSelSigRef.current) return;
9900
+ prevSelSigRef.current = sig;
9901
+ cb(cells);
9902
+ }, [cellRect, selectionMode, leafRows, ordered]);
9852
9903
  const canReorderRows = rowReorder && !activeGroupBy.length && !sort;
9853
9904
  function onRowDrop(targetKey) {
9854
9905
  const fromKey = rowDrag.from;
@@ -10261,6 +10312,7 @@ function Datatable({
10261
10312
  const next = { key: k, field: col.field };
10262
10313
  setActiveCell(next);
10263
10314
  setActiveRow(k);
10315
+ if (!(e.shiftKey && anchorCell)) setAnchorCell(next);
10264
10316
  onCellClick?.(row[col.field], row, col.field);
10265
10317
  onActiveCellChange?.(next);
10266
10318
  }
@@ -10272,6 +10324,24 @@ function Datatable({
10272
10324
  if (editing) return;
10273
10325
  const td = e.target.closest(".twc-dt__td[data-r]");
10274
10326
  if (!td) return;
10327
+ if (enableClipboard && selectionMode === "cell" && (e.ctrlKey || e.metaKey)) {
10328
+ const k = e.key.toLowerCase();
10329
+ if (k === "c") {
10330
+ e.preventDefault();
10331
+ copySelection(false);
10332
+ return;
10333
+ }
10334
+ if (k === "x") {
10335
+ e.preventDefault();
10336
+ copySelection(true);
10337
+ return;
10338
+ }
10339
+ if (k === "v") {
10340
+ e.preventDefault();
10341
+ pasteSelection();
10342
+ return;
10343
+ }
10344
+ }
10275
10345
  let r2 = +td.getAttribute("data-r"), c = +td.getAttribute("data-c");
10276
10346
  const maxR = leafRows.length - 1, maxC = ordered.length - 1;
10277
10347
  let handled = true;
@@ -10324,6 +10394,16 @@ function Datatable({
10324
10394
  e.preventDefault();
10325
10395
  setFocus({ r: r2, c });
10326
10396
  focusCell(r2, c);
10397
+ if (selectionMode === "cell") {
10398
+ const nk = keyOf(leafRows[r2], r2), col = ordered[c];
10399
+ if (col) {
10400
+ const nc = { key: nk, field: col.field };
10401
+ setActiveCell(nc);
10402
+ setActiveRow(nk);
10403
+ if (!e.shiftKey) setAnchorCell(nc);
10404
+ onActiveCellChange?.(nc);
10405
+ }
10406
+ }
10327
10407
  }
10328
10408
  }
10329
10409
  React51__namespace.default.useEffect(() => {
@@ -10440,6 +10520,161 @@ function Datatable({
10440
10520
  setBatchEdit(null);
10441
10521
  closeBatchEdit();
10442
10522
  }
10523
+ const [clipboardMsg, setClipboardMsg] = React51__namespace.default.useState("");
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
+ };
10540
+ const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10541
+ const writeClipboard = (text) => {
10542
+ const execFallback = () => {
10543
+ try {
10544
+ if (typeof document !== "undefined") {
10545
+ const ta = document.createElement("textarea");
10546
+ ta.value = text;
10547
+ ta.style.position = "fixed";
10548
+ ta.style.opacity = "0";
10549
+ document.body.appendChild(ta);
10550
+ ta.focus();
10551
+ ta.select();
10552
+ document.execCommand("copy");
10553
+ document.body.removeChild(ta);
10554
+ }
10555
+ } catch {
10556
+ }
10557
+ };
10558
+ try {
10559
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text).catch(execFallback);
10560
+ } catch {
10561
+ }
10562
+ execFallback();
10563
+ return Promise.resolve();
10564
+ };
10565
+ const writeCellPatches = (patchByKey) => {
10566
+ if (!patchByKey.size) return 0;
10567
+ const changed = [];
10568
+ const nextAll = rows.map((r2, i) => {
10569
+ const k = keyOf(r2, i);
10570
+ const patch = patchByKey.get(k);
10571
+ if (!patch) return r2;
10572
+ const updated = { ...r2, ...patch };
10573
+ changed.push([updated, r2, patch]);
10574
+ return updated;
10575
+ });
10576
+ changed.forEach(([updated, orig, patch]) => {
10577
+ for (const f of Object.keys(patch)) onRowUpdate?.(updated, orig, f);
10578
+ });
10579
+ if (onRowsChange && changed.length) onRowsChange(nextAll);
10580
+ return changed.length;
10581
+ };
10582
+ const rectCells = () => {
10583
+ if (!cellRect) return null;
10584
+ const out = [];
10585
+ for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) {
10586
+ const row = leafRows[r2];
10587
+ if (!row) continue;
10588
+ const line = [];
10589
+ for (let c = cellRect.c0; c <= cellRect.c1; c++) {
10590
+ const col = ordered[c];
10591
+ if (col) line.push({ key: keyOf(row, r2), field: col.field, col, value: getColVal(col, row) });
10592
+ }
10593
+ out.push(line);
10594
+ }
10595
+ return out.length ? out : null;
10596
+ };
10597
+ const copySelection = (cut) => {
10598
+ const grid = rectCells();
10599
+ if (!grid) return;
10600
+ const tsv = grid.map((line) => line.map((cell) => cell.value == null ? "" : String(cell.value)).join(" ")).join("\n");
10601
+ clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
10602
+ writeClipboard(tsv);
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 });
10605
+ if (cut) {
10606
+ const patchByKey = /* @__PURE__ */ new Map();
10607
+ let clearable = 0;
10608
+ for (const line of grid) for (const cell of line) {
10609
+ if (!isColEditable(cell.col)) continue;
10610
+ clearable++;
10611
+ const p = patchByKey.get(cell.key) || {};
10612
+ p[cell.field] = cell.col.type === "number" ? null : "";
10613
+ patchByKey.set(cell.key, p);
10614
+ }
10615
+ writeCellPatches(patchByKey);
10616
+ announceClip(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`, cellRect);
10617
+ } else {
10618
+ announceClip(`Copied ${n} cell${n === 1 ? "" : "s"}`, cellRect);
10619
+ }
10620
+ };
10621
+ const pasteSelection = async () => {
10622
+ if (!cellRect) return;
10623
+ let text = null;
10624
+ try {
10625
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) text = await navigator.clipboard.readText();
10626
+ } catch {
10627
+ setClipboardMsg("Paste unavailable \u2014 clipboard permission denied");
10628
+ return;
10629
+ }
10630
+ if (text == null) {
10631
+ setClipboardMsg("Nothing to paste");
10632
+ return;
10633
+ }
10634
+ const inApp = clipboardRef.current && clipboardRef.current.tsv === text ? clipboardRef.current : null;
10635
+ let matrix;
10636
+ if (inApp) matrix = inApp.matrix;
10637
+ else {
10638
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
10639
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
10640
+ matrix = lines.map((line) => line.split(" "));
10641
+ }
10642
+ const patchByKey = /* @__PURE__ */ new Map();
10643
+ let written = 0, skipped = 0;
10644
+ for (let i = 0; i < matrix.length; i++) {
10645
+ const rr = cellRect.r0 + i;
10646
+ const row = leafRows[rr];
10647
+ if (!row) break;
10648
+ for (let j = 0; j < matrix[i].length; j++) {
10649
+ const cc = cellRect.c0 + j;
10650
+ const col = ordered[cc];
10651
+ if (!col) break;
10652
+ if (!isColEditable(col)) {
10653
+ skipped++;
10654
+ continue;
10655
+ }
10656
+ if (inApp && inApp.colTypes[j] != null && inApp.colTypes[j] !== copyTypeOf(col)) {
10657
+ skipped++;
10658
+ continue;
10659
+ }
10660
+ let v = matrix[i][j];
10661
+ if (col.type === "number") {
10662
+ v = v === "" ? null : Number(v);
10663
+ if (Number.isNaN(v)) v = null;
10664
+ }
10665
+ const k = keyOf(row, rr);
10666
+ const p = patchByKey.get(k) || {};
10667
+ p[col.field] = v;
10668
+ patchByKey.set(k, p);
10669
+ written++;
10670
+ }
10671
+ }
10672
+ writeCellPatches(patchByKey);
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);
10677
+ };
10443
10678
  React51__namespace.default.useEffect(() => {
10444
10679
  if (!editing) return;
10445
10680
  const onDown = (e) => {
@@ -10696,6 +10931,7 @@ function Datatable({
10696
10931
  if (node) {
10697
10932
  node.style.removeProperty("--twc-dt-pop-w");
10698
10933
  node.style.removeProperty("--twc-dt-pop-h");
10934
+ node.style.removeProperty("left");
10699
10935
  node.removeAttribute("data-pop-sized");
10700
10936
  }
10701
10937
  setPopSizes((s) => {
@@ -10705,7 +10941,7 @@ function Datatable({
10705
10941
  return n;
10706
10942
  });
10707
10943
  };
10708
- const startPopResize = (id) => (e) => {
10944
+ const startPopResize = (id, side) => (e) => {
10709
10945
  if (e.button != null && e.button !== 0) return;
10710
10946
  e.preventDefault();
10711
10947
  e.stopPropagation();
@@ -10714,21 +10950,27 @@ function Datatable({
10714
10950
  const grip = e.currentTarget;
10715
10951
  const startX = e.clientX, startY = e.clientY;
10716
10952
  const r2 = pop.getBoundingClientRect();
10717
- const startW = r2.width, startH = r2.height;
10953
+ const startW = r2.width, startH = r2.height, startLeft = r2.left, startRight = r2.right;
10718
10954
  const dir = typeof getComputedStyle !== "undefined" && getComputedStyle(pop).direction === "rtl" ? -1 : 1;
10955
+ const toStart = side === "start";
10719
10956
  pop.style.setProperty("--twc-dt-pop-w", `${startW}px`);
10720
10957
  pop.style.setProperty("--twc-dt-pop-h", `${startH}px`);
10721
10958
  grip.setAttribute("data-active", "true");
10722
10959
  pop.setAttribute("data-pop-sized", "");
10723
- let lw = startW, lh = startH;
10960
+ let lw = startW, lh = startH, ll = startLeft;
10724
10961
  const onMove = (ev) => {
10725
- lw = clampNum(startW + (ev.clientX - startX) * dir, popMinW(id), fMaxW());
10962
+ const dw = toStart ? (startX - ev.clientX) * dir : (ev.clientX - startX) * dir;
10963
+ lw = clampNum(startW + dw, popMinW(id), fMaxW());
10726
10964
  lh = clampNum(startH + (ev.clientY - startY), 120, fMaxH());
10727
10965
  pop.style.setProperty("--twc-dt-pop-w", `${lw}px`);
10728
10966
  pop.style.setProperty("--twc-dt-pop-h", `${lh}px`);
10967
+ if (toStart) {
10968
+ ll = Math.max(8, startRight - lw);
10969
+ pop.style.left = `${ll}px`;
10970
+ }
10729
10971
  };
10730
10972
  const onUp = () => {
10731
- setPopSizes((s) => ({ ...s, [id]: { w: lw, h: lh } }));
10973
+ setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w: lw, h: lh, ...toStart ? { left: ll } : {} } }));
10732
10974
  grip.removeAttribute("data-active");
10733
10975
  window.removeEventListener("pointermove", onMove);
10734
10976
  window.removeEventListener("pointerup", onUp);
@@ -10736,7 +10978,7 @@ function Datatable({
10736
10978
  window.addEventListener("pointermove", onMove);
10737
10979
  window.addEventListener("pointerup", onUp);
10738
10980
  };
10739
- const onPopGripKey = (id) => (e) => {
10981
+ const onPopGripKey = (id, side) => (e) => {
10740
10982
  if (e.key === "Enter" || e.key === " " || e.key === "Backspace") {
10741
10983
  e.preventDefault();
10742
10984
  clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
@@ -10761,25 +11003,54 @@ function Datatable({
10761
11003
  w = fMaxW();
10762
11004
  h = fMaxH();
10763
11005
  }
10764
- setPopSizes((s) => ({ ...s, [id]: { w: clampNum(w, popMinW(id), fMaxW()), h: clampNum(h, 120, fMaxH()) } }));
11006
+ w = clampNum(w, popMinW(id), fMaxW());
11007
+ h = clampNum(h, 120, fMaxH());
11008
+ const left = side === "start" ? Math.max(8, r2.right - w) : void 0;
11009
+ setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w, h, ...left != null ? { left } : {} } }));
10765
11010
  };
10766
- const popStyle = (id) => popSizes[id] ? { "--twc-dt-pop-w": `${popSizes[id].w}px`, "--twc-dt-pop-h": `${popSizes[id].h}px` } : null;
10767
- const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsxRuntime.jsx(
10768
- "span",
10769
- {
10770
- className: "twc-dt__pop-grip",
10771
- role: "slider",
10772
- tabIndex: 0,
10773
- "aria-label": "Resize panel (Enter or double-click to reset)",
10774
- "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
10775
- onPointerDown: startPopResize(id),
10776
- onKeyDown: onPopGripKey(id),
10777
- onDoubleClick: (e) => {
10778
- e.preventDefault();
10779
- clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
11011
+ const popStyle = (id) => {
11012
+ const s = popSizes[id];
11013
+ if (!s) return null;
11014
+ const st = { "--twc-dt-pop-w": `${s.w}px`, "--twc-dt-pop-h": `${s.h}px` };
11015
+ if (s.left != null) st.left = `${s.left}px`;
11016
+ return st;
11017
+ };
11018
+ const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11019
+ /* @__PURE__ */ jsxRuntime.jsx(
11020
+ "span",
11021
+ {
11022
+ className: "twc-dt__pop-grip",
11023
+ "data-side": "end",
11024
+ role: "slider",
11025
+ tabIndex: 0,
11026
+ "aria-label": "Resize panel (Enter or double-click to reset)",
11027
+ "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
11028
+ onPointerDown: startPopResize(id, "end"),
11029
+ onKeyDown: onPopGripKey(id, "end"),
11030
+ onDoubleClick: (e) => {
11031
+ e.preventDefault();
11032
+ clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
11033
+ }
10780
11034
  }
10781
- }
10782
- ) : null;
11035
+ ),
11036
+ /* @__PURE__ */ jsxRuntime.jsx(
11037
+ "span",
11038
+ {
11039
+ className: "twc-dt__pop-grip",
11040
+ "data-side": "start",
11041
+ role: "slider",
11042
+ tabIndex: 0,
11043
+ "aria-label": "Resize panel leftward (Enter or double-click to reset)",
11044
+ "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
11045
+ onPointerDown: startPopResize(id, "start"),
11046
+ onKeyDown: onPopGripKey(id, "start"),
11047
+ onDoubleClick: (e) => {
11048
+ e.preventDefault();
11049
+ clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
11050
+ }
11051
+ }
11052
+ )
11053
+ ] }) : null;
10783
11054
  const fieldHandle = (field, rowIdx) => resizableFilters && rowIdx === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
10784
11055
  "span",
10785
11056
  {
@@ -10935,22 +11206,29 @@ function Datatable({
10935
11206
  const editable = isColEditable(c);
10936
11207
  const isEditing = editing && editing.key === k && editing.field === c.field;
10937
11208
  const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
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;
11211
+ const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
10938
11212
  const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
10939
11213
  const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
10940
11214
  return /* @__PURE__ */ jsxRuntime.jsxs(
10941
11215
  "td",
10942
11216
  {
11217
+ id: cellId,
10943
11218
  className: "twc-dt__td",
10944
11219
  role: "gridcell",
10945
11220
  "data-r": ri,
10946
11221
  "data-c": ci,
10947
11222
  "aria-colindex": ci + 1 + (checkboxSelection ? 1 : 0),
10948
11223
  tabIndex: focus.r === ri && focus.c === ci ? 0 : -1,
11224
+ "aria-selected": cellSelected || void 0,
10949
11225
  "data-num": c.type === "number" || void 0,
10950
11226
  "data-actions": isActions || void 0,
10951
11227
  "data-editable": editable && !isEditing || void 0,
10952
11228
  "data-editing": isEditing || void 0,
10953
11229
  "data-cell-active": cellActive || void 0,
11230
+ "data-cell-selected": cellSelected || void 0,
11231
+ "data-copied": cellFlash || void 0,
10954
11232
  "data-wrap": wrapped.has(c.field) || void 0,
10955
11233
  "data-pin": st.pin,
10956
11234
  "data-pin-edge": st.edge,
@@ -11312,6 +11590,8 @@ function Datatable({
11312
11590
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-dt__groupbar-clear", onClick: () => setGroupBy([]), children: "Clear all" })
11313
11591
  ] }) : null,
11314
11592
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
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,
11315
11595
  pivotActive ? renderPivot() : /* @__PURE__ */ jsxRuntime.jsx(
11316
11596
  "div",
11317
11597
  {
@@ -11333,6 +11613,8 @@ function Datatable({
11333
11613
  "aria-label": ariaLabelAttr || ariaLabel,
11334
11614
  "aria-rowcount": totalRows + 1,
11335
11615
  "aria-colcount": ordered.length + (checkboxSelection ? 1 : 0),
11616
+ "aria-multiselectable": selectionMode === "cell" || void 0,
11617
+ "aria-activedescendant": activeCellId,
11336
11618
  "aria-busy": loading || void 0,
11337
11619
  onKeyDown: onGridKeyDown,
11338
11620
  children: [