@@ -3,6 +3,7 @@ import type {
33 TableMeasure ,
44 TableFragment ,
55 TableColumnBoundary ,
6+ TableRowBoundary ,
67 TableFragmentMetadata ,
78 TableRowMeasure ,
89 TableRow ,
@@ -195,6 +196,7 @@ export function rescaleColumnWidths(
195196
196197const COLUMN_MIN_WIDTH_PX = 25 ;
197198const COLUMN_MAX_WIDTH_PX = 200 ;
199+ const ROW_MIN_HEIGHT_PX = 10 ;
198200
199201/**
200202 * Calculate minimum width for a table column from its measured width.
@@ -261,6 +263,98 @@ function generateColumnBoundaries(measure: TableMeasure, effectiveWidths?: numbe
261263 return boundaries ;
262264}
263265
266+ /**
267+ * Generate row boundary metadata for interactive table row resizing.
268+ *
269+ * Creates metadata that enables the overlay component to position horizontal
270+ * resize handles and enforce minimum height constraints during drag operations.
271+ *
272+ * Boundaries are marked non-resizable when:
273+ * - A cell in the row above has a rowSpan that crosses the boundary
274+ * - The row is a repeated header on a continuation fragment (resize originals only)
275+ *
276+ * @param measure - Table measurement containing row heights
277+ * @param block - Table block (used for rowSpan inspection)
278+ * @param fromRow - Starting body row index (inclusive)
279+ * @param toRow - Ending body row index (exclusive)
280+ * @param repeatHeaderCount - Number of repeated header rows on this fragment
281+ * @param cellSpacingPx - Cell spacing in pixels (border-spacing)
282+ * @returns Array of row boundary metadata
283+ */
284+ function generateRowBoundaries (
285+ measure : TableMeasure ,
286+ block : TableBlock ,
287+ fromRow : number ,
288+ toRow : number ,
289+ repeatHeaderCount : number ,
290+ cellSpacingPx : number ,
291+ partialRow ?: PartialRowInfo | null ,
292+ ) : TableRowBoundary [ ] {
293+ const boundaries : TableRowBoundary [ ] = [ ] ;
294+
295+ // Build ordered list of rendered rows: headers first, then body rows
296+ const renderedRows : Array < { rowIndex : number ; isRepeatedHeader : boolean } > = [ ] ;
297+ if ( repeatHeaderCount > 0 ) {
298+ for ( let r = 0 ; r < repeatHeaderCount && r < measure . rows . length ; r ++ ) {
299+ renderedRows . push ( { rowIndex : r , isRepeatedHeader : fromRow > 0 } ) ;
300+ }
301+ }
302+ for ( let r = fromRow ; r < toRow && r < measure . rows . length ; r ++ ) {
303+ renderedRows . push ( { rowIndex : r , isRepeatedHeader : false } ) ;
304+ }
305+
306+ // Build a set of ABSOLUTE row boundaries blocked by rowspan cells.
307+ // A boundary after absolute row N is blocked if any cell starting at row N
308+ // has a rowSpan that extends beyond row N.
309+ const blockedBoundaries = new Set < number > ( ) ;
310+ for ( let ri = 0 ; ri < renderedRows . length ; ri ++ ) {
311+ const { rowIndex } = renderedRows [ ri ] ;
312+ const rowMeasure = measure . rows [ rowIndex ] ;
313+ if ( ! rowMeasure ) continue ;
314+
315+ for ( const cellMeasure of rowMeasure . cells ) {
316+ const rowSpan = cellMeasure . rowSpan ?? 1 ;
317+ if ( rowSpan <= 1 ) continue ;
318+
319+ // This cell spans from rowIndex to rowIndex + rowSpan - 1.
320+ // Block absolute boundaries between the start row and end row.
321+ // Example: rowIndex=2, rowSpan=3 blocks boundaries after rows 2 and 3.
322+ for ( let boundaryRow = rowIndex ; boundaryRow < rowIndex + rowSpan - 1 ; boundaryRow ++ ) {
323+ blockedBoundaries . add ( boundaryRow ) ;
324+ }
325+ }
326+ }
327+
328+ let yPosition = cellSpacingPx ;
329+ for ( let ri = 0 ; ri < renderedRows . length ; ri ++ ) {
330+ const { rowIndex, isRepeatedHeader } = renderedRows [ ri ] ;
331+ const rowMeasure = measure . rows [ rowIndex ] ;
332+ if ( ! rowMeasure ) continue ;
333+
334+ const isPartial = partialRow ?. rowIndex === rowIndex ;
335+ const height = isPartial ? partialRow . partialHeight : rowMeasure . height ;
336+ const contentHeight = getRowContentHeight ( block . rows [ rowIndex ] , rowMeasure ) ;
337+ const minHeight = isPartial ? Math . max ( 1 , height ) : Math . max ( ROW_MIN_HEIGHT_PX , contentHeight ) ;
338+
339+ // A boundary is resizable unless:
340+ // 1. It's a repeated header on a continuation fragment
341+ // 2. A rowspan crosses this boundary (blockedBoundaries)
342+ const resizable = ! isRepeatedHeader && ! isPartial && ! blockedBoundaries . has ( rowIndex ) ;
343+
344+ boundaries . push ( {
345+ index : rowIndex ,
346+ y : yPosition ,
347+ height,
348+ minHeight,
349+ resizable,
350+ } ) ;
351+
352+ yPosition += height + cellSpacingPx ;
353+ }
354+
355+ return boundaries ;
356+ }
357+
264358/**
265359 * Count contiguous header rows from the beginning of the table.
266360 *
@@ -1097,23 +1191,29 @@ function findSplitPoint(
10971191/**
10981192 * Generate fragment metadata for a table fragment.
10991193 *
1100- * Currently only includes column boundaries; row boundaries omitted to reduce DOM overhead .
1194+ * Includes column boundaries and row boundaries for interactive resizing .
11011195 *
11021196 * @param measure - Table measurements
1103- * @param fromRow - Starting row (unused but kept for future row boundaries)
1104- * @param toRow - Ending row (unused but kept for future row boundaries)
1105- * @param repeatHeaderCount - Header count (unused but kept for future metadata)
1197+ * @param block - Table block (used for rowSpan and content height inspection)
1198+ * @param fromRow - Starting body row index (inclusive)
1199+ * @param toRow - Ending body row index (exclusive)
1200+ * @param repeatHeaderCount - Number of repeated header rows on this fragment
1201+ * @param effectiveWidths - Optional rescaled column widths
11061202 * @returns Table fragment metadata
11071203 */
11081204function generateFragmentMetadata (
11091205 measure : TableMeasure ,
1110- _fromRow : number ,
1111- _toRow : number ,
1112- _repeatHeaderCount : number ,
1206+ block : TableBlock ,
1207+ fromRow : number ,
1208+ toRow : number ,
1209+ repeatHeaderCount : number ,
11131210 effectiveWidths ?: number [ ] ,
1211+ partialRow ?: PartialRowInfo | null ,
11141212) : TableFragmentMetadata {
1213+ const cellSpacingPx = measure . cellSpacingPx ?? 0 ;
11151214 return {
11161215 columnBoundaries : generateColumnBoundaries ( measure , effectiveWidths ) ,
1216+ rowBoundaries : generateRowBoundaries ( measure , block , fromRow , toRow , repeatHeaderCount , cellSpacingPx , partialRow ) ,
11171217 coordinateSystem : 'fragment' ,
11181218 } ;
11191219}
@@ -1138,10 +1238,14 @@ function layoutMonolithicTable(context: TableLayoutContext): void {
11381238 const { x, width } = resolveTableFrame ( baseX , context . columnWidth , baseWidth , context . block . attrs ) ;
11391239 const columnWidths = rescaleColumnWidths ( context . measure . columnWidths , context . measure . totalWidth , width ) ;
11401240
1141- const metadata : TableFragmentMetadata = {
1142- columnBoundaries : generateColumnBoundaries ( context . measure , columnWidths ) ,
1143- coordinateSystem : 'fragment' ,
1144- } ;
1241+ const metadata = generateFragmentMetadata (
1242+ context . measure ,
1243+ context . block ,
1244+ 0 ,
1245+ context . block . rows . length ,
1246+ 0 ,
1247+ columnWidths ,
1248+ ) ;
11451249
11461250 const fragment : TableFragment = {
11471251 kind : 'table' ,
@@ -1288,10 +1392,7 @@ export function layoutTableBlock({
12881392 const { x, width } = resolveTableFrame ( baseX , columnWidth , baseWidth , block . attrs ) ;
12891393 const columnWidths = rescaleColumnWidths ( measure . columnWidths , measure . totalWidth , width ) ;
12901394
1291- const metadata : TableFragmentMetadata = {
1292- columnBoundaries : generateColumnBoundaries ( measure , columnWidths ) ,
1293- coordinateSystem : 'fragment' ,
1294- } ;
1395+ const metadata = generateFragmentMetadata ( measure , block , 0 , 0 , 0 , columnWidths ) ;
12951396
12961397 const fragment : TableFragment = {
12971398 kind : 'table' ,
@@ -1408,7 +1509,15 @@ export function layoutTableBlock({
14081509 continuesOnNext : hasRemainingLinesAfterContinuation || rowIndex + 1 < block . rows . length ,
14091510 repeatHeaderCount,
14101511 partialRow : continuationPartialRow ,
1411- metadata : generateFragmentMetadata ( measure , rowIndex , rowIndex + 1 , repeatHeaderCount , scaledWidths ) ,
1512+ metadata : generateFragmentMetadata (
1513+ measure ,
1514+ block ,
1515+ rowIndex ,
1516+ rowIndex + 1 ,
1517+ repeatHeaderCount ,
1518+ scaledWidths ,
1519+ continuationPartialRow ,
1520+ ) ,
14121521 columnWidths : scaledWidths ,
14131522 } ;
14141523
@@ -1486,7 +1595,15 @@ export function layoutTableBlock({
14861595 continuesOnNext : ! forcedPartialRow . isLastPart || forcedEndRow < block . rows . length ,
14871596 repeatHeaderCount,
14881597 partialRow : forcedPartialRow ,
1489- metadata : generateFragmentMetadata ( measure , bodyStartRow , forcedEndRow , repeatHeaderCount , scaledWidths ) ,
1598+ metadata : generateFragmentMetadata (
1599+ measure ,
1600+ block ,
1601+ bodyStartRow ,
1602+ forcedEndRow ,
1603+ repeatHeaderCount ,
1604+ scaledWidths ,
1605+ forcedPartialRow ,
1606+ ) ,
14901607 columnWidths : scaledWidths ,
14911608 } ;
14921609
@@ -1530,7 +1647,15 @@ export function layoutTableBlock({
15301647 continuesOnNext : endRow < block . rows . length || ( partialRow ? ! partialRow . isLastPart : false ) ,
15311648 repeatHeaderCount,
15321649 partialRow : partialRow || undefined ,
1533- metadata : generateFragmentMetadata ( measure , bodyStartRow , endRow , repeatHeaderCount , scaledWidths ) ,
1650+ metadata : generateFragmentMetadata (
1651+ measure ,
1652+ block ,
1653+ bodyStartRow ,
1654+ endRow ,
1655+ repeatHeaderCount ,
1656+ scaledWidths ,
1657+ partialRow ,
1658+ ) ,
15341659 columnWidths : scaledWidths ,
15351660 } ;
15361661
@@ -1568,10 +1693,7 @@ export function createAnchoredTableFragment(
15681693 x : number ,
15691694 y : number ,
15701695) : TableFragment {
1571- const metadata : TableFragmentMetadata = {
1572- columnBoundaries : generateColumnBoundaries ( measure ) ,
1573- coordinateSystem : 'fragment' ,
1574- } ;
1696+ const metadata = generateFragmentMetadata ( measure , block , 0 , block . rows . length , 0 ) ;
15751697
15761698 const fragment : TableFragment = {
15771699 kind : 'table' ,
0 commit comments