@svgrid/grid 3.0.7 → 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 (91) hide show
  1. package/README.md +1 -1
  2. package/dist/GridMenus.svelte +3 -2
  3. package/dist/SvGrid.controller.svelte.d.ts +60 -8
  4. package/dist/SvGrid.controller.svelte.js +490 -114
  5. package/dist/SvGrid.css +186 -3
  6. package/dist/SvGrid.svelte +285 -164
  7. package/dist/SvGrid.types.d.ts +24 -7
  8. package/dist/cdn/{GridMenus-CcwU8I4t.js → GridMenus-B1sipj6y.js} +165 -165
  9. package/dist/cdn/{GridMenus-BgUS-W3o.js → GridMenus-DlAzfHLk.js} +201 -201
  10. package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
  11. package/dist/cdn/{src-kE9B98r6.js → src-BYsa66I0.js} +9747 -10178
  12. package/dist/cdn/{src-25JmJ_7g.js → src-pA4RroLV.js} +10709 -11140
  13. package/dist/cdn/svgrid.js +27 -27
  14. package/dist/cdn/svgrid.svelte-external.js +27 -27
  15. package/dist/cdn/validate-XuMe4_KR.js +80 -0
  16. package/dist/cell-render.js +25 -3
  17. package/dist/columns.js +16 -1
  18. package/dist/conditional-formatting.js +9 -2
  19. package/dist/core.d.ts +93 -0
  20. package/dist/core.js +313 -47
  21. package/dist/filtering/excel-filters.js +41 -6
  22. package/dist/headless.d.ts +1 -1
  23. package/dist/headless.js +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +2 -1
  26. package/dist/keyboard-handlers.js +5 -1
  27. package/dist/selection.d.ts +2 -2
  28. package/dist/selection.js +46 -12
  29. package/dist/server-block-cache.d.ts +7 -32
  30. package/dist/server-block-cache.js +21 -12
  31. package/dist/server-data-source.js +3 -1
  32. package/dist/server.d.ts +1 -0
  33. package/dist/server.js +1 -0
  34. package/dist/spreadsheet.js +7 -0
  35. package/dist/{subscribe.js → subscribe.svelte.js} +5 -1
  36. package/dist/validate.js +13 -14
  37. package/dist/virtualization/column-virtualizer.d.ts +4 -0
  38. package/dist/virtualization/column-virtualizer.js +2 -0
  39. package/dist/virtualization/types.d.ts +20 -0
  40. package/dist/virtualization/virtualizer.js +90 -24
  41. package/dist/windowed-brand.d.ts +22 -0
  42. package/dist/windowed-brand.js +60 -0
  43. package/dist/windowed-data.d.ts +6 -0
  44. package/dist/windowed-data.js +34 -0
  45. package/dist/windowed-row-model.d.ts +1 -0
  46. package/dist/windowed-row-model.js +97 -0
  47. package/package.json +1 -1
  48. package/src/GridMenus.svelte +3 -2
  49. package/src/SvGrid.controller.svelte.ts +483 -122
  50. package/src/SvGrid.css +186 -3
  51. package/src/SvGrid.svelte +285 -164
  52. package/src/SvGrid.types.ts +24 -7
  53. package/src/cell-render.ts +24 -2
  54. package/src/columns.ts +16 -1
  55. package/src/conditional-formatting.ts +6 -4
  56. package/src/core.sort.test.ts +61 -0
  57. package/src/core.ts +309 -49
  58. package/src/counting-cell.test.svelte +11 -0
  59. package/src/filtering/excel-filters.ts +39 -6
  60. package/src/headless.reactivity.svelte.test.ts +89 -0
  61. package/src/headless.ts +1 -1
  62. package/src/index.ts +7 -1
  63. package/src/keyboard-handlers.ts +5 -1
  64. package/src/new-features.test.ts +24 -0
  65. package/src/selection.test.ts +51 -0
  66. package/src/selection.ts +47 -14
  67. package/src/server-block-cache.ts +27 -10
  68. package/src/server-data-source.ts +3 -1
  69. package/src/server.ts +6 -0
  70. package/src/spreadsheet.ts +6 -0
  71. package/src/{subscribe.test.ts → subscribe.svelte.test.ts} +1 -1
  72. package/src/{subscribe.ts → subscribe.svelte.ts} +5 -1
  73. package/src/svgrid.behavior.test.ts +58 -0
  74. package/src/svgrid.column-cell-recycling.test.ts +120 -0
  75. package/src/svgrid.interaction.test.ts +4 -1
  76. package/src/svgrid.new-features.wrapper.test.ts +5 -4
  77. package/src/svgrid.pinned-column-virtualization.test.ts +162 -0
  78. package/src/svgrid.row-model-prop.svelte.test.ts +15 -3
  79. package/src/validate.test.ts +20 -4
  80. package/src/validate.ts +13 -16
  81. package/src/virtualization/column-virtualizer.ts +6 -0
  82. package/src/virtualization/types.ts +20 -0
  83. package/src/virtualization/virtualizer.test.ts +152 -0
  84. package/src/virtualization/virtualizer.ts +93 -27
  85. package/src/windowed-brand.ts +67 -0
  86. package/src/windowed-data.test.ts +105 -0
  87. package/src/windowed-data.ts +36 -0
  88. package/src/windowed-row-model.ts +113 -0
  89. package/dist/cdn/server-block-cache-DIqb3VZn.js +0 -241
  90. package/dist/cdn/validate-AZoD8BoR.js +0 -80
  91. /package/dist/{subscribe.d.ts → subscribe.svelte.d.ts} +0 -0
