diff --git a/packages/base/data/list-table-v2/index.ts b/packages/base/data/list-table-v2/index.ts index 14f3f8e..f87e5d7 100644 --- a/packages/base/data/list-table-v2/index.ts +++ b/packages/base/data/list-table-v2/index.ts @@ -28,15 +28,6 @@ export { // Hooks export { usePretextColumnWidths, computeFlexWidths, type ColumnFlexConfig } from "./usePretextColumnWidths"; -export { usePretextRowHeights, type RowHeightEntry } from "./usePretextRowHeights"; -export { - useVirtualRows, - buildOffsets, - type VirtualRow, - type VirtualRange, -} from "./useVirtualRows"; -export { - useRuntimeHeightAugment, - type HeightSample, - type ColumnHeightStats, -} from "./useRuntimeHeightAugment"; +export { resolveRowHeights, type RowHeightEntry } from "./usePretextRowHeights"; +export { useVirtualRows, buildOffsets, type VirtualRow, type VirtualRange } from "./useVirtualRows"; +export { useRuntimeHeightAugment, type HeightSample, type ColumnHeightStats } from "./useRuntimeHeightAugment"; diff --git a/packages/base/data/list-table-v2/list-table-v2.vue b/packages/base/data/list-table-v2/list-table-v2.vue index 914292f..b8148e8 100644 --- a/packages/base/data/list-table-v2/list-table-v2.vue +++ b/packages/base/data/list-table-v2/list-table-v2.vue @@ -8,8 +8,10 @@
Cell Heights: {{ - rowHeights.map((entry) => - entry.cellHeights?.map((entry) => `${entry.isCustomRenderer ? "*" : ""}${entry.height}`).join(",") + cellHeights?.map((rowCellHeights) => + rowCellHeights + .map((entry) => (entry ? `${entry.isCustomRenderer ? "*" : ""}${entry.height}` : "null")) + .join(",") ) }}
@@ -92,7 +94,7 @@ import { useI18n } from "vue3-i18n"; import * as lodash from "lodash-es"; import type { ListTableColumn, ListTableProps, PageResponse } from "./types"; import { usePretextColumnWidths, type ColumnFlexConfig } from "./usePretextColumnWidths"; -import { usePretextRowHeights, type RowHeightEntry } from "./usePretextRowHeights"; +import { resolveRowHeights, type RowHeightEntry } from "./usePretextRowHeights"; import { useVirtualRows } from "./useVirtualRows"; const DEFAULT_FONT = "14px sans-serif"; @@ -260,22 +262,20 @@ const { computedConfigs, totalFlexBasis, columnWidths } = usePretextColumnWidths { font: "14px Inter, sans-serif", headerFont: "bold 14px Inter, sans-serif", + formatCellValue, } ); // Row heights via pretext - use actual column widths from flexbox algorithm -const { rowHeights, totalHeight } = usePretextRowHeights(pageDataRef, columnsRef, columnWidths, formatCellValue, { +const { rowHeights, cellHeights } = resolveRowHeights(pageDataRef, columnsRef, columnWidths, formatCellValue, { font: props.font, lineHeight: 20, debug: props.debug, + fixedRowHeight: props.rowHeight, }); // Virtualizer (needs rowHeights as number[] and viewportHeight) -const virtualizer = useVirtualRows( - computed(() => rowHeights.value.map((r) => r.height)), - viewportHeight, - { overscan: 5 } -); +const virtualizer = useVirtualRows(rowHeights, viewportHeight, { overscan: 5 }); // Destructure for template auto-unwrap (Vue 3 auto-unwraps top-level refs) const { visibleRows, totalHeight: virtualTotalHeight, onScroll } = virtualizer; @@ -313,7 +313,6 @@ const tableContainerStyle = computed(() => { // ============================================================================= function getColumnStyle(col: ListTableColumn): Record { const config = computedConfigs.value.find((c) => c.key === col.key); - const explicitWidth = col.width !== undefined && col.width !== "auto" ? Number(col.width) : undefined; if (explicitWidth !== undefined && Number.isFinite(explicitWidth) && explicitWidth > 0) { return { @@ -327,16 +326,25 @@ function getColumnStyle(col: ListTableColumn): Record { // Find the column index to get the computed width const colIndex = columnsRef.value.findIndex((c) => c.key === col.key); const computedWidth = colIndex >= 0 ? columnWidths.value[colIndex] : 0; + const fallbackBasis = Number(col.minWidth ?? 120); if (!config) { - return { flex: "1 1 100px", minWidth: "50px", maxWidth: "300px" }; + return { + flexGrow: "1.1", + flexShrink: "1.1", + flexBasis: `${fallbackBasis}px`, + minWidth: `${Math.max(fallbackBasis, 50)}px`, + maxWidth: "300px", + }; } // Apply the computed flex factors to the real cells so the row can consume // any remaining space after fixed-width columns and scrollbar width. const flexBasis = computedWidth > 0 ? computedWidth : config.flexBasis; return { - flex: `${config.flexGrow} ${config.flexShrink} ${flexBasis}px`, + flexGrow: `${config.flexGrow}`, + flexShrink: `${config.flexShrink}`, + flexBasis: `${flexBasis}px`, minWidth: `${config.minWidth}px`, maxWidth: `${config.maxWidth}px`, }; @@ -455,6 +463,7 @@ onUnmounted(() => { .header-cell { display: flex; + flex: 1 1 120px; align-items: center; justify-content: center; padding: 8px 4px; @@ -462,6 +471,7 @@ onUnmounted(() => { border-right: 1px solid v-bind("state.style.tableBorderColor"); overflow: hidden; word-break: break-word; + min-width: 0; &:last-child { border-right: none; @@ -510,6 +520,7 @@ onUnmounted(() => { .table-cell { display: flex; + flex: 1 1 120px; align-items: center; padding: 8px 4px; box-sizing: border-box; @@ -517,6 +528,7 @@ onUnmounted(() => { overflow: hidden; word-break: break-word; min-height: 100%; + min-width: 0; &:last-child { border-right: none; @@ -530,6 +542,7 @@ onUnmounted(() => { text-overflow: clip; white-space: normal; word-break: break-word; + text-align: center; color: v-bind("state.style.tableColor"); font: v-bind("props.font"); } diff --git a/packages/base/data/list-table-v2/usePretextColumnWidths.ts b/packages/base/data/list-table-v2/usePretextColumnWidths.ts index 2cac166..7fffe00 100644 --- a/packages/base/data/list-table-v2/usePretextColumnWidths.ts +++ b/packages/base/data/list-table-v2/usePretextColumnWidths.ts @@ -192,11 +192,13 @@ export function usePretextColumnWidths( font?: string; headerFont?: string; sampleSize?: number; + formatCellValue?: (row: T, col: ListTableColumn) => string; } ) { const font = options?.font ?? DEFAULT_FONT; const headerFont = options?.headerFont ?? DEFAULT_HEADER_FONT; const sampleSize = options?.sampleSize ?? 100; + const formatCellValue = options?.formatCellValue; const computedConfigs = computed(() => { if (!columns.value.length) return []; @@ -222,7 +224,7 @@ export function usePretextColumnWidths( const cellWidths: number[] = []; for (const row of sampled) { const rawValue = (row as any)[col.dataKey || colKey]; - const cellText = rawValue == null ? "" : String(rawValue); + const cellText = formatCellValue?.(row, col) ?? (rawValue == null ? "" : String(rawValue)); if (cellText) { const w = measureShrinkWrapWidth(cellText, font) + CELL_PADDING; cellWidths.push(w); diff --git a/packages/base/data/list-table-v2/usePretextRowHeights.ts b/packages/base/data/list-table-v2/usePretextRowHeights.ts index 442b5cc..7ab6cf5 100644 --- a/packages/base/data/list-table-v2/usePretextRowHeights.ts +++ b/packages/base/data/list-table-v2/usePretextRowHeights.ts @@ -8,6 +8,7 @@ import { computed, type Ref } from "vue"; import { measureTextHeight } from "./measureText"; import type { ListTableColumn } from "./types"; +import { toRefs } from "@vueuse/core"; const DEFAULT_FONT = "14px Inter, sans-serif"; const DEFAULT_LINE_HEIGHT = 20; @@ -17,10 +18,9 @@ const CELL_VERTICAL_PADDING = 12; // top + bottom per cell export interface RowHeightEntry { height: number; isCustomRenderer: boolean; - cellHeights?: RowHeightEntry[]; } -export function usePretextRowHeights( +export function resolveRowHeights( data: Ref, columns: Ref[]>, columnWidths: Ref, @@ -29,6 +29,7 @@ export function usePretextRowHeights( font?: string; lineHeight?: number; rowPadding?: number; + fixedRowHeight?: number; debug?: boolean; } ) { @@ -36,74 +37,85 @@ export function usePretextRowHeights( const lineHeight = options?.lineHeight ?? DEFAULT_LINE_HEIGHT; const rowPadding = options?.rowPadding ?? DEFAULT_ROW_PADDING; - const rowHeights = computed(() => { - if (!data.value.length || !columns.value.length || !columnWidths.value.length) { - return []; - } - - return data.value.map((row) => { - let maxCellHeight = lineHeight; // minimum 1 line - let cellHeights: RowHeightEntry[] | undefined = options?.debug ? [] : undefined; - - for (let i = 0; i < columns.value.length; i++) { - const col = columns.value[i]; - // Use flex basis width for height calculation as an approximation - // of actual column width (CSS flex layout determines actual width) - const colWidth = columnWidths.value[i] ?? 100; - - // Check if custom renderer exists (we can't measure these with pretext) - const hasCustomRenderer = !!(col.cellRenderer || col.slot); - - if (hasCustomRenderer) { - // For custom renderers, we use a placeholder height - // Actual height will be measured at runtime via useRuntimeHeightAugment - // For now, use a reasonable minimum - const placeholderHeight = 44; // default row height - maxCellHeight = Math.max(maxCellHeight, placeholderHeight); - - cellHeights?.push({ - height: placeholderHeight, - isCustomRenderer: true, - }); - continue; - } - - // Get raw cell value - const cellText = formatCellValue(row, col); - if (!cellText) continue; - - // Calculate available width for text (excluding cell padding) - const availableWidth = colWidth - CELL_VERTICAL_PADDING * 2; - if (availableWidth <= 0) continue; - - try { - const cellHeight = measureTextHeight(cellText, font, availableWidth, lineHeight); - maxCellHeight = Math.max(maxCellHeight, cellHeight); + const { rowHeights, cellHeights } = toRefs( + computed(() => { + if (!data.value.length || !columns.value.length || !columnWidths.value.length) { + return { rowHeights: [], cellHeights: [] }; + } - cellHeights?.push({ - height: cellHeight, - isCustomRenderer: false, - }); - } catch { - // Fallback: assume single line + const rowHeights: number[] = []; + const cellHeights: Array[] | undefined = options?.debug ? [] : undefined; + + for (const row of data.value) { + let maxCellHeight = lineHeight; // minimum 1 line + let rowCellHeights: Array = []; + + for (let i = 0; i < columns.value.length; i++) { + const col = columns.value[i]; + // Use flex basis width for height calculation as an approximation + // of actual column width (CSS flex layout determines actual width) + const colWidth = columnWidths.value[i] ?? 100; + + // Check if custom renderer exists (we can't measure these with pretext) + const isCustomRenderer = !!(col.cellRenderer || col.slot); + let rowHeightEntry: RowHeightEntry; + + if (options?.fixedRowHeight) { + rowHeightEntry = { + height: options.fixedRowHeight, + isCustomRenderer, + }; + } else if (isCustomRenderer) { + rowHeightEntry = { + height: 42, + isCustomRenderer, + }; + } else { + try { + const cellText = formatCellValue(row, col); + if (!cellText) throw "Invalid Cell Text"; + + // Calculate available width for text (excluding cell padding) + const availableWidth = colWidth - CELL_VERTICAL_PADDING * 2; + if (availableWidth <= 0) throw "Invalid Column Width"; + + rowHeightEntry = { + height: measureTextHeight(cellText, font, availableWidth, lineHeight), + isCustomRenderer, + }; + } catch { + // Fallback: assume single line + rowHeightEntry = { + height: 42, + isCustomRenderer, + }; + } + } + + maxCellHeight = Math.max(maxCellHeight, rowHeightEntry?.height ?? 0); + if (options?.debug) { + rowCellHeights.push(rowHeightEntry); + } } - } - const totalHeight = maxCellHeight + rowPadding * 2; + const rowHeight = maxCellHeight + rowPadding * 2; + rowHeights.push(rowHeight); + cellHeights?.push(rowCellHeights); + } return { - height: totalHeight, - isCustomRenderer: false, - cellHeights: cellHeights ?? undefined, + rowHeights, + cellHeights, }; - }); - }); + }) + ); // Total height (sum of all row heights) - useful for virtualizer - const totalHeight = computed(() => rowHeights.value.reduce((sum, entry) => sum + entry.height, 0)); + const totalHeight = computed(() => rowHeights.value.reduce((sum, height) => sum + height, 0)); return { rowHeights, totalHeight, + cellHeights, }; }