From e16118138d802b48624155eff2c950fb49d0454c Mon Sep 17 00:00:00 2001 From: hechang27-sprt Date: Thu, 9 Apr 2026 17:52:40 +0800 Subject: [PATCH] feat(json-view): support reactive values and circular paths --- .../task.json | 6 +- .../check.jsonl | 6 ++ .../debug.jsonl | 1 + .../implement.jsonl | 6 ++ .../04-09-json-view-virtualized-pretty/prd.md | 63 +++++++++++++ .../task.json | 45 +++++++++ examples/view/base/json-view.vue | 67 ++++++++++++++ packages/base/data/json-view/flattenJson.ts | 92 ++++++++++++++++--- packages/base/data/json-view/json-view.vue | 88 +++++++++++++++--- .../base/data/json-view/normalizeValue.ts | 52 +++++++++++ .../base/data/list-table-v2/list-table-v2.vue | 36 ++++---- 11 files changed, 414 insertions(+), 48 deletions(-) create mode 100644 .trellis/tasks/04-09-json-view-virtualized-pretty/check.jsonl create mode 100644 .trellis/tasks/04-09-json-view-virtualized-pretty/debug.jsonl create mode 100644 .trellis/tasks/04-09-json-view-virtualized-pretty/implement.jsonl create mode 100644 .trellis/tasks/04-09-json-view-virtualized-pretty/prd.md create mode 100644 .trellis/tasks/04-09-json-view-virtualized-pretty/task.json create mode 100644 packages/base/data/json-view/normalizeValue.ts 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 index ee1b7bd..3d40584 100644 --- a/.trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json +++ b/.trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json @@ -61,7 +61,9 @@ "description": "Fixed column pinning, pagination UI, column slots, dict/timestamp formatting, debug mode" } ], - "children": [], + "children": [ + "04-09-json-view-virtualized-pretty" + ], "parent": null, "relatedFiles": [ "packages/base/data/list-table-v2/", @@ -79,4 +81,4 @@ "packages/base/data/list-table-v2/useRuntimeHeightAugment.ts" ] } -} +} \ No newline at end of file diff --git a/.trellis/tasks/04-09-json-view-virtualized-pretty/check.jsonl b/.trellis/tasks/04-09-json-view-virtualized-pretty/check.jsonl new file mode 100644 index 0000000..d0b4b02 --- /dev/null +++ b/.trellis/tasks/04-09-json-view-virtualized-pretty/check.jsonl @@ -0,0 +1,6 @@ +{"file": ".claude/commands/trellis/finish-work.md", "reason": "Finish work checklist"} +{"file": ".claude/commands/trellis/check.md", "reason": "Code quality check spec"} +{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Review component structure and template patterns"} +{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "Review whether shared logic should be reused or extracted"} +{"file": ".trellis/spec/packages-base/base-components.md", "reason": "Review base component export integration and theme/i18n conventions"} +{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Review build/test workflow and package verification"} diff --git a/.trellis/tasks/04-09-json-view-virtualized-pretty/debug.jsonl b/.trellis/tasks/04-09-json-view-virtualized-pretty/debug.jsonl new file mode 100644 index 0000000..ce8c4b4 --- /dev/null +++ b/.trellis/tasks/04-09-json-view-virtualized-pretty/debug.jsonl @@ -0,0 +1 @@ +{"file": ".claude/commands/trellis/check.md", "reason": "Code quality check spec"} diff --git a/.trellis/tasks/04-09-json-view-virtualized-pretty/implement.jsonl b/.trellis/tasks/04-09-json-view-virtualized-pretty/implement.jsonl new file mode 100644 index 0000000..c98b7d8 --- /dev/null +++ b/.trellis/tasks/04-09-json-view-virtualized-pretty/implement.jsonl @@ -0,0 +1,6 @@ +{"file": ".trellis/workflow.md", "reason": "Project workflow and conventions"} +{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend development guide"} +{"file": ".trellis/spec/packages-base/base-components.md", "reason": "Base component export patterns and theme/i18n conventions"} +{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Build/test workflow and package verification expectations"} +{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Vue component structure, props/emits, template patterns"} +{"file": ".trellis/spec/guides/code-reuse-thinking-guide.md", "reason": "Shared utility extraction and avoiding duplicated virtualizer/measurement logic"} diff --git a/.trellis/tasks/04-09-json-view-virtualized-pretty/prd.md b/.trellis/tasks/04-09-json-view-virtualized-pretty/prd.md new file mode 100644 index 0000000..67ef708 --- /dev/null +++ b/.trellis/tasks/04-09-json-view-virtualized-pretty/prd.md @@ -0,0 +1,63 @@ +# Build Virtualized `json-view` Component + +## Goal + +Build a new `json-view` component under `packages/base/data/` that behaves similarly to `vue-json-pretty`, with strong support for large payloads via virtual scrolling and a more DOM-efficient rendering strategy. + +## Requirements + +- Create a new base component alongside `list-table-v2.vue`. +- Render formatted JSON objects/arrays/primitives in an interactive tree viewer. +- Keep the supported public API close to `vue-json-pretty` where practical. +- Support most `vue-json-pretty` viewer features except: + - selector + - inline editing +- Support expand/collapse interactions. +- Support virtual scrolling for large JSON trees. +- Reuse and adapt relevant pretext-based utilities from `packages/base/data/list-table-v2/` where useful. +- Optimize DOM output aggressively compared with `vue-json-pretty`, including: + - sharing one indentation element across multiple contiguous lines of the same level where possible + - using CSS pseudo-elements for affordances such as expand/collapse icons +- Place the component implementation under `packages/base/data/`, near `list-table-v2.vue`. + +## Feature Targets Compared to `vue-json-pretty` + +- Tree expansion/collapse +- Key/value display for objects +- Array index display +- Primitive type styling +- Empty object/array handling +- Path-aware rendering as needed for stable keys and interactions +- Optional depth/default-expand behavior +- `showLine`, `showLineNumber`, `showIcon`, `showDoubleQuotes` +- `collapsedNodeLength`, `deep`, `collapsedOnClickBrackets` +- render hooks/slots for node key and value +- virtual list controls such as `virtual`, `height`, `itemHeight` +- Virtualized rendering of visible rows + +## Acceptance Criteria + +- [ ] A new `json-view` component exists under `packages/base/data/` +- [ ] Component can render nested JSON structures with correct formatting +- [ ] Expand/collapse works for objects and arrays +- [ ] Large JSON payloads render with virtualization rather than mounting every visible line at once +- [ ] DOM structure is materially leaner than a naive per-line/per-indent implementation +- [ ] Shared utilities are reused where it improves consistency with `list-table-v2` +- [ ] Public API stays close to `vue-json-pretty` for the supported subset +- [ ] Public API is documented in code/types and is coherent for consumers +- [ ] `npm run type-check` passes + +## Technical Notes + +- Likely architecture: + - normalize JSON into a flat row model representing visible logical lines + - track expansion state by stable path keys + - compute visible rows from expansion state + - use a virtualizer similar to `useVirtualRows` from `list-table-v2` +- DOM minimization ideas to validate: + - row text assembled with fewer wrapper nodes + - indentation rendered via CSS background/pseudo-elements where practical + - toggle affordances rendered without dedicated icon nodes +- Need targeted research on: + - `vue-json-pretty` feature surface and prop API + - DOM-efficient strategies for indentation/toggles/row assembly diff --git a/.trellis/tasks/04-09-json-view-virtualized-pretty/task.json b/.trellis/tasks/04-09-json-view-virtualized-pretty/task.json new file mode 100644 index 0000000..2804f2b --- /dev/null +++ b/.trellis/tasks/04-09-json-view-virtualized-pretty/task.json @@ -0,0 +1,45 @@ +{ + "id": "json-view-virtualized-pretty", + "name": "json-view-virtualized-pretty", + "title": "Build virtualized json-view component", + "description": "", + "status": "planning", + "dev_type": "frontend", + "scope": null, + "package": null, + "priority": "P2", + "creator": "hechang27-sprt", + "assignee": "hechang27-sprt", + "createdAt": "2026-04-09", + "completedAt": null, + "branch": null, + "base_branch": "dev", + "worktree_path": null, + "current_phase": 0, + "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": [], + "children": [], + "parent": "04-03-list-table-v2-tanstack-pretext", + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/examples/view/base/json-view.vue b/examples/view/base/json-view.vue index 9cdd6f7..bcb94d7 100644 --- a/examples/view/base/json-view.vue +++ b/examples/view/base/json-view.vue @@ -144,6 +144,29 @@ :item-height="20" /> + +
+
+
+

Vue Reactivity Wrappers

+

Exercises nested `ref`, `computed`, `reactive`, and reactive `Map`/`Set` values.

+
+
+ + +
@@ -259,6 +282,50 @@ const jsData = computed(() => { }; }); +const vueWrappedData = computed(() => { + const status = ref("ready"); + const score = ref(42); + const doubledScore = computed(() => score.value * 2); + const owner = reactive({ + id: ref(7), + name: "Avery Stone", + role: computed(() => "ops"), + }); + + const wrappedMap = reactive( + new Map([ + ["status", status], + ["doubled", doubledScore], + ["owner", owner], + ]) + ); + + const wrappedSet = reactive(new Set([status, doubledScore, owner])); + + const state = reactive({ + status, + doubledScore, + owner, + wrappedMap, + wrappedSet, + nested: { + currentScore: score, + summary: computed(() => `${status.value}:${doubledScore.value}`), + }, + }); + + const rawState = state as unknown as Record; + rawState.self = ref(state); + + return ref({ + root: state, + derived: computed(() => ({ + active: status.value === "ready", + ownerName: owner.name, + })), + }); +}); + const menuItems = ({ value, copy }: { value: unknown; copy: (text: string) => Promise }) => [ { key: "copy-type", diff --git a/packages/base/data/json-view/flattenJson.ts b/packages/base/data/json-view/flattenJson.ts index 8de94dd..6b644e8 100644 --- a/packages/base/data/json-view/flattenJson.ts +++ b/packages/base/data/json-view/flattenJson.ts @@ -1,4 +1,9 @@ import type { BuildVisibleJsonRowsOptions, JsonViewNode, JsonViewNodeType } from "./types"; +import { + formatCircularReference, + isCircularReferenceMarker, + normalizeVueValue, +} from "./normalizeValue"; type PlainObject = Record; type JsonContainerKind = "object" | "array" | "map" | "set"; @@ -20,6 +25,22 @@ interface LineCounter { nextLineNumber: number; } +function toDisplayPath(path: string, rootPath: string) { + if (path === rootPath) { + return "$"; + } + + if (path.startsWith(`${rootPath}.`)) { + return `$${path.slice(rootPath.length)}`; + } + + if (path.startsWith(`${rootPath}[`)) { + return `$${path.slice(rootPath.length)}`; + } + + return path; +} + function isRecordLike(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set); } @@ -79,6 +100,13 @@ function isExpanded(path: string, level: number, length: number, options: BuildV } function formatKeyDisplay(value: unknown) { + const normalizedValue = normalizeVueValue(value); + if (isCircularReferenceMarker(normalizedValue)) { + return formatCircularReference(normalizedValue.path); + } + + value = normalizedValue; + if (typeof value === "string") { return JSON.stringify(value); } @@ -113,6 +141,13 @@ function formatKeyDisplay(value: unknown) { } function formatPrimitiveDisplay(value: unknown) { + const normalizedValue = normalizeVueValue(value); + if (isCircularReferenceMarker(normalizedValue)) { + return formatCircularReference(normalizedValue.path); + } + + value = normalizedValue; + if (typeof value === "string") { return JSON.stringify(value); } @@ -138,6 +173,10 @@ function formatPrimitiveDisplay(value: unknown) { } function getContainerKind(value: unknown): JsonContainerKind | null { + value = normalizeVueValue(value); + if (isCircularReferenceMarker(value)) { + return null; + } if (Array.isArray(value)) { return "array"; } @@ -201,7 +240,12 @@ function getContainerNodeType(kind: JsonContainerKind, state: "start" | "end" | return "arrayCollapsed"; } -function countValueLines(value: unknown, ancestors = new WeakSet()): number { +function countValueLines(value: unknown, ancestors = new WeakMap()): number { + value = normalizeVueValue(value); + if (isCircularReferenceMarker(value)) { + return 1; + } + const kind = getContainerKind(value); if (!kind) { return 1; @@ -212,7 +256,7 @@ function countValueLines(value: unknown, ancestors = new WeakSet()): num return 1; } - ancestors.add(reference); + ancestors.set(reference, true); const total = kind === "array" @@ -232,7 +276,7 @@ function countValueLines(value: unknown, ancestors = new WeakSet()): num return total; } -function countContainerChildLines(kind: JsonContainerKind, value: JsonContainerValue, ancestors: WeakSet) { +function countContainerChildLines(kind: JsonContainerKind, value: JsonContainerValue, ancestors: WeakMap) { if (kind === "array") { return (value as unknown[]).reduce((sum, entry) => sum + countValueLines(entry, ancestors), 0); } @@ -287,8 +331,24 @@ function appendValueRows( rows: JsonViewNode[], options: BuildVisibleJsonRowsOptions, counter: LineCounter, - ancestors: WeakSet + ancestors: WeakMap ) { + const normalizedValue = normalizeVueValue(value, toDisplayPath(context.path, options.rootPath)); + if (isCircularReferenceMarker(normalizedValue)) { + rows.push( + createNode( + counter, + context, + "content", + formatCircularReference(normalizedValue.path ?? toDisplayPath(context.path, options.rootPath)), + normalizedValue, + { isLeaf: true } + ) + ); + return; + } + + value = normalizedValue; const kind = getContainerKind(value); if (!kind) { rows.push(createNode(counter, context, "content", formatPrimitiveDisplay(value), value, { isLeaf: true })); @@ -296,12 +356,17 @@ function appendValueRows( } const reference = value as object; - if (ancestors.has(reference)) { - rows.push(createNode(counter, context, "content", "[Circular]", value, { isLeaf: true })); + const existingPath = ancestors.get(reference); + if (existingPath) { + rows.push( + createNode(counter, context, "content", formatCircularReference(toDisplayPath(existingPath, options.rootPath)), value, { + isLeaf: true, + }) + ); return; } - ancestors.add(reference); + ancestors.set(reference, context.path); appendContainerRows(kind, value as JsonContainerValue, context, rows, options, counter, ancestors); ancestors.delete(reference); } @@ -313,11 +378,11 @@ function appendContainerRows( rows: JsonViewNode[], options: BuildVisibleJsonRowsOptions, counter: LineCounter, - ancestors: WeakSet + ancestors: WeakMap ) { const length = getContainerLength(value, kind); const expanded = isExpanded(context.path, context.level, length, options); - const totalLineCount = 2 + countContainerChildLines(kind, value, ancestors); + const totalLineCount = 2 + countContainerChildLines(kind, value, new WeakMap()); if (!expanded) { rows.push( @@ -398,13 +463,14 @@ function appendContainerRows( }); } else { Array.from(value as Map).forEach(([keyValue, childValue], index) => { + const normalizedKeyValue = normalizeVueValue(keyValue, toDisplayPath(context.path, options.rootPath)); appendValueRows( childValue, { - path: buildMapPath(context.path, keyValue, index), + path: buildMapPath(context.path, normalizedKeyValue, index), level: context.level + 1, - displayKey: formatKeyDisplay(keyValue), - keyValue, + displayKey: formatKeyDisplay(normalizedKeyValue), + keyValue: normalizedKeyValue, showComma: index < length - 1, posInSet: index + 1, setSize: length, @@ -446,7 +512,7 @@ export function buildVisibleJsonRows(value: unknown, options: BuildVisibleJsonRo rows, options, counter, - new WeakSet() + new WeakMap() ); return rows; } diff --git a/packages/base/data/json-view/json-view.vue b/packages/base/data/json-view/json-view.vue index 04f9f0a..a4ca4cf 100644 --- a/packages/base/data/json-view/json-view.vue +++ b/packages/base/data/json-view/json-view.vue @@ -180,6 +180,11 @@ import type { DropdownInstance } from "element-plus"; import { measureShrinkWrapWidth, measureTextHeight } from "../list-table-v2/measureText"; import { useVirtualRows } from "../list-table-v2/useVirtualRows"; import { buildVisibleJsonRows } from "./flattenJson"; +import { + formatCircularReference, + isCircularReferenceMarker, + normalizeVueValue, +} from "./normalizeValue"; import type { JsonViewMenuActionContext, JsonViewMenuItem, JsonViewNode, JsonViewProps } from "./types"; const DEFAULT_FONT = "13px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"; @@ -518,7 +523,52 @@ const ActionsRenderer = ({ node }: { node: JsonViewNode }) => { return null; }; -function toSerializableValue(value: unknown, ancestors = new WeakSet()): unknown { +function buildDisplayObjectPath(parentPath: string, key: string) { + if (/^[A-Za-z_$][\w$]*$/.test(key)) { + return `${parentPath}.${key}`; + } + + return `${parentPath}[${JSON.stringify(key)}]`; +} + +function buildDisplayArrayPath(parentPath: string, index: number) { + return `${parentPath}[${index}]`; +} + +function buildDisplayMapPath(parentPath: string, key: unknown, index: number) { + if (typeof key === "string") { + return `${parentPath}.get(${JSON.stringify(key)})`; + } + + if (typeof key === "number" || typeof key === "boolean" || typeof key === "bigint") { + return `${parentPath}.get(${String(key)})`; + } + + if (key === null) { + return `${parentPath}.get(null)`; + } + + if (key === undefined) { + return `${parentPath}.get(undefined)`; + } + + if (typeof key === "symbol") { + return `${parentPath}.get(${String(key)})`; + } + + return `${parentPath}.entries[${index}]`; +} + +function toSerializableValue( + value: unknown, + currentPath = "$", + ancestors = new WeakMap() +): unknown { + value = normalizeVueValue(value, currentPath); + if (isCircularReferenceMarker(value)) { + return formatCircularReference(value.path ?? currentPath); + } + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) { return value; } @@ -543,29 +593,36 @@ function toSerializableValue(value: unknown, ancestors = new WeakSet()): return String(value); } - if (ancestors.has(value)) { - return "[Circular]"; + const existingPath = ancestors.get(value); + if (existingPath) { + return formatCircularReference(existingPath); } - ancestors.add(value); + ancestors.set(value, currentPath); if (Array.isArray(value)) { - const serialized = value.map((entry) => toSerializableValue(entry, ancestors)); + const serialized = value.map((entry, index) => + toSerializableValue(entry, buildDisplayArrayPath(currentPath, index), ancestors) + ); ancestors.delete(value); return serialized; } if (value instanceof Set) { - const serialized = { $set: Array.from(value, (entry) => toSerializableValue(entry, ancestors)) }; + const serialized = { + $set: Array.from(value, (entry, index) => + toSerializableValue(entry, buildDisplayArrayPath(currentPath, index), ancestors) + ), + }; ancestors.delete(value); return serialized; } if (value instanceof Map) { const serialized = { - $map: Array.from(value, ([key, entry]) => [ - toSerializableValue(key, ancestors), - toSerializableValue(entry, ancestors), + $map: Array.from(value, ([key, entry], index) => [ + toSerializableValue(key, buildDisplayMapPath(currentPath, key, index), ancestors), + toSerializableValue(entry, buildDisplayMapPath(currentPath, key, index), ancestors), ]), }; ancestors.delete(value); @@ -573,13 +630,16 @@ function toSerializableValue(value: unknown, ancestors = new WeakSet()): } const serialized = Object.fromEntries( - Object.entries(value).map(([key, entry]) => [key, toSerializableValue(entry, ancestors)]) + Object.entries(value).map(([key, entry]) => [ + key, + toSerializableValue(entry, buildDisplayObjectPath(currentPath, key), ancestors), + ]) ); ancestors.delete(value); return serialized; } -function serializeNodeValue(value: unknown): string { +function serializeNodeValue(value: unknown, currentPath = "$"): string { if (typeof value === "string") { return value; } @@ -587,7 +647,7 @@ function serializeNodeValue(value: unknown): string { return "undefined"; } try { - const json = JSON.stringify(toSerializableValue(value), null, 2); + const json = JSON.stringify(toSerializableValue(value, currentPath), null, 2); if (json !== undefined) { return json; } @@ -654,8 +714,8 @@ function getMenuItems(node: JsonViewNode): JsonViewMenuItem[] { { key: "copy-value", label: "Copy Value", - onSelect: async ({ value, copy }) => { - await copy(serializeNodeValue(value)); + onSelect: async ({ path, value, copy }) => { + await copy(serializeNodeValue(value, path)); }, }, { diff --git a/packages/base/data/json-view/normalizeValue.ts b/packages/base/data/json-view/normalizeValue.ts new file mode 100644 index 0000000..3789228 --- /dev/null +++ b/packages/base/data/json-view/normalizeValue.ts @@ -0,0 +1,52 @@ +import { isReactive, isReadonly, isRef, toRaw, unref } from "vue"; + +export const CIRCULAR_REFERENCE_TEXT = "[Circular]"; + +export interface CircularReferenceMarker { + __jsonViewCircular: true; + path?: string; +} + +export function createCircularReferenceMarker(path?: string): CircularReferenceMarker { + return { + __jsonViewCircular: true, + path, + }; +} + +export function isCircularReferenceMarker(value: unknown): value is CircularReferenceMarker { + return Boolean( + value && + typeof value === "object" && + "__jsonViewCircular" in value && + (value as { __jsonViewCircular?: boolean }).__jsonViewCircular + ); +} + +export function formatCircularReference(path?: string) { + return path ? `[Circular: ${path}]` : CIRCULAR_REFERENCE_TEXT; +} + +export function normalizeVueValue( + value: unknown, + currentPath?: string, + seenRefs = new WeakMap() +): unknown { + let current = value; + + while (isRef(current)) { + const reference = current as object; + const existingPath = seenRefs.get(reference); + if (existingPath !== undefined || seenRefs.has(reference)) { + return createCircularReferenceMarker(existingPath ?? currentPath); + } + seenRefs.set(reference, currentPath); + current = unref(current); + } + + if (current !== null && typeof current === "object" && (isReactive(current) || isReadonly(current))) { + return toRaw(current); + } + + return current; +} 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 dbb8d3b..33db698 100644 --- a/packages/base/data/list-table-v2/list-table-v2.vue +++ b/packages/base/data/list-table-v2/list-table-v2.vue @@ -2,25 +2,7 @@
-
Container Width: {{ containerWidth }}
-
Total Height: {{ virtualTotalHeight }}
-
Visible Rows: {{ visibleRows.map((entry) => entry.height).join(",") }}
-
- Cell Heights: - {{ - cellHeights?.map((rowCellHeights) => - rowCellHeights - .map((entry) => (entry ? `${entry.isCustomRenderer ? "*" : ""}${entry.height}` : "null")) - .join(",") - ) - }} -
-
Total Rows: {{ pageData.length }}
-
Column widths: {{ columnWidths.map((w) => Math.round(w)).join(",") }}
-
- Column configs: - {{ computedConfigs }} -
+
@@ -94,6 +76,7 @@ import { resolveRowHeights } from "./usePretextRowHeights"; import { useVirtualRows } from "./useVirtualRows"; import { formatTimestampFromValue } from "../../../../plugs/composables"; import { match } from "ts-pattern"; +import { JsonView } from "noob-mengyxu"; const DEFAULT_FONT = "14px sans-serif"; const DEFAULT_TEXT_MAX_WIDTH = 400; @@ -405,6 +388,21 @@ const handleCurrentChange = (val: number) => { emit("query"); }; +// ============================================================================ +// Debug Info +// ============================================================================ +const debugInfo = computed(() => ({ + containerWidth, + virtualTotalHeight, + visibleRows: visibleRows.value.map((entry) => entry.height).join(","), + cellHeights: cellHeights.value?.map((rowCellHeights) => + rowCellHeights.map((entry) => (entry ? `${entry.isCustomRenderer ? "*" : ""}${entry.height}` : "null")).join(",") + ), + totalRows: pageData.value.length, + columnWidths: columnWidths.value.map((w) => Math.round(w)).join(","), + columnConfigs: computedConfigs, +})); + // ============================================================================= // ResizeObserver for container dimensions // =============================================================================