@treelocator/runtime 0.1.7 → 0.2.0

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.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +15 -13
  2. package/.turbo/turbo-dev.log +32 -0
  3. package/.turbo/turbo-test.log +54 -10
  4. package/dist/adapters/adapterApi.d.ts +1 -1
  5. package/dist/adapters/createTreeNode.js +32 -4
  6. package/dist/adapters/jsx/getExpressionData.d.ts +1 -1
  7. package/dist/adapters/jsx/getExpressionData.js +8 -4
  8. package/dist/adapters/jsx/getJSXComponentBoundingBox.d.ts +1 -1
  9. package/dist/adapters/jsx/getJSXComponentBoundingBox.js +11 -6
  10. package/dist/adapters/jsx/jsxAdapter.js +13 -8
  11. package/dist/adapters/nextjs/parseNextjsDataAttributes.d.ts +31 -0
  12. package/dist/adapters/nextjs/parseNextjsDataAttributes.js +106 -0
  13. package/dist/adapters/phoenix/__tests__/parsePhoenixComments.test.d.ts +4 -0
  14. package/dist/adapters/phoenix/__tests__/parsePhoenixComments.test.js +218 -0
  15. package/dist/adapters/phoenix/detectPhoenix.d.ts +11 -0
  16. package/dist/adapters/phoenix/detectPhoenix.js +38 -0
  17. package/dist/adapters/phoenix/index.d.ts +10 -0
  18. package/dist/adapters/phoenix/index.js +9 -0
  19. package/dist/adapters/phoenix/parsePhoenixComments.d.ts +35 -0
  20. package/dist/adapters/phoenix/parsePhoenixComments.js +131 -0
  21. package/dist/adapters/phoenix/types.d.ts +16 -0
  22. package/dist/adapters/phoenix/types.js +1 -0
  23. package/dist/adapters/react/getFiberLabel.js +2 -1
  24. package/dist/components/MaybeOutline.js +65 -3
  25. package/dist/components/Runtime.js +1 -1
  26. package/dist/functions/formatAncestryChain.d.ts +3 -0
  27. package/dist/functions/formatAncestryChain.js +104 -15
  28. package/dist/functions/formatAncestryChain.test.js +26 -20
  29. package/dist/functions/normalizeFilePath.d.ts +14 -0
  30. package/dist/functions/normalizeFilePath.js +40 -0
  31. package/dist/output.css +87 -15
  32. package/dist/types/ServerComponentInfo.d.ts +14 -0
  33. package/dist/types/ServerComponentInfo.js +1 -0
  34. package/package.json +4 -3
  35. package/src/adapters/adapterApi.ts +1 -1
  36. package/src/adapters/createTreeNode.ts +35 -3
  37. package/src/adapters/jsx/getExpressionData.ts +9 -5
  38. package/src/adapters/jsx/getJSXComponentBoundingBox.ts +13 -8
  39. package/src/adapters/jsx/jsxAdapter.ts +14 -12
  40. package/src/adapters/nextjs/parseNextjsDataAttributes.ts +112 -0
  41. package/src/adapters/phoenix/__tests__/parsePhoenixComments.test.ts +264 -0
  42. package/src/adapters/phoenix/detectPhoenix.ts +44 -0
  43. package/src/adapters/phoenix/index.ts +11 -0
  44. package/src/adapters/phoenix/parsePhoenixComments.ts +140 -0
  45. package/src/adapters/phoenix/types.ts +16 -0
  46. package/src/adapters/react/getFiberLabel.ts +2 -1
  47. package/src/components/MaybeOutline.tsx +63 -4
  48. package/src/components/Runtime.tsx +3 -1
  49. package/src/functions/formatAncestryChain.test.ts +26 -20
  50. package/src/functions/formatAncestryChain.ts +121 -15
  51. package/src/functions/normalizeFilePath.ts +41 -0
  52. package/src/types/ServerComponentInfo.ts +14 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from "vitest";
