twico-ui 1.24.0 → 1.26.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
@@ -1825,8 +1825,9 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1825
1825
  resizableFilters?: boolean;
1826
1826
  /** #304: extend the drag-resize grip to the other toolbar popovers too — Columns, Aggregation, Pivot,
1827
1827
  * and Batch-edit — each with a corner grip + keyboard resize (Arrows, Home/End, Enter/double-click to
1828
- * reset). Implies `resizableFilters`. Each popover's size persists independently via `stateKey`
1829
- * (`popoverSizes`). Great for a wide grid whose Columns list is otherwise capped. @default false */
1828
+ * reset). Independent of `resizableFilters` (which controls the Filters panel): each defaults `true` and
1829
+ * can be turned off on its own. Each popover's size persists independently via `stateKey` (`popoverSizes`).
1830
+ * Great for a wide grid whose Columns list is otherwise capped. Set `false` to opt a grid out. @default true */
1830
1831
  resizablePopovers?: boolean;
1831
1832
  /** Raise the drag cap for the filter panel's column/operator fields, in px (default col 360 / op 260) —
1832
1833
  * the #289-deferred "optional prop to raise the cap". Only affects `resizableFilters`. */
@@ -1937,7 +1938,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1937
1938
  onStateChange?: (state: DatatableState) => void;
1938
1939
  /** Show a "Go to" page jumper in the footer when there are more than 5 pages. @default true */
1939
1940
  showPageJumper?: boolean;
1940
- /** 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" */
1941
1945
  selectionMode?: "none" | "row" | "cell";
1942
1946
  /** Fired when a row is clicked in "row" selection mode: (row, key). */
1943
1947
  onRowClick?: (row: T, key: string | number) => void;
@@ -1945,6 +1949,15 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1945
1949
  onCellClick?: (value: any, row: T, field: string) => void;
1946
1950
  /** Fired when the active cell changes: ({ key, field } | null). */
1947
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
+ * @default false */
1960
+ enableClipboard?: boolean;
1948
1961
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1949
1962
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1950
1963
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
@@ -2156,6 +2169,11 @@ interface DatatableColumn<T = any> {
2156
2169
  loadValueOptions?: (query: string) => Promise<Array<string | { value: string; label: string }>>;
2157
2170
  /** Format the raw value to a string/number for display. */
2158
2171
  valueFormatter?: (value: any, row: T) => React.ReactNode;
2172
+ /** #318: opaque copy/paste compatibility token for the cell clipboard (`enableClipboard`). On an in-app
2173
+ * paste, a source cell is written to a target only when the two columns' `copyType` match — so e.g. a
2174
+ * "part-name" value never lands in a "measurement" column. Falls back to a number-vs-text bucket derived
2175
+ * from `type` when omitted. Examples: "text", "number", "part-name", "master:suppliers". */
2176
+ copyType?: string;
2159
2177
  /** Fully custom cell renderer (badges, avatars, etc.). */
2160
2178
  renderCell?: (value: any, row: T) => React.ReactNode;
2161
2179
  /** Diff-mode (#239) per-column equality override — return true when the two values are equal.
package/dist/index.d.ts CHANGED
@@ -1825,8 +1825,9 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1825
1825
  resizableFilters?: boolean;
1826
1826
  /** #304: extend the drag-resize grip to the other toolbar popovers too — Columns, Aggregation, Pivot,
1827
1827
  * and Batch-edit — each with a corner grip + keyboard resize (Arrows, Home/End, Enter/double-click to
1828
- * reset). Implies `resizableFilters`. Each popover's size persists independently via `stateKey`
1829
- * (`popoverSizes`). Great for a wide grid whose Columns list is otherwise capped. @default false */
1828
+ * reset). Independent of `resizableFilters` (which controls the Filters panel): each defaults `true` and
1829
+ * can be turned off on its own. Each popover's size persists independently via `stateKey` (`popoverSizes`).
1830
+ * Great for a wide grid whose Columns list is otherwise capped. Set `false` to opt a grid out. @default true */
1830
1831
  resizablePopovers?: boolean;
1831
1832
  /** Raise the drag cap for the filter panel's column/operator fields, in px (default col 360 / op 260) —
1832
1833
  * the #289-deferred "optional prop to raise the cap". Only affects `resizableFilters`. */
@@ -1937,7 +1938,10 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1937
1938
  onStateChange?: (state: DatatableState) => void;