@@ -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);
@@ -1212,7 +1315,10 @@
1212
1315
  column: Column<TData>,
1213
1316
  cellValue: unknown,
1214
1317
  )}
1215
- {#if column.columnDef.editorType === "checkbox" || typeof cellValue === "boolean"}
1318
+ <!-- A column's own `cell` renderer wins over every built-in display, the
1319
+ checkbox included: a boolean field drawn as a flag button or a status
1320
+ pill used to come out as a read-only checkbox, with `cell` never called. -->
1321
+ {#if (column.columnDef.editorType === "checkbox" || typeof cellValue === "boolean") && column.columnDef.cell == null}
1216
1322
  <div
1217
1323
  class="sv-grid-checkbox sv-grid-checkbox-readonly"
1218
1324
  role="checkbox"
@@ -1331,18 +1437,22 @@
1331
1437
  getValue: () => cellValue,
1332
1438
  })}
1333
1439
  {#if rendered instanceof RenderSnippetConfig}
1334
- {@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}
1335
1445
  {:else if rendered instanceof RenderComponentConfig}
1336
- <rendered.component {...rendered.props ?? {}} />
1446
+ {#key column.id}<rendered.component {...rendered.props ?? {}} />{/key}
1337
1447
  {:else if typeof rendered === "string" || typeof rendered === "number"}
1338
- {rendered}
1448
+ <span class="sv-grid-cell-text">{rendered}</span>
1339
1449
  {:else}
1340
- {formatCellValue(column, cellValue, row)}
1450
+ <span class="sv-grid-cell-text">{formatCellValue(column, cellValue, row)}</span>
1341
1451
  {/if}
1342
1452
  {:else if typeof cellTemplate === "string"}
1343
- {cellTemplate}
1453
+ <span class="sv-grid-cell-text">{cellTemplate}</span>
1344
1454
  {:else}
1345
- {formatCellValue(column, cellValue, row)}
1455
+ <span class="sv-grid-cell-text">{formatCellValue(column, cellValue, row)}</span>
1346
1456
  {/if}
1347
1457
  {/if}
1348
1458
  {/snippet}
@@ -1602,11 +1712,16 @@
1602
1712
  {#if showDetailToggleEffective}
1603
1713
  <td class="sv-grid-cell sv-grid-detail-toggle-cell"></td>
1604
1714
  {/if}
1605
- {#each allColumns as col (col.id)}
1606
- <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)}`}>
1607
1721
  <span class="sv-grid-placeholder-skeleton" aria-hidden="true"></span>
1608
1722
  </td>
1609
1723
  {/each}
1724
+ {#if showRightSpacer}{@render cellGap(columnWindowRightSpacer)}{/if}
1610
1725
  {/if}
1611
1726
  </tr>
1612
1727
  {/snippet}
@@ -1637,7 +1752,7 @@
1637
1752
  {@const stickyTop = stickyTopOfRow(rowIndex)}
1638
1753
  <tr
1639
1754
  class="sv-grid-row sv-grid-group-row"
1640
- class:sv-grid-row-selected={isRowSelected(row.id)}
1755
+ class:sv-grid-row-selected={isRowSelected(row.id, row.original)}
1641
1756
  class:sv-grid-row-frozen={rowIndex < frozenRowCount}
1642
1757
  class:sv-grid-row-frozen-last={frozenRowCount > 0 && rowIndex === frozenRowCount - 1}
1643
1758
  class:sv-grid-row-sticky-group={stickyTop !== undefined}
@@ -1666,7 +1781,7 @@
1666
1781
  {@const stickyTop = stickyTopOfRow(rowIndex)}
1667
1782
  <tr
1668
1783
  class={`sv-grid-row ${userRowClass} ${rowDropClass(rowIndex)}`}
1669
- class:sv-grid-row-selected={isRowSelected(row.id)}
1784
+ class:sv-grid-row-selected={isRowSelected(row.id, row.original)}
1670
1785
  class:sv-grid-row-alt={opt.zebraRows &&
1671
1786
  rowIndex % 2 === 1}
1672
1787
  class:sv-grid-row-draggable={rowDragManagedEffective}
@@ -1689,31 +1804,33 @@
1689
1804
  <td
1690
1805
  class="sv-grid-cell sv-grid-row-number-cell"
1691
1806
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; inset-inline-start: 0;`}
1692
- >{rowIndex + 1}</td
1807
+ ><span class="sv-grid-cell-line">{rowIndex + 1}</span></td
1693
1808
  >
1694
1809
  {/if}
1695
1810
  {#if showRowSelectionEffective}
1696
1811
  <td
1697
1812
  class="sv-grid-cell sv-grid-selection-cell"
1698
1813
  style={`width: ${selectionColumnWidth}px; min-width: ${selectionColumnWidth}px; max-width: ${selectionColumnWidth}px; inset-inline-start: ${showRowNumbersEffective ? rowNumberColumnWidth : 0}px;`}
1699
- onclick={() => toggleRowSelectionById(row.id)}
1814
+ onclick={() => toggleRowSelectionById(row.id, row.original)}
1700
1815
  >
1701
- <button
1702
- type="button"
1703
- class="sv-grid-checkbox"
1704
- role="checkbox"
1705
- aria-checked={isRowSelected(row.id)}
1706
- aria-label="Select row"
1707
- onclick={(event) => {
1708
- event.stopPropagation();
1709
- toggleRowSelectionById(row.id);
1710
- }}
1711
- onkeydown={(event) =>
1712
- 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) => {
1713
1824
  event.stopPropagation();
1714
- toggleRowSelectionById(row.id);
1715
- })}
1716
- ></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
+ >
1717
1834
  </td>