6
+ import { findPrecedingPhoenixComments, phoenixMatchesToServerComponents, parsePhoenixServerComponents } from "../parsePhoenixComments";
7
+ describe("parsePhoenixComments", () => {
8
+ let container;
9
+ beforeEach(() => {
10
+ container = document.createElement("div");
11
+ });
12
+ describe("findPrecedingPhoenixComments", () => {
13
+ it("parses @caller comment", () => {
14
+ container.innerHTML = `
15
+ <!-- @caller lib/app_web/home_live.ex:20 -->
16
+ <header>Content</header>
17
+ `;
18
+ const header = container.querySelector("header");
19
+ const matches = findPrecedingPhoenixComments(header);
20
+ expect(matches).toHaveLength(1);
21
+ expect(matches[0]).toMatchObject({
22
+ name: "@caller",
23
+ filePath: "lib/app_web/home_live.ex",
24
+ line: 20,
25
+ type: "caller"
26
+ });
27
+ });
28
+ it("parses component comment", () => {
29
+ container.innerHTML = `
30
+ <!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
31
+ <header>Content</header>
32
+ `;
33
+ const header = container.querySelector("header");
34
+ const matches = findPrecedingPhoenixComments(header);
35
+ expect(matches).toHaveLength(1);
36
+ expect(matches[0]).toMatchObject({
37
+ name: "AppWeb.CoreComponents.header",
38
+ filePath: "lib/app_web/core_components.ex",
39
+ line: 123,
40
+ type: "component"
41
+ });
42
+ });
43
+ it("ignores closing tag comments", () => {
44
+ container.innerHTML = `
45
+ <!-- </AppWeb.CoreComponents.header> -->
46
+ <header>Content</header>
47
+ `;
48
+ const header = container.querySelector("header");
49
+ const matches = findPrecedingPhoenixComments(header);
50
+ expect(matches).toHaveLength(0);
51
+ });
52
+ it("finds multiple preceding comments in correct order", () => {
53
+ container.innerHTML = `
54
+ <!-- @caller lib/app_web/home_live.ex:20 -->
55
+ <!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
56
+ <header>Content</header>
57
+ `;
58
+ const header = container.querySelector("header");
59
+ const matches = findPrecedingPhoenixComments(header);
60
+ expect(matches).toHaveLength(2);
61
+ // Should be ordered from outermost to innermost
62
+ expect(matches[0].name).toBe("@caller");
63
+ expect(matches[0].line).toBe(20);
64
+ expect(matches[1].name).toBe("AppWeb.CoreComponents.header");
65
+ expect(matches[1].line).toBe(123);
66
+ });
67
+ it("stops at non-comment element node", () => {
68
+ container.innerHTML = `
69
+ <div>Other element</div>
70
+ <!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
71
+ <header>Content</header>
72
+ `;
73
+ const header = container.querySelector("header");
74
+ const matches = findPrecedingPhoenixComments(header);
75
+
76
+ // Should only find the comment between the div and header
77
+ expect(matches).toHaveLength(1);
78
+ expect(matches[0].name).toBe("AppWeb.CoreComponents.header");
79
+ });
80
+ it("skips whitespace text nodes", () => {
81
+ container.innerHTML = `
82
+ <!-- @caller lib/app_web/home_live.ex:20 -->
83
+
84
+ <header>Content</header>
85
+ `;
86
+ const header = container.querySelector("header");
87
+ const matches = findPrecedingPhoenixComments(header);
88
+
89
+ // Should find the comment despite whitespace text node
90
+ expect(matches).toHaveLength(1);
91
+ expect(matches[0].name).toBe("@caller");
92
+ });
93
+ it("stops at non-whitespace text node", () => {
94
+ container.innerHTML = `
95
+ <!-- @caller lib/app_web/home_live.ex:20 -->
96
+ Some text
97
+ <header>Content</header>
98
+ `;
99
+ const header = container.querySelector("header");
100
+ const matches = findPrecedingPhoenixComments(header);
101
+
102
+ // Should stop at the text node, not finding the comment
103
+ expect(matches).toHaveLength(0);
104
+ });
105
+ it("returns empty array if no preceding comments", () => {
106
+ container.innerHTML = `<header>Content</header>`;
107
+ const header = container.querySelector("header");
108
+ const matches = findPrecedingPhoenixComments(header);
109
+ expect(matches).toHaveLength(0);
110
+ });
111
+ it("ignores non-Phoenix comments", () => {
112
+ container.innerHTML = `
113
+ <!-- Regular HTML comment -->
114
+ <header>Content</header>
115
+ `;
116
+ const header = container.querySelector("header");
117
+ const matches = findPrecedingPhoenixComments(header);
118
+ expect(matches).toHaveLength(0);
119
+ });
120
+ it("finds Phoenix comments and ignores non-Phoenix comments", () => {
121
+ container.innerHTML = `
122
+ <!-- Regular comment -->
123
+ <!-- @caller lib/app_web/home_live.ex:20 -->
124
+ <!-- Another regular comment -->
125
+ <!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
126
+ <header>Content</header>
127
+ `;
128
+ const header = container.querySelector("header");
129
+ const matches = findPrecedingPhoenixComments(header);
130
+
131
+ // Should only find the 2 Phoenix comments
132
+ expect(matches).toHaveLength(2);
133
+ expect(matches[0].name).toBe("@caller");
134
+ expect(matches[1].name).toBe("AppWeb.CoreComponents.header");
135
+ });
136
+ });
137
+ describe("phoenixMatchesToServerComponents", () => {
138
+ it("converts matches to ServerComponentInfo format", () => {
139
+ container.innerHTML = `
140
+ <!-- @caller lib/app_web/home_live.ex:20 -->
141
+ <!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->
142
+ <button>Click</button>
143
+ `;
144
+ const button = container.querySelector("button");
145
+ const matches = findPrecedingPhoenixComments(button);
146
+ const serverComponents = phoenixMatchesToServerComponents(matches);
147
+ expect(serverComponents).toHaveLength(2);
148
+ expect(serverComponents[0]).toEqual({
149
+ name: "@caller",
150
+ filePath: "lib/app_web/home_live.ex",
151
+ line: 20,
152
+ type: "caller"
153
+ });
154
+ expect(serverComponents[1]).toEqual({
155
+ name: "AppWeb.CoreComponents.button",
156
+ filePath: "lib/app_web/core_components.ex",
157
+ line: 456,
158
+ type: "component"
159
+ });
160
+ });
161
+ });
162
+ describe("parsePhoenixServerComponents", () => {
163
+ it("returns ServerComponentInfo array when comments found", () => {
164
+ container.innerHTML = `
165
+ <!-- @caller lib/app_web/home_live.ex:48 -->
166
+ <!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->
167
+ <button data-phx-loc="458">Click Me</button>
168
+ `;
169
+ const button = container.querySelector("button");
170
+ const result = parsePhoenixServerComponents(button);
171
+ expect(result).not.toBeNull();
172
+ expect(result).toHaveLength(2);
173
+ expect(result[0]).toEqual({
174
+ name: "@caller",
175
+ filePath: "lib/app_web/home_live.ex",
176
+ line: 48,
177
+ type: "caller"
178
+ });
179
+ expect(result[1]).toEqual({
180
+ name: "AppWeb.CoreComponents.button",
181
+ filePath: "lib/app_web/core_components.ex",
182
+ line: 456,
183
+ type: "component"
184
+ });
185
+ });
186
+ it("returns null when no comments found", () => {
187
+ container.innerHTML = `<button>Click Me</button>`;
188
+ const button = container.querySelector("button");
189
+ const result = parsePhoenixServerComponents(button);
190
+ expect(result).toBeNull();
191
+ });
192
+ it("handles nested structure with multiple components", () => {
193
+ container.innerHTML = `
194
+ <!-- @caller lib/app_web/home_live.ex:20 -->
195
+ <!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
196
+ <header data-phx-loc="125" class="p-5">
197
+ <!-- @caller lib/app_web/home_live.ex:48 -->
198
+ <!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->
199
+ <button data-phx-loc="458" class="px-2">Click</button>
200
+ </header>
201
+ `;
202
+ const header = container.querySelector("header");
203
+ const headerResult = parsePhoenixServerComponents(header);
204
+ expect(headerResult).not.toBeNull();
205
+ expect(headerResult).toHaveLength(2);
206
+ expect(headerResult[0].name).toBe("@caller");
207
+ expect(headerResult[0].line).toBe(20);
208
+ expect(headerResult[1].name).toBe("AppWeb.CoreComponents.header");
209
+ const button = container.querySelector("button");
210
+ const buttonResult = parsePhoenixServerComponents(button);
211
+ expect(buttonResult).not.toBeNull();
212
+ expect(buttonResult).toHaveLength(2);
213
+ expect(buttonResult[0].name).toBe("@caller");
214
+ expect(buttonResult[0].line).toBe(48);
215
+ expect(buttonResult[1].name).toBe("AppWeb.CoreComponents.button");
216
+ });
217
+ });
218
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Detect if Phoenix LiveView is present on the page.
3
+ *
4
+ * Checks for multiple signals:
5
+ * 1. window.liveSocket - Phoenix LiveView JS client
6
+ * 2. data-phx-* attributes in the DOM
7
+ * 3. Phoenix debug comment patterns
8
+ *
9
+ * Returns true if any signal indicates Phoenix LiveView is running.
10
+ */
11
+ export declare function detectPhoenix(): boolean;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Detect if Phoenix LiveView is present on the page.
3
+ *
4
+ * Checks for multiple signals:
5
+ * 1. window.liveSocket - Phoenix LiveView JS client
6
+ * 2. data-phx-* attributes in the DOM
7
+ * 3. Phoenix debug comment patterns
8
+ *
9
+ * Returns true if any signal indicates Phoenix LiveView is running.
10
+ */
11
+ export function detectPhoenix() {
12
+ // Check 1: Look for LiveView socket (most reliable)
13
+ if (typeof window !== "undefined" && window.liveSocket) {
14
+ return true;
15
+ }
16
+
17
+ // Check 2: Look for Phoenix data attributes in the DOM
18
+ if (typeof document !== "undefined") {
19
+ // Phoenix LiveView adds data-phx-main or data-phx-session to the main LiveView container
20
+ if (document.querySelector("[data-phx-main], [data-phx-session]")) {
21
+ return true;
22
+ }
23
+
24
+ // Check 3: Quick scan for Phoenix debug comments (first 50 comments)
25
+ // This is useful when debug_heex_annotations is enabled
26
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT, null);
27
+ let count = 0;
28
+ while (walker.nextNode() && count < 50) {
29
+ const text = walker.currentNode.textContent;
30
+ // Look for Phoenix-specific comment patterns
31
+ if (text?.includes("@caller") || text?.includes("<App")) {
32
+ return true;
33
+ }
34
+ count++;
35
+ }
36
+ }
37
+ return false;
38
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Phoenix LiveView adapter for TreeLocatorJS.
3
+ *
4
+ * Parses server-side component information from Phoenix LiveView debug annotations.
5
+ * Requires Phoenix LiveView v1.1+ with debug_heex_annotations: true in config.
6
+ */
7
+ export { parsePhoenixServerComponents, findPrecedingPhoenixComments } from "./parsePhoenixComments";
8
+ export { detectPhoenix } from "./detectPhoenix";
9
+ export type { ServerComponentInfo } from "../../types/ServerComponentInfo";
10
+ export type { PhoenixCommentMatch } from "./types";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Phoenix LiveView adapter for TreeLocatorJS.
3
+ *
4
+ * Parses server-side component information from Phoenix LiveView debug annotations.
5
+ * Requires Phoenix LiveView v1.1+ with debug_heex_annotations: true in config.
6
+ */
7
+
8
+ export { parsePhoenixServerComponents, findPrecedingPhoenixComments } from "./parsePhoenixComments";
9
+ export { detectPhoenix } from "./detectPhoenix";
@@ -0,0 +1,35 @@
1
+ import { ServerComponentInfo } from "../../types/ServerComponentInfo";
2
+ import { PhoenixCommentMatch } from "./types";
3
+ /**
4
+ * Find all Phoenix comment annotations immediately preceding an element.
5
+ * Walks backward through previous siblings until hitting a non-comment node.
6
+ *
7
+ * Returns array ordered from outermost to innermost (matching Phoenix nesting order).
8
+ * Example: [@caller, CoreComponents.button] where @caller is outermost.
9
+ *
10
+ * Example DOM structure:
11
+ * ```html
12
+ * <!-- @caller lib/app_web/home_live.ex:48 -->
13
+ * <!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->
14
+ * <button>Click Me</button>
15
+ * ```
16
+ *
17
+ * Would return: [
18
+ * { name: "@caller", filePath: "lib/app_web/home_live.ex", line: 48, type: "caller" },
19
+ * { name: "AppWeb.CoreComponents.button", filePath: "lib/app_web/core_components.ex", line: 456, type: "component" }
20
+ * ]
21
+ */
22
+ export declare function findPrecedingPhoenixComments(element: Element): PhoenixCommentMatch[];
23
+ /**
24
+ * Convert PhoenixCommentMatch[] to ServerComponentInfo[].
25
+ * Filters and transforms matches into the format expected by AncestryItem.
26
+ */
27
+ export declare function phoenixMatchesToServerComponents(matches: PhoenixCommentMatch[]): ServerComponentInfo[];
28
+ /**
29
+ * Main entry point: extract server component info from element.
30
+ * Returns null if no Phoenix annotations found.
31
+ *
32
+ * This function is called during ancestry collection to enrich each AncestryItem
33
+ * with server-side component information.
34
+ */
35
+ export declare function parsePhoenixServerComponents(element: Element): ServerComponentInfo[] | null;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Regex patterns for Phoenix LiveView debug annotations.
3
+ *
4
+ * These comments are added by Phoenix LiveView when configured with:
5
+ * config :phoenix_live_view, debug_heex_annotations: true
6
+ *
7
+ * Pattern 1 (Caller): <!-- @caller lib/app_web/home_live.ex:20 -->
8
+ * Pattern 2 (Component): <!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
9
+ * Pattern 3 (Closing): <!-- </AppWeb.CoreComponents.header> -->
10
+ */
11
+ const PHOENIX_CALLER_PATTERN = /^@caller\s+(.+):(\d+)$/;
12
+ const PHOENIX_COMPONENT_PATTERN = /^<([^>]+)>\s+(.+):(\d+)$/;
13
+ const PHOENIX_CLOSING_PATTERN = /^<\/([^>]+)>$/;
14
+
15
+ /**
16
+ * Parse a single HTML comment node for Phoenix debug annotations.
17
+ * Returns null if the comment doesn't match Phoenix patterns.
18
+ */
19
+ function parseCommentNode(commentNode) {
20
+ const text = commentNode.textContent?.trim();
21
+ if (!text) return null;
22
+
23
+ // Check for @caller pattern: <!-- @caller lib/app_web/home_live.ex:20 -->
24
+ const callerMatch = text.match(PHOENIX_CALLER_PATTERN);
25
+ if (callerMatch && callerMatch[1] && callerMatch[2]) {
26
+ return {
27
+ commentNode,
28
+ name: "@caller",
29
+ filePath: callerMatch[1],
30
+ line: parseInt(callerMatch[2], 10),
31
+ type: "caller"
32
+ };
33
+ }
34
+
35
+ // Check for component opening pattern: <!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
36
+ const componentMatch = text.match(PHOENIX_COMPONENT_PATTERN);
37
+ if (componentMatch && componentMatch[1] && componentMatch[2] && componentMatch[3]) {
38
+ return {
39
+ commentNode,
40
+ name: componentMatch[1],
41
+ filePath: componentMatch[2],
42
+ line: parseInt(componentMatch[3], 10),
43
+ type: "component"
44
+ };
45
+ }
46
+
47
+ // Closing tags are ignored (we only care about opening tags)
48
+ // Example: <!-- </AppWeb.CoreComponents.header> -->
49
+ if (text.match(PHOENIX_CLOSING_PATTERN)) {
50
+ return null;
51
+ }
52
+
53
+ // Not a Phoenix comment
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Find all Phoenix comment annotations immediately preceding an element.
59
+ * Walks backward through previous siblings until hitting a non-comment node.
60
+ *
61
+ * Returns array ordered from outermost to innermost (matching Phoenix nesting order).
62
+ * Example: [@caller, CoreComponents.button] where @caller is outermost.
63
+ *
64
+ * Example DOM structure:
65
+ * ```html
66
+ * <!-- @caller lib/app_web/home_live.ex:48 -->
67
+ * <!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->
68
+ * <button>Click Me</button>
69
+ * ```
70
+ *
71
+ * Would return: [
72
+ * { name: "@caller", filePath: "lib/app_web/home_live.ex", line: 48, type: "caller" },
73
+ * { name: "AppWeb.CoreComponents.button", filePath: "lib/app_web/core_components.ex", line: 456, type: "component" }
74
+ * ]
75
+ */
76
+ export function findPrecedingPhoenixComments(element) {
77
+ const matches = [];
78
+ let node = element.previousSibling;
79
+
80
+ // Walk backward through siblings, collecting comment nodes
81
+ while (node) {
82
+ if (node.nodeType === Node.COMMENT_NODE) {
83
+ const match = parseCommentNode(node);
84
+ if (match) {
85
+ matches.push(match);
86
+ }
87
+ // Continue even if this comment didn't match - keep looking for more Phoenix comments
88
+ } else if (node.nodeType === Node.TEXT_NODE) {
89
+ // Skip whitespace text nodes
90
+ const text = node.textContent?.trim();
91
+ if (text && text.length > 0) {
92
+ // Hit non-whitespace text, stop searching
93
+ break;
94
+ }
95
+ } else {
96
+ // Hit another element, stop searching
97
+ break;
98
+ }
99
+ node = node.previousSibling;
100
+ }
101
+
102
+ // Reverse so outermost comes first (matches Phoenix nesting order)
103
+ // This makes the array order match the visual hierarchy: [@caller, Component]
104
+ return matches.reverse();
105
+ }
106
+
107
+ /**
108
+ * Convert PhoenixCommentMatch[] to ServerComponentInfo[].
109
+ * Filters and transforms matches into the format expected by AncestryItem.
110
+ */
111
+ export function phoenixMatchesToServerComponents(matches) {
112
+ return matches.map(match => ({
113
+ name: match.name,
114
+ filePath: match.filePath,
115
+ line: match.line,
116
+ type: match.type
117
+ }));
118
+ }
119
+
120
+ /**
121
+ * Main entry point: extract server component info from element.
122
+ * Returns null if no Phoenix annotations found.
123
+ *
124
+ * This function is called during ancestry collection to enrich each AncestryItem
125
+ * with server-side component information.
126
+ */
127
+ export function parsePhoenixServerComponents(element) {
128
+ const matches = findPrecedingPhoenixComments(element);
129
+ if (matches.length === 0) return null;
130
+ return phoenixMatchesToServerComponents(matches);
131
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Represents a parsed Phoenix LiveView debug annotation HTML comment.
3
+ * Phoenix LiveView adds these comments when debug_heex_annotations: true is configured.
4
+ */
5
+ export interface PhoenixCommentMatch {
6
+ /** The actual HTML Comment node from the DOM */
7
+ commentNode: Comment;
8
+ /** Component name (e.g., "AppWeb.CoreComponents.button") or "@caller" */
9
+ name: string;
10
+ /** File path (e.g., "lib/app_web/core_components.ex") */
11
+ filePath: string;
12
+ /** Line number */
13
+ line: number;
14
+ /** Type of annotation */
15
+ type: "component" | "caller";
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,11 @@
1
1
  import { getUsableName } from "../../functions/getUsableName";
2
+ import { normalizeFilePath } from "../../functions/normalizeFilePath";
2
3
  export function getFiberLabel(fiber, source) {
3
4
  const name = getUsableName(fiber);
4
5
  const label = {
5
6
  label: name,
6
7
  link: source ? {
7
- filePath: source.fileName,
8
+ filePath: normalizeFilePath(source.fileName),
8
9
  projectPath: "",
9
10
  line: source.lineNumber,
10
11
  column: source.columnNumber || 0
@@ -1,14 +1,35 @@
1
1
  import { template as _$template } from "solid-js/web";
2
2
  import { effect as _$effect } from "solid-js/web";
3
+ import { insert as _$insert } from "solid-js/web";
3
4
  import { setStyleProperty as _$setStyleProperty } from "solid-js/web";
4
5
  import { createComponent as _$createComponent } from "solid-js/web";
5
6
  import { memo as _$memo } from "solid-js/web";
6
- var _tmpl$ = /*#__PURE__*/_$template(`<div><div class="fixed rounded border border-solid border-amber-500"style=z-index:2></div><div class="fixed text-xs font-medium rounded-md"style="z-index:3;box-shadow:0 4px 16px rgba(0, 0, 0, 0.2);font-family:ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace;letter-spacing:0.01em">No source found`);
7
+ var _tmpl$ = /*#__PURE__*/_$template(`<div><div class="fixed rounded border border-solid border-amber-500"style=z-index:2></div><div class="fixed text-xs font-medium rounded-md"style="z-index:3;box-shadow:0 4px 16px rgba(0, 0, 0, 0.2);font-family:ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace;letter-spacing:0.01em">`),
8
+ _tmpl$2 = /*#__PURE__*/_$template(`<div><div class="fixed rounded border border-solid border-gray-500"style=z-index:2></div><div class="fixed text-xs font-medium rounded-md"style="z-index:3;box-shadow:0 4px 16px rgba(0, 0, 0, 0.2);font-family:ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace;letter-spacing:0.01em">`);
7
9
  import { createMemo } from "solid-js";
8
10
  import { getElementInfo } from "../adapters/getElementInfo";
9
11
  import { Outline } from "./Outline";
12
+ import { parsePhoenixServerComponents } from "../adapters/phoenix/parsePhoenixComments";
10
13
  export function MaybeOutline(props) {
11
14
  const elInfo = createMemo(() => getElementInfo(props.currentElement, props.adapterId));
15
+
16
+ // Check for Phoenix server components when client framework data is not available
17
+ const phoenixInfo = createMemo(() => {
18
+ if (elInfo()) return null; // Client framework takes precedence
19
+
20
+ // Try current element first
21
+ let serverComponents = parsePhoenixServerComponents(props.currentElement);
22
+ if (serverComponents) return serverComponents;
23
+
24
+ // Walk up the tree to find the nearest parent with Phoenix components
25
+ let parent = props.currentElement.parentElement;
26
+ while (parent && parent !== document.body) {
27
+ serverComponents = parsePhoenixServerComponents(parent);
28
+ if (serverComponents) return serverComponents;
29
+ parent = parent.parentElement;
30
+ }
31
+ return null;
32
+ });
12
33
  const box = () => props.currentElement.getBoundingClientRect();
13
34
  return _$memo(() => _$memo(() => !!elInfo())() ? _$createComponent(Outline, {
14
35
  get element() {
@@ -17,14 +38,15 @@ export function MaybeOutline(props) {
17
38
  get targets() {
18
39
  return props.targets;
19
40
  }
20
- }) : (() => {
41
+ }) : _$memo(() => !!phoenixInfo())() ? (() => {
21
42
  var _el$ = _tmpl$(),
22
43
  _el$2 = _el$.firstChild,
23
44
  _el$3 = _el$2.nextSibling;
24
45
  _$setStyleProperty(_el$3, "padding", "4px 10px");
25
- _$setStyleProperty(_el$3, "background", "rgba(120, 53, 15, 0.85)");
46
+ _$setStyleProperty(_el$3, "background", "rgba(79, 70, 229, 0.85)");
26
47
  _$setStyleProperty(_el$3, "color", "#fff");
27
48
  _$setStyleProperty(_el$3, "border", "1px solid rgba(255, 255, 255, 0.15)");
49
+ _$insert(_el$3, () => phoenixInfo().filter(sc => sc.type === "component").map(sc => sc.name.split(".").pop()).join(" > "));
28
50
  _$effect(_p$ => {
29
51
  var _v$ = box().x + "px",
30
52
  _v$2 = box().y + "px",
@@ -48,5 +70,45 @@ export function MaybeOutline(props) {
48
70
  n: undefined
49
71
  });
50
72
  return _el$;
73
+ })() : (() => {
74
+ var _el$4 = _tmpl$2(),
75
+ _el$5 = _el$4.firstChild,
76
+ _el$6 = _el$5.nextSibling;
77
+ _$setStyleProperty(_el$6, "padding", "4px 10px");
78
+ _$setStyleProperty(_el$6, "background", "rgba(75, 85, 99, 0.85)");
79
+ _$setStyleProperty(_el$6, "color", "#fff");
80
+ _$setStyleProperty(_el$6, "border", "1px solid rgba(255, 255, 255, 0.15)");
81
+ _$insert(_el$6, () => props.currentElement.tagName.toLowerCase(), null);
82
+ _$insert(_el$6, (() => {
83
+ var _c$ = _$memo(() => !!props.currentElement.id);
84
+ return () => _c$() ? `#${props.currentElement.id}` : "";
85
+ })(), null);
86
+ _$insert(_el$6, (() => {
87
+ var _c$2 = _$memo(() => !!props.currentElement.className);
88
+ return () => _c$2() ? `.${props.currentElement.className.split(" ")[0]}` : "";
89
+ })(), null);
90
+ _$effect(_p$ => {
91
+ var _v$7 = box().x + "px",
92
+ _v$8 = box().y + "px",
93
+ _v$9 = box().width + "px",
94
+ _v$0 = box().height + "px",
95
+ _v$1 = box().x + 4 + "px",
96
+ _v$10 = box().y + 4 + "px";
97
+ _v$7 !== _p$.e && _$setStyleProperty(_el$5, "left", _p$.e = _v$7);
98
+ _v$8 !== _p$.t && _$setStyleProperty(_el$5, "top", _p$.t = _v$8);
99
+ _v$9 !== _p$.a && _$setStyleProperty(_el$5, "width", _p$.a = _v$9);
100
+ _v$0 !== _p$.o && _$setStyleProperty(_el$5, "height", _p$.o = _v$0);
101
+ _v$1 !== _p$.i && _$setStyleProperty(_el$6, "left", _p$.i = _v$1);
102
+ _v$10 !== _p$.n && _$setStyleProperty(_el$6, "top", _p$.n = _v$10);
103
+ return _p$;
104
+ }, {
105
+ e: undefined,
106
+ t: undefined,
107
+ a: undefined,
108
+ o: undefined,
109
+ i: undefined,
110
+ n: undefined
111
+ });
112
+ return _el$4;
51
113
  })());
52
114
  }
@@ -64,7 +64,7 @@ function Runtime(props) {
64
64
  if (!element) {
65
65
  const target = e.target;
66
66
  if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
67
- element = target instanceof SVGElement ? target.closest('svg') ?? target : target;
67
+ element = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
68
68
  }
69
69
  }
70
70
  if (element && !isLocatorsOwnElement(element)) {
@@ -1,4 +1,5 @@
1
1
  import { TreeNode } from "../types/TreeNode";
2
+ import { ServerComponentInfo } from "../types/ServerComponentInfo";
2
3
  export interface OwnerComponentInfo {
3
4
  name: string;
4
5
  filePath?: string;
@@ -13,6 +14,8 @@ export interface AncestryItem {
13
14
  nthChild?: number;
14
15
  /** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
15
16
  ownerComponents?: OwnerComponentInfo[];
17
+ /** Server-side components (Phoenix LiveView, Rails, Next.js RSC, etc.) */
18
+ serverComponents?: ServerComponentInfo[];
16
19
  }
17
20
  export declare function collectAncestry(node: TreeNode): AncestryItem[];
18
21
  export declare function formatAncestryChain(items: AncestryItem[]): string;