Browse Source

chore(task): archive 03-31-list-table-v2-resize-perf

dev
hechang27-sprt 3 months ago
parent
commit
0f72b8390d
  1. 390
      .trellis/tasks/04-03-list-table-v2-tanstack-pretext/prd.md
  2. 82
      .trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json
  3. 4
      .trellis/tasks/archive/2026-04/03-31-list-table-v2-resize-perf/task.json

390
.trellis/tasks/04-03-list-table-v2-tanstack-pretext/prd.md

@ -0,0 +1,390 @@ @@ -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 }) => <span>{data}</span>
```
**Size-hinted VNode (new):**
```typescript
cellRenderer: ({ data }) => ({
vnode: <el-tag>{data}</el-tag>,
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<columnKey, { samples: number[], avg: number }>`
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<T>, 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

82
.trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json

@ -0,0 +1,82 @@ @@ -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"
]
}
}

4
.trellis/tasks/03-31-list-table-v2-resize-perf/task.json → .trellis/tasks/archive/2026-04/03-31-list-table-v2-resize-perf/task.json

@ -3,14 +3,14 @@ @@ -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,
Loading…
Cancel
Save