1718
1835
  {/if}
1719
1836
  {#if showDetailToggleEffective}
@@ -1723,15 +1840,17 @@
1723
1840
  style={`width: ${detailToggleColumnWidth}px; min-width: ${detailToggleColumnWidth}px; max-width: ${detailToggleColumnWidth}px; left: ${detailToggleColumnLeft}px;`}
1724
1841
  >
1725
1842
  {#if dt}
1726
- <button
1727
- type="button"
1728
- class="sv-grid-detail-toggle"
1729
- aria-expanded={dt.open ? "true" : "false"}
1730
- aria-label={dt.open ? messages.closeDetail : messages.openDetail}
1731
- onclick={(event) => {
1732
- event.stopPropagation();
1733
- dt.toggle();
1734
- }}>{@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
1735
1854
  >
1736
1855
  {/if}
1737
1856
  </td>
@@ -1743,151 +1862,121 @@
1743
1862
  style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
1744
1863
  ></td>
1745
1864
  {/if}
1746
- {#each renderedColumns as rendered (rendered.column.id)}
1747
- {@const colIndex = rendered.item.index}
1748
- {@const draw = mergeIndex
1749
- ? mergeDrawAt(mergeIndex, rowIndex, colIndex, rowIndex < frozenRowCount ? mergeFrozenWindow : mergeBodyWindow)
1750
- : null}
1751
- {#if draw?.kind !== "skip"}
1752
- {@const cellRow = draw?.kind === "continuation" ? (allRows[draw.merge.rowIndex] ?? row) : row}
1753
- {@const cellColumn = draw?.kind === "continuation" ? (allColumns[draw.merge.colIndex] ?? rendered.column) : rendered.column}
1754
- {@const baseValue = getColumnBaseValue(
1755
- cellRow,
1756
- cellColumn,
1757
- )}
1758
- {@const cellValue = getCellDisplayValue(
1759
- cellRow.id,
1760
- cellColumn.id,
1761
- baseValue,
1762
- )}
1763
- {@const isEditing =
1764
- ctrl.editingCell?.rowId === cellRow.id &&
1765
- ctrl.editingCell?.columnId === cellColumn.id}
1766
- {@const inRowEdit =
1767
- !!fullRowEdit &&
1768
- fullRowEdit.rowId === cellRow.id &&
1769
- cellColumn.id in fullRowEdit.draft}
1770
- {@const cellSel = cellSelectionState(rowIndex, colIndex)}
1771
- {@const fillEdges = fillMarqueeEdges(rowIndex, colIndex)}
1772
- {@const userCellClass = computeCellClass(
1773
- cellRow,
1774
- cellColumn,
1775
- )}
1776
- {@const cellTooltip = computeCellTooltip(
1777
- cellRow,
1778
- cellColumn,
1779
- )}
1780
- {@const cellValidity = computeCellValidity(
1781
- cellRow,
1782
- cellColumn,
1783
- )}
1784
- {@const cellNote = computeCellNote(
1785
- cellRow,
1786
- cellColumn,
1787
- )}
1788
- {@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)}
1789
1870
  <td
1790
- rowspan={draw && draw.rowSpan > 1 ? draw.rowSpan : undefined}
1791
- colspan={draw && draw.colSpan > 1 ? draw.colSpan : undefined}
1792
- data-merge-origin={draw ? `${draw.merge.rowIndex}:${draw.merge.colIndex}` : undefined}
1793
- class:sv-grid-cell-merged={!!draw}
1794
- class={`sv-grid-cell ${userCellClass}`}
1795
- 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)}
1796
1877
  class:sv-grid-cell-collapsed={!!collapsedColumns[rendered.column.id]}
1797
- class:sv-grid-cell-active={!!cellSel?.active}
1798
- 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}
1799
1880
  class:sv-grid-cell-cf={hasConditionalFormats}
1800
- class:sv-grid-cell-invalid={cellValidity.invalid}
1801
- class:sv-grid-cell-has-note={cellNote != null}
1802
- 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}
1803
1884
  data-svgrid-row={rowIndex}
