@treelocator/runtime 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,30 @@
1
1
 
2
- > @treelocator/runtime@0.1.5 build /Users/wende/projects/locatorjs/packages/runtime
2
+ > @treelocator/runtime@0.1.8 build /Users/wende/projects/locatorjs/packages/runtime
3
3
  > concurrently pnpm:build:*
4
4
 
5
- [ts]
6
- [ts] > @treelocator/runtime@0.1.5 build:ts /Users/wende/projects/locatorjs/packages/runtime
7
- [ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
8
- [ts]
9
5
  [tailwind]
10
- [tailwind] > @treelocator/runtime@0.1.5 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
6
+ [tailwind] > @treelocator/runtime@0.1.8 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
11
7
  [tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
12
8
  [tailwind]
13
- [wrapImage]
14
- [wrapImage] > @treelocator/runtime@0.1.5 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
15
- [wrapImage] > node ./scripts/wrapImage.js
16
- [wrapImage]
17
9
  [babel]
18
- [babel] > @treelocator/runtime@0.1.5 build:babel /Users/wende/projects/locatorjs/packages/runtime
10
+ [babel] > @treelocator/runtime@0.1.8 build:babel /Users/wende/projects/locatorjs/packages/runtime
19
11
  [babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
20
12
  [babel]
13
+ [ts]
14
+ [ts] > @treelocator/runtime@0.1.8 build:ts /Users/wende/projects/locatorjs/packages/runtime
15
+ [ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
16
+ [ts]
17
+ [wrapImage]
18
+ [wrapImage] > @treelocator/runtime@0.1.8 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
19
+ [wrapImage] > node ./scripts/wrapImage.js
20
+ [wrapImage]
21
21
  [wrapImage] Tree icon file generated
22
22
  [wrapImage] pnpm run build:wrapImage exited with code 0
23
23
  [tailwind]
24
24
  [tailwind] Rebuilding...
25
25
  [tailwind]
26
- [tailwind] Done in 294ms.
26
+ [tailwind] Done in 164ms.
27
27
  [tailwind] pnpm run build:tailwind exited with code 0
28
- [babel] Successfully compiled 72 files with Babel (2364ms).
28
+ [babel] Successfully compiled 72 files with Babel (409ms).
29
29
  [babel] pnpm run build:babel exited with code 0
30
30
  [ts] pnpm run build:ts exited with code 0
@@ -1,18 +1,19 @@
1
1
 
2
- > @treelocator/runtime@0.1.2 test /Users/wende/projects/locatorjs/packages/runtime
2
+ > @treelocator/runtime@0.1.5 test /Users/wende/projects/locatorjs/packages/runtime
3
3
  > vitest run
4
4
 
5
5
 
6
6
  RUN v2.1.9 /Users/wende/projects/locatorjs/packages/runtime
7
7
 
8
- ✓ src/functions/cropPath.test.ts (2 tests) 1ms
9
- ✓ src/functions/evalTemplate.test.ts (1 test) 1ms
10
- ✓ src/functions/transformPath.test.ts (3 tests) 2ms
11
- ✓ src/functions/mergeRects.test.ts (1 test) 1ms
12
- ✓ src/functions/getUsableFileName.test.tsx (4 tests) 1ms
8
+ ✓ src/functions/mergeRects.test.ts (1 test) 2ms
9
+ ✓ src/functions/evalTemplate.test.ts (1 test) 2ms
10
+ ✓ src/functions/transformPath.test.ts (3 tests) 3ms
11
+ ✓ src/functions/getUsableFileName.test.tsx (4 tests) 3ms
12
+ ✓ src/functions/formatAncestryChain.test.ts (7 tests) 2ms
13
+ ✓ src/functions/cropPath.test.ts (2 tests) 2ms
13
14
 
14
- Test Files 5 passed (5)
15
- Tests 11 passed (11)
16
- Start at 18:29:05
17
- Duration 687ms (transform 240ms, setup 0ms, collect 278ms, tests 7ms, environment 1ms, prepare 366ms)
15
+ Test Files 6 passed (6)
16
+ Tests 18 passed (18)
17
+ Start at 17:17:22
18
+ Duration 718ms (transform 282ms, setup 0ms, collect 336ms, tests 14ms, environment 1ms, prepare 463ms)
18
19
 
@@ -13,4 +13,5 @@ export declare class HtmlElementTreeNode implements TreeNode {
13
13
  getParent(): TreeNode | null;
14
14
  getSource(): Source | null;
15
15
  getComponent(): TreeNodeComponent | null;
16
+ getOwnerComponents(): TreeNodeComponent[];
16
17
  }
@@ -18,7 +18,7 @@ export class HtmlElementTreeNode {
18
18
  getChildren() {
19
19
  const children = Array.from(this.element.children);
20
20
  return children.map(child => {
21
- if (child instanceof HTMLElement) {
21
+ if (child instanceof HTMLElement || child instanceof SVGElement) {
22
22
  // @ts-ignore
23
23
  return new this.constructor(child);
24
24
  } else {
@@ -40,4 +40,7 @@ export class HtmlElementTreeNode {
40
40
  getComponent() {
41
41
  throw new Error("Method not implemented.");
42
42
  }
43
+ getOwnerComponents() {
44
+ return [];
45
+ }
43
46
  }
@@ -8,7 +8,7 @@ export type ElementInfo = {
8
8
  };
9
9
  export type FullElementInfo = {
10
10
  thisElement: ElementInfo;
11
- htmlElement: HTMLElement;
11
+ htmlElement: HTMLElement | SVGElement;
12
12
  parentElements: ElementInfo[];
13
13
  componentBox: SimpleDOMRect;
14
14
  componentsLabels: LabelData[];
@@ -1,2 +1,2 @@
1
1
  import type { ExpressionInfo, FileStorage } from "@locator/shared";
2
- export declare function getExpressionData(target: HTMLElement, fileData: FileStorage | null): ExpressionInfo | null;
2
+ export declare function getExpressionData(target: Element, fileData: FileStorage | null): ExpressionInfo | null;
@@ -1,8 +1,12 @@
1
1
  import { parseDataId, parseDataPath } from "../../functions/parseDataId";
2
2
  export function getExpressionData(target, fileData) {
3
+ // Use getAttribute instead of dataset to support both HTML and SVG elements
4
+ const dataLocatorjs = target.getAttribute("data-locatorjs");
5
+ const dataLocatorjsId = target.getAttribute("data-locatorjs-id");
6
+
3
7
  // First check for data-locatorjs (path-based, for server components)
4
- if (target.dataset.locatorjs) {
5
- const parsed = parseDataPath(target.dataset.locatorjs);
8
+ if (dataLocatorjs) {
9
+ const parsed = parseDataPath(dataLocatorjs);
6
10
  if (parsed) {
7
11
  const [, line, column] = parsed;
8
12
 
@@ -33,8 +37,8 @@ export function getExpressionData(target, fileData) {
33
37
  }
34
38
 
35
39
  // Fall back to data-locatorjs-id (ID-based, traditional approach)
36
- if (target.dataset.locatorjsId && fileData) {
37
- const [, id] = parseDataId(target.dataset.locatorjsId);
40
+ if (dataLocatorjsId && fileData) {
41
+ const [, id] = parseDataId(dataLocatorjsId);
38
42
  const expData = fileData.expressions[Number(id)];
39
43
  if (expData) {
40
44
  return expData;
@@ -1,5 +1,5 @@
1
1
  import type { FileStorage } from "@locator/shared";
2
2
  import type { SimpleDOMRect } from "../../types/types";
3
- export declare function getJSXComponentBoundingBox(found: HTMLElement, locatorData: {
3
+ export declare function getJSXComponentBoundingBox(found: Element, locatorData: {
4
4
  [filename: string]: FileStorage;
5
5
  }, componentFolder: string, componentId: number): SimpleDOMRect;
@@ -9,19 +9,24 @@ export function getJSXComponentBoundingBox(found, locatorData, componentFolder,
9
9
  if (!parent) {
10
10
  return;
11
11
  }
12
- if (parent instanceof HTMLElement) {
12
+ // Support both HTMLElement and SVGElement
13
+ if (parent instanceof HTMLElement || parent instanceof SVGElement) {
14
+ // Use getAttribute instead of dataset to support both HTML and SVG elements
15
+ const dataLocatorjs = parent.getAttribute("data-locatorjs");
16
+ const dataLocatorjsId = parent.getAttribute("data-locatorjs-id");
17
+
13
18
  // Check for either data-locatorjs (path-based) or data-locatorjs-id (ID-based)
14
- if (parent.dataset.locatorjs || parent.dataset.locatorjsId) {
19
+ if (dataLocatorjs || dataLocatorjsId) {
15
20
  let fileFullPath;
16
- if (parent.dataset.locatorjs) {
17
- const parsed = parseDataPath(parent.dataset.locatorjs);
21
+ if (dataLocatorjs) {
22
+ const parsed = parseDataPath(dataLocatorjs);
18
23
  if (!parsed) {
19
24
  goParent(parent);
20
25
  return;
21
26
  }
22
27
  [fileFullPath] = parsed;
23
- } else if (parent.dataset.locatorjsId) {
24
- [fileFullPath] = parseDataId(parent.dataset.locatorjsId);
28
+ } else if (dataLocatorjsId) {
29
+ [fileFullPath] = parseDataId(dataLocatorjsId);
25
30
  } else {
26
31
  goParent(parent);
27
32
  return;
@@ -5,10 +5,13 @@ import { getExpressionData } from "./getExpressionData";
5
5
  import { getJSXComponentBoundingBox } from "./getJSXComponentBoundingBox";
6
6
  export function getElementInfo(target) {
7
7
  const found = target.closest("[data-locatorjs-id], [data-locatorjs]");
8
- if (found && found instanceof HTMLElement && found.dataset && (found.dataset.locatorjsId || found.dataset.locatorjs || found.dataset.locatorjsStyled)) {
9
- const dataId = found.dataset.locatorjsId;
10
- const dataPath = found.dataset.locatorjs;
11
- const styledDataId = found.dataset.locatorjsStyled;
8
+
9
+ // Support both HTMLElement and SVGElement
10
+ // SVG elements don't have dataset, so use getAttribute instead
11
+ const dataId = found?.getAttribute("data-locatorjs-id");
12
+ const dataPath = found?.getAttribute("data-locatorjs");
13
+ const styledDataId = found?.getAttribute("data-locatorjs-styled");
14
+ if (found && (found instanceof HTMLElement || found instanceof SVGElement) && (dataId || dataPath || styledDataId)) {
12
15
  if (!dataId && !dataPath) {
13
16
  return null;
14
17
  }
@@ -89,8 +92,9 @@ export function getElementInfo(target) {
89
92
  }
90
93
  export class JSXTreeNodeElement extends HtmlElementTreeNode {
91
94
  getSource() {
92
- const dataId = this.element.dataset.locatorjsId;
93
- const dataPath = this.element.dataset.locatorjs;
95
+ // Use getAttribute instead of dataset to support both HTML and SVG elements
96
+ const dataId = this.element.getAttribute("data-locatorjs-id");
97
+ const dataPath = this.element.getAttribute("data-locatorjs");
94
98
  if (!dataId && !dataPath) {
95
99
  return null;
96
100
  }
@@ -131,8 +135,9 @@ export class JSXTreeNodeElement extends HtmlElementTreeNode {
131
135
  return null;
132
136
  }
133
137
  getComponent() {
134
- const dataId = this.element.dataset.locatorjsId;
135
- const dataPath = this.element.dataset.locatorjs;
138
+ // Use getAttribute instead of dataset to support both HTML and SVG elements
139
+ const dataId = this.element.getAttribute("data-locatorjs-id");
140
+ const dataPath = this.element.getAttribute("data-locatorjs");
136
141
  if (!dataId && !dataPath) {
137
142
  return null;
138
143
  }
@@ -5,7 +5,16 @@ import { HtmlElementTreeNode } from "../HtmlElementTreeNode";
5
5
  export declare function getElementInfo(found: HTMLElement): FullElementInfo | null;
6
6
  export declare class ReactTreeNodeElement extends HtmlElementTreeNode {
7
7
  getSource(): Source | null;
8
+ private fiberToTreeNodeComponent;
8
9
  getComponent(): TreeNodeComponent | null;
10
+ /**
11
+ * Traverse the _debugOwner chain to collect all owner components.
12
+ * This finds wrapper components (like Sidebar) that don't render their own DOM elements
13
+ * but wrap other components (like GlassPanel) that do.
14
+ *
15
+ * Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
16
+ */
17
+ getOwnerComponents(): TreeNodeComponent[];
9
18
  }
10
19
  declare const reactAdapter: AdapterObject;
11
20
  export default reactAdapter;
@@ -63,23 +63,61 @@ export class ReactTreeNodeElement extends HtmlElementTreeNode {
63
63
  }
64
64
  return null;
65
65
  }
66
+ fiberToTreeNodeComponent(fiber) {
67
+ const fiberLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
68
+ return {
69
+ label: fiberLabel.label,
70
+ callLink: fiberLabel.link && {
71
+ fileName: fiberLabel.link.filePath,
72
+ lineNumber: fiberLabel.link.line,
73
+ columnNumber: fiberLabel.link.column,
74
+ projectPath: fiberLabel.link.projectPath
75
+ } || undefined
76
+ };
77
+ }
66
78
  getComponent() {
67
79
  const fiber = findFiberByHtmlElement(this.element, false);
68
80
  const componentFiber = fiber?._debugOwner;
69
81
  if (componentFiber) {
70
- const fiberLabel = getFiberLabel(componentFiber, findDebugSource(componentFiber)?.source);
71
- return {
72
- label: fiberLabel.label,
73
- callLink: fiberLabel.link && {
74
- fileName: fiberLabel.link.filePath,
75
- lineNumber: fiberLabel.link.line,
76
- columnNumber: fiberLabel.link.column,
77
- projectPath: fiberLabel.link.projectPath
78
- } || undefined
79
- };
82
+ return this.fiberToTreeNodeComponent(componentFiber);
80
83
  }
81
84
  return null;
82
85
  }
86
+
87
+ /**
88
+ * Traverse the _debugOwner chain to collect all owner components.
89
+ * This finds wrapper components (like Sidebar) that don't render their own DOM elements
90
+ * but wrap other components (like GlassPanel) that do.
91
+ *
92
+ * Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
93
+ */
94
+ getOwnerComponents() {
95
+ const fiber = findFiberByHtmlElement(this.element, false);
96
+ if (!fiber) return [];
97
+
98
+ // Get the parent DOM element's owner to know when to stop
99
+ const parentElement = this.element.parentElement;
100
+ let parentOwnerFiber = null;
101
+ if (parentElement) {
102
+ const parentFiber = findFiberByHtmlElement(parentElement, false);
103
+ parentOwnerFiber = parentFiber?._debugOwner || null;
104
+ }
105
+ const components = [];
106
+ let currentFiber = fiber._debugOwner;
107
+
108
+ // Traverse up the _debugOwner chain until we hit the parent's owner or run out
109
+ while (currentFiber) {
110
+ // Stop if we've reached the parent DOM element's owner component
111
+ if (parentOwnerFiber && currentFiber === parentOwnerFiber) {
112
+ break;
113
+ }
114
+ components.push(this.fiberToTreeNodeComponent(currentFiber));
115
+ currentFiber = currentFiber._debugOwner || null;
116
+ }
117
+
118
+ // Reverse so outermost (Sidebar) comes first, innermost (GlassPanel) last
119
+ return components.reverse();
120
+ }
83
121
  }
84
122
  function getTree(element) {
85
123
  const originalRoot = new ReactTreeNodeElement(element);
@@ -39,14 +39,36 @@ function Runtime(props) {
39
39
  setHoldingModKey(e.altKey);
40
40
  }
41
41
  function mouseOverListener(e) {
42
- const target = e.target;
43
- if (target && target instanceof HTMLElement) {
44
- if (isLocatorsOwnElement(target)) {
45
- return;
42
+ // Also update modifier state
43
+ setHoldingModKey(e.altKey);
44
+
45
+ // Use elementsFromPoint to find elements including ones with pointer-events-none
46
+ const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
47
+
48
+ // Find the topmost element with locator data for highlighting
49
+ let element = null;
50
+ for (const el of elementsAtPoint) {
51
+ if (isLocatorsOwnElement(el)) {
52
+ continue;
53
+ }
54
+ if (el instanceof HTMLElement || el instanceof SVGElement) {
55
+ const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
56
+ if (withLocator) {
57
+ element = withLocator;
58
+ break;
59
+ }
60
+ }
61
+ }
62
+
63
+ // Fallback to e.target
64
+ if (!element) {
65
+ const target = e.target;
66
+ if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
67
+ element = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
46
68
  }
47
- setCurrentElement(target);
48
- // Also update modifier state
49
- setHoldingModKey(e.altKey);
69
+ }
70
+ if (element && !isLocatorsOwnElement(element)) {
71
+ setCurrentElement(element);
50
72
  }
51
73
  }
52
74
  function mouseDownUpListener(e) {
@@ -62,31 +84,59 @@ function Runtime(props) {
62
84
  if (!e.altKey && !isCombinationModifiersPressed(e) && !locatorActive()) {
63
85
  return;
64
86
  }
65
- const target = e.target;
66
- if (target && target instanceof HTMLElement) {
67
- if (target.shadowRoot) {
68
- return;
87
+
88
+ // Use elementsFromPoint to find all elements at click position,
89
+ // including ones with pointer-events-none (like canvas overlays)
90
+ const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
91
+
92
+ // Find the topmost element with locator data
93
+ let element = null;
94
+ for (const el of elementsAtPoint) {
95
+ if (isLocatorsOwnElement(el)) {
96
+ continue;
69
97
  }
70
- if (isLocatorsOwnElement(target)) {
71
- return;
98
+ if (el instanceof HTMLElement || el instanceof SVGElement) {
99
+ // Check if this element or its closest ancestor has locator data
100
+ const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
101
+ if (withLocator) {
102
+ element = withLocator;
103
+ break;
104
+ }
72
105
  }
73
- e.preventDefault();
74
- e.stopPropagation();
106
+ }
75
107
 
76
- // Copy ancestry to clipboard on alt+click
77
- const treeNode = createTreeNode(target, props.adapterId);
78
- if (treeNode) {
79
- const ancestry = collectAncestry(treeNode);
80
- const formatted = formatAncestryChain(ancestry);
81
- navigator.clipboard.writeText(formatted).then(() => {
82
- setToastMessage("Copied to clipboard");
83
- });
108
+ // Fallback to e.target if elementsFromPoint didn't find anything
109
+ if (!element) {
110
+ const target = e.target;
111
+ if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
112
+ element = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
84
113
  }
114
+ }
115
+ if (!element) {
116
+ return;
117
+ }
118
+ if (element instanceof HTMLElement && element.shadowRoot) {
119
+ return;
120
+ }
121
+ if (isLocatorsOwnElement(element)) {
122
+ return;
123
+ }
124
+ e.preventDefault();
125
+ e.stopPropagation();
85
126
 
86
- // Deactivate toggle after click
87
- if (locatorActive()) {
88
- setLocatorActive(false);
89
- }
127
+ // Copy ancestry to clipboard on alt+click
128
+ const treeNode = createTreeNode(element, props.adapterId);
129
+ if (treeNode) {
130
+ const ancestry = collectAncestry(treeNode);
131
+ const formatted = formatAncestryChain(ancestry);
132
+ navigator.clipboard.writeText(formatted).then(() => {
133
+ setToastMessage("Copied to clipboard");
134
+ });
135
+ }
136
+
137
+ // Deactivate toggle after click
138
+ if (locatorActive()) {
139
+ setLocatorActive(false);
90
140
  }
91
141
  }
92
142
  function scrollListener() {
@@ -1,4 +1,9 @@
1
1
  import { TreeNode } from "../types/TreeNode";
2
+ export interface OwnerComponentInfo {
3
+ name: string;
4
+ filePath?: string;
5
+ line?: number;
6
+ }
2
7
  export interface AncestryItem {
3
8
  elementName: string;
4
9
  componentName?: string;
@@ -6,6 +11,8 @@ export interface AncestryItem {
6
11
  line?: number;
7
12
  id?: string;
8
13
  nthChild?: number;
14
+ /** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
15
+ ownerComponents?: OwnerComponentInfo[];
9
16
  }
10
17
  export declare function collectAncestry(node: TreeNode): AncestryItem[];
11
18
  export declare function formatAncestryChain(items: AncestryItem[]): string;
@@ -19,6 +19,13 @@ function getNthChildIfAmbiguous(element) {
19
19
  const index = siblings.indexOf(element);
20
20
  return index + 1; // 1-indexed for CSS nth-child compatibility
21
21
  }
22
+ function treeNodeComponentToOwnerInfo(comp) {
23
+ return {
24
+ name: comp.label,
25
+ filePath: comp.callLink?.fileName,
26
+ line: comp.callLink?.lineNumber
27
+ };
28
+ }
22
29
  export function collectAncestry(node) {
23
30
  const items = [];
24
31
  let current = node;
@@ -28,7 +35,6 @@ export function collectAncestry(node) {
28
35
  current = current.getParent();
29
36
  continue;
30
37
  }
31
- const component = current.getComponent();
32
38
  const source = current.getSource();
33
39
  const item = {
34
40
  elementName: current.name
@@ -47,11 +53,27 @@ export function collectAncestry(node) {
47
53
  }
48
54
  }
49
55
  }
50
- if (component) {
51
- item.componentName = component.label;
52
- if (component.callLink) {
53
- item.filePath = component.callLink.fileName;
54
- item.line = component.callLink.lineNumber;
56
+
57
+ // Get all owner components (from outermost like Sidebar to innermost like GlassPanel)
58
+ const ownerComponents = current.getOwnerComponents();
59
+ const outermost = ownerComponents[0];
60
+ if (outermost) {
61
+ item.ownerComponents = ownerComponents.map(treeNodeComponentToOwnerInfo);
62
+ // Use outermost component as the primary component name
63
+ item.componentName = outermost.label;
64
+ if (outermost.callLink) {
65
+ item.filePath = outermost.callLink.fileName;
66
+ item.line = outermost.callLink.lineNumber;
67
+ }
68
+ } else {
69
+ // Fallback to single component if getOwnerComponents not available
70
+ const component = current.getComponent();
71
+ if (component) {
72
+ item.componentName = component.label;
73
+ if (component.callLink) {
74
+ item.filePath = component.callLink.fileName;
75
+ item.line = component.callLink.lineNumber;
76
+ }
55
77
  }
56
78
  }
57
79
  if (!item.filePath && source) {
@@ -88,7 +110,12 @@ export function formatAncestryChain(items) {
88
110
  selector += `#${item.id}`;
89
111
  }
90
112
  let description = selector;
91
- if (item.componentName) {
113
+
114
+ // Show all owner components if available (Sidebar > GlassPanel)
115
+ if (item.ownerComponents && item.ownerComponents.length > 0) {
116
+ const componentChain = item.ownerComponents.map(c => c.name).join(" > ");
117
+ description = `${selector} in ${componentChain}`;
118
+ } else if (item.componentName) {
92
119
  description = `${selector} in ${item.componentName}`;
93
120
  }
94
121
  const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
@@ -78,4 +78,45 @@ describe("formatAncestryChain", () => {
78
78
  const result = formatAncestryChain([]);
79
79
  expect(result).toBe("");
80
80
  });
81
+ it("shows all owner components when ownerComponents is provided", () => {
82
+ const items = [{
83
+ elementName: "div",
84
+ id: "sidebar-panel",
85
+ componentName: "Sidebar",
86
+ filePath: "src/components/game/Sidebar.jsx",
87
+ line: 78,
88
+ ownerComponents: [{
89
+ name: "Sidebar",
90
+ filePath: "src/components/game/Sidebar.jsx",
91
+ line: 78
92
+ }, {
93
+ name: "GlassPanel",
94
+ filePath: "src/components/common/GlassPanel.jsx",
95
+ line: 39
96
+ }]
97
+ }, {
98
+ elementName: "div",
99
+ componentName: "App",
100
+ filePath: "src/App.jsx",
101
+ line: 104
102
+ }];
103
+ const result = formatAncestryChain(items);
104
+ expect(result).toBe(`div in App at src/App.jsx:104
105
+ └─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`);
106
+ });
107
+ it("shows single owner component without arrow when only one in chain", () => {
108
+ const items = [{
109
+ elementName: "button",
110
+ componentName: "Button",
111
+ ownerComponents: [{
112
+ name: "Button",
113
+ filePath: "src/Button.tsx",
114
+ line: 10
115
+ }],
116
+ filePath: "src/Button.tsx",
117
+ line: 10
118
+ }];
119
+ const result = formatAncestryChain(items);
120
+ expect(result).toBe("button in Button at src/Button.tsx:10");
121
+ });
81
122
  });
@@ -8,6 +8,12 @@ export interface TreeNode {
8
8
  getChildren(): TreeNode[];
9
9
  getSource(): Source | null;
10
10
  getComponent(): TreeNodeComponent | null;
11
+ /**
12
+ * Get all owner components in the ownership chain.
13
+ * For React: traverses _debugOwner chain until hitting parent DOM element's owner.
14
+ * Returns array from outermost (user's component like Sidebar) to innermost (wrapper like GlassPanel).
15
+ */
16
+ getOwnerComponents(): TreeNodeComponent[];
11
17
  }
12
18
  export type TreeNodeComponent = {
13
19
  label: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treelocator/runtime",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "TreeLocatorJS runtime for component ancestry tracking. Alt+click any element to copy its component tree to clipboard. Exposes window.__treelocator__ API for browser automation (Playwright, Puppeteer, Selenium, Cypress).",
5
5
  "keywords": [
6
6
  "locator",
@@ -72,5 +72,5 @@
72
72
  "directory": "packages/runtime"
73
73
  },
74
74
  "license": "MIT",
75
- "gitHead": "49e1500d0dee128e4e7b549a012f406cb0b55568"
75
+ "gitHead": "69fb05167bd689b2e3602fcbc74a70912695f136"
76
76
  }
@@ -25,9 +25,9 @@ export class HtmlElementTreeNode implements TreeNode {
25
25
  const children = Array.from(this.element.children);
26
26
  return children
27
27
  .map((child) => {
28
- if (child instanceof HTMLElement) {
28
+ if (child instanceof HTMLElement || child instanceof SVGElement) {
29
29
  // @ts-ignore
30
- return new this.constructor(child);
30
+ return new this.constructor(child as HTMLElement);
31
31
  } else {
32
32
  return null;
33
33
  }
@@ -48,4 +48,7 @@ export class HtmlElementTreeNode implements TreeNode {
48
48
  getComponent(): TreeNodeComponent | null {
49
49
  throw new Error("Method not implemented.");
50
50
  }
51
+ getOwnerComponents(): TreeNodeComponent[] {
52
+ return [];
53
+ }
51
54
  }
@@ -10,7 +10,7 @@ export type ElementInfo = {
10
10
 
11
11
  export type FullElementInfo = {
12
12
  thisElement: ElementInfo;
13
- htmlElement: HTMLElement;
13
+ htmlElement: HTMLElement | SVGElement;
14
14
  parentElements: ElementInfo[];
15
15
  componentBox: SimpleDOMRect;
16
16
  componentsLabels: LabelData[];
@@ -2,12 +2,16 @@ import type { ExpressionInfo, FileStorage } from "@locator/shared";
2
2
  import { parseDataId, parseDataPath } from "../../functions/parseDataId";
3
3
 
4
4
  export function getExpressionData(
5
- target: HTMLElement,
5
+ target: Element,
6
6
  fileData: FileStorage | null
7
7
  ): ExpressionInfo | null {
8
+ // Use getAttribute instead of dataset to support both HTML and SVG elements
9
+ const dataLocatorjs = target.getAttribute("data-locatorjs");
10
+ const dataLocatorjsId = target.getAttribute("data-locatorjs-id");
11
+
8
12
  // First check for data-locatorjs (path-based, for server components)
9
- if (target.dataset.locatorjs) {
10
- const parsed = parseDataPath(target.dataset.locatorjs);
13
+ if (dataLocatorjs) {
14
+ const parsed = parseDataPath(dataLocatorjs);
11
15
  if (parsed) {
12
16
  const [, line, column] = parsed;
13
17
 
@@ -35,8 +39,8 @@ export function getExpressionData(
35
39
  }
36
40
 
37
41
  // Fall back to data-locatorjs-id (ID-based, traditional approach)
38
- if (target.dataset.locatorjsId && fileData) {
39
- const [, id] = parseDataId(target.dataset.locatorjsId);
42
+ if (dataLocatorjsId && fileData) {
43
+ const [, id] = parseDataId(dataLocatorjsId);
40
44
  const expData = fileData.expressions[Number(id)];
41
45
  if (expData) {
42
46
  return expData;
@@ -5,32 +5,37 @@ import type { SimpleDOMRect } from "../../types/types";
5
5
  import { getExpressionData } from "./getExpressionData";
6
6
 
7
7
  export function getJSXComponentBoundingBox(
8
- found: HTMLElement,
8
+ found: Element,
9
9
  locatorData: { [filename: string]: FileStorage },
10
10
  componentFolder: string,
11
11
  componentId: number
12
12
  ): SimpleDOMRect {
13
13
  let composedBox: SimpleDOMRect = found.getBoundingClientRect();
14
14
  // Currently it works well only for components with one root element, but for components with multiple root elements we would need to track instance ids.
15
- function goParent(current: HTMLElement) {
15
+ function goParent(current: Element) {
16
16
  const parent = current.parentNode;
17
17
  if (!parent) {
18
18
  return;
19
19
  }
20
- if (parent instanceof HTMLElement) {
20
+ // Support both HTMLElement and SVGElement
21
+ if (parent instanceof HTMLElement || parent instanceof SVGElement) {
22
+ // Use getAttribute instead of dataset to support both HTML and SVG elements
23
+ const dataLocatorjs = parent.getAttribute("data-locatorjs");
24
+ const dataLocatorjsId = parent.getAttribute("data-locatorjs-id");
25
+
21
26
  // Check for either data-locatorjs (path-based) or data-locatorjs-id (ID-based)
22
- if (parent.dataset.locatorjs || parent.dataset.locatorjsId) {
27
+ if (dataLocatorjs || dataLocatorjsId) {
23
28
  let fileFullPath: string;
24
29
 
25
- if (parent.dataset.locatorjs) {
26
- const parsed = parseDataPath(parent.dataset.locatorjs);
30
+ if (dataLocatorjs) {
31
+ const parsed = parseDataPath(dataLocatorjs);
27
32
  if (!parsed) {
28
33
  goParent(parent);
29
34
  return;
30
35
  }
31
36
  [fileFullPath] = parsed;
32
- } else if (parent.dataset.locatorjsId) {
33
- [fileFullPath] = parseDataId(parent.dataset.locatorjsId);
37
+ } else if (dataLocatorjsId) {
38
+ [fileFullPath] = parseDataId(dataLocatorjsId);
34
39
  } else {
35
40
  goParent(parent);
36
41
  return;
@@ -20,17 +20,17 @@ import { getJSXComponentBoundingBox } from "./getJSXComponentBoundingBox";
20
20
  export function getElementInfo(target: HTMLElement): FullElementInfo | null {
21
21
  const found = target.closest("[data-locatorjs-id], [data-locatorjs]");
22
22
 
23
+ // Support both HTMLElement and SVGElement
24
+ // SVG elements don't have dataset, so use getAttribute instead
25
+ const dataId = found?.getAttribute("data-locatorjs-id");
26
+ const dataPath = found?.getAttribute("data-locatorjs");
27
+ const styledDataId = found?.getAttribute("data-locatorjs-styled");
28
+
23
29
  if (
24
30
  found &&
25
- found instanceof HTMLElement &&
26
- found.dataset &&
27
- (found.dataset.locatorjsId ||
28
- found.dataset.locatorjs ||
29
- found.dataset.locatorjsStyled)
31
+ (found instanceof HTMLElement || found instanceof SVGElement) &&
32
+ (dataId || dataPath || styledDataId)
30
33
  ) {
31
- const dataId = found.dataset.locatorjsId;
32
- const dataPath = found.dataset.locatorjs;
33
- const styledDataId = found.dataset.locatorjsStyled;
34
34
 
35
35
  if (!dataId && !dataPath) {
36
36
  return null;
@@ -135,8 +135,9 @@ export function getElementInfo(target: HTMLElement): FullElementInfo | null {
135
135
 
136
136
  export class JSXTreeNodeElement extends HtmlElementTreeNode {
137
137
  getSource(): Source | null {
138
- const dataId = this.element.dataset.locatorjsId;
139
- const dataPath = this.element.dataset.locatorjs;
138
+ // Use getAttribute instead of dataset to support both HTML and SVG elements
139
+ const dataId = this.element.getAttribute("data-locatorjs-id");
140
+ const dataPath = this.element.getAttribute("data-locatorjs");
140
141
 
141
142
  if (!dataId && !dataPath) {
142
143
  return null;
@@ -184,8 +185,9 @@ export class JSXTreeNodeElement extends HtmlElementTreeNode {
184
185
  return null;
185
186
  }
186
187
  getComponent(): TreeNodeComponent | null {
187
- const dataId = this.element.dataset.locatorjsId;
188
- const dataPath = this.element.dataset.locatorjs;
188
+ // Use getAttribute instead of dataset to support both HTML and SVG elements
189
+ const dataId = this.element.getAttribute("data-locatorjs-id");
190
+ const dataPath = this.element.getAttribute("data-locatorjs");
189
191
 
190
192
  if (!dataId && !dataPath) {
191
193
  return null;
@@ -81,30 +81,68 @@ export class ReactTreeNodeElement extends HtmlElementTreeNode {
81
81
  }
82
82
  return null;
83
83
  }
84
+
85
+ private fiberToTreeNodeComponent(fiber: Fiber): TreeNodeComponent {
86
+ const fiberLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
87
+ return {
88
+ label: fiberLabel.label,
89
+ callLink:
90
+ (fiberLabel.link && {
91
+ fileName: fiberLabel.link.filePath,
92
+ lineNumber: fiberLabel.link.line,
93
+ columnNumber: fiberLabel.link.column,
94
+ projectPath: fiberLabel.link.projectPath,
95
+ }) ||
96
+ undefined,
97
+ };
98
+ }
99
+
84
100
  getComponent(): TreeNodeComponent | null {
85
101
  const fiber = findFiberByHtmlElement(this.element, false);
86
102
  const componentFiber = fiber?._debugOwner;
87
103
 
88
104
  if (componentFiber) {
89
- const fiberLabel = getFiberLabel(
90
- componentFiber,
91
- findDebugSource(componentFiber)?.source
92
- );
93
-
94
- return {
95
- label: fiberLabel.label,
96
- callLink:
97
- (fiberLabel.link && {
98
- fileName: fiberLabel.link.filePath,
99
- lineNumber: fiberLabel.link.line,
100
- columnNumber: fiberLabel.link.column,
101
- projectPath: fiberLabel.link.projectPath,
102
- }) ||
103
- undefined,
104
- };
105
+ return this.fiberToTreeNodeComponent(componentFiber);
105
106
  }
106
107
  return null;
107
108
  }
109
+
110
+ /**
111
+ * Traverse the _debugOwner chain to collect all owner components.
112
+ * This finds wrapper components (like Sidebar) that don't render their own DOM elements
113
+ * but wrap other components (like GlassPanel) that do.
114
+ *
115
+ * Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
116
+ */
117
+ getOwnerComponents(): TreeNodeComponent[] {
118
+ const fiber = findFiberByHtmlElement(this.element, false);
119
+ if (!fiber) return [];
120
+
121
+ // Get the parent DOM element's owner to know when to stop
122
+ const parentElement = this.element.parentElement;
123
+ let parentOwnerFiber: Fiber | null = null;
124
+ if (parentElement) {
125
+ const parentFiber = findFiberByHtmlElement(parentElement, false);
126
+ parentOwnerFiber = parentFiber?._debugOwner || null;
127
+ }
128
+
129
+ const components: TreeNodeComponent[] = [];
130
+ let currentFiber = fiber._debugOwner;
131
+
132
+ // Traverse up the _debugOwner chain until we hit the parent's owner or run out
133
+ while (currentFiber) {
134
+ // Stop if we've reached the parent DOM element's owner component
135
+ if (parentOwnerFiber && currentFiber === parentOwnerFiber) {
136
+ break;
137
+ }
138
+
139
+ components.push(this.fiberToTreeNodeComponent(currentFiber));
140
+ currentFiber = currentFiber._debugOwner || null;
141
+ }
142
+
143
+ // Reverse so outermost (Sidebar) comes first, innermost (GlassPanel) last
144
+ return components.reverse();
145
+ }
108
146
  }
109
147
 
110
148
  function getTree(element: HTMLElement): TreeState | null {
@@ -48,15 +48,41 @@ function Runtime(props: RuntimeProps) {
48
48
  }
49
49
 
50
50
  function mouseOverListener(e: MouseEvent) {
51
- const target = e.target;
52
- if (target && target instanceof HTMLElement) {
53
- if (isLocatorsOwnElement(target)) {
54
- return;
51
+ // Also update modifier state
52
+ setHoldingModKey(e.altKey);
53
+
54
+ // Use elementsFromPoint to find elements including ones with pointer-events-none
55
+ const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
56
+
57
+ // Find the topmost element with locator data for highlighting
58
+ let element: HTMLElement | null = null;
59
+ for (const el of elementsAtPoint) {
60
+ if (isLocatorsOwnElement(el as HTMLElement)) {
61
+ continue;
62
+ }
63
+ if (el instanceof HTMLElement || el instanceof SVGElement) {
64
+ const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
65
+ if (withLocator) {
66
+ element = withLocator as HTMLElement;
67
+ break;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Fallback to e.target
73
+ if (!element) {
74
+ const target = e.target;
75
+ if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
76
+ element = target instanceof SVGElement
77
+ ? (target.closest('[data-locatorjs-id], [data-locatorjs]') as HTMLElement | null) ??
78
+ (target.closest('svg') as HTMLElement | null) ??
79
+ (target as unknown as HTMLElement)
80
+ : target;
55
81
  }
82
+ }
56
83
 
57
- setCurrentElement(target);
58
- // Also update modifier state
59
- setHoldingModKey(e.altKey);
84
+ if (element && !isLocatorsOwnElement(element)) {
85
+ setCurrentElement(element);
60
86
  }
61
87
  }
62
88
 
@@ -76,33 +102,66 @@ function Runtime(props: RuntimeProps) {
76
102
  return;
77
103
  }
78
104
 
79
- const target = e.target;
80
- if (target && target instanceof HTMLElement) {
81
- if (target.shadowRoot) {
82
- return;
105
+ // Use elementsFromPoint to find all elements at click position,
106
+ // including ones with pointer-events-none (like canvas overlays)
107
+ const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
108
+
109
+ // Find the topmost element with locator data
110
+ let element: Element | null = null;
111
+ for (const el of elementsAtPoint) {
112
+ if (isLocatorsOwnElement(el as HTMLElement)) {
113
+ continue;
83
114
  }
115
+ if (el instanceof HTMLElement || el instanceof SVGElement) {
116
+ // Check if this element or its closest ancestor has locator data
117
+ const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
118
+ if (withLocator) {
119
+ element = withLocator;
120
+ break;
121
+ }
122
+ }
123
+ }
84
124
 
85
- if (isLocatorsOwnElement(target)) {
86
- return;
125
+ // Fallback to e.target if elementsFromPoint didn't find anything
126
+ if (!element) {
127
+ const target = e.target;
128
+ if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
129
+ element = target instanceof SVGElement
130
+ ? (target.closest('[data-locatorjs-id], [data-locatorjs]') as Element | null) ??
131
+ (target.closest('svg') as Element | null) ??
132
+ target
133
+ : target;
87
134
  }
135
+ }
88
136
 
89
- e.preventDefault();
90
- e.stopPropagation();
137
+ if (!element) {
138
+ return;
139
+ }
91
140
 
92
- // Copy ancestry to clipboard on alt+click
93
- const treeNode = createTreeNode(target, props.adapterId);
94
- if (treeNode) {
95
- const ancestry = collectAncestry(treeNode);
96
- const formatted = formatAncestryChain(ancestry);
97
- navigator.clipboard.writeText(formatted).then(() => {
98
- setToastMessage("Copied to clipboard");
99
- });
100
- }
141
+ if (element instanceof HTMLElement && element.shadowRoot) {
142
+ return;
143
+ }
101
144
 
102
- // Deactivate toggle after click
103
- if (locatorActive()) {
104
- setLocatorActive(false);
105
- }
145
+ if (isLocatorsOwnElement(element as HTMLElement)) {
146
+ return;
147
+ }
148
+
149
+ e.preventDefault();
150
+ e.stopPropagation();
151
+
152
+ // Copy ancestry to clipboard on alt+click
153
+ const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
154
+ if (treeNode) {
155
+ const ancestry = collectAncestry(treeNode);
156
+ const formatted = formatAncestryChain(ancestry);
157
+ navigator.clipboard.writeText(formatted).then(() => {
158
+ setToastMessage("Copied to clipboard");
159
+ });
160
+ }
161
+
162
+ // Deactivate toggle after click
163
+ if (locatorActive()) {
164
+ setLocatorActive(false);
106
165
  }
107
166
  }
108
167
 
@@ -91,4 +91,50 @@ describe("formatAncestryChain", () => {
91
91
  const result = formatAncestryChain([]);
92
92
  expect(result).toBe("");
93
93
  });
94
+
95
+ it("shows all owner components when ownerComponents is provided", () => {
96
+ const items: AncestryItem[] = [
97
+ {
98
+ elementName: "div",
99
+ id: "sidebar-panel",
100
+ componentName: "Sidebar",
101
+ filePath: "src/components/game/Sidebar.jsx",
102
+ line: 78,
103
+ ownerComponents: [
104
+ {
105
+ name: "Sidebar",
106
+ filePath: "src/components/game/Sidebar.jsx",
107
+ line: 78,
108
+ },
109
+ {
110
+ name: "GlassPanel",
111
+ filePath: "src/components/common/GlassPanel.jsx",
112
+ line: 39,
113
+ },
114
+ ],
115
+ },
116
+ { elementName: "div", componentName: "App", filePath: "src/App.jsx", line: 104 },
117
+ ];
118
+
119
+ const result = formatAncestryChain(items);
120
+ expect(result).toBe(
121
+ `div in App at src/App.jsx:104
122
+ └─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`
123
+ );
124
+ });
125
+
126
+ it("shows single owner component without arrow when only one in chain", () => {
127
+ const items: AncestryItem[] = [
128
+ {
129
+ elementName: "button",
130
+ componentName: "Button",
131
+ ownerComponents: [{ name: "Button", filePath: "src/Button.tsx", line: 10 }],
132
+ filePath: "src/Button.tsx",
133
+ line: 10,
134
+ },
135
+ ];
136
+
137
+ const result = formatAncestryChain(items);
138
+ expect(result).toBe("button in Button at src/Button.tsx:10");
139
+ });
94
140
  });
@@ -1,4 +1,10 @@
1
- import { TreeNode, TreeNodeElement } from "../types/TreeNode";
1
+ import { TreeNode, TreeNodeComponent, TreeNodeElement } from "../types/TreeNode";
2
+
3
+ export interface OwnerComponentInfo {
4
+ name: string;
5
+ filePath?: string;
6
+ line?: number;
7
+ }
2
8
 
3
9
  export interface AncestryItem {
4
10
  elementName: string;
@@ -7,6 +13,8 @@ export interface AncestryItem {
7
13
  line?: number;
8
14
  id?: string;
9
15
  nthChild?: number; // 1-indexed, only set when there are ambiguous siblings
16
+ /** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
17
+ ownerComponents?: OwnerComponentInfo[];
10
18
  }
11
19
 
12
20
  // Elements to exclude from ancestry (not useful for debugging)
@@ -36,6 +44,16 @@ function getNthChildIfAmbiguous(element: Element): number | undefined {
36
44
  return index + 1; // 1-indexed for CSS nth-child compatibility
37
45
  }
38
46
 
47
+ function treeNodeComponentToOwnerInfo(
48
+ comp: TreeNodeComponent
49
+ ): OwnerComponentInfo {
50
+ return {
51
+ name: comp.label,
52
+ filePath: comp.callLink?.fileName,
53
+ line: comp.callLink?.lineNumber,
54
+ };
55
+ }
56
+
39
57
  export function collectAncestry(node: TreeNode): AncestryItem[] {
40
58
  const items: AncestryItem[] = [];
41
59
  let current: TreeNode | null = node;
@@ -47,7 +65,6 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
47
65
  continue;
48
66
  }
49
67
 
50
- const component = current.getComponent();
51
68
  const source = current.getSource();
52
69
 
53
70
  const item: AncestryItem = {
@@ -68,11 +85,26 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
68
85
  }
69
86
  }
70
87
 
71
- if (component) {
72
- item.componentName = component.label;
73
- if (component.callLink) {
74
- item.filePath = component.callLink.fileName;
75
- item.line = component.callLink.lineNumber;
88
+ // Get all owner components (from outermost like Sidebar to innermost like GlassPanel)
89
+ const ownerComponents = current.getOwnerComponents();
90
+ const outermost = ownerComponents[0];
91
+ if (outermost) {
92
+ item.ownerComponents = ownerComponents.map(treeNodeComponentToOwnerInfo);
93
+ // Use outermost component as the primary component name
94
+ item.componentName = outermost.label;
95
+ if (outermost.callLink) {
96
+ item.filePath = outermost.callLink.fileName;
97
+ item.line = outermost.callLink.lineNumber;
98
+ }
99
+ } else {
100
+ // Fallback to single component if getOwnerComponents not available
101
+ const component = current.getComponent();
102
+ if (component) {
103
+ item.componentName = component.label;
104
+ if (component.callLink) {
105
+ item.filePath = component.callLink.fileName;
106
+ item.line = component.callLink.lineNumber;
107
+ }
76
108
  }
77
109
  }
78
110
 
@@ -116,7 +148,12 @@ export function formatAncestryChain(items: AncestryItem[]): string {
116
148
  }
117
149
 
118
150
  let description = selector;
119
- if (item.componentName) {
151
+
152
+ // Show all owner components if available (Sidebar > GlassPanel)
153
+ if (item.ownerComponents && item.ownerComponents.length > 0) {
154
+ const componentChain = item.ownerComponents.map((c) => c.name).join(" > ");
155
+ description = `${selector} in ${componentChain}`;
156
+ } else if (item.componentName) {
120
157
  description = `${selector} in ${item.componentName}`;
121
158
  }
122
159
 
@@ -9,6 +9,12 @@ export interface TreeNode {
9
9
  getChildren(): TreeNode[];
10
10
  getSource(): Source | null;
11
11
  getComponent(): TreeNodeComponent | null;
12
+ /**
13
+ * Get all owner components in the ownership chain.
14
+ * For React: traverses _debugOwner chain until hitting parent DOM element's owner.
15
+ * Returns array from outermost (user's component like Sidebar) to innermost (wrapper like GlassPanel).
16
+ */
17
+ getOwnerComponents(): TreeNodeComponent[];
12
18
  }
13
19
 
14
20
  export type TreeNodeComponent = {