From 0f72b8390d6b70dab9204cbf7922f5b749410bff Mon Sep 17 00:00:00 2001 From: hechang27-sprt Date: Fri, 3 Apr 2026 17:05:35 +0800 Subject: [PATCH] chore(task): archive 03-31-list-table-v2-resize-perf --- .../prd.md | 390 ++++++++++++++++++ .../task.json | 82 ++++ .../03-31-list-table-v2-resize-perf/task.json | 4 +- 3 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 .trellis/tasks/04-03-list-table-v2-tanstack-pretext/prd.md create mode 100644 .trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json rename .trellis/tasks/{ => archive/2026-04}/03-31-list-table-v2-resize-perf/task.json (93%) diff --git a/.trellis/tasks/04-03-list-table-v2-tanstack-pretext/prd.md b/.trellis/tasks/04-03-list-table-v2-tanstack-pretext/prd.md new file mode 100644 index 0000000..0933b4b --- /dev/null +++ b/.trellis/tasks/04-03-list-table-v2-tanstack-pretext/prd.md @@ -0,0 +1,390 @@ +# Rebuild list-table-v2 with TanStack + pretext + +## Goal + +Replace `el-table-v2` dependency with a custom implementation using: +- **@chenglou/pretext** for text measurement (column widths, row heights for plain text) +- **Custom prefix-sum virtualizer** (pretext-table style, lightweight) +- **TanStack Table** for headless table logic (optional, depends on complexity) + +Keep the existing prop interface of `list-table-v2.vue` so consumers don't need changes. + +## What I already know + +### Existing Component (`list-table-v2.vue`) +- Vue 3 + TypeScript component +- Props: `data`, `columns` (with key, dataKey, name, i18n, type, width, minWidth, fixed, align, slot, dict, timestamp, filesize, cellRenderer, headerCellRenderer), `page`, `height`, `minHeight`, `maxHeight`, `example`, `rowKey`, `selectKey`, `treeProps`, `lazy`, `border`, `timestampFormat`, `rowHeight`, `estimatedRowHeight`, `headerHeight`, `debug` +- Emits: `query`, `selection-change`, `row-click`, `cell-click` +- Uses mini hidden table for row height probing +- Uses `el-auto-resizer` + `el-table-v2` +- Features: pagination, fixed columns, sorting via slot, dict/timestamp/filesize formatting + +### pretext-table (React) Implementation Insights +- **@chenglou/pretext** two-phase API: + - `prepare(text, font)` → cached handle (Canvas measureText for segmentation) + - `layout(prepared, maxWidth, lineHeight)` → `{ height, lineCount }` (pure arithmetic, no DOM) +- **Column width**: Binary search with pretext to find minimum single-line width. Samples rows, measures header + cell text widths, respects minWidth/maxWidth +- **Row height**: `prepare()` per cell text, `layout()` at column width, takes max cell height + padding +- **Virtual scrolling**: Custom binary search over prefix-sum offsets (NOT TanStack Virtual) +- Does NOT use TanStack Table or TanStack Virtual + +### Available Packages +- `@tanstack/vue-table` v8.21.3 - headless Vue 3 table +- `@tanstack/vue-virtual` v3.13.23 - headless Vue 3 virtualization +- `@chenglou/pretext` v0.0.3 - text measurement + +## Virtualization Approach: Custom Prefix-Sum (pretext-table Style) + +Chosen over TanStack Virtual because: +1. Lightweight — binary search + prefix-sum offsets, no library dependency +2. Works naturally with pretext — all row heights pre-computed via `layout()`, just math +3. Full control over how pretext integrates + +### Algorithm +1. Pretext computes ALL row heights upfront (pure arithmetic, no DOM) +2. Build prefix-sum offset array: `offsets[i] = sum(heights[0..i-1])` +3. Binary search on scroll to find visible range: O(log n) +4. Render only visible rows + overscan + +### cellRenderer Return Types + +**Plain VNode (backward compatible):** +```typescript +cellRenderer: ({ data }) => {data} +``` + +**Size-hinted VNode (new):** +```typescript +cellRenderer: ({ data }) => ({ + vnode: {data}, + minHeight: 30, // pixel hint for row height estimation + minWidth: 100 // pixel hint for column width estimation (no runtime augmentation) +}) +``` + +### Runtime Augmentation for Row Heights + +For columns with custom `cellRenderer` (where pretext can't measure): +1. **Initial**: Use `minHeight` from size-hinted return, or sensible default (44px) +2. **After mount**: Measure actual DOM height per row using ResizeObserver +3. **Running average per column**: Maintain `Map` +4. **Self-adjust**: When average shifts >10%, recompute offsets and re-render + +### Column Width Strategy (Pre-computed, No Runtime Augmentation) + +Flex parameters computed via pretext-measured sampling: + +1. **Sample**: For each column, sample values from data rows +2. **Measure**: For each sampled value, measure "shrink wrap" width via `walkLineRanges` (finds widest line, handles `\n`) +3. **Compute statistics**: mean (μ), variance (σ²) of measured widths +4. **Assign flex parameters**: + - **flexBasis**: `μ` (average measured width + padding) — can be overridden per column + - **flexGrow**: derived from variance (see formula below) — can be overridden per column + - **flexShrink**: derived from variance — can be overridden per column + - **minWidth**: `max(μ - 2*σ, 50px)` — can be overridden per column + - **maxWidth**: `300px` fixed for now + +**Variance-based flex formula** (normalized 0-1 scale): +```typescript +const varianceScore = Math.min(1, σ / (μ * 0.5)); // 0 = no variance, 1 = high variance +const flexGrow = 0.1 + varianceScore * 1.9; // 0.1 (stable) to 2.0 (variable) +const flexShrink = 0.1 + varianceScore * 1.9; // same scaling +``` + +**User overrides**: Column definition can specify `flexGrow`, `flexShrink`, `flexBasis`, `minWidth`, `maxWidth` directly, which takes precedence over computed values. + +### Dynamic Wrapping Behavior + +- CSS `word-break: break-word` + `white-space: normal` on cells +- When column width reaches `maxWidth` (300px), text wraps to multiple lines +- When table can't expand (parent has `overflow-x: hidden`), columns compress (flexShrink applies) +- For custom renderer columns: uses `minWidth` from size-hinted return or default 80px + +User can override via column definition props. No runtime width adjustment. + +## Assumptions (to validate) +- pretext performance is acceptable for large datasets (~1M rows) +- Custom virtualizer approach is sufficient (no TanStack Virtual needed) +- Runtime height adjustment for custom renderers is acceptable UX + +## Open Questions + +All resolved: +- Fixed columns: **YES** (left/right pinning via `fixed: 'left' | 'right'`) +- flexGrow/flexShrink: **Data-driven via variance**; average affects flexBasis; manual overrides allowed +- maxWidth: **300px** fixed + +## Requirements + +- **Backward compatible Props**: Keep existing prop interface (data, columns, page, height, rowKey, border, etc.) +- **Config-less behavior**: Table works without any width configuration +- **Content-aware column widths**: Pretext measures sampled data; variance drives flexGrow/flexShrink, mean drives flexBasis +- **User overrides**: Column definition can override flexGrow, flexShrink, flexBasis, minWidth, maxWidth +- **Dynamic wrapping**: maxWidth=300px triggers CSS wrapping; parent overflow-x hidden triggers compression +- **Virtual scrolling**: Custom prefix-sum + binary search virtualizer; all row heights pre-computed via pretext +- **Runtime height augmentation**: For custom cellRenderer columns, measure DOM after mount, maintain running average +- **cellRenderer flexibility**: Return plain VNode or `{vnode, minHeight, minWidth}` object +- **Pagination**: Support existing pagination UI and behavior +- **Fixed columns**: Support left/right pinning via `fixed: 'left' | 'right'` +- **Slots**: Support column slots and header slots +- **Dict/timestamp/filesize formatting**: Keep existing built-in formatters + +## Acceptance Criteria (evolving) + +- [ ] Existing prop interface maintained (backward compatible) +- [ ] Table renders correctly without any width config +- [ ] Column widths adapt to content +- [ ] Long text wraps instead of overflowing +- [ ] Virtual scrolling works for large datasets +- [ ] Row heights estimated via pretext (no DOM measurement for height) +- [ ] Pagination works +- [ ] Fixed columns work +- [ ] Lint/typecheck passes + +## Out of Scope (explicit) + +- Tree data / expandable rows (not in current impl either) +- Column resizing by drag (nice to have) +- Sort/filter (via slot customization, not built-in) + +## Technical Notes + +### Implementation Structure (planned) + +``` +packages/base/data/list-table-v2.vue (main component) +├── usePretextColumnWidths() # pretext-based column width computation +├── usePretextRowHeights() # pretext-based row height pre-computation +├── useVirtualRows() # custom prefix-sum + binary search virtualizer +└── useRuntimeHeightAugment() # ResizeObserver + running average for custom renderers +``` + +### Column Width Computation (`usePretextColumnWidths`) + +1. Sample data rows (up to 100 samples evenly distributed) +2. For each sampled value, measure "shrink wrap" width via pretext: + ```typescript + let maxW = 0; + walkLineRanges(prepared, 10000, line => { + if (line.width > maxW) maxW = line.width; + }); + // maxW = minimum width to contain all lines (widest line wins) + ``` + This handles embedded `\n` correctly and avoids binary search. +3. Compute mean (μ) and variance (σ²) per column +4. Derive flex parameters: + - `flexBasis = μ + padding` + - `flexGrow = 0.1 + (σ/μ*0.5) * 1.9` (clamped 0.1-2.0) + - `flexShrink = same as flexGrow` + - `minWidth = max(μ - 2*σ, 50) + padding` + - `maxWidth = 300px` +5. User overrides from column definition applied after computation + +### Row Height Computation (`usePretextRowHeights`) + +For plain text columns (no custom renderer): +1. For each row, for each column: + - `prepare(text, font)` → cached + - `layout(prepared, columnWidth - padding, lineHeight)` → height +2. Row height = max cell height + row padding + +For custom renderer columns: +1. Use `minHeight` from size-hinted return +2. After mount, measure DOM via ResizeObserver +3. Update running average per column +4. Recompute offsets when average shifts >10% + +### Virtualization (`useVirtualRows`) + +1. Build prefix-sum offsets: `offsets[i+1] = offsets[i] + heights[i]` +2. On scroll: binary search for `startIndex` where `offsets[i+1] >= scrollTop` +3. Linear scan to find `endIndex` where `offsets[i] < scrollTop + viewportHeight` +4. Render rows [startIndex-overscan, endIndex+overscan] with `transform: translateY(offsets[startIndex])` + +### Packages Needed +- `@chenglou/pretext` v0.0.4 — text measurement + +--- + +## Implementation Plan (4 PRs) + +### PR1: Scaffold Hooks & Types ✅ COMPLETED + +**Files created:** +``` +packages/base/data/list-table-v2/ +├── index.ts # Exports all hooks and types +├── types.ts # ListTableColumn, ListTableProps, CellRendererResult, etc. +├── measureText.ts # Cached pretext measurement (prepareWithSegments caching) +├── usePretextColumnWidths.ts # Flex params from variance/mean +├── usePretextRowHeights.ts # Pre-computed row heights via pretext +├── useVirtualRows.ts # Prefix-sum + binary search virtualizer +└── useRuntimeHeightAugment.ts # ResizeObserver + running average +``` + +**Key decisions:** +- `prepareWithSegments()` for everything (superset type works with `layout()` + `walkLineRanges()`) +- Single cache for PreparedTextWithSegments, LRU-capped at 10,000 entries +- Types isolated in `types.ts` for backward compatibility + +--- + +### PR2: Core Virtualizer + Pretext Measurement (PENDING) + +**Goal**: Wire hooks into Vue component, implement flex container, basic table shell with virtual scrolling. + +**Files to create/modify:** +- `packages/base/data/list-table-v2/list-table-v2.vue` (rewrite) + +**Implementation steps:** + +1. **Vue component shell** + - Props: same as existing `list-table-v2.vue` (data, columns, page, height, etc.) + - Emits: query, selection-change, row-click, cell-click + - Use `el-auto-resizer` replacement or manual ResizeObserver for container width + +2. **Wire usePretextColumnWidths** + - `const { computedConfigs, totalFlexBasis } = usePretextColumnWidths(data, columns, containerWidth)` + - Convert computed flex configs to CSS/flexbox styling + +3. **Wire usePretextRowHeights** + - `const { rowHeights, totalHeight } = usePretextRowHeights(data, columns, columnWidths)` + - Returns array of heights for virtualizer + +4. **Wire useVirtualRows** + - `const { visibleRows, totalHeight, onScroll, scrollToIndex } = useVirtualRows(rowHeights, viewportHeight)` + - Container div with `overflow-y: auto` + - Inner spacer div with `height: totalHeight` + - Render visible rows at `transform: translateY(offsetY)` + +5. **Basic cell rendering** + - Plain text cells: render formatted value + - Use existing formatters (dict, timestamp, filesize) + +6. **CSS flex container for columns** + - Each column: `flex: ${flexGrow} ${flexShrink} ${flexBasis}px` + - `min-width: ${minWidth}px` + - `max-width: ${maxWidth}px` + - CSS `word-break: break-word` for wrapping + +7. **Header row** + - Fixed height: 44px (or `headerHeight` prop if provided) + - Render header cells with same flex styling + +**Acceptance criteria:** +- [ ] Table renders without any width config +- [ ] Column widths adapt to content +- [ ] Scrolling works for 1000+ rows (virtualized) +- [ ] No layout shift on scroll + +--- + +### PR3: Custom cellRenderer + Runtime Augmentation (PENDING) + +**Goal**: Support flexible cellRenderer return types and DOM measurement for custom renderers. + +**Files to create/modify:** +- `packages/base/data/list-table-v2/list-table-v2.vue` (update) + +**Implementation steps:** + +1. **cellRenderer return type handling** + ```typescript + function resolveCellContent(cellResult: CellRendererResult, params) { + if (!cellResult) return null; + // Plain VNode + if (cellResult.component) return cellResult; // VNode has 'component' + // Size-hinted object: { vnode, minHeight, minWidth } + return cellResult.vnode; + } + ``` + +2. **Track which columns have custom renderers** + - `const customRendererColumns = computed(() => columns.filter(c => c.cellRenderer))` + +3. **Wire useRuntimeHeightAugment** + - `const { recordHeight, flushSamples, getColumnHeight } = useRuntimeHeightAugment()` + +4. **Add ResizeObserver for custom renderer rows** + - For each rendered row, observe cells with custom renderers + - On resize: call `recordHeight(columnKey, measuredHeight)` + +5. **Recompute on significant change** + - After `flushSamples()`, check if any column average shifted >10% + - If so, update rowHeights and re-trigger virtualizer + +6. **Initial height for custom renderers** + - Check if cellRenderer returns `{vnode, minHeight}` — use that + - Otherwise fall back to 44px default + +**Acceptance criteria:** +- [ ] cellRenderer returning plain VNode works as before +- [ ] cellRenderer returning `{vnode, minHeight, minWidth}` works +- [ ] DOM heights measured after mount for custom renderers +- [ ] Running average updates row heights dynamically + +--- + +### PR4: Fixed Columns, Pagination, Slots, Polish (PENDING) + +**Goal**: Full feature parity with existing list-table-v2.vue + +**Files to create/modify:** +- `packages/base/data/list-table-v2/list-table-v2.vue` (update) + +**Implementation steps:** + +1. **Fixed columns (left/right pinning)** + - Split columns into: left-fixed, scrollable, right-fixed + - Left-fixed: `position: sticky; left: 0` + - Right-fixed: `position: sticky; right: 0` + - Main scroll container only contains scrollable columns + - Synchronize scroll position between fixed and scrollable headers + +2. **Pagination** + - Add pagination div below table + - Use existing `el-pagination` component + - On page/size change: emit `query` + +3. **Column slots** + - `v-slot:[column.key]` support for custom cell rendering + - `v-slot:header-[column.key]` support for custom header rendering + +4. **Slots passthrough** + - Pass through `slots` from parent component + +5. **Built-in formatters** + - dict: `formatterByDist()` from existing code + - timestamp: `TzDateTime` component + - filesize: `formatFileSize()` from existing code + +6. **Debug mode** + - Show estimated row heights, column flex params + - Toggle via `debug` prop + +7. **Border styling** + - CSS borders on cells/rows when `border` prop is true + +8. **Row/column events** + - `@row-click`: emit when row div clicked + - `@cell-click`: emit when cell clicked + +**Acceptance criteria:** +- [ ] Fixed left/right columns work +- [ ] Pagination emits query event +- [ ] Column slots work +- [ ] dict/timestamp/filesize formatting works +- [ ] Debug mode shows info +- [ ] Border styling works +- [ ] Row/cell click events fire + +--- + +## How to Resume After Context Clear + +1. Read `prd.md` for full requirements +2. Check `task.json` for current PR status (`subtasks` array) +3. PR1 is complete — hooks are scaffolded in `packages/base/data/list-table-v2/` +4. Continue with PR2: implement `list-table-v2.vue` component +5. PR3: add cellRenderer support + runtime augmentation +6. PR4: fixed columns, pagination, slots, polish diff --git a/.trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json b/.trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json new file mode 100644 index 0000000..ee1b7bd --- /dev/null +++ b/.trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json @@ -0,0 +1,82 @@ +{ + "id": "list-table-v2-tanstack-pretext", + "name": "list-table-v2-tanstack-pretext", + "title": "Rebuild list-table-v2 with pretext + custom virtualizer", + "description": "Replace el-table-v2 with custom virtualized table using @chenglou/pretext for text measurement", + "status": "planning", + "dev_type": "frontend", + "scope": "packages/base/data/list-table-v2/", + "priority": "P2", + "creator": "hechang27-sprt", + "assignee": "hechang27-sprt", + "createdAt": "2026-04-03", + "completedAt": null, + "branch": null, + "base_branch": "dev", + "worktree_path": null, + "current_phase": 1, + "next_action": [ + { + "phase": 1, + "action": "implement" + }, + { + "phase": 2, + "action": "check" + }, + { + "phase": 3, + "action": "finish" + }, + { + "phase": 4, + "action": "create-pr" + } + ], + "commit": null, + "pr_url": null, + "subtasks": [ + { + "id": "pr1-scaffold", + "title": "PR1: Scaffold hooks and types", + "status": "completed", + "description": "Install @chenglou/pretext, create hooks structure, basic types" + }, + { + "id": "pr2-core-virtualizer", + "title": "PR2: Core virtualizer + pretext measurement", + "status": "pending", + "description": "Wire hooks into Vue component, implement flex container, basic table shell with virtual scrolling" + }, + { + "id": "pr3-custom-renderers", + "title": "PR3: Custom cellRenderer support + runtime augmentation", + "status": "pending", + "description": "Implement cellRenderer return types (plain VNode + size-hinted), ResizeObserver for custom renderers" + }, + { + "id": "pr4-polish", + "title": "PR4: Fixed columns, pagination, slots, polish", + "status": "pending", + "description": "Fixed column pinning, pagination UI, column slots, dict/timestamp formatting, debug mode" + } + ], + "children": [], + "parent": null, + "relatedFiles": [ + "packages/base/data/list-table-v2/", + "packages/base/data/list-table-v2.vue" + ], + "notes": "", + "meta": { + "pr1_files": [ + "packages/base/data/list-table-v2/index.ts", + "packages/base/data/list-table-v2/types.ts", + "packages/base/data/list-table-v2/measureText.ts", + "packages/base/data/list-table-v2/usePretextColumnWidths.ts", + "packages/base/data/list-table-v2/usePretextRowHeights.ts", + "packages/base/data/list-table-v2/useVirtualRows.ts", + "packages/base/data/list-table-v2/useRuntimeHeightAugment.ts" + ] + } +} diff --git a/.trellis/tasks/03-31-list-table-v2-resize-perf/task.json b/.trellis/tasks/archive/2026-04/03-31-list-table-v2-resize-perf/task.json similarity index 93% rename from .trellis/tasks/03-31-list-table-v2-resize-perf/task.json rename to .trellis/tasks/archive/2026-04/03-31-list-table-v2-resize-perf/task.json index 86eb197..0a797ba 100644 --- a/.trellis/tasks/03-31-list-table-v2-resize-perf/task.json +++ b/.trellis/tasks/archive/2026-04/03-31-list-table-v2-resize-perf/task.json @@ -3,14 +3,14 @@ "name": "list-table-v2-resize-perf", "title": "Optimize list-table-v2 resize performance", "description": "", - "status": "planning", + "status": "completed", "dev_type": null, "scope": null, "priority": "P2", "creator": "hechang27-sprt", "assignee": "hechang27-sprt", "createdAt": "2026-03-31", - "completedAt": null, + "completedAt": "2026-04-03", "branch": null, "base_branch": "dev", "worktree_path": null,