refactor: [Option A] inline node footer layout with core _footerHeight/_collapsedHeight#10711
refactor: [Option A] inline node footer layout with core _footerHeight/_collapsedHeight#10711
Conversation
- Replace absolute overlay footer with inline flow layout - Use z-index layering: body(z-5) > footer(z-0/z-2) to keep footer behind body while maintaining hover interactivity - Move resize handles inside body to avoid footer overlap - Use border-2 with negative inset for root border overlay to render outside body bounds, preventing slot dot occlusion (z-0) - Shape-aware radius classes for error/enter/footer buttons - Remove hasFooter computed and all footer offset classes
- Move border from overlay div to root element directly - Replace separate selection/execution overlay with outline on root - Remove overlay div, selectionShapeClass computed, and hasFooter - Inline all footer sizing constants - Add ring-inset border for Enter/Advanced buttons in error state - Hide root border in error state (transparent) since ring-4 covers it - Remove isCollapsed prop from NodeFooter (no longer needed)
- Split SE/SW handles: body handles hide when footer exists, footer-level handles render after NodeFooter - Use body element (node-inner-wrapper) for resize start size instead of root element to exclude footer height - Add z-10 to resize handles so they appear above footer buttons - Restore hasFooter computed for handle visibility control
- Measure body (node-inner-wrapper) for node.size to exclude footer height, preventing size accumulation on Vue/legacy mode switching - Store footer height separately (_footerHeight) for boundingRect - Store collapsed width/height from DOM in ResizeObserver instead of relying on canvas text measurement - Skip _collapsed_width text measurement in Vue nodes mode since measure() ctx overwrites ResizeObserver values - Restore selectionBorder.ts to use createBounds (no per-frame DOM)
- Rename loadWithPositions to repositionNodes, decompose into getSerializedGraph + applyNodePositions (pure) + loadGraph - Extract measureSelectionBounds to shared boundsUtils helper - Add JSDoc to setCollapsed - Move test helpers out of spec file into fixture utils
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ❌ 792 passed, 26 failed · 4 flaky❌ Failed Tests📊 Browser Reports
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds selection-bounds measurement helpers and Playwright tests; introduces graph fixture and node operations helpers; updates Vue node rendering, footer sizing, and resize-tracking to include footer/collapsed dimensions; adjusts LiteGraph node measurement to account for footer and cached collapsed height. Changes
Sequence Diagram(s)sequenceDiagram
participant TestRunner as Test Runner
participant App as Canvas App
participant DOM as Browser DOM
participant BoundsUtil as Bounds Utility
participant Assert as Assertions
TestRunner->>App: loadGraphData(fixture)
TestRunner->>App: repositionNodes / setCollapsed(nodeId, state)
TestRunner->>App: select all (Ctrl+A)
App-->>TestRunner: selected items confirm (2 nodes)
TestRunner->>BoundsUtil: measureSelectionBounds([targetId, refId])
activate BoundsUtil
BoundsUtil->>DOM: read canvas.selectedItems and canvas.ds (scale, offset)
DOM-->>BoundsUtil: selected items + transform
BoundsUtil->>DOM: query node elements by data-node-id, getBoundingClientRect(), footer extents
DOM-->>BoundsUtil: node rects and footer measurements
BoundsUtil->>BoundsUtil: convert DOM coords -> canvas/world coords using scale & offset
BoundsUtil-->>TestRunner: return { selectionBounds, nodeVisualBounds }
deactivate BoundsUtil
TestRunner->>Assert: verify selectionBounds non-null and encloses target node bounds
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 Bundle: 5.1 MB gzip 🔴 +219 BDetailsSummary
Category Glance App Entry Points — 22.3 kB (baseline 22.3 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.17 MB (baseline 1.17 MB) • 🔴 +1.87 kBGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 76.6 kB (baseline 76.6 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 484 kB (baseline 484 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 12 unchanged User & Accounts — 17.1 kB (baseline 17.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 109 kB (baseline 109 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 2 added / 2 removed UI Components — 60.3 kB (baseline 60.3 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 2.96 MB (baseline 2.96 MB) • 🔴 +350 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 338 kB (baseline 338 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 13 unchanged Vendor & Third-Party — 9.8 MB (baseline 9.8 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.44 MB (baseline 8.44 MB) • ⚪ 0 BBundles that do not match a named category
Status: 55 added / 55 removed / 79 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-03-30T08:56:54.664Z",
"gitSha": "9c8c08b5354eae3602b6c2f507414e36c3f8327b",
"branch": "refactor/node-footer-inline-layout",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2024.6319999999969,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.323000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 465.24899999999997,
"heapDeltaBytes": 20337880,
"heapUsedBytes": 63063240,
"domNodes": 22,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 31.797,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2048.004999999989,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.145000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 419.74999999999994,
"heapDeltaBytes": 20891404,
"heapUsedBytes": 64809700,
"domNodes": 22,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 24.380000000000006,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2037.3749999999973,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.073,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 420.12899999999996,
"heapDeltaBytes": 20376024,
"heapUsedBytes": 63109408,
"domNodes": 22,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 30.523000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1960.5140000000176,
"styleRecalcs": 79,
"styleRecalcDurationMs": 54.187000000000005,
"layouts": 12,
"layoutDurationMs": 4.689,
"taskDurationMs": 930.087,
"heapDeltaBytes": 16653852,
"heapUsedBytes": 60778504,
"domNodes": 64,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 151.76700000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1862.0270000000119,
"styleRecalcs": 77,
"styleRecalcDurationMs": 42.013000000000005,
"layouts": 12,
"layoutDurationMs": 4.132000000000001,
"taskDurationMs": 832.615,
"heapDeltaBytes": 6934000,
"heapUsedBytes": 59624552,
"domNodes": 61,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 130.154,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1797.099000000003,
"styleRecalcs": 75,
"styleRecalcDurationMs": 37.331,
"layouts": 12,
"layoutDurationMs": 3.488,
"taskDurationMs": 819.863,
"heapDeltaBytes": 15786128,
"heapUsedBytes": 59643376,
"domNodes": 58,
"jsHeapTotalBytes": 23592960,
"scriptDurationMs": 126.94,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1732.3080000000175,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.617999999999995,
"layouts": 6,
"layoutDurationMs": 0.7299999999999998,
"taskDurationMs": 352.785,
"heapDeltaBytes": 24703464,
"heapUsedBytes": 67395824,
"domNodes": 77,
"jsHeapTotalBytes": 20447232,
"scriptDurationMs": 32.073,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1743.9489999999864,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.528999999999996,
"layouts": 6,
"layoutDurationMs": 0.7559999999999999,
"taskDurationMs": 353.086,
"heapDeltaBytes": 24660884,
"heapUsedBytes": 67423304,
"domNodes": 79,
"jsHeapTotalBytes": 20709376,
"scriptDurationMs": 32.571,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1746.1849999999686,
"styleRecalcs": 31,
"styleRecalcDurationMs": 19.206,
"layouts": 6,
"layoutDurationMs": 0.7160000000000001,
"taskDurationMs": 352.73299999999995,
"heapDeltaBytes": 24672376,
"heapUsedBytes": 67386104,
"domNodes": 78,
"jsHeapTotalBytes": 20971520,
"scriptDurationMs": 29.184,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 571.1009999999987,
"styleRecalcs": 13,
"styleRecalcDurationMs": 10.199,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 358.00399999999996,
"heapDeltaBytes": 6047748,
"heapUsedBytes": 48985256,
"domNodes": 22,
"jsHeapTotalBytes": 13631488,
"scriptDurationMs": 61.155,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 581.3499999999863,
"styleRecalcs": 13,
"styleRecalcDurationMs": 9.454999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 368.366,
"heapDeltaBytes": 6701988,
"heapUsedBytes": 50498236,
"domNodes": 21,
"jsHeapTotalBytes": 13631488,
"scriptDurationMs": 63.120999999999995,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 559.6700000000965,
"styleRecalcs": 13,
"styleRecalcDurationMs": 9.644000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 361.439,
"heapDeltaBytes": 6837116,
"heapUsedBytes": 49375824,
"domNodes": 22,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 63.154,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2036.151999999987,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.881999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 636.8929999999999,
"heapDeltaBytes": 2548860,
"heapUsedBytes": 53392636,
"domNodes": -256,
"jsHeapTotalBytes": 14876672,
"scriptDurationMs": 101.724,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2029.1189999999801,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.8790000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 582.7869999999999,
"heapDeltaBytes": 2643380,
"heapUsedBytes": 54876468,
"domNodes": -261,
"jsHeapTotalBytes": 15925248,
"scriptDurationMs": 90.72800000000001,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2038.0930000000035,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.662999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 624.952,
"heapDeltaBytes": 4858644,
"heapUsedBytes": 55869880,
"domNodes": -258,
"jsHeapTotalBytes": 16449536,
"scriptDurationMs": 102.113,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2180.2749999999946,
"styleRecalcs": 69,
"styleRecalcDurationMs": 17.038,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1209.875,
"heapDeltaBytes": 18242656,
"heapUsedBytes": 71167816,
"domNodes": -260,
"jsHeapTotalBytes": 17702912,
"scriptDurationMs": 406.826,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2125.684999999976,
"styleRecalcs": 66,
"styleRecalcDurationMs": 13.724,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1165.4940000000001,
"heapDeltaBytes": 15254568,
"heapUsedBytes": 68741360,
"domNodes": -270,
"jsHeapTotalBytes": 18227200,
"scriptDurationMs": 386.247,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2140.423999999939,
"styleRecalcs": 68,
"styleRecalcDurationMs": 15.537000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1122.889,
"heapDeltaBytes": 15174096,
"heapUsedBytes": 68571068,
"domNodes": -262,
"jsHeapTotalBytes": 18489344,
"scriptDurationMs": 380.85,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3265.7299999999905,
"styleRecalcs": 66,
"styleRecalcDurationMs": 19.616000000000003,
"layouts": 60,
"layoutDurationMs": 8.418,
"taskDurationMs": 1439.873,
"heapDeltaBytes": 8501740,
"heapUsedBytes": 64286516,
"domNodes": -261,
"jsHeapTotalBytes": 17555456,
"scriptDurationMs": 500.757,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3201.233000000002,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.047,
"layouts": 60,
"layoutDurationMs": 8.103000000000002,
"taskDurationMs": 1387.0090000000002,
"heapDeltaBytes": 4113064,
"heapUsedBytes": 58830812,
"domNodes": -265,
"jsHeapTotalBytes": 17235968,
"scriptDurationMs": 474.31499999999994,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3211.3460000000487,
"styleRecalcs": 66,
"styleRecalcDurationMs": 18.017,
"layouts": 60,
"layoutDurationMs": 8.213,
"taskDurationMs": 1390.8039999999999,
"heapDeltaBytes": 7456020,
"heapUsedBytes": 61780528,
"domNodes": -263,
"jsHeapTotalBytes": 18022400,
"scriptDurationMs": 488.633,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2051.296999999977,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.764000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 632.6700000000001,
"heapDeltaBytes": 4689080,
"heapUsedBytes": 57516944,
"domNodes": -259,
"jsHeapTotalBytes": 16187392,
"scriptDurationMs": 102.40900000000002,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2040.0070000000028,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.423999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 633.345,
"heapDeltaBytes": 4636836,
"heapUsedBytes": 56815968,
"domNodes": -258,
"jsHeapTotalBytes": 15663104,
"scriptDurationMs": 101.096,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2036.514000000011,
"styleRecalcs": 11,
"styleRecalcDurationMs": 12.275,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 606.8690000000001,
"heapDeltaBytes": 3030380,
"heapUsedBytes": 55252808,
"domNodes": -256,
"jsHeapTotalBytes": 15663104,
"scriptDurationMs": 102.45199999999998,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 572.8289999999845,
"styleRecalcs": 49,
"styleRecalcDurationMs": 13.655000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 383.783,
"heapDeltaBytes": 6448664,
"heapUsedBytes": 49209304,
"domNodes": 23,
"jsHeapTotalBytes": 12845056,
"scriptDurationMs": 125.29499999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 573.8289999999893,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.65,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 393.043,
"heapDeltaBytes": 6512008,
"heapUsedBytes": 49540724,
"domNodes": 20,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 128.47,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 601.5129999999544,
"styleRecalcs": 49,
"styleRecalcDurationMs": 13.331000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 386.774,
"heapDeltaBytes": 6217780,
"heapUsedBytes": 49202056,
"domNodes": 24,
"jsHeapTotalBytes": 14155776,
"scriptDurationMs": 125.336,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2050.0749999999925,
"styleRecalcs": 10,
"styleRecalcDurationMs": 13.403999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 422.86400000000003,
"heapDeltaBytes": 20169868,
"heapUsedBytes": 63233192,
"domNodes": 20,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 27.373999999999995,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2020.7139999999981,
"styleRecalcs": 12,
"styleRecalcDurationMs": 11.584000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 411.974,
"heapDeltaBytes": 20008000,
"heapUsedBytes": 62980012,
"domNodes": 23,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 25.123,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2031.3190000000532,
"styleRecalcs": 12,
"styleRecalcDurationMs": 13.519000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 406.58099999999996,
"heapDeltaBytes": 19981580,
"heapUsedBytes": 62650724,
"domNodes": 23,
"jsHeapTotalBytes": 22282240,
"scriptDurationMs": 25.169999999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1998.061999999976,
"styleRecalcs": 85,
"styleRecalcDurationMs": 49.020999999999994,
"layouts": 16,
"layoutDurationMs": 4.869,
"taskDurationMs": 973.7040000000002,
"heapDeltaBytes": 11846708,
"heapUsedBytes": 54536796,
"domNodes": 73,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 106.9,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1696.7089999999985,
"styleRecalcs": 76,
"styleRecalcDurationMs": 40.498,
"layouts": 16,
"layoutDurationMs": 5.157,
"taskDurationMs": 740.518,
"heapDeltaBytes": 12264240,
"heapUsedBytes": 56483920,
"domNodes": 63,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 98.482,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1994.9430000000348,
"styleRecalcs": 85,
"styleRecalcDurationMs": 50.263000000000005,
"layouts": 16,
"layoutDurationMs": 4.722,
"taskDurationMs": 973.623,
"heapDeltaBytes": 11917520,
"heapUsedBytes": 54907992,
"domNodes": 73,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 98.36200000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8174.977999999982,
"styleRecalcs": 251,
"styleRecalcDurationMs": 47.023999999999994,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4345.768999999999,
"heapDeltaBytes": 32059232,
"heapUsedBytes": 82619684,
"domNodes": -256,
"jsHeapTotalBytes": 25829376,
"scriptDurationMs": 1459.339,
"eventListeners": -105,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8182.164999999997,
"styleRecalcs": 250,
"styleRecalcDurationMs": 46.002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4082.739,
"heapDeltaBytes": 17263732,
"heapUsedBytes": 68556056,
"domNodes": -256,
"jsHeapTotalBytes": 20062208,
"scriptDurationMs": 1290.166,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8200.67599999993,
"styleRecalcs": 251,
"styleRecalcDurationMs": 46.517,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4078.15,
"heapDeltaBytes": 27025504,
"heapUsedBytes": 77484108,
"domNodes": -257,
"jsHeapTotalBytes": 20848640,
"scriptDurationMs": 1329.478,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 10831.99400000001,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10809.262999999999,
"heapDeltaBytes": -25672212,
"heapUsedBytes": 184909300,
"domNodes": -8331,
"jsHeapTotalBytes": 28139520,
"scriptDurationMs": 580.533,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 10807.684999999992,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10787.543,
"heapDeltaBytes": -37306104,
"heapUsedBytes": 162245740,
"domNodes": -8331,
"jsHeapTotalBytes": 24207360,
"scriptDurationMs": 585.1650000000001,
"eventListeners": -16470,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 10591.646000000082,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10569.860999999999,
"heapDeltaBytes": -53100396,
"heapUsedBytes": 150045724,
"domNodes": -8332,
"jsHeapTotalBytes": 18440192,
"scriptDurationMs": 559.542,
"eventListeners": -16468,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 12756.272000000024,
"styleRecalcs": 65,
"styleRecalcDurationMs": 15.241000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12736.87,
"heapDeltaBytes": -53615088,
"heapUsedBytes": 161655144,
"domNodes": -8331,
"jsHeapTotalBytes": 23420928,
"scriptDurationMs": 893.665,
"eventListeners": -16462,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 12810.810000000003,
"styleRecalcs": 64,
"styleRecalcDurationMs": 15.159000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12789.967999999999,
"heapDeltaBytes": -24979648,
"heapUsedBytes": 172976948,
"domNodes": -8332,
"jsHeapTotalBytes": 24645632,
"scriptDurationMs": 846.063,
"eventListeners": -16468,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 12737.915999999928,
"styleRecalcs": 66,
"styleRecalcDurationMs": 14.903000000000027,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12719.152999999998,
"heapDeltaBytes": -38798048,
"heapUsedBytes": 159151148,
"domNodes": -8333,
"jsHeapTotalBytes": -3842048,
"scriptDurationMs": 833.121,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.216666666666665,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 460.2199999999925,
"styleRecalcs": 20,
"styleRecalcDurationMs": 29.438,
"layouts": 5,
"layoutDurationMs": 1.981,
"taskDurationMs": 147.297,
"heapDeltaBytes": 4759432,
"heapUsedBytes": 48729004,
"domNodes": 169,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 30.9,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "workflow-execution",
"durationMs": 449.18300000006184,
"styleRecalcs": 15,
"styleRecalcDurationMs": 22.8,
"layouts": 5,
"layoutDurationMs": 1.636,
"taskDurationMs": 129.64600000000002,
"heapDeltaBytes": 4528548,
"heapUsedBytes": 48468664,
"domNodes": 154,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 27.721999999999998,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 449.79000000000724,
"styleRecalcs": 18,
"styleRecalcDurationMs": 24.275,
"layouts": 5,
"layoutDurationMs": 1.4220000000000002,
"taskDurationMs": 131.818,
"heapDeltaBytes": 4515140,
"heapUsedBytes": 48065100,
"domNodes": 158,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 30.886999999999997,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
browser_tests/fixtures/utils/boundsUtils.ts (1)
18-21: Consider moving tofixtures/helpers/since this has a Page dependency.Per the coding guidelines,
fixtures/utils/is for "pure utility functions with no Page dependency", whilefixtures/helpers/is for "focused helper classes for domain-specific actions". This file accepts aPageparameter, making it a Page-dependent helper rather than a pure utility.This is a minor organizational concern that could be addressed in a follow-up. As per coding guidelines: "Place pure utility functions with no Page dependency in
fixtures/utils/for stateless helpers usable anywhere".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@browser_tests/fixtures/utils/boundsUtils.ts` around lines 18 - 21, The function measureSelectionBounds accepts a Playwright Page and therefore belongs in the Page-dependent helpers folder; move the file from fixtures/utils/ to fixtures/helpers/, update any import paths referencing measureSelectionBounds, and ensure the module export remains the same (measureSelectionBounds) so callers (tests/fixtures) continue to work; also run/adjust any tests or lints to confirm there are no remaining imports pointing to the old fixtures/utils path.src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts (1)
161-180: Vue-specific fields on LGraphNode noted as Option A limitation.This adds
_footerHeighttoLGraphNodeinstances, which is a Vue-rendering-specific field. Per ADR guidance, this grows the LGraphNode "God-object" with Vue-specific concerns.This is explicitly acknowledged in the PR objectives as a limitation of Option A: "core changes add Vue-specific fields (_footerHeight, _collapsed_height) to LGraphNode". The alternative Option B is noted as pending.
The implementation itself is correct - measuring the body element for
node.sizewhile tracking footer contribution separately ensures accurate bounding rect calculation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@browser_tests/fixtures/utils/litegraphUtils.ts`:
- Line 335: Update the docstring for the deterministic setter to correctly
describe that LGraphNode.collapse() is a toggle: state is toggled by
node.collapse(), but this setter enforces deterministic behavior by guarding the
call (so it only calls node.collapse() when the desired state differs from the
current state). Mention LGraphNode.collapse() and the deterministic guard in the
comment for clarity.
In `@src/lib/litegraph/src/LGraphNode.ts`:
- Around line 425-434: The patch adds renderer-specific fields to the core
LGraphNode (_collapsed_height and _footerHeight), coupling graph state to Vue
DOM measurement; instead, remove these properties from LGraphNode and move the
measurement state into a Vue-side store/composable keyed by node id (or maintain
a Map<string, {collapsedHeight:number, footerHeight:number}>), update any code
that reads/writes _collapsed_height/_footerHeight to instead call the new
composable/store API, and ensure any functions that relied on those fields
(e.g., measure() callers) retrieve measurement values through the store keyed by
node.id rather than from LGraphNode.
- Line 2103: measure() currently uses Vue-only cached fields _footerHeight and
_collapsed_height unconditionally; update measure() so it only consumes those
cached values when the node is actually in Vue rendering mode. In practice:
inside LGraphNode.measure(), replace direct uses of this._footerHeight and
this._collapsed_height with conditional expressions that use the cached value
only when a Vue-mode flag (e.g. this._isVueMode / this._vueMode / whatever
existing boolean indicates Vue rendering in this class) is true, otherwise
default to 0 (or the legacy calculation). Apply the same guard at both sites
that reference _footerHeight and _collapsed_height so legacy rendering cannot
pick up stale Vue-populated heights.
---
Nitpick comments:
In `@browser_tests/fixtures/utils/boundsUtils.ts`:
- Around line 18-21: The function measureSelectionBounds accepts a Playwright
Page and therefore belongs in the Page-dependent helpers folder; move the file
from fixtures/utils/ to fixtures/helpers/, update any import paths referencing
measureSelectionBounds, and ensure the module export remains the same
(measureSelectionBounds) so callers (tests/fixtures) continue to work; also
run/adjust any tests or lints to confirm there are no remaining imports pointing
to the old fixtures/utils path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 96707eed-e152-4374-8055-d3d2c9d130d5
📒 Files selected for processing (10)
browser_tests/assets/selection/subgraph-with-regular-node.jsonbrowser_tests/fixtures/helpers/NodeOperationsHelper.tsbrowser_tests/fixtures/utils/boundsUtils.tsbrowser_tests/fixtures/utils/litegraphUtils.tsbrowser_tests/tests/selectionBoundingBox.spec.tssrc/lib/litegraph/src/LGraphNode.tssrc/renderer/extensions/vueNodes/components/LGraphNode.vuesrc/renderer/extensions/vueNodes/components/NodeFooter.vuesrc/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.tssrc/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts
| async isCollapsed() { | ||
| return !!(await this.getFlags()).collapsed | ||
| } | ||
| /** Deterministic setter using node.collapse() API (not a toggle). */ |
There was a problem hiding this comment.
Correct the collapse() semantics in the docstring.
Line 335 says collapse() is “not a toggle”, but LGraphNode.collapse() toggles state. This setter is deterministic because of the guard on Line 341.
✏️ Proposed wording fix
- /** Deterministic setter using node.collapse() API (not a toggle). */
+ /** Deterministic collapsed-state setter using guard + node.collapse() toggle API. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@browser_tests/fixtures/utils/litegraphUtils.ts` at line 335, Update the
docstring for the deterministic setter to correctly describe that
LGraphNode.collapse() is a toggle: state is toggled by node.collapse(), but this
setter enforces deterministic behavior by guarding the call (so it only calls
node.collapse() when the desired state differs from the current state). Mention
LGraphNode.collapse() and the deterministic guard in the comment for clarity.
| /** | ||
| * The height of the node when collapsed (including footer). | ||
| * Set by ResizeObserver in Vue nodes mode. | ||
| */ | ||
| _collapsed_height?: number | ||
| /** | ||
| * The footer height in Vue nodes mode. | ||
| * Set by ResizeObserver, used by measure() to extend boundingRect. | ||
| */ | ||
| _footerHeight?: number |
There was a problem hiding this comment.
Avoid expanding LGraphNode with renderer-specific fields.
Adding _collapsed_height and _footerHeight to core LGraphNode couples LiteGraph entity state to Vue DOM measurement concerns. Prefer storing this UI measurement state in Vue-side store/composable structures keyed by node id.
Based on learnings: Applies to {src/lib/litegraph/**,src/ecs/**}/*.{ts,tsx,js} : ADR compliance for entity/litegraph changes: Flag new methods/properties added to LGraphNode, LGraphCanvas, LGraph, or Subgraph that add responsibilities rather than extracting/migrating existing ones (God-object growth).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/litegraph/src/LGraphNode.ts` around lines 425 - 434, The patch adds
renderer-specific fields to the core LGraphNode (_collapsed_height and
_footerHeight), coupling graph state to Vue DOM measurement; instead, remove
these properties from LGraphNode and move the measurement state into a Vue-side
store/composable keyed by node id (or maintain a Map<string,
{collapsedHeight:number, footerHeight:number}>), update any code that
reads/writes _collapsed_height/_footerHeight to instead call the new
composable/store API, and ensure any functions that relied on those fields
(e.g., measure() callers) retrieve measurement values through the store keyed by
node.id rather than from LGraphNode.
| if (!this.flags?.collapsed) { | ||
| out[2] = this.size[0] | ||
| out[3] = this.size[1] + titleHeight | ||
| out[3] = this.size[1] + titleHeight + (this._footerHeight ?? 0) |
There was a problem hiding this comment.
Gate Vue-only cached heights to Vue mode to prevent stale bounds in legacy rendering.
Line 2103 and Line 2114 consume _footerHeight / _collapsed_height unconditionally. If values were populated in Vue mode and then rendering switches to legacy mode, node bounds/hitboxes can remain inflated.
🩹 Proposed guard in measure()
- out[3] = this.size[1] + titleHeight + (this._footerHeight ?? 0)
+ const footerHeight = LiteGraph.vueNodesMode
+ ? (this._footerHeight ?? 0)
+ : 0
+ out[3] = this.size[1] + titleHeight + footerHeight
...
- out[3] = this._collapsed_height || LiteGraph.NODE_TITLE_HEIGHT
+ out[3] = LiteGraph.vueNodesMode
+ ? (this._collapsed_height ?? LiteGraph.NODE_TITLE_HEIGHT)
+ : LiteGraph.NODE_TITLE_HEIGHTAlso applies to: 2114-2114
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/litegraph/src/LGraphNode.ts` at line 2103, measure() currently uses
Vue-only cached fields _footerHeight and _collapsed_height unconditionally;
update measure() so it only consumes those cached values when the node is
actually in Vue rendering mode. In practice: inside LGraphNode.measure(),
replace direct uses of this._footerHeight and this._collapsed_height with
conditional expressions that use the cached value only when a Vue-mode flag
(e.g. this._isVueMode / this._vueMode / whatever existing boolean indicates Vue
rendering in this class) is true, otherwise default to 0 (or the legacy
calculation). Apply the same guard at both sites that reference _footerHeight
and _collapsed_height so legacy rendering cannot pick up stale Vue-populated
heights.
- Remove overlay div assertions (node-state-outline-overlay removed) - Add createSharedComposable to @vueuse/core mock - Add offsetWidth/offsetHeight to ResizeObserver test elements
|
Updating Playwright Expectations |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/extensions/vueNodes/components/LGraphNode.vue`:
- Around line 13-16: The class list currently applies outlineClass after
rootBorderShapeClass which causes outlineClass's rounded-* to override the
larger error radius; update outlineClass so it does not hardcode rounded-* but
instead derives its radius from the same source as rootBorderShapeClass (i.e.,
reuse or compute the same value used to build rootBorderShapeClass) so the outer
error radius is defined in one place; locate where outlineClass and
rootBorderShapeClass are composed in LGraphNode.vue and remove/replace any
hardcoded rounded-* in outlineClass with the shared radius value or a reference
to rootBorderShapeClass.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b580d4be-c763-4d5b-9bc2-2ad4f2a660ff
📒 Files selected for processing (1)
src/renderer/extensions/vueNodes/components/LGraphNode.vue
| 'group/node lg-node absolute isolate box-border flex flex-col border border-solid text-sm contain-layout contain-style', | ||
| hasAnyError ? 'border-transparent' : 'border-component-node-border', | ||
| rootBorderShapeClass, | ||
| outlineClass, |
There was a problem hiding this comment.
Keep the outer error radius defined in one place.
Because Line 16 applies outlineClass after rootBorderShapeClass, the rounded-* classes in Lines 598-604 override the larger error-state radius from Lines 658-663. That means selected/executing error nodes drop back to the smaller 16px corners in exactly the state where the 21px outer radius is supposed to preserve the ring/outline alignment.
🛠️ Minimal fix
const outlineClass = computed(() => {
const color = isSelected.value
? 'outline-node-component-outline'
: executing.value
? 'outline-node-stroke-executing'
: null
if (!color) return ''
if (!hasAnyError.value) return cn('outline-3', color)
- const errorRadius =
- nodeData.shape === RenderShape.BOX
- ? ''
- : nodeData.shape === RenderShape.CARD
- ? 'rounded-tl-[16px] rounded-br-[16px]'
- : 'rounded-[16px]'
- return cn('outline-4 outline-offset-[3px]', errorRadius, color)
+ return cn('outline-4 outline-offset-[3px]', color)
})If you still need an explicit outline radius here, derive it from the same source as rootBorderShapeClass instead of hardcoding a second set of values.
Based on learnings, the cn utility with tailwind-merge keeps the last conflicting Tailwind class, so later rounded-* entries override earlier ones.
Also applies to: 588-605, 652-663
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/extensions/vueNodes/components/LGraphNode.vue` around lines 13 -
16, The class list currently applies outlineClass after rootBorderShapeClass
which causes outlineClass's rounded-* to override the larger error radius;
update outlineClass so it does not hardcode rounded-* but instead derives its
radius from the same source as rootBorderShapeClass (i.e., reuse or compute the
same value used to build rootBorderShapeClass) so the outer error radius is
defined in one place; locate where outlineClass and rootBorderShapeClass are
composed in LGraphNode.vue and remove/replace any hardcoded rounded-* in
outlineClass with the shared radius value or a reference to
rootBorderShapeClass.
|
Due to too many design changes, I will resubmit the PR with a new approach. |
Summary
Fix selection bounding box not encompassing node footer, and refactor footer from absolute overlay to inline flow layout. Option A uses
_footerHeightand_collapsedHeightproperties onLGraphNodewithvueNodesModeguard inmeasure().See also: Option B — #10712 (uses
onBoundingcallback withvueBoundsOverridesMap instead of core changes)Background
The node footer (Enter Subgraph, Advanced, Error buttons) was previously rendered as an absolute overlay (
absolute top-full) outside the node body. This caused the selection bounding box, resize handles, and border/outline overlays to not account for the footer height, requiring multiple hardcoded pixel offsets (32px, 34px) to compensate.Changes
Footer inline layout
z-5> footerz-0/z-2)-mt-5) to slide behind the body's rounded bottom edge-ml-5overlapBorder/outline unification
border border-solid border-component-node-border)outlineon root (no layout shift)border-transparent) sincering-4covers itResize handles
node-inner-wrapper) for resize start size to exclude footer heightz-10to resize handles so they appear above footer buttonsAccurate bounding rect (Option A approach)
node-inner-wrapper) viaoffsetHeightfornode.sizeto exclude footer, preventing size accumulation on Vue/legacy mode switching_footerHeight) onLGraphNodeformeasure()to extendboundingRect_collapsed_widthtext measurement in Vue nodes mode sincemeasure()ctx overwrites ResizeObserver valuesselectionBorder.tsunchanged — usescreateBoundsas before, no per-frame DOM queriesLimitations of Option A
_footerHeightand_collapsed_heightproperties toLGraphNode.ts(core change, Vue-specific fields)measure()hasvueNodesModebranch (core knows about rendering mode)Tests
repositionNodes(decomposed intogetSerializedGraph+applyNodePositions+loadGraph),NodeReference.setCollapsed,measureSelectionBoundsReview Focus
_footerHeight/_collapsed_heightapproach vsonBoundingcallback (see Option B PR)offsetHeightusage instead ofborderBoxSizein ResizeObserver (needed becausecontain-layoutexcludes footer fromgetBoundingClientRect)selectionShapeClasscomputed┆Issue is synchronized with this Notion page by Unito