Browse Source

feat(json-view): support reactive values and circular paths

dev
hechang27-sprt 3 months ago
parent
commit
e16118138d
  1. 6
      .trellis/tasks/04-03-list-table-v2-tanstack-pretext/task.json
  2. 6
      .trellis/tasks/04-09-json-view-virtualized-pretty/check.jsonl
  3. 1
      .trellis/tasks/04-09-json-view-virtualized-pretty/debug.jsonl
  4. 6
      .trellis/tasks/04-09-json-view-virtualized-pretty/implement.jsonl
  5. 63
      .trellis/tasks/04-09-json-view-virtualized-pretty/prd.md
  6. 45
      .trellis/tasks/04-09-json-view-virtualized-pretty/task.json
  7. 67
      examples/view/base/json-view.vue
  8. 92
      packages/base/data/json-view/flattenJson.ts
  9. 88
      packages/base/data/json-view/json-view.vue
  10. 52
      packages/base/data/json-view/normalizeValue.ts
  11. 36
      packages/base/data/list-table-v2/list-table-v2.vue

6
.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" "description": "Fixed column pinning, pagination UI, column slots, dict/timestamp formatting, debug mode"
} }
], ],
"children": [], "children": [
"04-09-json-view-virtualized-pretty"
],
"parent": null, "parent": null,
"relatedFiles": [ "relatedFiles": [
"packages/base/data/list-table-v2/", "packages/base/data/list-table-v2/",
@ -79,4 +81,4 @@
"packages/base/data/list-table-v2/useRuntimeHeightAugment.ts" "packages/base/data/list-table-v2/useRuntimeHeightAugment.ts"
] ]
} }
} }

6
.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"}

1
.trellis/tasks/04-09-json-view-virtualized-pretty/debug.jsonl

@ -0,0 +1 @@
{"file": ".claude/commands/trellis/check.md", "reason": "Code quality check spec"}

6
.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"}

63
.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

45
.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": {}
}

67
examples/view/base/json-view.vue

