@svgrid/grid 3.0.8 → 3.0.9

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.
Files changed (80) hide show
  1. package/dist/GridMenus.svelte +3 -2
  2. package/dist/SvGrid.controller.svelte.d.ts +60 -8
  3. package/dist/SvGrid.controller.svelte.js +475 -110
  4. package/dist/SvGrid.css +186 -3
  5. package/dist/SvGrid.svelte +281 -163
  6. package/dist/SvGrid.types.d.ts +24 -7
  7. package/dist/cdn/{GridMenus-D15bhpX5.js → GridMenus-B1sipj6y.js} +165 -165
  8. package/dist/cdn/{GridMenus-B8-Gbfq2.js → GridMenus-DlAzfHLk.js} +201 -201
  9. package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
  10. package/dist/cdn/{src-Dz625Fhx.js → src-BYsa66I0.js} +9738 -10174
  11. package/dist/cdn/{src-CleI0g9l.js → src-pA4RroLV.js} +10702 -11138
  12. package/dist/cdn/svgrid.js +27 -27
  13. package/dist/cdn/svgrid.svelte-external.js +27 -27
  14. package/dist/cdn/{validate-GEs4ZPSW.js → validate-XuMe4_KR.js} +2 -2
  15. package/dist/cell-render.js +25 -3
  16. package/dist/columns.js +16 -1
  17. package/dist/conditional-formatting.js +9 -2
  18. package/dist/core.d.ts +93 -0
  19. package/dist/core.js +313 -52
  20. package/dist/filtering/excel-filters.js +41 -6
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/selection.d.ts +2 -2
  24. package/dist/selection.js +32 -8
  25. package/dist/server-block-cache.d.ts +7 -32
  26. package/dist/server-block-cache.js +21 -12
  27. package/dist/server-data-source.js +3 -1
  28. package/dist/server.d.ts +1 -0
  29. package/dist/server.js +1 -0
  30. package/dist/spreadsheet.js +7 -0
  31. package/dist/validate.js +3 -11
  32. package/dist/virtualization/column-virtualizer.d.ts +4 -0
  33. package/dist/virtualization/column-virtualizer.js +2 -0
  34. package/dist/virtualization/types.d.ts +20 -0
  35. package/dist/virtualization/virtualizer.js +90 -24
  36. package/dist/windowed-brand.d.ts +22 -0
  37. package/dist/windowed-brand.js +60 -0
  38. package/dist/windowed-data.d.ts +6 -0
  39. package/dist/windowed-data.js +34 -0
  40. package/dist/windowed-row-model.d.ts +1 -0
  41. package/dist/windowed-row-model.js +97 -0
  42. package/package.json +1 -1
  43. package/src/GridMenus.svelte +3 -2
  44. package/src/SvGrid.controller.svelte.ts +469 -114
  45. package/src/SvGrid.css +186 -3
  46. package/src/SvGrid.svelte +281 -163
  47. package/src/SvGrid.types.ts +24 -7
  48. package/src/cell-render.ts +24 -2
  49. package/src/columns.ts +16 -1
  50. package/src/conditional-formatting.ts +6 -4
  51. package/src/core.sort.test.ts +61 -0
  52. package/src/core.ts +309 -52
  53. package/src/counting-cell.test.svelte +11 -0
  54. package/src/filtering/excel-filters.ts +39 -6
  55. package/src/headless.reactivity.svelte.test.ts +1 -1
  56. package/src/index.ts +6 -0
  57. package/src/new-features.test.ts +24 -0
  58. package/src/selection.test.ts +14 -0
  59. package/src/selection.ts +31 -8
  60. package/src/server-block-cache.ts +27 -10
  61. package/src/server-data-source.ts +3 -1
  62. package/src/server.ts +6 -0
  63. package/src/spreadsheet.ts +6 -0
  64. package/src/svgrid.behavior.test.ts +29 -0
  65. package/src/svgrid.column-cell-recycling.test.ts +120 -0
  66. package/src/svgrid.interaction.test.ts +4 -1
  67. package/src/svgrid.new-features.wrapper.test.ts +5 -4
  68. package/src/svgrid.pinned-column-virtualization.test.ts +162 -0
  69. package/src/svgrid.row-model-prop.svelte.test.ts +15 -3
  70. package/src/validate.test.ts +4 -4
  71. package/src/validate.ts +3 -14
  72. package/src/virtualization/column-virtualizer.ts +6 -0
  73. package/src/virtualization/types.ts +20 -0
  74. package/src/virtualization/virtualizer.test.ts +152 -0
  75. package/src/virtualization/virtualizer.ts +93 -27
  76. package/src/windowed-brand.ts +67 -0
  77. package/src/windowed-data.test.ts +105 -0
  78. package/src/windowed-data.ts +36 -0
  79. package/src/windowed-row-model.ts +113 -0
  80. package/dist/cdn/server-block-cache-DIqb3VZn.js +0 -241
@@ -466,6 +466,9 @@
466
466
  const measuredRowHeightPx = $derived(ctrl.measuredRowHeightPx);
467
467
  // Under auto height the row must be free to grow, so it gets `min-height`
468
468
  // (the pre-measure estimate) instead of a fixed `height` that would clip it.
469
+ // Otherwise the row is exactly the height the virtualizer placed it at: a
470
+ // flex row's `height` is exact. (A table row's is a minimum, so under the
471
+ // table layout content taller than the row still grows it.)
469
472
  const rowHeightStyle = (size: number): string =>
470
473
  autoRowHeightOn ? `min-height: ${size}px;` : `height: ${size}px;`;
471
474
  // DOM-space spacer heights + capped total: identical to the logical
@@ -476,34 +479,40 @@
476
479
  const rowBottomSpacer = $derived(ctrl.rowBottomSpacer);
477
480
  const rowDomTotalSize = $derived(ctrl.rowDomTotalSize);
478
481
  const renderedColumns = $derived(ctrl.renderedColumns);
482
+ const columnRunOf = $derived(ctrl.columnRunOf);
479
483
  const totalColumnWidth = $derived(ctrl.totalColumnWidth);
480
484
 
481
485
  // ---- Built-in cell flash (ColumnDef `cellFlash`) ----------------------
482
486
  // Flashes a cell when its value changes (edits, streaming feeds, server