1938
1939
  /** Show a "Go to" page jumper in the footer when there are more than 5 pages. @default true */
1939
1940
  showPageJumper?: boolean;
1940
- /** 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" */
1941
1945
  selectionMode?: "none" | "row" | "cell";
1942
1946
  /** Fired when a row is clicked in "row" selection mode: (row, key). */
1943
1947
  onRowClick?: (row: T, key: string | number) => void;
@@ -1945,6 +1949,15 @@ interface DatatableProps<T = any> extends Omit<React.HTMLAttributes<HTMLDivEleme
1945
1949
  onCellClick?: (value: any, row: T, field: string) => void;
1946
1950
  /** Fired when the active cell changes: ({ key, field } | null). */
1947
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
+ * @default false */
1960
+ enableClipboard?: boolean;
1948
1961
  /** Show the **Aggregation** toolbar button and start with the totals row on. From that panel the user
1949
1962
  * toggles totals and picks columns + functions (a column's `aggregation` prop seeds the initial choice);
1950
1963
  * changing the prop re-applies it. When false (the default) the Aggregation button is hidden. @default false */
@@ -2156,6 +2169,11 @@ interface DatatableColumn<T = any> {
2156
2169
  loadValueOptions?: (query: string) => Promise<Array<string | { value: string; label: string }>>;
2157
2170
  /** Format the raw value to a string/number for display. */
2158
2171
  valueFormatter?: (value: any, row: T) => React.ReactNode;
2172
+ /** #318: opaque copy/paste compatibility token for the cell clipboard (`enableClipboard`). On an in-app
2173
+ * paste, a source cell is written to a target only when the two columns' `copyType` match — so e.g. a
2174
+ * "part-name" value never lands in a "measurement" column. Falls back to a number-vs-text bucket derived
2175
+ * from `type` when omitted. Examples: "text", "number", "part-name", "master:suppliers". */
2176
+ copyType?: string;
2159
2177
  /** Fully custom cell renderer (badges, avatars, etc.). */
2160
2178
  renderCell?: (value: any, row: T) => React.ReactNode;
2161
2179
  /** Diff-mode (#239) per-column equality override — return true when the two values are equal.
package/dist/index.mjs CHANGED
@@ -8440,6 +8440,8 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8440
8440
  .twc-dt__row[data-selectable="true"] { cursor: pointer; }
8441
8441
  .twc-dt__row[data-active="true"] .twc-dt__td { background: var(--color-primary-subtle); box-shadow: inset 0 0 0 9999px transparent; }
8442
8442
  .twc-dt__row[data-active="true"] .twc-dt__td[data-pin] { background: var(--color-primary-subtle); }
8443
+ /* #317: data-cell-selected fills the range rectangle; data-cell-active keeps the ring on the moving corner. */
8444
+ .twc-dt__td[data-cell-selected="true"] { background: var(--color-primary-subtle); }
8443
8445
  .twc-dt__td[data-cell-active="true"] { box-shadow: inset 0 0 0 2px var(--color-primary); background: var(--color-primary-subtle); }
8444
8446
  .twc-dt__row:last-child .twc-dt__td { border-bottom: none; }
8445
8447
 
@@ -8594,6 +8596,9 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
8594
8596
  .twc-dt__pop-grip:hover, .twc-dt__pop-grip:focus-visible, .twc-dt__pop-grip[data-active="true"] { opacity: 1; }
8595
8597
  .twc-dt__pop-grip:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
8596
8598
  [dir="rtl"] .twc-dt__pop-grip { cursor: nesw-resize; }
8599
+ /* #316: a second grip in the bottom-inline-start corner grows a right-anchored panel toward the empty side (right edge pinned). */
8600
+ .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%); }
8601
+ [dir="rtl"] .twc-dt__pop-grip[data-side="start"] { cursor: nwse-resize; }
8597
8602
  @media (pointer: coarse) {
8598
8603
  .twc-dt__f-rz { width: 16px; inset-inline-end: -6px; opacity: 0.4; }
8599
8604
  .twc-dt__pop-grip { width: 20px; height: 20px; opacity: 0.6; }
@@ -9148,6 +9153,8 @@ function Datatable({
9148
9153
  onRowClick,
9149
9154
  onCellClick,
9150
9155
  onActiveCellChange,
9156
+ onCellSelectionChange,
9157
+ enableClipboard = false,
9151
9158
  showAggregation = false,
9152
9159
  ariaLabel = "Data table",
9153
9160
  "aria-label": ariaLabelAttr,
@@ -9159,7 +9166,8 @@ function Datatable({
9159
9166
  onQuickFilterChange,
9160
9167
  toolbarActions,
9161
9168
  resizableFilters = true,
9162
- resizablePopovers = false,
9169
+ resizablePopovers = true,
9170
+ // #314: default-on (matches resizableFilters) — set false to opt a grid out
9163
9171
  filterFieldMaxWidth,
9164
9172
  rowPinning = false,
9165
9173
  rowReorder = false,
@@ -9277,6 +9285,8 @@ function Datatable({
9277
9285
  const [selected, setSelected] = React51__default.useState(() => /* @__PURE__ */ new Set());
9278
9286
  const [activeRow, setActiveRow] = React51__default.useState(null);
9279
9287
  const [activeCell, setActiveCell] = React51__default.useState(null);
9288
+ const [anchorCell, setAnchorCell] = React51__default.useState(null);
9289
+ const gridId = React51__default.useId();
9280
9290
  const [internalPage, setInternalPage] = React51__default.useState(0);
9281
9291
  const [internalRpp, setInternalRpp] = React51__default.useState(pageSize > 0 ? pageSize : 10);
9282
9292
  const pageControlled = page !== void 0;
@@ -9376,6 +9386,7 @@ function Datatable({
9376
9386
  for (const [id, sz] of Object.entries(s.popoverSizes)) {
9377
9387
  if (!sz || typeof sz !== "object" || typeof sz.w !== "number" || typeof sz.h !== "number") continue;
9378
9388
  next[id] = { w: clampNum(sz.w, popMinW(id), fMaxW()), h: clampNum(sz.h, 120, fMaxH()) };
9389
+ if (typeof sz.left === "number") next[id].left = clampNum(sz.left, 8, fMaxW());
9379
9390
  }
9380
9391
  if (Object.keys(next).length) setPopSizes(next);
9381
9392
  }
@@ -9827,6 +9838,38 @@ function Datatable({
9827
9838
  middleRows.forEach((r2, i) => m.set(keyOf(r2, keyIndex.get(keyOf(r2)) ?? i), i));
9828
9839
  return m;
9829
9840
  }, [middleRows, keyIndex]);
9841
+ const cellColIndex = React51__default.useCallback((field) => ordered.findIndex((c) => c.field === field), [ordered]);
9842
+ const cellRect = React51__default.useMemo(() => {
9843
+ if (selectionMode !== "cell" || !activeCell) return null;
9844
+ const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
9845
+ if (ar == null || ac < 0) return null;
9846
+ const anchor = anchorCell || activeCell;
9847
+ const nr = keyIndex.get(anchor.key), nc = cellColIndex(anchor.field);
9848
+ const r0 = nr == null ? ar : Math.min(ar, nr), r1 = nr == null ? ar : Math.max(ar, nr);
9849
+ const c0 = nc < 0 ? ac : Math.min(ac, nc), c1 = nc < 0 ? ac : Math.max(ac, nc);
9850
+ return { r0, r1, c0, c1 };
9851
+ }, [selectionMode, activeCell, anchorCell, keyIndex, cellColIndex]);
9852
+ const activeCellId = React51__default.useMemo(() => {
9853
+ if (selectionMode !== "cell" || !activeCell) return void 0;
9854
+ const ar = keyIndex.get(activeCell.key), ac = cellColIndex(activeCell.field);
9855
+ return ar == null || ac < 0 ? void 0 : `${gridId}-${ar}-${ac}`;
9856
+ }, [selectionMode, activeCell, keyIndex, cellColIndex, gridId]);
9857
+ const onCellSelRef = React51__default.useRef(onCellSelectionChange);
9858
+ onCellSelRef.current = onCellSelectionChange;
9859
+ const prevSelSigRef = React51__default.useRef("");
9860
+ React51__default.useEffect(() => {
9861
+ const cb = onCellSelRef.current;
9862
+ if (selectionMode !== "cell" || !cb) return;
9863
+ const cells = [];
9864
+ if (cellRect) for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) for (let c = cellRect.c0; c <= cellRect.c1; c++) {
9865
+ const row = leafRows[r2], col = ordered[c];
9866
+ if (row && col) cells.push({ key: keyOf(row, r2), field: col.field });
9867
+ }
9868
+ const sig = cells.map((x) => `${x.key}|${x.field}`).join(",");
9869
+ if (sig === prevSelSigRef.current) return;
9870
+ prevSelSigRef.current = sig;
9871
+ cb(cells);
9872
+ }, [cellRect, selectionMode, leafRows, ordered]);
9830
9873
  const canReorderRows = rowReorder && !activeGroupBy.length && !sort;
9831
9874
  function onRowDrop(targetKey) {
9832
9875
  const fromKey = rowDrag.from;
@@ -10239,6 +10282,7 @@ function Datatable({
10239
10282
  const next = { key: k, field: col.field };
10240
10283
  setActiveCell(next);
10241
10284
  setActiveRow(k);
10285
+ if (!(e.shiftKey && anchorCell)) setAnchorCell(next);
10242
10286
  onCellClick?.(row[col.field], row, col.field);
10243
10287
  onActiveCellChange?.(next);
10244
10288
  }
@@ -10250,6 +10294,24 @@ function Datatable({
10250
10294
  if (editing) return;
10251
10295
  const td = e.target.closest(".twc-dt__td[data-r]");
10252
10296
  if (!td) return;
10297
+ if (enableClipboard && selectionMode === "cell" && (e.ctrlKey || e.metaKey)) {
10298
+ const k = e.key.toLowerCase();
10299
+ if (k === "c") {
10300
+ e.preventDefault();
10301
+ copySelection(false);
10302
+ return;
10303
+ }
10304
+ if (k === "x") {
10305
+ e.preventDefault();
10306
+ copySelection(true);
10307
+ return;
10308
+ }
10309
+ if (k === "v") {
10310
+ e.preventDefault();
10311
+ pasteSelection();
10312
+ return;
10313
+ }
10314
+ }
10253
10315
  let r2 = +td.getAttribute("data-r"), c = +td.getAttribute("data-c");
10254
10316
  const maxR = leafRows.length - 1, maxC = ordered.length - 1;
10255
10317
  let handled = true;
@@ -10302,6 +10364,16 @@ function Datatable({
10302
10364
  e.preventDefault();
10303
10365
  setFocus({ r: r2, c });
10304
10366
  focusCell(r2, c);
10367
+ if (selectionMode === "cell") {
10368
+ const nk = keyOf(leafRows[r2], r2), col = ordered[c];
10369
+ if (col) {
10370
+ const nc = { key: nk, field: col.field };
10371
+ setActiveCell(nc);
10372
+ setActiveRow(nk);
10373
+ if (!e.shiftKey) setAnchorCell(nc);
10374
+ onActiveCellChange?.(nc);
10375
+ }
10376
+ }
10305
10377
  }
10306
10378
  }
10307
10379
  React51__default.useEffect(() => {
@@ -10418,11 +10490,149 @@ function Datatable({
10418
10490
  setBatchEdit(null);
10419
10491
  closeBatchEdit();
10420
10492
  }
10493
+ const [clipboardMsg, setClipboardMsg] = React51__default.useState("");
10494
+ const clipboardRef = React51__default.useRef(null);
10495
+ const copyTypeOf = (col) => col && col.copyType != null ? col.copyType : col && col.type === "number" ? "number" : "text";
10496
+ const writeClipboard = (text) => {
10497
+ const execFallback = () => {
10498
+ try {
10499
+ if (typeof document !== "undefined") {
10500
+ const ta = document.createElement("textarea");
10501
+ ta.value = text;
10502
+ ta.style.position = "fixed";
10503
+ ta.style.opacity = "0";
10504
+ document.body.appendChild(ta);
10505
+ ta.focus();
10506
+ ta.select();
10507
+ document.execCommand("copy");
10508
+ document.body.removeChild(ta);
10509
+ }
10510
+ } catch {
10511
+ }
10512
+ };
10513
+ try {
10514
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text).catch(execFallback);
10515
+ } catch {
10516
+ }
10517
+ execFallback();
10518
+ return Promise.resolve();
10519
+ };
10520
+ const writeCellPatches = (patchByKey) => {
10521
+ if (!patchByKey.size) return 0;
10522
+ const changed = [];
10523
+ const nextAll = rows.map((r2, i) => {
10524
+ const k = keyOf(r2, i);
10525
+ const patch = patchByKey.get(k);
10526
+ if (!patch) return r2;
10527
+ const updated = { ...r2, ...patch };
10528
+ changed.push([updated, r2, patch]);
10529
+ return updated;
10530
+ });
10531
+ changed.forEach(([updated, orig, patch]) => {
10532
+ for (const f of Object.keys(patch)) onRowUpdate?.(updated, orig, f);
10533
+ });
10534
+ if (onRowsChange && changed.length) onRowsChange(nextAll);
10535
+ return changed.length;
10536
+ };
10537
+ const rectCells = () => {
10538
+ if (!cellRect) return null;
10539
+ const out = [];
10540
+ for (let r2 = cellRect.r0; r2 <= cellRect.r1; r2++) {
10541
+ const row = leafRows[r2];
10542
+ if (!row) continue;
10543
+ const line = [];
10544
+ for (let c = cellRect.c0; c <= cellRect.c1; c++) {
10545
+ const col = ordered[c];
10546
+ if (col) line.push({ key: keyOf(row, r2), field: col.field, col, value: getColVal(col, row) });
10547
+ }
10548
+ out.push(line);
10549
+ }
10550
+ return out.length ? out : null;
10551
+ };
10552
+ const copySelection = (cut) => {
10553
+ const grid = rectCells();
10554
+ if (!grid) return;
10555
+ const tsv = grid.map((line) => line.map((cell) => cell.value == null ? "" : String(cell.value)).join(" ")).join("\n");
10556
+ clipboardRef.current = { tsv, colTypes: grid[0].map((cell) => copyTypeOf(cell.col)), matrix: grid.map((line) => line.map((cell) => cell.value)) };
10557
+ writeClipboard(tsv);
10558
+ const n = grid.reduce((a, line) => a + line.length, 0);
10559
+ if (cut) {
10560
+ const patchByKey = /* @__PURE__ */ new Map();
10561
+ let clearable = 0;
10562
+ for (const line of grid) for (const cell of line) {
10563
+ if (!isColEditable(cell.col)) continue;
10564
+ clearable++;
10565
+ const p = patchByKey.get(cell.key) || {};
10566
+ p[cell.field] = cell.col.type === "number" ? null : "";
10567
+ patchByKey.set(cell.key, p);
10568
+ }
10569
+ writeCellPatches(patchByKey);
10570
+ setClipboardMsg(`Cut ${clearable} cell${clearable === 1 ? "" : "s"}${clearable < n ? `, ${n - clearable} read-only kept` : ""}`);
10571
+ } else {
10572
+ setClipboardMsg(`Copied ${n} cell${n === 1 ? "" : "s"}`);
10573
+ }
10574
+ };
10575
+ const pasteSelection = async () => {
10576
+ if (!cellRect) return;
10577
+ let text = null;
10578
+ try {
10579
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.readText) text = await navigator.clipboard.readText();
10580
+ } catch {
10581
+ setClipboardMsg("Paste unavailable \u2014 clipboard permission denied");
10582
+ return;
10583
+ }
10584
+ if (text == null) {
10585
+ setClipboardMsg("Nothing to paste");
10586
+ return;
10587
+ }
10588
+ const inApp = clipboardRef.current && clipboardRef.current.tsv === text ? clipboardRef.current : null;
10589
+ let matrix;
10590
+ if (inApp) matrix = inApp.matrix;
10591
+ else {
10592
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
10593
+ if (lines.length && lines[lines.length - 1] === "") lines.pop();
10594
+ matrix = lines.map((line) => line.split(" "));
10595
+ }
10596
+ const patchByKey = /* @__PURE__ */ new Map();
10597
+ let written = 0, skipped = 0;
10598
+ for (let i = 0; i < matrix.length; i++) {
10599
+ const rr = cellRect.r0 + i;
10600
+ const row = leafRows[rr];
10601
+ if (!row) break;
10602
+ for (let j = 0; j < matrix[i].length; j++) {
10603
+ const cc = cellRect.c0 + j;
10604
+ const col = ordered[cc];
10605
+ if (!col) break;
10606
+ if (!isColEditable(col)) {
10607
+ skipped++;
10608
+ continue;
10609
+ }
10610
+ if (inApp && inApp.colTypes[j] != null && inApp.colTypes[j] !== copyTypeOf(col)) {
10611
+ skipped++;
10612
+ continue;
10613
+ }
10614
+ let v = matrix[i][j];
10615
+ if (col.type === "number") {
10616
+ v = v === "" ? null : Number(v);
10617
+ if (Number.isNaN(v)) v = null;
10618
+ }
10619
+ const k = keyOf(row, rr);
10620
+ const p = patchByKey.get(k) || {};
10621
+ p[col.field] = v;
10622
+ patchByKey.set(k, p);
10623
+ written++;
10624
+ }
10625
+ }
10626
+ writeCellPatches(patchByKey);
10627
+ setClipboardMsg(`Pasted ${written} cell${written === 1 ? "" : "s"}${skipped ? `, ${skipped} skipped (incompatible column)` : ""}`);
10628
+ };
10421
10629
  React51__default.useEffect(() => {
10422
10630
  if (!editing) return;
10423
10631
  const onDown = (e) => {
10424
10632
  if (e.target.closest(".twc-dt__editor-wrap") || e.target.closest(".twc-pop")) return;
10425
- setEditing(null);
10633
+ const col = colByField[editing.field];
10634
+ if (col && col.renderEditCell) setEditing(null);
10635
+ else commitEdit();
10426
10636
  };
10427
10637
  document.addEventListener("mousedown", onDown, true);
10428
10638
  return () => document.removeEventListener("mousedown", onDown, true);
@@ -10672,6 +10882,7 @@ function Datatable({
10672
10882
  if (node) {
10673
10883
  node.style.removeProperty("--twc-dt-pop-w");
10674
10884
  node.style.removeProperty("--twc-dt-pop-h");
10885
+ node.style.removeProperty("left");
10675
10886
  node.removeAttribute("data-pop-sized");
10676
10887
  }
10677
10888
  setPopSizes((s) => {
@@ -10681,7 +10892,7 @@ function Datatable({
10681
10892
  return n;
10682
10893
  });
10683
10894
  };
10684
- const startPopResize = (id) => (e) => {
10895
+ const startPopResize = (id, side) => (e) => {
10685
10896
  if (e.button != null && e.button !== 0) return;
10686
10897
  e.preventDefault();
10687
10898
  e.stopPropagation();
@@ -10690,21 +10901,27 @@ function Datatable({
10690
10901
  const grip = e.currentTarget;
10691
10902
  const startX = e.clientX, startY = e.clientY;
10692
10903
  const r2 = pop.getBoundingClientRect();
10693
- const startW = r2.width, startH = r2.height;
10904
+ const startW = r2.width, startH = r2.height, startLeft = r2.left, startRight = r2.right;
10694
10905
  const dir = typeof getComputedStyle !== "undefined" && getComputedStyle(pop).direction === "rtl" ? -1 : 1;
10906
+ const toStart = side === "start";
10695
10907
  pop.style.setProperty("--twc-dt-pop-w", `${startW}px`);
10696
10908
  pop.style.setProperty("--twc-dt-pop-h", `${startH}px`);
10697
10909
  grip.setAttribute("data-active", "true");
10698
10910
  pop.setAttribute("data-pop-sized", "");
10699
- let lw = startW, lh = startH;
10911
+ let lw = startW, lh = startH, ll = startLeft;
10700
10912
  const onMove = (ev) => {
10701
- lw = clampNum(startW + (ev.clientX - startX) * dir, popMinW(id), fMaxW());
10913
+ const dw = toStart ? (startX - ev.clientX) * dir : (ev.clientX - startX) * dir;
10914
+ lw = clampNum(startW + dw, popMinW(id), fMaxW());
10702
10915
  lh = clampNum(startH + (ev.clientY - startY), 120, fMaxH());
10703
10916
  pop.style.setProperty("--twc-dt-pop-w", `${lw}px`);
10704
10917
  pop.style.setProperty("--twc-dt-pop-h", `${lh}px`);
10918
+ if (toStart) {
10919
+ ll = Math.max(8, startRight - lw);
10920
+ pop.style.left = `${ll}px`;
10921
+ }
10705
10922
  };
10706
10923
  const onUp = () => {
10707
- setPopSizes((s) => ({ ...s, [id]: { w: lw, h: lh } }));
10924
+ setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w: lw, h: lh, ...toStart ? { left: ll } : {} } }));
10708
10925
  grip.removeAttribute("data-active");
10709
10926
  window.removeEventListener("pointermove", onMove);
10710
10927
  window.removeEventListener("pointerup", onUp);
@@ -10712,7 +10929,7 @@ function Datatable({
10712
10929
  window.addEventListener("pointermove", onMove);
10713
10930
  window.addEventListener("pointerup", onUp);
10714
10931
  };
10715
- const onPopGripKey = (id) => (e) => {
10932
+ const onPopGripKey = (id, side) => (e) => {
10716
10933
  if (e.key === "Enter" || e.key === " " || e.key === "Backspace") {
10717
10934
  e.preventDefault();
10718
10935
  clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
@@ -10737,26 +10954,55 @@ function Datatable({
10737
10954
  w = fMaxW();
10738
10955
  h = fMaxH();
10739
10956
  }
10740
- setPopSizes((s) => ({ ...s, [id]: { w: clampNum(w, popMinW(id), fMaxW()), h: clampNum(h, 120, fMaxH()) } }));
10957
+ w = clampNum(w, popMinW(id), fMaxW());
10958
+ h = clampNum(h, 120, fMaxH());
10959
+ const left = side === "start" ? Math.max(8, r2.right - w) : void 0;
10960
+ setPopSizes((s) => ({ ...s, [id]: { ...s[id] || {}, w, h, ...left != null ? { left } : {} } }));
10741
10961
  };
10742
- const popStyle = (id) => popSizes[id] ? { "--twc-dt-pop-w": `${popSizes[id].w}px`, "--twc-dt-pop-h": `${popSizes[id].h}px` } : null;
10743
- const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsx(
10744
- "span",
10745
- {
10746
- className: "twc-dt__pop-grip",
10747
- role: "slider",
10748
- tabIndex: 0,
10749
- "aria-label": "Resize panel (Enter or double-click to reset)",
10750
- "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
10751
- onPointerDown: startPopResize(id),
10752
- onKeyDown: onPopGripKey(id),
10753
- onDoubleClick: (e) => {
10754
- e.preventDefault();
10755
- clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
10962
+ const popStyle = (id) => {
10963
+ const s = popSizes[id];
10964
+ if (!s) return null;
10965
+ const st = { "--twc-dt-pop-w": `${s.w}px`, "--twc-dt-pop-h": `${s.h}px` };
10966
+ if (s.left != null) st.left = `${s.left}px`;
10967
+ return st;
10968
+ };
10969
+ const popGrip = (id) => resizablePopovers ? /* @__PURE__ */ jsxs(Fragment, { children: [
10970
+ /* @__PURE__ */ jsx(
10971
+ "span",
10972
+ {
10973
+ className: "twc-dt__pop-grip",
10974
+ "data-side": "end",
10975
+ role: "slider",
10976
+ tabIndex: 0,
10977
+ "aria-label": "Resize panel (Enter or double-click to reset)",
10978
+ "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
10979
+ onPointerDown: startPopResize(id, "end"),
10980
+ onKeyDown: onPopGripKey(id, "end"),
10981
+ onDoubleClick: (e) => {
10982
+ e.preventDefault();
10983
+ clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
10984
+ }
10756
10985
  }
10757
- }
10758
- ) : null;
10759
- const fieldHandle = (field, rowIdx) => (resizableFilters || resizablePopovers) && rowIdx === 0 ? /* @__PURE__ */ jsx(
10986
+ ),
10987
+ /* @__PURE__ */ jsx(
10988
+ "span",
10989
+ {
10990
+ className: "twc-dt__pop-grip",
10991
+ "data-side": "start",
10992
+ role: "slider",
10993
+ tabIndex: 0,
10994
+ "aria-label": "Resize panel leftward (Enter or double-click to reset)",
10995
+ "aria-valuetext": popSizes[id] ? `Width ${Math.round(popSizes[id].w)} pixels, height ${Math.round(popSizes[id].h)} pixels` : "Default size",
10996
+ onPointerDown: startPopResize(id, "start"),
10997
+ onKeyDown: onPopGripKey(id, "start"),
10998
+ onDoubleClick: (e) => {
10999
+ e.preventDefault();
11000
+ clearPop(id, e.currentTarget.closest(".twc-dt__pop"));
11001
+ }
11002
+ }
11003
+ )
11004
+ ] }) : null;
11005
+ const fieldHandle = (field, rowIdx) => resizableFilters && rowIdx === 0 ? /* @__PURE__ */ jsx(
10760
11006
  "span",
10761
11007
  {
10762
11008
  className: "twc-dt__f-rz",
@@ -10911,22 +11157,27 @@ function Datatable({
10911
11157
  const editable = isColEditable(c);
10912
11158
  const isEditing = editing && editing.key === k && editing.field === c.field;
10913
11159
  const cellActive = selectionMode === "cell" && activeCell && activeCell.key === k && activeCell.field === c.field;
11160
+ const cellSelected = selectionMode === "cell" && cellRect && ri >= cellRect.r0 && ri <= cellRect.r1 && ci >= cellRect.c0 && ci <= cellRect.c1;
11161
+ const cellId = selectionMode === "cell" ? `${gridId}-${ri}-${ci}` : void 0;
10914
11162
  const display = c.valueFormatter ? c.valueFormatter(val, row) : val;
10915
11163
  const cellTitle = !isActions && !c.renderCell && !isEditing && (typeof display === "string" || typeof display === "number") ? String(display) : void 0;
10916
11164
  return /* @__PURE__ */ jsxs(
10917
11165
  "td",
10918
11166
  {
11167
+ id: cellId,
10919
11168
  className: "twc-dt__td",
10920
11169
  role: "gridcell",
10921
11170
  "data-r": ri,
10922
11171
  "data-c": ci,
10923
11172
  "aria-colindex": ci + 1 + (checkboxSelection ? 1 : 0),
10924
11173
  tabIndex: focus.r === ri && focus.c === ci ? 0 : -1,
11174
+ "aria-selected": cellSelected || void 0,
10925
11175
  "data-num": c.type === "number" || void 0,
10926
11176
  "data-actions": isActions || void 0,
10927
11177
  "data-editable": editable && !isEditing || void 0,
10928
11178
  "data-editing": isEditing || void 0,
10929
11179
  "data-cell-active": cellActive || void 0,
11180
+ "data-cell-selected": cellSelected || void 0,
10930
11181
  "data-wrap": wrapped.has(c.field) || void 0,
10931
11182
  "data-pin": st.pin,
10932
11183
  "data-pin-edge": st.edge,
@@ -11288,6 +11539,7 @@ function Datatable({
11288
11539
  /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__groupbar-clear", onClick: () => setGroupBy([]), children: "Clear all" })
11289
11540
  ] }) : null,
11290
11541
  /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: reorderMsg }),
11542
+ /* @__PURE__ */ jsx("div", { className: "twc-dt__sr", role: "status", "aria-live": "polite", children: clipboardMsg }),
11291
11543
  pivotActive ? renderPivot() : /* @__PURE__ */ jsx(
11292
11544
  "div",
11293
11545
  {
@@ -11309,6 +11561,8 @@ function Datatable({
11309
11561
  "aria-label": ariaLabelAttr || ariaLabel,
11310
11562
  "aria-rowcount": totalRows + 1,
11311
11563
  "aria-colcount": ordered.length + (checkboxSelection ? 1 : 0),
11564
+ "aria-multiselectable": selectionMode === "cell" || void 0,
11565
+ "aria-activedescendant": activeCellId,
11312
11566
  "aria-busy": loading || void 0,
11313
11567
  onKeyDown: onGridKeyDown,
11314
11568
  children: [
@@ -12170,7 +12424,7 @@ function Datatable({
12170
12424
  /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__flogic-btn", "data-active": filterLogic === "and" || void 0, "aria-pressed": filterLogic === "and", onClick: () => setFilterLogic("and"), children: "AND" }),
12171
12425
  /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__flogic-btn", "data-active": filterLogic === "or" || void 0, "aria-pressed": filterLogic === "or", onClick: () => setFilterLogic("or"), children: "OR" })
12172
12426
  ] }) : null,
12173
- (resizableFilters || resizablePopovers) && hasFilterSizeOverride ? /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__link", onClick: resetFilterSizes, children: "Reset sizes" }) : null,
12427
+ resizableFilters && hasFilterSizeOverride ? /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__link", onClick: resetFilterSizes, children: "Reset sizes" }) : null,
12174
12428
  filters.length ? /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__link", onClick: () => setFilters([]), children: "Clear all" }) : null
12175
12429
  ] })
12176
12430
  ] }),
@@ -12250,7 +12504,7 @@ function Datatable({
12250
12504
  /* @__PURE__ */ jsx(Svg, { d: I.plus, style: { color: "var(--color-primary)" } }),
12251
12505
  " Add filter"
12252
12506
  ] }) }),
12253
- resizableFilters || resizablePopovers ? /* @__PURE__ */ jsx(
12507
+ resizableFilters ? /* @__PURE__ */ jsx(
12254
12508
  "span",
12255
12509
  {
12256
12510
  className: "twc-dt__pop-grip",