@ -144,6 +144,29 @@
:item-height="20" :item-height="20"
/> />
</section> </section>
<section class="demo-panel">
<div class="demo-panel__header">
<div>
<h3>Vue Reactivity Wrappers</h3>
<p>Exercises nested `ref`, `computed`, `reactive`, and reactive `Map`/`Set` values.</p>
</div>
</div>
<JsonView
:data="vueWrappedData"
:virtual="true"
:dynamic-height="true"
:show-line="true"
:show-line-number="true"
:show-icon="true"
:show-length="true"
theme="dark"
:indent="2"
:height="320"
:item-height="20"
/>
</section>
</div> </div>
</template> </template>
@ -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<unknown, unknown>([
["status", status],
["doubled", doubledScore],
["owner", owner],
])
);
const wrappedSet = reactive(new Set<unknown>([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<string, unknown>;
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<boolean> }) => [ const menuItems = ({ value, copy }: { value: unknown; copy: (text: string) => Promise<boolean> }) => [
{ {
key: "copy-type", key: "copy-type",

92
packages/base/data/json-view/flattenJson.ts

@ -1,4 +1,9 @@
import type { BuildVisibleJsonRowsOptions, JsonViewNode, JsonViewNodeType } from "./types"; import type { BuildVisibleJsonRowsOptions, JsonViewNode, JsonViewNodeType } from "./types";
import {
formatCircularReference,
isCircularReferenceMarker,
normalizeVueValue,
} from "./normalizeValue";
type PlainObject = Record<string, unknown>; type PlainObject = Record<string, unknown>;
type JsonContainerKind = "object" | "array" | "map" | "set"; type JsonContainerKind = "object" | "array" | "map" | "set";
@ -20,6 +25,22 @@ interface LineCounter {
nextLineNumber: number; 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<string, unknown> { function isRecordLike(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set); 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) { function formatKeyDisplay(value: unknown) {
const normalizedValue = normalizeVueValue(value);
if (isCircularReferenceMarker(normalizedValue)) {
return formatCircularReference(normalizedValue.path);
}
value = normalizedValue;
if (typeof value === "string") { if (typeof value === "string") {
return JSON.stringify(value); return JSON.stringify(value);
} }
@ -113,6 +141,13 @@ function formatKeyDisplay(value: unknown) {
} }
function formatPrimitiveDisplay(value: unknown) { function formatPrimitiveDisplay(value: unknown) {
const normalizedValue = normalizeVueValue(value);
if (isCircularReferenceMarker(normalizedValue)) {
return formatCircularReference(normalizedValue.path);
}
value = normalizedValue;
if (typeof value === "string") { if (typeof value === "string") {
return JSON.stringify(value); return JSON.stringify(value);
} }
@ -138,6 +173,10 @@ function formatPrimitiveDisplay(value: unknown) {
} }
function getContainerKind(value: unknown): JsonContainerKind | null { function getContainerKind(value: unknown): JsonContainerKind | null {
value = normalizeVueValue(value);
if (isCircularReferenceMarker(value)) {
return null;
}
if (Array.isArray(value)) { if (Array.isArray(value)) {
return "array"; return "array";
} }
@ -201,7 +240,12 @@ function getContainerNodeType(kind: JsonContainerKind, state: "start" | "end" |
return "arrayCollapsed"; return "arrayCollapsed";
} }
function countValueLines(value: unknown, ancestors = new WeakSet<object>()): number { function countValueLines(value: unknown, ancestors = new WeakMap<object, true>()): number {
value = normalizeVueValue(value);
if (isCircularReferenceMarker(value)) {
return 1;
}
const kind = getContainerKind(value); const kind = getContainerKind(value);
if (!kind) { if (!kind) {
return 1; return 1;
@ -212,7 +256,7 @@ function countValueLines(value: unknown, ancestors = new WeakSet<object>()): num
return 1; return 1;
} }
ancestors.add(reference); ancestors.set(reference, true);
const total = const total =
kind === "array" kind === "array"
@ -232,7 +276,7 @@ function countValueLines(value: unknown, ancestors = new WeakSet<object>()): num
return total; return total;
} }
function countContainerChildLines(kind: JsonContainerKind, value: JsonContainerValue, ancestors: WeakSet<object>) { function countContainerChildLines(kind: JsonContainerKind, value: JsonContainerValue, ancestors: WeakMap<object, true>) {
if (kind === "array") { if (kind === "array") {
return (value as unknown[]).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0); return (value as unknown[]).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0);
} }
@ -287,8 +331,24 @@ function appendValueRows(
rows: JsonViewNode[], rows: JsonViewNode[],
options: BuildVisibleJsonRowsOptions, options: BuildVisibleJsonRowsOptions,
counter: LineCounter, counter: LineCounter,
ancestors: WeakSet<object> ancestors: WeakMap<object, string>
) { ) {
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); const kind = getContainerKind(value);
if (!kind) { if (!kind) {
rows.push(createNode(counter, context, "content", formatPrimitiveDisplay(value), value, { isLeaf: true })); rows.push(createNode(counter, context, "content", formatPrimitiveDisplay(value), value, { isLeaf: true }));
@ -296,12 +356,17 @@ function appendValueRows(
} }
const reference = value as object; const reference = value as object;
if (ancestors.has(reference)) { const existingPath = ancestors.get(reference);
rows.push(createNode(counter, context, "content", "[Circular]", value, { isLeaf: true })); if (existingPath) {
rows.push(
createNode(counter, context, "content", formatCircularReference(toDisplayPath(existingPath, options.rootPath)), value, {
isLeaf: true,
})
);
return; return;
} }
ancestors.add(reference); ancestors.set(reference, context.path);
appendContainerRows(kind, value as JsonContainerValue, context, rows, options, counter, ancestors); appendContainerRows(kind, value as JsonContainerValue, context, rows, options, counter, ancestors);
ancestors.delete(reference); ancestors.delete(reference);
} }
@ -313,11 +378,11 @@ function appendContainerRows(
rows: JsonViewNode[], rows: JsonViewNode[],
options: BuildVisibleJsonRowsOptions, options: BuildVisibleJsonRowsOptions,
counter: LineCounter, counter: LineCounter,
ancestors: WeakSet<object> ancestors: WeakMap<object, string>
) { ) {
const length = getContainerLength(value, kind); const length = getContainerLength(value, kind);
const expanded = isExpanded(context.path, context.level, length, options); const expanded = isExpanded(context.path, context.level, length, options);
const totalLineCount = 2 + countContainerChildLines(kind, value, ancestors); const totalLineCount = 2 + countContainerChildLines(kind, value, new WeakMap<object, true>());
if (!expanded) { if (!expanded) {
rows.push( rows.push(
@ -398,13 +463,14 @@ function appendContainerRows(
}); });
} else { } else {
Array.from(value as Map<unknown, unknown>).forEach(([keyValue, childValue], index) => { Array.from(value as Map<unknown, unknown>).forEach(([keyValue, childValue], index) => {
const normalizedKeyValue = normalizeVueValue(keyValue, toDisplayPath(context.path, options.rootPath));
appendValueRows( appendValueRows(
childValue, childValue,
{ {
path: buildMapPath(context.path, keyValue, index), path: buildMapPath(context.path, normalizedKeyValue, index),
level: context.level + 1, level: context.level + 1,
displayKey: formatKeyDisplay(keyValue), displayKey: formatKeyDisplay(normalizedKeyValue),
keyValue, keyValue: normalizedKeyValue,
showComma: index < length - 1, showComma: index < length - 1,
posInSet: index + 1, posInSet: index + 1,
setSize: length, setSize: length,
@ -446,7 +512,7 @@ export function buildVisibleJsonRows(value: unknown, options: BuildVisibleJsonRo
rows, rows,
options, options,
counter, counter,
new WeakSet<object>() new WeakMap<object, string>()
); );
return rows; return rows;
} }

88
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 { measureShrinkWrapWidth, measureTextHeight } from "../list-table-v2/measureText";
import { useVirtualRows } from "../list-table-v2/useVirtualRows"; import { useVirtualRows } from "../list-table-v2/useVirtualRows";
import { buildVisibleJsonRows } from "./flattenJson"; import { buildVisibleJsonRows } from "./flattenJson";
import {
formatCircularReference,
isCircularReferenceMarker,
normalizeVueValue,
} from "./normalizeValue";
import type { JsonViewMenuActionContext, JsonViewMenuItem, JsonViewNode, JsonViewProps } from "./types"; import type { JsonViewMenuActionContext, JsonViewMenuItem, JsonViewNode, JsonViewProps } from "./types";
const DEFAULT_FONT = "13px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"; 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; return null;
}; };
function toSerializableValue(value: unknown, ancestors = new WeakSet<object>()): 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<object, string>()
): 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) { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
return value; return value;
} }
@ -543,29 +593,36 @@ function toSerializableValue(value: unknown, ancestors = new WeakSet<object>()):
return String(value); return String(value);
} }
if (ancestors.has(value)) { const existingPath = ancestors.get(value);
return "[Circular]"; if (existingPath) {
return formatCircularReference(existingPath);
} }
ancestors.add(value); ancestors.set(value, currentPath);
if (Array.isArray(value)) { 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); ancestors.delete(value);
return serialized; return serialized;
} }
if (value instanceof Set) { 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); ancestors.delete(value);
return serialized; return serialized;
} }
if (value instanceof Map) { if (value instanceof Map) {
const serialized = { const serialized = {
$map: Array.from(value, ([key, entry]) => [ $map: Array.from(value, ([key, entry], index) => [
toSerializableValue(key, ancestors), toSerializableValue(key, buildDisplayMapPath(currentPath, key, index), ancestors),
toSerializableValue(entry, ancestors), toSerializableValue(entry, buildDisplayMapPath(currentPath, key, index), ancestors),
]), ]),
}; };
ancestors.delete(value); ancestors.delete(value);
@ -573,13 +630,16 @@ function toSerializableValue(value: unknown, ancestors = new WeakSet<object>()):
} }
const serialized = Object.fromEntries( 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); ancestors.delete(value);
return serialized; return serialized;
} }
function serializeNodeValue(value: unknown): string { function serializeNodeValue(value: unknown, currentPath = "$"): string {
if (typeof value === "string") { if (typeof value === "string") {
return value; return value;
} }
@ -587,7 +647,7 @@ function serializeNodeValue(value: unknown): string {
return "undefined"; return "undefined";
} }
try { try {
const json = JSON.stringify(toSerializableValue(value), null, 2); const json = JSON.stringify(toSerializableValue(value, currentPath), null, 2);
if (json !== undefined) { if (json !== undefined) {
return json; return json;
} }
@ -654,8 +714,8 @@ function getMenuItems(node: JsonViewNode): JsonViewMenuItem[] {
{ {
key: "copy-value", key: "copy-value",
label: "Copy Value", label: "Copy Value",
onSelect: async ({ value, copy }) => { onSelect: async ({ path, value, copy }) => {
await copy(serializeNodeValue(value)); await copy(serializeNodeValue(value, path));
}, },
}, },
{ {

52
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<object, string | undefined>()
): 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;
}

36
packages/base/data/list-table-v2/list-table-v2.vue

@ -2,25 +2,7 @@
<div class="list-table-v2"> <div class="list-table-v2">
<!-- Debug info --> <!-- Debug info -->
<div v-if="debug" class="debug-info"> <div v-if="debug" class="debug-info">
<div>Container Width: {{ containerWidth }}</div> <json-view :data="debugInfo" :deep="3" :virtual="true" :show-icon="true" :show-double-quotes="false" />
<div>Total Height: {{ virtualTotalHeight }}</div>
<div>Visible Rows: {{ visibleRows.map((entry) => entry.height).join(",") }}</div>
<div>
Cell Heights:
{{
cellHeights?.map((rowCellHeights) =>
rowCellHeights
.map((entry) => (entry ? `${entry.isCustomRenderer ? "*" : ""}${entry.height}` : "null"))
.join(",")
)
}}
</div>
<div>Total Rows: {{ pageData.length }}</div>
<div>Column widths: {{ columnWidths.map((w) => Math.round(w)).join(",") }}</div>
<div>
Column configs:
{{ computedConfigs }}
</div>
</div> </div>
<!-- Main table container with ResizeObserver for width tracking --> <!-- Main table container with ResizeObserver for width tracking -->
@ -94,6 +76,7 @@ import { resolveRowHeights } from "./usePretextRowHeights";
import { useVirtualRows } from "./useVirtualRows"; import { useVirtualRows } from "./useVirtualRows";
import { formatTimestampFromValue } from "../../../../plugs/composables"; import { formatTimestampFromValue } from "../../../../plugs/composables";
import { match } from "ts-pattern"; import { match } from "ts-pattern";
import { JsonView } from "noob-mengyxu";
const DEFAULT_FONT = "14px sans-serif"; const DEFAULT_FONT = "14px sans-serif";
const DEFAULT_TEXT_MAX_WIDTH = 400; const DEFAULT_TEXT_MAX_WIDTH = 400;
@ -405,6 +388,21 @@ const handleCurrentChange = (val: number) => {
emit("query"); 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 // ResizeObserver for container dimensions
// ============================================================================= // =============================================================================

Loading…
Cancel
Save