1804
- data-svgrid-col={colIndex}
1885
+ data-svgrid-col={c.colIndex}
1805
1886
  data-col-id={rendered.column.id}
1806
1887
  data-align={getColumnAlign(rendered.column)}
1807
- data-pinned={isColumnPinned(rendered.column.id) ??
1808
- undefined}
1809
- data-selected-range={cellSel?.edges ? "true" : undefined}
1810
- data-range-top={cellSel?.edges?.top ? "true" : undefined}
1811
- 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
1812
1892
  ? "true"
1813
1893
  : undefined}
1814
- data-range-left={cellSel?.edges?.left
1894
+ data-range-left={ui?.sel?.edges?.left
1815
1895
  ? "true"
1816
1896
  : undefined}
1817
- data-range-right={cellSel?.edges?.right
1897
+ data-range-right={ui?.sel?.edges?.right
1818
1898
  ? "true"
1819
1899
  : undefined}
1820
- data-fill-preview={isInFillPreview(rowIndex, colIndex)
1900
+ data-fill-preview={isInFillPreview(rowIndex, c.colIndex)
1821
1901
  ? "true"
1822
1902
  : undefined}
1823
- data-fill-top={fillEdges?.top ? "true" : undefined}
1824
- data-fill-bottom={fillEdges?.bottom ? "true" : undefined}
1825
- data-fill-left={fillEdges?.left ? "true" : undefined}
1826
- data-fill-right={fillEdges?.right ? "true" : undefined}
1827
- 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) : ""}`}
1828
1908
  onpointerdown={(event) =>
1829
- onCellPointerDown(rowIndex, colIndex, event)}
1830
- onpointerenter={(event) => {
1831
- 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);
1832
1919
  // The column tooltip fires on whole-cell hover; a
1833
1920
  // per-cell note waits for its corner below. A
1834
1921
  // validation message wins over the column's tooltip.
1835
1922
  const tip =
1836
- cellValidity.invalid && cellValidity.message
1837
- ? cellValidity.message
1838
- : cellTooltip;
1923
+ c.cellValidity.invalid && c.cellValidity.message
1924
+ ? c.cellValidity.message
1925
+ : c.cellTooltip;
1839
1926
  if (tip)
1840
1927
  showTooltipFor(
1841
1928
  event.currentTarget as HTMLElement,
1842
1929
  tip,
1843
1930
  );
1844
1931
  }}
1845
- onpointerleave={hideTooltip}
1932
+ onpointerout={(event) => {
1933
+ if (crossedCell(event)) hideTooltip();
1934
+ }}
1846
1935
  ondblclick={() =>
1847
- emitCellDoubleClick(rowIndex, colIndex)}
1848
- onclick={() => onCellClick(rowIndex, colIndex)}
1936
+ emitCellDoubleClick(rowIndex, c.colIndex)}
1937
+ onclick={() => onCellClick(rowIndex, c.colIndex)}
1849
1938
  oncontextmenu={(event) =>
1850
1939
  openContextMenu(
1851
1940
  event,
1852
1941
  rowIndex,
1853
- colIndex,
1942
+ c.colIndex,
1854
1943
  rendered.column.id,
1855
1944
  )}
1856
1945
  use:cellFlashAction={{
1857
- rowId: cellRow.id,
1858
- value: cellValue,
1859
- active: !!cellColumn.columnDef.cellFlash,
1946
+ rowId: c.cellRow.id,
1947
+ columnId: c.cellColumn.id,
1948
+ value: c.cellValue,
1949
+ active: !!c.cellColumn.columnDef.cellFlash,
1860
1950
  className: flashClassFor(
1861
- cellColumn.columnDef.cellFlash,
1951
+ c.cellColumn.columnDef.cellFlash,
1862
1952
  ),
1863
1953
  }}
1864
1954
  role="gridcell"
1865
- id={getGridCellDomId(ctrl.gridDomId, rowIndex, colIndex)}
1866
- aria-colindex={colIndex + 1}
1955
+ id={getGridCellDomId(ctrl.gridDomId, rowIndex, c.colIndex)}
1956
+ aria-colindex={c.colIndex + 1}
1867
1957
  aria-rowindex={rowIndex + 1}
1868
- aria-selected={isRowSelected(row.id)}
1958
+ aria-selected={isRowSelected(row.id, row.original)}
1869
1959
  >
1870
- {#if inRowEdit || isEditing}
1960
+ {#if ui && (ui.editing || ui.rowEdit)}
1871
1961
  <!-- The editing cell stays empty until the lazy
1872
1962
  editor chunk lands, exactly as it already did
1873
1963
  for the dropdown / date editors. Falling back
1874
1964
  to the read-only value here would flash stale
1875
1965
  content into a cell the user is editing. -->
1876
1966
  {#if CellEditor}
1877
- <CellEditor {ctrl} column={cellColumn} row={cellRow} fullRow={inRowEdit} />
1967
+ <CellEditor {ctrl} column={c.cellColumn} row={c.cellRow} fullRow={!!ui?.rowEdit} />
1878
1968
  {/if}
1879
1969
  {:else}
1880
- {#if plainCellBody}
1881
- {@render cellBody(cellRow, cellColumn, cellValue)}
1882
- {:else}
1883
- {@render cellBodyWithFormat(
1884
- cellRow,
1885
- cellColumn,
1886
- cellValue,
1887
- )}
1888
- {/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
+ >
1889
1978
  {/if}
1890
- {#if !isEditing && cellSel?.fillHandle}
1979
+ {#if !ui?.editing && ui?.sel?.fillHandle}
1891
1980
  <!-- Excel-style fill handle: drag down/right to
1892
1981
  extend the selection and pattern-fill the new
1893
1982
  cells on release. Rendered inside the bottom-
@@ -1900,7 +1989,7 @@
1900
1989
  aria-label="Fill handle"
1901
1990
  tabindex={-1}
1902
1991
  onpointerdown={(event) =>
1903
- startFillDrag(event, rowIndex, colIndex)}
1992
+ startFillDrag(event, rowIndex, c.colIndex)}
1904
1993
  ondblclick={(event) => {
1905
1994
  // Not the cell's double-click: this fills
1906
1995
  // down as far as the neighbour column goes.
@@ -1910,16 +1999,16 @@
1910
1999
  }}
1911
2000
  ></div>
1912
2001
  {/if}
1913
- {#if cellValidity.invalid && cellValidity.message}
2002
+ {#if c.cellValidity.invalid && c.cellValidity.message}
1914
2003
  <!-- The validation message, for assistive tech: the
1915
2004
  tooltip above is mouse-only, and unlike a title
1916
2005
  a visually-hidden span adds no second native
1917
2006
  tooltip competing with the custom one. -->
1918
2007
  <span class="sv-grid-sr-only"
1919
- >{cellValidity.message}</span
2008
+ >{c.cellValidity.message}</span
1920
2009
  >
1921
2010
  {/if}
1922
- {#if cellNote != null && !isEditing}
2011
+ {#if c.cellNote != null && !ui?.editing}
1923
2012
  <span
1924
2013
  class="sv-grid-cell-note-corner"
1925
2014
  aria-label="Note"
@@ -1927,7 +2016,7 @@
1927
2016
  event.stopPropagation();
1928
2017
  showTooltipFor(
1929
2018
  event.currentTarget as HTMLElement,
1930
- cellNote,
2019
+ c.cellNote ?? "",
1931
2020
  );
1932
2021
  }}
1933
2022
  onpointerleave={(event) => {
@@ -1939,7 +2028,7 @@
1939
2028
  </td>
1940
2029
  {/if}
1941
2030
  {/each}
1942
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2031
+ {#if showRightSpacer}
1943
2032
  <td
1944
2033
  class="sv-grid-cell sv-grid-cell-spacer"
1945
2034
  aria-hidden="true"
@@ -1954,6 +2043,23 @@
1954
2043
  outside the rendered window), so this one carries no cell ids, no
1955
2044
  row index attributes and is hidden from assistive tech; the group
1956
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}
1957
2063
  {#snippet stickyGroupRow(row: Row<TData>, rowIndex: number, top: number, size: number, last: boolean)}
1958
2064
  {#if isGroupRow(row) && !groupColumnMode}
1959
2065
  <tr
@@ -1986,7 +2092,7 @@
1986
2092
  <td
1987
2093
  class="sv-grid-cell sv-grid-row-number-cell"
1988
2094
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; left: 0;`}
1989
- >{rowIndex + 1}</td
2095
+ ><span class="sv-grid-cell-line">{rowIndex + 1}</span></td
1990
2096
  >