483
- // pushes). Keyed by rowId so virtualization recycling a <td> into a new row
484
- // on scroll does NOT trigger a spurious flash - only a same-row value change
485
- // does. Inert (active:false) for columns without `cellFlash`.
487
+ // pushes). Keyed by rowId and columnId so virtualization recycling a <td>
488
+ // into a new row or column on scroll does NOT trigger a spurious flash - only
489
+ // a same-cell value change does. Inert (active:false) for columns without
490
+ // `cellFlash`.
486
491
  function cellFlashAction(
487
492
  node: HTMLElement,
488
493
  params: {
489
494
  rowId: string;
495
+ columnId: string;
490
496
  value: unknown;
491
497
  active: boolean;
492
498
  className: string;
493
499
  },
494
500
  ) {
495
501
  let prevRow = params.rowId;
502
+ let prevColumn = params.columnId;
496
503
  let prev = params.value;
497
504
  return {
498
505
  update(next: {
499
506
  rowId: string;
507
+ columnId: string;
500
508
  value: unknown;
501
509
  active: boolean;
502
510
  className: string;
503
511
  }) {
504
- if (next.rowId !== prevRow) {
505
- // A different row scrolled into this recycled slot - reset, no flash.
512
+ if (next.rowId !== prevRow || next.columnId !== prevColumn) {
513
+ // A different cell scrolled into this recycled slot - reset, no flash.
506
514
  prevRow = next.rowId;
515
+ prevColumn = next.columnId;
507
516
  prev = next.value;
508
517
  return;
509
518
  }
@@ -543,6 +552,15 @@
543
552
  const hasHorizontalOverflow = $derived(ctrl.hasHorizontalOverflow);
544
553
  const columnWindowStart = $derived(ctrl.columnWindowStart);
545
554
  const columnWindowRightSpacer = $derived(ctrl.columnWindowRightSpacer);
555
+ // The trailing spacer cell after the column window. A flex row is as wide
556
+ // as the table without it (the table carries the full width), and the
557
+ // spacer's area is always past the overscan, off screen; table layout
558
+ // needs it to give every row the same cells. Read as one boolean, so a
559
+ // column crossing - which changes the spacer's width - no longer re-runs
560
+ // a block and restyles a cell in every row.
561
+ const showRightSpacer = $derived(
562
+ columnVirtualizationEnabled && columnWindowRightSpacer > 0 && !ctrl.flexLayoutActive,
563
+ );
546
564
  const activeCell = $derived(ctrl.activeCell);
547
565
  const activeDescendantId = $derived(ctrl.activeDescendantId);
548
566
  const summaryByColumn = $derived(ctrl.summaryByColumn);
@@ -583,9 +601,36 @@
583
601
  const setActiveCell = $derived(ctrl.setActiveCell);
584
602
  const scrollActiveCellIntoView = $derived(ctrl.scrollActiveCellIntoView);
585
603
  const cellSelectionState = $derived(ctrl.cellSelectionState);
586
- const getColumnWidth = $derived(ctrl.getColumnWidth);
587
604
  const isInFillPreview = $derived(ctrl.isInFillPreview);
588
605
  const fillMarqueeEdges = $derived(ctrl.fillMarqueeEdges);
606
+ /**
607
+ * A body cell's editing, selection and fill-drag state, read by the cell
608
+ * through one `{@const}`. `null` - the same value every time - for a cell
609
+ * in none of those states, which is nearly every cell, so the cell's
610
+ * attribute effect does not re-run when, say, the active cell moves
611
+ * elsewhere. It used to be four `{@const}`s per cell (cell edit, row edit,
612
+ * selection, fill edges): four deriveds created for every cell on mount,
613
+ * and dropping three of them took ~3.5 ms off a 100k-row mount of ~32 ms.
614
+ */
615
+ function cellUi(
616
+ rowIndex: number,
617
+ c: { cellRow: { id: string }; cellColumn: { id: string }; colIndex: number },
618
+ ) {
619
+ const ed = ctrl.editingCell;
620
+ const editing = !!ed && ed.rowId === c.cellRow.id && ed.columnId === c.cellColumn.id;
621
+ const rowEdit = !!fullRowEdit && fullRowEdit.rowId === c.cellRow.id && c.cellColumn.id in fullRowEdit.draft;
622
+ const sel = cellSelectionState(rowIndex, c.colIndex);
623
+ const fill = fillMarqueeEdges(rowIndex, c.colIndex);
624
+ if (!editing && !rowEdit && !sel && !fill) return null;
625
+ return { editing, rowEdit, sel, fill };
626
+ }
627
+ /** Whether a `pointerover`/`pointerout` on a cell crossed the cell's own
628
+ * edge, as `pointerenter`/`pointerleave` would, rather than a move between
629
+ * its children. */
630
+ function crossedCell(event: PointerEvent & { currentTarget: EventTarget }): boolean {
631
+ const other = event.relatedTarget as Node | null;
632
+ return !other || !(event.currentTarget as Node).contains(other);
633
+ }
589
634
  const startFillDrag = $derived(ctrl.startFillDrag);
590
635
  const onCellPointerDown = $derived(ctrl.onCellPointerDown);
591
636
  const onCellPointerEnter = $derived(ctrl.onCellPointerEnter);
@@ -595,6 +640,64 @@
595
640
  const emitCellDoubleClick = $derived(ctrl.emitCellDoubleClick);
596
641
  const openContextMenu = $derived(ctrl.openContextMenu);
597
642
 
643
+ /**
644
+ * Everything a body cell shows that depends on its data: the merge it
645
+ * belongs to, the row and column it draws (a merge continuation draws the
646
+ * origin's), the value, the user's class / tooltip / validity / note, and
647
+ * the width. One derived per cell for all of it - each `{@const}` is its own
648
+ * reactive node, and a cell that scrolls in used to create fifteen of them.
649
+ * Selection and editing stay separate: they change on every arrow key and
650
+ * must not re-run the value and the validators of every visible cell.
651
+ * Returns null for a cell a merge covers (nothing is drawn there).
652
+ */
653
+ /**
654
+ * The text of a cell that is only text, or null when the cell needs the full
655
+ * body snippet (a checkbox, chips, a sparkline, a custom `cell` template, a
656
+ * tree / group affordance or indent, or a visible conditional format). The
657
+ * full path walks about ten nested `{#if}` blocks to reach the same text
658
+ * node, each a reactive block of its own; most cells of a wide grid are
659
+ * plain text, and this is the per-cell cost a horizontal scroll pays.
660
+ * Produces exactly what `cellBody`'s last branch renders.
661
+ */
662
+ function plainCellText(row: Row<TData>, column: Column<TData>, value: unknown): string | null {
663
+ const def = column.columnDef;
664
+ if (def.cell != null || def.sparkline) return null;
665
+ const editor = def.editorType;
666
+ if (editor === "checkbox" || editor === "list" || editor === "chips" || typeof value === "boolean") return null;
667
+ if (groupColumnMode && isAutoGroupColumn(column.id)) return null;
668
+ if (treeData && column.id === treeColumnId) return null;
669
+ if (row.depth > 0 && column.id === allColumns[0]?.id && !treeData && !groupColumnMode) return null;
670
+ if (hasConditionalFormats) {
671
+ const cf = cellConditionalFormat(row, column, value);
672
+ if (cf && (cf.background || cf.dataBar || cf.icon || cf.color || cf.fontWeight != null)) return null;
673
+ }
674
+ return formatCellValue(column, value, row);
675
+ }
676
+
677
+ function bodyCellInfo(row: Row<TData>, rowIndex: number, rendered: (typeof renderedColumns)[number]) {
678
+ const colIndex = rendered.item.index;
679
+ const draw = mergeIndex
680
+ ? mergeDrawAt(mergeIndex, rowIndex, colIndex, { ...(rowIndex < frozenRowCount ? mergeFrozenWindow : mergeBodyWindow), ...columnRunOf(colIndex) })
681
+ : null;
682
+ if (draw?.kind === "skip") return null;
683
+ const cellRow = draw?.kind === "continuation" ? (allRows[draw.merge.rowIndex] ?? row) : row;
684
+ const cellColumn = draw?.kind === "continuation" ? (allColumns[draw.merge.colIndex] ?? rendered.column) : rendered.column;
685
+ const cellValue = getCellDisplayValue(cellRow.id, cellColumn.id, getColumnBaseValue(cellRow, cellColumn));
686
+ return {
687
+ colIndex,
688
+ draw,
689
+ cellRow,
690
+ cellColumn,
691
+ cellValue,
692
+ text: plainCellText(cellRow, cellColumn, cellValue),
693
+ userCellClass: computeCellClass(cellRow, cellColumn),
694
+ cellTooltip: computeCellTooltip(cellRow, cellColumn),
695
+ cellValidity: computeCellValidity(cellRow, cellColumn),
696
+ cellNote: computeCellNote(cellRow, cellColumn),
697
+ tdWidth: draw ? mergedWidth(colIndex, draw.colSpan, rendered.item.size) : rendered.item.size,
698
+ };
699
+ }
700
+
598
701
  const onHeaderSortClick = $derived(ctrl.onHeaderSortClick);
599
702
  const onGridKeyDown = $derived(ctrl.onGridKeyDown);
600
703
  const onGridPaste = $derived(ctrl.onGridPaste);
@@ -1334,18 +1437,22 @@
1334
1437
  getValue: () => cellValue,
1335
1438
  })}
1336
1439
  {#if rendered instanceof RenderSnippetConfig}
1337
- {@render rendered.snippet(rendered.params)}
1440
+ <!-- A <td> is handed to another column when its column leaves the
1441
+ column window (the body {#each} key), so a renderer of the app's
1442
+ own is re-created per column rather than handed another
1443
+ column's props with its old state. -->
1444
+ {#key column.id}{@render rendered.snippet(rendered.params)}{/key}
1338
1445
  {:else if rendered instanceof RenderComponentConfig}
1339
- <rendered.component {...rendered.props ?? {}} />
1446
+ {#key column.id}<rendered.component {...rendered.props ?? {}} />{/key}
1340
1447
  {:else if typeof rendered === "string" || typeof rendered === "number"}
1341
- {rendered}
1448
+ <span class="sv-grid-cell-text">{rendered}</span>
1342
1449
  {:else}
1343
- {formatCellValue(column, cellValue, row)}
1450
+ <span class="sv-grid-cell-text">{formatCellValue(column, cellValue, row)}</span>
1344
1451
  {/if}
1345
1452
  {:else if typeof cellTemplate === "string"}
1346
- {cellTemplate}
1453
+ <span class="sv-grid-cell-text">{cellTemplate}</span>
1347
1454
  {:else}
1348
- {formatCellValue(column, cellValue, row)}
1455
+ <span class="sv-grid-cell-text">{formatCellValue(column, cellValue, row)}</span>
1349
1456
  {/if}
1350
1457
  {/if}
1351
1458
  {/snippet}
@@ -1605,11 +1712,16 @@
1605
1712
  {#if showDetailToggleEffective}
1606
1713
  <td class="sv-grid-cell sv-grid-detail-toggle-cell"></td>
1607
1714
  {/if}
1608
- {#each allColumns as col (col.id)}
1609
- <td class="sv-grid-cell sv-grid-placeholder-cell">
1715
+ <!-- Same slots as a data row (spacers + the rendered window), so the
1716
+ skeleton lines up and a wide grid does not draw every column. -->
1717
+ {#if columnVirtualizationEnabled && columnWindowStart > 0}{@render cellGap(columnWindowStart)}{/if}
1718
+ {#each renderedColumns as rendered (rendered.key)}
1719
+ {#if rendered.gap > 0}{@render cellGap(rendered.gap)}{/if}
1720
+ <td class="sv-grid-cell sv-grid-placeholder-cell" style={`width: ${rendered.item.size}px; ${cellPinStyle(rendered.column.id)}`}>
1610
1721
  <span class="sv-grid-placeholder-skeleton" aria-hidden="true"></span>
1611
1722
  </td>
1612
1723
  {/each}
1724
+ {#if showRightSpacer}{@render cellGap(columnWindowRightSpacer)}{/if}
1613
1725
  {/if}
1614
1726
  </tr>
1615
1727
  {/snippet}
@@ -1640,7 +1752,7 @@
1640
1752
  {@const stickyTop = stickyTopOfRow(rowIndex)}
1641
1753
  <tr
1642
1754
  class="sv-grid-row sv-grid-group-row"
1643
- class:sv-grid-row-selected={isRowSelected(row.id)}
1755
+ class:sv-grid-row-selected={isRowSelected(row.id, row.original)}
1644
1756
  class:sv-grid-row-frozen={rowIndex < frozenRowCount}
1645
1757
  class:sv-grid-row-frozen-last={frozenRowCount > 0 && rowIndex === frozenRowCount - 1}
1646
1758
  class:sv-grid-row-sticky-group={stickyTop !== undefined}
@@ -1669,7 +1781,7 @@
1669
1781
  {@const stickyTop = stickyTopOfRow(rowIndex)}
1670
1782
  <tr
1671
1783
  class={`sv-grid-row ${userRowClass} ${rowDropClass(rowIndex)}`}
1672
- class:sv-grid-row-selected={isRowSelected(row.id)}
1784
+ class:sv-grid-row-selected={isRowSelected(row.id, row.original)}
1673
1785
  class:sv-grid-row-alt={opt.zebraRows &&
1674
1786
  rowIndex % 2 === 1}
1675
1787
  class:sv-grid-row-draggable={rowDragManagedEffective}
@@ -1692,31 +1804,33 @@
1692
1804
  <td
1693
1805
  class="sv-grid-cell sv-grid-row-number-cell"
1694
1806
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; inset-inline-start: 0;`}
1695
- >{rowIndex + 1}</td
1807
+ ><span class="sv-grid-cell-line">{rowIndex + 1}</span></td
1696
1808
  >
1697
1809
  {/if}
1698
1810
  {#if showRowSelectionEffective}
1699
1811
  <td
1700
1812
  class="sv-grid-cell sv-grid-selection-cell"
1701
1813
  style={`width: ${selectionColumnWidth}px; min-width: ${selectionColumnWidth}px; max-width: ${selectionColumnWidth}px; inset-inline-start: ${showRowNumbersEffective ? rowNumberColumnWidth : 0}px;`}
1702
- onclick={() => toggleRowSelectionById(row.id)}
1814
+ onclick={() => toggleRowSelectionById(row.id, row.original)}
1703
1815
  >
1704
- <button
1705
- type="button"
1706
- class="sv-grid-checkbox"
1707
- role="checkbox"
1708
- aria-checked={isRowSelected(row.id)}
1709
- aria-label="Select row"
1710
- onclick={(event) => {
1711
- event.stopPropagation();
1712
- toggleRowSelectionById(row.id);
1713
- }}
1714
- onkeydown={(event) =>
1715
- toggleCheckboxWithKeyboard(event, () => {
1816
+ <span class="sv-grid-cell-line"
1817
+ ><button
1818
+ type="button"
1819
+ class="sv-grid-checkbox"
1820
+ role="checkbox"
1821
+ aria-checked={isRowSelected(row.id, row.original)}
1822
+ aria-label="Select row"
1823
+ onclick={(event) => {
1716
1824
  event.stopPropagation();
1717
- toggleRowSelectionById(row.id);
1718
- })}
1719
- ></button>
1825
+ toggleRowSelectionById(row.id, row.original);
1826
+ }}
1827
+ onkeydown={(event) =>
1828
+ toggleCheckboxWithKeyboard(event, () => {
1829
+ event.stopPropagation();
1830
+ toggleRowSelectionById(row.id, row.original);
1831
+ })}
1832
+ ></button></span
1833
+ >
1720
1834
  </td>
1721
1835
  {/if}
1722
1836
  {#if showDetailToggleEffective}
@@ -1726,15 +1840,17 @@
1726
1840
  style={`width: ${detailToggleColumnWidth}px; min-width: ${detailToggleColumnWidth}px; max-width: ${detailToggleColumnWidth}px; left: ${detailToggleColumnLeft}px;`}
1727
1841
  >
1728
1842
  {#if dt}
1729
- <button
1730
- type="button"
1731
- class="sv-grid-detail-toggle"
1732
- aria-expanded={dt.open ? "true" : "false"}
1733
- aria-label={dt.open ? messages.closeDetail : messages.openDetail}
1734
- onclick={(event) => {
1735
- event.stopPropagation();
1736
- dt.toggle();
1737
- }}>{@render icon("chevron-right")}</button
1843
+ <span class="sv-grid-cell-line"
1844
+ ><button
1845
+ type="button"
1846
+ class="sv-grid-detail-toggle"
1847
+ aria-expanded={dt.open ? "true" : "false"}
1848
+ aria-label={dt.open ? messages.closeDetail : messages.openDetail}
1849
+ onclick={(event) => {
1850
+ event.stopPropagation();
1851
+ dt.toggle();
1852
+ }}>{@render icon("chevron-right")}</button
1853
+ ></span
1738
1854
  >
1739
1855
  {/if}
1740
1856
  </td>
@@ -1746,151 +1862,121 @@
1746
1862
  style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
1747
1863
  ></td>
1748
1864
  {/if}
1749
- {#each renderedColumns as rendered (rendered.column.id)}
1750
- {@const colIndex = rendered.item.index}
1751
- {@const draw = mergeIndex
1752
- ? mergeDrawAt(mergeIndex, rowIndex, colIndex, rowIndex < frozenRowCount ? mergeFrozenWindow : mergeBodyWindow)
1753
- : null}
1754
- {#if draw?.kind !== "skip"}
1755
- {@const cellRow = draw?.kind === "continuation" ? (allRows[draw.merge.rowIndex] ?? row) : row}
1756
- {@const cellColumn = draw?.kind === "continuation" ? (allColumns[draw.merge.colIndex] ?? rendered.column) : rendered.column}
1757
- {@const baseValue = getColumnBaseValue(
1758
- cellRow,
1759
- cellColumn,
1760
- )}
1761
- {@const cellValue = getCellDisplayValue(
1762
- cellRow.id,
1763
- cellColumn.id,
1764
- baseValue,
1765
- )}
1766
- {@const isEditing =
1767
- ctrl.editingCell?.rowId === cellRow.id &&
1768
- ctrl.editingCell?.columnId === cellColumn.id}
1769
- {@const inRowEdit =
1770
- !!fullRowEdit &&
1771
- fullRowEdit.rowId === cellRow.id &&
1772
- cellColumn.id in fullRowEdit.draft}
1773
- {@const cellSel = cellSelectionState(rowIndex, colIndex)}
1774
- {@const fillEdges = fillMarqueeEdges(rowIndex, colIndex)}
1775
- {@const userCellClass = computeCellClass(
1776
- cellRow,
1777
- cellColumn,
1778
- )}
1779
- {@const cellTooltip = computeCellTooltip(
1780
- cellRow,
1781
- cellColumn,
1782
- )}
1783
- {@const cellValidity = computeCellValidity(
1784
- cellRow,
1785
- cellColumn,
1786
- )}
1787
- {@const cellNote = computeCellNote(
1788
- cellRow,
1789
- cellColumn,
1790
- )}
1791
- {@const tdWidth = draw ? mergedWidth(colIndex, draw.colSpan, rendered.item.size) : rendered.item.size}
1865
+ {#each renderedColumns as rendered (rendered.key)}
1866
+ {@const c = bodyCellInfo(row, rowIndex, rendered)}
1867
+ {#if rendered.gap > 0}{@render cellGap(rendered.gap)}{/if}
1868
+ {#if c}
1869
+ {@const ui = cellUi(rowIndex, c)}
1792
1870
  <td
1793
- rowspan={draw && draw.rowSpan > 1 ? draw.rowSpan : undefined}
1794
- colspan={draw && draw.colSpan > 1 ? draw.colSpan : undefined}
1795
- data-merge-origin={draw ? `${draw.merge.rowIndex}:${draw.merge.colIndex}` : undefined}
1796
- class:sv-grid-cell-merged={!!draw}
1797
- class={`sv-grid-cell ${userCellClass}`}
1798
- class:sv-grid-cell-editing={isEditing || inRowEdit}
1871
+ rowspan={c.draw && c.draw.rowSpan > 1 ? c.draw.rowSpan : undefined}
1872
+ colspan={c.draw && c.draw.colSpan > 1 ? c.draw.colSpan : undefined}
1873
+ data-merge-origin={c.draw ? `${c.draw.merge.rowIndex}:${c.draw.merge.colIndex}` : undefined}
1874
+ class:sv-grid-cell-merged={!!c.draw}
1875
+ class={`sv-grid-cell ${c.userCellClass}`}
1876
+ class:sv-grid-cell-editing={!!ui && (ui.editing || ui.rowEdit)}
1799
1877
  class:sv-grid-cell-collapsed={!!collapsedColumns[rendered.column.id]}
1800
- class:sv-grid-cell-active={!!cellSel?.active}
1801
- class:sv-grid-cell-has-fill-handle={!!cellSel?.fillHandle}
1878
+ class:sv-grid-cell-active={!!ui?.sel?.active}
1879
+ class:sv-grid-cell-has-fill-handle={!!ui?.sel?.fillHandle}
1802
1880
  class:sv-grid-cell-cf={hasConditionalFormats}
1803
- class:sv-grid-cell-invalid={cellValidity.invalid}
1804
- class:sv-grid-cell-has-note={cellNote != null}
1805
- aria-invalid={cellValidity.invalid ? "true" : undefined}
1881
+ class:sv-grid-cell-invalid={c.cellValidity.invalid}
1882
+ class:sv-grid-cell-has-note={c.cellNote != null}
1883
+ aria-invalid={c.cellValidity.invalid ? "true" : undefined}
1806
1884
  data-svgrid-row={rowIndex}
1807
- data-svgrid-col={colIndex}
1885
+ data-svgrid-col={c.colIndex}
1808
1886
  data-col-id={rendered.column.id}
1809
1887
  data-align={getColumnAlign(rendered.column)}
1810
- data-pinned={isColumnPinned(rendered.column.id) ??
1811
- undefined}
1812
- data-selected-range={cellSel?.edges ? "true" : undefined}
1813
- data-range-top={cellSel?.edges?.top ? "true" : undefined}
1814
- data-range-bottom={cellSel?.edges?.bottom
1888
+ data-pinned={rendered.pinned ?? undefined}
1889
+ data-selected-range={ui?.sel?.edges ? "true" : undefined}
1890
+ data-range-top={ui?.sel?.edges?.top ? "true" : undefined}
1891
+ data-range-bottom={ui?.sel?.edges?.bottom
1815
1892
  ? "true"
1816
1893
  : undefined}
1817
- data-range-left={cellSel?.edges?.left
1894
+ data-range-left={ui?.sel?.edges?.left
1818
1895
  ? "true"
1819
1896
  : undefined}
1820
- data-range-right={cellSel?.edges?.right
1897
+ data-range-right={ui?.sel?.edges?.right
1821
1898
  ? "true"
1822
1899
  : undefined}
1823
- data-fill-preview={isInFillPreview(rowIndex, colIndex)
1900
+ data-fill-preview={isInFillPreview(rowIndex, c.colIndex)
1824
1901
  ? "true"
1825
1902
  : undefined}
1826
- data-fill-top={fillEdges?.top ? "true" : undefined}
1827
- data-fill-bottom={fillEdges?.bottom ? "true" : undefined}
1828
- data-fill-left={fillEdges?.left ? "true" : undefined}
1829
- data-fill-right={fillEdges?.right ? "true" : undefined}
1830
- style={`width: ${tdWidth}px; min-width: ${tdWidth}px; max-width: ${tdWidth}px; ${cellPinStyle(rendered.column.id)}`}
1903
+ data-fill-top={ui?.fill?.top ? "true" : undefined}
1904
+ data-fill-bottom={ui?.fill?.bottom ? "true" : undefined}
1905
+ data-fill-left={ui?.fill?.left ? "true" : undefined}
1906
+ data-fill-right={ui?.fill?.right ? "true" : undefined}
1907
+ style={`width: ${c.tdWidth}px; min-width: ${c.tdWidth}px; max-width: ${c.tdWidth}px; ${rendered.pinned ? cellPinStyle(rendered.column.id) : ""}`}
1831
1908
  onpointerdown={(event) =>
1832
- onCellPointerDown(rowIndex, colIndex, event)}
1833
- onpointerenter={(event) => {
1834
- onCellPointerEnter(rowIndex, colIndex);
1909
+ onCellPointerDown(rowIndex, c.colIndex, event)}
1910
+ onpointerover={(event) => {
1911
+ // `pointerover`/`pointerout` rather than enter/leave:
1912
+ // Svelte delegates them to one listener on the root,
1913
+ // where enter/leave added two listeners to every
1914
+ // cell on mount (~1.5 ms of a 100k-row mount). A
1915
+ // move between the cell's own children is not an
1916
+ // enter.
1917
+ if (!crossedCell(event)) return;
1918
+ onCellPointerEnter(rowIndex, c.colIndex);
1835
1919
  // The column tooltip fires on whole-cell hover; a
1836
1920
  // per-cell note waits for its corner below. A
1837
1921
  // validation message wins over the column's tooltip.
1838
1922
  const tip =
1839
- cellValidity.invalid && cellValidity.message
1840
- ? cellValidity.message
1841
- : cellTooltip;
1923
+ c.cellValidity.invalid && c.cellValidity.message
1924
+ ? c.cellValidity.message
1925
+ : c.cellTooltip;
1842
1926
  if (tip)
1843
1927
  showTooltipFor(
1844
1928
  event.currentTarget as HTMLElement,
1845
1929
  tip,
1846
1930
  );
1847
1931
  }}
1848
- onpointerleave={hideTooltip}
1932
+ onpointerout={(event) => {
1933
+ if (crossedCell(event)) hideTooltip();
1934
+ }}
1849
1935
  ondblclick={() =>
1850
- emitCellDoubleClick(rowIndex, colIndex)}
1851
- onclick={() => onCellClick(rowIndex, colIndex)}
1936
+ emitCellDoubleClick(rowIndex, c.colIndex)}
1937
+ onclick={() => onCellClick(rowIndex, c.colIndex)}
1852
1938
  oncontextmenu={(event) =>
1853
1939
  openContextMenu(
1854
1940
  event,
1855
1941
  rowIndex,
1856
- colIndex,
1942
+ c.colIndex,
1857
1943
  rendered.column.id,
1858
1944
  )}
1859
1945
  use:cellFlashAction={{
1860
- rowId: cellRow.id,
1861
- value: cellValue,
1862
- active: !!cellColumn.columnDef.cellFlash,
1946
+ rowId: c.cellRow.id,
1947
+ columnId: c.cellColumn.id,
1948
+ value: c.cellValue,
1949
+ active: !!c.cellColumn.columnDef.cellFlash,
1863
1950
  className: flashClassFor(
1864
- cellColumn.columnDef.cellFlash,
1951
+ c.cellColumn.columnDef.cellFlash,
1865
1952
  ),
1866
1953
  }}
1867
1954
  role="gridcell"
1868
- id={getGridCellDomId(ctrl.gridDomId, rowIndex, colIndex)}
1869
- aria-colindex={colIndex + 1}
1955
+ id={getGridCellDomId(ctrl.gridDomId, rowIndex, c.colIndex)}
1956
+ aria-colindex={c.colIndex + 1}
1870
1957
  aria-rowindex={rowIndex + 1}
1871
- aria-selected={isRowSelected(row.id)}
1958
+ aria-selected={isRowSelected(row.id, row.original)}
1872
1959
  >
1873
- {#if inRowEdit || isEditing}
1960
+ {#if ui && (ui.editing || ui.rowEdit)}
1874
1961
  <!-- The editing cell stays empty until the lazy
1875
1962
  editor chunk lands, exactly as it already did
1876
1963
  for the dropdown / date editors. Falling back
1877
1964
  to the read-only value here would flash stale
1878
1965
  content into a cell the user is editing. -->
1879
1966
  {#if CellEditor}
1880
- <CellEditor {ctrl} column={cellColumn} row={cellRow} fullRow={inRowEdit} />
1967
+ <CellEditor {ctrl} column={c.cellColumn} row={c.cellRow} fullRow={!!ui?.rowEdit} />
1881
1968
  {/if}
1882
1969
  {:else}
1883
- {#if plainCellBody}
1884
- {@render cellBody(cellRow, cellColumn, cellValue)}
1885
- {:else}
1886
- {@render cellBodyWithFormat(
1887
- cellRow,
1888
- cellColumn,
1889
- cellValue,
1890
- )}
1891
- {/if}
1970
+ <!-- One line box for the cell's content, as a table
1971
+ cell has: centred in the cell, with inline content
1972
+ (pills, chips, checkboxes) aligned in it the same
1973
+ way under table or flex rows, and the ellipsis and
1974
+ text-align applied to it. -->
1975
+ <span class="sv-grid-cell-line"
1976
+ >{#if c.text !== null}{c.text}{:else if plainCellBody}{@render cellBody(c.cellRow, c.cellColumn, c.cellValue)}{:else}{@render cellBodyWithFormat(c.cellRow, c.cellColumn, c.cellValue)}{/if}</span
1977
+ >
1892
1978
  {/if}
1893
- {#if !isEditing && cellSel?.fillHandle}
1979
+ {#if !ui?.editing && ui?.sel?.fillHandle}
1894
1980
  <!-- Excel-style fill handle: drag down/right to
1895
1981
  extend the selection and pattern-fill the new
1896
1982
  cells on release. Rendered inside the bottom-
@@ -1903,7 +1989,7 @@
1903
1989
  aria-label="Fill handle"
1904
1990
  tabindex={-1}
1905
1991
  onpointerdown={(event) =>
1906
- startFillDrag(event, rowIndex, colIndex)}
1992
+ startFillDrag(event, rowIndex, c.colIndex)}
1907
1993
  ondblclick={(event) => {
1908
1994
  // Not the cell's double-click: this fills
1909
1995
  // down as far as the neighbour column goes.
@@ -1913,16 +1999,16 @@
1913
1999
  }}
1914
2000
  ></div>
1915
2001
  {/if}
1916
- {#if cellValidity.invalid && cellValidity.message}
2002
+ {#if c.cellValidity.invalid && c.cellValidity.message}
1917
2003
  <!-- The validation message, for assistive tech: the
1918
2004
  tooltip above is mouse-only, and unlike a title
1919
2005
  a visually-hidden span adds no second native
1920
2006
  tooltip competing with the custom one. -->
1921
2007
  <span class="sv-grid-sr-only"
1922
- >{cellValidity.message}</span
2008
+ >{c.cellValidity.message}</span
1923
2009
  >
1924
2010
  {/if}
1925
- {#if cellNote != null && !isEditing}
2011
+ {#if c.cellNote != null && !ui?.editing}
1926
2012
  <span
1927
2013
  class="sv-grid-cell-note-corner"
1928
2014
  aria-label="Note"
@@ -1930,7 +2016,7 @@
1930
2016
  event.stopPropagation();
1931
2017
  showTooltipFor(
1932
2018
  event.currentTarget as HTMLElement,
1933
- cellNote,
2019
+ c.cellNote ?? "",
1934
2020
  );
1935
2021
  }}
1936
2022
  onpointerleave={(event) => {
@@ -1942,7 +2028,7 @@
1942
2028
  </td>
1943
2029
  {/if}
1944
2030
  {/each}
1945
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2031
+ {#if showRightSpacer}
1946
2032
  <td
1947
2033
  class="sv-grid-cell sv-grid-cell-spacer"
1948
2034
  aria-hidden="true"
@@ -1957,6 +2043,23 @@
1957
2043
  outside the rendered window), so this one carries no cell ids, no
1958
2044
  row index attributes and is hidden from assistive tech; the group
1959
2045
  cell's own expander still works, since it is the same renderer. -->
2046
+ <!-- The spacer for the columns skipped between two rendered runs (pinned
2047
+ left, the virtual window, pinned right). Every row template draws it
2048
+ before the same column, so the fixed table layout keeps its slots. -->
2049
+ {#snippet cellGap(px: number)}
2050
+ <td
2051
+ class="sv-grid-cell sv-grid-cell-spacer"
2052
+ aria-hidden="true"
2053
+ style={`width: ${px}px; min-width: ${px}px; max-width: ${px}px;`}
2054
+ ></td>
2055
+ {/snippet}
2056
+ {#snippet columnGap(px: number)}
2057
+ <th
2058
+ class="sv-grid-column sv-grid-column-spacer"
2059
+ aria-hidden="true"
2060
+ style={`width: ${px}px; min-width: ${px}px; max-width: ${px}px;`}
2061
+ ></th>
2062
+ {/snippet}
1960
2063
  {#snippet stickyGroupRow(row: Row<TData>, rowIndex: number, top: number, size: number, last: boolean)}
1961
2064
  {#if isGroupRow(row) && !groupColumnMode}
1962
2065
  <tr
@@ -1989,7 +2092,7 @@
1989
2092
  <td
1990
2093
  class="sv-grid-cell sv-grid-row-number-cell"
1991
2094
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; left: 0;`}
1992
- >{rowIndex + 1}</td
2095
+ ><span class="sv-grid-cell-line">{rowIndex + 1}</span></td
1993
2096
  >
1994
2097
  {/if}
1995
2098
  {#if showRowSelectionEffective}
@@ -2012,6 +2115,7 @@
2012
2115
  {/if}
2013
2116
  {#each renderedColumns as rendered (rendered.column.id)}
2014
2117
  {@const cellValue = getCellDisplayValue(row.id, rendered.column.id, getColumnBaseValue(row, rendered.column))}
2118
+ {#if rendered.gap > 0}{@render cellGap(rendered.gap)}{/if}
2015
2119
  <td
2016
2120
  class={`sv-grid-cell ${computeCellClass(row, rendered.column)}`}
2017
2121
  class:sv-grid-cell-cf={hasConditionalFormats}
@@ -2027,7 +2131,7 @@
2027
2131
  {/if}
2028
2132
  </td>
2029
2133
  {/each}
2030
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2134
+ {#if showRightSpacer}
2031
2135
  <td
2032
2136
  class="sv-grid-cell sv-grid-cell-spacer"
2033
2137
  style={`width: ${columnWindowRightSpacer}px; min-width: ${columnWindowRightSpacer}px; max-width: ${columnWindowRightSpacer}px;`}
@@ -2050,7 +2154,9 @@
2050
2154
  <td
2051
2155
  class="sv-grid-cell sv-grid-row-number-cell"
2052
2156
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; inset-inline-start: 0;`}
2053
- >{#if where === "top"}{@render icon("pinned-row-top")}{:else}{@render icon("pinned-row-bottom")}{/if}</td
2157
+ ><span class="sv-grid-cell-line"
2158
+ >{#if where === "top"}{@render icon("pinned-row-top")}{:else}{@render icon("pinned-row-bottom")}{/if}</span
2159
+ ></td
2054
2160
  >
2055
2161
  {/if}
2056
2162
  {#if showRowSelectionEffective}
@@ -2078,16 +2184,17 @@
2078
2184
  rowData,
2079
2185
  rendered.column,
2080
2186
  )}
2187
+ {#if rendered.gap > 0}{@render cellGap(rendered.gap)}{/if}
2081
2188
  <td
2082
2189
  class={`sv-grid-cell ${userCellClass}`}
2083
2190
  data-col-id={rendered.column.id}
2084
2191
  data-align={getColumnAlign(rendered.column)}
2085
2192
  data-pinned={isColumnPinned(rendered.column.id) ?? undefined}
2086
2193
  style={`width: ${rendered.item.size}px; min-width: ${rendered.item.size}px; max-width: ${rendered.item.size}px; ${cellPinStyle(rendered.column.id)}`}
2087
- >{formatPinnedValue(rendered.column, value)}</td
2194
+ ><span class="sv-grid-cell-text">{formatPinnedValue(rendered.column, value)}</span></td
2088
2195
  >
2089
2196
  {/each}
2090
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2197
+ {#if showRightSpacer}
2091
2198
  <td
2092
2199
  class="sv-grid-cell sv-grid-cell-spacer"
2093
2200
  aria-hidden="true"
@@ -2204,7 +2311,9 @@
2204
2311
  <table
2205
2312
  bind:this={ctrl.gridRootEl}
2206
2313
  class="sv-grid-table"
2314
+ class:sv-grid-table-flex={ctrl.flexRows}
2207
2315
  class:sv-grid-no-row-hover={opt.enableRowHover !== true}
2316
+ class:sv-grid-numbers-beside-toggle={showDetailToggleEffective && !showRowSelectionEffective}
2208
2317
  {...getGridRootA11yProps({
2209
2318
  activeDescendantId,
2210
2319
  rowCount: allRows.length,
@@ -2213,7 +2322,7 @@
2213
2322
  })}
2214
2323
  onkeydown={onGridKeyDown}
2215
2324
  onpaste={onGridPaste}
2216
- style={`min-width: ${totalColumnWidth}px;`}
2325
+ style={`min-width: ${totalColumnWidth + ctrl.systemColumnsWidth}px;`}
2217
2326
  >
2218
2327
  <!-- svelte-ignore a11y_no_redundant_roles -->
2219
2328
  <thead class="sv-grid-head" bind:this={ctrl.theadEl} role="rowgroup">
@@ -2258,6 +2367,7 @@
2258
2367
  ></th>
2259
2368
  {/if}
2260
2369
  {#each row.cells as cell (cell.key)}
2370
+ {#if cell.gapBefore}{@render columnGap(cell.gapBefore)}{/if}
2261
2371
  <th
2262
2372
  class="sv-grid-column sv-grid-group-header-cell"
2263
2373
  class:sv-grid-group-header-placeholder={cell.isPlaceholder}
@@ -2294,7 +2404,7 @@
2294
2404
  {/if}
2295
2405
  </th>
2296
2406
  {/each}
2297
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2407
+ {#if showRightSpacer}
2298
2408
  <th
2299
2409
  class="sv-grid-column sv-grid-column-spacer"
2300
2410
  aria-hidden="true"
@@ -2362,8 +2472,9 @@
2362
2472
  style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
2363
2473
  ></th>
2364
2474
  {/if}
2365
- {#each renderedColumns as rendered (rendered.column.id)}
2475
+ {#each renderedColumns as rendered (rendered.key)}
2366
2476
  {@const header = headerGroup.headers[rendered.item.index]}
2477
+ {#if rendered.gap > 0}{@render columnGap(rendered.gap)}{/if}
2367
2478
  {#if header}
2368
2479
  {@const sortDirection =
2369
2480
  sortDirectionByColumn[header.column.id]}
@@ -2456,9 +2567,12 @@
2456
2567
  }}
2457
2568
  >
2458
2569
  {#if rendered instanceof RenderSnippetConfig}
2459
- {@render rendered.snippet(rendered.params)}
2570
+ <!-- Header cells are handed between columns as the
2571
+ window moves (slot-keyed, like body cells), so an
2572
+ app's own header renderer is re-created per column. -->
2573
+ {#key header.column.id}{@render rendered.snippet(rendered.params)}{/key}
2460
2574
  {:else if rendered instanceof RenderComponentConfig}
2461
- <rendered.component {...rendered.props ?? {}} />
2575
+ {#key header.column.id}<rendered.component {...rendered.props ?? {}} />{/key}
2462
2576
  {:else if typeof rendered === "string" || typeof rendered === "number"}
2463
2577
  {rendered}
2464
2578
  {:else}
@@ -2570,7 +2684,7 @@
2570
2684
  </th>
2571
2685
  {/if}
2572
2686
  {/each}
2573
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2687
+ {#if showRightSpacer}
2574
2688
  <th
2575
2689
  class="sv-grid-column sv-grid-column-spacer"
2576
2690
  aria-hidden="true"
@@ -2611,10 +2725,11 @@
2611
2725
  style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
2612
2726
  ></th>
2613
2727
  {/if}
2614
- {#each renderedColumns as rendered (rendered.column.id)}
2728
+ {#each renderedColumns as rendered (rendered.key)}
2615
2729
  {@const activeOperator =
2616
2730
  filterMenuValues[rendered.column.id]?.operator ??
2617
2731
  defaultOperatorFor(rendered.column)}
2732
+ {#if rendered.gap > 0}{@render columnGap(rendered.gap)}{/if}
2618
2733
  <th
2619
2734
  class="sv-grid-column"
2620
2735
  data-pinned={isColumnPinned(rendered.column.id) ??
@@ -2794,7 +2909,7 @@
2794
2909
  </div>
2795
2910
  </th>
2796
2911
  {/each}
2797
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2912
+ {#if showRightSpacer}
2798
2913
  <th
2799
2914
  class="sv-grid-column sv-grid-column-spacer"
2800
2915
  aria-hidden="true"
@@ -2937,17 +3052,18 @@
2937
3052
  style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
2938
3053
  ></th>
2939
3054
  {/if}
2940
- {#each renderedColumns as rendered (rendered.column.id)}
3055
+ {#each renderedColumns as rendered (rendered.key)}
3056
+ {#if rendered.gap > 0}{@render columnGap(rendered.gap)}{/if}
2941
3057
  <th
2942
3058
  class="sv-grid-column sv-grid-summary-column"
2943
3059
  data-pinned={isColumnPinned(rendered.column.id) ??
2944
3060
  undefined}
2945
3061
  style={`width: ${rendered.item.size}px; min-width: ${rendered.item.size}px; max-width: ${rendered.item.size}px; ${cellPinStyle(rendered.column.id)}`}
2946
3062
  >
2947
- {summaryByColumn[rendered.column.id] ?? ""}
3063
+ <span class="sv-grid-cell-text">{summaryByColumn[rendered.column.id] ?? ""}</span>
2948
3064
  </th>
2949
3065
  {/each}
2950
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
3066
+ {#if showRightSpacer}
2951
3067
  <th
2952
3068
  class="sv-grid-column sv-grid-column-spacer"
2953
3069
  aria-hidden="true"
@@ -3124,10 +3240,12 @@
3124
3240
  <div class="sv-grid-skeleton" aria-hidden="true">
3125
3241
  {#each Array(opt.loadingSkeletonRows ?? 8) as _, r (r)}
3126
3242
  <div class="sv-grid-skeleton-row">
3127
- {#each allColumns as col (col.id)}
3243
+ <!-- The rendered window, not every column: a 10,000-column grid
3244
+ drew 80,000 skeleton cells here while it loaded. -->
3245
+ {#each renderedColumns as rendered (rendered.key)}
3128
3246
  <div
3129
3247
  class="sv-grid-skeleton-cell"
3130
- style={`width:${getColumnWidth(col.id)}px`}
3248
+ style={`width:${rendered.item.size}px`}
3131
3249
  >
3132
3250
  <span class="sv-grid-skeleton-bar"></span>
3133
3251
  </div>