diff --git a/bun.lock b/bun.lock
index 428ab10..02c7791 100644
--- a/bun.lock
+++ b/bun.lock
@@ -21,6 +21,7 @@
"xterm": "^5.3.0",
"xterm-addon-attach": "^0.9.0",
"xterm-addon-fit": "^0.8.0",
+ "yoga-layout": "^3.2.1",
},
"devDependencies": {
"@types/js-md5": "^0.8.0",
@@ -1888,6 +1889,8 @@
"yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
+ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
+
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"@microsoft/api-extractor/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
diff --git a/package.json b/package.json
index 703ec2a..d3eb61e 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,8 @@
"vuex": "^4.1.0",
"xterm": "^5.3.0",
"xterm-addon-attach": "^0.9.0",
- "xterm-addon-fit": "^0.8.0"
+ "xterm-addon-fit": "^0.8.0",
+ "yoga-layout": "^3.2.1"
},
"devDependencies": {
"@types/js-md5": "^0.8.0",
diff --git a/packages/base/data/list-table-v2/index.ts b/packages/base/data/list-table-v2/index.ts
index 61b59aa..14f3f8e 100644
--- a/packages/base/data/list-table-v2/index.ts
+++ b/packages/base/data/list-table-v2/index.ts
@@ -27,7 +27,7 @@ export {
} from "./measureText";
// Hooks
-export { usePretextColumnWidths, type ColumnFlexConfig } from "./usePretextColumnWidths";
+export { usePretextColumnWidths, computeFlexWidths, type ColumnFlexConfig } from "./usePretextColumnWidths";
export { usePretextRowHeights, type RowHeightEntry } from "./usePretextRowHeights";
export {
useVirtualRows,
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 dde0152..62a9087 100644
--- a/packages/base/data/list-table-v2/list-table-v2.vue
+++ b/packages/base/data/list-table-v2/list-table-v2.vue
@@ -4,8 +4,17 @@
Container Width: {{ containerWidth }}
Total Height: {{ virtualTotalHeight }}
-
Visible Rows: {{ visibleRows.length }}
+
Visible Rows: {{ visibleRows.map((entry) => entry.height).join(",") }}
+
+ Cell Heights:
+ {{
+ rowHeights.map((entry) =>
+ entry.cellHeights?.map((entry) => `${entry.isCustomRenderer ? "*" : ""}${entry.height}`).join(",")
+ )
+ }}
+
Total Rows: {{ pageData.length }}
+
Column widths: {{ columnWidths.map((w) => Math.round(w)).join(",") }}
Column configs:
{{
@@ -86,6 +95,8 @@ import { usePretextColumnWidths, type ColumnFlexConfig } from "./usePretextColum
import { usePretextRowHeights, type RowHeightEntry } from "./usePretextRowHeights";
import { useVirtualRows } from "./useVirtualRows";
+const DEFAULT_FONT = "14px sans-serif";
+
const slots = useSlots();
const { t } = useI18n();
const { state } = useStore();
@@ -119,6 +130,7 @@ interface Props {
rowHeight?: number;
estimatedRowHeight?: number;
headerHeight?: number;
+ font?: string;
debug?: boolean;
}
@@ -139,6 +151,7 @@ const props = withDefaults(defineProps
(), {
rowHeight: undefined,
estimatedRowHeight: undefined,
headerHeight: 44,
+ font: DEFAULT_FONT,
debug: false,
});
@@ -173,26 +186,88 @@ const columnsRef = toRef(props, "columns");
// Pass computed pageData to hooks
const pageDataRef = toRef(pageData);
+// =============================================================================
+// Cell formatting
+// =============================================================================
+const getHeaderText = (col: ListTableColumn): string => {
+ if (col.name) return col.name;
+ if (col.i18n) return t(col.i18n);
+ return col.key;
+};
+
+const formatCellValue = (row: T, col: ListTableColumn): string => {
+ const value = (row as any)[col.dataKey || col.key];
+
+ if (col.dict) {
+ return formatterByDist(col.dict, value);
+ }
+ if (col.timestamp) {
+ return formatStamp(value);
+ }
+ if (col.filesize) {
+ return formatFileSize(value);
+ }
+
+ if (value === undefined || value === null || value === "") {
+ return "--";
+ }
+ return String(value);
+};
+
+const getValue = (value: any): string => {
+ if ((typeof value === "undefined" || value === null || value === "") && value !== 0) {
+ return "--";
+ }
+ return value;
+};
+
+const formatStamp = (value: any): string => {
+ const date = new Date(value * 1000);
+ const month = date.getMonth() < 9 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1;
+ const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
+ const hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
+ const minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
+ const second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
+ return `${date.getFullYear()}-${month}-${day} ${hour}:${minute}:${second}`;
+};
+
+const formatFileSize = (value: any): string => {
+ const k = value / 1024;
+ if (k < 1) return value + "B";
+ const m = k / 1024;
+ if (m < 1) return k.toFixed(2) + "K";
+ const g = m / 1024;
+ if (g < 1) return m.toFixed(2) + "M";
+ return g.toFixed(2) + "G";
+};
+
+const formatterByDist = (dictKey: string, cellData: any): string => {
+ if (!dictKey) return getValue(cellData);
+ const mapping = (state as any).dict[dictKey];
+ if (mapping == null) return getValue(cellData);
+ return mapping[cellData] == null ? cellData : mapping[cellData];
+};
+
// =============================================================================
// Wire hooks
// =============================================================================
// Column width computation via pretext
-const { computedConfigs, totalFlexBasis } = usePretextColumnWidths(pageDataRef, columnsRef, containerWidth, {
- font: "14px Inter, sans-serif",
- headerFont: "bold 14px Inter, sans-serif",
-});
-
-// Column widths as array of numbers (for usePretextRowHeights)
-const columnWidths = computed(() => {
- return computedConfigs.value.map((c) => c.flexBasis);
-});
+const { computedConfigs, totalFlexBasis, columnWidths } = usePretextColumnWidths(
+ pageDataRef,
+ columnsRef,
+ containerWidth,
+ {
+ font: "14px Inter, sans-serif",
+ headerFont: "bold 14px Inter, sans-serif",
+ }
+);
-// Row heights via pretext
-const { rowHeights, totalHeight } = usePretextRowHeights(pageDataRef, columnsRef, columnWidths, {
- font: "14px Inter, sans-serif",
+// Row heights via pretext - use actual column widths from flexbox algorithm
+const { rowHeights, totalHeight } = usePretextRowHeights(pageDataRef, columnsRef, columnWidths, formatCellValue, {
+ font: props.font,
lineHeight: 20,
- rowPadding: 12,
+ debug: props.debug,
});
// Virtualizer (needs rowHeights as number[] and viewportHeight)
@@ -237,14 +312,28 @@ const tableContainerStyle = computed(() => {
// Column styling
// =============================================================================
function getColumnStyle(col: ListTableColumn): Record {
- const config = computedConfigs.value.find((c) => c.key === col.key);
- if (!config) {
- return { flex: "1 1 100px", minWidth: "50px", maxWidth: "300px" };
+ // 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;
+
+ if (!computedWidth || computedWidth <= 0) {
+ // Fallback to flex property if no computed width
+ const config = computedConfigs.value.find((c) => c.key === col.key);
+ if (!config) {
+ return { flex: "1 1 100px", minWidth: "50px", maxWidth: "300px" };
+ }
+ return {
+ flex: `${config.flexGrow} ${config.flexShrink} ${config.flexBasis}px`,
+ minWidth: `${config.minWidth}px`,
+ maxWidth: `${config.maxWidth}px`,
+ };
}
+
+ // Use the yoga-computed width directly for consistent measurement
return {
- flex: `${config.flexGrow} ${config.flexShrink} ${config.flexBasis}px`,
- minWidth: `${config.minWidth}px`,
- maxWidth: `${config.maxWidth}px`,
+ width: `${computedWidth}px`,
+ minWidth: `${computedWidth}px`,
+ maxWidth: `${computedWidth}px`,
};
}
@@ -255,68 +344,6 @@ const resolvedHeaderHeight = computed(() => {
return props.headerHeight ?? 44;
});
-// =============================================================================
-// Cell formatting
-// =============================================================================
-const getHeaderText = (col: ListTableColumn): string => {
- if (col.name) return col.name;
- if (col.i18n) return t(col.i18n);
- return col.key;
-};
-
-const formatCellValue = (row: T, col: ListTableColumn): string => {
- const value = (row as any)[col.dataKey || col.key];
-
- if (col.dict) {
- return formatterByDist(col.dict, value);
- }
- if (col.timestamp) {
- return formatStamp(value);
- }
- if (col.filesize) {
- return formatFileSize(value);
- }
-
- if (value === undefined || value === null || value === "") {
- return "--";
- }
- return String(value);
-};
-
-const getValue = (value: any): string => {
- if ((typeof value === "undefined" || value === null || value === "") && value !== 0) {
- return "--";
- }
- return value;
-};
-
-const formatStamp = (value: any): string => {
- const date = new Date(value * 1000);
- const month = date.getMonth() < 9 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1;
- const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
- const hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
- const minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
- const second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
- return `${date.getFullYear()}-${month}-${day} ${hour}:${minute}:${second}`;
-};
-
-const formatFileSize = (value: any): string => {
- const k = value / 1024;
- if (k < 1) return value + "B";
- const m = k / 1024;
- if (m < 1) return k.toFixed(2) + "K";
- const g = m / 1024;
- if (g < 1) return m.toFixed(2) + "M";
- return g.toFixed(2) + "G";
-};
-
-const formatterByDist = (dictKey: string, cellData: any): string => {
- if (!dictKey) return getValue(cellData);
- const mapping = (state as any).dict[dictKey];
- if (mapping == null) return getValue(cellData);
- return mapping[cellData] == null ? cellData : mapping[cellData];
-};
-
// =============================================================================
// Scroll handling
// =============================================================================
@@ -499,7 +526,7 @@ onUnmounted(() => {
white-space: normal;
word-break: break-word;
color: v-bind("state.style.tableColor");
- font-size: var(--el-font-size-base);
+ font: v-bind("props.font");
}
.my-pagination {
diff --git a/packages/base/data/list-table-v2/usePretextColumnWidths.ts b/packages/base/data/list-table-v2/usePretextColumnWidths.ts
index 308903a..75ef799 100644
--- a/packages/base/data/list-table-v2/usePretextColumnWidths.ts
+++ b/packages/base/data/list-table-v2/usePretextColumnWidths.ts
@@ -53,8 +53,7 @@ function computeStats(widths: number[]): { mean: number; variance: number } {
if (widths.length === 0) return { mean: 0, variance: 0 };
const mean = widths.reduce((sum, w) => sum + w, 0) / widths.length;
- const variance =
- widths.reduce((sum, w) => sum + Math.pow(w - mean, 2), 0) / widths.length;
+ const variance = widths.reduce((sum, w) => sum + Math.pow(w - mean, 2), 0) / widths.length;
return { mean, variance };
}
@@ -71,6 +70,118 @@ function varianceToFlex(mean: number, variance: number): number {
return 0.1 + varianceScore * 1.9;
}
+/**
+ * Compute actual column widths using a flexbox-inspired distribution algorithm.
+ * - First, compute raw flex widths based on available space
+ * - Then, enforce minWidth/maxWidth constraints iteratively
+ * - If constraints cause overflow, reduce widths proportionally while respecting mins
+ */
+export function computeFlexWidths(configs: ColumnFlexConfig[], containerWidth: number): number[] {
+ if (!configs.length || containerWidth <= 0) {
+ return configs.map(() => 0);
+ }
+
+ // Calculate total flex basis
+ const totalFlexBasis = configs.reduce((sum, c) => sum + c.flexBasis, 0);
+
+ if (totalFlexBasis === 0) {
+ // Edge case: all zero bases - distribute evenly
+ const evenWidth = containerWidth / configs.length;
+ return configs.map((c) => clamp(evenWidth, c.minWidth, c.maxWidth));
+ }
+
+ // Calculate raw flex widths
+ let widths = computeRawFlexWidths(configs, containerWidth, totalFlexBasis);
+
+ // Apply min/max constraints iteratively until stable
+ // This handles the case where clamping some columns causes others to overflow
+ for (let iteration = 0; iteration < 10; iteration++) {
+ // Apply constraints
+ widths = widths.map((w, i) => clamp(w, configs[i].minWidth, configs[i].maxWidth));
+
+ const totalWidth = widths.reduce((sum, w) => sum + w, 0);
+
+ if (totalWidth <= containerWidth) {
+ // Fits! Distribute remaining space proportionally based on flexGrow
+ const remaining = containerWidth - totalWidth;
+ if (remaining > 0) {
+ const totalFlexGrow = configs.reduce((sum, c) => sum + c.flexGrow * c.flexBasis, 0);
+ if (totalFlexGrow > 0) {
+ widths = widths.map((w, i) => {
+ const growAmount = (configs[i].flexGrow * configs[i].flexBasis) / totalFlexGrow * remaining;
+ return w + growAmount;
+ });
+ }
+ }
+ break;
+ }
+
+ // Overflow - need to reduce widths that are above their minimums
+ const overflow = totalWidth - containerWidth;
+
+ // Calculate how much each column can be reduced (only those above minWidth)
+ const reducible = widths.map((w, i) => Math.max(0, w - configs[i].minWidth));
+ const totalReducible = reducible.reduce((sum, r) => sum + r, 0);
+
+ if (totalReducible === 0) {
+ // All at minimum - force even distribution
+ const evenWidth = containerWidth / configs.length;
+ widths = configs.map((c) => clamp(evenWidth, c.minWidth, c.maxWidth));
+ break;
+ }
+
+ // Reduce proportionally based on how much each column can be reduced
+ widths = widths.map((w, i) => {
+ const reduction = reducible[i] / totalReducible * overflow;
+ return Math.max(w - reduction, configs[i].minWidth);
+ });
+ }
+
+ // Final clamp
+ widths = widths.map((w, i) => clamp(w, configs[i].minWidth, configs[i].maxWidth));
+
+ return widths;
+}
+
+/**
+ * Compute raw flex widths without min/max constraints
+ */
+function computeRawFlexWidths(configs: ColumnFlexConfig[], containerWidth: number, totalFlexBasis: number): number[] {
+ const availableSpace = containerWidth - totalFlexBasis;
+
+ if (availableSpace === 0) {
+ return configs.map((c) => c.flexBasis);
+ }
+
+ if (availableSpace > 0) {
+ // Grow phase: distribute extra space proportionally by flexGrow * flexBasis
+ const totalGrowFactor = configs.reduce((sum, c) => sum + c.flexGrow * c.flexBasis, 0);
+ if (totalGrowFactor === 0) {
+ return configs.map((c) => c.flexBasis);
+ }
+ return configs.map((c) => {
+ const growAmount = (c.flexGrow * c.flexBasis) / totalGrowFactor * availableSpace;
+ return c.flexBasis + growAmount;
+ });
+ } else {
+ // Shrink phase: remove space proportionally by flexShrink * flexBasis
+ const totalShrinkFactor = configs.reduce((sum, c) => sum + c.flexShrink * c.flexBasis, 0);
+ if (totalShrinkFactor === 0) {
+ // No shrink - distribute loss evenly
+ const evenShrink = (-availableSpace) / configs.length;
+ return configs.map((c) => Math.max(0, c.flexBasis - evenShrink));
+ }
+ return configs.map((c) => {
+ const shrinkAmount = (c.flexShrink * c.flexBasis) / totalShrinkFactor * (-availableSpace);
+ return Math.max(0, c.flexBasis - shrinkAmount);
+ });
+ }
+}
+
+function clamp(value: number, min: number, max: number): number {
+ return Math.max(min, Math.min(max, value));
+}
+
export function usePretextColumnWidths(
data: Ref,
columns: Ref[]>,
@@ -97,14 +208,9 @@ export function usePretextColumnWidths(
// User overrides (convert string values to numbers)
const userFlexGrow = col.flexGrow;
const userFlexShrink = col.flexShrink;
- const userFlexBasis =
- col.flexBasis !== undefined && col.flexBasis !== "auto"
- ? Number(col.flexBasis)
- : undefined;
- const userMinWidth =
- col.minWidth !== undefined ? Number(col.minWidth) : undefined;
- const userMaxWidth =
- col.maxWidth !== undefined ? Number(col.maxWidth) : undefined;
+ const userFlexBasis = col.flexBasis !== undefined && col.flexBasis !== "auto" ? Number(col.flexBasis) : undefined;
+ const userMinWidth = col.minWidth !== undefined ? Number(col.minWidth) : undefined;
+ const userMaxWidth = col.maxWidth !== undefined ? Number(col.maxWidth) : undefined;
// Measure header width
const headerText = col.name || col.i18n || colKey;
@@ -151,12 +257,14 @@ export function usePretextColumnWidths(
});
// Total flex basis (sum of all flexBasis values)
- const totalFlexBasis = computed(() =>
- computedConfigs.value.reduce((sum, c) => sum + c.flexBasis, 0)
- );
+ const totalFlexBasis = computed(() => computedConfigs.value.reduce((sum, c) => sum + c.flexBasis, 0));
+
+ // Actual column widths computed using flexbox algorithm
+ const columnWidths = computed(() => computeFlexWidths(computedConfigs.value, containerWidth.value));
return {
computedConfigs,
totalFlexBasis,
+ columnWidths,
};
}
diff --git a/packages/base/data/list-table-v2/usePretextRowHeights.ts b/packages/base/data/list-table-v2/usePretextRowHeights.ts
index 314ba55..442b5cc 100644
--- a/packages/base/data/list-table-v2/usePretextRowHeights.ts
+++ b/packages/base/data/list-table-v2/usePretextRowHeights.ts
@@ -11,22 +11,25 @@ import type { ListTableColumn } from "./types";
const DEFAULT_FONT = "14px Inter, sans-serif";
const DEFAULT_LINE_HEIGHT = 20;
-const DEFAULT_ROW_PADDING = 12;
-const CELL_VERTICAL_PADDING = 8; // top + bottom per cell
+const DEFAULT_ROW_PADDING = 5;
+const CELL_VERTICAL_PADDING = 12; // top + bottom per cell
export interface RowHeightEntry {
height: number;
isCustomRenderer: boolean;
+ cellHeights?: RowHeightEntry[];
}
export function usePretextRowHeights(
data: Ref,
columns: Ref[]>,
columnWidths: Ref,
+ formatCellValue: (row: T, col: ListTableColumn) => string,
options?: {
font?: string;
lineHeight?: number;
rowPadding?: number;
+ debug?: boolean;
}
) {
const font = options?.font ?? DEFAULT_FONT;
@@ -40,9 +43,12 @@ export function usePretextRowHeights(
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)
@@ -54,13 +60,16 @@ export function usePretextRowHeights(
// 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 rawValue = (row as any)[col.dataKey || col.key];
- const cellText = rawValue == null ? "" : String(rawValue);
-
+ const cellText = formatCellValue(row, col);
if (!cellText) continue;
// Calculate available width for text (excluding cell padding)
@@ -68,31 +77,30 @@ export function usePretextRowHeights(
if (availableWidth <= 0) continue;
try {
- const cellHeight = measureTextHeight(
- cellText,
- font,
- availableWidth,
- lineHeight
- );
+ const cellHeight = measureTextHeight(cellText, font, availableWidth, lineHeight);
maxCellHeight = Math.max(maxCellHeight, cellHeight);
+
+ cellHeights?.push({
+ height: cellHeight,
+ isCustomRenderer: false,
+ });
} catch {
// Fallback: assume single line
}
}
- const totalHeight = maxCellHeight + rowPadding * 2 + CELL_VERTICAL_PADDING * 2;
+ const totalHeight = maxCellHeight + rowPadding * 2;
return {
height: totalHeight,
isCustomRenderer: false,
+ cellHeights: cellHeights ?? undefined,
};
});
});
// 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, entry) => sum + entry.height, 0));
return {
rowHeights,