1991
2097
  {/if}
1992
2098
  {#if showRowSelectionEffective}
@@ -2009,6 +2115,7 @@
2009
2115
  {/if}
2010
2116
  {#each renderedColumns as rendered (rendered.column.id)}
2011
2117
  {@const cellValue = getCellDisplayValue(row.id, rendered.column.id, getColumnBaseValue(row, rendered.column))}
2118
+ {#if rendered.gap > 0}{@render cellGap(rendered.gap)}{/if}
2012
2119
  <td
2013
2120
  class={`sv-grid-cell ${computeCellClass(row, rendered.column)}`}
2014
2121
  class:sv-grid-cell-cf={hasConditionalFormats}
@@ -2024,7 +2131,7 @@
2024
2131
  {/if}
2025
2132
  </td>
2026
2133
  {/each}
2027
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2134
+ {#if showRightSpacer}
2028
2135
  <td
2029
2136
  class="sv-grid-cell sv-grid-cell-spacer"
2030
2137
  style={`width: ${columnWindowRightSpacer}px; min-width: ${columnWindowRightSpacer}px; max-width: ${columnWindowRightSpacer}px;`}
@@ -2047,7 +2154,9 @@
2047
2154
  <td
2048
2155
  class="sv-grid-cell sv-grid-row-number-cell"
2049
2156
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; inset-inline-start: 0;`}
2050
- >{#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
2051
2160
  >
2052
2161
  {/if}
2053
2162
  {#if showRowSelectionEffective}
@@ -2075,16 +2184,17 @@
2075
2184
  rowData,
2076
2185
  rendered.column,
2077
2186
  )}
2187
+ {#if rendered.gap > 0}{@render cellGap(rendered.gap)}{/if}
2078
2188
  <td
2079
2189
  class={`sv-grid-cell ${userCellClass}`}
2080
2190
  data-col-id={rendered.column.id}
2081
2191
  data-align={getColumnAlign(rendered.column)}
2082
2192
  data-pinned={isColumnPinned(rendered.column.id) ?? undefined}
2083
2193
  style={`width: ${rendered.item.size}px; min-width: ${rendered.item.size}px; max-width: ${rendered.item.size}px; ${cellPinStyle(rendered.column.id)}`}
2084
- >{formatPinnedValue(rendered.column, value)}</td
2194
+ ><span class="sv-grid-cell-text">{formatPinnedValue(rendered.column, value)}</span></td
2085
2195
  >
2086
2196
  {/each}
2087
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2197
+ {#if showRightSpacer}
2088
2198
  <td
2089
2199
  class="sv-grid-cell sv-grid-cell-spacer"
2090
2200
  aria-hidden="true"
@@ -2201,7 +2311,9 @@
2201
2311
  <table
2202
2312
  bind:this={ctrl.gridRootEl}
2203
2313
  class="sv-grid-table"
2314
+ class:sv-grid-table-flex={ctrl.flexRows}
2204
2315
  class:sv-grid-no-row-hover={opt.enableRowHover !== true}
2316
+ class:sv-grid-numbers-beside-toggle={showDetailToggleEffective && !showRowSelectionEffective}
2205
2317
  {...getGridRootA11yProps({
2206
2318
  activeDescendantId,
2207
2319
  rowCount: allRows.length,
@@ -2210,7 +2322,7 @@
2210
2322
  })}
2211
2323
  onkeydown={onGridKeyDown}
2212
2324
  onpaste={onGridPaste}
2213
- style={`min-width: ${totalColumnWidth}px;`}
2325
+ style={`min-width: ${totalColumnWidth + ctrl.systemColumnsWidth}px;`}
2214
2326
  >
2215
2327
  <!-- svelte-ignore a11y_no_redundant_roles -->
2216
2328
  <thead class="sv-grid-head" bind:this={ctrl.theadEl} role="rowgroup">
@@ -2255,6 +2367,7 @@
2255
2367
  ></th>
2256
2368
  {/if}
2257
2369
  {#each row.cells as cell (cell.key)}
2370
+ {#if cell.gapBefore}{@render columnGap(cell.gapBefore)}{/if}
2258
2371
  <th
2259
2372
  class="sv-grid-column sv-grid-group-header-cell"
2260
2373
  class:sv-grid-group-header-placeholder={cell.isPlaceholder}
@@ -2291,7 +2404,7 @@
2291
2404
  {/if}
2292
2405
  </th>
2293
2406
  {/each}
2294
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2407
+ {#if showRightSpacer}
2295
2408
  <th
2296
2409
  class="sv-grid-column sv-grid-column-spacer"
2297
2410
  aria-hidden="true"
@@ -2359,8 +2472,9 @@
2359
2472
  style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
2360
2473
  ></th>
2361
2474
  {/if}
2362
- {#each renderedColumns as rendered (rendered.column.id)}
2475
+ {#each renderedColumns as rendered (rendered.key)}
2363
2476
  {@const header = headerGroup.headers[rendered.item.index]}
2477
+ {#if rendered.gap > 0}{@render columnGap(rendered.gap)}{/if}
2364
2478
  {#if header}
2365
2479
  {@const sortDirection =
2366
2480
  sortDirectionByColumn[header.column.id]}
@@ -2453,9 +2567,12 @@
2453
2567
  }}
2454
2568
  >
2455
2569
  {#if rendered instanceof RenderSnippetConfig}
2456
- {@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}
2457
2574
  {:else if rendered instanceof RenderComponentConfig}
2458
- <rendered.component {...rendered.props ?? {}} />
2575
+ {#key header.column.id}<rendered.component {...rendered.props ?? {}} />{/key}
2459
2576
  {:else if typeof rendered === "string" || typeof rendered === "number"}
2460
2577
  {rendered}
2461
2578
  {:else}
@@ -2567,7 +2684,7 @@
2567
2684
  </th>
2568
2685
  {/if}
2569
2686
  {/each}
2570
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2687
+ {#if showRightSpacer}
2571
2688
  <th
2572
2689
  class="sv-grid-column sv-grid-column-spacer"
2573
2690
  aria-hidden="true"
@@ -2608,10 +2725,11 @@
2608
2725
  style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
2609
2726
  ></th>
2610
2727
  {/if}
2611
- {#each renderedColumns as rendered (rendered.column.id)}
2728
+ {#each renderedColumns as rendered (rendered.key)}
2612
2729
  {@const activeOperator =
2613
2730
  filterMenuValues[rendered.column.id]?.operator ??
2614
2731
  defaultOperatorFor(rendered.column)}
2732
+ {#if rendered.gap > 0}{@render columnGap(rendered.gap)}{/if}
2615
2733
  <th
2616
2734
  class="sv-grid-column"
2617
2735
  data-pinned={isColumnPinned(rendered.column.id) ??
@@ -2791,7 +2909,7 @@
2791
2909
  </div>
2792
2910
  </th>
2793
2911
  {/each}
2794
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
2912
+ {#if showRightSpacer}
2795
2913
  <th
2796
2914
  class="sv-grid-column sv-grid-column-spacer"
2797
2915
  aria-hidden="true"
@@ -2934,17 +3052,18 @@
2934
3052
  style={`width: ${columnWindowStart}px; min-width: ${columnWindowStart}px; max-width: ${columnWindowStart}px;`}
2935
3053
  ></th>
2936
3054
  {/if}
2937
- {#each renderedColumns as rendered (rendered.column.id)}
3055
+ {#each renderedColumns as rendered (rendered.key)}
3056
+ {#if rendered.gap > 0}{@render columnGap(rendered.gap)}{/if}
2938
3057
  <th
2939
3058
  class="sv-grid-column sv-grid-summary-column"
2940
3059
  data-pinned={isColumnPinned(rendered.column.id) ??
2941
3060
  undefined}
2942
3061
  style={`width: ${rendered.item.size}px; min-width: ${rendered.item.size}px; max-width: ${rendered.item.size}px; ${cellPinStyle(rendered.column.id)}`}
2943
3062
  >
2944
- {summaryByColumn[rendered.column.id] ?? ""}
3063
+ <span class="sv-grid-cell-text">{summaryByColumn[rendered.column.id] ?? ""}</span>
2945
3064
  </th>
2946
3065
  {/each}
2947
- {#if columnVirtualizationEnabled && columnWindowRightSpacer > 0}
3066
+ {#if showRightSpacer}
2948
3067
  <th
2949
3068
  class="sv-grid-column sv-grid-column-spacer"
2950
3069
  aria-hidden="true"
@@ -3121,10 +3240,12 @@
3121
3240
  <div class="sv-grid-skeleton" aria-hidden="true">
3122
3241
  {#each Array(opt.loadingSkeletonRows ?? 8) as _, r (r)}
3123
3242
  <div class="sv-grid-skeleton-row">
3124
- {#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)}
3125
3246
  <div
3126
3247
  class="sv-grid-skeleton-cell"
3127
- style={`width:${getColumnWidth(col.id)}px`}
3248
+ style={`width:${rendered.item.size}px`}
3128
3249
  >
3129
3250
  <span class="sv-grid-skeleton-bar"></span>
3130
3251
  </div>