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,
};
}